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

# Prediction Markets & Exchanges - Orderbook Depth & Limits

> Build market-making and arbitrage systems on OddsPapi: aggregated exchange prices with back/lay orderbook depth, native venue IDs for order routing, stake limits, and measured per-venue delay.

Market making across prediction markets means holding a consistent view of several venues at once — prices, depth, the identifiers you need to send an order back, and an honest answer to "is this quote still real?". OddsPapi normalizes all of that into one stream, so a venue you add is a filter change, not an integration project.

This guide is about the trading decisions on top of that stream: deriving a tradeable view, sizing against real depth, knowing when a price is no longer actionable, and behaving correctly when the connection breaks. Payload schemas and wire examples live on the [`odds`](/websocket/channels/odds) channel page — this page assumes you have read it.

***

## What you get per price

Every outcome carries the market-making primitives in one object — no secondary lookup:

| Field                     | What it gives you                                                        |
| ------------------------- | ------------------------------------------------------------------------ |
| `price`                   | Best available price (decimal), plus `priceAmerican` / `priceFractional` |
| `meta.back` / `meta.lay`  | Orderbook ladders — `{ price, size }`, best price first                  |
| `limit`                   | Maximum accepted stake at this price                                     |
| `bookmakerMarketId`       | The venue's own market identifier                                        |
| `bookmakerOutcomeId`      | The venue's own selection identifier                                     |
| `bookmakerChangedAt`      | The venue's change timestamp (t0)                                        |
| `changedAt`               | When OddsPapi observed the change (t1)                                   |
| `active` / `marketActive` | Hard stops — do not quote against a false                                |

The four-stamp chain (`bookmakerChangedAt` → `changedAt` → envelope `ts` → your receipt) lets you measure delay per venue yourself rather than take it on trust. See [Latency and freshness](/api-reference/reliability#latency-and-freshness) for the published per-book figures.

***

## Deriving a tradeable view

`meta.back` and `meta.lay` are uniform across every exchange and prediction market: arrays of `{ price, size }`, best price first, with identical semantics everywhere. The first `back` entry is top of book and matches the outcome's `price`. See [Advanced metadata](/websocket/channels/odds#advanced-metadata-meta) for the exact shape.

Because the shape does not vary, cross-venue comparison is direct — no per-venue parsing branch, and no normalization layer of your own to keep in sync as venues change their APIs.

**Best bid and best offer across venues.** Group every quote by selection, then take the highest `back` price and the lowest `lay` price across the venues you are permissioned for. Both sides matter even if you only ever take one: the tightest spread tells you where the consensus actually sits, and a venue quoting far outside it is either carrying information or carrying a stale price — which of the two you decide with the timestamps, not the price.

**Crossed and locked books are a signal, not an opportunity.** If the best `back` on one venue exceeds the best `lay` on another, check `changedAt` on both sides before you treat it as arbitrage. A genuine cross closes in milliseconds; a persistent one almost always means one side has stopped updating. Gate on the staleness checks below before sizing into it.

**Depth is per venue, not aggregate.** Summing ladders across venues gives you a number you cannot trade against — each venue's queue is separate, and you cannot fill one venue's order against another's resting liquidity. Model per-venue executable size, then aggregate the *outcomes*, not the books.

**Sizes carry the venue's own denomination.** `size` is in the venue's base currency. For crypto venues, convert through the [`currencies`](/websocket/channels/currencies) channel before you compare depth or set position limits, otherwise a large number on one venue and a small number on another can mean the same exposure.

***

## Sizing: `limit` and depth do different jobs

Two size signals arrive on the same row and bound different things — conflating them over-sizes you:

* **`limit`** bounds a single order — the most the venue accepts at this price. See [Limits](/websocket/channels/odds#limits).
* **The ladder** bounds the fill — what rests behind the touch, and what it costs to clear a given size.

Walk the ladder to your target size and price the fill at the volume-weighted average, not at top of book. Limit cuts arrive as ordinary odds updates on the same `oddsId` and are often the earlier signal. Full treatment, including traditional books where `limit` is the only size signal, in [sharp-line trading & CLV](/guides/sharp-line-trading#limit-and-depth-answer-different-questions).

***

## Knowing when a quote is tradeable

Price freshness is a separate question from price value, and for automated trading it is the one that costs money when you get it wrong. Four independent gates, each of which can veto:

| Gate           | Source                                         | Meaning                                                                     |
| -------------- | ---------------------------------------------- | --------------------------------------------------------------------------- |
| `active`       | `odds`                                         | This selection is not currently available                                   |
| `marketActive` | `odds`                                         | The whole market is down — every selection under it is untradeable          |
| `suspended`    | [`bookmakers`](/websocket/channels/bookmakers) | The venue has suspended this fixture                                        |
| `staleOdds`    | [`bookmakers`](/websocket/channels/bookmakers) | **Our connection to the venue was lost.** Freshness is no longer guaranteed |

`staleOdds` is the one people miss, because nothing about the last price you received looks wrong. The prices you hold may have moved at the venue with no way for us to tell you. Treat everything from that venue as unvalidated until it flips back to `false`, and pull your quotes rather than re-price them. Detail in [Critical fields for automated trading](/websocket/channels/bookmakers#critical-fields-for-automated-trading).

Add a fifth gate of your own: **an age check against the published bound.** Each book publishes `maxDelayLiveInSec` / `maxDelayPregameInSec` on `GET /bookmakers`. If `now − changedAt` exceeds that book's bound by a comfortable margin, the quote is out of contract even though no flag has fired — a quiet venue and a broken venue look identical for the first few seconds. See [Status signals](/coverage/bookmakers#status-signals).

Wire all five into a single "can I quote against this right now" predicate, evaluated on every update rather than on a timer. One predicate, one place to fix.

For fixture markets, `participantsRotated` on the `bookmakers` channel is a sixth check of a different kind: when true, that venue's home/away assignment is flipped relative to ours, which reverses the meaning of side-based outcomes and flips the sign of handicaps. It affects correctness rather than freshness, and it is silent if you map outcomes by position instead of by ID.

***

## Delay budgeting

Your reaction time is the sum of four segments, and you control exactly one of them:

1. Venue → OddsPapi observation (`changedAt − bookmakerChangedAt`) — published per book, measured where the venue gives usable timestamps.
2. Conflation — a fixed \~10 ms batching window; the newest value always lands, so this bounds staleness rather than dropping data.
3. Gateway → you (`your receipt − ts`) — the delivery hop, the one segment you can shorten. See [Server location](/api-reference/concepts#server-location-matters).
4. Your own decision and order round-trip back to the venue.

Measure segment 3 continuously from your own clock; it is the segment that degrades first and the one your infrastructure decisions actually move. Budget segments 1 and 4 against the venue's update cadence: if a venue re-prices faster than your loop closes, you are not competing on that venue's touch and should be quoting behind it or sizing smaller, not chasing.

Per-venue delay is also why a uniform quoting policy across venues is wrong. A 0.05 s venue and a \~1 s venue justify different spreads for the same underlying, and the numbers to parameterize that are published rather than something you have to infer. See [sharp-line trading & CLV](/guides/sharp-line-trading) for the same reasoning applied to line taking.

***

## Surviving gaps

A market maker's worst state is quoting from a book it believes is current and is not. The correct disconnect behavior is therefore ordered: **pull quotes first, reconcile second.**

* **Server-initiated reconnect.** Before a deploy or rebalance we push a `reconnect` control frame and keep streaming for a short grace window. Act on the frame, not on the eventual socket error — you get a clean handover instead of a gap.
* **Within the replay window.** Reconnect with your per-channel cursor (`serverEpoch` + `entryId`) and missed updates replay with no gaps.
* **Outside it.** The gateway emits `snapshot_required`; restore with one REST snapshot. This is a designed path with a bounded worst case, not an error.
* **On a changed `serverEpoch`**, your old cursors are meaningless — re-snapshot rather than resume.

Full mechanics in [Resume & Replay](/websocket/resume-replay). The discipline that matters: your book's "last known good" timestamp per venue should gate quoting independently of whether your socket happens to be up, because a live socket with a stale venue behind it is the failure that actually hurts.

***

## Routing orders back to the venue

`bookmakerMarketId` and `bookmakerOutcomeId` travel **on every price, on the live stream**. When your strategy picks a selection, you already hold the venue's own identifiers — no mapping table to maintain, no second call before you can send. This matters most exactly when it is hardest: the moment you want to act is the moment you least want a lookup in the path.

At fixture level, `GET /fixtures/mapping` resolves in both directions — pass `fixtureIds` to get the venue's IDs, or `bookmakerFixtureIds` to go from a venue ID back to the OddsPapi fixture. The [`bookmakers`](/websocket/channels/bookmakers) channel also carries `bookmakerFixtureId` and `fixturePath` per venue per fixture, so a manual desk can open the venue's own page for the same market.

***

## Pricing across venues

Group by `marketId` before you price anything. All outcomes under one `marketId` form a complete probability space, which is what overround, normalization, and completeness checks require — and each distinct line value is its own `marketId`, so a market never silently mixes lines.

From there the usual construction applies: normalize each venue's quotes within the market, then combine across venues. Two weightings are worth carrying explicitly, because the data supports both:

* **By size** — a venue showing real depth and a high `limit` is a stronger observation than one quoting a token amount at the same price.
* **By delay** — a venue with a measured sub-100 ms bound and one with a multi-second bound are not equally current, and the published per-book figures let you weight that rather than guess it.

For post-hoc work — fill auditing, backtests, closing lines — the REST [historical odds and CLV](/api-reference/concepts#historical-odds-and-clv) endpoints share the same `oddsId` key as the live stream, so live decisions and their audit trail join without a translation step.

***

## Non-sports markets are futures

Politics, elections, finance, crypto, and weather are modeled as sports with `sportId` 69+ and exist as **futures only** — one future per outright market:

```
{futureId}:{bookmaker}:{futureOutcomeId}:{participantId}
```

Use the [`futures`](/websocket/channels/futures) and [`oddsFutures`](/websocket/channels/oddsFutures) channels, which follow the same delivery semantics and carry the same orderbook `meta`. Everything above about gating, sizing, and depth applies unchanged. See [How Futures Work](/api-reference/concepts#how-futures-work-prediction-markets) for the model and [Coverage](/coverage) for the sportId bands.

**Which question a future asks** is `market.marketId` on the `futures` payload — the 5-digit `futureMarketId` also carried in the `futureId` tail, allocated per sport (`69002` = Politics Prediction).

**Which selection a price is on** is the last two segments of the key, and which one carries it depends on the market's shape. Participant-keyed markets (`winner`, `topscorer`, `relegation`, `mvp`) own no outcome rows: the selection is the entity you back, and `futureOutcomeId` is the sentinel `0`. Decomposed markets (Yes / No, Over / Under) carry a real `futureOutcomeId` per side per line — one future market spans many lines, and unlike fixture markets the handicap sits on the outcome rather than on the market. Read both from the explicit fields on the odds row rather than splitting `oddsId`; see [Future odds IDs](/api-reference/concepts#future-odds-ids).

***

## Wiring it up

<Steps>
  <Step title="Snapshot">
    Pull current state over REST (`/fixtures/odds` or `/futures/odds`) to hydrate your book.
  </Step>

  <Step title="Stream">
    Connect to `wss://v5.oddspapi.io/ws` and log in with the channels, sports, and venues you trade. Subscribe to `bookmakers` alongside `odds` — without it you have prices but no staleness signal.
  </Step>

  <Step title="Apply updates">
    Key by `oddsId` and overwrite in place. Each message is state, not a tick.
  </Step>

  <Step title="Gate">
    Evaluate the quoting predicate on every update, per venue, before anything downstream sees the price.
  </Step>

  <Step title="Recover">
    Handle `reconnect` and `snapshot_required` as designed paths. See [Resume & Replay](/websocket/resume-replay).
  </Step>
</Steps>

Filter at login — `sportIds`, `tournamentIds`, and `bookmakers` cut volume far more effectively than client-side filtering — and prefer `receiveType: "zstd"` on `odds` at scale ([Compression](/websocket/compression)). Login shapes and a worked client are on the [`odds`](/websocket/channels/odds) channel page and in the [Quickstart](/quickstart).

***

## What you can rely on

* **One orderbook shape, not one per venue.** `meta.back` / `meta.lay` are `{ price, size }` arrays with identical semantics across every exchange and prediction market, so you write one parser rather than a per-venue adapter set. Unknown `meta` keys are additive; the `back` / `lay` core will not be repurposed.
* **Venue-native IDs ride on the price itself.** `bookmakerMarketId` and `bookmakerOutcomeId` are on the live odds row, so routing an order back to the venue costs no second lookup and no reverse-mapping table of your own.
* **Size is a first-class field.** `limit` is normalized across books and carried for close to every sharp book, so what you can actually get filled for is in the same payload as the price.
* **Loss of contact is reported, not hidden.** `staleOdds` tells you when we can no longer vouch for a venue's prices, which is the difference between pulling quotes and unknowingly trading against a frozen book.
