Deep Dive — Order Book Reconstruction
client.orderBook(symbol) returns a LiveOrderBook — a local L2 order book the SDK keeps synchronized from the WebSocket depth stream. This page explains how it stays correct, how it detects and recovers from gaps, and the guarantees it makes.
The snapshot + diff model
The exchange does not stream the full book on every change. Instead:
- The SDK subscribes to the depth stream and requests an initial snapshot — the complete book at a point in time.
- Every subsequent message is a diff (
DepthUpdate) — only the price levels that changed. - The SDK applies each diff in sequence to keep its local copy current.
The first "update" event you receive carries the full book (the snapshot). Every event after that carries only the changed levels.
Code
Sequence integrity
Each diff carries two sequence IDs: U (first update ID) and u (last update ID). The SDK tracks the last applied u and enforces strict ordering:
- Stale diff (
u <= lastUpdateId): already covered, silently dropped. - Contiguous diff (
U <= lastUpdateId < u): applied,lastUpdateIdadvances tou. - Gap (
U > lastUpdateId): a frame was missed. The book can no longer be trusted — it raisesResyncRequiredErrorinternally and goes stale.
Gemini's depth stream overlaps at U == lastUpdateId (unlike a strictly contiguous stream), so any U strictly greater than the last applied ID indicates a real gap, not normal overlap.
Self-healing
When a gap is detected — or a malformed frame arrives, or the connection reconnects — the book:
- Marks itself stale (
live = false). - Emits a single
"resync"event (deduplicated: one per stale period). - Requests a fresh snapshot in the background.
- On snapshot arrival, rebuilds and emits a full-book
"update".
You never call anything to trigger recovery — it is automatic. Your job is to respect the "resync" signal:
Code
Stale reads return nothing
While stale, every read method returns empty — a gapped book must never look tradeable:
| Method | While live | While stale |
|---|---|---|
bestBid() / bestAsk() | Level | undefined |
topN(side, n) | Level[] | [] |
spread() / mid() | number | undefined |
snapshot() | { bids, asks } | { bids: [], asks: [] } |
This means you cannot accidentally read a torn book: if bestBid() returns undefined, the book is either not yet initialized or currently resyncing.
Reads
Code
spread() and mid() return floating-point numbers for display. Do not use them for exact execution math — prices on the wire are decimal strings (see Data Types) and converting to float loses precision.
Price level identity
Levels are keyed by a canonical price string, so "0.50" and "0.5" map to the same level. This guarantees a removal (quantity: "0") can never leave a stale duplicate at a differently-formatted price.
Separate session
The order book runs on its own WebSocket session, isolated from public streams (trades, bookTicker, etc.). A reconnect or failure on one does not stall the other. In sandbox, the SDK uses a dedicated snapshot stream automatically.
Events
Code
Errors are always delivered as SdkError. A malformed depth frame (e.g. a level that isn't a [price, quantity] string tuple) is wrapped so the "error" listener always receives an SdkError, and the book goes stale before the listener runs — a throwing listener cannot bypass recovery.
What's next
- WebSocket Reference — the
DepthUpdatewire format - Data Types — decimal strings and precision