Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.7 KiB
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_avgcan'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 thesoccer_*league keys; (b) arm the soccer prefetch (or wire api-football live) sosoccerFeatureExtractorisn'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,parseWnbaBoxscoreeach (reuse verbatim), match graded players bynameKey. Add a WNBA/NBABOX_KEYlocal map (mirrorMLB_LOG_FIELD; keep the 3-map split intentional per S11). - MMA moneyline: settles free from the
winnerboolean 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 toopsWatch.SETTLEABLE_SPORTSonly with a real-finals probe (count yesterday's ESPNstate==='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}+getModelAggregateby_tieralready flow per-sport; the Wave-3TierRecordlights up automatically.
WAVE 2 — OFFSEASON / NEVER-DARK HUB
- News/transactions/injury wire: injuries already ingested (
injuryService, mlb/wnba/nba). NEWS is a gap — ESPN/newsfeed is FREE, unused. BuildnewsService(mirrorinjuryService) +/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
outrightson the ALREADY-paid key but each futures sport-key call spends the scarce 500/mo quota; PropLine has none. Needs newFUTURES_KEYS+markets=outrightsparam + a newnormalizeOutrightsbranch (outrights havename+price, nopoint— can't reusenormalizeProps) + long TTL/offseason gating. - Line-movement on futures: high reuse —
computeLineDeltas/trackHistory/signedDeltaport cleanly withline→odds(price). News→move causal tie ("signing → win total +2.5") is NEW small join code. - Render: extend
/explore(ExploreHub) + the offseason-awareoutlook.js/emptyState.js; reskin the retiredTerminalTemplatesinjury-wire/leader LAYOUTS with REAL data (they're SAMPLE data today — never route as-is). New/futuresroute 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_KEYSpath, 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_KEYis 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_KEYSpath 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.