← home

Case study · hey-metrics.com

Metrics

Independent analytics for basketball lovers. Schedules, standings, advanced team and player stats, lineup data, shot charts, video play-by-play, assist network and more... Built for women's basketball first (WNBA + NCAAW), with NBA and NCAAM now inmplemented.

LLM · MCP Sports data Pipeline FastAPI Vanilla JS SPA PBP video VPS · nginx ● Live — closed beta
4
leagues
30+
advanced metrics
MCP
in prod
<1s
cached page load
VIDEOS
play by play module
50+
beta testers

What it is

Metrics is a single web tool built for the people who care deeply about what happens on a basketball court. Podcasters, writers, content creators, video editors, scouts, and of course Twitter basketball threaders. It surfaces the data those users actually need, at the granularity they actually want.

Women's basketball first. The WNBA and NCAAW are underserved by existing analytics platforms. Metrics was built to fix that. Starting with the leagues where the gap is largest. NBA and NCAAM are both now available on Metrics as well.

Currently running in closed beta.

Features

Play-by-Play video tool: the signature feature

Filter every WNBA or NBA boxscore action into a video playlist. Season · Team · Player · Focus (player plays, plays drawn, plays caused) · Play type (assists, steals, threes, turnovers, blocks, rebounds, fouls, free throws) · Advanced filters (opposing team, game segment, score margin, period).

Result: a queued video reel with matchup, period, and play description under each clip. 60 seconds to build a Jessica Shepard assist reel. 90 seconds to export a Kelsey Plum pull-up highlight for a Twitter thread or a Youtube video.

PBP video player — Paige Bueckers playlist

League stats: Four Factors, six tabs, every filter

Selected league Teams ranked across the Four Factors: Offense (ORtg, eFG%, AST%, TOV%, ORB%, FT rate) and Defense (DRtg, opp eFG%, opp AST%, opp TOV%, DRB%, opp FT rate). Every cell carries a colored percentile rank.

Toggle Adjusted to switch values into deltas from league average. Filter by date range, regular season vs playoffs, or limit to games against Top 5 / Bottom 5. Standings panel adds Top 5 / Bot 5 records — the kind of split that tells you whether a hot start is real or padded by easy matchups. Six tabs: Basics · Overall · Net · Offense · Defense · Shooting · Opp Shooting.

WNBA League Stats — Shooting tab
Metrics Charts — PACE × OFFTS% scatter

Team pages — lineups, trends, roster stats

Every 5-player combination the team has fielded, with minutes together, GP, all the shooting splits, plus-minus — filterable by minimum offensive possessions. Season sparklines (pick any stat, see per-game evolution for every player overlapped with the league average). Minute-by-minute court presence heatmap partitioned by quarter, with color intensity as usage indicator. Roster payroll, contract years, cap percentage.

Team page — ON/OFF lineups & trends heatmap
Team page — Roster stats, Four Factors

Player pages — radar, chemistry, shot charts, game log

Five seasons stacked vertically with career row. 6-axis radar (Scoring · Playmaking · Rebounding · Defense · Shooting · Discipline) against position-bucket peers. Lineups featuring this player — best and worst NetRtg chemistry by teammate. Shot charts (shots taken + shots created via assists). Assist network (outgoing and incoming). Full career game log, every row linkable to the game page. Season trends sparklines with rolling window (3 / 5 / 10 games).

Player page — Paige Bueckers radar profile
Player page — chemistry & 5-player lineups
Player page — shot charts & assist network

Games — calendar, score progression, advanced box scores

Month calendar with per-day game counts. Sidebar on each day: season series record, last 5 form line, tip-off times. On played days: final score, score progression line chart, top performers. Dedicated game page: by-quarter table, score arc, top performers, Four Factors mirrored across both teams, full box score with percentile ranks. Six tabs: Basics · Advanced · Shooting · Offense · Playmaking · Defense.

Games calendar — played day with results
Game page — NYL 85 @ MIN 90, score progression
Game page — advanced box score

Players table — every player, every filter

Every WNBA player who's logged a minute this season. Two hundred players. One sortable table with Four Factors per player, percentile ranks baked in under each value. Filter by date range, playoffs, vs Top 5 / Bot 5. Compare modal, scatter chart export, glossary.

Players table — WNBA all players, Overall tab

NCAAW — same engine, college side

Power 5 + Big East coverage (79 teams, ~1000 players per season). Independent ESPN crawler. UI patterns mirror the WNBA side — anyone fluent on Metrics WNBA reads NCAAW the same way. Players index, single-player pages with Basics / Advanced / Shooting / Profile / Game Log / Season Trends.

Under the hood

Data All cached locally. No live external calls during a user session. ↓ Refresh cycle Daily warmup at 6 AM Paris time Past seasons cached permanently Sub-second on cached pages — cold-start scans under 30s ↓ Backend FastAPI · 8 route modules · 4 uvicorn workers · systemd Disk-based JSON cache (~9.5 GB) · atomic writes · schema versioning PBP possession counter validated at 99.5% accuracy ↓ Frontend Vanilla JS SPA · custom router · lazy-loaded page modules No framework. No bundler. Every line hand-coded. ↓ Infrastructure Self-hosted on OVH VPS (Paris) nginx · Let's Encrypt TLS · GitLab CI/CD (~30s push to live) No third-party analytics · no ads · no tracking

What made this hard

Reverse-engineering

Most of the data is available for anyone, but still protected. A standard Python request returns may return a 403. I built a system to gather it and cache it indefinitely. Now runs silently on every request.

PBP possession counter at 99.5% accuracy

Counting possessions from raw play-by-play events sounds simple. And-ones, technical fouls, clock resets, quarter boundaries, team rebounds — all edge cases that break naive implementations. The remaining 0.5% gap is in the official data itself.

30+ advanced metrics, implemented from scratch

True Shooting %, Effective FG%, Usage Rate, Win Shares, PER, Dean Oliver's Four Factors, on/off court ratings, shot zone analytics across 6 zones, league-adjusted stats. Every formula from the source papers and cross-referenced.

A full SPA with no framework

No React. No Vue. No bundler. Custom router, lazy-loaded page modules. The 8-tab analytics grid — sortable columns, sticky team rows, Per Game / Per 100 / Totals / Adjusted toggles — hand-coded, every line. Bundle size near zero. Full control.

Admin panel & agentic maintenance layer

A full admin panel, built into the product

Three-column dashboard, auto-refreshed every 30 seconds.

Stats — signups by tier (free / beta / pro), beta funnel conversion, 24h visit counts parsed from nginx access logs, activity feed.

Ops — beta invite queue (pending / approved / rejected), feedback inbox from testers, one-click invite dispatch via Brevo.

Health — live VPS card (CPU, RAM, disk as color-coded bars with sparklines over time), API service status, cache state (stacked bar: today / yesterday / this week / older), warmup history for the last 7 runs, 6 alert rules with on/off toggle and test email trigger, quick actions (trigger warmup, reload nginx, restart API — each requiring typed confirmation). All side-effectful actions are logged in an admin_actions table.

6 automated alert rules, email-triggered

A cron runs every 5 minutes and evaluates six rules against the metrics_history table (itself populated every 10 minutes via psutil + journalctl): memory above 90% for 10+ minutes, disk above 85%, CPU above 85% for 15+ minutes, 5xx errors above 20 per hour, workers below 1, fewer than 10 cache files written today. Anti-flap cooldown built in. Alert emails go out via Brevo the moment a rule fires. No third-party monitoring service needed.

MCP servers & agentic layer for maintenance

Routine admin tasks — subscription management, email list operations, deployment triggers, beta approvals — are handled through a set of MCP servers connected to the product's external services: Lemon Squeezy (payments), Brevo (transactional email + lists), GitLab (CI/CD pipeline).

This means an LLM can be instructed to approve a batch of beta applicants, trigger a deployment, resync a subscription, or send a newsletter — through natural language, without touching the codebase. The MCP layer sits alongside the runtime app as a maintenance agent, used exclusively for operator-side tasks. FastAPI talks directly to the external APIs at runtime; MCP handles the human-in-the-loop operations.

In practice: what used to be 15 minutes of manual API calls across three dashboards is now a single instruction. This is MCP in production — not as a developer tool, but as an operational layer that reduces maintenance overhead at scale.

Stack

LLM MCP Python 3.12 FastAPI Vanilla JS nginx SQLite Git CI/CD VPS

"Built solo in Paris. No funding. No ads.""


Building something in sports data, analytics infrastructure, or AI systems? Most of the hard problems here are already solved.


Get in touch →