← All findingssource · memory/project_worldcup_pipeline.md
Full World Cup betting pipeline built 2026-06 to reach NBA/WNBA parity (graded picks + pre/post explanation). Data is ESPN keyless (site.api.espn.com/.../soccer/fifa.world/...) — football-data.org was abandoned (account disabled after key exposure + rate burst); don't revive it.
Producers (sport-agents core/): worldcup_screen.py (game lines: AH/totals, Pinnacle de-vig), soccer_projections.py (Poisson player props w/ position-aware Gamma-Poisson shrinkage — shots/SOG/saves/cards), worldcup_analyst.py (LLM over/under "call" w/ grade+reasoning = the soccer claude_combined). Post with --log. Three formula_ids: worldcup_lines, worldcup_props, worldcup_analyst (all whitelisted in grading picks_manual.go).
SDS adapter: internal/adapters/soccer/ (ESPN-backed, league="worldcup"). Games-list route is /api/v1/worldcup/games/date/YYYY-MM-DD; per-game /api/v1/worldcup/games/{id} + /boxscore + /events (play-by-play). Score lives in the FLAT home_score/away_score fields (NOT home_team.score, which is null — don't probe the wrong field and cry settlement-bug); FetchGameScore reads the flat ones.
Play-by-play /events (SDS bcf66ac): optional EventsAdapter capability, soccer-only, off ESPN keyEvents → goals/cards/subs with minute+scorer+detail. ⚠️ The event feed is PARTIAL — ESPN omitted 1 of Argentina's 3 goals (keyEvents showed 3 goals, real score 1-3=4). So it's COLOR ONLY: grading recaps (2c88650) name scorers/cards from it but take the goal COUNT from the authoritative score. Guardrail verified: Jordan-Argentina recap said "3-1 away win" (score) + "Messi and Lo Celso scored free-kicks" (timeline), never recounting. NEVER derive a score/total/result from keyEvents.
Grading (shipped + verified 2026-06-28, commits dab8b08 + 9296cb6): market-based settlement (by prop_type family + league, NOT formula_id). SettleWorldCupLines (off match score), SettleWorldCupProps (off boxscore, DNP-void on appearances==0). Post-game re-grade (buildWorldCupOutcome — bet SIDE discriminates AH cover-margin from over/under; no minutes so never "excused"; no decision_snapshot so posterior is margin-only) + recap (recap/worldcup.go, haiku, score/box-grounded). Full flow runs in the settle cron AND POST /clv/settle-worldcup?date= (settle→regrade→recap). Verified: 6/6 picks graded to rubric (AH A→A+, total miss B→C, blowout-under B→D, saves A→A+) each with grounded recap.
Pitfall fixed: WC prop settler allowlist omitted saves/yellow_cards/red_cards/fouls → those props captured but never settled/regraded/recapped. Fixed 9296cb6. If adding a new soccer prop_type, add it to ListUnsettledWorldCupPropBets.
Ledger-rendering fixes (2026-06-28): (1) Game-line picks showed "Match total Over 3 TOTAL_GOALS" — producers collapsed the game into "Match total"; now matchup_label() puts "Home vs Away"/"Backed (vs Opp)" in player_name (sport-agents 9a11b05), and grading gradedPickLabel/propAbbrev are soccer-aware (total_goals→"Goals", AH→"Team -0.5", no bogus Over/Under) (b8f66e7). (2) ConvictionTiers showed the WNBA ladder on the worldcup tab (mapped non-nba→wnba+claude_combined); now hidden for non-nba/wnba (nightly-picks f216173). (3) Recap grounding bug: the game-LINE recap was fed ONLY the score, so a real 0-0 (Colombia–Portugal, verified vs ESPN) got a FABRICATED "zero shots on target" — box actually had 37 shots/8 SOG. Fixed d2b3d25: line recaps now fed COMBINED shots+SOG from the box; model forbidden to mention shots when absent. LESSON: never let a recap narrate detail it wasn't given. Labels/recaps recompute from stored fields on read; to repair existing rows, DELETE+re-POST (nulls recap) then re-settle — the producers//clv/settle-worldcup?date= already do DELETE+re-POST idempotently.
Real-time? NO (investigated 2026-06-28). WC is NOT linked to live-service or sports-broadcaster. live-service polls 5 leagues (NBA/WNBA/NCAAB/NFL/MLB, ACTIVE_LEAGUES default "nba"), no soccer entry; it's poll→SSE (real-time downstream to clients, polled upstream — no true push feed exists). sports-broadcaster is league-agnostic downstream (Redis live:events/debate/grading → Discord/webhooks) but only relays what live-service emits. To add live WC: live-service config.go Leagues map + worldcup ESPN path (soccer/fifa.world, NOT sportlib's EPL soccer/eng.1 default) + ACTIVE_LEAGUES env; broadcaster needs zero changes. NOT worth it unless you want WC on the live SSE stream / Discord in-game pings — it's latency reduction (~30-60s ESPN poll), not a new capability; the pipeline is settlement-oriented, not live-scoreboard-oriented.