available

Grégoire Marty

Full Stack Dev · Data Engineering & Analytics · AI / LLM / MCP · Production Systems · France → Worldwide

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.

Dada engineer. AI / MCP orchestrator. Phlegmatic efficiency.

[01]

Expertise

Full Stack Dev with a twist.

01

Full Stack Dev

Front ends · Vanilla JS SPA · GSAP motion
FastAPI · VPS · CI/CD · Release process

02

Data Analysis & Computing

Multi-source pipelines · Stats engines · Advanced metrics
Normalization · Caching · REST API exposure

03

AI Engineering

LLM integration · RAG · Agents
LangChain · Structured extraction · FastAPI

04

Model Context Protocol

Custom MCP server development · Plugin design
Integration · Internal tool connectivity

05

Sports Analytics

WNBA · NBA · NCAAW · NCAAM
PBP · 30+ advanced metrics · Shot zones · Charts · Computing · Centiles

[02]

Projects

● Live · hey-metrics.com

Metrics

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.

Data pipeline FastAPI Vanilla JS SPA 30+ metrics 99.5% PBP accuracy VPS · nginx Git CI/CD
more about Metrics →
Metrics — landing Metrics — league stats Metrics — PBP playlist Metrics — teams Metrics — lineups & trends Metrics — roster stats Metrics — players Metrics — player profile Metrics — chemistry Metrics — shot charts Metrics — scatter chart Metrics — schedule Metrics — game results Metrics — game view Metrics — advanced box score
Metrics — landing 01 / 15
Mon Meilleur CV

MMC

01 2023

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.

LLM Structured prompts Schema validation Document parsing FastAPI Python
Le Roster — page d'accueil

Le Roster

02 2023 – 2025

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.

40-writer newsroom Platform ownership Release process Hosting Full-stack
La League — basketball roleplaying league

La League

03 2023

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.

Game engine Headless testing Procedural generation OBS broadcast layer Python
Reel Studio — vertical video editor

Reel Studio

04 2026

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.

Playwright rendering Local Whisper ffmpeg FastAPI Python
[03]

Open Source

Four libraries. Each one started as a wall, not a feature.

MIT · zero-dependency · hatchling · CI matrix on Python 3.11 / 3.12 / 3.13 · pytest and ruff on every push

twinrun

differential testing

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.

bytecap

memory-bounded cache

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

staleguard

freshness contract for derived data

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.

hardwood

coordinate normalization

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 →
[04]

Scars

Four things I got wrong. Every one of them was silent.

On this site, not on a client's · each one is fixed, and each fix is held by a test that has been made to fail on purpose

eight dead transitions

a variable that was never declared

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.

a test suite that certified the bug

reference screenshots recorded after the fact

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.

a tolerance that saw nothing

18 432 pixels of allowed difference

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.

a debug script on the public domain

HTTP 200, application/javascript

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.

[05]

Services

Scoped engagements — or a permanent seat.

Open to contract work and to full-time roles · remote from France, worldwide · available since January 2026

Website — Frontend & Backend3–5 weeks
Online / Local Tool for Independent Businesses4–6 weeks
LLM Integration Sprint2–4 weeks
Custom MCP Server1–3 weeks
Sports Analytics Pipeline4–8 weeks
AI Audit & Roadmap3–5 days
Monthly Retainerongoing
Content Creationongoing