# 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`, or `20` (default).                                                                        |
| `marketTypes` | string\[] | No       | All-markets only. Filters delivery by market type — see [All-markets filter](#all-markets-filter). Rejected if combined with `coins`. |

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

**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, or 20
6. `Rate limit exceeded` - Reduce subscription frequency


---

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