The current (statistical) model is tiny (api/model.json ≈ 230 KB — 48 teams, ~1250 players)
and inference is analytic, so the whole app runs on Vercel: a static frontend + tiny Python
serverless functions. This mirrors the working reference project aevum-orrin/applet-material.
public/— static frontend (outputDirectoryinvercel.json), served at/.api/health.py,api/teams.py,api/squad.py,api/predict.py— one serverless function per endpoint, hit directly at/api/<name>. Python standard library only (no numpy/fastapi → nothing to install, cannot fail to import). They shareapi/_engine.py(analytic Poisson inference: scoreline + per-player Poisson-thinning anytime probabilities) andapi/model.json.vercel.json—outputDirectory: public; a negative-lookahead rewrite sends every non-/api/path to the frontend;includeFiles: "api/**"bundles the wholeapi/dir (the shared_engine.pyandmodel.json) into each function's lambda. Eachapi/*.pyis a separate lambda, so every one needs_engine.pyshipped with it; each handler also doessys.path.insert(0, dirname(__file__))soimport _engineresolves inside the lambda.requirements.txt— empty (the functions need no third-party deps)..vercelignore— excludespyproject.toml(andsrc/). This matters: newer Vercel builders scanpyproject.toml, and ours listsfastapi(optional-deps) while the real FastAPI app lives insrc/goalforge/api/app.py. If Vercel sees that, it switches to "FastAPI backend" mode, hunts for a single ASGI entrypoint, finds only thehandlerfunctions, and fails the build with "No FastAPI entrypoint found". Ignoringpyproject.toml/src/keeps it on the classic static-site + per-file Python serverless-functions path (what applet-material uses).
- Push to GitHub (done).
- vercel.com → New Project → import
GoalForge-AI. Framework preset Other; root = repo root. - Deploy → public URL. Every
git pushredeploys. Check<url>/api/healthfirst — it returns{"status":"ok","teams":48}when the model is bundled.
python scripts/train.py # (re)fit the Dixon-Coles team layer -> models/agent_intl.pkl
python scripts/scrape_wc2026.py # scrape the 48 real 2026 squads (Wikipedia) -> scratch cache
python scripts/fetch_understat.py # club xG/xA per player (5 big leagues; free Understat mirror)
python scripts/build_player_form.py # name-match club form onto the 2026 squads -> player_form
python scripts/build_wc2026_model.py # combine -> api/model.json (xA assists + club-enriched scoring)
git add api/model.json && git commit -m "update 2026 model" && git pushbuild_wc2026_model.py reuses the validated team checkpoint for the scoreline, and for the ~1/3
of squad players with top-5-league data blends recent club xG (scorer) and xA (assister,
the validated chance-creation signal); the rest fall back to international goals/caps + a position
prior. See docs/evaluation.md §8-11 for the honest evaluation behind these choices.
Once neural models arrive, the heavy model moves to a Hugging Face Space (torch + GPU); Vercel keeps serving the frontend, calling the HF backend. See docs/workflow.md.