> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polynode.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Orderbook Streaming and PN1

> Stream prediction-market depth, maintain local books, and optionally fail closed on PN1 sequence or checksum errors.

The orderbook WebSocket is separate from the settlement stream. It emits full snapshots, absolute depth updates, price changes, and last-trade metadata for selected outcome token IDs.

Use one of three levels:

| Level                | Best for                                                                                        |
| -------------------- | ----------------------------------------------------------------------------------------------- |
| Raw orderbook stream | applications that already manage book state                                                     |
| `LocalOrderbook`     | applying messages to an in-memory book yourself                                                 |
| `OrderbookEngine`    | managed connection, local state, filtered views, reconnect recovery, and optional PN1 integrity |

For most applications, start with `OrderbookEngine`.

## Managed local book

Set `TOKEN_ID` to a Polymarket outcome token ID. You can obtain token IDs from V3 market data or the `clobTokenIds` / `clob_token_ids` field on a [short-form rotation](/sdks/short-form).

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    import {
      OrderbookEngine,
      type BookSnapshot,
      type BookUpdate,
    } from 'polynode-sdk';

    const apiKey = process.env.POLYNODE_API_KEY;
    const tokenId = process.env.TOKEN_ID;
    if (!apiKey || !tokenId) throw new Error('Set POLYNODE_API_KEY and TOKEN_ID');

    const engine = new OrderbookEngine({ apiKey });
    engine.on('ready', () => {
      console.log('best bid', engine.bestBid(tokenId));
      console.log('best ask', engine.bestAsk(tokenId));
      console.log('midpoint', engine.midpoint(tokenId));
    });
    engine.on('update', (update: BookSnapshot | BookUpdate) => {
      console.log(update.type, update.asset_id);
    });

    await engine.subscribe([tokenId]);
    process.once('SIGINT', () => engine.close());
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import asyncio
    import os
    from polynode import OrderbookEngine

    async def main() -> None:
        token_id = os.environ["TOKEN_ID"]
        engine = OrderbookEngine(api_key=os.environ["POLYNODE_API_KEY"])
        engine.on(
            "ready",
            lambda: print(
                "touch",
                engine.best_bid(token_id),
                engine.best_ask(token_id),
                engine.midpoint(token_id),
            ),
        )
        engine.on("update", lambda update: print(update.type, update.asset_id))
        try:
            await engine.subscribe([token_id])
            await asyncio.Event().wait()
        finally:
            engine.close()

    asyncio.run(main())
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    use polynode::{EngineOptions, OrderbookEngine};

    #[tokio::main]
    async fn main() -> polynode::Result<()> {
        let api_key = std::env::var("POLYNODE_API_KEY")
            .expect("Set POLYNODE_API_KEY");
        let token_id = std::env::var("TOKEN_ID").expect("Set TOKEN_ID");
        let engine = OrderbookEngine::connect(&api_key, EngineOptions::default()).await?;
        engine.subscribe(vec![token_id.clone()]).await?;

        loop {
            if let Some(book) = engine.book(&token_id).await {
                println!("{} bids, {} asks", book.0.len(), book.1.len());
                println!("midpoint {:?}", engine.midpoint(&token_id).await);
                break;
            }
            tokio::task::yield_now().await;
        }

        engine.close().await?;
        Ok(())
    }
    ```
  </Tab>
</Tabs>

`subscribe()` resolves after the server acknowledges the requested identifiers. The local book becomes readable after its initial snapshot arrives. TypeScript and Python emit `ready` when every acknowledged token has a valid baseline.

## Book update rules

Apply raw messages with these rules:

| Message            | Effect on depth                                                               |
| ------------------ | ----------------------------------------------------------------------------- |
| `book_snapshot`    | replace the complete book for that token                                      |
| `book_update`      | upsert each absolute price level; size `"0"` removes it                       |
| `price_change`     | upsert the absolute level on `BUY` bids or `SELL` asks; size `"0"` removes it |
| `last_trade_price` | update trade metadata only; never mutate depth                                |

Prices and sizes are decimal strings. Keep them as strings or use a decimal library for exact calculations. Converting them to binary floating point can change level identity and is incompatible with PN1 checksum verification.

## Enable PN1 integrity

PN1 lets the SDK detect a missing or out-of-order depth message instead of continuing with a book that merely looks plausible. It validates:

* a trusted anchor snapshot for each token
* sequence continuity across depth changes
* the checksum after every verified mutation

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const engine = new OrderbookEngine({
      apiKey,
      integrity: true,
      allowStaleReads: false,
    });

    engine.on('integrity_error', (error) => {
      console.error(error.token, error.code, error.message);
    });
    await engine.subscribe([tokenId]);
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    engine = OrderbookEngine(
        api_key=os.environ["POLYNODE_API_KEY"],
        integrity=True,
        allow_stale_reads=False,
    )
    engine.on(
        "integrity_error",
        lambda error: print(error.token, error.code, error),
    )
    await engine.subscribe([token_id])
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    let engine = OrderbookEngine::connect(
        &api_key,
        EngineOptions {
            integrity: true,
            allow_stale_reads: false,
            ..Default::default()
        },
    ).await?;

    let mut errors = engine.integrity_errors();
    tokio::spawn(async move {
        while let Ok(error) = errors.recv().await {
            eprintln!("{} {:?}: {}", error.token, error.code, error.message);
        }
    });
    engine.subscribe(vec![token_id]).await?;
    ```
  </Tab>
</Tabs>

PN1 mode requires explicit market identifiers. A wildcard/firehose orderbook subscription is not available with integrity enabled.

## Fail-closed lifecycle

With `allowStaleReads` / `allow_stale_reads` left at its default `false`, the local book moves through these states:

| State          | Readable? | Meaning                                                |
| -------------- | --------- | ------------------------------------------------------ |
| `initializing` | No        | waiting for a valid anchor                             |
| `ready`        | Yes       | current sequence and checksum are valid                |
| `stale`        | No        | connection was lost                                    |
| `resyncing`    | No        | a replacement anchor was requested                     |
| `failed`       | No        | integrity could not be recovered on the current stream |

On a sequence or checksum failure, the engine gates only the affected token and requests a fresh anchor. It does not serve the last book as verified state.

Set stale reads to `true` only when your application deliberately prefers availability over verified depth. Surface that choice to downstream consumers; a stale book should not be mistaken for current market state.

## Reconnects

Orderbook reconnect is automatic by default. The SDK replays the active token set and waits for fresh snapshots. Sequence state from the old connection is never reused as if it belonged to the replacement connection.

Application handlers remain attached across reconnects. You do not need to register them again.

## Filtered views

Views share the engine's connection and state but receive updates only for their token set.

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    const view = engine.view([tokenId]);
    view.on('update', (update) => console.log(update.asset_id));
    console.log(view.midpoint(tokenId));

    view.setTokens([anotherTokenId]);
    view.destroy();
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    view = engine.view([token_id])
    view.on("update", lambda update: print(update.asset_id))
    print(view.midpoint(token_id))

    view.set_tokens([another_token_id])
    view.destroy()
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    let mut view = engine.view(vec![token_id.clone()]);
    if let Some(update) = view.next().await {
        println!("{update:?}");
    }
    view.set_tokens(vec![another_token_id]).await;
    ```
  </Tab>
</Tabs>

Use views when several application components need different slices of one shared book connection.

## Raw stream

Choose the raw client when you need protocol messages or maintain state elsewhere:

* TypeScript: `pn.orderbook.subscribe(tokenIds)` and `pn.orderbook.on(...)`
* Python: `await pn.orderbook.subscribe(token_ids)` and `pn.orderbook.on(...)`
* Rust: `client.orderbook_stream(ObStreamOptions::default()).await?`

In raw mode, your code is responsible for applying snapshots and deltas correctly. `LocalOrderbook` provides the same deterministic update logic without the higher-level engine.

## Cleanup

<Tabs>
  <Tab title="TypeScript">
    ```typescript theme={null}
    engine.close();
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    engine.close()
    ```
  </Tab>

  <Tab title="Rust">
    ```rust theme={null}
    engine.close().await?;
    ```
  </Tab>
</Tabs>

See the [orderbook protocol](/orderbook/overview), [message reference](/orderbook/messages), and [PN1 integrity reference](/orderbook/integrity) for wire-level details.
