Skip to main content
Every 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 by action.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 order, 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 (, and :) with no whitespace, and exclude undefined/null fields. In Python this is exactly json.dumps(action, sort_keys=True, separators=(",", ":")).
Key ordering is crucial. The server re-serializes the action using the same rules; any mismatch produces a different hash and 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 for registerAccount, 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

where typeHash(EIP712Domain) = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)").
2

EIP-712 digest

Use Ethereum keccak256, not NIST SHA-3. The digest is already the final hash — sign it directly (prehash = false). Never hash it again.
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 the signature field of the request envelope:
Both 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 full POST /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.