Full Stack Dev
Front ends · Vanilla JS SPA · GSAP motion
FastAPI · VPS · CI/CD · Release process
I build data systems that keep running when nobody is watching.
Front ends, pipelines, stats engines, LLM and MCP integration — from the storage layer to the deploy.
One of them serves four basketball leagues off a single machine, on 0.25 GB where it once took six.
No fluff. No slop.
Full Stack Dev with a twist.
Front ends · Vanilla JS SPA · GSAP motion
FastAPI · VPS · CI/CD · Release process
Multi-source pipelines · Stats engines · Advanced metrics
Normalization · Caching · REST API exposure
LLM integration · RAG · Agents
LangChain · Structured extraction · FastAPI
Custom MCP server development · Plugin design
Integration · Internal tool connectivity
WNBA · NBA · NCAAW · NCAAM
PBP · 30+ advanced metrics · Shot zones · Charts · Computing · Centiles
Independent basketball analytics platform covering WNBA, NBA, NCAAW and NCAAM. Multi-source PBP data pipeline, 30+ advanced metrics computed from scratch, vanilla JS SPA, FastAPI, VPS infrastructure. Solo. No framework.
















In production since 2023. Every output is schema-validated before a user ever sees it.
Parses an existing CV, enriches it through an LLM, then generates a version tailored to a specific job description. Every output is validated against a schema before it reaches the user — a generated resume that invents a job title is worse than no resume at all.

40 writers publishing for two years, on a platform I owned end to end.
Sports media outlet, built and run end to end. I owned the platform — frontend, backend, hosting, release process — and coordinated the newsroom publishing on it. Two years of shipping against an editorial calendar, where a deploy that breaks at 8pm breaks somebody else's deadline, not mine.

Game state is computed, not written — and every scenario is tested outside a browser.
A fictional basketball league played live on stream, and the engine that runs
it. Game state, rosters, standings and schedule are computed rather than
written: ~2,500 lines of JavaScript and Python, no framework, no build. The
trade engine runs headless under Node behind a window shim, so
every scenario is tested outside a browser. The broadcast layer is part of the
system, not bolted on — pure-green camera slots injected into each
sequence screen, keyed live in OBS and configured per page.

Preview and final render are the same page. Nothing leaves the machine.
Vertical video tool for social clips, driven from a phone. Preview and final render load the same page — Playwright replays it frame by frame as transparent PNGs, ffmpeg composites them onto the source. What you see is what you get, by construction rather than by care. Subtitles come from a local Whisper model: no third-party API, nothing leaves the machine.
Four libraries. Each one started as a wall, not a feature.
Hundreds of computed values had to survive a storage-layer rewrite. Unit and integration tests passed on both sides and proved nothing about equality.
Runs two implementations across a grid of inputs and proves not a single value moved — 174/174 and 109/109 identical, zero divergence. It caught a tie-break silently permuting a ranked list, and a date column changing type underneath a comparison. Both invisible to the tests that were already passing.
An unbounded module-level dict grew until the machine gave up, then took every worker with it on restart. 71 workers killed in a loop.
A cache bounded by measured bytes that refuses a load it cannot afford, instead of evicting everything and dying anyway. The measurement behind it: one parsed object costs 3.54 MB resident against 302 KB pickled — serialized size understates real memory by roughly 12×.
A refactor moved a consumer without moving its producer. 112 games invisible for four weeks — zero errors in any log, a fully green test suite.
A derived artifact now refuses to pretend it is fresh. The failure became loud instead of silent, which is the only property that mattered.
Every provider ships shot coordinates in its own frame of reference, and every league draws its court differently. Comparing anything meant rewriting the same conversion twice.
One canonical half-court, with league-aware geometry — NBA, WNBA, NCAA men and women. FIBA arcs, corners, lanes and restricted areas.
twinrun and bytecap both came out of one afternoon spent under a memory ceiling — the migration that took Metrics from 5–6 GB down to 0.25.
Read the code on GitHub →Four things I got wrong. Every one of them was silent.
I assumed a CSS custom property behaved like a missing value — ignored, with the rest of the rule surviving. It does not. An unresolved var() with no fallback makes the entire declaration invalid, so transition resolves to nothing at all.
Eight hover transitions were dead for weeks. The page rendered, nothing errored, the hovers just snapped. I only found it by reading the file, not by looking at the site. The linter now fails the build on an undeclared custom property — the one rule that would have caught it on day one.
Rewriting the project grid, I dropped the rule that sized the images. They reverted to their natural height — 750 px inside a 277 px frame — and overflow: hidden cut away two thirds of each one. Then I regenerated the reference screenshots on top of that state.
67 tests green, protecting the breakage. A baseline recorded after a defect does not catch it, it ratifies it. What matters is now held by layout invariants instead of pixels: a visual that overflows its frame fails a test that looks at no image at all.
The visual comparison ran at a 2 % threshold. On a 1280×720 screen that is 18 432 pixels — and the comparator only counts pixels that differ sharply, which on dark text over cream is a thin fraction of any change.
An entire section went from four blocks in a side column to five full-width blocks without a single test moving. Tightening the threshold surfaced eight silently stale baselines at once. The number is not the point: a mutant now restructures that grid and demands the guard fall.
The deploy job copies by exclusion — more reliable than an include list you forget to extend, but the trade is real. A Playwright diagnostic script committed by mistake matched no exclusion rule, so it was valid, so it shipped. No alert was possible.
The fix is not a longer denylist — that expires at the next file. The build now refuses to publish anything whose extension is not on a whitelist, and an unknown format fails by default. It found a second file on its first run.
Scoped engagements — or a permanent seat.