Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Signing

Every /exchange action is an EIP-712 signature, so any Ethereum wallet or library can produce it; /info reads are unsigned. There are two flows, and both end in the same envelope and the same keccak256(0x19 ‖ 0x01 ‖ domainSeparator ‖ structHash) digest:

FlowSigned byUsed for
Direct EIP-712Account keyFund movements and authorizations — approveAgent, tokenSend, withdraw3, approveBuilderFee, revokeBuilderFee
Agent signingAgent keyTrading — orders, cancels, margin, outcomeOperate, claimBuilderRewards

Domain

The standard four-field EIP-712 domain, separate per network so a testnet signature can never replay on mainnet. salt is unused.

EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)
NetworknameversionchainIddomainSeparator
MainnetTxFlow-Mainnet"1"8690x878a21f7c464d592bad925771b5ab6f52897c4321b30eb6cfdf4ea55c749ad8d
TestnetTxFlow-Testnet"1"25110x839dd579adc05b1545a37213db5dd3dc6928532ddf73dfa1b53f647421060a8f

version is the apiVersion as a decimal string, and verifyingContract is always the zero address.

Request envelope

Every action, whichever flow signed it, is submitted in the same envelope:

{
  "action": { "type": "<actionType>", "...": "action fields" },
  "signature": { "r": "0x…", "s": "0x…", "v": 27 },
  "nonce": 1757992743994,
  "vaultAddress": null,
  "signatureChainId": null
}
FieldTypeDescription
actionobjectThe action body. type selects the operation; see Exchange API.
signature.r, signature.shex stringThe 32-byte ECDSA scalars.
signature.vinteger27 or 28 (27 + recoveryId).
nonceuint64Millisecond timestamp. Whenever the action body carries its own nonce, the envelope must repeat the same value.
vaultAddressaddress · optionalTrade on behalf of a vault. Covered by the signature.
signatureChainIduint32 · optionalOverrides the domain chainId for wallets that force their connected chain.

signatureChainId only changes the EIP-712 domain, never the struct hash, and it applies only to direct EIP-712 actions (approveAgent, approveBuilderFee, revokeBuilderFee, tokenSend, withdraw3). Agent-signed actions ignore it.

Flow 1 — Direct EIP-712

Each account-key action has its own EIP-712 primary type, listed on its reference page, and the action fields are the signed message — so the wallet prompt shows the real values.

  1. Build the struct hash for the action's type: structHash = keccak256(abi.encode(typeHash, encodedFields…)). String fields are hashed with keccak256, addresses are left-padded to 32 bytes, integers are ABI-encoded.
  2. signingHash = keccak256(0x19 ‖ 0x01 ‖ domainSeparator ‖ structHash).
  3. Sign signingHash with secp256k1 and emit {r, s, v}.

The recovered address is the account itself. The nonce comes from action.nonce.

Example — approveAgent

This is the action that enables trading: the account key authorizes an agent key to sign orders on its behalf.

ApproveAgent(string txflowNetwork,uint32 chainId,uint32 apiVersion,address agentAddress,string agentName,uint64 nonce)
{
  "type": "approveAgent",
  "txflowNetwork": "TxFlow-Testnet",
  "chainId": 2511,
  "apiVersion": 1,
  "agentAddress": "0x0202020202020202020202020202020202020202",
  "agentName": "my-trading-bot",
  "nonce": 2000
}

A null agentName is hashed as the empty string. To revoke an agent, submit the same action with agentAddress set to the zero address and the existing agentName.

Flow 2 — Agent signing

The trading actions (and vaultTransfer) are not typed data themselves. The body is compacted into a 32-byte connectionId and wrapped in one fixed EIP-712 type:

Agent(string txflowNetwork,uint32 chainId,uint32 apiVersion,bytes32 connectionId)
  1. MessagePack-encode the action body with the type field removed, as a map with named keys.
  2. Append the nonce as 8 bytes big-endian.
  3. Append the vault flag: 0x00 when vaultAddress is null, otherwise 0x01 followed by the 20 address bytes.
  4. connectionId = keccak256(msgpack ‖ nonceBE8 ‖ vaultFlag).
  5. Hash Agent{txflowNetwork, chainId, apiVersion, connectionId} as EIP-712 and sign the result.

The recovered address must be an agent already registered through approveAgent — the node rejects any other signer with Agent … is not authorized by any account. The nonce comes from the envelope's nonce.

MessagePack rules

The encoding must be byte-identical to the server's, so the details matter:

  • Encode as a map with named keys, not an array. Keys are the wire names, including the abbreviated order fields (a, b, p, s, r, t, c, m, o).
  • Keep the node's field order, which is the key order of each action's request example (for an order body: a, b, p, s, r, t, c, m, expiresAt). The field tables sort required fields first, so do not copy their order, and do not sort keys.
  • Omit optional fields that are null rather than encoding them as nil — for example, leave tpsl out of a plain modify2 instead of sending "tpsl": null.
  • Use the most compact integer encoding, native booleans, and native strings. Prices and sizes are strings and are hashed exactly as written, so "50000.0" and "50000" are different messages.
  • Addresses inside an action body (such as vaultTransfer.vaultAddress) are encoded as binary, not as a hex string.

Example — order

{
  "type": "order",
  "grouping": "na",
  "orders": [
    { "a": 1, "b": true, "p": "50000.0", "s": "0.01", "r": false, "t": { "limit": { "tif": "Gtc" } } }
  ]
}

With nonce = 1700000000000 and no vault, the msgpack bytes are 0x82a867726f7570696e67a26e61a66f72646572739186a16101a162c3a170a735303030302e30a173a4302e3031a172c2a17481a56c696d697481a3746966a3477463, giving connectionId = 0x6546d1c3acf430a28a0b2552c66e985f6543646660874734732ae9e9daf4d806.

Builder-fee actions

The builder-code actions intentionally use both signing flows.

The approval actions use direct EIP-712. approveBuilderFee and revokeBuilderFee are signed by the account key under distinct primary types. Their different typehashes prevent a signature for one action from being replayed as the other. Both support signatureChainId, which changes only the EIP-712 domain chain while the business chainId remains in the signed struct.

The reward claim uses tagged Agent signing. claimBuilderRewards has no fields. Its MessagePack preimage keeps the type tag so it cannot collide with other field-less actions:

{ "type": "claimBuilderRewards" }
ActionPreimageSignerReplay nonce
approveBuilderFeeApproveBuilderFee EIP-712 structAccount keyaction.nonce
revokeBuilderFeeRevokeBuilderFee EIP-712 structAccount keyaction.nonce
claimBuilderRewardsTagged actionRegistered agentenvelope nonce

The approval signer must be the account itself; an authorized trading agent cannot approve or revoke a builder on the account's behalf. The reward claim follows the Agent flow and therefore requires an agent authorized by the builder account.

Nonces

Nonces are millisecond timestamps, not counters, so concurrent clients can sign in parallel without coordinating. The node applies three checks:

  • The nonce must fall within two days before and one day after the block timestamp.
  • The nonce must not already be used. Each address keeps its 100 highest nonces; once that set is full, a new nonce must exceed the smallest one still stored.
  • Two transactions from the same address in the same block may not share a nonce.

In practice Date.now() per request is correct. A client that sends more than 100 transactions inside the retention window must simply keep its nonces increasing.

Signatures must also be canonical low-S (s <= n/2). Most libraries normalize this automatically; high-S signatures are rejected as non-canonical.

Transaction hash

Once signed, a transaction's id is derived from the digest and the signature together, so two signers submitting the same action and nonce still get distinct transaction hashes:

txHash = keccak256(signingHash[32] ‖ r[32] ‖ s[32] ‖ v[1])

The v byte here is the raw recovery id (0 or 1), not the 27/28 used in JSON.

Verifying a signature

To check a signature server-side, or to debug your own client:

  1. Recompute signingHash with the flow that matches the action.
  2. Take recoveryId = v - 27.
  3. Recover the public key with secp256k1 ECDSA recovery.
  4. The signer address is the last 20 bytes of the keccak256 hash of the uncompressed public key without its leading byte.

For agent actions, resolve that address through agentOwner to get the account it trades for.

Test vectors

All vectors use the throwaway key 0xabab…abab (address 0xe239cdc5fbe977a8a141b72194d3cf8c41bc5bc6) and testnet domain parameters. If your implementation reproduces both, the two flows are wired correctly.

order — the action and connectionId from the example above, nonce = 1700000000000, no vault:

signingHash0xeae1ae63709636fb9567accd33bc5dd2717bfac4d97e9a853f91c7fb391e1cc6
r0x4064848dd881985c52dacdc00f7046f1010f7f370e34620d36fcf3b2d1ecb3e0
s0x25c45ad67b9506ac653fc3658f47aa8b6002bfa86a31ae619b334d71aa8b4f6f
v27

approveAgent — the action from the example above, nonce = 2000:

structHash0xbb5293971660f3252f455b6debbda781841fb23bd45cbbf0199c4de5250573b9
signingHash0x1df22801b48ae1402ad36b244aa245fd6e842fa5fef2c40819b1e6c661cd2212
r0x4431d4afb9fcd0dcd619afd55a7044cc26cb0ee1d436a1b06d836da217842ead
s0x54a0755ee3d8c241235a1828a2be732d72e9c152b5de25fc7c169966de4210f7
v27

Next steps

  • Try Testnet — a working browser client that approves an agent and signs a real order.
  • Exchange API — every action type and which flow signs it.
  • TypeScript SDK — typed clients that implement this scheme for you.