Skip to main content
Every state-changing action — placing an order, transferring spot, withdrawing, creating a session key — carries its own EIP-712 signature over an Action struct, re-verified independently by the protocol. This allows the protocol to be low-latency and self-custodial at the same time.
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 Action struct

Every action is wrapped in the same 7-field envelope. This is the EIP-712 typed structure that gets signed:

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.

Building the signature

1

ABI-encode the action data

Encode the module-specific payload into data (layouts below).
2

Compute the struct hash

data is hashed, then the whole struct is ABI-encoded with the type hash:
ACTION_TYPEHASH is keccak256 of the struct signature above and is invariant across every deployment:
3

Compute the EIP-712 digest

Prefix with 0x1901 and the deployment’s domain separator:
The domain is the Matching contract domain — EIP712Domain(name="Matching", version="1.0", chainId, verifyingContract). The verifyingContract is the Matching contract, constant across every deployment:
Only chainId varies per deployment, so the resulting domainSeparator is deployment-specific and must match the deployment’s on-chain ActionEip712Params:The per-action module addresses, by contrast, are fixed protocol constants (identical on every deployment) — see the table below.
4

Sign the digest

Sign the 32-byte digest directly with the signing key, producing a 65-byte r || s || v signature. Submit it with the request’s nonce, signer, and signature fields.
The testnet domain separator is 0x24d674cd5f2b9d564691c51e9d88f649b99246a2244dd74ce27b96578d773e85. Do not treat this as universal — mainnet and other deployments differ. The mainnet separator is not published here; read it from the deployment’s on-chain ActionEip712Params, or via the *_debug helpers below. Always verify against your target.

Per-action modules and data

The module field names the contract whose ABI layout data follows. Module addresses are fixed protocol constants — identical across every deployment, safe to hardcode:
The module is a field inside the Action struct, not the domain’s verifyingContract. The verifyingContract is always the single Matching contract; module selects the per-action payload contract. Signing against the wrong module yields a valid-looking but rejected signature.

Amounts are e18 on the wire

Numeric amounts, prices, and fees are e18 fixed-point in the ABI-encoded data — i.e. signers sign parseUnits(x, 18). Withdrawal’s underlying amount is the exception: it uses the asset’s native ERC-20 decimals and is never scaled. The exact ABI word layout for each module’s data is defined by the SDK codecs — use them as the source of truth rather than hand-encoding the payload: See Transfers & withdrawals for the full request flows.

Nonce and expiry

nonce
uint256
A UTC timestamp in nanoseconds (~19 digits, e.g. 1751558400000000123). A common convention is now_ms × 1_000_000 + random 6-digit suffix, producing a time-ordered value; the suffix keeps concurrent actions unique within the same millisecond. A nanosecond timestamp exceeds JavaScript’s safe-integer range, so build it with BigInt and submit it as a string (the API accepts a string or a number). For non-trade signed actions the nonce must be strictly increasing per subaccount (monotonic anti-replay); trades and RFQs are tracked per (owner, nonce), so partial fills accumulate against a single nonce and need not increase.
The nonce must also fall inside a validity window, which differs by action type: Windows are environment-tunable; the server also applies a coarser ± 5-minute pre-check before the action reaches the protocol.
expiry
uint256
The Action’s expiry, in unix seconds; rejected once now > expiry. It must also sit at least the deployment’s minimum signature-validity window in the future (MIN_ORDER_SIGNATURE_VALIDITY_SEC, deployment-specific), or the order is rejected with 11011 order_invalid_signature_expiry — leave a comfortable margin (e.g. orders now + 3600, create_session_key now + 600).

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: Whichever transport you use, this is the private/order request the SDK puts on the wire:
The instrument name above is an option, named <CURRENCY>-<YYYYMMDD>-<STRIKE>-<C|P> (perps are <CURRENCY>-PERP). See Instrument names for the full grammar.

Verify your bytes

If a signature is rejected, use the signing-preview helpers to byte-compare each stage (encoded data, hashes, digest) against what the server computes. Each returns the encoded_data, action_hash, and typed_data_hash of the rebuilt action. These are a debugging aid, not a required step:
  • private/order_debug (orders) — takes the same params as private/order; needs a logged-in session
  • public/withdraw_debug
  • public/send_quote_debug and public/execute_quote_debug

Session keys

Delegate signing to a session key and manage its scopes.

Transfers & withdrawals

Full flows for the transfer and withdraw modules.

Contracts

On-chain contract addresses per deployment (action manager, vApp, outbox, spot vault).

Authentication

The session-login layer (EIP-191).