Product

Kickoff

Live football across 65 competitions, on a dashboard you compose.

Working — runs on synthetic data with no API key

kickoff / by the numbers

  • 103end-to-end tests passing across 16 Playwright suites, none of which call the vendor
  • 6 → 0vendor calls per match-page view, on a warm cache
  • 65competitions across Europe and the Americas, added as configuration rows

Live scores from 65 competitions where the API bill tracks how much football is being played, not how many people are watching.

The challenge

Live sports data is rented from a small number of vendors, billed per request, and every competitor rents it from the same place. So there are two problems, and the interesting one is not the obvious one. The obvious problem is that you cannot beat the incumbents on data coverage. The real problem is that the naive architecture — fetch on page view — makes your cost scale with your success, so the product gets less viable exactly as it starts working.

What we built

A Next.js application whose central rule is that pages read our cache and only the background poller reads the vendor. One poll returns every live match; ten thousand readers cost one upstream call.

Match and team pages cannot work that way — nobody can prefetch tens of thousands of them — so they read through a cache instead, on TTLs derived from the value rather than a config default: a finished match is final forever, the same call for a match in play is worth thirty seconds, an empty lineup means “not announced yet” and must be re-asked, and head-to-head is history that cannot change at all. Concurrent requests for the same key are deduplicated. The measured effect was 6 vendor calls per match-page view down to 0 on a warm cache.

Competitions are data, never code. All 65 live in one registry; adding a league is a row, and the sitemap, static generation, live filter and competitions page all read from it. Nothing in the codebase may branch on a specific competition.

Bad sports data doesn’t throw — it shows a wrong number to someone who trusts it. So automated monitors run on every poll, checking for things impossible in real football: a match still live 150 minutes after kickoff, a score that went down, a team playing itself, an unmapped provider status code, two matches claiming the same URL. Findings surface at /api/health.

The news aggregator’s feed list is the entire SSRF boundary — a feed URL is source code, reviewed as source code. The fetcher refuses any host not derived from that list, refuses non-HTTPS, and sets redirect: 'error', because an allowlisted host that 302s to a cloud metadata endpoint is the textbook attack. We store a headline, a summary and a link, never a body, and every card names its publisher — the conventions a well-behaved aggregator follows. “Most read” counts our own opens and the UI says so, rather than inventing a plausible number.

Capabilities this demonstrates

  • Cloud & DevOps — cost-bounded architecture, value-derived TTLs, cron polling, actionable health
  • Secure Software Engineering — SSRF allowlist with redirect refusal, rate-limited anonymous write, protected cron endpoint