For the complete documentation index, see llms.txt. This page is also available as Markdown.

Transaction Signing

This page specifies the byte-level methodology behind /trade signing, for building your own client in any language. If you use the Python SDK, it signs every action for you — you do not need anything on this page.

signature is not a signature over the JSON text. The write path reconstructs the canonical unsigned transaction payload from action, nonce, agent_epoch, and expires_after_ms, then verifies the recoverable secp256k1 signature over that exact binary payload. For order and modify actions, public JSON price and quantity are display decimals; the signed binary action contains the raw atoms obtained from market price_decimals and base_quantity_decimals. The JSON action and signed binary action must describe the same action.

Canonical unsigned payload:

string("NATIVE_CORE_TX_SIGNING_V1")
u32(1)                  // tx codec version
u32(696969)             // Native Core chain id (mainnet; testnet is 969696)
u64(nonce)
option<u64>(agent_epoch)
option<u64>(expires_after_ms)
action_bytes

The chain id is the target deployment's Native chain id (mainnet: 696969, testnet: 969696). It is folded into the signed bytes but is never sent on the wire, so it must equal the deployment you target — signing with the wrong chain id makes the API recover a different authority and reject the write.

Encoding rules:

  • Integers are unsigned big-endian.

  • string and byte vectors encode as u32 byte_length followed by raw bytes.

  • option<T> encodes as u8(0) for null/omitted, or u8(1) followed by T.

  • Hex values are raw bytes after removing the 0x prefix.

  • The signing digest is keccak256(unsigned_payload_bytes).

  • signature is 0x + 65 bytes encoded as r || s || v; v may be 0/1 or 27/28. High-s signatures are rejected.

The legacy scheme above applies to trading actions. Authorization-sensitive actions use EIP-712 (next section).

EIP-712 signing (auth_scheme: "eip712")

Public withdraw, settle, repay, approveAgent, and revokeAgent must be submitted with auth_scheme: "eip712". (The full cutover set also covers deposit and the operator admin* writes, which are not part of this public trading contract.) This is a direct cutover: the moment the new binary is live, legacy signatures over these actions are rejected (legacy_signature_not_accepted), and there is no config switch, height activation, or grace window — clients must switch at deploy. Conversely, auth_scheme="eip712" on any non-target action (order/cancel/cancelAll/modify/batch) is rejected (eip712_not_allowed_for_action), and an EIP-712 request may not carry agent_epoch (eip712_agent_epoch_not_allowed).

The signature covers an EIP-712 typed-data digest, not a binary payload. Clients sign the v4 scheme, which is MetaMask-compatible: the domain is EIP712Domain{name:"Native Core", version:"1", verifyingContract:0x0000…0000}no chainId — so a wallet can sign while connected to any EVM chain. The Native chain id is instead a signed message field, nativeChainId, so replay separation across environments is preserved. Each target action has its own primary type whose fields mirror the action, prefixed by the common fields uint256 nativeChainId, uint256 authKind, uint256 authScope, uint256 nonce, bool expiresAfterMsPresent, uint256 expiresAfterMs. nativeChainId is the Native Core chain id; authKind is 1 (single) and authScope is 0 for these public user actions. Amounts are signed as canonical atoms; addresses as address; an optional cloid as bool cloidPresent + bytes16 cloid. The presence flags keep an absent value distinct from an explicit 0. The transaction authority is the recovered signer, exactly as for legacy single-signature actions.

A superseded v3 EIP-712 scheme (domain included chainId; no nativeChainId field) is retained only for historical decode/replay and is not accepted at submit. Because /trade carries no codec-version field, a request whose signature was produced under the old v3 scheme is assembled as v4 and recovers a different address, so it fails with a signature/authority error — re-sign with the v4 scheme.

withdraw keeps an optional cloid at the protocol level (legacy WAL records may omit it), but the public API JSON requires cloid; the EIP-712 cloidPresent flag models the optionality.

Public action tags:

JSON action
Canonical tag
Notes

order

0

Top-level order.

cancel with oid

2

If both oid and cloid are present, oid wins.

cancel with only cloid

4

Canonical cancel-by-cloid.

modify with oid

6

If both oid and cloid are present, oid wins.

modify with only cloid

8

Canonical modify-by-cloid.

cancelAll

26

Cancel every open order for the effective owner in one market. No cloid.

batch

18

Batch item tags are order=0, cancel by oid=1, modify by oid=2, modify by cloid=3, cancel by cloid=4, cancelAll=5.

Order action bytes:

Cancel action bytes:

Modify action bytes:

replacement_order uses the same fields as order after market_id:

CancelAll action bytes:

cancelAll carries no oid and no cloid. The signed payload is a single u32 market id; the JSON body must match ({ "type": "cancelAll", "market_id": "<id>" }).

Batch action bytes:

Each batch item starts with a u8 item tag followed by the item payload without the top-level u16 action tag:

TypeScript signing helper example for Node.js with ethers. The example uses Node's global Buffer; import it from node:buffer only if your TypeScript setup requires explicit Buffer types.

agent_epoch is mandatory for these API-wallet–signed trading actions: an API wallet is always an active agent, so a /trade write that omits agent_epoch is treated as direct-owner mode and rejected with DirectSignerIsActiveAgent. Read the current epoch from userAgents.

/trade is synchronous, so the response carries the outcome. submission_status is accepted (the transaction landed and executed), rejected (refused, or failed at execution), or timeout. /trade reports that the order landed, not its fill state — read orderStatus to see whether it rested or filled. See POST /trade and error responses for the full model.

Order outcome (the order landed):

Rejected response (node-admission code, returned verbatim):

Request-shaping rejections usually have no tx_hash because canonical bytes were not assembled; admission and execution rejections include it. Rate-limit responses also include error.retry_after_ms. A submission_status: "timeout" means the outcome was not observed in time — either the wait budget elapsed (HTTP 200, no error) or the submission could not be routed (HandoffTimeout / HandoffBufferFull:* / HandoffMultipleActive at HTTP 503, or node_unreachable: … at HTTP 504). The two cases need opposite handling — see Handle outcomes & timeouts.

Malformed or non-decodable JSON is handled by the API and returns the TradeResponse shape with error.code = "invalid_json". This includes invalid action type tags and invalid field types. Negative numeric strings and malformed decimal strings are also invalid_json — they are rejected inside deserialization, before any field-specific check runs. Failures caught after decoding, such as a precision violation, return their specific code in the same response shape. All such responses include x-trace-id.

/trade error codes

Every /trade error.code — request-shaping, gateway, node-admission, and execution — is cataloged in Error responses. Node-admission codes are returned verbatim (CamelCase) in top-level error.code. A lowercase execution code such as tick never appears there — for an order-ish action it comes back inside the response envelope while submission_status stays accepted.

Supported public top-level action types:

  • order

  • cancel

  • cancelAll

  • modify

  • batch

  • withdraw (user single-signature, EIP-712 auth_scheme:"eip712"; see withdraw)

  • settle (user single-signature, EIP-712 auth_scheme:"eip712")

  • repay (user single-signature, EIP-712 auth_scheme:"eip712")

  • approveAgent (owner single-signature, EIP-712 auth_scheme:"eip712"; see approveAgent)

  • revokeAgent (owner single-signature, EIP-712 auth_scheme:"eip712")

Operator/accounting writes (deposit, adminSetAccountingWithdrawTokens, admin*, setMultisigPolicy, addAsset, openMarket, …) are also accepted by the API but require operator/admin authority and are not part of this public trading contract.

Last updated