Betting Exchange Odds by API: Prices, Liquidity and Normalization

How betting exchange and prediction market prices are normalized into a sportsbook schema, what the liquidity field means, how to handle commission, and liquidity-aware best prices in code.

A betting exchange price is not posted by a bookmaker. It is the best price other users are currently offering, and it comes with a quantity attached: how much could trade at that price before the next level of the order book. Put exchange data next to sportsbook data without handling that difference and you get a board where a $40 order on a thin exchange market looks like the best price in the country.

This post is the engineering view of exchange data in PreciseAPI. It covers which exchange and prediction market sources the feed includes, how their prices are normalized into the same schema as sportsbooks, what the liquidity field on every exchange outcome means, how to account for commission without hard-coding anyone's fee schedule, and how to build liquidity-aware consensus and best-price logic. For the conceptual comparison of venues, read prediction markets vs sportsbooks; for contract-specific conversion and tick sizes, read Kalshi and Polymarket data. This post does not repeat either.

Which exchange sources are in the feed

The catalog has eight sources of type: "exchange", split by tag:

SourceidTag
Novignovigexchange
Prophetprophetxexchange
4cx4cxexchange
Kalshikalshiprediction
Polymarketpolymarketprediction
Polymarket USpolymarket_usprediction
Fanatics Marketsfanatics_marketsprediction
DraftKings Predictionsdk_predictionsprediction

The exchange tag marks peer-to-peer betting exchanges; prediction marks event-contract venues. Both kinds share the same type, the same response shape and the same liquidity field, so most of what follows applies to all eight. List them, with live status and the leagues each currently covers, from the free sportsbooks endpoint:

curl "https://api.preciseapi.com/v1/sportsbooks?type=exchange" \
  -H "Authorization: Bearer $PRECISEAPI_KEY"

Availability of these venues varies by jurisdiction. The feed reports their prices; it says nothing about who can trade on them.

How exchange prices are normalized

Exchange prices go through the same normalization as every sportsbook price, which is what makes them usable without special cases:

Which market keys a venue lists varies by venue and event. GET /v1/events/{id} returns the sources currently pricing an event and every market key available, which is a cheap way to see what an exchange covers before requesting odds.

What differs is the bookmaker type and the extra field on each outcome. A trimmed /v1/odds response, with illustrative values:

{
  "id": "evt_...",
  "bookmakers": [
    {
      "id": "novig",
      "name": "Novig",
      "type": "exchange",
      "updated_at": "2026-10-08T23:41:07.000Z",
      "markets": [
        {
          "key": "moneyline",
          "period": "full_game",
          "outcomes": [
            { "id": "sel_...", "name": "Home Team", "side": "home", "price": 1.95, "liquidity": 4200 },
            { "id": "sel_...", "name": "Away Team", "side": "away", "price": 2.0, "liquidity": 3150 }
          ],
          "suspended": false,
          "updated_at": "2026-10-08T23:41:07.000Z"
        }
      ]
    }
  ]
}

What liquidity means

liquidity is the size available at the quoted price, in US dollars. It is attached per outcome, so the two sides of a market usually show different amounts, and it changes as orders are placed, filled and cancelled. Sportsbook outcomes do not carry the field.

Read it as an answer to one question: how much could trade at this exact price right now? It does not describe:

The useful rule of thumb follows from that. A price backed by $4,200 is information about where the market is. A price backed by $40 may be one participant's stale order. Your code should treat them differently.

Exchange overround

Because exchange prices come from competing participants rather than a bookmaker's margin, the two sides of a liquid market usually sum close to 1 in implied probability. Three illustrative moneyline markets on the same game:

VenueHomeAwaySum of impliedOverroundHome liquidity
novig1.95 (-105)2.00 (+100)1.01281.28%$4,200
prophetx1.93 (-108)2.02 (+102)1.01321.32%$900
4cx2.00 (+100)1.96 (-104)1.01021.02%$250

Compare that with 4.76% for a -110 / -110 sportsbook market. Two caveats keep the comparison honest. The quoted prices exclude commission or trading fees, covered next. And a tight sum on a thin market is not a sign of quality; it can just as well be two stale orders. The hold comparison post treats exchange margins as a separate series for exactly this reason.

Commission: inject it, don't bake it in

Exchanges typically charge commission or fees on top of the quoted price, and prediction markets often charge trading fees. Structures differ: commission on net winnings, per-contract fees, maker and taker rates, promotional periods. Schedules also change over time. PreciseAPI prices reflect the market, not any account's costs, so commission belongs in your code as a function you can update and version.

For a commission charged as a share c of net winnings, the effective decimal price is:

effective_decimal = 1 + (decimal - 1) × (1 - c)

With hypothetical rates on a 2.10 (+110) price:

Hypothetical commissionEffective decimalEffective American
0%2.100+110
2%2.078+108
4%2.056+106

Neither rate describes a real venue. Plug in each venue's current published terms. The Kalshi and Polymarket post shows the same pattern for per-contract fee shapes.

// Effective decimal price after costs. The rates below are placeholders:
// set each one from the venue's current published terms, and version the table.
const onWinnings = (rate) => (d) => 1 + (d - 1) * (1 - rate);
const COSTS_V1 = {
  novig: onWinnings(0),
  prophetx: onWinnings(0),
  '4cx': onWinnings(0),
};
const effective = (book, d) => (COSTS_V1[book] ?? ((x) => x))(d);

Store quoted prices and apply costs at read time. If you bake commission into stored prices, you cannot reprocess history when a schedule changes.

Liquidity-aware consensus

Exchanges are often used as part of a reference price alongside sharp books. The two decisions are which exchange prices to admit and how much weight each gets. A simple, defensible scheme:

  1. De-vig each venue's two-sided market on its own.
  2. Drop venues whose liquidity on the outcome is below a floor, for example $500.
  3. Weight the rest by liquidity, capped so one deep book cannot dominate, for example at $5,000.

Applied to the table above for the home side:

VenueFair home probabilityLiquidityWeight
novig0.5063$4,2004,200
prophetx0.5114$900900
4cx0.4949$250excluded (below floor)

(0.5063 × 4200 + 0.5114 × 900) / 5100 = 0.5072, a fair decimal price of 1.972 (-103). The 4cx price is not wrong, but at $250 it carries too little size to move the reference. The no-vig guide covers how to combine this exchange consensus with sharp sportsbook prices.

Liquidity-aware best price

/v1/best-lines returns the best quoted price per outcome across sources, with every book at that price in best_books. It includes exchanges and excludes DFS apps. It compares quoted prices only: it does not apply commission and does not consider liquidity. In the table above it would report 4cx at 2.00 as the best home price, backed by $250.

That is the right primitive for a general odds screen. If your product displays a best price for a stated size, post-filter with the liquidity from /v1/odds:

const API = 'https://api.preciseapi.com/v1';
const headers = { Authorization: `Bearer ${process.env.PRECISEAPI_KEY}` };
const SIZE = 1000; // USD the displayed price must cover

const res = await fetch(`${API}/odds?league=nba&markets=moneyline&format=decimal`, { headers });
if (!res.ok) throw new Error((await res.json()).error.message);
const { data } = await res.json();

for (const event of data) {
  const best = new Map(); // outcome id -> { book, price, liquidity }
  for (const bm of event.bookmakers) {
    const m = bm.markets.find((x) => x.key === 'moneyline' && x.period === 'full_game');
    if (!m) continue;
    for (const o of m.outcomes) {
      if (bm.type === 'exchange' && (o.liquidity ?? 0) < SIZE) continue; // too thin for this size
      const price = bm.type === 'exchange' ? effective(bm.id, o.price) : o.price;
      const cur = best.get(o.id);
      if (!cur || price > cur.price) best.set(o.id, { name: o.name, book: bm.id, price, liquidity: o.liquidity ?? null });
    }
  }
  console.log(event.away.abbr, '@', event.home.abbr, [...best.values()]);
}

Sportsbook outcomes pass through untouched, since their prices are good up to limits the feed does not report. Exchange outcomes must cover the size and are compared after costs. Change SIZE and the answer changes, which is the point: on an exchange, the best price is a function of size.

Showing liquidity in a UI

Most users do not need the exact dollar figure, but they do need to know when a price is thin. A few display patterns work well on odds screens that mix exchanges with sportsbooks:

These are presentation choices layered on the same data; none of them changes what the API returns. The odds screen post covers the surrounding architecture.

Streaming and history

The WebSocket stream pushes exchange market changes in the same shape as REST, with liquidity on each exchange outcome, so size can be tracked alongside price without extra requests. Streaming is on Pro and above, and the sandbox includes one connection. On REST, /v1/odds costs 2 credits per call and /v1/best-lines 3.

/v1/history records timestamped prices for exchange sources on main full-game moneyline, spread and total markets, with book=novig or any other exchange id. It stores prices only, not liquidity. If you need size history, for example to study how deep a market was when it moved, archive it yourself from the stream:

CREATE TABLE exchange_quote (
  observed_at  TIMESTAMPTZ NOT NULL,
  event_id     TEXT NOT NULL,
  book         TEXT NOT NULL,     -- novig, prophetx, 4cx, kalshi, ...
  outcome_id   TEXT NOT NULL,     -- joins to sportsbook rows
  price_dec    NUMERIC NOT NULL,  -- quoted, before costs
  liquidity    NUMERIC,           -- USD at the quoted price
  PRIMARY KEY (event_id, book, outcome_id, observed_at)
);

Data-quality checks

A few checks catch most exchange-specific problems:

The prediction markets API page summarizes exchange coverage, the prediction market tools use case shows common builds, and plan limits are on pricing.

Frequently asked questions

Which betting exchanges does PreciseAPI include?

Three sources are tagged exchange: Novig (novig), Prophet (prophetx) and 4cx (4cx). Five prediction market sources share the same exchange type: Kalshi, Polymarket, Polymarket US, Fanatics Markets and DraftKings Predictions. GET /v1/sportsbooks?type=exchange lists all eight.

What unit is the liquidity field in?

US dollars available at the quoted price, per outcome. It is present on exchange and prediction market outcomes and absent on sportsbook outcomes.

Is liquidity the full order book?

No. It is the size at the quoted price only. An order larger than that would fill partly at worse levels, which the field does not describe.

Do exchange prices include commission?

No. Prices reflect the market. Apply each venue's current commission or fee terms in your own code, preferably as a versioned function so you can reprocess history.

Does /v1/best-lines account for liquidity?

No. It compares quoted prices across sportsbooks and exchanges and lists every book at the best price. For size-aware or fee-adjusted best prices, post-process /v1/odds, which carries liquidity on each exchange outcome.

Can I get line history for exchange prices?

Yes, for main full-game moneyline, spread and total markets through /v1/history. History stores prices, not liquidity, so archive liquidity yourself if you need it.