Sports Dashboard

MI Bivariate Poisson + Dixon-Coles + Elo

← Back to Blog
|System|FIX

Stop Patching Symptoms: The Team Name Mapping Anti-Pattern

Eight settlement failures in 5 weeks, all from the same root cause: a hardcoded 100-entry team map sitting next to a verified 588-entry map that was never loaded. Fixed by loading the verified map at runtime and auto-generating it to 1,257 entries across 40+ leagues.

The Question

Why did settlement keep breaking? Eight times since March 11, bets silently failed to settle because team names didn't match between the betting source and the results source. PSG, Bolton, Doncaster, Sporting CP, Angers SCO — each time the fix was adding one hardcoded alias. Each time the bug came back with a different team.

What We Found

The root cause was architectural, not data.

The settler had two team name mapping systems:

SystemEntriesScopeMaintained
Hardcoded `FOOTYSTATS_TEAM_MAP`~100settler.ts onlyManual, per-bug
Verified JSON (`footystats-to-cache-team-map.json`)588Auto-generated via date+score matchingOne-time

The hardcoded map was a manually-maintained subset of the verified map. The settler loaded the hardcoded map. The verified map sat unused. Every team in the gap was a ticking time bomb.

Timeline of identical bugs:

DateTeamDays StuckFix
Mar 11Multiple (Fotmob 404)~10Switch to FootyStats
Mar 21PSG, Bolton, Doncaster~2Add 3 aliases
Mar 27Sheffield Wed, Ath Bilbao~5Add 2 aliases
Apr 11Sporting CP~2Add 1 alias
Apr 13Angers SCO~2Add 1 alias

Five incidents, same shape, same cause. Classic symptom-patching loop.

The Anti-Pattern

Never maintain a hardcoded subset of a generated dataset.

The verified map was built correctly — date+score matching across thousands of matches, 99.8% coverage, zero ambiguity. It was the right artifact. But it was treated as a reference document rather than runtime infrastructure.

Meanwhile, every time a team broke, the fix was FOOTYSTATS_TEAM_MAP["Angers SCO"] = "Angers" — one more line in the hardcoded map. Quick, satisfying, and guaranteed to recur.

The deeper failure: nobody asked why we were maintaining two maps. The first bug was a data fix. The third was a pattern. By the fifth, it was an architectural problem being solved with data fixes.

What Was Fixed

Three changes that kill the entire bug class:

1. Runtime loading. The settler now loads footystats-to-cache-team-map.json at startup (lazy, cached). The hardcoded map is a fallback for formal long names the API occasionally returns (e.g., "AFC Bournemouth" → "Bournemouth").

2. Auto-generation script. scripts/build-footystats-team-map.ts fetches current FootyStats data and matches it against the local cache using the same date+score approach. Expanded the map from 588 → 1,257 entries covering all 40+ leagues.

3. Diagnostic logging. When a bet can't find its match, the settler now logs near-matches:

[settler] UNMATCHED: 2026-04-12 Stuttgart vs Hamburg
  (key: 2026-04-12_VfB Stuttgart_Hamburg)
  — possible name mismatch? Near results:
  2026-04-04 Stuttgart vs Dortmund (key: ...)

This surfaces the gap immediately instead of a silent "no result available yet."

What This Means

The meta-lesson: When the same bug shape recurs 3+ times with different data but identical cause, stop fixing instances and fix the architecture. The cost of each individual fix was 2 minutes. The cost of the pattern was 8 incidents, 30+ days of cumulative stale bets, and corrupted P&L tracking.

For new leagues: Run the generation script after adding a league. No manual alias work needed.

What's Next

  1. CLV backfill still misses some matches due to date drift (Odds API logs bets 1–3 days before actual kickoff). Lower priority — enrichment, not settlement.
  2. The backfill code path doesn't have the date-drift correction that the main settlement path has. Could unify.
  3. Remaining gap: when FootyStats returns a team name that's not in the verified map AND not in the hardcoded fallback, the bet still silently fails. The diagnostic logging catches this, but a proactive check at bet-logging time would be better.