> For the complete documentation index, see [llms.txt](https://docs.hydromancer.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.hydromancer.xyz/readme/websocket/l2book.md).

# l2Book

Stream L2 orderbook snapshots at configurable depth. Sent every block (\~70ms blocks). Supports optional price aggregation via significant figures and mantissa parameters.

For single-coin subscriptions, `data` is a single snapshot object. For multi-coin subscriptions, `data` is an array of snapshot objects batched per block.

### Choosing the right orderbook stream

| Stream                                                | Latency         | Best for                                                             |
| ----------------------------------------------------- | --------------- | -------------------------------------------------------------------- |
| [`l4BookUpdates`](/readme/websocket/l4bookupdates.md) | Fastest         | HFT, market making — lowest latency, includes user address per level |
| [`l2BookDiff`](/readme/websocket/l2bookdiff.md)       | \~2ms after L4  | Building local orderbooks — low bandwidth, client maintains state    |
| [`bbo`](/readme/websocket/bbo.md)                     | Same as l2Book  | Price tracking — minimal bandwidth                                   |
| `l2Book` (raw)                                        | \~5ms after L4  | Stateless consumers — every message is a complete snapshot           |
| `l2Book` (aggregated)                                 | \~15ms after L4 | Display/charting — slightly slower than raw because of aggregations  |

### Subscribe

```json
{
    "method": "subscribe",
    "subscription": {
        "type": "l2Book",
        "coins": ["ETH", "BTC"]
    }
}
```

**Parameters:**

| Parameter     | Type      | Required | Description                                                                                                                                                                    |
| ------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `coins`       | string\[] | No       | Coins to subscribe to. Omit for all markets (requires permission).                                                                                                             |
| `nSigFigs`    | number    | No       | Aggregate price levels to N significant figures (2-5). Bids round down, asks round up.                                                                                         |
| `mantissa`    | number    | No       | Snap to mantissa step (2 or 5). Only valid when `nSigFigs=5`.                                                                                                                  |
| `nLevels`     | number    | No       | Number of levels per side: `1` (BBO), `10`, `20` (default), or `50`. `50` is allowed on per-coin/multi-coin subs; on the all-markets firehose it requires `pushMode: "delta"`. |
| `marketTypes` | string\[] | No       | All-markets only. Filters delivery by market type — see [All-markets filter](#all-markets-filter). Rejected if combined with `coins`.                                          |
| `pushMode`    | string    | No       | Delivery cadence for the all-markets firehose: `"delta"` (recommended) or `"full"` (current default) — see [Push modes](#push-modes).                                          |

### All-markets filter

When `coins` is omitted, the optional `marketTypes` field restricts the firehose to specific market types. Each entry is `"perp"`, `"spot"`, `"outcome"`, or the wildcard `"*"` (alone) for "every type the server currently tracks":

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

Omitting `marketTypes` 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 subscribe with a different `marketTypes` value **replaces** the previous filter rather than coexisting with it.

### Push modes

For the all-markets firehose (`coins` omitted), `pushMode` sets delivery cadence (raw and aggregated alike). It has no effect on single- or multi-coin subscriptions — those already send a coin only when it changes.

**Use `"delta"`** — same data as `"full"` for \~80% less bandwidth. `"full"` re-sends every coin every block and stays the default only for backward compatibility while consumers migrate.

| Mode                       | Behavior                                                                                                                              |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `"delta"` (recommended)    | Full snapshot of all coins **once on subscribe**, then only the coins that **changed** each block. Keep a local book keyed by `coin`. |
| `"full"` (current default) | Complete snapshot for **every** coin every block. Legacy; being phased out.                                                           |

`nLevels: 50` is available on per-coin and multi-coin subscriptions directly, and on the firehose in `"delta"` mode.

In `"delta"` mode each message is still a **complete snapshot for the coins it contains** — there are no incremental level diffs. A client simply overwrites its stored book for each `coin` it receives. Coins that never change after subscribe are covered by the initial snapshot. The per-subscription `seq` is gap-free, so a seq jump signals a lost message — reconnect to receive a fresh full snapshot and resync. (For true per-level diffs and the lowest bandwidth, use [`l2BookDiff`](/readme/websocket/l2bookdiff.md) instead.)

The server never starts a delta stream without first sending that initial snapshot; in the rare case it can't be produced at subscribe (e.g. briefly during an internal resync), the subscribe is rejected with a retryable error — resubscribe.

```json
{
    "method": "subscribe",
    "subscription": {
        "type": "l2Book",
        "marketTypes": ["perp"],
        "pushMode": "delta"
    }
}
```

**Aggregation example:**

```json
{
    "method": "subscribe",
    "subscription": {
        "type": "l2Book",
        "coins": ["BTC"],
        "nSigFigs": 5,
        "mantissa": 2
    }
}
```

With `nSigFigs=5, mantissa=2`, prices are rounded to 5 significant figures then snapped to the nearest multiple of 2 at the last digit. Bids round down, asks round up. For example, bid `70325` becomes `70324`, ask `70325` becomes `70326`.

### Unsubscribe

```json
{
    "method": "unsubscribe",
    "subscription": {
        "type": "l2Book",
        "coins": ["ETH", "BTC"]
    }
}
```

### Update data format

For single-coin subscriptions, `data` is a single snapshot object (with `coin`, `time`, `levels`). For multi-coin or all-markets subscriptions, `data` is an array of snapshot objects batched per block.

When the subscription includes aggregation parameters, the response includes `nSigFigs` and (if applicable) `mantissa` so clients can distinguish messages from multiple concurrent l2Book subscriptions at different aggregation levels.

**Single-coin subscription (raw):**

```json
{
    "type": "l2Book",
    "channel": "l2Book",
    "seq": 15,
    "cursor": "0",
    "data": {
        "coin": "ETH",
        "time": 1704067200000,
        "levels": [
            [
                {"px": "3245.5", "sz": "12.4", "n": 3},
                {"px": "3244.0", "sz": "8.7", "n": 5},
                {"px": "3243.5", "sz": "25.1", "n": 8}
            ],
            [
                {"px": "3246.0", "sz": "6.2", "n": 2},
                {"px": "3247.5", "sz": "15.3", "n": 4},
                {"px": "3248.0", "sz": "9.8", "n": 6}
            ]
        ]
    }
}
```

**Single-coin subscription (aggregated, nSigFigs=5, mantissa=2):**

```json
{
    "type": "l2Book",
    "channel": "l2Book",
    "nSigFigs": 5,
    "mantissa": 2,
    "seq": 15,
    "cursor": "0",
    "data": {
        "coin": "BTC",
        "time": 1704067200000,
        "levels": [
            [
                {"px": "68604", "sz": "96.9", "n": 5},
                {"px": "68602", "sz": "42.1", "n": 3}
            ],
            [
                {"px": "68606", "sz": "15.3", "n": 4},
                {"px": "68608", "sz": "9.8", "n": 6}
            ]
        ]
    }
}
```

**Multi-coin subscription:**

```json
{
    "type": "l2Book",
    "channel": "l2Book",
    "seq": 15,
    "cursor": "0",
    "data": [
        {
            "coin": "ETH",
            "time": 1704067200000,
            "levels": [
                [{"px": "3245.5", "sz": "12.4", "n": 3}],
                [{"px": "3246.0", "sz": "6.2", "n": 2}]
            ]
        },
        {
            "coin": "BTC",
            "time": 1704067200000,
            "levels": [
                [{"px": "68605", "sz": "96.9", "n": 5}],
                [{"px": "68610", "sz": "42.1", "n": 3}]
            ]
        }
    ]
}
```

### Field reference

**Envelope fields** (on every message):

| Field      | Type   | Description                                                                                       |
| ---------- | ------ | ------------------------------------------------------------------------------------------------- |
| `seq`      | number | Per-subscription sequence number                                                                  |
| `cursor`   | string | Always `"0"` — l2Book is stateless (every message is a complete snapshot, no replay)              |
| `nSigFigs` | number | Present only on aggregated subscriptions. Echoes the `nSigFigs` value from the subscribe request. |
| `mantissa` | number | Present only when the subscribe request included `mantissa`. Echoes the value.                    |

**Per-coin data fields** (each item in `data`):

| Field           | Type   | Description                                                      |
| --------------- | ------ | ---------------------------------------------------------------- |
| `coin`          | string | Market symbol (e.g., "ETH", "BTC")                               |
| `time`          | number | Block timestamp (milliseconds since epoch)                       |
| `levels`        | array  | Tuple of `[bids, asks]`. Bids sorted descending, asks ascending. |
| `levels[][].px` | string | Price as decimal string                                          |
| `levels[][].sz` | string | Total size at this price level as decimal string                 |
| `levels[][].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: 'l2Book',
                coins: ['ETH', 'BTC']
            }
        }));
    } else if (msg.type === 'ping') {
        ws.send(JSON.stringify({ type: 'pong' }));
    } else if (msg.type === 'l2Book') {
        const { coin, time, levels } = msg.data;
        const [bids, asks] = levels;
        const bestBid = bids[0]?.px || 'n/a';
        const bestAsk = asks[0]?.px || 'n/a';
        console.log(`${coin} @ ${new Date(time).toISOString()}: bid=${bestBid} ask=${bestAsk} (${bids.length}/${asks.length} levels)`);
    }
});
```

{% endtab %}

{% tab title="Python" %}

```python
import websocket
import json
import os
from datetime import datetime, timezone

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

    if msg['type'] == 'connected':
        ws.send(json.dumps({
            "method": "subscribe",
            "subscription": {
                "type": "l2Book",
                "coins": ["ETH", "BTC"]
            }
        }))
    elif msg['type'] == 'ping':
        ws.send(json.dumps({'type': 'pong'}))
    elif msg['type'] == 'l2Book':
        d = msg['data']
        bids, asks = d['levels']
        best_bid = bids[0]['px'] if bids else 'n/a'
        best_ask = asks[0]['px'] if asks else 'n/a'
        ts = datetime.fromtimestamp(d['time'] / 1000, tz=timezone.utc)
        print(f"{d['coin']} @ {ts.isoformat()}: bid={best_bid} ask={best_ask} ({len(bids)}/{len(asks)} levels)")

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. `Too many coins` - Reduce number of coins or upgrade tier
2. `Subscribing to all markets requires permission` - Needs `ws:l2BookAll` add-on
3. `Invalid nSigFigs value` - Must be 2, 3, 4, or 5
4. `Invalid mantissa value` - Must be 2 or 5, only valid with `nSigFigs=5`
5. `Invalid nLevels value` - Must be 1, 10, 20, or 50. On the all-markets firehose, `50` requires `pushMode: "delta"`.
6. `Rate limit exceeded` - Reduce subscription frequency
7. `l2Book snapshot not ready, please resubscribe` - Retryable. A `pushMode: "delta"` firehose subscribe arrived while the initial snapshot couldn't be produced (e.g. during an internal resync). Resubscribe.
