NovelFlame

A multi-agent creative pipeline that turns a one-line prompt into an illustrated, multi-chapter interactive story: an art director agent, per-genre editors, character-consistency tracking, and cinematic video at climactic beats.

NovelFlame product screenshot

Why this stack

SvelteKit's server-first routing puts every auth and content-access gate at the server layer instead of scattered client checks, which mattered once a security audit found the auth guard excluded all /api routes from protection entirely. Supabase paired with Drizzle keeps the schema in version-controlled TypeScript instead of a GUI-managed database, important when a solo operator is the only reviewer of every migration. Cloudflare R2 skips egress fees for a product that generates images and video on every story. OpenTofu over Terraform was a license call, not a technical one: Terraform's 2023 move to the Business Source License made a state file holding database passwords and API keys a bigger risk than a drop-in open-source replacement with native state encryption. Railway's Docker-based deploy meant the Docker build itself, not a platform-specific buildpack, was the thing worth optimizing, which is why the multi-stage Dockerfile and GitHub Actions layer caching became their own spec.

AI-assist note

Built spec-driven with Claude Code across 189 specs (spec, plan, tasks per feature, with a CodeRabbit review pass on higher-risk PRs). I wrote every spec, reviewed every diff, and made the calls that don't show up in a diff: routing narrative and image-prompt authoring to different providers for quality, OpenTofu over Terraform, and how to hold character appearance consistent across a multi-chapter story. Claude Code wrote most of the implementation and tests.

Stack

  • SvelteKit 2 / Svelte 5
  • TypeScript
  • Supabase (Postgres + Auth)
  • Drizzle ORM
  • Cloudflare R2 + Turnstile
  • Vercel AI SDK (xAI + OpenAI in production; Claude + Gemini as eval judges)
  • Stripe + Apple IAP
  • OpenTofu (IaC)
  • Docker + GitHub Actions
  • PostHog (product analytics)
  • Loops.so (lifecycle + newsletter email)
  • Railway

Domains

  • Full-Stack SaaS Engineering
  • Infrastructure as Code
  • Payments & Compliance
  • AI Content Safety
Live4 mo
Users200+ users, 450+ story sessions in soft launch (Supabase, Jul 2026); still early on paid conversion
PaymentsStripe subscriptions (web); Apple IAP built + tested, dormant (never shipped)
InfraCloudflare (DNS/CDN/R2/Turnstile), OpenTofu IaC (~30 resources), Docker multi-stage (non-root), Railway, Axiom + Discord alerting
AuthSupabase Auth via SSR cookies
MarketingPaid Facebook ads via a marketing partner (Jul 2026)
Specs189

Why this exists

NovelFlame is an interactive fiction platform: readers pick a genre and premise, and the app generates a branching story with inline images and a cinematic video, choice by choice. I run it solo, end to end. The hard engineering here is the AI itself, not a wrapper around someone else’s model: choosing and routing models by measured story quality, keeping a cast visually consistent across an illustrated multi-chapter arc, making the branching choices feel meaningful instead of formulaic, and layering editorial and art-direction agents so it doesn’t read or look like generic AI. The infrastructure, release pipeline, and safety systems around it are what let a team of one keep all of that running in production.

Billing was a smaller side of the work than the AI. It shipped on a pay-per-story token model billed through CCBill, which I implemented straight from their docs. Their sales team never came back to onboard me, CCBill’s checkout is higher-friction for users than Stripe, and competitor reviews named per-generation currency counting as the category’s top complaint, so within a few weeks I switched to Stripe and a flat unlimited subscription.

Architecture

Five views, because the request path, the generation pipeline, the money, the release pipeline, and the operations are genuinely different machines; flattening them into one diagram buries the parts a production app is actually judged on.

The request path: five gates before a model

Request path and gatesA reader’s request passes the Cloudflare edge, Supabase SSR auth, per-surface rate limits, and the subscription gate, then an input safety gate of CSAM blocklist, OpenAI moderation, and prompt-injection filtering, before reaching the story generation pipeline. The response streams back to the reader over SSE.Readerbrowser (SvelteKit SSR) · iOS appCloudflare edgeDNS/CDN · Turnstile bot check · CSP headers(all codified in OpenTofu)Supabase Auth (SSR)cookie session (web) ·Bearer JWT (mobile)Rate limitsUpstash sliding window;per-surface capsSubscription gatefree: 3 stories per 30 days →402 paywall · Plus: unlimitedInput safety gateCSAM blocklist → OpenAI Moderation API→ prompt-injection pattern filterStory generation pipelinemulti-agent (next diagram) · media to Cloudflare R2SSE stream back · 15s heartbeat, segment by segment

Nothing reaches a model until a request clears five gates in order: the Cloudflare edge (DNS, CDN, Turnstile on auth forms, CSP headers, all managed as OpenTofu code instead of dashboard clicks), Supabase SSR auth, per-surface rate limits on Upstash Redis (story generation, signup, purchases, and feedback each have their own cap), the subscription gate (free tier is 3 completed stories per 30 days, answered with a 402 paywall; Plus is unlimited), and the input safety gate: a CSAM blocklist, the OpenAI Moderation API, and a prompt-injection filter covering the places user-typed text reaches an LLM. The response streams back over SSE with a 15-second heartbeat and hard timeouts, so a stuck generation fails visibly instead of hanging the reader.

The generation pipeline: a relay of specialists

Story generation pipelineEach story beat runs eight stages: narrative on gpt-5.5, a genre editorial agent, a deterministic beat classifier, an art director authoring the image prompt on Grok, character identity locks with reference portraits, illustration, a generated video at the climax, and a trigger-warning classifier. Model-powered stages are marked with the model named in each.1 · Story beatOpenAI gpt-5.5,structured output2 · Editorial agentgenre style guides; stripsAI-tell prose, 2nd pass3 · Beat classifierdeterministic: beat type,focal cast, shot recipe4 · Art directorimage prompt on xAI Grok,kept deliberately literal5 · Identity locksappearance block + referenceportraits · vision QA loop6 · Illustrationgrok-imagine baseline ·gpt-image-2 for the cast7 · Climax videogrok-imagine-video,auto-plays inline8 · Trigger warningsclassifier tags sensitivecategories for opt-outsmodel call (model named in box)deterministic step

Each beat is a relay of specialists, and the model is named per stage because the routing is the point. gpt-5.5 writes the narrative (it won the blind persona eval); the editorial agent reruns the prose against genre style guides and a banned-patterns list so it doesn’t read like AI; a deterministic classifier picks the shot; the art director authors the image prompt on Grok, which doesn’t soften the story’s own action the way the narrative model did; identity locks and reference portraits keep the cast recognizable across chapters, with a vision-QA loop grading the results; and the climax beat gets a generated video in the same art style. A trigger-warning classifier then tags sensitive categories so readers can opt out before they hit them. Anthropic’s Claude sits outside this live path on purpose: it serves as a cross-family judge in the eval harness that gates model swaps, so no model grades its own family. Utility calls (summaries, choice fallbacks, prompt extraction) run on a cheaper tier, which is what moved cost per story down 30%.

Payments: one live door, one dormant lane

Payments and entitlementsStripe Checkout on web feeds a server-verified webhook into one subscriptions and entitlements store in Postgres, which the subscription gate reads at story creation. A parallel Apple in-app-purchase lane is fully built and tested but dormant; the iOS app never shipped.Stripe (web)hosted Checkout + customer portalApple IAP (iOS): dormantfull App Store setup, app never shippedStripe webhooksignature-verified: activation,renewals, cancellationsApp Store notifications V2signed transactions, server-sidevalidation, built + testedsubscriptions + user_entitlementsPostgres via Drizzle · Plus status, video allowance, creditsSubscription gate, enforced at story createfree tier: 3 stories per 30 days → 402 paywall · Plus unlimitedPostHog + Loops.so events on every transition

Payments deliberately live outside the story path. Stripe’s hosted Checkout is the live door: a signature-verified webhook writes activations, renewals, and cancellations into one entitlements store in Postgres, and the only place billing touches the reading experience is the subscription gate at story creation, the same gate in the request-path diagram above. Every transition fires PostHog analytics and Loops.so lifecycle email events, so a cancellation or a paywall hit is a measurable funnel step, not a mystery. The Apple lane is real code, deliberately dormant: I took it all the way to the edge of launch (StoreKit products, server-side receipt validation, App Store Server Notifications, the full App Store application with screenshots and pricing tiers, Apple Pay tested on my own phone) and then made the web-first call in the mobile story below instead of shipping. The lane stays in the diagram because it stays in the codebase, waiting for a native app that earns its keep.

Ship to production: governance as code

CI/CD and infrastructure pipelineA pull request passes CodeRabbit review and GitHub Actions CI, gated by a required branch-protection check that is itself managed in OpenTofu. Merging to main applies infrastructure changes with tofu, deploys to Railway in Docker, and runs a staged deploy with Playwright end-to-end tests.Pull requestsolo dev + Claude Code,spec-driven (189 specs)CodeRabbit reviewassertive profile · gitleaks ·actionlint · slop detectionCI · GitHub Actionslint · svelte-check · vitest ·build · tofu plan · eval gateBranch protectionrequired CI Status check,admins included, set in IaCtofu apply on mainCloudflare DNS · R2 · Turnstile · CSP ·Supabase · GitHub repo config(state encrypted in R2)railway up · production3-stage Docker build, non-root,+ production migrationsstaging + e2estaging deploy, seeded DB,Playwright smoke suite

Everything between me and production is codified. A PR gets CodeRabbit’s assertive review (with gitleaks, actionlint, and slop detection wired in) and a CI run that aggregates lint, type checks, tests, the build, an OpenTofu plan, and an eval-prompt regression gate into a single required “CI Status” check, and the branch protection requiring that check is itself OpenTofu-managed, enforced for admins too, so repo governance survives even a full repo recreation. Merging to main applies infrastructure changes (Cloudflare DNS, R2, Turnstile, CSP headers, Supabase, GitHub config, with state encrypted at rest in R2), deploys the 3-stage Docker image to Railway with production migrations, and refreshes staging behind a Playwright smoke suite. If I changed something in a dashboard by hand, the next plan would show it as drift.

Operations: two Discord servers, on purpose

Operations and observabilityMachine signals flow from the app through a Railway log drain into Axiom, where hourly GitHub Actions monitors and a synthetic probe raise alerts into a Discord alerts server and deduplicated GitHub incident issues. Human signals flow from the in-app feedback button through a triage webhook into a separate Discord feedback server.MACHINE SIGNALSHUMAN SIGNALSApp on Railwaystructured JSON logs · named production eventsReader feedback buttonin-app, every pageTelemetryRailway log drain → Axiom · Sentry errorsLangfuse trace per model call · PostHog funnelFeedback triagesecond Discord webhook,posted with user contextWatchers · GitHub Actions cronhourly Axiom monitors: generation failures, silent anchordrops, 5xx, dead-man silence · synthetic probe every 2hDiscord feedback serverseparate server; every reportlands as a message I readDiscord alerts servermonitors, probe + CI failuresland as embedsGitHub incident issuesdeduped, auto-assigned,with Axiom query links

Two Discord servers, split by who’s talking. Machine signals run down the left: the app writes structured JSON logs and named production events (silent identity-anchor drops, SSE timeouts, moderation blocks) through Railway’s log drain into Axiom, and hourly GitHub Actions monitors query Axiom for generation failures, anchor-drop spikes, 5xx bursts, and dead-man staleness; a synthetic probe also creates a real story end to end every two hours. What they find lands as embeds in an alerts server and as deduplicated GitHub incident issues, auto-assigned to me with the Axiom query attached. Human signals run down the right: the in-app feedback button posts through a second webhook into a separate feedback server, so a reader complaint never drowns in machine noise. This telemetry isn’t decoration: the 621 silent identity-lock drops in the character-consistency story below were found by exactly this pipeline. Langfuse traces every model call for cost forensics, PostHog holds the product funnel, Sentry catches errors, and Loops.so runs lifecycle email off the same event stream.

What shipped

189 specs took NovelFlame from a forked prototype to a live, solo-operated product. The work I’m proudest of is the applied-AI engineering: routing narrative to the model that won a blind persona eval while keeping image-prompt authoring on a provider that didn’t soften the story’s own action, holding character appearance consistent across an illustrated multi-chapter story when production telemetry showed it silently breaking, and layering genre editorial agents and an art director on top of raw generation so it doesn’t read or look like generic AI. The infrastructure-as-code, CI/CD, and content-safety work underneath is what lets a solo operator keep all of that running in production.

The throughline across all of it is the same: a solo operator’s infrastructure has to be legible enough that 1 person can trust it, and honest enough to change course when a shipped feature turns out to be the wrong bet.

Skill stories

Each card below opens the engineering story behind one skill: the decision I made, what broke, how I measured it, and how I fixed it. Click any card to read it.

  1. Applied AI & Model SelectionPer-Task Model Routing for Story Quality
  2. Applied AI & EvaluationA Production-Fidelity Model Eval Harness
  3. Applied AI & Visual ConsistencyCharacter Consistency Across Chapters and Images
  4. Applied AI & Multimodal GenerationMultimodal Generation: Illustration and Cinematic Video
  5. Applied AI & Creative QualityMulti-Agent Editorial and Art Direction
  6. Applied AI & Interactive DesignMeaningful, Non-Repetitive Choices
  7. Infrastructure & DevOpsInfrastructure as Code (OpenTofu over Terraform)
  8. AI Safety & TrustAI Safety: Content Filter, Trigger Warnings, and Prompt-Injection Hardening
  9. Production Cost GovernanceA Per-Call Cost Ledger for Unit Economics
  10. Product Scope & Solo-Dev EconomicsWeb-First Mobile: Killing My Own Native App

Applied AI & Model Selection

Per-Task Model Routing for Story Quality

Decision
I route each job to the model that wins for it instead of running one model for everything. A blind persona eval on real stories ranked OpenAI gpt-5.5 unanimously for narrative, a 100% would-pay rate versus the incumbent xAI model's 53% at p<0.001, so I moved production text generation to gpt-5.5 behind a provider-aware factory that reads a provider:model string, which makes a rollback a 1 env-var edit. But I kept image-prompt authoring on xAI Grok, because gpt-5.5 systematically softened the physical action: when the prose said 2 characters kiss, its image prompt said 'foreheads touching' and the illustrator drew the near-miss.
What broke
Running the flagship narrative model on every call was both a quality problem and a cost problem. On quality, that model wrote image prompts that under-depicted the story's own action. On cost, 4 days of production spend was $84.06, about $3.12 per completed story across roughly 5 model calls per beat, which pushed the 'unlimited' Plus subscription break-even to a bad place.
How I measured it
The narrative swap was gated on a head-to-head persona eval before cutover, not a hunch. For the image-prompt split, observability captured 9 real image prompts and confirmed the downstream sanitizer was a passthrough, so the model was the cause, not the filter. For cost, Langfuse traces on 5 recent production stories, before and after.
How I fixed it
Narrative runs on OpenAI gpt-5.5, image-prompt authoring on xAI Grok, and I split the text factory into a flagship main tier and a cheaper utility tier so the roughly 7 non-creative calls (prompt extraction, sanitization, choice fallbacks) stopped burning flagship rates. Cost per completed story dropped over 30% with no regression on the main narrative beat, moving Plus break-even from under 3 stories a month to 5 or 6.

Applied AI & Evaluation

A Production-Fidelity Model Eval Harness

Decision
Choosing a text model by vibes is how you ship one that wins the first paragraph and drifts by paragraph 5. I built a repeatable eval harness that judges candidates the way a paying reader actually experiences the product: a full 8-to-15-segment story, the inline images rendered rather than just their prompts, and the exact production prompt-assembly code, scored by 4 reader personas and 3 cross-family LLM judges on a would-you-keep-paying-$8.99-a-month rubric.
What broke
The v1 eval was directional but not load-bearing. It judged a single beat, never rendered the images the product ships, and used a simplified prompt instead of the real one, so a model that won the eval wasn't guaranteed to win in production. Separately, I was carrying paid services (ElevenLabs narration, mem0 graph memory) on the assumption they earned their subscription cost.
How I measured it
The harness ranked 5 candidates (xAI Grok, OpenAI gpt-5, Anthropic Sonnet, DeepSeek, Gemini) with cross-family judges so no model grades its own family. OpenAI gpt-5 landed a 1.37 mean judge rank against the incumbent's 4.15, and the would-you-keep-paying score was 100% for the top 2 candidates versus 0 to 18% for the model then in production. I ran the same measure-before-you-trust check on the paid add-ons.
How I fixed it
The harness drove the production model swap and now gates model changes instead of a hunch. It also made the cost calls concrete: I cut both ElevenLabs narration and the mem0 subscription after confirming the reading experience held without them, since a lighter running-summary approach covered long-story continuity. Proving whether a paid dependency earns its keep is cheaper than paying for it out of habit.

Applied AI & Visual Consistency

Character Consistency Across Chapters and Images

Decision
Keeping a character looking like themselves across an illustrated multi-chapter story is the product's main switching-cost moat and the #2 complaint about competitor apps. I hold each character with 2 mechanisms: a reference portrait where one is eligible, and a text identity-lock block appended to every image prompt that pins hair, eyes, build, and distinguishing marks.
What broke
Both mechanisms were failing silently. Axiom production telemetry over a 14-day window showed the identity-lock drop firing 621 times across 78 sessions, dropping 1,310 characters, peaking at 285 in a single day. When a 3-plus-character scene blew the identity-lock text budget, lower-priority characters fell back to a minimal anchor carrying only gender, age, and build, dropping exactly the highest-drift attributes (hair color, eye color, features), so those characters rendered as generic strangers. No user or operator ever saw an error.
How I measured it
The failure was invisible until I instrumented it. I tracked the drop and portrait-failure counts as named production events in Axiom, which is how the 621 drops surfaced alongside a small, mostly-correct 11 portrait failures, letting me separate the genuine bug (an over-strict age-band validation that silently denied a legitimate adult character a portrait) from the correct rejections (minor-coded or protected-IP content that should stay blocked).
How I fixed it
I reworked the identity-lock budgeting and priority so multi-character scenes keep the high-drift attributes instead of silently dropping them, and surfaced the previously-silent drops as an operator signal. It composes with the art-director scene enrichment: 1 pass makes the scene dynamic, this one makes the characters in it look like themselves.

Applied AI & Multimodal Generation

Multimodal Generation: Illustration and Cinematic Video

Decision
A story shouldn't just be text. At narrative beats the product renders an illustration, and at climactic moments it auto-plays a short generated video inline, so the reader moves from text to image to motion in the same art style and with the same characters. The video is the main thing that separates it from text-only competitors.
What broke
Early scene videos didn't reflect the story. They rendered generic, art-style-drifting clips that undercut the premium moment they were supposed to be, and the completion recap's captions were unreadable. A cinematic moment that looks nothing like the scene the reader just read destroys the value it was meant to add.
How I measured it
I treated video as a first-class product surface, not a nice-to-have: log-and-eyeball on real climactic beats to confirm the clip matched the narrative content and carried the established art style and character look, plus caption-readability checks on the recap video.
How I fixed it
Scene video is authored from the beat's actual content and the story's established visual identity, so the motion depicts what the narrative describes instead of a generic clip, and the recap reads cleanly. It composes with the character-consistency and art-director work: the same identity locks and scene enrichment that hold across images carry into the video.

Applied AI & Creative Quality

Multi-Agent Editorial and Art Direction

Decision
The biggest complaint about AI fiction is that it reads like AI wrote it, and generated illustrations of action beats tend to render as everyone standing around. I added 2 creative agents on top of raw generation: genre-specific editorial agents that do a post-generation quality pass (tightening prose, fixing pacing, adding sensory detail, killing AI-tell patterns like 'delve' and 'tapestry'), and an art-director pass that re-authors the image prompt to depict the actual action of the beat, not a static portrait.
What broke
Raw generation had 2 quality gaps. The prose read generic and AI-typical, which hurts completion and word-of-mouth. And the image-prompt authoring instruction was intimacy-skewed: its only worked examples were kiss and embrace, so non-romance action beats (a LitRPG fight, traversing a hazard) defaulted to everyone standing around instead of the event the narrative described, which tracked with new users abandoning after 1 or 2 segments.
How I measured it
For images I confirmed the root cause by log-and-eyeball on real beats and ruled out the wrong hypothesis: the cheap utility-tier extraction path fired 0 times in 14 days of production, so the real authoring gate was the flagged one. I shipped the art director behind a fail-closed flag to measure before defaulting it on.
How I fixed it
A genre-aware editorial pass now runs after each segment to elevate the writing without rewriting it from scratch, modeled on an editorial-validation system I had built before for course content. The art director enriches every live image prompt with concrete poses and character-to-object-and-environment relationships, so action beats depict what is actually happening while preserving the existing intimacy depiction and spatial anchors.

Applied AI & Interactive Design

Meaningful, Non-Repetitive Choices

Decision
The 3 choices at every branch point were forced into a hardcoded taxonomy: 1 safe, 1 bold, 1 weird, every beat, every story. My heaviest user flagged that the menu read as a predictable tic. I replaced the fixed buckets with a rotating choice-variety axis system so the 3 options never share an axis and the pattern shifts beat to beat, plus a pacing-aware wildcard that periodically jumps scene or skips time instead of always continuing the same moment.
What broke
The hardcoded 3-bucket taxonomy (safe, bold, weird) repeated across stories and always assumed the reader stayed in the same scene, so the choices felt mechanical and every branch looked the same. It was the root complaint from the person who used the product most.
How I measured it
I validated the new choices against the eval harness's choiceMeaningfulness rubric rather than my own taste, checking that the 3 options each pulled a genuinely different direction and that the menu's shape changed from beat to beat instead of settling back into the old pattern.
How I fixed it
Choices now rotate across a set of decision axes, bias toward moving time or location forward when the pacing calls for it, and periodically break the scene with a wildcard, while keeping the free-text custom-action path so a reader can always write their own move.

Infrastructure & DevOps

Infrastructure as Code (OpenTofu over Terraform)

Decision
Cloudflare DNS, R2 buckets, Turnstile widgets, Supabase project settings, and GitHub branch protection all lived in dashboards, changeable by anyone with access and undocumented anywhere in the repo. I picked OpenTofu over Terraform for the codify-everything pass: same HCL, same providers, but MPL 2.0 instead of Terraform's 2023 move to the Business Source License, plus OpenTofu's native state encryption, which matters because the state file holds database passwords and API keys.
What broke
State lived only on my laptop. A disk failure would have meant losing the only record of how roughly 30 cloud resources were actually configured, and there was no way to detect drift if I, or an AI agent, changed something by hand in a dashboard.
How I measured it
Ran the import command against every existing resource first instead of applying from scratch, then confirmed a plan against the imported state showed 0 changes, proving the codified config matched what was actually running in production.
How I fixed it
State now lives encrypted in a Cloudflare R2 backend. Secrets moved out of plaintext .env/.tfvars into SOPS with age-encrypted keys. Branch protection and CI secrets are codified, so repo governance survives even a full repo recreation.

AI Safety & Trust

AI Safety: Content Filter, Trigger Warnings, and Prompt-Injection Hardening

Decision
The safety surface has 3 fronts and I built for all of them instead of trusting the upstream models. On output, a hybrid filter (OpenAI Moderation API plus custom keyword and pattern rules) gates every generated story, chat message, and image prompt. On reader consent, an editorial classifier auto-tags a closed set of sensitive categories (things like character death, substance abuse, and pregnancy loss) that readers can opt out of and filter on the discover feed. On input, a unified defense hardens the 3 places user-typed text reaches an LLM.
What broke
Each protection had grown independently and unevenly. The upstream providers run their own moderation, but those thresholds change without notice and differ by provider. Trigger warnings weren't firing on stories that clearly needed them: 1 story with incest mentions published to the discover feed with no warning. And the 3 user-text-to-LLM surfaces each had their own ad-hoc mitigation, with no shared threat model and 1 narrative-continuation path that had no injection gate at all.
How I measured it
For the content filter I set explicit target rates against dedicated test sets: 100% of prohibited prompts blocked pre-generation, under 1% false positives on legitimate content, and 95%+ catch on evasions like leetspeak and unicode substitution. For injection I wrote an adversarial regression suite that proves each chokepoint holds, including unicode-lookalike probes against the delimiter-wrapped paths.
How I fixed it
Filter actions are tiered: a hard block for the highest-risk category, a soft warning with a suggested rephrase for lower-risk ones, each logged with a content hash and the matched rule for audit, plus a user report button that files straight to GitHub Issues so a solo operator still has a review queue. The trigger-warning enum drives both the auto-classifier and the per-user opt-in filter, and the injection work added the missing pre-filter on the un-gated path plus a documented threat model so the defense stays load-bearing as the app changes.

Production Cost Governance

A Per-Call Cost Ledger for Unit Economics

Decision
An unlimited-subscription product where every story beat fans out to a dozen model calls across providers can lose money invisibly. So every text, image, and video call writes a ledger row, priced at write time from a per-provider rate table: model, tokens, latency, provider cost to 6 decimal places, tied to the story session and segment position. I've switched models several times over the product's short life, and each swap changes the unit economics; the ledger is what makes those swaps decisions instead of bets.
What broke
Naive cost accounting gets the billing rules wrong in ways that compound. OpenAI's cached input tokens bill at a 10x discount and its reasoning tokens roll into output tokens, so a rate table that ignores either misprices every gpt-5.5 call. And a generation that fails midway still costs real provider money, so a ledger that only logs successes systematically understates what the product spends.
How I measured it
The rate table encodes the real billing rules, cached-input discounts and reasoning-token treatment included; unknown models fall back to safe default rates instead of logging zero; and the failure path logs its cost before the error event goes out to the reader, so a failed generation isn't free in the books when it wasn't free at the provider.
How I fixed it
The flagship-versus-utility tier split, the model swaps, and the subscription break-even math all read off this ledger plus the Langfuse traces: cost per completed story dropped over 30% in the routing work above, and 'unlimited' is priced against actuals instead of estimates. The trade-off between cost and quality never goes away in an AI product; the ledger just makes it visible enough to manage.

Product Scope & Solo-Dev Economics

Web-First Mobile: Killing My Own Native App

Decision
Growth thinking said interactive fiction wants a native iOS app, I'd deliberately picked aligned JavaScript stacks across my products to keep a native path open, and I'd never shipped a native app before, so I built it. The app reached near feature parity with the web product and I took the launch prep to the edge of pulling the trigger: build pipeline, Apple IAP plumbing end to end, the full App Store application with screenshots and pricing tiers, Apple Pay tested on my own phone. Then I deliberately stopped and made the responsive web app the mobile experience.
What broke
Two surfaces more than doubled the solo workload. Native build times are long, every change needed smoke-testing on web and then again on iOS, and the redeploy-and-retest cycle fought directly against the fast spec-driven iteration loop the rest of the product runs on. I'd already walked this exact road on MTG Commander with a much less mature app and pulled back for the same reason.
How I measured it
The push to keep going was growth; the evidence said otherwise. My marketing partner's feedback from real users was that the mobile browser experience was already genuinely good, and my own cycle times said the native surface was consuming close to half my capacity while adding little the web app didn't already deliver.
How I fixed it
The product is web-first on mobile and the app never shipped. Months unmaintained now, it would take real rework to revive, and that's the point: the maintenance I'm not paying is exactly the capacity that goes to user acquisition, marketing, and quality instead. The IAP infrastructure stays built and tested for the day a native app earns its keep. Knowing what not to maintain is the highest-leverage scope decision a solo operator makes.