Skip to main content
All transactions submitted to POST /order require Ed25519 signatures for authentication.
4 October 2026 upgrade: ALO_SLIDE and ALO_JOIN add TIF indices 3 and 4; existing indices remain unchanged. JSON requests use the uppercase names. Use the updated SDK signing encoder for conditional orders, slippage overrides, and batches containing conditional actions. The handwritten examples below do not implement the upgraded conditional-action signing schema and must not be used to sign those transactions. Omitting a slippage override does not make an older conditional-action encoder compatible.

JSON vs Binary Formats

The API accepts transactions as JSON where all cryptographic fields are base58-encoded strings: For signing, construct the canonical wincode binary representation of the transaction (the action list plus the trailing nonce, account, and one-byte network signature domain), sign those bytes with Ed25519, then base58-encode the 64-byte signature for the JSON payload. The signer field is NOT part of the signed bytes.

JSON wire: input tolerance vs. canonical output


What Gets Signed

The signature is computed over the canonical wincode binary serialization of the transaction, excluding signer and signature:
Notes:
  • signer is required in the JSON payload but is not part of the signed bytes.
  • If signer != account, the signer must be an authorized agent for that account.
  • account_pubkey_bytes is raw 32 bytes (bs58.decode(account)).
  • signature_domain_u8 is selected from trusted client configuration. It is not a JSON transaction field and a server never accepts a client-selected domain header.
  • The signature field itself is not included in what gets signed.
Nonce: Use a unique value for replay protection (timestamp in milliseconds or an incrementing counter). On the wire it is u64 little-endian.

Transaction Structure

All signed requests to POST /order use this unified envelope:

Binary Serialization Format (wincode)

This describes how each type is encoded in the binary message used for signing, not the JSON format. The binary wire is bincode-compatible: fixed-width little-endian integers, u32 enum tags, u64 length prefixes.
TimeInForce is a serde-derived enum, so bincode writes its variant index as a u32, the same treatment as the outer action discriminants.

Scaled vs plain f64

The exchange uses a fixed-point-over-bincode convention for price- and size-like fields: multiply the f64 value by 1e8, round to the nearest integer, and serialize it as a u64 little-endian (8 bytes). Scaled f64 fields (encode as u64 LE of round(x * 1e8)): Scaled Option<f64> fields (1-byte tag + optional u64 LE of round(x * 1e8); encode absent as 0x00 tag with no payload):
The old NaN means market-style trigger convention is no longer supported. A market-style trigger is now encoded as Option::None (0x00), not as a NaN-valued f64.
Plain f64 fields (raw IEEE-754, 8 bytes LE, no 1e8 scaling):

Action Discriminants

The following values cover the actions documented on this page. Discriminant values are fixed; gaps must not be renumbered.
abc and rbc are master-account-only actions. Agent wallets cannot approve or revoke builder-code recipients, even when they are otherwise authorized to sign orders for the account.

Official Signing Library (bulk-keychain)

The recommended way to sign transactions is the official bulk-keychain library. One Rust core with bindings for: Repository: github.com/Bulk-trade/bulk-keychain It handles the canonical wincode encoding, scaled-f64 convention, single/batch/grouped signing, agent wallet, faucet, and external wallet (prepare/finalize) flows. For deterministic order ID computation, see Order IDs.

Binary Layout

All Pubkey and Hash fields are 32 raw bytes from bs58.decode() on the base58 string. scaled u64 means writeU64(round(value * 1e8)).

Transaction (signed message)

MarketOrder (discriminant 0)

LimitOrder (discriminant 1)

ModifyOrder (discriminant 2)

Cancel (discriminant 3)

CancelAll (discriminant 4)

Stop (discriminant 5) and TakeProfit (discriminant 6)

Range / OCO (discriminant 7)

Trigger Basket (discriminant 8)

Trigger baskets have no top-level i; each nested order or conditional action carries its own isolated-routing field.

Trailing (discriminant 9)

OnFill (discriminant 10)

The trigger action is embedded in of.trigger; on-fill does not serialize a parent action index or sequence number.

Price (discriminant 11): admin

PythOracle (discriminant 13): admin

Faucet (discriminant 16)

AgentWalletCreation (discriminant 17)

UpdateUserSettings (discriminant 18)

WhitelistFaucet (discriminant 19): admin

ApproveBuilderCode (discriminant 40)

RevokeBuilderCode (discriminant 41)

CreateSubAccount (discriminant 27)

RemoveSubAccount (discriminant 28)

Transfer (discriminant 29)

RenameSubAccount (discriminant 37, tag rsa)


Signing Example (JavaScript/TypeScript)

Reference only. Serialization must match the canonical BULK wincode encoding exactly. For production, prefer the official bulk-keychain library.

Install Dependencies

Implementation


Account vs Signer

The transaction includes two separate public key fields:
  • account: The account being traded (whose positions/orders are affected).
  • signer: Who is signing the transaction (usually same as account, or an authorized agent).

Same Account and Signer

Most common case, you’re trading your own account:

Agent Wallet (Different Signer)

Agent wallet trading on behalf of user:
Requirements:
  1. Agent must be pre-authorized via an agentWalletCreation action (see Manage Agent Wallet).
  2. Agent signs with their own private key.
  3. Order executes against the user’s account.
Agent Wallets: If signer != account, the signer must be pre-authorized via Manage Agent Wallet first.

Offchain Signing Mode

The default and preferred signing path is the raw canonical-binary form documented above. The server additionally accepts an offchain signing mode for any transaction shape. This exists so that wallets/devices that can only sign clear-text messages (Phantom, Ledger, hardware signers) can still produce valid transactions.

Signature Mode Hint Header

When you use offchain signing, set this hint header so the verifier tries the matching path first: Semantics:
  • Missing header: server uses the default raw canonical-binary verification path.
  • Invalid value: ignored safely (falls back to the default raw path).
  • Valid value: used only as an attempt-order hint. Signature validity still decides accept/reject.
  • The header is never trusted for authentication by itself.
  • Offchain-signed payloads should set x-bulk-sig-mode: offchain.

Offchain Envelope

The signed bytes are the full Solana v0 offchain-message envelope:
  • format = 0x00 -> printable ASCII payload.
  • format = 0x01 -> UTF-8 payload.
  • The server verifies the exact full envelope bytes; nothing is chunked or truncated. Practical signability is limited by the wallet/device.

Offchain Payload (Clear-Sign Text)

The payload is deterministic clear-sign text built from the transaction fields. Action lines are emitted in transaction order:
Optional debug/verbose line (off by default for cleaner Ledger UX):
Submission rules:
  • HTTP: send the standard transaction JSON to POST /order with x-bulk-sig-mode: offchain. signature is base58 of the 64-byte Ed25519 signature over the envelope above.
  • WebSocket: set the handshake header x-bulk-sig-mode: offchain on the upgrade, then post transactions normally via the post method.
  • The raw canonical-binary path remains the default for any connection that does not opt in.

Legacy Wallet-Compatibility Path (Owner-Signed agentWalletCreation Only)

For a single owner-signed agentWalletCreation action (signer == account and exactly one action), the server additionally accepts two legacy compatibility signatures, intended to support older wallet UIs that cannot use the general offchain mode above:
  1. Signature over bs58_encode(canonical_binary_message).as_bytes().
  2. Signature over the same v0 Solana offchain-message envelope described above, wrapping the canonical binary message as the payload (format = 0x00).
These paths exist only for owner registration / removal of agent wallets. Use the canonical binary path or the general offchain mode for everything else.

Implementation Notes

Official signing library: bulk-keychain provides signing for Node.js, browser (WASM), Python, and Rust with the correct protocol encoding (scaled-f64 convention, discriminants, and order-ID hashing). For production use, prefer bulk-keychain over manual implementation.

Common Issues

The binary message must match the server’s wincode serialization exactly:
  • Use bs58.decode() to convert base58 Pubkey/Hash strings into raw 32-byte arrays.
  • All enum variant tags (outer action types and tif) are u32 LE.
  • Vec/String lengths are u64 LE.
  • Price/size/threshold/limit f64 fields are fixed-point scaled (round(x * 1e8) as u64 LE), not raw f64. The plain-f64 fields are listed in the encoding table above.
  • The signature itself is NOT part of the signed message. Sign the binary, then bs58.encode() the 64-byte Ed25519 signature for the JSON payload.
  • Order actions l, m, st, tp, rng, and trl always serialise the trailing i (isolated routing) byte. Always include i in those JSON payloads (true or false); omitting it produces a bad signature.
  • A trig basket has no top-level i. Each nested order or conditional action carries its own i.
If signer != account, the signer must be pre-authorized for account:
  • The account itself is always allowed to sign for itself.
  • Agent wallets registered on a master can sign for that master and for any of its sub-accounts. Sub-accounts can also register their own agent wallets.
  • For sub-accounts, set account to the sub-account pubkey and signer to the master, the sub-account itself, or any agent wallet registered on either.
  • Isolated accounts are never set as account directly; orders routed there use the i flag on the master/sub-account transaction. See Isolated Margin.
Account must be funded first via a faucet action (see Request Faucet on testnet).
Each nonce can only be used once. Use timestamps or incrementing counters.
Scaled fields (sz, px, tr, pmin, pmax, lim, lmin, lmax) are returned as JSON strings in responses but must be signed from their numeric value (parseFloat → round(x * 1e8)).