STEP 0 disproved the premise before any request was fired. PropLine's OpenAPI contract states verbatim that `bookmakers` omitted = ALL books, so proplineAdapter omitting it is correct and always was. Firing a guessed param would have RESTRICTED the response and produced exactly the false negative the order warned about. The real cause is ours: PropLine sends 18 books; oddsNormalizer ALLOWED_BOOKS intersects them at exactly 5 -- which is precisely the "5 MLB books" the 2.18 audit measured. Measured on real public data (no key, no quota): 4.41 books/prop from the feed, 1.50 after our filter, and 12 of 34 props go invisible entirely. Also corrected: "73% single-book" is the long tail of deep props sole-posted by DraftKings or Bovada. On the core props we grade, the market is 10-12 books wide. pinnacle appears on 0 of 40 MLB props -- the independent low-vig references present on 100% of core props are exchanges (novig/smarkets/kalshi). DFS pick'em also covers 100% but is not a market price and must never enter a consensus. Verdict is outcome (d) ALREADY OPEN, not (a)/(b)/(c) -- all three assumed the feed was the constraint. Ruler change scoped (not built): split one allow-list into takeable/reference/excluded, fair_prob_lock becomes a median consensus with n>=2 or a labelled fallback. Gated on exchange price validation + the WNBA measurement, which needs the PropLine key (prod-only, absent locally). MLB isotonic p_win declared PROVISIONAL until re-run on the real ruler. Side finding: we use 1 of 29 endpoints. /odds/closing, /movement, /odds/history, /best-line, /ev, /results, /exports/resolved-props, /context (free) map directly onto documented gaps -- and resolution across 33 sports suggests "no free settled feed for NBA/soccer" may be a $19/mo problem, not a data problem. Documented, not verified. Plan edits: §10.1 rewritten, §10.2/§10.5 corrected, and §11 adds the sequential post-completion accrual clock -- pre-completion data does not count, no pooling across the completion boundary, two clocks stated separately, per-sport clocks, verification gate before any accrual, users onboarded to a complete product only. §9.1's "6-10 weeks out" corrected: that is accrual duration, not distance to the answer. The ruler change independently forces the same no-pooling boundary by arithmetic. No API key was used, printed, or committed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
13 KiB
Order Zero — BOOK-BREADTH TEST
Date: 2026-07-31 · Type: measure-only · Live adapter: UNTOUCHED · Our API key: NOT USED (measurement ran entirely on PropLine's public, unauthenticated endpoints — zero quota spent, zero risk to the live feed).
VERDICT UP FRONT
The order's premise is disproven, and the finding is better than any of the three anticipated outcomes.
The bookmakers param exists, but omitting it already returns every book.
We are not being starved by the feed. PropLine sends 18 books; we discard 13
of them ourselves, in src/utils/oddsNormalizer.js ALLOWED_BOOKS.
Outcome (d) — ALREADY OPEN. The constraint is one allow-list in our own code. No param, no cost, no tier upgrade, no second data source.
This is why Step 0 exists. Firing a guessed bookmakers= request would have
restricted the response and produced exactly the false negative the order
warned about — "doesn't open" — while the real cause sat in our repo.
STEP 0 — THE PARAM, CONFIRMED FROM THE AUTHORITATIVE SPEC
Source: https://api.prop-line.com/openapi.json (200, 78KB, 29 paths) — the
OpenAPI contract itself, not a marketing page.
GET /v1/sports/{sport_key}/odds parameters:
| param | required | default | documented behaviour |
|---|---|---|---|
markets |
no | h2h |
comma-separated market keys |
bookmakers |
no | (none) | "Comma-separated bookmaker keys (e.g. 'draftkings,fanduel'). Omitted = all books. Matches the-odds-api's bookmakers param." |
period |
no | (none) | full-game only unless set |
Omitted = all books. Verbatim from the spec. The param is a filter, not
an expander. proplineAdapter.js:152 omitting it is correct and always was.
§10.1 of MASTER-PLAN said the missing param was the root cause. That was wrong. Corrected in place.
STEP 1 — THE MEASUREMENT
Two public endpoints, no auth, no key, no quota:
GET /v1/freshness— per-bookmaker ingestion health = the real book universeGET /v1/exports/sample— "last 7 days of MLBpitcher_strikeoutsas CSV… no auth required" — real resolved props with book, line, price, resolution, actual value, and closing price
1a. The book universe — 18 keys, all live
/v1/freshness, sampled 2026-08-01T03:13Z, stale threshold 1800s:
| book | markets | staleness | in our ALLOWED_BOOKS? |
|---|---|---|---|
| draftkings | 487,182 | 16s | ✅ |
| bovada | 385,689 | 2s | ❌ dropped |
| betmgm | 166,171 | 119s | ✅ |
| betrivers | 151,845 | 12s | ✅ |
| pinnacle | 144,615 | 6s | ✅ |
| novig | 132,604 | 0s | ❌ dropped |
| prizepicks | 111,475 | 14s | ❌ dropped |
| unibet | 64,696 | 15s | ❌ dropped |
| smarkets | 39,113 | 8s | ❌ dropped |
| underdog | 33,632 | 66s | ❌ dropped |
| fanduel | 32,189 | 0s | ✅ |
| kalshi | 27,649 | 5s | ❌ dropped |
| matchbook | 24,682 | 5s | ❌ dropped |
| onexbet | 22,896 | 382s | ❌ dropped |
| polymarket | 17,347 | 1s | ❌ dropped |
| sleeper | 8,857 | 58s | ❌ dropped |
| tab_au | 6,183 | 45s | ❌ dropped |
| dabble | 4,409 | 51s | ❌ dropped |
Not one book is stale. The feed is healthy; the loss is entirely ours.
1b. The arithmetic that explains "5 MLB books" exactly
ALLOWED_BOOKS (11) ∩ PropLine (18) = 5:
draftkings, fanduel, betmgm, betrivers, pinnacle.
That is precisely the "5 MLB books" the 2.18 audit measured. Cause identified — it is a set intersection, not a coverage fact.
Two separate defects fall out:
- 13 of PropLine's 18 discarded by us.
- 6 of our 11 allow-list entries do not exist at PropLine at all —
caesars, fanatics, bet365, hardrockbet, pointsbet, thescore. They are inert on the PropLine path. (They are NOT dead code overall — the odds-api backup path may still carry them. Do not delete them on this evidence.)
1c. Per-prop breadth — MLB pitcher_strikeouts, 6 events, 2026-07-31
1,000 rows. 5 complete events / 34 props used for breadth (the 6th event is truncated by the export's 1,000-row cap and is excluded — reporting it would manufacture single-book props out of a row limit).
| mean books/prop | single-book | props visible | |
|---|---|---|---|
| PropLine sends (param omitted) | 4.41 | 67.6% | 34 / 34 |
After ALLOWED_BOOKS |
1.50 | 67.6% | 22 / 34 |
Our filter costs 2.9 books per prop and makes 12 of 34 props invisible entirely (zero allowed books → the prop does not exist to VYNDR).
The distribution is bimodal, and the shape is the real story:
- 13 "core" props (probable starters) — 10–12 books each
- 21 tail props — sole-posted: 13 by
draftkingsalone, 13 bybovadaalone, 1 byonexbet. These are deep/reliever props no one else prices.
The headline "67.5% single-book" is therefore not a statement about the market for the props we actually grade. It is the long tail of one-book deep props dragging the mean. On the props that matter, the market is 10–12 books wide.
Book presence across the 13 core props:
| book | coverage | role |
|---|---|---|
| underdog, novig, sleeper, prizepicks, dabble, smarkets, kalshi | 13/13 | 3 exchanges + 4 DFS |
| draftkings, betmgm | 13/13 | ✅ ours |
| onexbet | 12/13 | offshore |
| fanduel | 12/13 | ✅ ours |
| betrivers | 10/13 | ✅ ours |
1d. The sharp anchor — a real negative
pinnacle appears on 0 of 40 props. It carries 144,615 markets globally but
no MLB player props in this sample. The one sharp book already in our
allow-list contributes nothing to MLB props.
What is present on 100% of core props: novig, smarkets, kalshi — and
matchbook/polymarket elsewhere in the feed. These are exchanges:
order-book priced, so near-zero vig. Arguably a better fair-probability
reference than Pinnacle.
Stated as a hypothesis, not a fact: exchange prop books are thinner than
their game-line books, and kalshi/polymarket are prediction markets whose
prop coverage is recent. Their prices must be validated on real data before
being trusted as a ruler, not assumed good because the theory is good.
1e. Three buckets, not one list — DFS is not a market price
prizepicks, underdog, sleeper, dabble cover 13/13 core props, but they are
fixed-payout pick'em, not two-way markets. Their lines are deliberately
shaded and their effective juice is flat. They must never enter a consensus
fair price. Admitting them "for breadth" would bend the ruler in a new
direction — the exact error this order exists to fix.
1f. Cost and tier
/odds returns all books on the FREE tier. Nothing about book breadth costs
money. Our quota (3 free keys × 1,000/day = 3,000/day) is unchanged by this.
Tier gates apply only to the analytical endpoints (§ below): Free $0/1k-day · Hobby $9/5k · Pro $19/25k · Streaming Lite $39/100k · Streaming $79/1M · Enterprise custom.
1g. ⚠️ WNBA — NOT MEASURED
The public sample is MLB pitcher_strikeouts only. Every per-sport
alternative needs the API key, and PROPLINE_API_KEY_1/2/3 are absent from
the local .env (prod/Coolify only).
Do not assume WNBA mirrors MLB. WNBA's book set is likely thinner and its exchange coverage thinner still — which would tie directly to its anti-predictive result. This measurement is outstanding and needs one thing: a PropLine key.
THE BIGGER FINDING — WE USE 1 OF 29 ENDPOINTS
While confirming the param I read the full spec. Several §10 "gaps" are not gaps in the data — they are endpoints we never called.
| §10 gap | PropLine endpoint | tier |
|---|---|---|
| CLV instrument | /events/{id}/odds/closing — "the canonical CLV helper", last snapshot at/before commence_time per (book, market, outcome) |
Paid |
| Historical odds archive | /events/{id}/odds/history (from/to/relative_to/interval/changes_only) |
Hobby+ |
| Bulk archive / backtest | /exports/odds-history — full tick firehose |
Enterprise / one-time backfill |
| Steam detection | /events/{id}/movement — "steam … computed across all 16 books we poll" |
Hobby+ |
| Line shopping | /events/{id}/best-line — best price per (market, player, line) + all_prices best-first |
appears free |
| Cross-book +EV | /events/{id}/ev — no-vig via Pinnacle preferred, Bovada fallback |
appears free |
| Settlement for non-MLB sports | /events/{id}/results, /exports/resolved-props (line, price, resolution, actual value, closing price) |
Hobby+ / Pro+ |
| Park / weather / umpire / lineup | /events/{id}/context — probable pitchers, confirmed-lineup flag, home-plate umpire, first-pitch weather |
FREE |
| Base rates | /markets/hit-rates |
— |
| Player trend history | /players/{name}/trends, /players/{name}/history |
Hobby+ |
Two of these overturn standing beliefs in CLAUDE.md:
- "NBA/WNBA/soccer have no free settled-result feed" — PropLine resolves props across 33 sports and exports them with actual values. Our settle-nothing-but-MLB-and-WNBA constraint may be a $19/mo problem, not a data-availability problem.
- We hand-built probable pitchers, depth charts and lineup confirmation.
/contextserves them, free, joined to the graded outcome.
Caveat, stated plainly: these are documented capabilities. I have verified
the bookmakers contract (a spec-level guarantee) and the book universe +
per-prop breadth (real data). I have not verified what these endpoints
actually return for our tier — that needs the key. Do not plan on them as facts
yet.
STEP 3 — SCOPE OF THE RULER CHANGE (scope only — NOT built)
1. Split one allow-list into three roles. Today ALLOWED_BOOKS answers
"can we show this?" and "can we price against this?" with the same list. Those
are different questions and conflating them is what bent the ruler.
TAKEABLE_BOOKS— a US user can actually bet here. Drives best-price, line-shopping,takeable. (today's list, minus nothing)REFERENCE_BOOKS— the fair-probability ruler only; never surfaced as a place to bet:pinnacle, novig, smarkets, matchbook, kalshi, polymarket, bovadaEXCLUDED_FROM_PRICING— DFS pick'em (prizepicks, underdog, sleeper, dabble) and offshore/intl (onexbet, unibet, tab_au). May be surfaced as a separate DFS view; never in a consensus price.
2. fair_prob_lock becomes a consensus. Per-book two-way de-vig
(devigTwoWay, unchanged), then the median across reference books — median,
not mean, so one stale exchange cannot drag the ruler.
3. Require n≥2 reference books, and label which ruler was used. Below 2,
fall back to today's single-book de-vig and stamp
fair_prob_source: 'consensus_n' | 'single_book'. Never silently mix the
two — that pools two different rulers inside one column, the same error class
as pooling across a model-version boundary.
4. Stamp a ruler_version at the boundary. Changing the denominator means
every edge and CLV number measured to date was measured on the old ruler and
cannot be pooled with post-change rows.
5. Gate the build on two things that do not exist yet: (i) exchange prop-price sanity validated on real data (§1d), and (ii) the WNBA measurement (§1g).
STEP 4 — MLB ISOTONIC p_win IS PROVISIONAL
Declared explicitly: the MLB isotonic p_win recalibration was measured
against the bent single-book ruler. It is PROVISIONAL and GATED behind the
ruler fix. It does not finalize or promote until re-run on the real
consensus ruler.
Plan sequencing edit: MLB promotion is downstream of the ruler fix, not before it.
WHAT CHANGED IN THE PLAN
- §10.1 rewritten — the param premise was wrong; the allow-list is the cause.
- §10.2 — book breadth reclassified from "needs a new source" to "one file".
- §10.5 — item 1 is now a build we can do, not a test to run.
- Accrual clock corrected to sequential / post-completion with a verification gate (separate ride-in, §11).
And the two interact: the ruler change resets the denominator, so pre-change settled rows cannot be pooled with post-change rows. That is an independent derivation of Kev's sequential-clock rule — the no-pooling boundary is forced by the arithmetic, not just by discipline.
REPRODUCE
curl -s https://api.prop-line.com/openapi.json # the param contract
curl -s https://api.prop-line.com/v1/freshness # the 18-book universe
curl -s https://api.prop-line.com/v1/exports/sample # real resolved MLB props
All three are public. No key was used, printed, or committed.
OPEN — needs one thing from Kev
A PropLine API key (it is in prod/Coolify, not in the local .env). With
it: the WNBA per-sport measurement, and live verification of the ten unused
endpoints above — including whether $19/mo settles NBA and soccer.