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

New here? Start with the Getting Started guide.

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

Getting Started

Building with APIs

  1. Connectivity — environment URLs and endpoints
  2. Account Setup — create an account and register a delegate key
  3. Transaction Signing — build, sign, and submit your first order (includes a runnable Rust demo)
  4. Order Types and Fields — field reference for NewOrderArgs
  5. REST API — Binance FAPI-compatible endpoints for trading, account, and market data
  6. WebSocket API — stream market data and submit orders over WebSocket

Rust SDK

The fastest path is the Rust SDK — it handles signing, serialization, and WebSocket management out of the box. See the SDK’s examples/ directory for working code.

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

Timestamps: all timestamps in this API are microseconds since Unix epoch (16 digits, e.g. 1706745600000000). Binance uses milliseconds (13 digits). Divide by 1,000 if converting from Bullet to Binance format.


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. Most WebSocket libraries answer ping frames automatically. Anything the client sends that proves liveness resets the keepalive timeout: a pong frame, the client’s own ping frame, or an application-level ping message.

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

Building the tx payload: see Place Orders (or Replace Orders for atomic quote refresh) for construction, Order Types and Fields for NewOrderArgs reference, and Signing for the sign → encode → submit flow.

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

Building the tx payload: see Cancel Orders for construction and Signing for the sign → encode → submit flow.

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

Building the tx payload: see Amend Orders for construction and Signing for the sign → encode → submit flow.

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

Building the tx payload: see Cancel Market Orders / Cancel All Orders for construction and Signing for the sign → encode → submit flow.

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

Responses:


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 — see status values below- (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

Tx Status Values

StatusDescription
processedtransaction was executed by the sequencer — check order_ids for affected orders
publishedtransaction accepted and will be processed in a subsequent block
submittedtransaction received by the sequencer but not yet published
finalizedtransaction finalized on-chain
droppedtransaction was dropped (e.g. expired uniqueness, duplicate generation value)
unknownstatus could not be determined

Most successful order operations return processed. If you receive dropped, the order was not placed — inspect the error and retry with a fresh uniqueness value.


Market Data

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

DepthUpdate

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

{
  "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.

An update (mt: "u") describes how the top N levels of the subscribed depth changed since the previous message. A level that leaves the top N, including one pushed out by a better price, arrives with qty "0"; a level that enters the top N arrives with its full qty. Changes deeper in the book are not sent. Every depth receives each update, possibly with empty b and a, so its pu chain stays continuous.

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": "",
  "oi": 0,
  "mk": false,
  "ff": false,
  "lq": false,
  "fe": "0",
  "nf": "0",
  "fa": "",
  "co": 0,
  "sd": "",
  "ft": "",
  "z": "0",
  "Z": "0",
  "rs": "0"
}
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)
uastring?deprecated placeholder (""); address not exposed- (DEX-specific)
oiu64?deprecated placeholder (0)- (DEX-specific)
mkbool?deprecated placeholder (false)- (DEX-specific)
ffbool?deprecated placeholder (false)- (DEX-specific)
lqbool?deprecated placeholder (false)- (DEX-specific)
festring?deprecated placeholder ("0")- (DEX-specific)
nfstring?deprecated placeholder ("0"); no Binance equivalent- (DEX-specific)
fastring?deprecated placeholder ("")- (DEX-specific)
cou64?deprecated placeholder (0); client order id belongs on user order updates- (DEX-specific)
sdstring?deprecated placeholder (""); infer aggressor side from m- (DEX-specific)
ftstring?deprecated placeholder (""); fill type belongs on user order updates- (DEX-specific)
zstring?deprecated placeholder ("0"); cumulative fill data belongs on user order updatesz
Zstring?deprecated placeholder ("0"); cumulative fill data belongs on user order updatesZ
rsstring?deprecated placeholder ("0"); remaining size belongs on user order updates- (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)

Mark-price E is publication time in Unix microseconds. Oracle and funding updates can republish the same cached mark with a new E.

Two optional DEX fields identify the actual cached mark event:

FieldTypeDescription
mark_execution_timestamp_usu64?Mark event execution time in Unix microseconds
mark_event_numberstring?Exact UInt64 sequencer event number as a decimal string

These fields and th refer to the mark event, even when an oracle or funding update triggers publication. Startup REST snapshots and upstream events without a transaction hash or event number omit the new fields: their mark execution identity is unknown. Never reinterpret E or the client’s receive time as archived execution time.

To join recent mark history, subscribe and buffer live events before fetching history. Merge identified events by (th, mark_event_number) (history calls th txHash), using execution timestamps for ordering and second buckets. Repeated publications of the same mark add no observation. Take the greatest (timestampUs, eventNumber, txHash) per UTC second, comparing event numbers as integers, and retain the winning event’s actual execution timestamp. Do not shift history to publication time or overwrite a newer live tail with an older HTTP result. Unknown-identity snapshots may update a current-price label, but must not seed historical observations. Missing intervals remain gaps; do not forward-fill them for volatility. Late history can repair older buckets but must not reset the current live price or replay chart animations.

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)

Market Status

Topic: marketStatus, marketStatuses, !marketStatus, !marketStatus@arr

Every market’s trading status. On subscribe you get a full snapshot (mt: "s"); after that only what changed (mt: "u"). Each update message carries every change observed in one refresh, so a single message may list several markets. The status vocabulary is the same as the status field of GET /fapi/v1/exchangeInfo, and the feed covers the same symbols (perpetuals; spot markets are not reported).

{
  "e": "marketStatusUpdate",
  "E": 1706745600000000,
  "mt": "s",
  "m": [
    {
      "s": "BTC-USD",
      "i": 1,
      "st": "TRADING"
    },
    {
      "s": "ETH-USD",
      "i": 2,
      "st": "POST_ONLY"
    }
  ]
}
{
  "e": "marketStatusUpdate",
  "E": 1706745600000000,
  "mt": "u",
  "m": [
    {
      "s": "BTC-USD",
      "i": 1,
      "st": "HALT",
      "ps": "TRADING",
      "ch": "status"
    },
    {
      "s": "SOL-USD",
      "i": 3,
      "st": "POST_ONLY",
      "ch": "listed"
    },
    {
      "s": "OLD-USD",
      "i": 7,
      "st": "CLOSE",
      "ch": "delisted"
    }
  ]
}
FieldTypeDescription
estring"marketStatusUpdate"
Eu64event time (μs)
mtstring"s" snapshot (every market) or "u" update (deltas)
marraythe markets reported in this message
m[].sstringsymbol
m[].iu16market id
m[].ststringstatus, see below
m[].psstring?previous status; only on ch: "status"
m[].chstring?what changed; absent in a snapshot

Status values (st):

ValueDescription
TRADINGopen for trading
POST_ONLYorders may only be posted, not executed; cancels allowed
CANCEL_ONLYorders may only be cancelled
HALThalted; no trading
CLOSEbeing force-settled or settled, pending deletion

Change values (ch):

ValueDescription
listeda market that was not in the previous view
statusthe market’s reported state changed (see below)
delistedthe market is gone; st is the status it last had

ch: "status" covers any change to an existing market’s reported state: its status, its symbol, or both. ps is the previous status, so ps == st means only the symbol changed. Every entry carries the market’s current s and st in full, so applying the entry as-is converges either way.

Deltas are idempotent — applying one twice leaves you in the same state — so a client may safely replay the last message it received.

Notes:

  • The snapshot arrives once the server has synced the market registry. A client that subscribes before the very first sync receives nothing until it lands.
  • Most status changes are picked up from the rollup as they happen; if that event stream is interrupted, they still arrive on the registry’s periodic refresh.
  • One transition is only ever observed on that periodic refresh, because the rollup emits no event for it: a market moving to CLOSE during force-settle.

OrderUpdate

Topic: ADDRESS@user.orders, 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": "",
    "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.uastring?deprecated placeholder (""); address implicit from authenticated stream- (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:

{
  "e": "orderTradeUpdate",
  "E": 1706745600000000,
  "o": {
    "s": "BTC-USD",
    "i": 100001,
    "X": "FILLED",
    "x": "TRADE",
    "T": 1706745600000000,
    "th": "0xabc123def456",
    "ua": "",
    "S": "BUY",
    "ap": "50000",
    "l": "1.0",
    "L": "50000.00",
    "n": "0.025",
    "N": "USDC",
    "m": false,
    "isDegen": false,
    "t": 200001,
    "rp": "10.50",
    "ft": "o",
    "z": "1.0",
    "Z": "50000.00",
    "rs": "0"
  }
}
FieldTypeDescriptionBinance Equivalent
o.Sstringsideo.S
o.apstring?average priceo.ap
o.lstringlast filled qtyo.l
o.Lstringlast filled priceo.L
o.nstringcommissiono.n
o.Nstringcommission asseto.N
o.mboolis maker sideo.m
o.isDegenboolfill of a Degen position- (DEX-specific)
o.tu64trade ido.t
o.rpstringrealized PnLo.rp
o.ftstring?fill type (see fill type values)- (DEX-specific)
o.zstring?cumulative filled size for this ordero.z
o.Zstring?cumulative filled cost (size × price) for this ordero.Z
o.rsstring?remaining size on this order- (DEX-specific)

Fill type values (ft):

ValueDescription
oorderbook
lliquidation
bbackstop liquidation
aADL (auto-deleveraging)

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@depth100-orderbook 100 levels
SYMBOL@aggTradesymbol@aggTradetrades
SYMBOL@bookTickersymbol@bookTickerbest bid/offer
SYMBOL@markPricesymbol@markPricemark price + funding
SYMBOL@tickersymbol@ticker24hr ticker (coming soon)
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 EquivalentNote
tickers!ticker@arr, !ticker!ticker@arrcoming soon
markPrices!markPrice@arr, !markPrice!markPrice@arr
bookTickers!bookTicker, !bookTicker@arr!bookTicker
liquidations!liquidations, !forceOrder, forceOrders!forceOrder@arr
marketStatusmarketStatuses, !marketStatus-market status

Parse errors: InvalidFormat, UnknownTopic

User Data Topics

Bullet TopicAliasesBinance Equivalent
ADDRESS@user.ordersADDRESS@ORDER_TRADE_UPDATElistenKey stream

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

Note: Use your main account address, not the delegate address. Order updates for delegate trades are published under the main account.

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_addressdeprecated aggTrade placeholder
oiorder_iddeprecated aggTrade placeholder
titrade_idsequencer trade ID
coclient_order_idreal on order updates; deprecated aggTrade placeholder
ftfill_typeBullet fill provenance
rsremaining_sizeremaining order size
nfnet_feedeprecated aggTrade placeholder
ffis_full_filldeprecated aggTrade placeholder
lqis_liquidationdeprecated aggTrade placeholder

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
  • Order Types and Fields — order types, NewOrderArgs fields, and take-profit/stop-loss
  • 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

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.

For detailed instructions (including Ledger and cold wallet support), see the full account setup guide.

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 on your behalf, but cannot deposit or 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.

For full setup instructions, see the delegate account setup guide. This page covers API-specific usage.

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 — they 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

Register your delegate’s public key via the webapp. See the Bullet UI guide for full instructions including Ledger and cold wallet setup.

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_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
DepositNoYes
WithdrawNoYes

Revoking a delegate

Revoke a delegate from the webapp at any time - revocation is immediate and the delegate key can no longer submit transactions for your account. See the Bullet UI guide for details.

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.

The Rust SDK does this for you. The Rust code below is a reference implementation; the same steps apply in any language:

1. Build call message           (e.g. PlaceOrdersCall, CancelAllOrders)
2. Wrap in UnsignedTransaction  (call + uniqueness + tx details)
3. Sign                         borsh(SigningPayload::V0 { 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 the chain hash and available markets:

GET /fapi/v1/exchangeInfo

From the response, extract:

  • chainHash — 32-byte hex string, decode to [u8; 32] (strip 0x prefix if present). It is both the signing domain separator and the source of TxDetails.chain_hash_fragment (its first 8 bytes as a little-endian u64); chainInfo.chainId is no longer part of a current-format transaction (only the pre-fork encoding accepted during the transition window below still carries it)
  • symbols — array of available markets; each entry contains marketId (u16) and symbol (e.g. "BTC-USD")

Use the symbols array to map between human-readable symbol names and the market_id values required by all order calls.

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.23"

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() {
// 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]>,
    /// first 8 bytes of the chain hash as a little-endian u64; see `chain_hash_fragment`
    pub chain_hash_fragment: u64,
}

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

#[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,
    /// account to execute as; `None` executes as the signer's own address
    pub address_override: Option<[u8; 32]>,
}

#[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,
}

/// what gets signed: the unsigned transaction plus the chain hash, behind a version byte
#[derive(Clone, BorshSerialize)]
pub struct SigningPayloadV0 {
    pub runtime_call: RuntimeCall,
    pub uniqueness: UniquenessData,
    pub details: TxDetails,
    pub address_override: Option<[u8; 32]>,
    pub chain_hash: [u8; 32],
}

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

For field reference (order types, TP/SL, sub-account index), see Order Types and Fields.

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_ms() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system time after unix epoch")
        .as_millis() as u64
}

/// the chain hash fragment every transaction commits to: the first 8 bytes of the
/// chain hash read as a little-endian u64
pub fn chain_hash_fragment(chain_hash: &[u8; 32]) -> u64 {
    u64::from_le_bytes(chain_hash[..8].try_into().expect("8 bytes"))
}

pub fn default_tx_details(chain_hash: &[u8; 32]) -> 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_hash_fragment: chain_hash_fragment(chain_hash),
    }
}
}

Signing

The signed bytes are the borsh-serialized SigningPayload::V0: a version byte (0), the unsigned transaction (call, uniqueness, details, address_override), and the 32-byte chain hash as a domain separator. The wire format wraps the same unsigned transaction with the signature and public key.

#![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> {
    // the signed bytes are the borsh-serialized signing payload: a version byte (0),
    // the unsigned transaction, then the chain hash as a domain separator
    let payload = SigningPayload::V0(SigningPayloadV0 {
        runtime_call: unsigned.runtime_call.clone(),
        uniqueness: unsigned.uniqueness.clone(),
        details: unsigned.details.clone(),
        address_override: unsigned.address_override,
        chain_hash: *chain_hash,
    });
    let msg = borsh::to_vec(&payload).expect("borsh serialize");

    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")
}
}

Transition from the previous format. Before the accounts hard fork, transactions carried chain_id in TxDetails, had no address_override, and were signed over borsh(unsigned_tx) ++ chain_hash with no version byte. Each environment can keep accepting that encoding for single-signer transactions below a configured cutoff height (ACCEPT_LEGACY_V0_TXS_UNTIL_HEIGHT); the cutoff ships disabled and is announced with the upgrade schedule, and above it only the format on this page is accepted. Switch as soon as you can.

Place Orders

#![allow(unused)]
fn main() {
/// build signed PlaceOrders transaction bytes
pub fn create_place_order_bytes(
    keypair: &SigningKey,
    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::Window(timestamp_ms()),
        details: default_tx_details(chain_hash),
        address_override: None,
    };

    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_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::Window(timestamp_ms()),
        details: default_tx_details(chain_hash),
        address_override: None,
    };

    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_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::Window(timestamp_ms()),
        details: default_tx_details(chain_hash),
        address_override: None,
    };

    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_hash: &[u8; 32]) -> Vec<u8> {
    let unsigned = UnsignedTransaction {
        runtime_call: RuntimeCall::Exchange((ExchangeCallMessage::User(
            ExchangeUserAction::CancelAllOrders {
                sub_account_index: None,
            },
        ),)),
        uniqueness: UniquenessData::Window(timestamp_ms()),
        details: default_tx_details(chain_hash),
        address_override: None,
    };

    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_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::Window(timestamp_ms()),
        details: default_tx_details(chain_hash),
        address_override: None,
    };

    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_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::Window(timestamp_ms()),
        details: default_tx_details(chain_hash),
        address_override: None,
    };

    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;
    use base64::engine::general_purpose::STANDARD as BASE64;
    use ed25519_dalek::SigningKey;

    // generate a random keypair for demo
    let keypair = SigningKey::generate(&mut rand::thread_rng());
    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_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_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_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_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_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_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:

REST:

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

{ "body": "<base64>" }

A DegenTrade call message also has its own route, POST /tx/submitDegen, with the same body and the same response. It accepts nothing else.

WebSocket:

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

tx is the base64-encoded borsh-serialized SignedTransaction bytes from the signing step above.

Transaction typeWebSocket method
PlaceOrdersorder.place
PlaceOrders (replace)order.place
CancelOrdersorder.cancel
CancelMarketOrdersorder.cancelAll
CancelAllOrdersorder.cancelAll
AmendOrdersorder.amend

Order Types and Fields

Reference for the types and fields used when building order transactions. For the signing and submission flow, see Transaction Signing. For decimal encoding, see Decimal Encoding.

Order Types

VariantBehavior
Limitstandard limit order, rests on the book until filled or cancelled
PostOnlyrejected if it would immediately match (guarantees maker execution)
FillOrKillmust fill entirely in one match or the entire order is cancelled
ImmediateOrCancelfills as much as possible immediately, cancels any remaining size
PostOnlySlideif the limit price would cross, slides to the best non-crossing price instead of rejecting
PostOnlyFrontposts at the front of the queue at the best price that doesn’t cross

NewOrderArgs

FieldTypeDescription
priceSurrogateDecimalorder price — see Decimal Encoding
sizeSurrogateDecimalorder size
sideSideBid (buy) or Ask (sell)
order_typeOrderTypeone of the order types above
reduce_onlyboolif true, can only reduce an existing position — rejected if it would open or increase one
client_order_idOption<ClientOrderId>optional user-defined u64 identifier; returned in order updates and usable for cancels
pending_tpsl_pairOption<PendingTpslPair>optional take-profit / stop-loss pair attached to the order

Take-Profit / Stop-Loss (TP/SL)

A PendingTpslPair attaches conditional exit orders to a placement. Each leg is optional:

FieldTypeDescription
take_profitOption<TpslLeg>triggers when price reaches the target
stop_lossOption<TpslLeg>triggers when price hits the stop level
dynamic_sizeboolif true, the TP/SL size tracks the current position size at trigger time

Each TpslLeg contains:

FieldTypeDescription
trigger_priceSurrogateDecimalprice at which the leg activates
order_priceOption<SurrogateDecimal>limit price for the triggered order; None for market execution
trigger_conditionTriggerPriceConditionMark (mark price), Oracle (oracle price), or LastTrade

Sub-Account Index

Most call structs include a sub_account_index: Option<u8> field. None acts on the main account (for a delegate key, the delegator’s main account); Some(i) acts on that account’s sub-account i.

Transaction Fields

Every transaction submitted to the Bullet exchange is wrapped in an UnsignedTransaction containing the call message, a uniqueness value, transaction details, and an optional address_override. This guide covers the uniqueness, details, and address override fields.

Uniqueness

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

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

The uniqueness value can be whatever you want — it just needs to be unique per key within an increasing block window of 1000 nonces. We recommend using millisecond timestamps.

Compared to the strictly monotonic Nonce variant multiple transactions can be in-flight concurrently.

Best practice: use SystemTime::now() in milliseconds. 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_hash_fragment: u64,
}
}

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

FieldTypeDescription
chain_hash_fragmentu64First 8 bytes of the chain hash (GET /fapi/v1/exchangeInfo → chainHash) as a little-endian u64; see chain_hash_fragment
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

The fragment lets the exchange pick the chain hash a transaction was signed over when more than one is valid during a chain-hash rotation. It replaces the former chain_id field.

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
uniquenessWindow(timestamp_ms())Millisecond 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_hash_fragmentchain_hash_fragment(&chain_hash)Derived from the chainHash you sign with
address_overrideNoneExecute as your own address. Only set it to act on another account that explicitly authorized your key

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

Address override

UnsignedTransaction.address_override is an Option<[u8; 32]> and is part of the signed bytes. Leave it None: the transaction then executes as the account the exchange resolves for the signing credential, which is the key’s own account or, for a registered delegate key, the delegator’s main account (see Delegate Accounts; delegation needs no override). Some(address) executes as that account instead and is only admitted when the account has explicitly authorized the signing credential; otherwise the transaction is skipped before execution.

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" }

Changelog

2026-10-20

REST API

  • add: GET /fapi/v1/userTransfers accepts an optional asset name and comma-separated action values, e.g. asset=USDC&action=DEPOSIT,WITHDRAWAL. Filters apply before pagination and persist in the cursor; omit asset for all assets
  • fix: GET /fapi/v1/userTransfers now matches the action filter against the stored movement kind. Previously, filtering by action always returned an empty page
  • change: the generated SDK userTransfers / user_transfers method gains an asset argument after address, shifting the positional endTime, limit, and startTime arguments. Pass null / undefined in WASM or None in Rust to keep querying all assets
  • add: POST /tx/submitDegen — the route for a User/DegenTrade call message, which is all it accepts. Same request envelope and response as POST /tx/submit, which still carries DegenTrade as well for now

2026-10-19

REST API

  • add: tradingCredits on each GET /api/v1/balance iso item: trading credits funding an isolated position on a cross-margin market. Moving cross margin into the position spends credits before USDC; any remaining credits return to cross when the position closes, or on a transfer back to cross once it has no position or open orders (a transfer with no USDC to move then ends there, with no Transfer event). A transfer to any other destination moves USDC only. They count as margin but are never withdrawable

2026-10-16

REST API

  • change: POST /tx/submit and POST /api/v1/solanaOffchainTx now relay only User, Vault and Public exchange call messages, plus Bank/TransferWithMemo and Warp/TransferRemote. Keeper and Admin call messages are rejected with 403 before reaching the sequencer and must be submitted to the sequencer directly

WebSocket

  • change: order.place applies the same call-message policy; a Keeper or Admin call message fails with error code -1020 (Unsupported operation)

2026-10-09

REST API

  • add: isDegen on every GET /fapi/v1/userTrades item, plus an optional isDegen=true|false query filter. Only the Degen user’s fill is true; ordinary counterparties in the same transaction and TWAP fills remain false

WebSocket

  • add: o.isDegen on orderTradeUpdate TRADE messages: true for the Degen user’s fills of a DegenTrade open or close and of the liquidation that sweeps the position; false for the counterparties and every other fill

2026-10-03

REST API

  • change: GET /fapi/v1/depth — limit maximum raised from 25 to 1000 (default still 20). Larger values are still clamped to the maximum rather than rejected

WebSocket

  • add: SYMBOL@depth100 — orderbook depth stream with 100 levels per side, same message format as the other depth topics
  • fix: depth updates now describe only the top N levels of the subscribed depth. Previously an update could carry changes from deeper in the book, or drop changes when more than N levels changed at once. A level leaving the top N now arrives with qty "0"; a level entering it arrives with its full qty
  • fix: the snapshot sent on subscribing to a depth topic now chains into the first update (its u equals that update’s pu). Previously, when the book had not changed since the last update, the snapshot carried the next, not-yet-published update id, so the first update looked like a sequence gap. GET /fapi/v1/depth’s lastUpdateId had the same off-by-one and now matches the stream

2026-09-29

REST API

  • remove: GET /api/v1/account, /api/v1/accounts, /api/v1/balance — account.unrealizedLossBorrow is no longer returned. The synthetic unrealized-loss borrow it reported was retired and the value was 0 for every account at the time of removal. Clients reading the field should drop it

2026-09-28

Transactions

  • change: transaction format for the accounts hard fork. TxDetails.chain_id is replaced by chain_hash_fragment (the first 8 bytes of chainHash as a little-endian u64; a decimal string in the Solana offchain JSON), UnsignedTransaction gains an optional address_override (send None), signatures cover the borsh-serialized SigningPayload::V0 (version byte, unsigned transaction, chain hash) instead of borsh(unsigned_tx) ++ chain_hash, and the Solana offchain JSON payload carries "version": 0. See Transaction Signing and Transaction Fields
  • note: each environment may keep accepting the previous encoding for single-signer transactions below a configured cutoff height announced with the upgrade schedule; above it the previous encoding is rejected. chainInfo.chainId remains in exchangeInfo but is no longer used in transactions

2026-09-24

REST API

  • add: accountVariant.kind may now be sub_vault on GET /api/v1/account, /api/v1/accounts, /api/v1/balance — one of a vault’s sub-accounts. For this kind accountVariant.master is the vault’s address and accountVariant.leader the vault leader’s. Clients branching on kind must accept the new value

2026-09-18

REST API

  • change: GET /fapi/v1/fundingRate — now returns funding rate history as on Binance: one row per hourly settlement with symbol, fundingRate (the dampened 8h-basis rate) and fundingTime (hour start, Unix microseconds), oldest first; markPrice is no longer returned. Optional symbol, startTime / endTime (inclusive Unix microseconds) and limit (default 100, max 1000); without startTime the most recent rows are returned. At most 2 * limit hours are searched from the given bound, so a gap in the snapshot feed shortens the page rather than reaching further back. Previously returned the live rate per symbol — clients wanting the live or estimated rate, or the mark price, should use GET /fapi/v1/premiumIndex. Responds 503 where historical data is not configured; Cache-Control: public, max-age=60

2026-09-10

REST API

  • add: GET /api/v1/oraclePrices — asset oracle prices, including collateral assets. Each row contains assetId, asset, oraclePrice (a decimal string), and nullable lastUpdateTimestamp (Unix microseconds). Optional assetId filters the result; the response is always an array sorted by asset ID. An explicit asset lookup returns 404 if the asset or its oracle price is unavailable; an unfiltered request omits assets without an oracle price. Responses use Cache-Control: public, max-age=1

2026-09-14

REST API

  • add: GET /fapi/v1/userTransfers — ledger of every value movement touching an address, newest first: wallet deposits and withdrawals, moves between cross, spot and isolated buckets, sends to other accounts or wallets, vault deposits and withdrawals, and Hyperlane bridge arrivals and departures. Each entry carries action, both from and to sides (address, bucket, symbol for isolated markets, vaultName for vaults), amount, fee, shares on vault movements and memo. Same address / startTime / endTime / limit / cursor parameters as userDeposits, plus an optional action filter
  • note: bridge and wallet-to-wallet entries have no on-chain execution time, so their time is the indexing time, typically a few seconds later. Bucket context (from / to) on deposits is only available from 2026-08-13; earlier deposits show as wallet to cross, spot or isolated

2026-09-11

REST API

  • change: GET /fapi/v1/userLiquidations — now returns orderbook, ADL, backstop and borrow-lend liquidations (previously backstop only); new liquidationType field (orderbook, backstop, adl, borrow_lend). markPrice renamed to price. liquidatorAddress, side, realizedPnl, liquidatorReward are nullable; symbol and marketId are null for borrow_lend
  • add: protocolReward, fee, orderId, tradeId, isMaker on GET /fapi/v1/userLiquidations — null where not applicable to the liquidationType
  • add: borrowLend object on GET /fapi/v1/userLiquidations with liabilityAsset, liabilityAssetId, collateralAsset, collateralAssetId, collateralSize; present only for liquidationType: borrow_lend, null otherwise

2026-09-03

REST API

  • change: GET /fapi/v3/account — marginDiscount can now also be "SUPER_LP", "ASSET_WEIGHT_LP" or "ASSET_WEIGHT_SUPER_LP" in addition to "NONE" and "LP". Clients branching on the old two values must accept the new ones. SUPER_LP accounts post no initial margin on open orders, only maintenance margin on the open position; ASSET_WEIGHT_* accounts value all collateral at full asset weight
  • fix: GET /fapi/v3/account — maxWithdrawAmount now reflects the account’s asset weight discount, and per-position initialMargin reflects the Super LP discount (no initial margin on open orders). totalMarginBalance and availableBalance are unweighted and unchanged

2026-08-05

REST API

  • change: GET /api/v1/account, /api/v1/accounts, /api/v1/balance — accountVariant.kind for a sub-account is now sub_v1 or legacy_sub instead of sub, and for a vault vault_v1 or protocol_vault_v1 instead of vault / protocol_vault. Clients branching on "sub", "vault" or "protocol_vault" must accept the new values. The unversioned kinds are transitional — they cover accounts that have not yet recorded their master or leader on-chain — and disappear once every account has been migrated
  • add: accountVariant.master on GET /api/v1/account, /api/v1/accounts, /api/v1/balance — the sub-account’s master address. Only present when kind is sub_v1; null otherwise
  • add: accountVariant.leader on the same endpoints — the vault’s leader address. Only present when kind is vault_v1 or protocol_vault_v1; null otherwise

Transactions

  • add: UserAction::SetAccountGroup { address, group, sub_account_index } — sets an account’s self-trade group. Accounts sharing a group are treated as one entity and cannot trade against each other. address: null targets the sender’s own account with sub_account_index applied; naming an address ignores sub_account_index and is permitted for the sender’s own account, a sub-account whose recorded master is the sender, a vault the sender leads, or any account if the sender is the Protocol admin. Delegates may not set a group. Setting the group to the account’s own address restores the ungrouped default
  • note: a master and its sub-accounts, two sub-accounts of one master, and a vault and its leader can never trade against each other regardless of their groups — that relationship is recorded on-chain and is not configurable

2026-07-23

REST API

  • add: maxUtilizationRate field on GET /fapi/v1/borrowLendPools — the pool’s hard cap on borrow utilization; borrows that would push utilization above this are rejected
  • change: GET /fapi/v1/borrowLendPools — rateParams.optimal_utilisation_rate renamed to rateParams.optimal_utilization_rate (American spelling, matching the exchange). Update clients reading the old key

2026-07-03

REST API

  • add: marginDiscount field on GET /fapi/v3/account — user’s margin discount ("NONE" or "LP" for liquidity-provider discount)
  • fix: GET /fapi/v3/account — per-position initialMargin now uses the account’s margin discount (LP users previously showed standard initial margin on each position row)

2026-05-23

WebSocket

  • change: @aggTrade — ua, oi, mk, ff, lq, fe, nf, fa, co, sd, ft, z, Z, rs are now deprecated empty/zero placeholders; treat the values as meaningless and use @user.orders for real fill data
  • add: o.ap (average price), o.N (commission asset), o.rp (realized PnL) on @user.orders TRADE fills
  • change: @user.orders — o.ua is now a deprecated empty-string placeholder; the address is implicit from the authenticated stream

2026-05-19

REST API

  • add: feeTier and feeDiscountBps fields on GET /fapi/v1/account — user’s fee tier (0–9) and additional per-user fee discount in basis points

2026-05-15

REST API

  • add: GET /api/v1/balance — Bullet account balance with per-asset gross assetAmount/liabilityAmount, account-level tradingCredits and unrealizedLossBorrow, and referral/liquidity rewards
  • fix: GET /fapi/v3/balance — maxWithdrawAmount now uses the configured USDC withdraw fee instead of zero

2026-05-14

REST API

  • add: GET /fapi/v1/premiumIndex — mark price, index price, last funding rate (8h), interest rate, estimated next funding rate (1h), and next funding time per symbol. Optional symbol query param; omit to return all markets

2026-05-13

REST API

  • add: GET /fapi/v1/openOrders — user TP/SL orders are now returned alongside regular limit orders, matching Binance’s response shape. New rows carry orderType of STOP or TAKE_PROFIT, populated stopPrice, and workingType (MARK_PRICE or CONTRACT_PRICE)
  • add: isTrigger boolean field on every open-order response row. TP/SL and regular orders draw from independent ID counters in the rollup, so clients should use (orderId, isTrigger) as the composite key
  • fix: GET /fapi/v1/openOrders — timeInForce, orderType, workingType, origType, priceMatch, selfTradePreventionMode no longer return the literal string "TODO"; populated with GTC / LIMIT / CONTRACT_PRICE / NONE
  • add: GET /fapi/v1/openOrders — timeInForce is now GTD and goodTillDate is populated when the order has an expiry timestamp
  • add: GET /fapi/v1/openOrder — new optional triggerOrderId query param to look up a specific user TP/SL order. Mutually exclusive with orderId / clientOrderId

WebSocket

  • add: o.m field on @user.orders TRADE fills — true if the fill was on the maker side, matching Binance ORDER_TRADE_UPDATE
  • add: o.t field on @user.orders TRADE fills — trade id, matching Binance ORDER_TRADE_UPDATE

2026-05-12

REST API

  • add: pendingTpsl field on GET /fapi/v1/openOrders and /fapi/v1/openOrder — reserved TP/SL pair attached to an unfilled parent limit order, with optional tp and sl legs (triggerOrderId, orderPrice, triggerPrice, priceCondition, orderType, timeInForce) and a dynamicSize flag. Omitted when the parent has no TP/SL reservation
  • change: stopPrice on /fapi/v1/openOrders and /fapi/v1/openOrder now derives from the SL leg trigger price when a pending TP/SL reservation exists

2026-05-10

REST API

  • fix: GET /fapi/v1/allOrders — resolved timeout errors for high-volume addresses; query now returns reliably regardless of order history size

2026-05-08

REST API

  • change: GET /fapi/v1/openOrders — symbol query parameter is now optional; omit it to return open orders across all markets
  • add: marketId field added to open order response objects
  • fix: OpenAPI schema for GET /fapi/v1/fundingRate corrected to return an array — was incorrectly documented as a single object
  • fix: OpenAPI schema for GET /fapi/v1/insuranceBalance corrected to return a single object — was incorrectly documented as an array

2026-05-07

REST API

  • fix: GET /fapi/v1/allOrders — avgPrice was incorrectly reported as a large negative number for orders where price × size > 170; all affected values are now correct

2026-05-04

REST API

  • fix: GET /fapi/v1/allOrders — unfilled IOC and FOK orders now correctly report status EXPIRED; previously they fell through to NEW or PARTIALLY_FILLED
  • fix: GET /fapi/v1/allOrders — if a fully-filled order also has a cancel record, status is now correctly FILLED rather than CANCELED

2026-05-01

Documentation

  • add: docs are now served as raw markdown at parallel .md URLs (e.g. /docs/getting-started.md) for programmatic access
  • add: llms.txt index served at /llms.txt listing all available REST and WebSocket API references

2026-04-30

REST API

  • add: GET /fapi/v1/fundingInfo — funding rate metadata per symbol: cap rate, floor rate, and funding interval. Accepts optional symbol query param; omit for all markets

2026-04-24

WebSocket

  • add: ft field on @aggTrade and @user.orders TRADE fills — fill type (o orderbook, l liquidation, b backstop liquidation, a ADL)
  • add: z, Z, rs fields on @aggTrade and @user.orders TRADE fills — cumulative filled size, cumulative filled cost, remaining size

2026-03-26

REST API

  • add: GET /fapi/v1/userLiquidations — query liquidation history by address, with optional symbol and time range filters
  • add: GET /fapi/v1/userDeposits — query deposit history by address, with time range filters
  • add: GET /fapi/v1/userWithdrawals — query withdrawal history by address, with time range filters

2026-03-25

Documentation

  • add: getting started page — step-by-step guide linking connectivity, account setup, signing, order fields, REST, and WebSocket docs
  • add: cross-links from WebSocket order methods to the corresponding transaction signing sections
  • add: tx status values table documenting processed, published, submitted, finalized, dropped, and unknown
  • fix: WebSocket table of contents structure and indentation

2026-03-24

REST API

  • add: GET /fapi/v1/openInterest — open interest per market
  • add: GET /fapi/v1/ticker/24hr — 24-hour rolling window price change statistics for all markets (price, volume, trade count)

2026-03-20

REST API

  • add: txHashes field to GET /fapi/v1/allOrders response — deduplicated list of tx hashes per order (place, fills, cancels)
  • add: address and txHash fields to GET /fapi/v1/userTrades response
  • fix: OpenAPI spec now uses Decimal types for prices and quantities instead of f64

Documentation

  • add: order types and fields page — reference for NewOrderArgs, order types, TP/SL, and sub-account index
  • note: @ticker and broadcast tickers topics marked as coming soon
  • add: user data stream note — use main account address, not delegate address

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
  • 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-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-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