# Bullet Trading API WebSocket Specification

<!--
  JSON examples in this document are auto-generated from Rust types.
  DO NOT edit JSON blocks manually - they will be overwritten.

  To update examples:
    make gen-ws-docs

  Marker format (content between markers is replaced):
    < !-- ws:example:group/scenario -- >
    < !-- /ws:example:group/scenario -- >

  Suffixes:
    :request  - shows client request only
    :response - shows server response only
    (none)    - shows server response

  Source: crates/trading-api/src/services/ws/ws_spec.rs
-->

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.

<!-- TOC -->

* [Bullet Trading API WebSocket Specification](#bullet-trading-api-websocket-specification)
    * [Connection Lifecycle](#connection-lifecycle)
        * [Status Message](#status-message)
        * [Connection Establishment](#connection-establishment)
        * [Disconnection](#disconnection)
            * [Disconnect Reasons](#disconnect-reasons)
        * [Keepalive (Ping/Pong)](#keepalive-pingpong)
            * [Ping](#ping)
            * [Pong](#pong)
    * [Subscription Streams](#subscription-streams)
        * [Subscribe](#subscribe)
            * [Subscribe (success)](#subscribe-success)
        * [Unsubscribe](#unsubscribe)
            * [Unsubscribe (success)](#unsubscribe-success)
        * [ListSubscriptions](#listsubscriptions)
            * [ListSubscriptions (success)](#listsubscriptions-success)
    * [Request-Response](#request-response)
        * [OrderPlace](#orderplace)
        * [OrderCancel](#ordercancel)
        * [OrderAmend](#orderamend)
        * [OrderCancelAll](#ordercancelall)
        * [Responses](#responses)
            * [Error](#error)
            * [OrderResult](#orderresult)
            * [OrderError](#ordererror)
            * [Tx Status Values](#tx-status-values)
    * [Market Data](#market-data)
        * [DepthUpdate](#depthupdate)
        * [AggTrade](#aggtrade)
        * [BookTicker](#bookticker)
        * [MarkPrice](#markprice)
        * [Liquidation (ForceOrder)](#liquidation-forceorder)
        * [OrderUpdate](#orderupdate)
    * [Topics](#topics)
        * [Symbol-Based Topics](#symbol-based-topics)
        * [Broadcast Topics](#broadcast-topics)
        * [User Data Topics](#user-data-topics)
        * [Speed Suffixes](#speed-suffixes)
    * [Error Codes](#error-codes)
        * [General](#general)
        * [Parameters](#parameters)
        * [Subscriptions](#subscriptions)
        * [Orders](#orders)
        * [Internal](#internal)
    * [Notes](#notes)
        * [DEX-Specific Fields](#dex-specific-fields)
        * [Message Type Field](#message-type-field)
        * [Response Format Differences](#response-format-differences)

<!-- TOC -->

---

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

1. Client connects to WebSocket endpoint
2. Server assigns unique `clientId`
3. Server sends `"connected"` status message

<!-- ws:example:connection_lifecycle/StatusMessage -->

```json
{
  "e": "status",
  "E": 1706745600000000,
  "status": "connected",
  "clientId": "ws_abc123"
}
```

<!-- /ws:example:connection_lifecycle/StatusMessage -->

### Disconnection

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

<!-- ws:example:connection_lifecycle/Disconnection_PongTimeout -->

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

<!-- /ws:example:connection_lifecycle/Disconnection_PongTimeout -->

#### 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.

<!-- ws:example:keepalive/Ping:request -->

```json
{
  "method": "ping",
  "id": 4
}
```

<!-- /ws:example:keepalive/Ping:request -->

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

#### Pong

<!-- ws:example:keepalive/Ping:response -->

```json
{
  "e": "pong",
  "id": 4,
  "E": 1706745600000000
}
```

<!-- /ws:example:keepalive/Ping:response -->

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

<!-- ws:example:subscription_streams/Subscribe:request -->

```json
{
  "method": "subscribe",
  "id": 1,
  "params": [
    "BTC-USD@aggTrade",
    "ETH-USD@depth10"
  ]
}
```

<!-- /ws:example:subscription_streams/Subscribe:request -->

| 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:**

- Success: [Subscribe](#subscribe-success)
- Failure: [Error](#error)

#### Subscribe (success)

<!-- ws:example:subscription_streams/Subscribe:response -->

```json
{
  "e": "subscribe",
  "id": 1,
  "E": 1706745600000000,
  "result": "success"
}
```

<!-- /ws:example:subscription_streams/Subscribe:response -->

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

<!-- ws:example:subscription_streams/Unsubscribe:request -->

```json
{
  "method": "unsubscribe",
  "id": 5,
  "params": [
    "BTC-USD@aggTrade"
  ]
}
```

<!-- /ws:example:subscription_streams/Unsubscribe:request -->

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

#### Unsubscribe (success)

<!-- ws:example:subscription_streams/Unsubscribe:response -->

```json
{
  "e": "unsubscribe",
  "id": 5,
  "E": 1706745600000000,
  "result": "success"
}
```

<!-- /ws:example:subscription_streams/Unsubscribe:response -->

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

<!-- ws:example:subscription_streams/ListSubscriptions:request -->

```json
{
  "method": "list_subscriptions",
  "id": 6
}
```

<!-- /ws:example:subscription_streams/ListSubscriptions:request -->

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

#### ListSubscriptions (success)

<!-- ws:example:subscription_streams/ListSubscriptions:response -->

```json
{
  "e": "list_subscriptions",
  "id": 6,
  "E": 1706745600000000,
  "result": [
    "BTC-USD@depth10",
    "ETH-USD@aggTrade"
  ]
}
```

<!-- /ws:example:subscription_streams/ListSubscriptions:response -->

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

<!-- ws:example:request_response/OrderPlace:request -->

```json
{
  "method": "order.place",
  "id": 10,
  "params": {
    "tx": "c2lnbmVkX3RyYW5zYWN0aW9uX2J5dGVz"
  }
}
```

<!-- /ws:example:request_response/OrderPlace:request -->

| 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 `tx` payload:** see [Place Orders](../tx-signing.md#place-orders) (or
> [Replace Orders](../tx-signing.md#replace-orders) for atomic quote refresh) for construction,
> [Order Types and Fields](../order-fields.md) for `NewOrderArgs` reference, and
> [Signing](../tx-signing.md#signing) for the sign → encode → submit flow.

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

**Responses:**

- Success: [OrderResult](#orderresult)
- Failure: [OrderError](#ordererror)

---

### OrderCancel

Cancel an order via WebSocket.

<!-- ws:example:request_response/OrderCancel:request -->

```json
{
  "method": "order.cancel",
  "id": 11,
  "params": {
    "tx": "c2lnbmVkX3RyYW5zYWN0aW9uX2J5dGVz"
  }
}
```

<!-- /ws:example:request_response/OrderCancel:request -->

| 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 `tx` payload:** see [Cancel Orders](../tx-signing.md#cancel-orders) for construction and
> [Signing](../tx-signing.md#signing) for the sign → encode → submit flow.

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

**Responses:**

- Success: [OrderResult](#orderresult)
- Failure: [OrderError](#ordererror)

---

### OrderAmend

Amend an existing order via WebSocket.

```json
{
  "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 `tx` payload:** see [Amend Orders](../tx-signing.md#amend-orders) for construction and
> [Signing](../tx-signing.md#signing) for the sign → encode → submit flow.

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

**Responses:**

- Success: [OrderResult](#orderresult)
- Failure: [OrderError](#ordererror)

---

### OrderCancelAll

Cancel all open orders for a market via WebSocket.

```json
{
  "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 `tx` payload:** see [Cancel Market Orders](../tx-signing.md#cancel-market-orders) /
> [Cancel All Orders](../tx-signing.md#cancel-all-orders) for construction and
> [Signing](../tx-signing.md#signing) for the sign → encode → submit flow.

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

**Responses:**

- Success: [OrderResult](#orderresult)
- Failure: [OrderError](#ordererror)

---

### Responses

#### Error

Error response to any client request.

<!-- ws:example:subscription_streams/Subscribe_InvalidFormat:response -->

```json
{
  "e": "error",
  "id": 2,
  "E": 1706745600000000,
  "error": {
    "param": "invalid-topic-format",
    "code": -1004,
    "msg": "invalid subscription format: expected <symbol>@<stream>"
  }
}
```

<!-- /ws:example:subscription_streams/Subscribe_InvalidFormat:response -->

| 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`.

<!-- ws:example:request_response/OrderPlace:response -->

```json
{
  "e": "order.place",
  "id": 10,
  "E": 1706745600000000,
  "results": {
    "tx_id": "0xabc123def456",
    "status": "processed",
    "order_ids": [
      1
    ],
    "client_order_ids": [
      1
    ]
  }
}
```

<!-- /ws:example:request_response/OrderPlace:response -->

| 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](#tx-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`.

<!-- ws:example:request_response/OrderPlace_Rejected:response -->

```json
{
  "id": 12,
  "E": 1706745600000000,
  "error": {
    "code": -2010,
    "msg": "new order rejected: insufficient margin"
  }
}
```

<!-- /ws:example:request_response/OrderPlace_Rejected:response -->

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

<!-- ws:example:market_data/DepthUpdate_Snapshot -->

```json
{
  "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"
}
```

<!-- /ws:example:market_data/DepthUpdate_Snapshot -->

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

<!-- ws:example:market_data/AggTrade -->

```json
{
  "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",
  "ft": "o",
  "z": "0.5",
  "Z": "25000.25",
  "rs": "0"
}
```

<!-- /ws:example:market_data/AggTrade -->

| 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)   |
| ft    | string? | fill type (see table below)                          | - (DEX-specific)   |
| z     | string? | cumulative filled size for this order                | - (DEX-specific)   |
| Z     | string? | cumulative filled cost (size × price) for this order | - (DEX-specific)   |
| 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) |

### BookTicker

Topic: `SYMBOL@bookTicker`, `!bookTicker`

<!-- ws:example:market_data/BookTicker -->

```json
{
  "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"
}
```

<!-- /ws:example:market_data/BookTicker -->

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

<!-- ws:example:market_data/MarkPrice -->

```json
{
  "e": "markPriceUpdate",
  "E": 1706745600000000,
  "s": "BTC-USD",
  "p": "50000.50",
  "i": "50000.00",
  "r": "0.0001",
  "T": 1706774400000000
}
```

<!-- /ws:example:market_data/MarkPrice -->

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

<!-- ws:example:market_data/ForceOrder -->

```json
{
  "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
  }
}
```

<!-- /ws:example:market_data/ForceOrder -->

| 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: `ADDRESS@user.orders`, `ADDRESS@ORDER_TRADE_UPDATE`

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

<!-- ws:example:market_data/OrderUpdate_New -->

```json
{
  "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"
  }
}
```

<!-- /ws:example:market_data/OrderUpdate_New -->

**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                |
| o.ft  | string? | fill type (see [fill type values](#fill-type-values-ft)) | - (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)   |

---

## 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 (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`  |             |

**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    | 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 `e` field for event type (e.g., `"e":"subscribe"`, `"e":"error"`)
- **Order responses** still use Binance-style format with `status` code
- **Market data** messages use `e` field for event type (e.g., `"e":"depthUpdate"`)
