All work

Case study · Product

AI Running Coach

My best friend challenged me to run an ultra. So I built an AI running coach to beat him — and six months later I did, by twelve minutes.

Year
2025–26
Role
Solo founder — the app, the plan engine, the guardrails, and the guinea pig.
Expo / React NativeSupabaseInngestGeminiDeepSeekStrava
AI Running Coach — illustration53 KM
01

Problem

My best friend — a slightly more experienced runner, with the same insane drive and the same lust for winning a stupid bet — challenged me to run an ultramarathon. I'd barely run before. I said yes anyway, because there has to be a way, and then I gave myself a second problem to make the first one interesting: I'd build my own AI running coach and let it take me from my first jog to the start line. Not a training app with a coach bolted on. A coach — one that plans, remembers, adapts, and is honest when the plan meets a body.

The target was concrete and unforgiving: the Amerongse Berg Ultra, 53 km of trail, 28 March 2026. Six months out, my longest run was measured in minutes. The bet was real, the date didn't move, and I wanted to win — so the coach couldn't be a demo. It had to survive contact with an actual season of training, an actual injury, and an actual person who does not always do what the plan says.

02

Constraints

  • Six months, one race date, zero base. From first jog to 53 km trail — no room to start over.
  • The plan has to be grounded in reality, not vibes — it reads my actual runs (heart rate, pace, distance), not what I claim I did.
  • Plan quality can't depend on the model grading its own homework. A coach that contradicts itself once loses my trust forever.
  • Safety is non-negotiable: nothing that reshapes the season — the race, the phase, the timeline — gets applied without me saying yes.
  • It runs on a phone. The heavy plan generation has to survive me closing the app mid-thought.
  • n = 1. I was the only user, the only test subject, and the only person who paid the price when it was wrong.
03

Architecture

The app is Expo / React Native on a Supabase back end — Postgres with Row-Level Security so my data is mine, Auth, and Deno edge functions at the boundary. Strava is wired in over OAuth with a webhook that auto-syncs every run the moment it lands, so the coach is always reasoning about what my legs actually did — heart rate, pace, distance — not a self-reported fantasy.

The interesting part is the plan engine, and the load-bearing decision there is that generating a training plan is not one prompt. It's a pipeline: a durable draft is written first, then a senior-coach review pass critiques it, then an enhance pass rewrites it into the final plan. That pipeline runs on Inngest over Google Cloud Run, checkpointed at every step — so when I close the phone mid-generation (which I did, constantly), it picks up exactly where it left off instead of starting over or, worse, half-finishing. Every applied plan also stores a snapshot of the prior state, which makes the whole thing undoable: one tap and I'm back to the plan I had before.

Two models, chosen for two different jobs. Google Gemini runs the real-time chat coach — function-calling across eleven tools, with a long-term “coach notes” memory so it actually remembers me between conversations (the calf that grumbles, the week work ate). DeepSeek-v4, reached through an OpenRouter gateway, does the reasoning-heavy plan generation, where I want a model that will sit and think about progression rather than answer fast. Using the right model for each half was cheaper and better than forcing one model to do both.

Over all of it sits the part I'm proudest of: a deterministic coherence guardrail engine, layered on top of the LLM, that never asks the model whether its own plan is any good. It checks the things a real coach's eye checks — is there a taper before the race, do the long runs progress without a reckless spike, are hard days spaced 48–72 hours apart, is race week structured sanely, does the week land near the 80/20 easy-to-hard intensity split. If the plan violates the rules, it doesn't ship. Plan quality is enforced by code, not by the model's opinion of itself.

And the safety line is a hard one: anything that changes the shape of the season — the race, the current phase, the timeline — is staged as a proposal I have to approve. The coach can suggest a rebuild all day long. It cannot silently rewrite my season while I sleep.

04

Decisions & tradeoffs

The biggest call was refusing to let the model grade itself. The seductive design is a single clever prompt that generates a plan and a second prompt that says “is this good?” — and it demos beautifully. But an LLM asked to critique its own output is a yes-man with extra steps, and a training plan that quietly loses its taper is the kind of failure you only discover on race week. So I paid for a deterministic guardrail engine instead: real rules, checked in code, that a plan has to pass before it reaches me. It's more work and it's less magic. It's also the only reason I trusted the thing with six months of training.

Splitting the pipeline into draft → review → enhance was a deliberate cost-and-quality tradeoff. One monster prompt is cheaper and simpler on paper. But a plan is a reasoning task with distinct passes — get a shape down, criticise it like a senior coach, then polish — and separating them gave each pass one job it could actually do well. The price is orchestration complexity and more tokens; the payoff is plans that hold together instead of plans that look right until you read them twice.

Making the whole engine checkpointed and undoable was the concession to reality that I underrated at first and now wouldn't build without. It runs on a phone; the phone gets closed; generation takes real time. Checkpointing on Inngest means a closed app is a paused job, not a lost one. Storing a snapshot of the prior state before every apply means “undo” is a real button, not a promise — which matters enormously when the user (me) is about to let an AI rearrange the next month of their body.

Two models instead of one was the un-flashy right answer. It would have been tidier to standardise on a single provider. But real-time chat wants fast function-calling with memory, and plan generation wants slow, deep reasoning — so Gemini took the conversation and its eleven tools, DeepSeek took the thinking, and the OpenRouter gateway kept me from marrying either. The tradeoff is two integrations to maintain; the win is that each half of the product uses a model that's actually good at its half.

The theme across all four: I kept choosing the boring, verifiable version over the impressive, self-certifying one. A coach you bet your season on can't be a magic trick.

05

What didn't work

Around month four or five, running in the snow, I hurt my popliteus — a small muscle behind the knee that is not small when it's angry. This is the part no clever architecture saves you from: the plan meets a body, and the body wins. The honest move, the one the coach and I made together, was to stop pretending software could physio its way out of an injury. We saw a real physiotherapist, and I paused the AI coaching entirely until the PT cleared me to run.

That cost more than a month. A couple of weeks with no running at all, then a slow, humbling rebuild — my first run back was a laughable three-kilometre run/walk, six weeks out from a fifty-three-kilometre ultra. The PT was honest too: he didn't think the ultra was still possible. On the numbers, he was right to doubt it.

So the coach did the one thing it was actually built for. Cleared to train, it generated a thirty-day “masterplan” — the taper, the long-run progression, the 48–72 hour spacing, the 80/20 split, all of it still passing the same deterministic guardrails — to go from three kilometres to fifty-three. I followed it, day by day. The plan that a human expert doubted was possible was one the engine could at least build coherently, and that I could actually run.

Pausing my own product to trust a human expert is the least “look at my AI” thing I could have written here, and it's the most important. The coach's best decision was knowing when it wasn't the right tool.

06

Outcome

3 km
before
53 km
after
30 days
run/walk → trail ultra, post-injury

First run back from the popliteus injury was a 3 km run/walk with ~6 weeks to race day; the coach's 30-day masterplan bridged the gap the physiotherapist doubted was closable.

On 28 March 2026 I finished the Amerongse Berg Ultra in 6 hours 43 minutes. My best friend — the one who started this whole thing — came in twelve minutes later, at 6:55. I won the bet. Six months from my first jog, one lost month to injury, thirty days from a laughable 3 km back to 53 km of trail. I filmed all of it, and I'm cutting it into a documentary, because the story is the product: this is my startup, and the coach earned its first case study on my own two legs.

The honest limitation, stated plainly: n = 1. I was the sole user of my own product — a founder-athlete of one, which is a great story and a terrible sample size. Everything here worked for exactly one runner. Proving it generalises beyond me is the actual next chapter, and a public release is still a good while off; I'd rather ship something that's been tested than something that's been demoed.

07

My role

Everything, on both sides of the screen. I designed and built the Expo / React Native app, the Supabase back end with RLS and edge functions, the Strava OAuth + webhook sync, the checkpointed draft → review → enhance plan engine on Inngest and Cloud Run, the two-model split across Gemini and DeepSeek via OpenRouter, the deterministic coherence guardrail engine, and the human-in-the-loop approval flow. Then I was test subject n = 1 — the person who trusted all of it with six months, one injury, and a bet I refused to lose.