Community discounts for those who serve.

The question I couldn't answer last November

I had a goal: finish Unbound Gravel 100 in Emporia, Kansas on May 31, 2026. One hundred miles of Flint Hills gravel, unmarked, remote, and in a year like this one, deep mud.

I had the data. Years of Strava rides. A Whoop on my wrist recording HRV, resting heart rate, and recovery every morning. An FTP number from my last test. A race on the calendar.

What I didn't have was a plan that connected any of it. Or someone to build one with me.

I talked to a few cycling coaches. Good ones in the endurance space run $200 to $400 a month. They'll build you a periodized plan, adjust it weekly, and answer your messages, usually within 24 hours, usually with a few sentences, usually without having read your Whoop data or your Strava files in any meaningful depth. For 13 weeks of Unbound prep, that's $800 to $1,600 before you've bought a single gel.

I decided to build it myself. With Claude.

This is the story of how I did it, what happened, and what 106.3 miles of Kansas mud taught me about AI-driven training, the real version, not the deck pitch.


The coaching gap is not what you think

Every cycling app promises a training plan. TrainingPeaks, Garmin, Wahoo, Intervals.icu, they all have algorithms that will generate a 12-week build if you tell them your FTP and your goal event.

The problem isn't generating the plan. The problem is that life doesn't follow the plan.

A human coach earns their fee in the exceptions: you get sick in week 11. You're traveling to Tahiti in week 5 and can't bike for 10 days. You moved your long ride from Sunday to Saturday because Sunday is a travel day. Your Strava cut off a Tuesday ride halfway through. The coach reads the context, adjusts, and tells you what to do next.

Most apps handle none of this. They show you a generic plan and a red dot when you miss a session.

The insight that made this project work: Claude can handle all of it, in real time, with your actual data, if you give it the right context and the right tools.


What you actually need to build this

Before I get into the technical details, let me answer the question I get most: do you need a power meter?

No. Power is useful, but it's not the bottleneck. Reason why I don't track power on every ride is that I ride multiple bikes mainly off the grid. 

Here's what actually matters:

Strava activities. Every ride you do, automatically logged. Distance, moving time, elevation gain, average heart rate. If you have a power meter, average watts and normalized power come along too. This is your ground truth for what training you actually completed.

Whoop recovery data. HRV, resting heart rate, recovery score, every morning, before you've done anything to bias the numbers. This is your body's vote on what today should look like. A red recovery day (below 34) means the plan changes, regardless of what was scheduled.

Your FTP. One number. This anchors everything: your power zones, your TSS calculation, what "threshold" actually means on a given ride. If you don't have a recent test, use a 20-minute all-out effort × 0.95.

Your constraints. Which days you don't ride (I don't ride Wednesdays). Your race date. Any trips or obligations that need to be baked in. A two-line settings file.

That's the whole data model. No $1,000 power meter required. For rides without power data, I built a terrain-corrected TSS estimate — the hillier the ride, the more it corrects average watts upward to approximate normalized power. It's not perfect, but it's far more useful than ignoring the data entirely.


Building the coach

The architecture is simpler than it sounds: a FastAPI backend in Python, a Next.js frontend for the dashboard, and a SQLite database on my laptop. The whole thing talks to the Strava API and the Whoop API on a schedule, syncing every six hours automatically.

The brain is the planning engine. I wrote it to:

Generate a 13-week periodized plan anchored to May 31. Base phase (weeks 1 to 4): long rides ramping from 96 to 120 km at Z2. Build phase (weeks 5 to 9): threshold intervals, long rides with tempo embedded. Peak phase (weeks 10 to 11): back-to-back weekend simulation. Taper (weeks 12 to 13): volume drops 40 to 50%, intensity stays.

Match actual rides to planned workouts, scoring each day 0 to 100 based on volume, duration, and power zones. A 79-mile Sunday long ride scores 85 against an 85-mile target. A 9-mile ride scores 41 against a 25-mile threshold target. The week score is the average across all scored sessions.

Pull Whoop recovery into the plan. Red recovery day? The coach downgrades threshold sessions to easy spins, flags the reduction, and adjusts the week target. Green day? Plan runs as written.

Compute zone distribution, both planned (how the 13 weeks were designed across Z1 to Z5, including warm-up and cool-down time for threshold sessions) and actual (per-ride, terrain-corrected NP classification). Planned: 71% Z2, 21% Z1, 5% Z4. The kind of aerobic-polarized distribution you want for a 7-hour gravel race.

Power zones, TSS, terrain correction, the scoring engine, all of it lives in about 600 lines of Python. The dashboard is the visual layer on top.


The dashboard

I built it to answer one question every morning: what does my week look like and how am I tracking against the plan?

Five components:

Current Week Card. Day-by-day grid with colored dots (green/yellow/red/gray), actual km completed, heart rate if available, and a progress bar showing actual vs target distance. Bonus rides on rest days show up here too. If I rode 48 km on a Wednesday off day, it counts.

Volume Bar Chart. 13 weeks of planned target bars with actual overlay. Past weeks show what I completed. Future weeks show what's coming. The week I was in Tahiti shows hiking hours, not bike km, because hiking was the plan.

Training Load Line. Weekly TSS over time. Actual (terrain-corrected per-ride) in orange. Planned (50 TSS/hr estimate for Z2 base) in gray. The gap between them tells you whether your training stress is tracking the plan or diverging.

Zone Distribution Donuts. Planned vs actual, side by side. The planned donut uses realistic splits. Threshold workouts aren't 100% Z4 (that's 36 minutes of intervals; the other 70 minutes are warm-up, cool-down, and recovery). Commutes aren't Z2, they're Z1, because the plan says "stay Z1, 0 to 105W." Hiking days are excluded entirely from the planned bike distribution.

Recovery Widget. Today's Whoop score, HRV, and resting heart rate. Green/yellow/red status. The historical 14-day trend. If you're heading into a key workout, you want to know whether your body voted yes.

The whole dashboard builds automatically from the live data. No manual logging.


When the plan met reality

This is the part the training apps don't show you.

Week 5, Tahiti. I was gone March 29 to April 10. Not a sabbatical, real hiking. The plan automatically overlaid the trip: departure day became travel and rest, weekdays became 2 to 3 hour aerobic hikes (Z1 to Z2 on the trails, excellent aerobic maintenance), Saturday became a long hike with 1,500m elevation gain, and Sunday became easier. When I got back, the distance targets for weeks with hiking excluded the hike hours from the bike volume. One specific problem: week 4 had the Sunday long ride scheduled, and Sunday was my departure day to Tahiti. The coach moved the long ride to Saturday automatically, so I got the 121 km in before flying. These are the small adjustments a human coach makes intuitively. I had to build them, but once built, they just work.

Week 9, schedule flip. I did my Thursday Base ride on Wednesday instead. One line change in the plan: swap Wednesday Rest and Thursday Base. The Wednesday ride matched against the Base workout, scored correctly, and Thursday showed no penalty. Without the fix, it would have logged Wednesday as a gratuitous rest-day bonus and Thursday as a missed session, a zero.

Week 10, back-to-back moved. The peak back-to-back weekend (Day 1: 105 km, Day 2: 64 km) was planned for Saturday to Sunday. I moved it to Friday to Saturday. The coach updated the plan accordingly: Friday became Day 1, Saturday became Day 2, Sunday became rest, and the weekly volume target adjusted for one fewer commute.

Week 11, flu. I got a mild flu Monday. Missed Tuesday through Friday. Felt better but didn't want to push the Sunday long ride (85 km, race pace). The right move, with 16 days to race, was not to panic and not to try to make it up.

I updated the plan for the week: Tuesday and Thursday became rest days with an illness note. Friday became an optional easy spin. Sunday became a 64 km recovery ride, just spin, no intensity, shake the legs out. And the week note read: "Fitness is banked from week 10 back-to-back. Rest, recover, easy spin Sunday. Taper starts fresh next week."

That's the kind of thing a good coach says. It helped that week 10 was actually my best training week of the build: 77.5 miles on Saturday alone (SF to Point Reyes and back), which became my longest training ride heading into the race.


13 weeks by the numbers

Before race day, here's what 13 weeks of AI-coached training actually looked like:

Total
Rides completed 52
Distance 2,035 km (1,264 miles)
Moving time 97.5 hours
Elevation gain 26,064m (85,500 ft)
Longest training ride 132 km (82 miles)
Peak week 296 km (184 miles)

The peak week was the back-to-back: 74 km on Friday and 125 km on Saturday, after a threshold session Tuesday. That simulation was the closest I came to race-day conditions before the race itself.

The two Tahiti weeks show up as outliers in the data, low bike volume, high hiking hours. Everything else tracks the periodized plan within reasonable margins: base building in March, threshold work in April, volume peaks in late April and early May, taper in the final two weeks.

No coach reviewed these numbers. The system generated them, tracked them, and adjusted the plan around them automatically.


Race day

The 2026 Unbound 100 started at 7:30 AM in Emporia. By then the wind was already up: 10 to 11 km/h from the southeast, building to 18 km/h by afternoon.

The course goes south first. Wind from the southeast, blowing northwest, is a partial headwind when you ride south and a tailwind when you return north. The right call was to be patient on the outbound leg and let the tailwind work on the return.

Then it rained. And the gravel turned to mud.

The course stats tell the story:

  • Distance: 106.3 miles 
  • Moving time: 7h 21m
  • Average heart rate: 153 bpm
  • Max heart rate: 180 bpm
  • Elevation: 1,278m

153 bpm average for 104W is a big gap. In clean conditions at 104W, my HR would be 130 to 135. The 20-bpm difference is the heat tax (27°C, 76% humidity), the mud tax (more muscular effort per kilometer), and the cardiac drift tax (7+ hours is a long time). The 180 bpm max was the steep muddy pitches where we had to muscle the bike through or transition to walking.

I finished. In conditions where DNF rates run 15% to 30% in muddy editions, completing 106.3 miles off an illness-shortened taper and a compromised final peak week felt like the actual win.

The training plan called for a 7h15m finish. Moving time was 7h21m. The model held.


What I learned

The data is not the hard part. Strava and Whoop cover 90% of what you need. The hard part is building the engine that connects the data to decisions, and then trusting the decisions when your body is telling you something different from the plan.

You don't need a power meter, but you need honesty about effort. Terrain-corrected TSS gets you close. Where it falls short is highly variable efforts, the 180 bpm spikes in the mud, the hike-a-bike sections, the five minutes of all-out effort to crest a rise. None of that shows up cleanly in a moving average. A power meter would capture it. HR is a lagging proxy. Ride by feel when the numbers are lying.

The plan is a scaffold, not a script. The weeks I deviated from the plan, moved rides, flipped days, skipped sessions, weren't failures. They were the product working correctly. A rigid plan that penalizes a Wednesday ride because Wednesday is "rest day" is a worse plan than one that absorbs the deviation and updates the score.

Illness during taper is probably fine. My week 11 flu was genuinely stressful. The week 10 back-to-back had been so strong that the fitness was already banked. Rest was the right call, and the data backed it up: I went into race day with 16 days of recovery since my last hard effort.

The hardest part isn't the training. It's the mud. No plan prepares you for 106 miles of Kansas mud. The walking sections, the mechanical risk, the caloric burn from isometric mud-fighting at 0W average power, the 180 bpm spikes just trying to stay upright. The aerobic base matters, but so does bike handling, mental resilience, and the willingness to run with your bike on your shoulder at mile 73 without it derailing your day.


What it cost

  • Claude Pro: $20/month
  • Whoop: existing subscription
  • Strava: existing subscription
  • About 4 hours of building
  • No coach

A human coach for 13 weeks: $800 to $1,600. What I got instead: a system that saw every ride, processed every recovery score, adjusted for illness and travel and schedule changes, and built a dashboard I could check in 90 seconds every morning. With full memory of everything.

The coaching layer is the only part that's actually scarce. And as of 2026, it's no longer scarce.


Build your version

You don't need to replicate my exact stack. But here's the minimum viable version:

Connect Strava and Whoop to a Python script that syncs daily. Write your periodized plan as structured data, phases, weekly targets, workout types. Build a matcher that scores each actual ride against the plan. Surface the results in whatever format makes you look at it every morning.

The single highest-leverage thing I built was the weekly score. Not as a grade, as a signal. A 22% week because I was sick looks different from a 22% week because I was lazy. The coach knows which is which because it has the Whoop data.

Your data is already being generated. The question is whether it's driving a behavior change or sitting in an app you check three times and forget.

The tools to close that gap are all available. The model is rentable for $20 a month. The APIs are public. The only thing left is to start building.

The mud will still be there on race day. But at least you'll know you put in the weeks.


Ari Tulla is the co-founder of Elo Health. He finished the 2026 Unbound Gravel 100 in 7h21m moving time in some of the muddiest conditions in the race's history.