I run a visa agency in Bali. The other half of my week goes into Apify Actors, and one of them is a feed of Pinnacle's own board: moneyline, spread and total for every game it prices, with the implied and vig-free probability on each side, and one field almost nobody else publishes — the maximum stake Pinnacle will accept on that market.
That field is why people buy the thing. Pinnacle runs on thin margins and does not close winning accounts, so its price is the market's reference, and the limit is read as the book's own confidence in its number: big limit, sure; small limit, unsure. I have written that sentence myself, in a post about stripping the vig out of odds data.
Having logged the same board three days running, I want to correct it. The limit is mostly a clock. Compare two games without controlling for time to kick-off and you are measuring the calendar, not the book's opinion.
The measurement
Three runs of the same input, one a day, 17–19 October 2026, each about three seconds:
{ "mode": "odds", "leagues": ["nfl", "nba", "mlb", "epl"], "markets": ["moneyline", "spread", "total"], "daysAhead": 7, "oddsFormat": "american" } 120, 120 and 111 rows — 40, 40 and 37 games, one row per game × market. Then I grouped the limitUsd field by game and lined the three snapshots up.
One game, three days, three limits
Everton at Arsenal, a Premier League fixture that was still a week out across all three runs:
| Snapshot | Hours to kick-off | Moneyline | Spread | Total |
|---|---|---|---|---|
| 17 Oct | 181.3 | $500 | $500 | $500 |
| 18 Oct | 157.4 | $1,000 | $1,000 | $675 |
| 19 Oct | 133.6 | $3,000 | $3,000 | $2,000 |
Six times the limit in forty-eight hours, on a game nobody learned anything new about. Coventry City at Tottenham, closer in, did the same: $3,000 at 66 hours, $4,500 at 42 hours, $7,500 at 19 hours.
And the values are not arbitrary. Every Premier League game in all three snapshots sat on one of exactly five rungs:
$500 / $500 / $500 $1,000 / $1,000 / $675 $3,000 / $3,000 / $2,000 $4,500 / $4,500 / $3,000 $7,500 / $7,500 / $5,000 Thirty-six soccer games, five triples. The limit is a step function, not a dial.
It is not even a clean countdown
Here is the detail that killed my original interpretation. On 18 October, Everton at Arsenal was 157.4 hours out and carried $1,000. On 19 October, Brighton at Liverpool was 157.6 hours out — the same distance, to twelve minutes — and carried $3,000.
What changed between the two readings was not the countdown. It was that the weekend's matchday finished. The whole next round moved up a rung at once, including games 160 hours away. Pinnacle's rungs track its own fixture cycle: which matchday is the next one, not how many hours your startTime subtraction produces.
So the honest version of "limits rise into kick-off" is: limits rise in jumps, on the book's calendar, and a game being the next thing on the board matters more than the raw hours.
Fourteen NBA games, one limit
Meanwhile, every basketball game in every snapshot:
moneyline $1,800 spread $3,000 total $1,125 Fourteen games, three days, distances from 42 to 121 hours out. Not one of them differed by a dollar. Celtics at Pistons gets the same number as Warriors at Lakers.
Build a "which games does the sharp book trust" dashboard on NBA limits and it ranks every game identically — stable, plausible and empty. There is no per-game signal there at all in that window.
Where the signal actually lives
There is one place in the data where limits do separate games — a window roughly a day and a half out. The 17 October snapshot caught the NFL board at 40.3 hours, eight games kicking off in the same window:
| Game | Spread limit |
|---|---|
| New York Jets at New England Patriots | $30,000 |
| Tennessee Titans at Indianapolis Colts | $30,000 |
| New Orleans Saints at New York Giants | $20,000 |
| Pittsburgh Steelers at Tampa Bay Buccaneers | $20,000 |
| Baltimore Ravens at Cleveland Browns | $10,000 |
| Chicago Bears at Atlanta Falcons | $10,000 |
| Carolina Panthers at Philadelphia Eagles | $10,000 |
Identical distance to kick-off, three different limits, a 3× spread. That is the book expressing an opinion.
By the next snapshot, 16.4 hours out, all eight had converged on $30,000. Nine NFL fixtures a week away were uniformly $2,000 / $3,000 / $2,000.
Uniform far out, differentiated in the middle, uniform again at the gate. Which means the intuition I had before I measured this — limits rising into kick-off means the book has settled on its number — had it backwards. Near kick-off everything is maxed and the differences are gone. The judgement is in the mid-window, and it is gone by Sunday morning.
The extreme case, for scale: Dallas Cowboys at Green Bay Packers went $20,000 / $30,000 / $5,000 at 48 hours, $20,000 / $30,000 / $10,000 at 24 hours, and $100,000 / $100,000 / $50,000 in the snapshot that caught it three minutes after kick-off, live.
The one ordering that holds everywhere
Across all 117 game-snapshots in the three runs, with zero exceptions:
total limit ≤ moneyline limit ≤ spread limit NBA: $1,125, $1,800, $3,000. NFL a week out: $2,000, $2,000, $3,000. The marquee game at kick-off: $50,000, $100,000, $100,000. The ranking of markets within a game is a stable property; the absolute numbers are the clock. If you want a confidence signal out of one row rather than a time series, that ratio is the part that survives.
Where the number comes from
Pinnacle's guest API is public — no key, no account, no browser. Two calls per sport: one returns every matchup with teams and start time, the other every straight market with prices, points and a limits array. The limit is the entry whose type is maxRisk.
Two traps in that feed:
- Markets are keyed by period. Period 0 is the full game; 1 and 3 are halves and quarters, shipped in the same list with the same shape. Forget to filter and a third of your "NFL spreads" are second-half lines, with their own much smaller limits — which is the fastest way to invent a confidence signal that is really a period mix-up.
- It throttles instead of queueing. Bursts come back 401, 403 or 429 rather than a polite 503. The Actor backs off six times with growing pauses and only then changes address.
The game list itself comes from ESPN, which is where eventId, venue and the ESPN link on each row come from, and the two sides get joined by team name plus a three-hour start-time window. Name matching is the fragile half: the tokeniser drops stop words like city and united, so Manchester United and Manchester City both reduce to manchester and happily match each other. A single matching token is therefore never enough — the join requires both teams to hit, plus the clock. If you are building this yourself, that pair of guards is the bug you will ship first.
A row, straight out of the 19 October run:
{ "league": "epl", "game": "Coventry City at Tottenham Hotspur", "startTime": "2026-10-19T19:00Z", "market": "spread", "line": -1.25, "homeTeam": "Tottenham Hotspur", "homePrice": 112, "awayPrice": -124, "homeDecimal": 2.12, "awayDecimal": 1.806, "homeImpliedProbability": 0.4717, "awayImpliedProbability": 0.5536, "homeFairProbability": 0.4601, "awayFairProbability": 0.5399, "overround": 0.0253, "limitUsd": 7500, "pinnacleMatchupId": "1637090767" } One flat shape per row, with the inapplicable columns null — a total row fills overPrice / underPrice and leaves the home and away columns empty, which keeps CSV exports honest instead of reshaping the header per market.
Median overround in that run, while I had the margins in hand: NFL spreads 2.91% against NFL totals 3.85%, NBA spreads 2.90% against NBA totals 3.38%. The spread is the cheapest market on the board in every sport, and the fourteen NBA spreads ran 2.86% to 2.91% — a band of five hundredths of a point across the league.
Pulling it yourself is one POST:
import requests from collections import defaultdict rows = requests.post( "https://api.apify.com/v2/acts/lergassy~pinnacle-odds-api/run-sync-get-dataset-items", params={"token": TOKEN}, json={"mode": "odds", "leagues": ["nfl", "nba", "epl"], "markets": ["moneyline", "spread", "total"], "daysAhead": 7}, ).json() limits = defaultdict(dict) for r in rows: limits[r["game"]][r["market"]] = r["limitUsd"] for game, m in sorted(limits.items(), key=lambda kv: -(kv[1].get("spread") or 0)): print(f'{m.get("moneyline"):>7} {m.get("spread"):>7} {m.get("total"):>7} {game}') Run that on a schedule and keep the output — the time series is the product, not the snapshot.
When this is the wrong tool
- You want limits as a standalone confidence score. They are not one, unless you compare inside the same sport and the same distance-to-kick-off band. Most of the variance is the calendar.
- You need line-movement history today. Nothing stores the past. Three snapshots is why I could write this post; one snapshot would have taught me the wrong lesson.
- You need in-play or sub-minute prices. Full-game pre-match markets, one reading per run. Live lines move faster than any sane schedule.
- Money settles on the number. Public feeds get throttled and go stale. If payouts depend on it, buy a licensed feed.
The Actor is Pinnacle Odds API — 20 leagues, no key, no browser, two JSON calls per sport. The 19 October run was 111 rows at $0.002 each, about 22 cents, plus eight hundredths of a cent of platform usage for 3.1 seconds of compute.
Whatever you read limits out of: before you call a number a signal, hold the clock still and see if it survives.

