# VYNDR — TRUTH-EVERYWHERE + OFFSEASON HUB · STEP 0 MAP ### The map before the build. Governing: accountable-before-wide; never fabricate a grade/settle/market value; absent-beats-wrong; n≥20 gate; the record includes the misses. Design ref = `specs/design-reference/vyndr-system.html` (live wordmark kept). ## THE HEADLINE FINDING (this reorders the train) The work order assumed grades are MADE everywhere but only CHECKED for MLB. **That's not the situation. Only MLB actually GRADES in prod.** NBA/WNBA/soccer silently refuse their entire slate; combat/NFL/NHL aren't in the grade loop at all. So the record isn't "MLB wearing a platform badge because only MLB settles" — it's MLB because **only MLB produces grades in the first place.** ⇒ **The first domino is GRADING, not settlement.** You cannot settle what never grades. Wave 0 (grade-first) is not a quick gate — it is the actual unlock, and it's the highest-leverage work in the product. ## PER-SPORT GRADE → SETTLE → LEARN MATRIX | Sport | Grades in prod? | Why | Free settle path? | |---|---|---|---| | **MLB** | ✅ yes | `featureCache` MLB branch → statsapi.mlb.com (free, no auth) → real l5/l20 → projection | ✅ live (`outcomeService` via statsapi last10) | | **NBA** | ❌ refuses ALL | `featureCache` non-MLB branch → `gameLogService` hits ONLY the Python nba_api (`:8000`), **usually offline in prod, no fallback** → null logs → no projection → refuse. The espnStatsAdapter "fallback" is NOT wired into the grade path (only profile/savant). | ✅ **viable** — ESPN boxscore; `liveTrackingService.parseWnbaBoxscore` + `WNBA_BOX_KEY` ALREADY parse it | | **WNBA** | ❌ refuses ALL | same Python-only root cause (in-season now, still refuses) | ✅ same as NBA (identical ESPN shape) | | **Soccer** | ❌ errors THEN refuses | (1) `getOdds('soccer')` throws `Unknown sport: soccer` — `SPORT_KEYS` has `soccer_wc/soccer_epl…` not bare `soccer` → snapshot errors pre-grade; (2) `soccerFeatureExtractor` reads prefetch Redis keys that **nothing writes** (prefetch script unarmed) → starved → refuse | ⚠️ api-football only (free 100/day, key-gated `API_FOOTBALL_KEY`), NOT ESPN cleanly | | **Combat/MMA** | ❌ not graded | v1 is a standalone surface; no engine branch, no projection field, not in `ACTIVE_SPORTS` | ML: ✅ free (ESPN `winner` boolean, already parsed); round-total: ❌ no free method/round feed | | **NFL/NHL** | ❌ dormant | not in `ACTIVE_SPORTS`; no `featureCache` gameLog branch either | ✅-pattern (ESPN box, same shape) when in-season | ## REVISED WAVE PLAN (grade → settle → learn, per sport) ### WAVE 0 — GRADE-FIRST (the real unlock; do FIRST) Make NBA/WNBA/soccer actually produce grades in prod off a FREE source. - **NBA/WNBA:** wire a free per-game-log source into `featureCache.gameLogFeatures` (mirroring the MLB branch). **CRUX/RISK to verify first:** the source must give **per-game logs (last 5/20), not just season averages** — `l5_avg`/`l20_avg` can't come from a season mean. ESPN's per-athlete gamelog endpoint (`.../athletes/{id}/gamelog`) is the candidate; confirm it returns per-game rows before building. If ESPN gamelogs work, NBA (off-season) + WNBA (in-season NOW) grade for free. - **Soccer:** (a) fix `getOdds('soccer')` to resolve the `soccer_*` league keys; (b) arm the soccer prefetch (or wire api-football live) so `soccerFeatureExtractor` isn't starved. Soccer is the hardest (double-blocked + key-gated) — consider deferring to last. - Combat/NFL/NHL: out of scope for grading now (no engine branch; combat stays its standalone surface). ### WAVE 1 — SETTLEMENT EVERYWHERE (once a sport grades) The plumbing is ALREADY sport-agnostic — the ONLY gate is `defaultGetPlayerStats` (MLB-only, **duplicated in `outcomeService.js` AND `ledgerService.js` — both must change**) + a per-sport box-field map. - **NBA/WNBA:** a game-centric settle branch — sweep yesterday's `status.state==='post'` ESPN summaries, `parseWnbaBoxscore` each (reuse verbatim), match graded players by `nameKey`. Add a WNBA/NBA `BOX_KEY` local map (mirror `MLB_LOG_FIELD`; keep the 3-map split intentional per S11). - **MMA moneyline:** settles free from the `winner` boolean already parsed. Requires combat to first WRITE grades/locks (it's not in the snapshot loop) — smaller than a box feed. Round-total = honest gap (no free source). - **Soccer:** api-football per-fixture stats, quota-bound + key-gated → defer with grading. - **Observability (non-negotiable):** boot announce per sport (`[settle:nba] armed`); add the sport to `opsWatch.SETTLEABLE_SPORTS` **only with a real-finals probe** (count yesterday's ESPN `state==='post'` events for the sport) so an off-day (0 games) never false-pages "settled 0". Thrown-error paging already covers all sports. - Records need ZERO new plumbing — `accuracy:{sport}` + `getModelAggregate` `by_tier` already flow per-sport; the Wave-3 `TierRecord` lights up automatically. ### WAVE 2 — OFFSEASON / NEVER-DARK HUB - **News/transactions/injury wire:** injuries already ingested (`injuryService`, mlb/wnba/nba). NEWS is a gap — ESPN `/news` feed is FREE, unused. Build `newsService` (mirror `injuryService`) + `/api/news/:sport` + proxy. Signings appear as prose headlines (ESPN has no typed transactions feed — honest limitation). - **Futures/outrights:** genuine gap (zero support today). **⚠️ PAID-QUOTA DECISION (D1 below):** The Odds API carries `outrights` on the ALREADY-paid key but each futures sport-key call spends the scarce 500/mo quota; PropLine has none. Needs new `FUTURES_KEYS` + `markets=outrights` param + a new `normalizeOutrights` branch (outrights have `name`+`price`, no `point` — can't reuse `normalizeProps`) + long TTL/offseason gating. - **Line-movement on futures:** high reuse — `computeLineDeltas`/`trackHistory`/`signedDelta` port cleanly with `line`→`odds` (price). News→move causal tie ("signing → win total +2.5") is NEW small join code. - **Render:** extend `/explore` (`ExploreHub`) + the offseason-aware `outlook.js`/`emptyState.js`; **reskin the retired `TerminalTemplates` injury-wire/leader LAYOUTS with REAL data** (they're SAMPLE data today — never route as-is). New `/futures` route only if wanted. ### WAVE 3 — FUTURES HONESTY LINE Season-long grading is a different model (projection+roster+schedule, not last-5). The hub SHOWS + TRACKS futures + news + movement honestly, and GRADES only where the model genuinely supports it — "tracked, not graded" everywhere else. The deliberate futures-grading model is a LATER train. ## DECISIONS — LOCKED (2026-07-13) - **D1 = BUILD FUTURES, quota-disciplined.** Separate `FUTURES_KEYS` path, 6–12h/daily TTL, offseason-month gated, never touches the daily player-prop budget. - **D2 = DEFER SOCCER to the end / its own follow-up.** NBA/WNBA are the free high-value wins first; soccer wired when `API_FOOTBALL_KEY` is provided. - **D3 = ANSWERED (green).** ESPN per-athlete gamelog verified live: NBA returns 73 per-game rows + stat labels; WNBA endpoint 200. Free per-game logs exist → NBA/WNBA can grade. Endpoint: `site.web.api.espn.com/apis/common/v3/sports/basketball/{nba|wnba}/athletes/{id}/gamelog` → `events[]` + `labels`/`names`. ## ORIGINAL DECISION RECOMMENDATIONS (superseded by LOCKED above) - **D1 — Futures quota.** Futures spend the already-paid odds-api 500/mo quota, competing with game-prop credits. **Rec: YES but disciplined — futures move slowly, so a 6–12h/daily TTL + offseason-month gating keeps the cost tiny; wire them on a separate `FUTURES_KEYS` path so they never touch the daily player-prop budget.** (Alt: skip futures, ship only the free news wire + line-movement on existing markets.) - **D2 — Soccer.** Grading + settling soccer both need `API_FOOTBALL_KEY` (free tier, 100/day) which YOU provision (I can't create accounts/keys), plus arming the prefetch. **Rec: defer soccer to the END of the train (or its own follow-up) — it's the only double-blocked, key-gated sport; NBA/WNBA are the high-value free wins. Provide the key when convenient and I'll wire it.** - **D3 — NBA/WNBA grade source.** Wave 0 hinges on a FREE per-game-log feed. **Rec: verify ESPN's per-athlete gamelog endpoint returns per-game rows FIRST (a spike), before building the featureCache branch — if ESPN only gives season averages, we need another free source or NBA/WNBA can't honestly grade yet.** This is the single technical risk of the whole train; I'll de-risk it in Step 0.5. ## SEQUENCE Wave 0 (NBA/WNBA grade via ESPN gamelogs — verify the feed first) → Wave 1 (NBA/WNBA settle via existing box parser + observability; MMA moneyline) → Wave 2 (news wire free + futures per D1) → Wave 3 (futures honesty). Soccer deferred per D2. Each wave: green → merge → deploy → fingerprint → report.