I run a visa agency in Bali. The other half of my week goes into Apify Actors, and one of them is an odds feed: pick leagues and dates, get one row per game × market × book, with DraftKings, Pinnacle and the Kalshi prediction market side by side.

The question I get asked most about it is some version of "which book is right". The question people should ask first is "are these three numbers even on the same scale". They are not, and the gap is bigger than most of the disagreements you will go hunting for.

Here is a run from today — 12 October 2026, NFL, four days ahead, moneyline, spread and total, three sources. Fifteen rows, two games, 2.4 seconds.

The same team, three implied probabilities

Monday night, Buffalo Bills at Los Angeles Rams. Moneyline, Rams to win:

Book Price Implied probability Overround Fair probability
DraftKings −180 64.29% 4.29% 61.64%
Pinnacle −171 63.10% 2.47% 61.58%
Kalshi −163 62.00% 1.00% 61.38%

Read the third column and the three books look 2.29 percentage points apart on who wins a football game. Read the fifth and they are 0.26 points apart — closer than the rounding on most odds screens.

Nothing moved between those two columns except arithmetic. Implied probability is just 1 / decimal_odds, and for a two-sided market the two implied probabilities sum to more than 100%. That excess is the overround — the bookmaker's margin, the vig, the cut. DraftKings' moneyline added up to 104.29%. Pinnacle's to 102.47%. Kalshi's to 101.00%.

Scale each side down by the sum and you get the fair, vig-free probability:

fair_home = implied_home / (implied_home + implied_away) 61.64% = 0.6429 / 1.0429 

Compare raw implied probabilities across books and you are mostly measuring which book charges more, not which book thinks differently. DraftKings looks like the most confident in the Rams in that table. It isn't. It is the most expensive.

De-vigging does not erase disagreement — it isolates it

The second game in the run, Seattle Seahawks at Denver Broncos, three days out:

Book Broncos price Implied Fair
DraftKings +130 43.48% 41.70%
Pinnacle +142 41.32% 40.12%
Kalshi +150 40.00% 40.00%

Naive gap: 3.48 points. Fair gap: 1.70 points. Half the apparent disagreement was margin, and half of it was real — DraftKings genuinely rates Denver higher than the other two do. That survivor is the part worth modelling.

That is why the rows carry both numbers. homeImpliedProbability is what the price literally says. homeFairProbability and overround are what you need to compare anything to anything. Ship only the first and every cross-book chart is wrong by the margin spread; ship only the second and nobody can reconstruct the price.

Six DraftKings rows in this run ran 4.26% to 4.75% overround. Six Pinnacle rows ran 2.41% to 4.01%. One Kalshi row came back at −0.01%, which is not a bug and not free money: Kalshi is an exchange, the price I quote is the ask on each side, and two asks can sum to a hair under 100% when the book is tight. There is no bookmaker margin there at all, only a bid-ask spread.

The field that says how much the sharp book means it

Pinnacle's public feed carries something nobody else publishes: the risk limit it will accept on each market. Same run, same two games:

Game Moneyline limit Spread limit Total limit
Bills at Rams (tonight) $50,000 $75,000 $10,000
Seahawks at Broncos (Thursday) $3,000 $5,000 $3,000

A number a book will take $75,000 on and a number it will take $3,000 on are not the same kind of fact, even though they arrive in the same JSON shape. The limit is a confidence and liquidity signal rolled into one, and it is the honest antidote to treating every row as equally solid.

One caveat before you build on it: part of that spread is time, because Pinnacle raises limits as kickoff approaches and one of these games starts tonight while the other is Thursday. Within a single game the ordering is cleaner — the Rams spread takes $75,000 and the Rams total takes $10,000, so the book is willing to risk 7.5× as much on the spread as on the points.

Kalshi's independent liquidity numbers agree about which market is real: $2,740,094 traded on the Monday-night game, $84,989 on Thursday's. Two unrelated venues, same verdict, 32× apart.

What a cross-book edge actually looks like

With two or more books on the same market the Actor emits edge rows. Across 14 odds rows this run produced exactly one, and zero arbitrage:

{ "type": "edge", "kind": "value", "game": "Buffalo Bills at Los Angeles Rams", "market": "total", "line": 54.5, "side": "under", "book": "DraftKings", "decimal": 1.98, "impliedProbability": 0.5051, "fairProbability": 0.5114, "sharpBook": "Pinnacle", "edgePercent": 1.257, "sharpLimitUsd": 10000, "booksCount": 2 } 

The arithmetic, end to end: Pinnacle priced that total over −101 / under −111, which is 1.990 and 1.901 in decimal, implying 50.25% and 52.61% — 102.86% together. Scale down and the sharp book's true read on the under is 51.14%. DraftKings paid −102 on the same under, decimal 1.98. Multiply: 0.5114 × 1.98 = 1.0126, so that price returns 1.26% more than the sharp line says it is worth.

Note sharpLimitUsd: 10000 on the row. A 1.26% edge against a number Pinnacle will take $10,000 on is a different proposition from a 1.26% edge against a number it will take $500 on, so the limit rides along.

And note what did not happen. The other game's totals were DraftKings 41.5 and Pinnacle 42.5. A full point apart, so they were never compared. Grouping is by market and line — total|54.5, not total — because "over at 41.5" and "over at 42.5" are different bets, and comparing their prices produces a confident, meaningless number. If you are merging odds feeds yourself, that grouping key is the bug you will ship first.

Arbitrage being empty is the normal answer, by the way. Books watch each other. An empty arbitrage list means the market is working, not that the scraper failed.

The rest of the potholes

  • Pinnacle throttles rather than queues. Bursts come back 401, 403 or 429, not a 503 you can politely retry. Grinding through six growing backoffs on the same address pushed a gateway call past its 48-second timeout once; now it gives up after two quick tries and changes address instead.
  • Pinnacle's feed mixes periods. Every matchup ships full-game, half and quarter markets in one list, all with the same shape. Filter on period 0 or your "NFL spreads" table is a third second-half lines.
  • Opening lines are DraftKings-only. openLine, openHomePrice and lineMovement come from DraftKings' own feed. Pinnacle does not publish an open, so those fields are null on its rows — it is a source difference, not a gap. Tonight's Rams spread opened −3 at −111 both ways and is now −3.5 at −102; Pinnacle is still on −3.
  • Nothing stores the past. Movement history beyond what DraftKings itself carries means scheduling your own runs now.

Calling it is one POST:

import requests rows = requests.post( "https://api.apify.com/v2/acts/lergassy~sports-odds-api/run-sync-get-dataset-items", params={"token": TOKEN}, json={ "mode": "odds", "leagues": ["nfl"], "daysAhead": 4, "markets": ["moneyline", "spread", "total"], "books": ["draftkings", "pinnacle", "kalshi"], "oddsFormat": "american", "minEdgePercent": 1, "maxItems": 24, }, ).json() for r in (x for x in rows if x["type"] == "odds" and x["market"] == "moneyline"): print(f'{r["book"]:12} {r["homePrice"]:>5} implied {r["homeImpliedProbability"]:.4f}' f' fair {r["homeFairProbability"]:.4f} vig {r["overround"]:.2%}') 

When this is the wrong tool

  • You need every price tick. This is a snapshot per run. Sub-minute movement means a licensed feed with a websocket, not a scraper on a schedule.
  • You need in-play odds. Full-game pre-match markets only. Live lines move faster than any run interval that makes sense.
  • You need settlement-grade data for a product that pays out. Public feeds go stale and get throttled. If money moves on the number, buy the number.
  • You want a tip service. The edge rows are arithmetic on two prices, nothing more. A 1.26% edge against a $10,000 limit is a measurement, not advice.

The Actor is Sports Betting Odds API — 28 leagues, props, futures and Kalshi contracts, no API key and no browser. The whole measurement in this post was 15 rows, about four and a half cents.

Whatever you pull odds from: before you compare two books, make sure the two numbers add up to the same thing.