Build your own Couchcall
Couchcall is a few thousand lines of plain Python, a SQLite file, a static site and a scheduled job. Nothing in it needs a language model or an application server. This page is the recipe, the kinds of sources you can build on, and how to put an agent skill on top, so you can ask it questions in plain language.
The shape
- Ingest. Fixtures, prices and ratings once a day, results every hour: fixtures and results for each league into a games table, prices into a market table, ratings and tables into context. Keep the raw responses on disk so a parser change is a re-run, not a re-fetch.
- Score. For every upcoming game, seven numbers between 0 and 1 (closeness, quality, stakes, rivalry, pace, form and upset spice), combined into one weighted sum and stored with its components so the page can show its working. The weights.
- Verdict. For every finished game, an excitement number from how the game moved, turned into stars and into three words. Store the final score with the verdict, but never print the final score where the verdict is shown.
- Publish. Render every page as static HTML, check the tree (no spoiler words outside disclosures, unique titles, one heading, an image), and upload. A scheduler runs the whole thing hourly for results and once a day for the rest.
Sources you can build on
Our own mix is still changing: a blend of open feeds and paid APIs, swapped as one proves better or cheaper than another, which is why the pages describe the kind of source rather than a name, Wikipedia aside. These are the kinds of places to look, by what you need.
Fixtures, results and tables
Most leagues publish a schedule and a table on their own site. Check the terms before reading anything the site does not offer as a feed. Open football data projects cover the big European leagues with free keys. American leagues have public stats APIs (the NHL's is open and widely used; college football has a community data project with a free tier). Some large sports sites are driven by undocumented JSON endpoints; read their terms first, because they can change or block access without notice. Wikipedia's league tables are structured enough to parse for the small leagues nothing else covers; its licence asks for attribution, so credit it on the page.
Prices
You need a win probability, and a bookmaker consensus is the best cheap one. Some odds aggregators offer a free monthly allowance; pulled once a day and gated, it can cover a dozen leagues. Keep every price as a probability with the margin removed and never link to a bookmaker, so the page stays a guide to watching and not to betting. Where there is no market, points per game or an Elo rating gives a fallback, and the page should say so.
Strength ratings
Public Elo tables exist for club football and for several other sports, and the Elo sites publish CSVs. American football has published power ratings and playoff probabilities. The league table itself, pulled towards the average early in the season, is a sound rating where no other is published.
What happened in the game
The spoiler-free verdict needs a record of how the game moved: a win-probability series where one is published, otherwise a goal timeline with minutes. Look at match pages on league sites, open football data projects (events with minutes) and hockey leagues' play-by-play, and use the box score as a last resort (score only, and say so).
TV listings
Rights are sold by league and country, usually for several seasons at a time, and the details shift every season, so keep a small hand table with a review date per row and a form for corrections. Broadcaster schedules and the leagues' own 'where to watch' pages are the sources, and Wikipedia's broadcaster lists are a reasonable monthly cross-check.
Conventions worth copying
- Read matchups the way the league does: away at home with away-home scores in America, home v away in Europe. Use the start word the sport uses: kick-off for football, puck drop or face-off for hockey, as the league says it.
- Show times in the league's own zone and let a script convert; never make the server guess the reader's zone.
- Gate every paid call: a league spends credits only when it has a game within 36 hours and no fresh line.
- Keep a rivalry table per league as a CSV with a weight and a note; it is the one signal no feed gives you, and readers can correct it.
- Keep the TV table small: who holds the rights per league and market, linked, with a review date on every row and a way for readers to correct it.
An agent skill on top
Our own day-to-day use of the pipeline, away from the site, is a conversation: what is on tonight, was that game worth the replay, make me the weekend page. That works best as a skill, a short Markdown file an agent loads when the question matches, which tells it where the project is, which command answers which question, and the rules it must keep (spoiler-free, no invented games). This is a trimmed version of ours:
---
name: couchcall
description: Which games are worth a neutral's evening? Runs the local pipeline to rank upcoming games and rate finished ones spoiler-free. Use when I ask what to watch, whether last night's game is worth the replay, or to run the daily update.
---
Project directory: ~/work/couchcall (Python 3.12, SQLite at data/couchcall.sqlite). Read README.md if unclear.
| Ask | Command |
|---|---|
| Daily update | `--league all update` |
| What to watch this weekend | `digest --from <fri> --to <sun> --top 10` |
| Was last night's game good, spoiler-free | `replay --from <date> --to <date>` |
Rules: past games stay spoiler-free in every answer: name the game, its verdict, kickoff and channel, in kickoff order, never a score, winner, margin or comeback. Answer from the tables; if a source fails, say so and do not invent games. After I watched a game, append a line to data/notes.md with the date, game, our score and my verdict in a few words.
Three things make it work. The skill names commands, not procedures, so the agent runs the deterministic code and only phrases the answer. The spoiler rule is stated in the skill and enforced in the code, so the model is never handed a score to leak. And the last line turns watching into data: every game you rate by hand becomes a row for refitting the weights.
To tailor it, change three files: the league configuration (time zone, convention, which signals exist, promotion and relegation places), the rivalry CSV, and the skill's table of asks. A new league is one configuration entry, one rivalry CSV and, if no existing adapter covers its data, one adapter. The pages, feeds, images and checks follow.
python3 -m couchcall.cli --league all update
python3 -m couchcall.cli --league nhl replay --from 2026-10-27 --to 2026-10-28