POST /order require Ed25519 signatures for authentication.
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, excludingsigner and signature:
signeris 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_bytesis raw 32 bytes (bs58.decode(account)).signature_domain_u8is selected from trusted client configuration. It is not a JSON transaction field and a server never accepts a client-selected domain header.- The
signaturefield 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 toPOST /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):
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.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
AllPubkey 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)
i; each nested order or conditional action
carries its own isolated-routing field.
Trailing (discriminant 9)
OnFill (discriminant 10)
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)
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:- Agent must be pre-authorized via an
agentWalletCreationaction (see Manage Agent Wallet). - Agent signs with their own private key.
- Order executes against the user’s account.
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)
Thepayload is deterministic clear-sign text built from the transaction fields. Action lines are emitted in transaction order:
- HTTP: send the standard transaction JSON to
POST /orderwithx-bulk-sig-mode: offchain.signatureis base58 of the 64-byte Ed25519 signature over the envelope above. - WebSocket: set the handshake header
x-bulk-sig-mode: offchainon the upgrade, then post transactions normally via thepostmethod. - 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:
- Signature over
bs58_encode(canonical_binary_message).as_bytes(). - Signature over the same v0 Solana offchain-message envelope described above, wrapping the canonical binary message as the payload (
format = 0x00).
Implementation Notes
Common Issues
Invalid signature
Invalid signature
The binary message must match the server’s wincode serialization exactly:
- Use
bs58.decode()to convert base58Pubkey/Hashstrings into raw 32-byte arrays. - All enum variant tags (outer action types and
tif) areu32LE. Vec/Stringlengths areu64LE.- Price/size/threshold/limit
f64fields are fixed-point scaled (round(x * 1e8)asu64LE), not rawf64. The plain-f64fields are listed in the encoding table above. - The
signatureitself is NOT part of the signed message. Sign the binary, thenbs58.encode()the 64-byte Ed25519 signature for the JSON payload. - Order actions
l,m,st,tp,rng, andtrlalways serialise the trailingi(isolated routing) byte. Always includeiin those JSON payloads (trueorfalse); omitting it produces abad signature. - A
trigbasket has no top-leveli. Each nested order or conditional action carries its owni.
Account not found
Account not found
Account must be funded first via a faucet action (see Request Faucet on testnet).
Replay attack / duplicate nonce
Replay attack / duplicate nonce
Each nonce can only be used once. Use timestamps or incrementing counters.
Round-tripping server responses into signatures
Round-tripping server responses into signatures
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)).