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
- Bullet Trading API WebSocket Specification
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. WebSocket ping frames reset the keepalive timeout.
Ping
Ping the server to check connection health. Alternatively clients can also send WebSocket ping frames.
{
"method": "ping",
"id": 4
}
| 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 |
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 |
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 |
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 |
Possible errors: ValidationError (empty tx), ServiceUnavailable (submission failed), Timeout, CancelRejected
Responses:
- Success: OrderResult
- Failure: OrderError
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 ("processed", "skipped") | - (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 |
Market Data
All market data messages are pushed to subscribed clients. No type wrapper.
DepthUpdate
Topic: SYMBOL@depth, SYMBOL@depth5, SYMBOL@depth10, SYMBOL@depth20
{
"e": "depthUpdate",
"E": 1706745600000000,
"T": 1706745600000000,
"s": "BTC-USD",
"U": 1000,
"u": 1000,
"pu": 0,
"b": [
[
"50000.00",
"1.5"
],
[
"49999.00",
"2.0"
]
],
"a": [
[
"50001.00",
"1.2"
],
[
"50002.00",
"3.0"
]
],
"mt": "s"
}
| 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.
AggTrade
Topic: SYMBOL@aggTrade
{
"e": "aggTrade",
"E": 1706745600000000,
"s": "BTC-USD",
"a": 200001,
"p": "50000.50",
"q": "0.5",
"f": 200001,
"l": 200001,
"T": 1706745600000000,
"m": false,
"th": "0xabc123def456",
"ua": "0xuser123",
"oi": 100001,
"mk": false,
"ff": true,
"lq": false,
"fe": "0.025",
"nf": "0.025",
"fa": "USD",
"sd": "BUY"
}
| 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 | trader address | - (DEX-specific) |
| oi | u64 | order id | - (DEX-specific) |
| mk | bool | is maker | - (DEX-specific) |
| ff | bool | fully filled | - (DEX-specific) |
| lq | bool | liquidation trade | - (DEX-specific) |
| fe | string | fee amount | - (DEX-specific) |
| nf | string | net fee | - (DEX-specific) |
| fa | string | fee asset | - (DEX-specific) |
| co | string? | client order id | - (DEX-specific) |
| sd | string | "BUY" or "SELL" | - (DEX-specific) |
BookTicker
Topic: SYMBOL@bookTicker, !bookTicker
{
"e": "bookTicker",
"u": 1000,
"E": 1706745600000000,
"T": 1706745600000000,
"s": "BTC-USD",
"b": "50000.00",
"B": "1.5",
"a": "50001.00",
"A": "1.2",
"mt": "s"
}
| 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) |
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) |
OrderUpdate
Topic: [email protected], ADDRESS@ORDER_TRADE_UPDATE
Published for order lifecycle events (NEW, TRADE, CANCELED).
{
"e": "orderTradeUpdate",
"E": 1706745600000000,
"o": {
"s": "BTC-USD",
"i": 100001,
"X": "NEW",
"x": "NEW",
"T": 1706745600000000,
"th": "0xabc123def456",
"ua": "0xuser123",
"S": "BUY",
"o": "LIMIT",
"f": "GTC",
"p": "50000.00",
"q": "1.0"
}
}
Common fields (all events):
| 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 | user address | - (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:
| Field | Type | Description | Binance Equivalent |
|---|---|---|---|
| o.S | string | side | o.S |
| o.l | string | last filled qty | o.l |
| o.L | string | last filled price | o.L |
| o.n | string | commission | o.n |
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@aggTrade | symbol@aggTrade | trades | |
SYMBOL@bookTicker | symbol@bookTicker | best bid/offer | |
SYMBOL@markPrice | symbol@markPrice | mark price + funding | |
SYMBOL@ticker | symbol@ticker | 24hr ticker | |
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 |
|---|---|---|
tickers | !ticker@arr, !ticker | !ticker@arr |
markPrices | !markPrice@arr, !markPrice | !markPrice@arr |
bookTickers | !bookTicker, !bookTicker@arr | !bookTicker |
liquidations | !liquidations, !forceOrder, forceOrders | !forceOrder@arr |
Parse errors: InvalidFormat, UnknownTopic
User Data Topics
| Bullet Topic | Aliases | Binance Equivalent |
|---|---|---|
[email protected] | ADDRESS@ORDER_TRADE_UPDATE | listenKey stream |
Key difference: Bullet uses address-prefixed topics directly. Binance requires a listenKey from REST API.
Resolution errors: MissingUserAddress (when address not provided)
Speed Suffixes
Speed suffixes are accepted but ignored: @100ms, @500ms, @1s
Example: BTC-USD@depth@100ms is equivalent to BTC-USD@depth
Error Codes
General
| 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 | user’s wallet address |
oi | order_id | sequencer order ID |
ti | trade_id | sequencer trade ID |
mk | is_maker | whether the trade was a maker |
ff | is_full_fill | fully filled indicator |
lq | is_liquidation | liquidation trade indicator |
fe | fee | fee amount |
nf | net_fee | net fee after rebates |
fa | fee_asset | fee asset symbol |
sd | side | trade side (BUY/SELL) |
co | client_order_id | client-provided order ID |
Message Type Field
The mt field in orderbook and BBO messages indicates:
"s"- snapshot (complete state at that depth level)"u"- update (incremental changes since last update)
Response Format Differences
- All responses use
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")