OpenAthlete
AGPLv3 endurance training platform: EU-oriented hosting, transparent CTL/ATL/TSB logic in code, self-hosting, and full export. Built in Grenoble.
Model-derived OVER/UNDER calls with confidence for college football and NFL player props.
Sports & fitnessPython · TypeScriptGitHub @lgdsbrandOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
A weekly board that states, for each relevant college football player, whether
the model favours the OVER or UNDER on that player's book prop line, with a
confidence percentage.
The model projects a full outcome distribution internally; the over/under
call and the confidence figure are derived from it — the call is the side
holding the majority of the mass, the confidence is the mass past the line. The
projected range stays available as secondary detail, never as the headline
claim.
The project brief and working agreement is CLAUDE.md. Read it
before contributing.Read the full README ↗A weekly board that states, for each relevant college football player, whether the model favours the OVER or UNDER on that player's book prop line, with a confidence percentage.
The model projects a full outcome distribution internally; the over/under call and the confidence figure are derived from it — the call is the side holding the majority of the mass, the confidence is the mass past the line. The projected range stays available as secondary detail, never as the headline claim.
The project brief and working agreement is CLAUDE.md. Read it before contributing.
| docs/runbook.md | Every job: what it writes, when it runs, and what to do when it does not |
| docs/configuration.md | The 15 app_config keys and every environment variable |
| docs/schema.md | Schema reference and the two rules it exists to enforce |
| docs/odds.md | What The Odds API actually serves, measured — CLAUDE.md §9.1 |
| docs/calibration-report.html | The Phase 3 deliverable (generated by run_backtest) |
| docs/odds-coverage-probe.md | Raw probe output (generated by probe_odds — overwritten each run) |
Phases 1–4 complete; Phase 5 in progress. Odds ingestion, the weekly AI-read seam, the client reskin and pipeline monitoring have all shipped.
| Phase | Scope | State |
|---|---|---|
| 1 | Foundations: schema, migrations, scaffolding, env | done |
| 2 | CFBD ingest, position-split engine, opponent adjustment | done |
| 3 | Per-market distribution models, anytime-TD model, calibration report | done |
| 4 | Board, filters, player detail, defense detail, weekly targets | done |
| 5 | Odds ingestion, weekly AI reads, reskin, monitoring, docs | in progress |
Two seams ship switched off, which is a supported product state rather than an
unfinished one: app_config.odds_adapter and ai_adapter both read "none",
so the board shows model leans with no book line attached and the player page
renders its empty read slot. Turning either on is a row edit — see
docs/configuration.md.
ai_adapter was switched to "gemini" on 2026-08-03 and reverted the same day:
the key resolves to Gemini's free tier (20 requests/day), which trains on
submitted content. It needs a key issued from the project that has billing.
The Phase 3 deliverable is docs/calibration-report.html: Brier skill +0.180, ECE 0.019 over 320,125 graded predictions across 2024–25.
Those numbers were +0.186 over 328,005 until 2026-08-02, when a lookahead bug was found and fixed: CFBD numbers postseason games from 1, so bowl games were stored as week 1 and leaked into every earlier week's features. The corrected model scores slightly worse, which is the point — the old figure was flattered by knowing what happened in December. ECE is unchanged, so calibration never depended on the leak. See the commit message on
1f4c781for the full account.
The board is filled by run_projections, which writes one distribution per
player-market and a pick wherever there is a line to call against:
python -m worker.jobs.run_projections --season 2025 --weeks 10
Yardage markets show the model's lean until a book posts; anytime TD is callable immediately because its line is structural (over 0.5). That split is the late-line behaviour CLAUDE.md §7 requires. Full details, and every other job, in docs/runbook.md.
--synthetic-lines is a development aid and its edges are meaningless. No
real prop line exists yet, so the flag posts −110/−110 quotes at each player's
trailing average under a book named DEV (synthetic). Those de-vig to exactly
0.500, which makes edge a restatement of the model's own confidence rather
than a disagreement with a market. It exists so the OVER/UNDER card can be
built and reviewed before books post in late August; it refuses to run outside
ENVIRONMENT=development.
web/ Next.js 16 (App Router) + TypeScript + Tailwind v4 → Vercel
app/ routes; /health is the live read-path proof
lib/core/ sport-agnostic: env, formatting, types
lib/supabase/ read-only clients (anon key)
app/globals.css ALL theme tokens — the only file the reskin touches
worker/ Python 3.11 → Render cron
worker/core/ sport-agnostic: projection → probability math
worker/adapters/ CFBD adapter; the layer the NFL build replaces.
odds/, ai/ and alerts/ are pluggable seams, each chosen
by an `app_config` row rather than an import
worker/jobs/ entrypoints, `python -m worker.jobs.<name>`
tests/ pytest
supabase/migrations/ numbered SQL, the source of truth for the schema
docs/ schema, runbook, configuration, odds findings (index above)
render.yaml worker blueprint; every secret is `sync: false`
The seam between sport-agnostic core and sport-specific adapter is deliberate (CLAUDE.md §3) so the core can be copied into the NFL repo cleanly. There is no shared package — just a disciplined boundary.
The migrations are the source of truth, so the database is a build artefact: either target reproduces it exactly.
Option A — local stack (needs Docker running):
npx supabase start
npx supabase db reset # applies every migration + seeds
Option B — a Supabase cloud project (no Docker needed):
npx supabase login
npx supabase link --project-ref <your-project-ref>
npx supabase db push
A free project is fine for development. Production should be a project in the client's Supabase organisation, on their billing, with you added as an admin — see the note at the end of this file.
Connection string: use the session pooler (port 5432,
aws-N-<region>.pooler.supabase.com). The direct connection is IPv6-only without the paid IPv4 add-on, and the transaction pooler on 6543 breaks psycopg3's prepared statements partway through a bulk load. See .env.example for the full reasoning.
cp .env.example .env # worker
cp .env.example web/.env.local # web (only the two NEXT_PUBLIC_ values)
Fill in SUPABASE_DB_URL and CFBD_API_KEY for the worker, and
NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY for the web app.
The service role key must never appear in web/.env.local. Anything
prefixed NEXT_PUBLIC_ is inlined into the browser bundle.
cd web
npm install
npm run dev # http://localhost:3000
cd worker
python -m venv .venv
.venv/Scripts/activate # Windows; source .venv/bin/activate elsewhere
pip install -r requirements-dev.txt
pytest
python -m worker.jobs.healthcheck
Six layers, in increasing strength. The last two are the standing rules: run
audit_data at the end of every phase, and let the docs tests fail rather than
letting a runbook go quietly out of date.
python -m worker.jobs.healthcheck verifies config loads,
Postgres is reachable, all 27 expected tables exist, the seed landed, the
worker can write (via the same pipeline_runs code path every later job
uses), and the CFBD key authenticates. Every check runs even if an earlier one
fails, so you get a full readout during setup. Exactly one CFBD call./health performs a real anon-key read on every request. Passing
proves the environment is wired, the migrations ran, and the RLS read policies
grant access. It also asserts play_player_stats is denied to anon, so a
too-permissive RLS change fails the check.cd worker
pytest tests/test_schema_constraints.py -v # needs SUPABASE_DB_URL
Applying a migration only proves the DDL runs. These tests prove the design:
that season-final ratings cannot be read as in-season knowledge, that the
backtest tripwire rejects future knowledge, that the splits function excludes
the prediction week, that picks.side/confidence/edge derive correctly from
the distribution, that a player with no book line still reaches the board,
and that anon can neither read play-level data nor write anything.
Everything runs in one rolled-back transaction, so no row survives. The suite
skips automatically when SUPABASE_DB_URL is unset.
cd worker && pytest tests/test_probability.py
Pure unit tests, no database needed.
cd web
npm run check:schema # selects every column the app reads, against the live DB
npm run typecheck
npm run test # hit-rate maths, via node --test (no test framework)
lib/core/types.ts is hand-written — the Supabase CLI is not installed and the
project is not linked, so there are no generated types. Hand-written types keep
compiling perfectly after the view beneath them changes, which TypeScript cannot
detect. check:schema closes that gap by asking the database directly, and it
also proves the anon role can still read what the app needs.
cd worker && python -m worker.jobs.audit_data # exits non-zero on any failure
160 checks over the schema, the RLS posture, the anti-lookahead guarantees, the odds math, the board's display contract and the ingested data. Every check states a pass condition rather than printing a number for a human to eyeball, because a report that only shows counts cannot fail.
Standing rule: run this at the end of every phase, and after any ingest. The defects it exists for are all silent ones — an endpoint truncating at 2,000 rows, ids arriving as strings so a join matched nothing, bowl games stored as week 1 and leaking into every earlier week. None of them raised an error.
cd worker && pytest tests/test_docs.py
Documentation is the one artefact with no natural failure mode: code that names
a renamed job crashes, and a runbook that names a renamed job reads perfectly.
These tests pin docs/runbook.md to the jobs that exist and to
render.yaml, and docs/configuration.md to the
app_config keys the migrations actually insert. Offline.
These are enforced in the schema or the tests, not left to memory. Full rationale in docs/schema.md.
team_rating_snapshots makes season-final ratings structurally
incapable of being read as in-season knowledge; backtest_predictions has a
as_of_week <= week tripwire. Never add a read path that returns "current"
derived metrics without a cutoff argument.model probability − de-vigged book implied probability, on the
side being taken. Not (projection − line) / line. Defined once in SQL
(edge_on_side) and mirrored in worker/core/probability.py; the tests pin
both to the same vectors. This matches the client's existing MLB pitcher
model so the two boards report comparable numbers.conferences.is_displayed) and must never restrict data collection —
cross-conference games cannot be opponent-adjusted with one side missing..env.example belongs in
git, render.yaml uses sync: false throughout, and app_config is
world-readable so it must never hold a credential.web/. Set the two NEXT_PUBLIC_ variables.
Preview deploy per branch so changes are reviewable on a live URL before merge.sync: false so it is entered in the
dashboard and never committed. The schedules and what to do when one stops are
in docs/runbook.md; monitor_pipeline watches the rest and
exits non-zero on anything critical so Render's own cron-failure notification
is the backstop.supabase db push against a fresh project.