# bbo

Stream the best bid and offer (top-of-book) for a single coin. Updated on change (only sent when the best bid or ask changes). This is the most bandwidth-efficient way to track live prices.

For deeper orderbook data, use [`l2Book`](/readme/websocket/l2book.md). For incremental diffs, use [`l2BookDiff`](/readme/websocket/l2bookdiff.md).

### Subscribe

```json
{
    "method": "subscribe",
    "subscription": {
        "type": "bbo",
        "coin": "BTC"
    }
}
```

**Parameters:**

| Parameter     | Type      | Required | Description                                                                                                                     |
| ------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `coin`        | string    | No       | Coin symbol (e.g., "BTC", "ETH"). One subscription per coin. Omit to subscribe to all coins (requires add-on, see below).       |
| `marketTypes` | string\[] | No       | All-coins only. Filters delivery by market type — see [All-coins BBO](#all-coins-bbo-add-on). Rejected if combined with `coin`. |

### All-coins BBO (add-on)

Omit the `coin` field to receive BBO updates for every market in a single subscription:

```json
{
    "method": "subscribe",
    "subscription": {
        "type": "bbo",
        "marketTypes": ["perp", "outcome"]
    }
}
```

This requires the **bboAll** add-on on your API key. Without it the subscription will be rejected.

The optional `marketTypes` filter restricts the firehose to specific market types. Each entry is `"perp"`, `"spot"`, `"outcome"`, or the wildcard `"*"` (alone) for "every type the server currently tracks." Omitting the field defaults to `["perp"]` — outcome and spot markets do **not** appear unless you opt in. The default never grows; new market types must be added to your `marketTypes` array explicitly. Pass `["*"]` to auto-opt-in to future types.

A second all-coins subscribe with a different `marketTypes` value **replaces** the previous filter rather than coexisting with it. Per-coin BBO subscriptions coexist independently with the all-coins subscription.

### Unsubscribe

```json
{
    "method": "unsubscribe",
    "subscription": {
        "type": "bbo",
        "coin": "BTC"
    }
}
```

### Update data format

Each message contains the best bid and best ask for the subscribed coin:

```json
{
    "channel": "bbo",
    "seq": 5,
    "cursor": "0",
    "data": {
        "coin": "BTC",
        "time": 1704067200000,
        "bbo": [
            {"px": "70325", "sz": "12.5", "n": 3},
            {"px": "70326", "sz": "8.3", "n": 2}
        ]
    }
}
```

The `bbo` array always contains exactly 2 elements: `[best_bid, best_ask]`. If a side is empty (no bids or no asks), that element is `null`.

### Field reference

| Field      | Type   | Description                                            |
| ---------- | ------ | ------------------------------------------------------ |
| `coin`     | string | Market symbol                                          |
| `time`     | number | Block timestamp (milliseconds since epoch)             |
| `bbo`      | array  | `[best_bid, best_ask]` — each is an L2 level or `null` |
| `bbo[].px` | string | Price as decimal string                                |
| `bbo[].sz` | string | Total size at this price level                         |
| `bbo[].n`  | number | Number of orders at this price level                   |

### Examples

{% tabs %}
{% tab title="JavaScript" %}

```javascript
const WebSocket = require('ws');

const ws = new WebSocket(`wss://api.hydromancer.xyz/ws?token=${process.env.HYDROMANCER_API_KEY}`);

ws.on('message', (data) => {
    const msg = JSON.parse(data);

    if (msg.type === 'connected') {
        ws.send(JSON.stringify({
            method: 'subscribe',
            subscription: { type: 'bbo', coin: 'BTC' }
        }));
    } else if (msg.type === 'ping') {
        ws.send(JSON.stringify({ type: 'pong' }));
    } else if (msg.channel === 'bbo') {
        const { coin, time, bbo } = msg.data;
        const [bid, ask] = bbo;
        console.log(`${coin}: bid=${bid?.px} ask=${ask?.px}`);
    }
});
```

{% endtab %}

{% tab title="Python" %}

```python
import websocket
import json
import os

def on_message(ws, message):
    msg = json.loads(message)

    if msg['type'] == 'connected':
        ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {"type": "bbo", "coin": "BTC"}
        }))
    elif msg['type'] == 'ping':
        ws.send(json.dumps({'type': 'pong'}))
    elif msg.get('channel') == 'bbo':
        d = msg['data']
        bid, ask = d['bbo']
        print(f"{d['coin']}: bid={bid['px'] if bid else 'n/a'} ask={ask['px'] if ask else 'n/a'}")

ws = websocket.WebSocketApp(
    f"wss://api.hydromancer.xyz/ws?token={os.environ.get('HYDROMANCER_API_KEY')}",
    on_message=on_message
)
ws.run_forever()
```

{% endtab %}
{% endtabs %}

### Common errors

1. `Rate limit exceeded` - Too many BBO subscriptions for your tier
2. `Permission denied` - All-coins BBO requires the **bboAll** add-on


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.hydromancer.xyz/readme/websocket/bbo.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
