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 The domain is the Matching contract domain —
Only
0x1901 and the deployment’s domain separator:EIP712Domain(name="Matching", version="1.0", chainId, verifyingContract). The
verifyingContract is the Matching contract, constant across every deployment: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
Themodule field names the contract whose ABI layout data follows. Module
addresses are fixed protocol constants — identical across every deployment, safe
to hardcode:
Amounts are e18 on the wire
Numeric amounts, prices, and fees are e18 fixed-point in the ABI-encodeddata — 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
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.
Windows are environment-tunable; the server also applies a coarser ± 5-minute pre-check before the action reaches the
protocol.
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 theencoded_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 asprivate/order; needs a logged-in sessionpublic/withdraw_debugpublic/send_quote_debugandpublic/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).
