Why sign actions?
Every state-changing action — placing an order, transferring spot, withdrawing, creating a session key — carries its own EIP-712 signature over anAction struct. This ensures the protocol is self-custodial.
Action signing is not session login.
Login (EIP-191) authenticates a connection; it does not authorize actions.
Each action is signed and verified on its own. Keep the two layers distinct.
Example Order Signed Action
Example Order Signed Action
data words are hashed into the Action struct, which is hashed
with the domain separator into the digest signer signs.Worked example: a buy order
An option buy order signed by the wallet directly (owner == signer). The SDK
ABI-encodes the payload, builds and signs the EIP-712 digest, and submits it in a
single call — amounts and prices are plain decimals, with e18 scaling handled
internally. The last tab does the same thing without an SDK, one step at a time:
The instrument name above is an option, named
<CURRENCY>-<YYYYMMDD>-<STRIKE>-<C|P> (perps are <CURRENCY>-PERP).
See Instrument names for the full grammar.Nonce and expiry
uint256
The
nonce param (decoded as a UTC timestamp in nanoseconds * 6-digit suffix) has some special rules depending on the action.uint256
The Action’s
expiry, in unix seconds; rejected once now > expiry. The exchange enforces certain minimums depending on the action type to ensure the action is valid long enough.Debugging signature issues
A rejected signature is almost always one of three things:- Bad action data encoding — your ABI-encoded
encoded_databytes are wrong. Check them withpublic/decode_action. - Bad action envelope encoding — the action data is fine, but something the
signature also commits to (module, nonce, expiry, owner, signer, domain
separator) differs from what the exchange rebuilt. Byte-compare each stage
against the relevant
private/*_debugroutes (e.g.private/order_debug). - Wallet issues — the encoding is right on both counts, but the wrong key
signed, or
signerdoes not match the recovered address. This is often because you passed in theownerinstead of thesigner.
Constants
ACTION_TYPEHASH
The EIP-712 type hash of the Action struct. It is the same on every network.
data replaced by its keccak:
Domain separator
The action domain is a standard four-fieldEIP712Domain with no salt. Only chainId differs
between networks.
The
signer signs the digest keccak256(0x1901 || DOMAIN_SEPARATOR || actionHash).
Every private/*_debug route returns the domain_separator and action_typehash the exchange
verifies against, e.g. private/order_debug.
Compare yours against them when a signature is rejected.
Modules
Themodule field names the contract whose ABI layout data follows. Module
addresses are fixed protocol constants — identical across every deployment, safe
to hardcode:
owner vs signer
owner is always the wallet that owns the subaccount. signer is whoever
produced the signature — the wallet itself (owner == signer) or a delegated
session key, in which case signer is the key’s address and
owner stays the wallet. The protocol recovers the ECDSA signer and requires it
to equal signer; if signer != owner, it loads the session key and checks its
scopes cover the action.