New READ endpoint. No grade, ledger row, lock_line, or scoring write. Push
scoring untouched.
REVIEW ZERO CORRECTED THE PREMISE: the handler NEVER EXISTED in any commit
(searched git rev-list --all for a /top-graded definition in src/ — zero hits).
Not "removed" — the three axios callers (cheatsheetGenerator, gradeOfTheDay,
widget) and the Next proxy were written against a phantom endpoint, so those
three content generators have silently received [] for their entire life.
Contract recovered from the four consumers, not guessed: {props:[...]},
?sport=UPPERCASE (absent = all sports, which gradeOfTheDay relies on) + ?limit,
rows carrying player/stat/line/direction/sport/grade/confidence? plus the
player_name/stat_type aliases and game_id.
POPULATED-PATH RISK FOUND: the board's populated branch had never run in prod,
and dashboard/page.tsx:463 calls g.stat.replace(/_/g,' ') UNGUARDED (g.player
also feeds the row key, /scan URL and heading; sport must be UPPERCASE for
SportPill). toRow requires non-empty string player+stat and a finite line,
uppercases sport, and DROPS unrenderable rows — a shorter board beats a broken
one.
THE LEAK BOUNDARY (why this is server-side): the browser cannot rank on p_win
for all tiers because stripModelPrice deliberately withholds it from unentitled
tiers. Order of operations is
read cache -> RANK with p_win (every tier) -> map rows incl. model fields
-> stripModelPrice(rows, tier) -> serialize
so a free caller receives the paid RANKING without the paid VALUES. Tier comes
from resolveTierFromRequest, which FAILS CLOSED to 'free'. Cache-Control is
private under a bearer token, public otherwise (the /api/snapshot precedent).
ONE SHARED DEFINITION, no drift: new src/utils/gradeRanking.js
(takeablePWin/descNullsLast/rankGrades). heroPropService now imports
takeablePWin instead of its inline copy (behaviour unchanged — it was that
logic verbatim); the selector imports rankGrades; web/src/lib/slateAdapter
keeps its mirror (the browser cannot import src/, S25) and a test cross-checks
the two on identical fixtures (playerName.js precedent). Board is grade-first
("top GRADES"), hero is p_win-first ("top read") — they differ BY DESIGN and
agree within the leading tier.
HONEST LIMIT: the Next proxy (cachedBackendJson) sends no Authorization header
and caches under a shared key, so via the dashboard every viewer gets the
free-tier payload — correct order, no paid values. That is the SAFE behaviour;
forwarding auth into a shared cache is exactly how a paid payload leaks to
anonymous viewers. Per-tier delivery through the proxy needs a tier-keyed cache
and is not done here.
Verified on real prod snapshot data (anonymous path): MLB 8 props, WNBA 10,
0 paid-field leaks, render-contract safe on every row, sport uppercase.
Floor: 311 suites / 3882 tests green (18 new — leak test uses POPULATED p_win,
not today's nulls: entitled gets p_win and it drove the order, unentitled gets
a byte-identical order with all five MODEL_FIELDS absent and no trace in
JSON.stringify, while book/fair market facts survive). Web build exit 0.
Dashboard visual is auth-gated -> tagged for the Chrome audit, not faked.
Held: edge_pct rescale/retirement (Order B); board columns/contract unchanged;
tier-keyed proxy caching.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
5.1 KiB
SPEC — /api/props/top-graded server selector (rank with p_win, serve without it)
Status: built 2026-07-29. New READ endpoint. No grade/ledger/lock_line/scoring write.
Push scoring untouched. Spec companion: specs/grade-board-sort.md.
1. Review Zero findings
- 0.1 CORRECTED — the handler NEVER EXISTED. Not "removed": searched every commit
(
git rev-list --all) for a/top-gradeddefinition insrc/— zero hits. The three axios callers (content/cheatsheetGenerator:21,content/gradeOfTheDay:17,routes/widget:76) and the Next proxy were written against a phantom endpoint, so those three content generators have silently received[]for their entire life. The contract is therefore recoverable ONLY from consumers, which is what this spec builds to. - CONTRACT (VERIFIED, union of four consumers). Envelope
{ props: [...] }(all three callers doArray.isArray(res.data?.props); the Next proxy returns the same). Query:sport(UPPERCASENBA|MLB|WNBA— the proxy validates that set; absent = all sports, whichgradeOfTheDayrelies on) andlimit. Row fields REQUIRED bydashboard/page.tsxTopGrade:player,stat,line,direction('over'|'under'),sport,grade,confidence?. Callers additionally readplayer_name || player,stat_type || stat, andgame_id(cheatsheet'sgameCount). - 0.5 POPULATED-PATH RISK — FLAGGED. The board's populated branch has effectively never
run in prod.
dashboard/page.tsx:463callsg.stat.replace(/_/g,' ')UNGUARDED — a row without a stringstatTHROWS and takes out the board.g.playeris used in the key, the/scanURL and the<h3>;sportmust be UPPERCASE (type Sport = 'NBA'|'MLB'|'WNBA', fed toSportPill).confidenceis the only null-guarded field. The selector therefore emitsplayer/statas non-empty strings and uppercasessport, and DROPS any row that cannot satisfy that (a crashed board is worse than a shorter board). - 0.2 VERIFIED — the strip can run server-side, after ranking.
utils/requestTier.resolveTierFromRequest(req)is purpose-built for a PUBLIC endpoint (bearer token when present, else'free') and FAILS CLOSED — any error returns the LEAST entitled tier, so a resolution failure can only withhold, never leak.utils/snapshotGating.stripModelPrice(rows, tier)deletesmodel_odds|p_win|ev_pct|value|takeablefor unentitled tiers and stampsmodel_price_lockedwhere a book+fair pair survives. - 0.3 VERIFIED LIVE — the server HAS p_win right now.
GET /api/hero-propreturnsavailable:true(Brionna Jones, B, wnba), and the hero rule REQUIRES a non-nullp_winAND a takeable price, so the snapshot cache carries both server-side. The public/api/snapshotshowsp_winon 0/8 MLB + 0/25 WNBA rows only because it is stripped on the way out. - 0.4 ONE SHARED DEFINITION. Extracted to
src/utils/gradeRanking.js(takeablePWin,descNullsLast,rankGrades).heroPropServicenow importstakeablePWininstead of its inline copy, and the new selector importsrankGrades. The browser cannot importsrc/(S25 rule), soweb/src/lib/slateAdapterkeeps its mirror — a test cross-checks the two on identical fixtures, theplayerName.jsprecedent. Takeable band =config/valueEngine.isTakeable(−160..+200), strict-null.
2. The order of operations (the leak surface)
read snapshot cache → RANK with p_win (all tiers, server-side)
→ map to contract rows INCLUDING model fields
→ stripModelPrice(rows, tier) ← the boundary
→ serialize
Ranking happens BEFORE the strip, so a free caller gets the SAME order an entitled caller
gets, without the paid values. Cache-Control follows the /api/snapshot precedent:
private when a bearer token is present, public otherwise — a CDN must never hand a paid
payload to an anonymous viewer.
3. Rank order
grade → confidence → takeable-gated p_win (nulls LAST) → SIGNED edge (nulls LAST) → stable input order. Grade-first because this is "top GRADES", not "top read" — it is
INTENDED that this board and the hero can lead with different picks (the hero is p_win-first).
They agree within the leading grade tier.
4. Acceptance criteria
- Entitled request: ranked by takeable-gated p_win;
p_winPRESENT in the payload. - Unentitled request: byte-identical ORDER;
p_win/ev_pct/model_odds/value/takeableABSENT. Proven with POPULATED p_win, not today's nulls. - Nulls sort LAST server-side; untakeable chalk never tops the board.
- Grade tier dominates every signal.
- Thin/empty slate →
200 { props: [] }, never 404, never filler. - Every emitted row satisfies the populated-render contract (string
player+stat, uppercasesport). - Hero / client / server use ONE definition — cross-checked by test.
- No grade/ledger/lock_line/scoring write. Suite green, web build exit 0.
5. Held
edge_pct rescale or display retirement (Order B) · changing the board's columns or contract · making the board and hero lead with the same pick.