Skip to main content
The easiest way to begin signing is to either use the SDKs directly. Continue reading for a deep dive into the signing scheme, its parameters, and how to debug signature issues.

Why sign actions?

Every state-changing action — placing an order, transferring spot, withdrawing, creating a session key — carries its own EIP-712 signature over an Action 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.
The seven 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_data bytes are wrong. Check them with public/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/*_debug routes (e.g. private/order_debug).
  • Wallet issues — the encoding is right on both counts, but the wrong key signed, or signer does not match the recovered address. This is often because you passed in the owner instead of the signer.

Constants

ACTION_TYPEHASH

The EIP-712 type hash of the Action struct. It is the same on every network.
An action’s struct hash encodes its fields in that order, with data replaced by its keccak:

Domain separator

The action domain is a standard four-field EIP712Domain with no salt. Only chainId differs between networks.
verifyingContract is the v2 mainnet Matching address, fixed on every network. It is not one of the v3 contracts. Using the ACTION_MANAGER here produces a signature that never verifies.
The signer signs the digest keccak256(0x1901 || DOMAIN_SEPARATOR || actionHash).
The SDKs set both values for each network. You only need these formulas to sign without one.
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

The module 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.