Files
vyndr/specs/order-zero-book-breadth-test.md
T
builtbykev 293367917c Order Zero: book-breadth test + accrual clock correction (measure-only)
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
2026-07-31 23:19:23 -04:00

13 KiB
Raw Blame History

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:

  1. GET /v1/freshness — per-bookmaker ingestion health = the real book universe
  2. GET /v1/exports/sample — "last 7 days of MLB pitcher_strikeouts as 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 allcaesars, 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) — 1012 books each
  • 21 tail props — sole-posted: 13 by draftkings alone, 13 by bovada alone, 1 by onexbet. 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 1012 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:

  1. "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.
  2. We hand-built probable pitchers, depth charts and lineup confirmation. /context serves 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, bovada
  • EXCLUDED_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.