Order Book
Live order book depth over WebSocket — snapshot semantics, the levels/speed knobs, and the four rules that keep a bot from trading on a corrupt book.
Live order book depth (top-of-book snapshots) for every supported pair, streamed over socket.io. Companion to the Chart Price Stream — the connection and authentication model are the same; the payload semantics are not.
Connecting
| Property | Value |
|---|---|
| Protocol | socket.io v4 (not raw WebSocket) |
| Production | wss://bot.mywellat.com/bot/v1/orderbook |
| Staging | wss://pg-bot.mywellat.com/bot/v1/orderbook |
| Namespace | /bot/v1/orderbook |
| Transports | websocket only — disable HTTP long-polling |
Authentication is identical to the Chart Price Stream — same credentials, same auth object (or x-bot-* header fallback), same 30-second window and failed-handshake block — except the signed path:
message = timestamp + "GET" + "/bot/v1/orderbook"
Sign each namespace separatelyThe path is part of the signed message, so a signature minted for
/bot/v1/marketwill not authenticate here — it fails withAUTH_INVALID_SIGNATURE, and repeated attempts count toward the 15-minute auth block.
This is a separate connection from the chart price stream, and both count against the same 5-connections-per-key budget.
Subscribing
Send a subscribe event:
{ "pair": "BTCUSDT", "levels": 20, "speed": "1000ms" }| Field | Required | Values | Default |
|---|---|---|---|
pair | yes | any pair from GET /bot/v1/spot/pairs | — |
levels | no | 5, 10, 20 | 20 |
speed | no | "1000ms", "100ms" | "1000ms" |
There is notimeframehereUnknown fields are rejected with
SUB_UNEXPECTED_FIELDrather than ignored, so a copied chart-price payload fails loudly instead of appearing to work.
100msis expensive — default to1000msA
100msstream delivers 10× the messages, counts as 5 subscriptions against your per-connection cap, and is separately capped at 2 fast streams per API key (SUB_SPEED_LIMIT_EXCEEDEDbeyond that). A bot that decides on closed candles has no use for 10 Hz depth.
To stop: send unsubscribe with { "pair": "BTCUSDT" } to drop every stream for the pair, or the full pair/levels/speed triple to drop one specific stream.
Events
| Event | Payload | Meaning |
|---|---|---|
subscribed | { pair, levels, speed } | Subscription active. |
unsubscribed | { pair, levels, speed } | Subscription removed. |
orderBook | see below | A depth snapshot. |
error | { status: false, code, message } | Every failure — always listen to it. |
The orderBook payload
orderBook payload{
"event": "depth",
"pair": "BTCUSDT",
"levels": 20,
"speed": "1000ms",
"lastUpdateId": 69827431556,
"bids": [
["64270.31000000", "0.48210000"],
["64270.30000000", "1.02900000"]
],
"asks": [
["64270.32000000", "0.11500000"],
["64270.33000000", "2.40010000"]
]
}Each entry is [price, quantity], both strings — parse with a decimal library, never Number, where the value feeds an order. bids are sorted descending and asks ascending, so bids[0] / asks[0] are the top of book.
Handling the book correctly
These four rules are where depth consumers go wrong:
Every message is a complete snapshot — replace your book, never mergeThis is Binance partial depth, not the diff stream. Merging snapshots leaves phantom price levels in your book forever, and your bot sizes orders against liquidity nobody is quoting.
socket.on('orderBook', (msg) => {
const prev = books.get(msg.pair);
if (prev && msg.lastUpdateId <= prev.lastUpdateId) return; // stale or duplicate
books.set(msg.pair, {
// REPLACE — never merge
bids: msg.bids,
asks: msg.asks,
lastUpdateId: msg.lastUpdateId,
});
});- Drop stale snapshots.
lastUpdateIdincreases monotonically; after a reconnect you may receive an older snapshot than the one you hold — discard anything withlastUpdateId <=your current one. - Reject a crossed book. If best bid ≥ best ask, the book is corrupt — discard it and wait for a clean snapshot. Never trade on it.
- This is a truncated book. You see at most 20 levels per side — summing visible quantity tells you what's in the top 20 levels, not what the market can absorb. Size orders from the pair's
filters(Trading Pairs), never from visible depth.
The book is a reference, not an execution guaranteeAt
1000msit is up to a second stale on arrival, it reserves nothing, and every order is re-validated and re-priced at execution.
Reliability
- Invalidate, don't coast. On disconnect, staleness, or a crossed book, discard the book and suspend depth-dependent decisions until a fresh snapshot arrives. A frozen order book actively lies about executable prices.
- Tight staleness watchdog. A healthy stream emits on schedule regardless of trading activity — silence past ~5 seconds means dead, not quiet. Unsubscribe/re-subscribe, then force a reconnect if that fails. Do not reuse a chart-price watchdog threshold.
- Subscriptions do not survive a reconnect — re-subscribe to everything on every
connect, and clear held books first. - REST fallback:
GET https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=20returns the same{ lastUpdateId, bids, asks }shape. Poll no faster than the stream speed you replaced, and treat polling as explicitly degraded.
Errors
The shared codes (AUTH_*, SUB_INVALID_PAYLOAD, SUB_UNSUPPORTED_SYMBOL, SUB_LIMIT_EXCEEDED, RATE_LIMITED, UPSTREAM_UNAVAILABLE, INTERNAL) behave exactly as on the Chart Price Stream. Depth-specific additions:
| Code | Meaning | Your response |
|---|---|---|
SUB_UNSUPPORTED_LEVELS | levels not in 5, 10, 20 | Fix client. |
SUB_UNSUPPORTED_SPEED | speed not in 1000ms, 100ms | Fix client. |
SUB_UNEXPECTED_FIELD | Unknown field (typically a copied timeframe) | Fix client. |
SUB_SPEED_LIMIT_EXCEEDED | Over the per-key 100ms cap | Use 1000ms, or unsubscribe another fast stream. |
Updated 9 days ago