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 matches a registered account, the operation proceeds. If it doesn’t, the server returns SIGNATURE_INVALID.
Domain
All signatures use a fixed EIP-712 domain: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 fororder, cancel, cancelByCloid, cancelAll, modify, updateIsolatedMargin, updateLeverage, updateMarginMode, updateFeeSetting, tpSl, cancelTpSl, cancelConditional, enrollUserToMarketDeployer, setMarginShareType, and the share-group transfer actions.
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, and exclude undefined/null fields. 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
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:
An optional
vaultAddress field can be included at the top level of the envelope for vault proxy operations. When present, the server verifies that the signer is an authorized agent of the specified vault.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, the error message includes the recovered address and the expected address, which helps pinpoint which key you actually signed with.