POST /exchange request must include a valid EIP-712 structured-data signature (secp256k1 ECDSA) over the action payload. The server recovers your Ethereum address from the signature at request time — there are no API keys, bearer tokens, or sessions to manage. If the recovered address belongs to a registered account (or to an API wallet approved by one), the operation proceeds. If the signature cannot be recovered, or recovers an address other than the one the action requires, the server returns 401 SIGNATURE_INVALID. If the signature is valid but the recovered address has no registered account and is not an approved API wallet, the server returns 400 BAD_REQUEST with the message signer has no registered account.
Domain
All signatures use this EIP-712 domain. OnlychainId differs between environments:
Two Signing Paths
The signing struct is chosen byaction.type:
Agent path
Trading and programmatic actions —
order, cancel, cancelByCloid, modify, and most others. The entire canonical action is folded into a single actionHash carried by a generic Agent(string source, bytes32 actionHash) struct.Typed path
Funds and permission actions —
registerAccount, approveAgent, revokeAgent, lockCollateral, unlockCollateral, transferBetweenDeployers. Each has a field-level EIP-712 struct so a wallet can render human-readable values for review.Both paths share the same domain, digest formula, and request envelope. Only the
hashStruct differs. If an action type is not in the typed list above, use the Agent path.Agent Path
Used for every action except the six typed-path ones — all trading and programmatic calls, such asorder, cancel, and modify, plus the margin, leverage, TP/SL, and share-group actions. As a rule of thumb: if action.type isn’t in the typed list below, sign it via the Agent path.
1
Build the action object
Construct the action as a plain JSON object. Every action has a
type field that identifies the operation.2
Serialize to canonical JSON
Serialize the action to canonical JSON: sort keys alphabetically, use compact separators (Key ordering is crucial. The server re-serializes the action using the same rules; any mismatch produces a different hash and
, and :) with no whitespace. Fields whose value is null are not removed — they serialize as the literal null. Do not strip them on the client; the simplest approach is to leave null-valued fields out of the action altogether. In Python this is exactly json.dumps(action, sort_keys=True, separators=(",", ":")).SIGNATURE_INVALID.3
Compute the actionHash
Concatenate the canonical JSON bytes with the nonce encoded as a big-endian 8-byte value, then keccak256:
4
Build the hashStruct
Wrap the
actionHash in the generic Agent struct with source = "b":5
Compute the EIP-712 digest and sign
See Digest & signature below — this step is identical for both paths.
Typed Path
Used forregisterAccount, approveAgent, revokeAgent, lockCollateral, unlockCollateral, and transferBetweenDeployers. Each action encodes its fields directly into a struct — there is no canonical JSON step.
The hashStruct is keccak256( typeHash(<Struct>) ‖ enc(field1) ‖ … ‖ enc(nonce) ), where each field is encoded as:
Struct definitions (the
nonce is always the final uint64 field):
Digest & Signature
Both paths finish identically. Build the domain separator, form the EIP-712 digest, and sign it directly.1
Domain separator
chainId is 9767 on Devnet and 9788 on Mainnet, and typeHash(EIP712Domain) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)").2
EIP-712 digest
3
Sign the digest
Sign the 32-byte digest with secp256k1 (RFC 6979 deterministic
k). Set v = 27 + recovery_bit.Signature Format
Include the signature in thesignature field of the request envelope:
r and s are 32-byte values as 0x-prefixed lowercase hex strings (66 characters including the prefix). v is 27 or 28.
Request Envelope
The fullPOST /exchange body is the same for both paths:
vaultAddress is an optional top-level field for vault-proxy operations only (not currently enabled). Delegated agent trading does not use it — an agent signs with its own key, and the server routes to the master account from the recovered signer address, so trade actions ignore vaultAddress entirely.Signing path by action
EveryPOST /exchange action uses one of two EIP-712 paths. The path is fixed per action.type — there is nothing to negotiate and nothing to infer.
The typed path covers funds movement and permission changes; the Agent path covers trading. An approved agent wallet can sign Agent-path actions for its master, but not the typed ones.
Test vectors
Verify your signer against these before sending anything. All were produced by the reference implementation and confirmed against the live API — the server recovered the signing address from each.name="Exchange", version="1", chainId=9767, verifyingContract=0x0000000000000000000000000000000000000000. If your domain separator differs, stop — nothing below will match.
Typed path
- registerAccount
- revokeAgent
- lockCollateral
Agent path
The canonical JSON is shown because it is the exact byte string that is hashed — keys sorted, no whitespace. If your canonical form differs by even one byte, your digest will not match.- order
- cancelAll
Reproduce the digest first — it needs no private key and isolates every encoding decision. Only once digests match should you compare signatures.
Browser wallet signing
The typed path builds its digest over the environment’schainId — 9767 on Devnet, 9788 on Mainnet. Signing with a raw private key — an SDK, eth_account, or a viem local account — needs nothing extra: keep the default.
Browser extension wallets are different. When signing through eth_signTypedData_v4, they require domain.chainId to equal the wallet’s currently active chain and refuse to sign otherwise. To support them, send the chain ID you actually signed with as an optional top-level envelope field:
string
signatureChainId — Hex string with a 0x prefix — the domain.chainId your client used to sign the typed struct. Defaults to the environment’s chain ID when omitted: 0x2627 (9767) on Devnet, 0x263c (9788) on Mainnet.- Typed path only. The Agent path —
order,cancel, and the rest — always uses the environment’s chain ID and ignores this field. - Not part of the signature. It sits at the top level beside
action,signature, andnonce, and never enters the canonical JSON. The server rebuilds the domain with it before verifying; a value that disagrees with what you actually signed recovers the wrong address and returns401 SIGNATURE_INVALID. - Omitting it makes the server rebuild the domain with the environment’s chain ID.
- A malformed value — missing the
0xprefix, containing non-hex characters, or longer than 64 bits — returns400 BAD_REQUEST.
Complete Signing Helper
A reusable helper that implements both paths. This is the reference implementation — copy, paste, and run.Common Errors
When you receive
SIGNATURE_INVALID, recompute the digest locally against the test vectors and compare the address your signer recovers with the address you expect; a mismatch means the request was signed with a different key or a different digest.