diff --git a/specs/MASTER-PLAN.md b/specs/MASTER-PLAN.md index 05e2ea7..c9d16e7 100644 --- a/specs/MASTER-PLAN.md +++ b/specs/MASTER-PLAN.md @@ -54,7 +54,9 @@ model-shaped is downstream of it: | **MLB isotonic `p_win`** | **PROVISIONAL** | calibrated on the bent ruler; does not promote until re-run | | **WNBA** | **still abstains** | a MODEL problem, not a coverage problem — coverage was never its constraint | | **soccer** | **settles** — ~15 competitions, 30d | "grades into a void" is a **$19/mo Pro-tier** problem, not a data problem | -| **CLV + results feeds** | `/odds/closing` + `/movement` **redacted**; `/results` + `/exports/resolved-props` **403** | free tier behaving exactly as its contract says | +| **CLV + results feeds** | `/odds/closing` + `/movement` **redacted**; `/results` **403 `required_tier: hobby`**; `/exports/resolved-props` **403 `required_tier: pro`** | **verified on our keys** — plain tier exclusion, not a key or plan fault. **$9/mo** buys CLV + steam + results; **$19/mo** adds the 90-day settlement export | +| **books SERVED** | 5 → **13**; props rendered **546 → 2,780** (5.1×); mean **4.22** books/prop | **AGGREGATOR widening is LIVE.** Of 2,234 newly-visible props, **31.2% carry a real non-DFS price**; **68.8% are DFS-only** — shown, tagged, never a market | +| **model input** | **byte-identical** — 546 gradeable props, `v1_first_book` | `MODEL_BOOKS` gate in `dedupeProps` + `indexOddsProps`. Lifts only on the re-run | | **accrual clock** | **sequential, post-completion** | see §11. Pre-completion data does not count and is never pooled | **The honest framing:** widening books is an **AGGREGATOR** win. **It does not fix diff --git a/specs/order-zero-widening.md b/specs/order-zero-widening.md new file mode 100644 index 0000000..288a59a --- /dev/null +++ b/specs/order-zero-widening.md @@ -0,0 +1,166 @@ +# Order Zero — TIER REALITY + AGGREGATOR WIDENING + +**Date:** 2026-08-01 · Phase 1 verified on **our real prod keys** · Phase 2 **LIVE** · +Phase 3 **model path byte-identical**. + +**Gates:** 4,028 tests / 322 suites green · `next build` **exit 0** · tier-reality +report per endpoint · widening fingerprinted on the served feed · model input unchanged. + +--- + +## PHASE 1 — THE FREE-TIER DISCREPANCY, RESOLVED + +The order was right to demand this. **I found and discarded a bad source on the +way.** A fetched rendering of PropLine's docs "tier matrix" claimed +`/odds/closing` is **403 on free** and that `/odds` returns **prices nulled** on +free. Both are contradicted by direct observation: we get `200` redacted on +`/odds/closing`, and **6,196 two-sided PRICED prop groups** on MLB `/odds`. That +matrix is not cited anywhere below. + +**Two authoritative sources are used instead:** the machine-readable OpenAPI +contract, and the verbatim `detail` bodies our own keys received. + +### The verdict per endpoint + +| endpoint | our keys | verbatim reason | verdict | +|---|---|---|---| +| `/events/{id}/results` | **403** | `upgrade_required` · **`required_tier: "hobby"`** · *"Prop resolution is a paid feature — start at $9/mo Hobby"* | **Free-tier exclusion. $9/mo.** | +| `/exports/resolved-props` | **403** ×4 sports | `upgrade_required` · **`required_tier: "pro"`** · *"Resolved-prop exports are a Pro feature"* | **Free-tier exclusion. $19/mo.** | +| `/events/{id}/odds/closing` | **200, 1,847 outcomes, 0 priced**, `upgrade_url` present | OpenAPI: *"Paid tiers: full data. Free tier: redacted (structure only)."* | **Free-tier redaction. $9/mo.** | +| `/events/{id}/movement` | **200, 1,847 outcomes, 0 priced**, `steam: []` | OpenAPI: *"Paid tiers (Hobby+): full data. Free tier: redacted."* | **Free-tier redaction. $9/mo.** | +| `/events/{id}/context` (MLB) | **200, full data** | — | **Free. Works.** | +| `/events/{id}/context` (WNBA) | 404 · *"No context on file for this event yet."* | — | **Not a tier issue — MLB-only coverage.** | + +**It is NOT a key-permission problem and NOT a plan problem.** The keys return +exactly what the Free tier's published contract says they should, on every one +of the six endpoints. `error: "upgrade_required"` with an explicit +`required_tier` is as unambiguous as this gets. + +### What money would buy + +| tier | cost | unlocks | +|---|---|---| +| **Hobby** | **$9/mo** | `/results` (settled outcomes + actual values) · `/odds/closing` (**the CLV instrument**) · `/movement` (**steam across 18 books**) · `/odds/history` (30d) · `/players/trends` + `/history` · `/ev` | +| **Pro** | **$19/mo** | the above + `/exports/resolved-props` — **90-day bulk settlement CSV incl. closing price**, the backtest input | + +**$9/mo buys the two instruments we have been unable to build** — a real closing +line and cross-book steam. `/exports/odds-history` (the full tick firehose) +stays Enterprise-only at any price we'd consider. + +**I am not recommending the spend in this order.** It is a decision, priced and +evidenced, for you to make. + +--- + +## PHASE 2 — AGGREGATOR WIDENING (LIVE) + +`normalizeProps` now emits every **DISPLAY** book instead of 5 of 18. + +### Fingerprint — the SERVED MLB feed, post-deploy + +| | before | after | +|---|---:|---:| +| distinct books served | 5 | **13** | +| book rows | — | **11,737** | +| props rendered (player+stat) | **546** | **2,780** | +| mean books/prop | ~0.6 | **4.22** | + +**Props that render now and rendered nothing before: 2,234.** The slate a user +sees is **5.1× larger.** + +### The part that isn't flattering, stated up front + +Of those 2,234 newly-visible props: + +- **698 (31.2%)** carry at least one **real non-DFS market price** — genuinely + new market data. +- **1,536 (68.8%)** are **DFS-only** — PrizePicks/Underdog/Sleeper/Dabble rows, + which are **not a market** at all. + +So the honest headline is **not** "80% of the slate was unlocked". It is: *the +board is 5× fuller, about a third of the new depth is real market data, and the +rest is pick'em inventory that is now shown but tagged.* + +Served role mix: `dfs` 8,515 · `reference` 2,042 · `both` 791 · `offshore` 389. + +### `book_role` is load-bearing, not decorative + +Every row carries `book_role` ∈ {`both`, `takeable`, `reference`, `dfs`, +`offshore`} — through the normalizer **and** through the odds route's grouping, +which was silently dropping it. + +**Why it matters, from live data.** On one MLB prop: + +``` +betmgm +450 / -750 ← a real two-way market +prizepicks +100 / +100 ← fixed-payout pick'em +``` + +Rendered side by side untagged, the PrizePicks row reads as a **dramatically +better price**. It is a different product. Untagged breadth would have been a +new way to mislead, dressed as a feature — which is exactly what the split +exists to prevent. Consumers gate on `book_role !== 'dfs'` before treating a row +as a market price. + +The `?book=` filter now accepts any DISPLAY book, since shopping a real book +against an exchange is the point. + +--- + +## PHASE 3 — THE MODEL DOES NOT MOVE + +`bookRoles.js` splits **`MODEL_BOOKS`** (the legacy allow-list, character for +character) from **`DISPLAY_BOOKS`**. Both model paths re-filter before they pick +a line: + +1. **`gradeSlateService.dedupeProps`** — filters to `MODEL_BOOKS` **before** + first-row-wins and **before** the 25-prop limit, so the graded set is + byte-identical through the widening. A test asserts exactly this. +2. **`intradayRefreshService.indexOddsProps`** — the one that would have bitten. + It **re-grades props at the current line**, so without a gate the widened + feed would have silently moved locked lines onto DFS and exchange numbers the + model has never been calibrated against. + +**Props the model can grade: 546 — unchanged.** +`CURRENT_RULER_VERSION` is still **`v1_first_book`**. + +**The gate lifts only when the MLB calibration is re-run on the consensus ruler +and v2 is promoted.** + +--- + +## HONEST FRAMING — do not let this read as model progress + +- **This is an AGGREGATOR win. It does not fix the model.** Not one grade + changed. +- **WNBA still abstains.** It is a **model** problem, not a coverage problem — + WNBA is *better* covered than MLB (4.21 vs 3.61 books/prop). Coverage was + never its constraint. +- **MLB isotonic `p_win` is still PROVISIONAL** until re-run on the consensus + ruler. +- **The consensus is MARKET, not SHARP.** `pinnacle`, `matchbook` and + `polymarket` are **0%** on both sports. **No sharp anchor exists in our feed** + — and none is purchasable from this provider, because it is a coverage fact, + not a tier gate. +- **A third of the new depth is real; two thirds is pick'em.** Both are shown; + only one is a market. + +--- + +## THREE SUPERSEDED TESTS, UPDATED TO STRONGER PROPERTIES (not deleted) + +1. `roleOf(dfs)` returned `'excluded'` → now names the **kind** (`'dfs'`), plus + assertions that it is display-visible, non-reference and non-model. +2. *"filters out books not in the allowed set"* → now asserts the display set + **widens with correct role tags**, unknown books are **still dropped**, and + the **model set does not move**. +3. *"returns 400 for invalid book"* (`bovada`) → split into: an **unknown** book + still 400s, and a **newly-visible** one no longer does. + +## OPEN + +1. **$9/mo Hobby** — buys the CLV instrument and steam. Evidenced above; your call. +2. **$19/mo Pro** — adds the 90-day settlement export, which is the backtest input + and the thing that makes soccer settle. +3. **The display layer should now surface multi-book line shopping** — the data + is live and tagged; no surface consumes it yet. That is a frontend order.