Every transaction submitted to the Proof Exchange is a MessagePack-encoded positional array containing a version tag, action type byte, sequence nonce, serialized payload, public key, and Ed25519 signature. Understanding this format is useful when debugging signature mismatches, integrating non-TypeScript SDKs, or building conformance tests across language implementations.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/Proof-labs/trading-sdk/llms.txt
Use this file to discover all available pages before exploring further.
Wire envelope structure
The top-level envelope is a 6-element MessagePack array:| Index | Field | Type | Notes |
|---|---|---|---|
| 0 | version | uint (fixed 2) | Wire protocol version. The SDK always emits 2. |
| 1 | action_type | uint8 | One-byte action identifier (see table below). |
| 2 | seq | uint64 | Timestamp nonce in milliseconds. |
| 3 | payload | bin | MessagePack-encoded action field array (nested). |
| 4 | pubkey | bin (32 bytes) | Ed25519 public key of the signer. Encoded as msgpack bin via serde_bytes. |
| 5 | signature | bin (64 bytes) | Ed25519 signature over the signing message. Encoded as msgpack bin via serde_bytes. |
payload field (index 3) is itself a MessagePack-encoded positional array whose field order matches the corresponding Rust struct definition. The SDK’s encodePayload() in src/codec.ts is the authoritative source of these layouts.
Action type constants
TheActionType object in src/types.ts defines the wire byte for each action:
Signing message (v3 layout)
The bytes passed toed25519.sign() are not the raw wire envelope. They are a separate, deterministic signing message constructed by signingMessage() in src/crypto.ts:
| Region | Size | Content |
|---|---|---|
| Domain prefix | 16 bytes | ASCII ProofExchange-v3 |
| Chain ID | 32 bytes | keccak256(cometbft_chain_id_string) |
| Action type | 1 byte | Same byte as envelope index 1 |
| Sequence | 8 bytes | seq as big-endian unsigned 64-bit integer |
| Payload | variable | The same MessagePack bytes stored in envelope index 3 |
v2 to v3 on 2026-04-23 (audit finding B4) when the 32-byte chain_id field was added. A transaction signed under v2 produces a different signing message and fails verification against a v3 engine.
Chain ID binding
The 32-byte chain ID iskeccak256 of the CometBFT chain_id string, produced by chainIdFromString():
ExchangeClient resolves and caches the chain ID automatically. Pass chainId explicitly in the constructor options to skip the network fetch:
Sequence (timestamp nonce)
Theseq field is a millisecond Unix timestamp used as a nonce. The SDK allocates it as max(now_ms, last_nonce + 1), guaranteeing strict monotonicity within a single process even when transactions are submitted faster than 1 ms apart.
The engine validates seq against a sliding window. A seq value that is too far in the past, too far in the future, or has already been used results in code 21 TimestampNonceRejected. The recommended handling is a simple retry — the SDK automatically advances the nonce on the next call:
signAndEncode() function
Most users go through ExchangeClient.submitTx() which handles signing internally. When you need direct access to the signed wire bytes — for example to POST them to a custom endpoint or inspect them — use signAndEncode():
src/codec.ts:
encodeSignedTx() instead:
Minimal-integer compatibility
The Rust MessagePack library (rmp-serde) encodes all integers in their minimal form — a value of 1 is a 1-byte positive fixint, not a 9-byte uint64. The TypeScript SDK replicates this by converting BigInt values that fit in a u32 (0..=4294967295) to plain Number before encoding:
Do not encode payload bytes yourself using
@msgpack/msgpack with useBigInt64: true on every field — small bigint values would be emitted as 9-byte uint64 rather than minimal fixints, producing payload bytes that differ from what the engine re-encodes and causing signature failures (code 17).