Deep Orderbook Account (SDK Reference)#
The deep orderbook account is the single fixed-size Solana account that holds one market's entire resting order book — both bid and ask sides, stored as bounded red-black trees for price-time-priority matching. This page is the SDK-facing reference for that account: what it exposes, how to decode it, its capacity tiers, and the order-ID format. For the design rationale (why one account, why a single canonical YES book), see Design → Order Book; for the general trading model, see How It Works → Order Book.
Seesaw is prelaunch: this page describes the live on-chain layout used by every current SDK and offchain component. There is no legacy layout to migrate from or stay compatible with.
Public model: decoded bids and asks#
Every SDK decoder exposes the same public model regardless of language:
a decoded orderbook has a bids array and an asks array, each in
descending/ascending price-time priority, plus a header with the market
address, bump, capacity tier, and the next order-insertion sequence. This
shape has been stable since the account's first deep-tiered layout and
remains unchanged in the current version (internal header version 1).
Always decode through your language's SDK function — never parse the raw
account bytes yourself. The account's current wire format (header version
1) is an internal implementation detail: earlier and later header versions
fail closed in both the on-chain program and every SDK decoder, so a decoder
mismatch surfaces immediately as a decode error rather than a silent
misread. Because the on-chain layout can evolve between versions while the
decoded public shape does not, hand-rolled parsers are the one thing that
can break silently — always go through the maintained decoder:
| Language | Decoder function |
|---|---|
| TypeScript | decodeDeepOrderbookAccount |
| Rust | decode_deep_orderbook |
| Python | decode_deep_orderbook |
Discover an orderbook's size only from the supported capacity tiers below. Seesaw has no alternate sorted-array account layout: clients must reject data that does not decode as the current versioned deep account.
Capacity tiers and account sizes#
A market selects one symmetric bid/ask capacity tier at creation. The
account's exact byte size is a pure function of that tier — useful for
sizing RPC reads or EnsureDeepOrderbookSpace allocation planning:
| Capacity (orders per side) | Account size |
|---|---|
| 64 | 14,528 B |
| 128 | 28,864 B |
| 256 | 57,536 B |
| 512 | 114,880 B |
| 1024 | 229,568 B |
| 2048 | 458,944 B |
| 4096 | 917,696 B |
Larger tiers cost more rent and more compute per matching pass, bounded by the chosen tier — see Market Capacity for rent figures and allocation-prelude requirements, and Design → Order Book for why the account is sized this way.
Larger tiers can also need more BPF heap than a transaction's default
32-KiB heap frame provides for a dense matching pass. If an order
instruction against a large-tier market fails with a heap/memory
access-violation error rather than a program logic error, request a 64-KiB
heap frame (a RequestHeapFrame compute-budget instruction, prepended
alongside the compute-unit-limit/price instructions from Building
Transactions → Priority Fees and Compute
Budget)
instead of assuming the default 32-KiB heap is always sufficient.
Order IDs#
Order IDs are opaque 64-bit values — never parse them as a bit-packed
struct. For a market with capacity C, given the canonical side, physical
slot, and insertion sequence at fill time:
id = sequence * (2 * C) + (side == Ask ? C : 0) + slot + 1
Decoding is the inverse: divide id - 1 by 2 * C to recover the sequence,
and take the remainder to recover the side and slot. You should not need to
do this yourself — the SDK's per-order decode already resolves and validates
this for you, checking that the capacity is supported, the node is
currently occupied, the slot matches, the node/order/locator all agree, the
canonical side is correct, and the decoded sequence is below the account's
current insertion counter. A stale or reused ID fails that check rather
than silently resolving to the wrong order.
The account's raw insertion-counter field is named next_order_id at the
Rust byte-offset level, but every SDK exposes it under a clearer name:
nextOrderSequence in TypeScript, next_order_sequence in Rust and Python.
Redeem is bounded and retryable#
Redeem is paginated: a single call may only partially process a large
position, so the caller is responsible for looping. Authenticate the
finalized three-byte return value and keep re-sending the identical
instruction until its has_more flag reads false. Do not build a
single-send helper that assumes one call always finishes the redemption — a
partial call intentionally performs no burn or payout on that step, and
re-sending with the same arguments is safe and idempotent. The retry loop is
implemented in the official clients' source (for example, the web app's
portfolio flow), but treat public release/production-readiness of any given
client as a separate question from source availability; if you are building
your own integration, implement the has_more loop yourself. For the
Redeem account layout and a worked single-call example, see Building
Transactions → Redeeming After Resolution.
External-market allocation sequencing#
External-market creators should use the receipt-bound allocation planners and
wait for each confirmed/finalized preallocation transaction before submitting
the next dependent step or OpenExternalMarket. The legacy four-account,
receipt-free allocation instruction remains a permissionless donation-only
primitive: a third-party payer is not recorded as the creator and cannot rely
on a later rent refund or attribution. Use the creator-funded receipt plan when
funding must be attributable and refundable, and treat transaction finality as
part of the safety boundary before opening the market.
ForceClose emits two terminal event shapes#
Because ForceClose is retryable to completion, any indexer or event
consumer that wants a fully-authoritative final state must handle both
terminal event shapes it can emit: an older ForceClosed-only event, and
the current ForceCloseProgress event that also carries the exact
has_more state at the moment it fires. New terminals report their exact
after-state via has_more; older terminals imply a subtractive-collateral
projection instead. If you consume ForceClose events directly rather than
reading position state after the fact, decode both shapes rather than
assuming only the newer one appears.
Related#
- Design → Order Book — why a single account, single canonical book, and the trade-offs of that choice.
- How It Works → Order Book — matching rules and the user-facing trading model.
- Market Capacity — tier rent costs and allocation prelude sizing.
- Reading Data — how to fetch and decode a market's orderbook (and other accounts) end to end.