available
← 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 implemented.

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
[01]

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.

[02]

Features

01 / 06

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.

02 / 06

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.

03 / 06

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.

04 / 06

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).

05 / 06

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.

06 / 06

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.

PBP video player — Paige Bueckers playlist WNBA League Stats — Shooting tab Metrics Charts — PACE × OFFTS% scatter Team page — ON/OFF lineups & trends heatmap Team page — Roster stats, Four Factors Player page — Paige Bueckers radar profile Player page — chemistry & 5-player lineups Player page — shot charts & assist network Games calendar — played day with results Game page — NYL 85 @ MIN 90, score progression Game page — advanced box score Players table — WNBA all players, Overall tab
PBP video player — Paige Bueckers playlist 01 / 12
[—]

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.

[03]

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

[04]

What made this hard

01

Reverse-engineering

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

02

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.

03

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.

04

A memory ceiling that could not be bought away

Resident memory sat at 5–6 GB on a single VPS that had already died of OOM once. Four leagues, seasons of play-by-play, and one machine with a hard limit. The obvious move was a second node.

I migrated the storage to a columnar layout instead. Resident memory dropped to ~0.25 GB and stored data shrank by a factor of fifty. The answer to a single-machine ceiling had to be a better structure, not more hardware — renting a bigger box would have moved the wall, not removed it.

Two of the published libraries came out of this work: twinrun, to prove not a single computed value moved across the rewrite, and bytecap, so an unbounded cache could never take the workers down again.

05

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.

[05]

Admin panel & agentic maintenance layer

Admin panel 01 / 03
[06]

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 →