Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Bullet Trading API

Welcome to the Bullet Trading API documentation. Everything you need to integrate with Bullet — from placing your first order to streaming real-time market data.

API Reference

  • Connectivity — environments, endpoints, and connection URLs
  • REST API — Binance FAPI-compatible endpoints for trading, account, and market data
  • WebSocket API — streaming market data, order updates, and request-response operations

Guides

SDKs

  • Rust — type-safe client with REST, WebSocket, transaction signing, and keypair management

Connectivity

Environments

EnvironmentHost
Mainnettradingapi.bullet.xyz
Testnettradingapi.testnet.bullet.xyz
Stagingtradingapi.staging.bullet.xyz

Endpoints

ProtocolURLDocs
RESThttps://<host>/fapi/v1/...Interactive API docs
WebSocketwss://<host>/wsWebSocket spec

Bullet Trading API WebSocket Specification

WebSocket endpoint: ws://<host>:<port>/ws

  • Localnet: ws://localhost:3000/ws
  • Staging: wss://tradingapi.staging.bullet.xyz/ws
  • Testnet: wss://tradingapi.testnet.bullet.xyz/ws
  • Mainnet: wss://tradingapi.bullet.xyz/ws

Connection Lifecycle

WebSocket connections go through three phases: establishment, keepalive, and disconnection. The server uses status messages to inform clients of connection state changes (e.g., successful connection, impending disconnection with reason).

Status Message

FieldTypeDescriptionBinance Equivalent
estring"status"- (no equivalent)
Eu64event time (μs)-
statusstring"connected" or "disconnecting"-
clientIdstringUUID client identifier-
reasonstring?disconnect reason (only for disconnecting)-

Connection Establishment

  1. Client connects to WebSocket endpoint
  2. Server assigns unique clientId
  3. Server sends "connected" status message
{
  "e": "status",
  "E": 1706745600000000,
  "status": "connected",
  "clientId": "ws_abc123"
}

Disconnection

The server sends a "disconnecting" status message before closing:

{
  "e": "status",
  "E": 1706745600000000,
  "status": "disconnecting",
  "clientId": "ws_abc123",
  "reason": "pong_timeout"
}

Disconnect Reasons

ReasonDefaultDescription
idle_timeout60sno valid message within timeout
pong_timeout60sclient didn’t respond to ping
max_duration24hconnection exceeded max lifetime

Keepalive (Ping/Pong)

Connections are kept alive via WebSocket ping/pong frames. The server sends ping frames every 30 seconds; clients must respond with pong frames. WebSocket ping frames reset the keepalive timeout.

Ping

Ping the server to check connection health. Alternatively clients can also send WebSocket ping frames.

{
  "method": "ping",
  "id": 4
}
FieldTypeRequiredDescriptionBinance Equivalent
methodstringyes"ping" or "PING"(WebSocket ping frame)
idu64norequest correlation id-

Possible errors: ValidationError (message parse)

Responses: Pong

Pong

{
  "e": "pong",
  "id": 4,
  "E": 1706745600000000
}
FieldTypeDescriptionBinance Equivalent
estring"pong"(WebSocket pong frame)
idu64?echoed request id-
Eu64event time (μs)-

Subscription Streams

All client messages use JSON format with method field. Method names are case-insensitive.

Subscribe

Subscribe to market data topics or user data streams.

{
  "method": "subscribe",
  "id": 1,
  "params": [
    "BTC-USD@aggTrade",
    "ETH-USD@depth10"
  ]
}
FieldTypeRequiredDescriptionBinance Equivalent
methodstringyes"subscribe" or "SUBSCRIBE"method: "SUBSCRIBE"
paramsstring[]yesarray of topic stringsparams
idu64norequest correlation idid

Behavior: Idempotent with atomic validation. All topics validated first - if any fails, entire request fails. Already-subscribed topics silently skipped.

Possible errors: ValidationError, TooManyRequests, InvalidSubscriptionFormat, Unauthorized, InvalidSymbol, ClientNotFound

Responses:

Subscribe (success)

{
  "e": "subscribe",
  "id": 1,
  "E": 1706745600000000,
  "result": "success"
}
FieldTypeDescriptionBinance Equivalent
estring"subscribe"- (Binance uses result: null)
idu64?echoed request idid
Eu64event time (μs)-
resultstring"success"result: null

Unsubscribe

Unsubscribe from market data topics or user data streams.

{
  "method": "unsubscribe",
  "id": 5,
  "params": [
    "BTC-USD@aggTrade"
  ]
}
FieldTypeRequiredDescriptionBinance Equivalent
methodstringyes"unsubscribe" or "UNSUBSCRIBE"method: "UNSUBSCRIBE"
paramsstring[]yestopics to unsubscribeparams
idu64norequest correlation idid

Behavior: Idempotent - always succeeds. Invalid or non-subscribed topics silently skipped.

Possible errors: ValidationError (message parse)

Responses:

Unsubscribe (success)

{
  "e": "unsubscribe",
  "id": 5,
  "E": 1706745600000000,
  "result": "success"
}
FieldTypeDescriptionBinance Equivalent
estring"unsubscribe"- (Binance uses result: null)
idu64?echoed request idid
Eu64event time (μs)-
resultstring"success"result: null

ListSubscriptions

List all active subscriptions for the client.

{
  "method": "list_subscriptions",
  "id": 6
}
FieldTypeRequiredDescriptionBinance Equivalent
methodstringyes"list_subscriptions" or "LIST_SUBSCRIPTIONS"method: "LIST_SUBSCRIPTIONS"
idu64norequest correlation idid

Possible errors: ValidationError (message parse)

Responses:

ListSubscriptions (success)

{
  "e": "list_subscriptions",
  "id": 6,
  "E": 1706745600000000,
  "result": [
    "BTC-USD@depth10",
    "ETH-USD@aggTrade"
  ]
}
FieldTypeDescriptionBinance Equivalent
estring"list_subscriptions"-
idu64?echoed request idid
Eu64event time (μs)-
resultstring[]subscribed topicsresult: […]

Request-Response

OrderPlace

Place an order via WebSocket.

{
  "method": "order.place",
  "id": 10,
  "params": {
    "tx": "c2lnbmVkX3RyYW5zYWN0aW9uX2J5dGVz"
  }
}
FieldTypeRequiredDescriptionBinance Equivalent
methodstringyes"order.place" or "ORDER.PLACE"method: "order.place"
params.txstringyessigned base64-encoded borsh transaction bytes(different: REST params)
idu64norequest correlation idid

Possible errors: ValidationError (empty tx), ServiceUnavailable (submission failed), Timeout, NewOrderRejected

Responses:


OrderCancel

Cancel an order via WebSocket.

{
  "method": "order.cancel",
  "id": 11,
  "params": {
    "tx": "c2lnbmVkX3RyYW5zYWN0aW9uX2J5dGVz"
  }
}
FieldTypeRequiredDescriptionBinance Equivalent
methodstringyes"order.cancel" or "ORDER.CANCEL"method: "order.cancel"
params.txstringyessigned base64-encoded borsh transaction bytes(different: orderId/clientOrderId)
idu64norequest correlation idid

Possible errors: ValidationError (empty tx), ServiceUnavailable (submission failed), Timeout, CancelRejected

Responses:


OrderAmend

Amend an existing order via WebSocket.

{
  "method": "order.amend",
  "params": {
    "tx": "<signed-base64-borsh-tx>"
  },
  "id": 7
}
FieldTypeRequiredDescriptionBinance Equivalent
methodstringyes"order.amend", "order.modify", or "ORDER.AMEND"-
params.txstringyessigned base64-encoded borsh transaction bytes-
idu64norequest correlation idid

Possible errors: ValidationError (empty tx), ServiceUnavailable (submission failed), Timeout, CancelRejected

Responses:


OrderCancelAll

Cancel all open orders for a market via WebSocket.

{
  "method": "order.cancelAll",
  "params": {
    "tx": "<signed-base64-borsh-tx>"
  },
  "id": 8
}
FieldTypeRequiredDescriptionBinance Equivalent
methodstringyes"order.cancelAll" or "ORDER.CANCEL_ALL"-
params.txstringyessigned base64-encoded borsh transaction bytes-
idu64norequest correlation idid

Possible errors: ValidationError (empty tx), ServiceUnavailable (submission failed), Timeout, CancelRejected

Responses:


Error

Error response to any client request.

{
  "e": "error",
  "id": 2,
  "E": 1706745600000000,
  "error": {
    "param": "invalid-topic-format",
    "code": -1004,
    "msg": "invalid subscription format: expected <symbol>@<stream>"
  }
}
FieldTypeDescriptionBinance Equivalent
estring"error"-
idu64?echoed request idid
Eu64event time (μs)-
error.codei32error codeerror.code
error.msgstringerror messageerror.msg
error.paramstring?parameter that caused error- (Bullet-specific)

OrderResult

Response to OrderPlace, OrderCancel, OrderAmend, or OrderCancelAll.

{
  "e": "order.place",
  "id": 10,
  "E": 1706745600000000,
  "results": {
    "tx_id": "0xabc123def456",
    "status": "processed",
    "order_ids": [
      1
    ],
    "client_order_ids": [
      1
    ]
  }
}
FieldTypeDescriptionBinance Equivalent
estring"order.place", "order.cancel", "order.amend", "order.cancelAll"-
idu64?echoed request idid
Eu64event time (μs)-
results.tx_idstringtransaction hash- (DEX-specific)
results.statusstringtx status ("processed", "skipped")- (DEX-specific)
results.order_idsu64[]affected order ids- (DEX-specific)
results.client_order_idsu64[]affected client order ids- (DEX-specific)

OrderError

Error response to OrderPlace, OrderCancel, OrderAmend, or OrderCancelAll.

{
  "id": 12,
  "E": 1706745600000000,
  "error": {
    "code": -2010,
    "msg": "new order rejected: insufficient margin"
  }
}
FieldTypeDescriptionBinance Equivalent
idu64?echoed request idid
Eu64event time (μs)-
error.codei32error codeerror.code
error.msgstringerror messageerror.msg

Market Data

All market data messages are pushed to subscribed clients. No type wrapper.

DepthUpdate

Topic: SYMBOL@depth, SYMBOL@depth5, SYMBOL@depth10, SYMBOL@depth20

{
  "e": "depthUpdate",
  "E": 1706745600000000,
  "T": 1706745600000000,
  "s": "BTC-USD",
  "U": 1000,
  "u": 1000,
  "pu": 0,
  "b": [
    [
      "50000.00",
      "1.5"
    ],
    [
      "49999.00",
      "2.0"
    ]
  ],
  "a": [
    [
      "50001.00",
      "1.2"
    ],
    [
      "50002.00",
      "3.0"
    ]
  ],
  "mt": "s"
}
FieldTypeDescriptionBinance Equivalent
estring"depthUpdate"e
Eu64event time (μs)E
Tu64transaction time (μs)T
sstringsymbols
Uu64first update idU
uu64last update id (always equals U)u
puu64previous update idpu
b[[price, qty], …]bids (descending)b
a[[price, qty], …]asks (ascending)a
mtstring"s" (snapshot) or "u" (update)- (Bullet-specific)

Note: U equals u and as we don’t batch updates.

AggTrade

Topic: SYMBOL@aggTrade

{
  "e": "aggTrade",
  "E": 1706745600000000,
  "s": "BTC-USD",
  "a": 200001,
  "p": "50000.50",
  "q": "0.5",
  "f": 200001,
  "l": 200001,
  "T": 1706745600000000,
  "m": false,
  "th": "0xabc123def456",
  "ua": "0xuser123",
  "oi": 100001,
  "mk": false,
  "ff": true,
  "lq": false,
  "fe": "0.025",
  "nf": "0.025",
  "fa": "USD",
  "sd": "BUY"
}
FieldTypeDescriptionBinance Equivalent
estring"aggTrade"e
Eu64event time (μs)E
sstringsymbols
au64aggregate trade ida
pstringpricep
qstringquantityq
fu64first trade idf
lu64last trade idl
Tu64trade time (μs)T
mboolis buyer makerm
thstringtransaction hash- (DEX-specific)
uastringtrader address- (DEX-specific)
oiu64order id- (DEX-specific)
mkboolis maker- (DEX-specific)
ffboolfully filled- (DEX-specific)
lqboolliquidation trade- (DEX-specific)
festringfee amount- (DEX-specific)
nfstringnet fee- (DEX-specific)
fastringfee asset- (DEX-specific)
costring?client order id- (DEX-specific)
sdstring"BUY" or "SELL"- (DEX-specific)

BookTicker

Topic: SYMBOL@bookTicker, !bookTicker

{
  "e": "bookTicker",
  "u": 1000,
  "E": 1706745600000000,
  "T": 1706745600000000,
  "s": "BTC-USD",
  "b": "50000.00",
  "B": "1.5",
  "a": "50001.00",
  "A": "1.2",
  "mt": "s"
}
FieldTypeDescriptionBinance Equivalent
estring"bookTicker"e
uu64update idu
Eu64event time (μs)E
Tu64transaction time (μs)T
sstringsymbols
bstringbest bid priceb
Bstringbest bid qtyB
astringbest ask pricea
Astringbest ask qtyA
mtstring"s" (snapshot) or "u" (update)- (Bullet-specific)

MarkPrice

Topic: SYMBOL@markPrice, !markPrice@arr

{
  "e": "markPriceUpdate",
  "E": 1706745600000000,
  "s": "BTC-USD",
  "p": "50000.50",
  "i": "50000.00",
  "r": "0.0001",
  "T": 1706774400000000
}
FieldTypeDescriptionBinance Equivalent
estring"markPriceUpdate"e
Eu64event time (μs)E
sstringsymbols
pstringmark pricep
istringindex price (median CEX price)i
Pstring?estimated settle priceP
rstringfunding rater
Tu64?next funding timeT
thstring?transaction hash- (DEX-specific)

Liquidation (ForceOrder)

Topic: SYMBOL@liquidations, SYMBOL@forceOrder, !liquidations, !forceOrder, liquidations, forceOrders

{
  "e": "liquidation",
  "E": 1706745600000000,
  "o": {
    "s": "BTC-USD",
    "S": "SELL",
    "o": "LIMIT",
    "f": "IOC",
    "p": "49000.00",
    "ap": "49000.00",
    "X": "FILLED",
    "l": "1.0",
    "T": 1706745600000000,
    "th": "0xabc123def456",
    "ua": "0xuser123",
    "oi": 100001,
    "ti": 200001
  }
}
FieldTypeDescriptionBinance Equivalent
estring"liquidation"e ("forceOrder")
Eu64event time (μs)E
o.sstringsymbolo.s
o.Sstringside ("BUY" or "SELL")o.S
o.ostringorder type ("LIMIT")o.o
o.fstringtime in force ("IOC")o.f
o.qstring?original quantityo.q
o.zstring?filled quantityo.z
o.pstringpriceo.p
o.apstringaverage priceo.ap
o.Xstringstatus ("FILLED")o.X
o.lstringlast filled qtyo.l
o.Tu64trade time (μs)o.T
o.thstringtransaction hash- (DEX-specific)
o.uastringliquidated address- (DEX-specific)
o.oiu64order id- (DEX-specific)
o.tiu64trade id- (DEX-specific)

OrderUpdate

Topic: [email protected], ADDRESS@ORDER_TRADE_UPDATE

Published for order lifecycle events (NEW, TRADE, CANCELED).

{
  "e": "orderTradeUpdate",
  "E": 1706745600000000,
  "o": {
    "s": "BTC-USD",
    "i": 100001,
    "X": "NEW",
    "x": "NEW",
    "T": 1706745600000000,
    "th": "0xabc123def456",
    "ua": "0xuser123",
    "S": "BUY",
    "o": "LIMIT",
    "f": "GTC",
    "p": "50000.00",
    "q": "1.0"
  }
}

Common fields (all events):

FieldTypeDescriptionBinance Equivalent
estring"orderTradeUpdate"e ("ORDER_TRADE_UPDATE")
Eu64event time (μs)E
o.sstringsymbolo.s
o.iu64order ido.i
o.Xstringorder statuso.X
o.xstringexecution typeo.x
o.Tu64transaction time (μs)o.T
o.thstringtransaction hash- (DEX-specific)
o.uastringuser address- (DEX-specific)

NEW order additional fields:

FieldTypeDescriptionBinance Equivalent
o.Sstringsideo.S
o.ostringorder typeo.o
o.fstringtime in forceo.f
o.pstringpriceo.p
o.qstringquantityo.q

TRADE fill additional fields:

FieldTypeDescriptionBinance Equivalent
o.Sstringsideo.S
o.lstringlast filled qtyo.l
o.Lstringlast filled priceo.L
o.nstringcommissiono.n

Topics

Symbol-Based Topics

Bullet TopicAliasesBinance EquivalentDescription
SYMBOL@depthsymbol@depthorderbook (default 10)
SYMBOL@depth5symbol@depth5orderbook 5 levels
SYMBOL@depth10symbol@depth10orderbook 10 levels
SYMBOL@depth20symbol@depth20orderbook 20 levels
SYMBOL@aggTradesymbol@aggTradetrades
SYMBOL@bookTickersymbol@bookTickerbest bid/offer
SYMBOL@markPricesymbol@markPricemark price + funding
SYMBOL@tickersymbol@ticker24hr ticker
SYMBOL@liquidationsSYMBOL@forceOrdersymbol@forceOrderliquidations
SYMBOL@kline_INTERVALsymbol@kline_INTERVALcandlesticks (coming soon)

Symbol format: Bullet uses BTC-USD (hyphen), Binance uses btcusdt (lowercase, no separator)

Kline intervals: 1m, 5m, 15m, 30m, 1h, 4h, 1d (coming soon)

Parse errors: InvalidFormat, MissingSymbol, InvalidDepth, InvalidInterval, UnknownTopic Resolution errors: SymbolNotFound

Broadcast Topics

Bullet TopicAliasesBinance Equivalent
tickers!ticker@arr, !ticker!ticker@arr
markPrices!markPrice@arr, !markPrice!markPrice@arr
bookTickers!bookTicker, !bookTicker@arr!bookTicker
liquidations!liquidations, !forceOrder, forceOrders!forceOrder@arr

Parse errors: InvalidFormat, UnknownTopic

User Data Topics

Bullet TopicAliasesBinance Equivalent
[email protected]ADDRESS@ORDER_TRADE_UPDATElistenKey stream

Key difference: Bullet uses address-prefixed topics directly. Binance requires a listenKey from REST API.

Resolution errors: MissingUserAddress (when address not provided)

Speed Suffixes

Speed suffixes are accepted but ignored: @100ms, @500ms, @1s

Example: BTC-USD@depth@100ms is equivalent to BTC-USD@depth


Error Codes

General

CodeNameDescriptionBinance Code
-1000Unknownunknown error-1000
-1001Disconnectedserver busy/disconnected-1001
-1002Unauthorizedauthentication required-1002
-1003TooManyRequestsrate limit exceeded-1003
-1006UnexpectedResponseunexpected response-1006
-1007Timeoutrequest timeout-1007
-1014UnknownOrderorder not found-1014
-1015TooManyOrdersorder rate limit-1015
-1016ServiceUnavailableservice down-1016
-1020UnsupportedOperationoperation not supported-1020
-1021InvalidTimestampbad timestamp-1021
-1022InvalidSignaturesignature invalid-1022

Parameters

CodeNameDescriptionBinance Code
-1102MandatoryParamMissingrequired param missing-1102
-1111BadPrecisionprecision error-1111
-1116InvalidOrderTypebad order type-1116
-1117InvalidSidebad side-1117
-1122InvalidSymbolinvalid symbol-1122
-1123InvalidUserAddressinvalid address- (Bullet-specific)

Subscriptions

CodeNameDescriptionBinance Code
-1004InvalidSubscriptionFormatbad topic format- (Bullet-specific)
-1005SymbolNotFoundsymbol not found- (Bullet-specific)
-1008ValidationErrorvalidation failed- (Bullet-specific)
-1010SubscriptionExistsalready subscribed- (Bullet-specific)

Orders

CodeNameDescriptionBinance Code
-2010NewOrderRejectedorder rejected-2010
-2011CancelRejectedcancel failed-2011
-2013NoSuchOrderorder doesn’t exist-2013
-2014ApiKeyFormatInvalidbad api key-2014
-2015InvalidApiKeyIpPermissionsauth failure-2015
-2021OrderWouldTriggerwould trigger immediately-2021

Internal

CodeNameDescriptionBinance Code
-4001ClientNotFoundclient not found- (internal)
-4002CouldNotSendMessagecould not send message- (internal)

Notes

DEX-Specific Fields

DEX-specific fields use 2-letter codes for compactness:

CodeFull NameDescription
thtx_hashon-chain transaction hash
uauser_addressuser’s wallet address
oiorder_idsequencer order ID
titrade_idsequencer trade ID
mkis_makerwhether the trade was a maker
ffis_full_fillfully filled indicator
lqis_liquidationliquidation trade indicator
fefeefee amount
nfnet_feenet fee after rebates
fafee_assetfee asset symbol
sdsidetrade side (BUY/SELL)
coclient_order_idclient-provided order ID

Message Type Field

The mt field in orderbook and BBO messages indicates:

  • "s" - snapshot (complete state at that depth level)
  • "u" - update (incremental changes since last update)

Response Format Differences

  • All responses use e field for event type (e.g., "e":"subscribe", "e":"error")
  • Order responses still use Binance-style format with status code
  • Market data messages use e field for event type (e.g., "e":"depthUpdate")

Guides

  • Account Setup — create an account, get a signing key, and start trading via the API
  • Delegate Accounts — register a separate keypair for secure, revocable API trading access
  • Transaction Signing — build, sign, and submit transactions (place orders, cancel orders, replace, amend, cancel all) to the Bullet exchange
  • Transaction Fields — uniqueness, TxDetails, gas limits, and paymaster
  • Decimal Encoding — convert fixed-point integers to RustDecimal for borsh serialization, with examples in C++, Python, and JavaScript
  • WebSocket Consistency Guarantees — snapshot delivery, backend reconnection behavior, and message ordering

Account Setup

This guide covers how to set up an account for trading via the API.

Overview

To trade via the API you need a delegate key — a separate keypair authorized to trade on your behalf, managed from the webapp. Delegate keys are revocable and cannot withdraw funds, making them the secure choice for API trading.

1. Create an account

Sign in at app.bullet.xyz (or app.testnet.bullet.xyz for testnet) with your wallet. This creates an embedded wallet that serves as your trading account on Bullet. On testnet, funds are airdropped automatically on signup.

2. Deposit funds

Deposit collateral through the webapp UI.

3. Set up a delegate key

Generate an ed25519 keypair locally and register it as a delegate — see Delegate Accounts for the full setup. The delegate key can place orders and deposit on your behalf, but cannot withdraw. You can revoke it at any time from the webapp.

4. Start trading

With a signing key in hand, you can build and submit transactions. See Transaction Signing for the full signing flow.

Query your account state at any time — no authentication needed for read endpoints:

GET /fapi/v3/account?address=<your_main_address>

Note: When using a delegate key, use your main account’s address (not the delegate’s) for all read endpoints. Balances, positions, and orders live on the main account — the delegate key is only used for signing transactions.

Trades made via the API with a delegate key are visible in the webapp UI.

Delegate Accounts

A delegate is a separate keypair authorized to trade on behalf of your main account. Orders placed by a delegate are attributed to the delegator’s account — they share the same balances, positions, and margin.

Why use delegates

  • Security: your main wallet key stays in your wallet; the delegate key is the only secret you handle - if the delegate key is compromised, your main wallet remains untouched
  • Scoped access: delegates can only trade, but cannot deposit or withdraw
  • Revocable: delegate access can be removed anytime by your main account
  • Visibility: all trades made by delegates appear in the webapp UI under your account

Setup

1. Generate a keypair

Generate an ed25519 keypair locally. This will be your delegate key.

Solana CLI (easiest):

solana-keygen new --outfile delegate-key.json --no-bip39-passphrase
solana-keygen pubkey delegate-key.json

The generated file contains the 64-byte secret key (first 32 bytes are the private key, last 32 are the public key). The printed public key (base58) is what you register in the webapp.

Rust:

#![allow(unused)]
fn main() {
use ed25519_dalek::SigningKey;

let keypair = SigningKey::generate(&mut rand::thread_rng());
let public_key = hex::encode(keypair.verifying_key().to_bytes());
let private_key = hex::encode(keypair.to_bytes());

println!("public key:  {public_key}");   // register this in the webapp
println!("private key: {private_key}");   // use this to sign API transactions
}

Python:

from nacl.signing import SigningKey

keypair = SigningKey.generate()
public_key = keypair.verify_key.encode().hex()
private_key = keypair.encode().hex()

print(f"public key:  {public_key}")   # register this in the webapp
print(f"private key: {private_key}")   # use this to sign API transactions

Store the private key securely. You will need the public key for the next step.

2. Register the delegate in the webapp

  1. Sign in at app.bullet.xyz
  2. Navigate to More → Delegate Accounts
  3. Enter a name and your delegate’s public key
  4. Confirm — this signs a delegation transaction with your main wallet

3. Trade with the delegate key

Sign transactions with the delegate’s private key. The exchange resolves the delegate to your main account automatically — no special fields or flags needed.

#![allow(unused)]
fn main() {
// sign with the delegate keypair — order executes on the main account
let order_bytes = create_place_order_bytes(
    &delegate_keypair,
    chain_id,
    &chain_hash,
    market_id,
    orders,
);
}

See Transaction Signing for the full signing flow.

Note: For read endpoints (account info, balances, positions, open orders), always use your main account’s address — not the delegate’s. All state lives on the main account.

Capabilities

OperationDelegateMain wallet
Place / cancel ordersYesYes
DepositYesYes
WithdrawNoYes

Revoking a delegate

From the webapp, go to More → Delegate Accounts and remove the delegate. Revocation is immediate — the delegate key can no longer submit transactions for your account.

Limits

  • Up to 10 delegates per account
  • Delegate names are max 20 characters
  • A delegate address cannot already have its own account
  • A delegate cannot be registered to multiple accounts

Transaction Signing

All write operations (placing orders, cancelling, deposits) are submitted as signed transactions to the Bullet exchange. This page describes how to build, sign, and submit a transaction.

This will be codified into official SDKs in the near future, but for now you can use the following Rust code as a reference implementation. The same principles apply in any language: build the call message, wrap in an unsigned transaction, sign, wrap in a signed transaction, and submit.

Overview

1. Build call message           (e.g. PlaceOrdersCall, CancelAllOrders)
2. Wrap in UnsignedTransaction  (call + uniqueness + tx details)
3. Sign                         borsh(unsigned_tx) ++ chain_hash → ed25519 sign
4. Wrap in SignedTransaction     (signature + pubkey + unsigned_tx)
5. Submit                       borsh(signed_tx) → base64 → POST or WS

Prerequisites

Fetch exchange info once to obtain chain_id and chain_hash:

GET /fapi/v1/exchangeInfo

From the response, extract:

  • chainInfo.chainId — u64 chain identifier
  • chainHash — 32-byte hex string, decode to [u8; 32]

You also need an ed25519 keypair for signing.

Dependencies

[dependencies]
borsh = { version = "1", features = ["derive"] }
ed25519-dalek = { version = "2.1", features = ["rand_core"] }
rust_decimal = "1.37"
rand = "0.8"
base64 = "0.22"

Rust Types

All types are borsh (borsh.io) serialized. Enum discriminants and struct field ordering must match the schema exactly (GET /rollup/schema).

#![allow(unused)]
fn main() {
// ── borsh types ──
// enum discriminants and struct field order must match the rollup schema
// fetch canonical schema: GET /rollup/schema

#[derive(Clone, BorshSerialize)]
pub struct Amount(pub u128);

#[derive(Clone, BorshSerialize)]
pub struct TxDetails {
    pub max_priority_fee_bips: u64,
    pub max_fee: Amount,
    pub gas_limit: Option<[u64; 2]>,
    pub chain_id: u64,
}

#[derive(Clone, BorshSerialize)]
#[borsh(use_discriminant = true)]
#[repr(u8)]
pub enum UniquenessData {
    Nonce(u64) = 0,
    Generation(u64) = 1,
}

#[derive(Clone, Copy, BorshSerialize)]
pub struct MarketId(pub u16);

#[derive(Clone, Copy, BorshSerialize)]
#[borsh(use_discriminant = true)]
#[repr(u8)]
pub enum Side {
    Bid = 0,
    Ask = 1,
}

#[derive(Clone, Copy, BorshSerialize)]
#[borsh(use_discriminant = true)]
#[repr(u8)]
pub enum OrderType {
    Limit = 0,
    PostOnly = 1,
    FillOrKill = 2,
    ImmediateOrCancel = 3,
    PostOnlySlide = 4,
    PostOnlyFront = 5,
}

#[derive(Clone, Copy, BorshSerialize)]
pub struct ClientOrderId(pub u64);

#[derive(Clone, Copy, BorshSerialize)]
pub struct OrderId(pub u64);

/// rust_decimal::Decimal layout: { flags, hi, lo, mid } as u32
#[derive(Clone, Copy, BorshSerialize)]
pub struct SurrogateDecimal {
    pub flags: u32,
    pub hi: u32,
    pub lo: u32,
    pub mid: u32,
}

#[derive(Clone, BorshSerialize)]
pub struct NewOrderArgs {
    pub price: SurrogateDecimal,
    pub size: SurrogateDecimal,
    pub side: Side,
    pub order_type: OrderType,
    pub reduce_only: bool,
    pub client_order_id: Option<ClientOrderId>,
    pub pending_tpsl_pair: Option<PendingTpslPair>,
}

#[derive(Clone, Copy, BorshSerialize)]
#[borsh(use_discriminant = true)]
#[repr(u8)]
pub enum TriggerPriceCondition {
    Mark = 0,
    Oracle = 1,
    LastTrade = 2,
}

#[derive(Clone, BorshSerialize)]
pub struct TpslLeg {
    pub trigger_price: SurrogateDecimal,
    pub order_price: Option<SurrogateDecimal>,
    pub trigger_condition: TriggerPriceCondition,
}

#[derive(Clone, BorshSerialize)]
pub struct TpslPair {
    pub take_profit: Option<TpslLeg>,
    pub stop_loss: Option<TpslLeg>,
}

#[derive(Clone, BorshSerialize)]
pub struct PendingTpslPair {
    pub tpsl_pair: TpslPair,
    pub dynamic_size: bool,
}

#[derive(Clone, BorshSerialize)]
pub struct CancelOrderArgs {
    pub order_id: Option<OrderId>,
    pub client_order_id: Option<ClientOrderId>,
}

#[derive(Clone, BorshSerialize)]
pub struct AmendOrderArgs {
    pub cancel: CancelOrderArgs,
    pub place: NewOrderArgs,
}

#[derive(Clone, BorshSerialize)]
pub struct AmendOrdersCall {
    pub market_id: MarketId,
    pub orders: Vec<AmendOrderArgs>,
    pub sub_account_index: Option<u8>,
}

#[derive(Clone, BorshSerialize)]
pub struct PlaceOrdersCall {
    pub market_id: MarketId,
    pub orders: Vec<NewOrderArgs>,
    pub replace: bool,
    pub sub_account_index: Option<u8>,
}

#[derive(Clone, BorshSerialize)]
pub struct CancelOrdersCall {
    pub market_id: MarketId,
    pub orders: Vec<CancelOrderArgs>,
    pub sub_account_index: Option<u8>,
}

#[derive(Clone, BorshSerialize)]
#[borsh(use_discriminant = true)]
#[repr(u8)]
pub enum ExchangeUserAction {
    PlaceOrders(PlaceOrdersCall) = 20,
    AmendOrders(AmendOrdersCall) = 21,
    CancelOrders(CancelOrdersCall) = 22,
    CancelMarketOrders {
        market_id: MarketId,
        sub_account_index: Option<u8>,
    } = 23,
    CancelAllOrders {
        sub_account_index: Option<u8>,
    } = 29,
}

#[derive(Clone, BorshSerialize)]
#[borsh(use_discriminant = true)]
#[repr(u8)]
pub enum ExchangeCallMessage {
    User(ExchangeUserAction) = 0,
}

/// only Exchange variant needed; discriminant = 7 encodes correctly via repr(u8)
#[derive(Clone, BorshSerialize)]
#[borsh(use_discriminant = true)]
#[repr(u8)]
pub enum RuntimeCall {
    Exchange((ExchangeCallMessage,)) = 7,
}

#[derive(Clone, BorshSerialize)]
pub struct UnsignedTransaction {
    pub runtime_call: RuntimeCall,
    pub uniqueness: UniquenessData,
    pub details: TxDetails,
}

#[derive(Clone, BorshSerialize)]
pub struct TransactionV0 {
    pub signature: [u8; 64],
    pub pub_key: [u8; 32],
    pub tx: UnsignedTransaction,
}

#[derive(Clone, BorshSerialize)]
#[borsh(use_discriminant = true)]
#[repr(u8)]
pub enum SignedTransaction {
    V0(TransactionV0) = 0,
}
}

Helpers

#![allow(unused)]
fn main() {
use std::time::{SystemTime, UNIX_EPOCH};
use rust_decimal::Decimal;

/// parse a decimal string into rust_decimal::Decimal
pub fn parse_decimal(s: &str) -> Decimal {
    s.parse().expect("valid decimal string")
}

/// convert rust_decimal::Decimal to SurrogateDecimal using the public unpack API
pub fn to_surrogate(d: Decimal) -> SurrogateDecimal {
    let u = d.unpack();
    SurrogateDecimal {
        flags: (u.scale << 16) | if u.negative { 1 << 31 } else { 0 },
        hi: u.hi,
        lo: u.lo,
        mid: u.mid,
    }
}

pub fn timestamp_us() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system time after unix epoch")
        .as_micros() as u64
}

pub fn default_tx_details(chain_id: u64) -> TxDetails {
    TxDetails {
        max_priority_fee_bips: 0,
        // Adjust to your needs;
        max_fee: Amount(1 << 48),
        // None defaults to block limit; use Some([5_000_000, 5_000_000]) for explicit caps
        gas_limit: None,
        chain_id,
    }
}
}

Signing

#![allow(unused)]
fn main() {
use borsh::BorshSerialize;
use ed25519_dalek::{Signer, SigningKey};

pub fn sign_transaction(
    keypair: &SigningKey,
    chain_hash: &[u8; 32],
    unsigned: UnsignedTransaction,
) -> Vec<u8> {
    let mut msg = borsh::to_vec(&unsigned).expect("borsh serialize");
    msg.extend_from_slice(chain_hash);

    let signature: [u8; 64] = keypair.sign(&msg).to_bytes();
    let pub_key: [u8; 32] = keypair.verifying_key().to_bytes();

    let signed = SignedTransaction::V0(TransactionV0 {
        signature,
        pub_key,
        tx: unsigned,
    });

    borsh::to_vec(&signed).expect("borsh serialize signed tx")
}
}

Place Orders

#![allow(unused)]
fn main() {
/// build signed PlaceOrders transaction bytes
pub fn create_place_order_bytes(
    keypair: &SigningKey,
    chain_id: u64,
    chain_hash: &[u8; 32],
    market_id: u16,
    orders: Vec<NewOrderArgs>,
) -> Vec<u8> {
    let unsigned = UnsignedTransaction {
        runtime_call: RuntimeCall::Exchange((ExchangeCallMessage::User(
            ExchangeUserAction::PlaceOrders(PlaceOrdersCall {
                market_id: MarketId(market_id),
                orders,
                replace: false,
                sub_account_index: None,
            }),
        ),)),
        uniqueness: UniquenessData::Generation(timestamp_us()),
        details: default_tx_details(chain_id),
    };

    sign_transaction(keypair, chain_hash, unsigned)
}
}

Cancel Orders

#![allow(unused)]
fn main() {
/// build signed CancelOrders transaction bytes
pub fn create_cancel_order_bytes(
    keypair: &SigningKey,
    chain_id: u64,
    chain_hash: &[u8; 32],
    market_id: u16,
    orders: Vec<CancelOrderArgs>,
) -> Vec<u8> {
    let unsigned = UnsignedTransaction {
        runtime_call: RuntimeCall::Exchange((ExchangeCallMessage::User(
            ExchangeUserAction::CancelOrders(CancelOrdersCall {
                market_id: MarketId(market_id),
                orders,
                sub_account_index: None,
            }),
        ),)),
        uniqueness: UniquenessData::Generation(timestamp_us()),
        details: default_tx_details(chain_id),
    };

    sign_transaction(keypair, chain_hash, unsigned)
}
}

Cancel Market Orders

Cancels every open order on a given market. No order list needed — just the market_id.

#![allow(unused)]
fn main() {
/// build signed CancelMarketOrders transaction bytes (cancel all orders on one market)
pub fn create_cancel_market_orders_bytes(
    keypair: &SigningKey,
    chain_id: u64,
    chain_hash: &[u8; 32],
    market_id: u16,
) -> Vec<u8> {
    let unsigned = UnsignedTransaction {
        runtime_call: RuntimeCall::Exchange((ExchangeCallMessage::User(
            ExchangeUserAction::CancelMarketOrders {
                market_id: MarketId(market_id),
                sub_account_index: None,
            },
        ),)),
        uniqueness: UniquenessData::Generation(timestamp_us()),
        details: default_tx_details(chain_id),
    };

    sign_transaction(keypair, chain_hash, unsigned)
}
}

Cancel All Orders

Cancels every open order across all markets.

#![allow(unused)]
fn main() {
/// build signed CancelAllOrders transaction bytes (cancel all orders on all markets)
pub fn create_cancel_all_order_bytes(
    keypair: &SigningKey,
    chain_id: u64,
    chain_hash: &[u8; 32],
) -> Vec<u8> {
    let unsigned = UnsignedTransaction {
        runtime_call: RuntimeCall::Exchange((ExchangeCallMessage::User(
            ExchangeUserAction::CancelAllOrders {
                sub_account_index: None,
            },
        ),)),
        uniqueness: UniquenessData::Generation(timestamp_us()),
        details: default_tx_details(chain_id),
    };

    sign_transaction(keypair, chain_hash, unsigned)
}
}

Replace Orders

Replace uses PlaceOrders with replace: true. This atomically cancels all existing orders on the market and places the new set in one transaction. Useful for market makers who want to refresh their entire quote set.

#![allow(unused)]
fn main() {
/// build signed PlaceOrders transaction bytes with replace=true
///
/// replaces all existing orders on the market with the new set atomically
pub fn create_replace_order_bytes(
    keypair: &SigningKey,
    chain_id: u64,
    chain_hash: &[u8; 32],
    market_id: u16,
    orders: Vec<NewOrderArgs>,
) -> Vec<u8> {
    let unsigned = UnsignedTransaction {
        runtime_call: RuntimeCall::Exchange((ExchangeCallMessage::User(
            ExchangeUserAction::PlaceOrders(PlaceOrdersCall {
                market_id: MarketId(market_id),
                orders,
                replace: true,
                sub_account_index: None,
            }),
        ),)),
        uniqueness: UniquenessData::Generation(timestamp_us()),
        details: default_tx_details(chain_id),
    };

    sign_transaction(keypair, chain_hash, unsigned)
}
}

Amend Orders

Amend atomically cancels specific orders by ID and places new ones. Unlike replace, it targets individual orders rather than wiping the entire market. Each AmendOrderArg pairs a cancel (by order_id or client_order_id) with a new order placement.

#![allow(unused)]
fn main() {
/// build signed AmendOrders transaction bytes
///
/// atomically cancels specific orders and places new ones in a single transaction
pub fn create_amend_order_bytes(
    keypair: &SigningKey,
    chain_id: u64,
    chain_hash: &[u8; 32],
    market_id: u16,
    orders: Vec<AmendOrderArgs>,
) -> Vec<u8> {
    let unsigned = UnsignedTransaction {
        runtime_call: RuntimeCall::Exchange((ExchangeCallMessage::User(
            ExchangeUserAction::AmendOrders(AmendOrdersCall {
                market_id: MarketId(market_id),
                orders,
                sub_account_index: None,
            }),
        ),)),
        uniqueness: UniquenessData::Generation(timestamp_us()),
        details: default_tx_details(chain_id),
    };

    sign_transaction(keypair, chain_hash, unsigned)
}
}

Order Management Comparison

ScopeAtomicUse caseKeeps queue priority
Cancel + PlacePer orderNoSimple workflowsNo
AmendPer orderYesAdjusting price/sizeNo
ReplaceEntire marketYesRefreshing full quote setNo

Demo

fn main() {
    use base64::engine::general_purpose::STANDARD as BASE64;
    use base64::Engine;
    use ed25519_dalek::SigningKey;

    // generate a random keypair for demo
    let keypair = SigningKey::generate(&mut rand::thread_rng());
    let chain_id: u64 = 1;
    let chain_hash: [u8; 32] = [0u8; 32]; // replace with real chain_hash from exchangeInfo

    // place a limit buy
    let order = NewOrderArgs {
        price: to_surrogate(parse_decimal("50000.0")),
        size: to_surrogate(parse_decimal("0.001")),
        side: Side::Bid,
        order_type: OrderType::Limit,
        reduce_only: false,
        client_order_id: Some(ClientOrderId(1)),
        pending_tpsl_pair: None,
    };

    let place_bytes = create_place_order_bytes(&keypair, chain_id, &chain_hash, 0, vec![order]);
    println!(
        "place order tx ({} bytes): {}",
        place_bytes.len(),
        BASE64.encode(&place_bytes)
    );

    // cancel by order id
    let cancel = CancelOrderArgs {
        order_id: Some(OrderId(12345)),
        client_order_id: None,
    };

    let cancel_bytes = create_cancel_order_bytes(&keypair, chain_id, &chain_hash, 0, vec![cancel]);
    println!(
        "cancel order tx ({} bytes): {}",
        cancel_bytes.len(),
        BASE64.encode(&cancel_bytes)
    );

    // cancel all orders on market 0
    let cancel_market_bytes =
        create_cancel_market_orders_bytes(&keypair, chain_id, &chain_hash, 0);
    println!(
        "cancel market orders tx ({} bytes): {}",
        cancel_market_bytes.len(),
        BASE64.encode(&cancel_market_bytes)
    );

    // cancel all orders across all markets
    let cancel_all_bytes = create_cancel_all_order_bytes(&keypair, chain_id, &chain_hash);
    println!(
        "cancel all tx ({} bytes): {}",
        cancel_all_bytes.len(),
        BASE64.encode(&cancel_all_bytes)
    );

    // replace all orders on a market — cancels all existing, places new set atomically
    let replacement = NewOrderArgs {
        price: to_surrogate(parse_decimal("51000.0")),
        size: to_surrogate(parse_decimal("0.002")),
        side: Side::Bid,
        order_type: OrderType::Limit,
        reduce_only: false,
        client_order_id: Some(ClientOrderId(2)),
        pending_tpsl_pair: None,
    };

    let replace_bytes =
        create_replace_order_bytes(&keypair, chain_id, &chain_hash, 0, vec![replacement]);
    println!(
        "replace orders tx ({} bytes): {}",
        replace_bytes.len(),
        BASE64.encode(&replace_bytes)
    );

    // amend a specific order — cancel by id and place a new one atomically
    let amend = AmendOrderArgs {
        cancel: CancelOrderArgs {
            order_id: Some(OrderId(12345)),
            client_order_id: None,
        },
        place: NewOrderArgs {
            price: to_surrogate(parse_decimal("50500.0")),
            size: to_surrogate(parse_decimal("0.0015")),
            side: Side::Bid,
            order_type: OrderType::Limit,
            reduce_only: false,
            client_order_id: Some(ClientOrderId(3)),
            pending_tpsl_pair: None,
        },
    };

    let amend_bytes = create_amend_order_bytes(&keypair, chain_id, &chain_hash, 0, vec![amend]);
    println!(
        "amend orders tx ({} bytes): {}",
        amend_bytes.len(),
        BASE64.encode(&amend_bytes)
    );

    // submit either via:
    //   POST /tx/submit          { "body": "<base64>" }
    //   WS   order.place         { "method": "order.place",     "params": { "tx": "<base64>" }, "id": 1 }
    //   WS   order.cancel        { "method": "order.cancel",    "params": { "tx": "<base64>" }, "id": 2 }
    //   WS   order.cancelAll     { "method": "order.cancelAll", "params": { "tx": "<base64>" }, "id": 3 }
}

Submitting

Base64-encode the borsh-serialized bytes, then submit via REST or WebSocket:

POST /tx/submit
Content-Type: application/json

{ "body": "<base64>" }
{
  "method": "order.place",
  "params": {
    "tx": "<base64>"
  },
  "id": 1
}

Use order.cancel for CancelOrders and order.cancelAll for CancelAllOrders.

Transaction Fields

Every transaction submitted to the Bullet exchange is wrapped in an UnsignedTransaction containing three components: the call message, a uniqueness value, and transaction details. This guide covers the uniqueness and details fields.

Uniqueness

The UniquenessData field prevents transaction replay. Bullet supports two modes:

#![allow(unused)]
fn main() {
enum UniquenessData {
    Nonce(u64) = 0,
    Generation(u64) = 1,
}
}

The uniqueness value can be whatever you want — it just needs to be unique per user within a block window (~5-6 seconds). We recommend using microsecond timestamps.

  • No need to track or increment a counter
  • Multiple transactions can be in-flight concurrently

Best practice: use SystemTime::now() in microseconds. Avoid reusing values — if you submit multiple transactions in a tight loop, ensure each is distinct.

Nonce mode

Use Nonce(u64) for strict sequential ordering. The nonce must be exactly current_nonce + 1 for the credential. This mode is simpler but only allows one in-flight transaction at a time.

TxDetails

#![allow(unused)]
fn main() {
struct TxDetails {
    max_priority_fee_bips: u64,
    max_fee: Amount,
    gas_limit: Option<[u64; 2]>,
    chain_id: u64,
}
}

These fields are required in every transaction. We recommend using the values below.

FieldTypeDescription
chain_idu64Chain identifier — fetch from GET /fapi/v1/exchangeInfochainInfo.chainId
max_feeAmount(u128)Maximum fee the sender is willing to pay
gas_limitOption<[u64; 2]>2D gas limit: [compute, storage]. None defaults to block limit
max_priority_fee_bipsu64Priority fee tip in basis points

Gas model

Bullet uses a 2D gas model with separate compute and storage dimensions. The gas_limit field caps how much of each resource a transaction may consume.

Paymaster

Gas fees are covered by a paymaster — users do not pay gas out of pocket. The fields are still required for transaction validity.

FieldRecommended valueNotes
uniquenessGeneration(timestamp_us())Microsecond timestamp
max_priority_fee_bips0
max_fee1 << 48
gas_limitNoneDefaults to block limit. Use [5_000_000, 5_000_000] for order placement or [500_000, 500_000] for simpler ops if you want explicit caps
chain_idfrom exchangeInfoMust match the chain you are submitting to

See the default_tx_details helper for a ready-to-use implementation.

Decimal Encoding

The Bullet exchange uses rust_decimal::Decimal (referred to as RustDecimal) for prices and sizes. This page explains how to encode a fixed-point integer (i64 value + u8 scale) into the 128-bit decimal layout used for borsh serialization.

Layout

A RustDecimal has four u32 fields:

fieldbitsdescription
flags[31] sign, [23:16] scalesign bit and decimal exponent
hi[95:64] of mantissahigh 32 bits (zero when abs value fits in u64)
lo[31:0] of mantissalow 32 bits of the 96-bit mantissa
mid[63:32] of mantissamiddle 32 bits

The mantissa is an unsigned 96-bit integer. The represented value is:

(-1)^sign * mantissa / 10^scale

For borsh serialization the fields are ordered as: flags, hi, lo, mid.

Converting from fixed-point to RustDecimal

If your system represents values as i64 with a known scale (e.g. N * 10^-scale), the conversion is straightforward — the absolute value of the i64 is the mantissa, and the scale goes into the flags field.

C++

// borsh wire layout: flags, hi, lo, mid
struct RustDecimal {
    uint32_t flags;
    uint32_t hi;
    uint32_t lo;
    uint32_t mid;
};

/// convert a fixed-point i64 with the given scale to RustDecimal
/// e.g. from_fixed(123456789, 8) encodes 1.23456789
RustDecimal from_fixed(int64_t value, uint8_t scale) {
    bool negative = value < 0;
    uint64_t abs_value = negative ? -static_cast<uint64_t>(value)
                                  : static_cast<uint64_t>(value);

    return RustDecimal{
        .flags = (static_cast<uint32_t>(scale) << 16)
               | (negative ? 0x80000000u : 0u),
        .hi  = 0,
        .lo  = static_cast<uint32_t>(abs_value),
        .mid = static_cast<uint32_t>(abs_value >> 32),
    };
}

Python

# borsh wire layout: flags, hi, lo, mid
@dataclass
class RustDecimal:
    flags: int
    hi: int
    lo: int
    mid: int

def from_fixed(value: int, scale: int) -> RustDecimal:
    """convert a fixed-point int with the given scale to RustDecimal
    e.g. from_fixed(123456789, 8) encodes 1.23456789"""
    negative = value < 0
    abs_value = abs(value)

    flags = (scale << 16) | (0x80000000 if negative else 0)
    lo = abs_value & 0xFFFFFFFF
    mid = (abs_value >> 32) & 0xFFFFFFFF
    hi = 0

    return RustDecimal(flags, hi, lo, mid)

JavaScript

// convert a fixed-point bigint with the given scale to RustDecimal fields
// borsh wire layout: flags, hi, lo, mid
// e.g. fromFixed(123456789n, 8) encodes 1.23456789
function fromFixed(value, scale) {
    const negative = value < 0n;
    const absValue = negative ? -value : value;

    const flags = (scale << 16) | (negative ? 0x80000000 : 0);
    const lo = Number(absValue & 0xFFFFFFFFn);
    const mid = Number((absValue >> 32n) & 0xFFFFFFFFn);
    const hi = 0;

    return { flags: flags >>> 0, hi, lo, mid };
}

Rust

The rust_decimal crate handles this natively — parse from string or construct via Decimal::from_parts(lo, mid, hi, negative, scale).

Examples

inputscaledecimal valueflagshilomid
10000000081.000000000x000800000x000000000x05F5E1000x00000000
-500000008-0.500000000x800800000x000000000x02FAF0800x00000000
12345678901281234.567890120x000800000x000000000xBE991A140x0000001C

Consistency Guarantees

Market data streaming

When streaming market data via a websocket topic, the following guarantees are provided:

Snapshots:

When subscribing to a market data topic, the first message received will be a snapshot of the current state of the order book.

This can be recognized by the mt field which will be set to "s" (snapshot). Subsequent messages will have mt set to "u" (update).

Backend disconnections:

If the backend experiences connectivity issues with the exchange rollup/matching engine, on recovery a snapshot will be sent to ensure the client has a consistent view of the order book.

Ordering

Depth updates are published with sequence identifiers, which allows clients to detect if any updates were missed.

  • u -> ‘last update id’ - a monotonic sequence number incremented with each new update.
  • pu -> ‘previous update id’ - the u value from the previous message. On the first snapshot, pu is 0.

By comparing the pu of the current message with the u of the last received message, clients can determine if any updates were missed. If there is a gap in the sequence, unsubscribe and resubscribe to get a fresh snapshot of the order book to ensure consistency.

Periodic snapshots:

In addition to the initial snapshot, the server periodically sends full snapshots (approximately every few seconds). This acts as a self-healing mechanism — even if a client misses a delta update, the next periodic snapshot will bring its local state back in sync without needing to resubscribe.

Any message with "mt":"s" is a full snapshot and should replace the client’s local state entirely. Snapshots are sent on initial subscription, periodically, and on recovery from backend connectivity issues. After a snapshot, sequence numbering may reset — clients should not validate pu continuity across snapshots.

SDKs

Rust

github.com/bulletxyz/bullet-rust-sdk

Type-safe client generated from the trading API’s OpenAPI spec. Includes REST, WebSocket, transaction signing, and keypair management.

[dependencies]
bullet-rust-sdk = { git = "https://github.com/bulletxyz/bullet-rust-sdk.git" }

Features

  • auto-generated REST client from OpenAPI spec
  • WebSocket client with type-safe topic subscriptions
  • Ed25519 keypair generation and management
  • transaction building and signing
  • cross-platform (native + WASM)

Changelog

2026-03-18

REST API

  • add: GET /fapi/v1/allOrders — query order history (filled, cancelled, expired) by address, with optional symbol, time range, and orderId filters

2026-03-17

REST API

  • add: GET /fapi/v1/userTrades — query account fill history by address, with optional symbol and time range filters

2026-03-16

SDK

  • add: Rust SDK — type-safe client with REST, WebSocket, transaction signing, and keypair management

Documentation

  • add: connectivity page — environment hosts and endpoint URLs

2026-03-13

Documentation

  • add: transaction fields guide — documents all order fields, time-in-force options, and self-trade prevention modes
  • add: amend order example in transaction signing guide
  • add: comparison of amend vs cancel-replace order modification strategies

2026-03-12

REST API

  • fix: OpenAPI schema now uses correct unsigned integer types for IDs and quantities
  • fix: removed internal metrics endpoint from public OpenAPI spec

2026-03-12

Documentation

  • add: account setup guide — sign in, deposit, get a signing key, start trading
  • add: delegate accounts guide — generate a keypair, register via webapp, trade with scoped and revocable access

2026-03-10

Documentation

  • fix: decimal encoding guide — simplified examples to encode RustDecimal directly, removing redundant SurrogateDecimal wrapper (no wire format change)

2026-03-05

WebSocket

  • add: consistency guarantees page — documents snapshot delivery, backend reconnection behavior, and message ordering
  • change: idle timeout disconnect reason is now idle_timeout — server disconnects if no valid message is received within 60 seconds of connecting

2026-02-20

Documentation

  • add: CancelAllOrders and CancelMarketOrders transaction signing examples
  • add: decimal encoding guide with C++, Python, and JavaScript examples
  • add: guides landing page and sidebar section
  • fix: removed internal terminology from public docs

2026-02-19

Documentation

  • fix: OrderResult field table now matches actual response schema (results.tx_id, results.status, results.order_ids, results.client_order_ids)
  • fix: transaction signing guide uses correct camelCase field names (chainInfo.chainId, chainHash) matching REST API response
  • fix: ListSubscriptions and Error examples now use correct 16-digit microsecond timestamps
  • add: documented order.amend / order.modify WebSocket method
  • add: documented order.cancelAll WebSocket method
  • fix: topic tables now list all accepted aliases (SYMBOL@liquidations, !liquidations, !ticker, !markPrice, etc.)
  • fix: @liquidations is now the primary topic name, @forceOrder as alias
  • note: kline topics marked as coming soon
  • change: WebSocket method names default to lowercase (order.place, subscribe), uppercase accepted as alias

2026-02-18

Initial Release

  • WebSocket API specification with Binance FAPI-compatible format
    • connection lifecycle (status, keepalive, disconnect reasons)
    • subscription streams (subscribe, unsubscribe, list_subscriptions)
    • request-response operations (order.place, order.cancel)
    • market data streams (depthUpdate, aggTrade, bookTicker, markPrice, forceOrder, orderTradeUpdate)
    • topic format reference and error codes
  • Transaction signing guide with Rust reference implementation
    • borsh type definitions matching exchange schema
    • ed25519 signing flow
    • PlaceOrders and CancelOrders examples
    • REST and WebSocket submission
  • REST API docs (Swagger/OpenAPI) served at /docs/rest