diff --git a/BACKEND_HANDOFF.md b/BACKEND_HANDOFF.md index 291502f..bcbca09 100644 --- a/BACKEND_HANDOFF.md +++ b/BACKEND_HANDOFF.md @@ -296,3 +296,28 @@ Every graded read (snapshot `grades:{sport}`, `/api/snapshot/:sport`, `/api/hero DESIGN: the triplet + VALUE marker + refusal copy are Session-2 design surfaces (reveal card, board row, hero). Backend ships the fields; rendering is Design's. + +## Book Comparison — `/api/books` (per-book price display, snapshot-locked) + +Data-only layer for the (still-unrouted) `BookComparison.tsx` grid. Never touches +the grade path. + +- `GET /api/books/:sport/:player/:stat?side=over|under` → the honest per-prop + grid: `{ player, stat, line, side, books:[{book,line,over_odds,under_odds, + isBest}], bestBook, bestOdds, bookCount, savings, crowned }`. HONEST-ABSENT: + a single-book prop returns that one book with `isBest:false, bestBook:null, + crowned:false` — never a placeholder second row. `:player` is matched by + `nameKey` (accent/dot/nickname-folded). +- `GET /api/books/:sport?side=&limit=` → "best lines tonight" (`bestLines`, sorted + by savings). This is a CROWN claim, so it returns `[]` while the crown is gated + off. Response `source` = `bookprices` (snapshot-locked store serving) or + `odds-cache` (fallback before first snapshot) — also the deploy fingerprint. +- **The crown is threshold-gated + OFF by default** (`BOOK_CROWN_ENABLED=1` to + enable). Pre-registered gate: median same-line spread ≥8¢ OR ≥2 implied-prob + pts (`scripts/measure-book-spread.js`). Measured 2026-07-27 on the live feed: + MLB median 5¢/0.95pp, WNBA 7¢/1.38pp → NOT met → crown does NOT ship. Render + books WITHOUT crowning one until a future measurement clears the bar. +- Source store: `bookprices:{sport}` (Redis, SNAP_TTL), written by + `snapshotService` from the pre-dedup multi-book `props`. Grouped + `{player, stat_type, books:[...]}`, normalized-name-keyed. DISPLAY-ONLY — + fenced from grade/selector/challenger/ledger.