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

1. The SDK subscribes to the depth stream and requests an initial **snapshot** — the complete book at a point in time.
2. Every subsequent message is a **diff** ([`DepthUpdate`](/tools/typescript-sdk/reference/websocket#depthupdate)) — only the price levels that changed.
3. 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.

```ts
const book = client.orderBook("BTCUSD");

book.on("update", (lob, delta) => {
  // `delta` = only the levels that changed on this update
  // `lob`   = the full current book, queried on demand
  console.log(lob.bestBid(), lob.bestAsk());
});
```

## 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, `lastUpdateId` advances to `u`.
- **Gap** (`U > lastUpdateId`): a frame was missed. The book can no longer be trusted — it raises `ResyncRequiredError` internally 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:

1. Marks itself **stale** (`live = false`).
2. Emits a single `"resync"` event (deduplicated: one per stale period).
3. Requests a fresh snapshot in the background.
4. 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:

```ts
book.on("resync", () => {
  // The book is stale and rebuilding. Do not trade on it until
  // the next "update" arrives with the fresh snapshot.
  console.warn("Order book resyncing — treat current state as unavailable");
});
```

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

```ts
book.bestBid();          // { price, qty } | undefined — highest bid
book.bestAsk();          // { price, qty } | undefined — lowest ask
book.topN("bids", 10);   // Level[] — top 10 bids, best-first
book.spread();           // number | undefined — ask − bid (display only)
book.mid();              // number | undefined — midpoint (display only)
book.snapshot();         // { bids: Level[], asks: Level[] } — full book copy
```

`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](/tools/typescript-sdk/deep-dives/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

```ts
book.on("update", (lob, delta) => { /* book changed */ });
book.on("resync", () => { /* stale, rebuilding — protect yourself */ });
book.on("error", (err) => { /* SdkError — malformed frame, etc. */ });

// Auto-remove on abort:
const controller = new AbortController();
book.on("update", handler, { signal: controller.signal });
controller.abort();

book.close(); // stop updates, remove listeners, release the stream
```

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](/tools/typescript-sdk/reference/websocket#depthupdate) — the `DepthUpdate` wire format
- [Data Types](/tools/typescript-sdk/deep-dives/data-types) — decimal strings and precision
