Build a SaaS solo, and make more from it than you spend on Claude Code. That was the entire goal. I kept a running log for 90 days starting in February 2026, and this article is that log made public.
This isn't a success story polished for social media. It includes the wrong turns, the bad calls, and the tokens I burned for nothing — because those parts are where you'll find the most useful signal.
There's one more thing. Months after publishing, I went back through every technical claim in this article and checked it against the Cloudflare and Claude Code docs. Three of them were wrong. Not the docs — me. I've left the original claims in place rather than quietly editing them, with a correction next to each one. A wrong belief you can see being corrected is more useful than a clean paragraph that never shows its work.
1. Why Claude Code?
"Why not Cursor?" is the first thing people ask. I asked it too, for a while.
What changed my mind was a bug fix session. Claude Code traced the impact across multiple files, rewrote the tests, and staged a commit — autonomously, without me orchestrating each step. Cursor excels at in-editor completion, but Claude Code thinks through work. For SaaS development where dependencies get tangled fast, that difference is meaningful.
The other reason was cost transparency. Claude Code runs on Anthropic API usage billing, so I always knew exactly what I was spending and why. Flat-rate tools obscure that feedback loop. As an indie developer, cost awareness is directly tied to profit margin.
2. Choosing the Product (Day 1–7)
The first week went entirely to product selection. I used three filters.
① Something I'd use every day
The most reliable demand signal is whether you personally need it. I picked a unified dashboard for managing sales, reviews, and rankings across multiple App Store products.
② A stack where Claude Code excels
Next.js + TypeScript + Stripe + Cloudflare Workers. I chose this because Claude Code is deeply familiar with it — error self-correction rates are high, and generated code rarely needs major rework. If I'd gone with Ruby on Rails or Django, the learning cost and debugging overhead would have compounded.
③ Monetizable complexity within three months
I designed for "small start, charge early." The plan wasn't to finish and then add billing — it was to ship the first paid feature on Day 30 and work backward from there.
3. MVP Design — A Week of Architecture Conversations (Day 8–14)
Before writing a line of code, I spent a week working through the design with Claude Code, leaning heavily on plan mode.
# Example from the design phase
claude --model claude-opus-4-6 "
Review this architecture for Next.js + Cloudflare Workers.
Requirements:
- Fetch data from App Store Connect API
- Unified dashboard for sales and reviews across multiple apps
- Stripe Pro monthly subscription ($5/month)
- Must scale to 10,000 users
Known constraints:
- App Store Connect API has rate limits
- Cloudflare Workers CPU time budget (10 ms per request on the Free plan)
"Two architectural pitfalls surfaced from that conversation.
Correction 1 (2026-08-31)
That prompt above carries a claim I repeated for months: "Cloudflare Workers CPU limit is 10ms per request." That figure only describes the Free plan.
| CPU time | Workers Free | Workers Paid |
|---|---|---|
| Per HTTP request | 10 ms | 30 seconds by default, raisable to 5 minutes |
| Per Cron Trigger | 10 ms | 30 seconds (interval under 1 hour) / 15 minutes (1 hour or more) |
The part that actually mattered, and that I had backwards: time spent waiting on fetch(), KV reads, or database queries does not count toward CPU time (Limits). So my worry — "we call several external APIs per request, we'll blow the CPU budget" — was aimed at the wrong thing. What burns CPU is walking the JSON you got back and aggregating it in JavaScript.
Raising the ceiling on the Paid plan is a single config block:
{
"limits": {
"cpu_ms": 300000 // default is 30000 (30 seconds)
}
}Was the Cron + KV offload wasted, then? No. Ten milliseconds is genuinely tight — Cloudflare's own guidance notes that workloads doing auth, server-side rendering, or large payload parsing typically land in the 10–20 ms range. Precomputing the aggregation was the right call. I just held the right conclusion for the wrong reason, which is a fragile place to be.
Pitfall 1: Cloudflare Workers CPU limits
Aggregating App Store Connect data inside the Worker would exceed the CPU budget. Claude Code's recommendation: offload aggregation to a Cron Trigger and write results to KV. Making that call on Day 8 prevented a painful refactor later.
Pitfall 2: Stripe Webhook idempotency
A naive "charge complete → set KV flag" design breaks under Webhook retries. Claude Code flagged this before I'd even thought about it. Building idempotency keys into the design from the start avoided a class of bugs I've been burned by before.
4. Implementation — Model Switching and Token Discipline (Day 15–60)
This is the phase with the most direct impact on cost. Here's the decision framework I settled on.
Model assignment by task type
| Task | Model | Reason |
|---|---|---|
| Architecture / design decisions | claude-opus-4-6 | Requires complex reasoning |
| Feature implementation | claude-sonnet-4-6 | Best cost-to-quality ratio |
| Simple refactors | claude-haiku-4-5 | Fast, cheap |
| Bug fixes with known root cause | claude-haiku-4-5 | Direction already set |
The IDs above — claude-opus-4-6, claude-sonnet-4-6, claude-haiku-4-5 — are what I actually ran during those 90 days. They're still valid IDs, though Opus 5 and Sonnet 5 have since shipped. The generation moved; the shape of the routing didn't. Reasoning to the top tier, drafting to the middle, mechanical edits to the bottom. Substitute the current names and the table still holds.
In practice, I used Opus only for the morning design session. Everything else went to Sonnet and Haiku. My invoice dropped noticeably starting that month. I originally wrote "roughly 40%" here, and I've cut that number: I changed two things at once — model routing and shorter sessions — and never isolated them. One of the two may have done nothing.
Session discipline
The most common Claude Code mistake I see — and made myself — is letting sessions run too long. When context bloats:
- The model starts proposing implementations that contradict earlier decisions
- Correction loops multiply, burning tokens
- It loses the thread of what changed and why
My fix was CLAUDE.md as a state ledger. At the end of each session, I wrote down what was decided. At the start of the next session, I loaded it in first.
<!-- CLAUDE.md state section example -->
## Current Implementation State (2026-03-15)
### Completed
- App Store Connect API wrapper (src/lib/asc.ts)
- Cloudflare KV caching strategy (5-minute TTL)
- Stripe Webhook idempotency key implementation
### Next session tasks
- Build dashboard UI components
- Add recharts for data visualization
- [ ] WARNING: recharts does not work in Cloudflare Workers (SSR is disabled)The warning notes are the most important part. Claude Code doesn't remember the last session, but the document does.
5. First Paying Customer (Day 30)
The first paid feature shipped on schedule. One design decision is worth sharing.
Reducing friction from free to paid
Claude Code's initial proposal for the onboarding flow asked for payment info immediately after signup. I pushed back and rewired it:
Sign up → 7-day full access trial → Day 8: paid features gated → Stripe Checkout
My reasoning was personal: services that ask for a card before I've seen any value lose me immediately. Let users experience the product first.
I'd like to report what happened to conversion. I can't, honestly. At Day 30 the denominator was under a hundred sessions, and "12% to 27%" at that sample size is a wish wearing a percentage sign. It was in the original draft; I've removed it. If you want to measure this properly, wait for a few hundred sessions per arm before you believe the delta.
The Stripe implementation was a one-line config change. The harder part was designing the email sequence — when to remind users the trial is ending, how to frame the ask — and that's where my back-and-forth with Claude Code was most iterative.
6. Production on Cloudflare (Day 60–90)
Three errors in production required Claude Code's help to diagnose. All were Cloudflare Workers-specific.
Error 1: Cannot perform I/O on behalf of a different request
claude "Analyze this stack trace from a Cloudflare Workers environment.
[paste stack trace]
I believe this is triggered when a KV operation is called from
a context that has already resolved. What's the fix?"The answer came back instantly, and I recorded it as: root cause was a KV write without waitUntil(). Fix: five lines.
Correction 2 (2026-08-31)
That's the wrong root cause. This error fires when an I/O object created in one request's context — a response body, a stream, an in-flight binding operation — is touched from a different request's handler. The usual way to cause it is stashing request-scoped state in a module-level variable. Isolates are reused across requests, so the next request reaches for the previous request's belongings and the runtime stops it. The fix is to keep per-request values out of global scope entirely and pass them through arguments or env bindings (Workers Best Practices).
waitUntil() solves a different problem: continuing work after the response has been sent. It extends execution for up to 30 seconds past the response or the client disconnect. My code had both defects at once, and adding waitUntil() made the symptom disappear — which I mistook for having found the cause. Those are not the same thing, and the difference cost me a rewrite months later.
Error 2: deploys started failing on Worker bundle size
This one came from the content-serving Worker I was maintaining alongside the dashboard, not from the dashboard itself. Content lived in one large JSON file bundled into the Worker, and past a certain record count the deploy stopped going through.
Claude Code proposed separating metadata from HTML content, writing the content out as individual static asset files and fetching them at runtime through the ASSETS binding. Having the reasoning explained alongside the generated code made a structural change feel manageable.
Correction 3 (2026-08-31)
I titled this "62 MiB Worker bundle limit" and called it a "traversal limit." Neither is right. There is no 62 MiB limit and no such thing as a traversal limit. The real numbers:
| Worker size | Workers Free | Workers Paid |
|---|---|---|
| After gzip compression | 3 MB | 10 MB |
| Before compression | 64 MB | 64 MB |
The 62 I was carrying around was a half-remembered 64 — the uncompressed ceiling — while the number that actually stops your deploy is the compressed one. You can check yours without deploying:
wrangler deploy --outdir bundled/ --dry-run
# Total Upload: 259.61 KiB / gzip: 47.23 KiBWhen that gzip: figure starts approaching 3 MB (or 10 MB on Paid), it's time to move config files, static data, and binary blobs out of the bundle and into KV, R2, or static assets.
90-day cost and revenue snapshot
| Item | Amount |
|---|---|
| Claude Code token cost (90 days) | ~$300 |
| Cloudflare Workers (free plan) | $0 |
| Stripe fees | Variable, scales with revenue |
| MRR at Day 90 | Not disclosed |
Token cost was heaviest in month one — about $130 — before I tightened the model-switching rules and CLAUDE.md discipline. Months two and three averaged around $85 each. Those are invoice figures, not estimates. What I can't tell you is which change did the work, because I made both in the same week. If you want the answer, introduce one at a time and read the invoice a month later. I didn't, and now I can only report the total.
I keep MRR private not to be coy, but because a number from a project this size sets an expectation that doesn't transfer. Different product, different audience, different order of magnitude.
7. What the 90 Days Actually Taught Me
What worked
Spending real time on design. The two-week architecture phase with Claude Code meant the implementation phase had almost no major course corrections. I never had to tear out a foundational decision.
Documenting failures in CLAUDE.md. Writing down what went wrong between sessions meant I didn't repeat mistakes. Claude Code doesn't have memory across sessions — but the project document does.
What I'd do differently
Less perfectionism on UI. The first paying customers cared about whether the tool solved their problem, not whether the dashboard looked polished. I burned probably ten days on UI detail that didn't move conversion.
Shorter sessions. I once ran an eight-hour session that ended with Claude Code generating code that contradicted designs it had made four hours earlier. I rolled everything back. Now I break at two hours and update CLAUDE.md before stopping.
Ninety days is enough time to find out whether you can build a SaaS — and whether you can get someone to pay for it. Claude Code compressed the implementation timeline meaningfully. But "what to build" and "when to cut your losses" remain human decisions.
Ninety days was enough to answer "can I build this at all." Getting to revenue wasn't Claude Code alone — years of shipping small products taught me when to stop polishing. I originally claimed it made me three to five times faster. I've cut that too. I never measured it, and a multiplier I can't reproduce isn't worth the credibility it borrows.
Rewriting this piece taught me something the original 90 days didn't: the answers that arrive instantly are the ones most worth checking against a primary source. All three corrections above trace back not to Claude Code being wrong, but to me compressing its answer into a shorter, more confident sentence than the evidence supported. A symptom disappearing is not the same as a cause being understood.
If you want to try this yourself, start with a 30-day challenge: get one person to pay you for something before the month ends. That constraint sorts your priorities for you. And when you write it up, put the doc URL next to every technical assertion while you still remember why you believed it. Your future self will thank you. Mine spent half a day reconstructing three of them.