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
- Account Setup — create an account and get a signing key
- Delegate Accounts — secure, revocable API trading access
- Transaction Signing — build, sign, and submit transactions to the Bullet exchange
- Order Types and Fields — order types, NewOrderArgs fields, take-profit/stop-loss
- Transaction Fields — uniqueness, TxDetails, gas limits, and paymaster
- Decimal Encoding — convert fixed-point integers to
RustDecimalfor borsh serialization - WebSocket Consistency Guarantees — snapshot delivery, reconnection behavior, and message ordering
SDKs
- Rust — type-safe client with REST, WebSocket, transaction signing, and keypair management
Getting Started
Building with APIs
- Connectivity — environment URLs and endpoints
- Account Setup — create an account and register a delegate key
- Transaction Signing — build, sign, and submit your first order (includes a runnable Rust demo)
- Order Types and Fields — field reference for
NewOrderArgs - REST API — Binance FAPI-compatible endpoints for trading, account, and market data
- 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
| Environment | Host |
|---|---|
| Mainnet | tradingapi.bullet.xyz |
| Testnet | tradingapi.testnet.bullet.xyz |
| Staging | tradingapi.staging.bullet.xyz |
Endpoints
| Protocol | URL | Docs |
|---|---|---|
| REST | https://<host>/fapi/v1/... | Interactive API docs |
| WebSocket | wss://<host>/ws | WebSocket 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
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "status" | - (no equivalent) |
| E | u64 | event time (μs) | - |
| status | string | "connected" or "disconnecting" | - |
| clientId | string | UUID client identifier | - |
| reason | string? | disconnect reason (only for disconnecting) | - |
Connection Establishment
- Client connects to WebSocket endpoint
- Server assigns unique
clientId - 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
| Reason | Default | Description |
|---|---|---|
idle_timeout | 60s | no valid message within timeout |
pong_timeout | 60s | client didn’t respond to ping |
max_duration | 24h | connection 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
}
| Field | Type | Required | Description | Binance Equivalent |
|---|---|---|---|---|
| method | string | yes | "ping" or "PING" | (WebSocket ping frame) |
| id | u64 | no | request correlation id | - |
Possible errors: ValidationError (message parse)
Responses: Pong
Pong
{
"e": "pong",
"id": 4,
"E": 1706745600000000
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "pong" | (WebSocket pong frame) |
| id | u64? | echoed request id | - |
| E | u64 | event 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"
]
}
| Field | Type | Required | Description | Binance Equivalent |
|---|---|---|---|---|
| method | string | yes | "subscribe" or "SUBSCRIBE" | method: "SUBSCRIBE" |
| params | string[] | yes | array of topic strings | params |
| id | u64 | no | request correlation id | id |
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"
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "subscribe" | - (Binance uses result: null) |
| id | u64? | echoed request id | id |
| E | u64 | event time (μs) | - |
| result | string | "success" | result: null |
Unsubscribe
Unsubscribe from market data topics or user data streams.
{
"method": "unsubscribe",
"id": 5,
"params": [
"BTC-USD@aggTrade"
]
}
| Field | Type | Required | Description | Binance Equivalent |
|---|---|---|---|---|
| method | string | yes | "unsubscribe" or "UNSUBSCRIBE" | method: "UNSUBSCRIBE" |
| params | string[] | yes | topics to unsubscribe | params |
| id | u64 | no | request correlation id | id |
Behavior: Idempotent - always succeeds. Invalid or non-subscribed topics silently skipped.
Possible errors: ValidationError (message parse)
Responses:
- Success: Unsubscribe
Unsubscribe (success)
{
"e": "unsubscribe",
"id": 5,
"E": 1706745600000000,
"result": "success"
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "unsubscribe" | - (Binance uses result: null) |
| id | u64? | echoed request id | id |
| E | u64 | event time (μs) | - |
| result | string | "success" | result: null |
ListSubscriptions
List all active subscriptions for the client.
{
"method": "list_subscriptions",
"id": 6
}
| Field | Type | Required | Description | Binance Equivalent |
|---|---|---|---|---|
| method | string | yes | "list_subscriptions" or "LIST_SUBSCRIPTIONS" | method: "LIST_SUBSCRIPTIONS" |
| id | u64 | no | request correlation id | id |
Possible errors: ValidationError (message parse)
Responses:
- Success: ListSubscriptions
ListSubscriptions (success)
{
"e": "list_subscriptions",
"id": 6,
"E": 1706745600000000,
"result": [
"BTC-USD@depth10",
"ETH-USD@aggTrade"
]
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "list_subscriptions" | - |
| id | u64? | echoed request id | id |
| E | u64 | event time (μs) | - |
| result | string[] | subscribed topics | result: […] |
Request-Response
OrderPlace
Place an order via WebSocket.
{
"method": "order.place",
"id": 10,
"params": {
"tx": "c2lnbmVkX3RyYW5zYWN0aW9uX2J5dGVz"
}
}
| Field | Type | Required | Description | Binance Equivalent |
|---|---|---|---|---|
| method | string | yes | "order.place" or "ORDER.PLACE" | method: "order.place" |
| params.tx | string | yes | signed base64-encoded borsh transaction bytes | (different: REST params) |
| id | u64 | no | request correlation id | id |
Building the
txpayload: see Place Orders (or Replace Orders for atomic quote refresh) for construction, Order Types and Fields forNewOrderArgsreference, and Signing for the sign → encode → submit flow.
Possible errors: ValidationError (empty tx), ServiceUnavailable (submission failed), Timeout,
NewOrderRejected
Responses:
- Success: OrderResult
- Failure: OrderError
OrderCancel
Cancel an order via WebSocket.
{
"method": "order.cancel",
"id": 11,
"params": {
"tx": "c2lnbmVkX3RyYW5zYWN0aW9uX2J5dGVz"
}
}
| Field | Type | Required | Description | Binance Equivalent |
|---|---|---|---|---|
| method | string | yes | "order.cancel" or "ORDER.CANCEL" | method: "order.cancel" |
| params.tx | string | yes | signed base64-encoded borsh transaction bytes | (different: orderId/clientOrderId) |
| id | u64 | no | request correlation id | id |
Building the
txpayload: see Cancel Orders for construction and Signing for the sign → encode → submit flow.
Possible errors: ValidationError (empty tx), ServiceUnavailable (submission failed), Timeout, CancelRejected
Responses:
- Success: OrderResult
- Failure: OrderError
OrderAmend
Amend an existing order via WebSocket.
{
"method": "order.amend",
"params": {
"tx": "<signed-base64-borsh-tx>"
},
"id": 7
}
| Field | Type | Required | Description | Binance Equivalent |
|---|---|---|---|---|
| method | string | yes | "order.amend", "order.modify", or "ORDER.AMEND" | - |
| params.tx | string | yes | signed base64-encoded borsh transaction bytes | - |
| id | u64 | no | request correlation id | id |
Building the
txpayload: see Amend Orders for construction and Signing for the sign → encode → submit flow.
Possible errors: ValidationError (empty tx), ServiceUnavailable (submission failed), Timeout, CancelRejected
Responses:
- Success: OrderResult
- Failure: OrderError
OrderCancelAll
Cancel all open orders for a market via WebSocket.
{
"method": "order.cancelAll",
"params": {
"tx": "<signed-base64-borsh-tx>"
},
"id": 8
}
| Field | Type | Required | Description | Binance Equivalent |
|---|---|---|---|---|
| method | string | yes | "order.cancelAll" or "ORDER.CANCEL_ALL" | - |
| params.tx | string | yes | signed base64-encoded borsh transaction bytes | - |
| id | u64 | no | request correlation id | id |
Building the
txpayload: 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:
- Success: OrderResult
- Failure: OrderError
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>"
}
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "error" | - |
| id | u64? | echoed request id | id |
| E | u64 | event time (μs) | - |
| error.code | i32 | error code | error.code |
| error.msg | string | error message | error.msg |
| error.param | string? | 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
]
}
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "order.place", "order.cancel", "order.amend", "order.cancelAll" | - |
| id | u64? | echoed request id | id |
| E | u64 | event time (μs) | - |
| results.tx_id | string | transaction hash | - (DEX-specific) |
| results.status | string | tx status — see status values below | - (DEX-specific) |
| results.order_ids | u64[] | affected order ids | - (DEX-specific) |
| results.client_order_ids | u64[] | 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"
}
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| id | u64? | echoed request id | id |
| E | u64 | event time (μs) | - |
| error.code | i32 | error code | error.code |
| error.msg | string | error message | error.msg |
Tx Status Values
| Status | Description |
|---|---|
processed | transaction was executed by the sequencer — check order_ids for affected orders |
published | transaction accepted and will be processed in a subsequent block |
submitted | transaction received by the sequencer but not yet published |
finalized | transaction finalized on-chain |
dropped | transaction was dropped (e.g. expired uniqueness, duplicate generation value) |
unknown | status 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"
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "depthUpdate" | e |
| E | u64 | event time (μs) | E |
| T | u64 | transaction time (μs) | T |
| s | string | symbol | s |
| U | u64 | first update id | U |
| u | u64 | last update id (always equals U) | u |
| pu | u64 | previous update id | pu |
| b | [[price, qty], …] | bids (descending) | b |
| a | [[price, qty], …] | asks (ascending) | a |
| mt | string | "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"
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "aggTrade" | e |
| E | u64 | event time (μs) | E |
| s | string | symbol | s |
| a | u64 | aggregate trade id | a |
| p | string | price | p |
| q | string | quantity | q |
| f | u64 | first trade id | f |
| l | u64 | last trade id | l |
| T | u64 | trade time (μs) | T |
| m | bool | is buyer maker | m |
| th | string | transaction hash | - (DEX-specific) |
| ua | string? | deprecated placeholder (""); address not exposed | - (DEX-specific) |
| oi | u64? | deprecated placeholder (0) | - (DEX-specific) |
| mk | bool? | deprecated placeholder (false) | - (DEX-specific) |
| ff | bool? | deprecated placeholder (false) | - (DEX-specific) |
| lq | bool? | deprecated placeholder (false) | - (DEX-specific) |
| fe | string? | deprecated placeholder ("0") | - (DEX-specific) |
| nf | string? | deprecated placeholder ("0"); no Binance equivalent | - (DEX-specific) |
| fa | string? | deprecated placeholder ("") | - (DEX-specific) |
| co | u64? | deprecated placeholder (0); client order id belongs on user order updates | - (DEX-specific) |
| sd | string? | deprecated placeholder (""); infer aggressor side from m | - (DEX-specific) |
| ft | string? | deprecated placeholder (""); fill type belongs on user order updates | - (DEX-specific) |
| z | string? | deprecated placeholder ("0"); cumulative fill data belongs on user order updates | z |
| Z | string? | deprecated placeholder ("0"); cumulative fill data belongs on user order updates | Z |
| rs | string? | 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"
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "bookTicker" | e |
| u | u64 | update id | u |
| E | u64 | event time (μs) | E |
| T | u64 | transaction time (μs) | T |
| s | string | symbol | s |
| b | string | best bid price | b |
| B | string | best bid qty | B |
| a | string | best ask price | a |
| A | string | best ask qty | A |
| mt | string | "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
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "markPriceUpdate" | e |
| E | u64 | event time (μs) | E |
| s | string | symbol | s |
| p | string | mark price | p |
| i | string | index price (median CEX price) | i |
| P | string? | estimated settle price | P |
| r | string | funding rate | r |
| T | u64? | next funding time | T |
| th | string? | 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:
| Field | Type | Description |
|---|---|---|
| mark_execution_timestamp_us | u64? | Mark event execution time in Unix microseconds |
| mark_event_number | string? | 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
}
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "liquidation" | e ("forceOrder") |
| E | u64 | event time (μs) | E |
| o.s | string | symbol | o.s |
| o.S | string | side ("BUY" or "SELL") | o.S |
| o.o | string | order type ("LIMIT") | o.o |
| o.f | string | time in force ("IOC") | o.f |
| o.q | string? | original quantity | o.q |
| o.z | string? | filled quantity | o.z |
| o.p | string | price | o.p |
| o.ap | string | average price | o.ap |
| o.X | string | status ("FILLED") | o.X |
| o.l | string | last filled qty | o.l |
| o.T | u64 | trade time (μs) | o.T |
| o.th | string | transaction hash | - (DEX-specific) |
| o.ua | string | liquidated address | - (DEX-specific) |
| o.oi | u64 | order id | - (DEX-specific) |
| o.ti | u64 | trade 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"
}
]
}
| Field | Type | Description |
|---|---|---|
| e | string | "marketStatusUpdate" |
| E | u64 | event time (μs) |
| mt | string | "s" snapshot (every market) or "u" update (deltas) |
| m | array | the markets reported in this message |
| m[].s | string | symbol |
| m[].i | u16 | market id |
| m[].st | string | status, see below |
| m[].ps | string? | previous status; only on ch: "status" |
| m[].ch | string? | what changed; absent in a snapshot |
Status values (st):
| Value | Description |
|---|---|
TRADING | open for trading |
POST_ONLY | orders may only be posted, not executed; cancels allowed |
CANCEL_ONLY | orders may only be cancelled |
HALT | halted; no trading |
CLOSE | being force-settled or settled, pending deletion |
Change values (ch):
| Value | Description |
|---|---|
listed | a market that was not in the previous view |
status | the market’s reported state changed (see below) |
delisted | the 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
CLOSEduring 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):
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| e | string | "orderTradeUpdate" | e ("ORDER_TRADE_UPDATE") |
| E | u64 | event time (μs) | E |
| o.s | string | symbol | o.s |
| o.i | u64 | order id | o.i |
| o.X | string | order status | o.X |
| o.x | string | execution type | o.x |
| o.T | u64 | transaction time (μs) | o.T |
| o.th | string | transaction hash | - (DEX-specific) |
| o.ua | string? | deprecated placeholder (""); address implicit from authenticated stream | - (DEX-specific) |
NEW order additional fields:
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| o.S | string | side | o.S |
| o.o | string | order type | o.o |
| o.f | string | time in force | o.f |
| o.p | string | price | o.p |
| o.q | string | quantity | o.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"
}
}
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| o.S | string | side | o.S |
| o.ap | string? | average price | o.ap |
| o.l | string | last filled qty | o.l |
| o.L | string | last filled price | o.L |
| o.n | string | commission | o.n |
| o.N | string | commission asset | o.N |
| o.m | bool | is maker side | o.m |
| o.isDegen | bool | fill of a Degen position | - (DEX-specific) |
| o.t | u64 | trade id | o.t |
| o.rp | string | realized PnL | o.rp |
| o.ft | string? | fill type (see fill type values) | - (DEX-specific) |
| o.z | string? | cumulative filled size for this order | o.z |
| o.Z | string? | cumulative filled cost (size × price) for this order | o.Z |
| o.rs | string? | remaining size on this order | - (DEX-specific) |
Fill type values (ft):
| Value | Description |
|---|---|
| o | orderbook |
| l | liquidation |
| b | backstop liquidation |
| a | ADL (auto-deleveraging) |
Topics
Symbol-Based Topics
| Bullet Topic | Aliases | Binance Equivalent | Description |
|---|---|---|---|
SYMBOL@depth | symbol@depth | orderbook (default 10) | |
SYMBOL@depth5 | symbol@depth5 | orderbook 5 levels | |
SYMBOL@depth10 | symbol@depth10 | orderbook 10 levels | |
SYMBOL@depth20 | symbol@depth20 | orderbook 20 levels | |
SYMBOL@depth100 | - | orderbook 100 levels | |
SYMBOL@aggTrade | symbol@aggTrade | trades | |
SYMBOL@bookTicker | symbol@bookTicker | best bid/offer | |
SYMBOL@markPrice | symbol@markPrice | mark price + funding | |
SYMBOL@ticker | symbol@ticker | 24hr ticker (coming soon) | |
SYMBOL@liquidations | SYMBOL@forceOrder | symbol@forceOrder | liquidations |
SYMBOL@kline_INTERVAL | symbol@kline_INTERVAL | candlesticks (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 Topic | Aliases | Binance Equivalent | Note |
|---|---|---|---|
tickers | !ticker@arr, !ticker | !ticker@arr | coming soon |
markPrices | !markPrice@arr, !markPrice | !markPrice@arr | |
bookTickers | !bookTicker, !bookTicker@arr | !bookTicker | |
liquidations | !liquidations, !forceOrder, forceOrders | !forceOrder@arr | |
marketStatus | marketStatuses, !marketStatus | - | market status |
Parse errors: InvalidFormat, UnknownTopic
User Data Topics
| Bullet Topic | Aliases | Binance Equivalent |
|---|---|---|
ADDRESS@user.orders | ADDRESS@ORDER_TRADE_UPDATE | listenKey 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
| Code | Name | Description | Binance Code |
|---|---|---|---|
| -1000 | Unknown | unknown error | -1000 |
| -1001 | Disconnected | server busy/disconnected | -1001 |
| -1002 | Unauthorized | authentication required | -1002 |
| -1003 | TooManyRequests | rate limit exceeded | -1003 |
| -1006 | UnexpectedResponse | unexpected response | -1006 |
| -1007 | Timeout | request timeout | -1007 |
| -1014 | UnknownOrder | order not found | -1014 |
| -1015 | TooManyOrders | order rate limit | -1015 |
| -1016 | ServiceUnavailable | service down | -1016 |
| -1020 | UnsupportedOperation | operation not supported | -1020 |
| -1021 | InvalidTimestamp | bad timestamp | -1021 |
| -1022 | InvalidSignature | signature invalid | -1022 |
Parameters
| Code | Name | Description | Binance Code |
|---|---|---|---|
| -1102 | MandatoryParamMissing | required param missing | -1102 |
| -1111 | BadPrecision | precision error | -1111 |
| -1116 | InvalidOrderType | bad order type | -1116 |
| -1117 | InvalidSide | bad side | -1117 |
| -1122 | InvalidSymbol | invalid symbol | -1122 |
| -1123 | InvalidUserAddress | invalid address | - (Bullet-specific) |
Subscriptions
| Code | Name | Description | Binance Code |
|---|---|---|---|
| -1004 | InvalidSubscriptionFormat | bad topic format | - (Bullet-specific) |
| -1005 | SymbolNotFound | symbol not found | - (Bullet-specific) |
| -1008 | ValidationError | validation failed | - (Bullet-specific) |
| -1010 | SubscriptionExists | already subscribed | - (Bullet-specific) |
Orders
| Code | Name | Description | Binance Code |
|---|---|---|---|
| -2010 | NewOrderRejected | order rejected | -2010 |
| -2011 | CancelRejected | cancel failed | -2011 |
| -2013 | NoSuchOrder | order doesn’t exist | -2013 |
| -2014 | ApiKeyFormatInvalid | bad api key | -2014 |
| -2015 | InvalidApiKeyIpPermissions | auth failure | -2015 |
| -2021 | OrderWouldTrigger | would trigger immediately | -2021 |
Internal
| Code | Name | Description | Binance Code |
|---|---|---|---|
| -4001 | ClientNotFound | client not found | - (internal) |
| -4002 | CouldNotSendMessage | could not send message | - (internal) |
Notes
DEX-Specific Fields
DEX-specific fields use 2-letter codes for compactness:
| Code | Full Name | Description |
|---|---|---|
th | tx_hash | on-chain transaction hash |
ua | user_address | deprecated aggTrade placeholder |
oi | order_id | deprecated aggTrade placeholder |
ti | trade_id | sequencer trade ID |
co | client_order_id | real on order updates; deprecated aggTrade placeholder |
ft | fill_type | Bullet fill provenance |
rs | remaining_size | remaining order size |
nf | net_fee | deprecated aggTrade placeholder |
ff | is_full_fill | deprecated aggTrade placeholder |
lq | is_liquidation | deprecated 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
efield for event type (e.g.,"e":"subscribe","e":"error") - Order responses still use Binance-style format with
statuscode - Market data messages use
efield 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
RustDecimalfor 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
| Operation | Delegate | Main wallet |
|---|---|---|
| Place / cancel orders | Yes | Yes |
| Deposit | No | Yes |
| Withdraw | No | Yes |
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](strip0xprefix if present). It is both the signing domain separator and the source ofTxDetails.chain_hash_fragment(its first 8 bytes as a little-endianu64);chainInfo.chainIdis 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 containsmarketId(u16) andsymbol(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_idinTxDetails, had noaddress_override, and were signed overborsh(unsigned_tx) ++ chain_hashwith 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
| Scope | Atomic | Use case | Keeps queue priority | |
|---|---|---|---|---|
| Cancel + Place | Per order | No | Simple workflows | No |
| Amend | Per order | Yes | Adjusting price/size | No |
| Replace | Entire market | Yes | Refreshing full quote set | No |
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 type | WebSocket method |
|---|---|
| PlaceOrders | order.place |
| PlaceOrders (replace) | order.place |
| CancelOrders | order.cancel |
| CancelMarketOrders | order.cancelAll |
| CancelAllOrders | order.cancelAll |
| AmendOrders | order.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
| Variant | Behavior |
|---|---|
Limit | standard limit order, rests on the book until filled or cancelled |
PostOnly | rejected if it would immediately match (guarantees maker execution) |
FillOrKill | must fill entirely in one match or the entire order is cancelled |
ImmediateOrCancel | fills as much as possible immediately, cancels any remaining size |
PostOnlySlide | if the limit price would cross, slides to the best non-crossing price instead of rejecting |
PostOnlyFront | posts at the front of the queue at the best price that doesn’t cross |
NewOrderArgs
| Field | Type | Description |
|---|---|---|
price | SurrogateDecimal | order price — see Decimal Encoding |
size | SurrogateDecimal | order size |
side | Side | Bid (buy) or Ask (sell) |
order_type | OrderType | one of the order types above |
reduce_only | bool | if true, can only reduce an existing position — rejected if it would open or increase one |
client_order_id | Option<ClientOrderId> | optional user-defined u64 identifier; returned in order updates and usable for cancels |
pending_tpsl_pair | Option<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:
| Field | Type | Description |
|---|---|---|
take_profit | Option<TpslLeg> | triggers when price reaches the target |
stop_loss | Option<TpslLeg> | triggers when price hits the stop level |
dynamic_size | bool | if true, the TP/SL size tracks the current position size at trigger time |
Each TpslLeg contains:
| Field | Type | Description |
|---|---|---|
trigger_price | SurrogateDecimal | price at which the leg activates |
order_price | Option<SurrogateDecimal> | limit price for the triggered order; None for market execution |
trigger_condition | TriggerPriceCondition | Mark (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,
}
}
Window mode (recommended)
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.
| Field | Type | Description |
|---|---|---|
chain_hash_fragment | u64 | First 8 bytes of the chain hash (GET /fapi/v1/exchangeInfo → chainHash) as a little-endian u64; see chain_hash_fragment |
max_fee | Amount(u128) | Maximum fee the sender is willing to pay |
gas_limit | Option<[u64; 2]> | 2D gas limit: [compute, storage]. None defaults to block limit |
max_priority_fee_bips | u64 | Priority 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.
Recommended Values
| Field | Recommended value | Notes |
|---|---|---|
uniqueness | Window(timestamp_ms()) | Millisecond timestamp |
max_priority_fee_bips | 0 | |
max_fee | 1 << 48 | |
gas_limit | None | Defaults 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_fragment | chain_hash_fragment(&chain_hash) | Derived from the chainHash you sign with |
address_override | None | Execute 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:
| field | bits | description |
|---|---|---|
flags | [31] sign, [23:16] scale | sign bit and decimal exponent |
hi | [95:64] of mantissa | high 32 bits (zero when abs value fits in u64) |
lo | [31:0] of mantissa | low 32 bits of the 96-bit mantissa |
mid | [63:32] of mantissa | middle 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
| input | scale | decimal value | flags | hi | lo | mid |
|---|---|---|---|---|---|---|
100000000 | 8 | 1.00000000 | 0x00080000 | 0x00000000 | 0x05F5E100 | 0x00000000 |
-50000000 | 8 | -0.50000000 | 0x80080000 | 0x00000000 | 0x02FAF080 | 0x00000000 |
123456789012 | 8 | 1234.56789012 | 0x00080000 | 0x00000000 | 0xBE991A14 | 0x0000001C |
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’ - theuvalue from the previous message. On the first snapshot,puis0.
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/userTransfersaccepts an optionalassetname and comma-separatedactionvalues, e.g.asset=USDC&action=DEPOSIT,WITHDRAWAL. Filters apply before pagination and persist in the cursor; omitassetfor all assets - fix:
GET /fapi/v1/userTransfersnow matches theactionfilter against the stored movement kind. Previously, filtering byactionalways returned an empty page - change: the generated SDK
userTransfers/user_transfersmethod gains anassetargument afteraddress, shifting the positionalendTime,limit, andstartTimearguments. Passnull/undefinedin WASM orNonein Rust to keep querying all assets - add:
POST /tx/submitDegen— the route for aUser/DegenTradecall message, which is all it accepts. Same request envelope and response asPOST /tx/submit, which still carriesDegenTradeas well for now
2026-10-19
REST API
- add:
tradingCreditson eachGET /api/v1/balanceisoitem: 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 noTransferevent). 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/submitandPOST /api/v1/solanaOffchainTxnow relay onlyUser,VaultandPublicexchange call messages, plusBank/TransferWithMemoandWarp/TransferRemote.KeeperandAdmincall messages are rejected with403before reaching the sequencer and must be submitted to the sequencer directly
WebSocket
- change:
order.placeapplies the same call-message policy; aKeeperorAdmincall message fails with error code-1020(Unsupported operation)
2026-10-09
REST API
- add:
isDegenon everyGET /fapi/v1/userTradesitem, plus an optionalisDegen=true|falsequery filter. Only the Degen user’s fill istrue; ordinary counterparties in the same transaction and TWAP fills remainfalse
WebSocket
- add:
o.isDegenonorderTradeUpdateTRADE messages:truefor the Degen user’s fills of aDegenTradeopen or close and of the liquidation that sweeps the position;falsefor the counterparties and every other fill
2026-10-03
REST API
- change:
GET /fapi/v1/depth—limitmaximum 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
uequals that update’spu). 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’slastUpdateIdhad 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.unrealizedLossBorrowis no longer returned. The synthetic unrealized-loss borrow it reported was retired and the value was0for 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_idis replaced bychain_hash_fragment(the first 8 bytes ofchainHashas a little-endianu64; a decimal string in the Solana offchain JSON),UnsignedTransactiongains an optionaladdress_override(sendNone), signatures cover the borsh-serializedSigningPayload::V0(version byte, unsigned transaction, chain hash) instead ofborsh(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.chainIdremains inexchangeInfobut is no longer used in transactions
2026-09-24
REST API
- add:
accountVariant.kindmay now besub_vaultonGET /api/v1/account,/api/v1/accounts,/api/v1/balance— one of a vault’s sub-accounts. For this kindaccountVariant.masteris the vault’s address andaccountVariant.leaderthe vault leader’s. Clients branching onkindmust 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 withsymbol,fundingRate(the dampened 8h-basis rate) andfundingTime(hour start, Unix microseconds), oldest first;markPriceis no longer returned. Optionalsymbol,startTime/endTime(inclusive Unix microseconds) andlimit(default 100, max 1000); withoutstartTimethe most recent rows are returned. At most2 * limithours 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 useGET /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 containsassetId,asset,oraclePrice(a decimal string), and nullablelastUpdateTimestamp(Unix microseconds). OptionalassetIdfilters 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 useCache-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 carriesaction, bothfromandtosides (address,bucket,symbolfor isolated markets,vaultNamefor vaults),amount,fee,shareson vault movements andmemo. Sameaddress/startTime/endTime/limit/cursorparameters asuserDeposits, plus an optionalactionfilter - note: bridge and wallet-to-wallet entries have no on-chain execution time, so their
timeis 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); newliquidationTypefield (orderbook,backstop,adl,borrow_lend).markPricerenamed toprice.liquidatorAddress,side,realizedPnl,liquidatorRewardare nullable;symbolandmarketIdarenullforborrow_lend - add:
protocolReward,fee,orderId,tradeId,isMakeronGET /fapi/v1/userLiquidations—nullwhere not applicable to theliquidationType - add:
borrowLendobject onGET /fapi/v1/userLiquidationswithliabilityAsset,liabilityAssetId,collateralAsset,collateralAssetId,collateralSize; present only forliquidationType: borrow_lend,nullotherwise
2026-09-03
REST API
- change:
GET /fapi/v3/account—marginDiscountcan 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_LPaccounts 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—maxWithdrawAmountnow reflects the account’s asset weight discount, and per-positioninitialMarginreflects the Super LP discount (no initial margin on open orders).totalMarginBalanceandavailableBalanceare unweighted and unchanged
2026-08-05
REST API
- change:
GET /api/v1/account,/api/v1/accounts,/api/v1/balance—accountVariant.kindfor a sub-account is nowsub_v1orlegacy_subinstead ofsub, and for a vaultvault_v1orprotocol_vault_v1instead ofvault/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.masteronGET /api/v1/account,/api/v1/accounts,/api/v1/balance— the sub-account’s master address. Only present whenkindissub_v1;nullotherwise - add:
accountVariant.leaderon the same endpoints — the vault’s leader address. Only present whenkindisvault_v1orprotocol_vault_v1;nullotherwise
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: nulltargets the sender’s own account withsub_account_indexapplied; naming an address ignoressub_account_indexand 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:
maxUtilizationRatefield onGET /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_raterenamed torateParams.optimal_utilization_rate(American spelling, matching the exchange). Update clients reading the old key
2026-07-03
REST API
- add:
marginDiscountfield onGET /fapi/v3/account— user’s margin discount ("NONE"or"LP"for liquidity-provider discount) - fix:
GET /fapi/v3/account— per-positioninitialMarginnow 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,rsare now deprecated empty/zero placeholders; treat the values as meaningless and use@user.ordersfor real fill data - add:
o.ap(average price),o.N(commission asset),o.rp(realized PnL) on@user.ordersTRADE fills - change:
@user.orders—o.uais now a deprecated empty-string placeholder; the address is implicit from the authenticated stream
2026-05-19
REST API
- add:
feeTierandfeeDiscountBpsfields onGET /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 grossassetAmount/liabilityAmount, account-leveltradingCreditsandunrealizedLossBorrow, and referral/liquidity rewards - fix:
GET /fapi/v3/balance—maxWithdrawAmountnow 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. Optionalsymbolquery 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 carryorderTypeofSTOPorTAKE_PROFIT, populatedstopPrice, andworkingType(MARK_PRICEorCONTRACT_PRICE) - add:
isTriggerboolean 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,selfTradePreventionModeno longer return the literal string"TODO"; populated withGTC/LIMIT/CONTRACT_PRICE/NONE - add:
GET /fapi/v1/openOrders—timeInForceis nowGTDandgoodTillDateis populated when the order has an expiry timestamp - add:
GET /fapi/v1/openOrder— new optionaltriggerOrderIdquery param to look up a specific user TP/SL order. Mutually exclusive withorderId/clientOrderId
WebSocket
- add:
o.mfield on@user.ordersTRADE fills —trueif the fill was on the maker side, matching BinanceORDER_TRADE_UPDATE - add:
o.tfield on@user.ordersTRADE fills — trade id, matching BinanceORDER_TRADE_UPDATE
2026-05-12
REST API
- add:
pendingTpslfield onGET /fapi/v1/openOrdersand/fapi/v1/openOrder— reserved TP/SL pair attached to an unfilled parent limit order, with optionaltpandsllegs (triggerOrderId,orderPrice,triggerPrice,priceCondition,orderType,timeInForce) and adynamicSizeflag. Omitted when the parent has no TP/SL reservation - change:
stopPriceon/fapi/v1/openOrdersand/fapi/v1/openOrdernow 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—symbolquery parameter is now optional; omit it to return open orders across all markets - add:
marketIdfield added to open order response objects - fix: OpenAPI schema for
GET /fapi/v1/fundingRatecorrected to return an array — was incorrectly documented as a single object - fix: OpenAPI schema for
GET /fapi/v1/insuranceBalancecorrected to return a single object — was incorrectly documented as an array
2026-05-07
REST API
- fix:
GET /fapi/v1/allOrders—avgPricewas incorrectly reported as a large negative number for orders whereprice × 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 statusEXPIRED; previously they fell through toNEWorPARTIALLY_FILLED - fix:
GET /fapi/v1/allOrders— if a fully-filled order also has a cancel record, status is now correctlyFILLEDrather thanCANCELED
2026-05-01
Documentation
- add: docs are now served as raw markdown at parallel
.mdURLs (e.g./docs/getting-started.md) for programmatic access - add:
llms.txtindex served at/llms.txtlisting 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 optionalsymbolquery param; omit for all markets
2026-04-24
WebSocket
- add:
ftfield on@aggTradeand@user.ordersTRADE fills — fill type (oorderbook,lliquidation,bbackstop liquidation,aADL) - add:
z,Z,rsfields on@aggTradeand@user.ordersTRADE 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, andunknown - 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:
txHashesfield toGET /fapi/v1/allOrdersresponse — deduplicated list of tx hashes per order (place, fills, cancels) - add:
addressandtxHashfields toGET /fapi/v1/userTradesresponse - fix: OpenAPI spec now uses
Decimaltypes for prices and quantities instead off64
Documentation
- add: order types and fields page — reference for
NewOrderArgs, order types, TP/SL, and sub-account index - note:
@tickerand broadcasttickerstopics 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
RustDecimaldirectly, removing redundantSurrogateDecimalwrapper (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.modifyWebSocket method - add: documented
order.cancelAllWebSocket method - fix: topic tables now list all accepted aliases (
SYMBOL@liquidations,!liquidations,!ticker,!markPrice, etc.) - fix:
@liquidationsis now the primary topic name,@forceOrderas 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