Domi, three months in: by the numbers
Two months ago I posted one month in; a month after that, two months in. Here’s the three-month mark, same method: every number comes from git, the GitHub API, the filesystem, and ccusage, with the commands at the bottom.
Month one built a shippable system. Month two made it safe to let other people in. Month three actually let them in — and grew a second codebase I didn’t have in June.
Two repos now
Everything through June was one repository. On July 28 a mobile app repo appeared, so from here on the numbers come in three flavours: domi (the web app, backend, MCP server, help site, specs), domi-mobile (the Expo app), and combined. I’ll show all three rather than quietly picking whichever is flattering.
The headline
Ninety-two calendar days. Sprints S0 through S41, with S42 in flight. Across both repos: 1,434 commits and 1,035 pull requests merged.
One correction to carry forward. The earlier posts counted PRs from git — merge commits in May, (#N) subjects in the squash era — because that’s what those eras allowed. The GitHub API is authoritative and disagrees: May was 312, not 299; June was 357, not 353. Monthly commit counts shift by a handful too, from where the timezone boundary cuts the month. The numbers here supersede the older ones, and I’d rather say so than quietly publish a different figure.
| Month | domi | domi-mobile | Combined | Per calendar day |
|---|---|---|---|---|
| May | 312 | — | 312 | 10.1 |
| June | 357 | — | 357 | 11.9 |
| July | 328 | 38 | 366 | 11.8 |
Three months sitting between ten and twelve merged PRs a day. What’s easy to miss in that flat line: July delivered its 366 on 22 working days out of 31 — the first month with real gaps in it. Per working day, throughput went from 10.8 in May to 16.6 in July.
The shared PR/issue numbering pool in domi climbed from #1048 to #1431 — 383 new numbers in a month.
Week by week
The monthly view flatters the cadence — a steady eleven-a-day average. The weekly view shows what it actually looked like.
Fourteen weeks, and after the first one the range is 50 to 147. That’s a sawtooth, not a flat line: sprint-shaped, with a heavy week followed by a lighter one where the previous week’s work got reviewed, fixed, and documented. The three peaks — 107 in late May, 114 in the first week of June, 147 in the last week of July — line up with a sprint close, an encryption push, and the beta gate respectively.
The last bar deserves its asterisk. 147 merged PRs in five days, 38 of them from a mobile repo that hadn’t existed the week before. That is not a sustainable cadence and I wouldn’t present it as one; it’s what a self-imposed deadline looks like when the gate is finally in reach.
The quietest stretch is mid-July — 50 and 52 in consecutive weeks — which is exactly when the ontology manifest and the action registry landed. Weeks where the PR count drops are often the weeks where the thinking happened.
The code
The authored surface — code plus docs, excluding generated JSON like lockfiles and i18n bundles — is now about 288,000 lines across both repos. July added 83,727 lines and removed 5,252 in domi, and brought 8,967 more with the mobile repo.
| Language | domi (files / LoC) | mobile (files / LoC) | Combined LoC |
|---|---|---|---|
| TypeScript | 945 / 166,135 | 33 / 7,439 | 173,574 |
| React TSX | 234 / 50,016 | 8 / 851 | 50,867 |
| Markdown | 182 / 36,546 | 11 / 528 | 37,074 |
| MDX (help site) | 270 / 11,617 | — | 11,617 |
| SQL migrations | 154 / 5,402 | — | 5,402 |
| YAML + OpenAPI | 22 / 9,283 | 3 / 149 | 9,432 |
The code subset — TypeScript, TSX, and SQL — is 229,843 lines across 1,374 files combined (221,553 / 1,333 in domi alone, up from 155,402 / 953 at the end of June). The authored surface grew 39% in July against 83% in June: growth slowed by half, which is what you want in a month that spent itself on hardening rather than new surface.
Tests and gates
2,317 test cases across 272 files combined — 2,303 in domi (up from 1,519) and 14 in the three-day-old mobile repo. Tests grew 52% against the code’s 39%, continuing the pattern where the test count outruns the thing it tests.
154 Drizzle migrations (was 111). Fifteen GitHub Actions workflows combined — 13 in domi (was 10), 2 in mobile — running lint and typecheck, a real-database integration suite on Neon ephemeral branches, a schema-drift gate, a real-LLM eval gate, OpenAPI/MCP-catalog sync, English/French key parity, an ontology-manifest drift check new in July, and a closes-an-issue check.
Two of July’s new gates exist because a convention failed silently first: a hand-maintained model-role validator lapsed for the second time, and a sprint got declared closed without its report. Both are enforced by CI now instead of by my memory. That’s the honest version of “process improvement” — you find out a discipline isn’t real when it quietly stops happening.
The bilingual surface
English and French from day one, still CI-hard-failed on drift: 2,396 translation keys in each locale, exactly matched (up from 1,651), and 268 help pages split evenly across the two languages — 134 each (up from 144 total). July nearly doubled the help site, mostly because walking through the entire app surfaced every page where the docs and the product had drifted apart.
What July actually shipped
- The beta gate cleared. Every blocking item closed except two directory submissions. The one that matters: a second human onboarded and validated in production — the first person who isn’t me holding a real account with real data. Alongside it, the production database role was swapped to one that cannot bypass row-level security, so tenant isolation is enforced in prod rather than merely intended.
- Domi became kinetic. July shipped an explicit ontology manifest (CI-checked against the schema in four directions) and, on top of it, an action registry and MCP write — Claude or ChatGPT can now change things in your household, not just read them, through one dispatcher enforcing roles, scopes, audit, and confirmation at a single choke point. Every write is confirmed before it commits.
- Encryption went live. June built encryption at rest; on July 2 I switched it on for my own production household. It stopped being a capability and became a fact.
- A mobile app, from empty repo to running on a phone in three days — then a capture-and-upload client, a sync protocol, and chat, all against the same backend.
- The assistant learned to answer “how do I…?” — retrieval over the help site in both languages, plus in-app support and ticketing.
- Purchases and warranties. Upload a receipt, get an asset with a warranty obligation and a reminder that fires before it lapses.
- A security and privacy program, not a document. Threat model v0.3, an incident-response runbook, a multi-jurisdiction privacy reference, a transfer assessment, a rewritten privacy policy in both languages, HSTS preload, and supply-chain hardening on the package manager.
- Connectors got real. Forward-to-Domi went live, and Gmail real-time push was finally verified end to end — after excavating three layers of failure, each of which had been failing silently for months.
One outage worth naming: a prompt-caching change took production chat down completely. Types passed, build passed, the SDK rejected the message shape at runtime. The rule that came out of it — any change to the shape of a model call gets one live call before merge — is a lesson you only buy once.
What it cost to build
This is the cost of the coding agent working alongside me, measured with ccusage against local session logs — not the product’s own runtime LLM usage.
July cost $5,724.90 across 22 active days: 14.07M billed input-plus-output tokens (4.31M in, 9.76M out), plus 4.64 billion cache-read tokens. That’s about $260 per working day. The heaviest single day was $704.63 on July 29, mid-beta-push.
| Month | Cost | Billed in+out | Active days |
|---|---|---|---|
| May | $4,418.00 | ~13.0M | 29 |
| June | $4,935.75 | ~20.9M | 30 |
| July | $5,724.90 | 14.07M | 22 |
July’s bill rose 16% while its billed token count fell 33%. My first instinct was that cache reads explained it — July read 4.64B cached tokens. That instinct was wrong, and the per-model data says why.
| Model (July) | Cost | Share | $ per 1M all-in tokens | Cache-read share |
|---|---|---|---|---|
| Claude Fable 5 | $4,899.28 | 85.6% | $1.383 | 97.9% |
| Claude Opus 4.8 | $780.47 | 13.6% | $0.690 | 98.2% |
| Claude Opus 5 | $33.47 | 0.6% | $0.95 | — |
| Claude Haiku 4.5 | $2.85 | 0.0% | $0.16 | — |
Fable 5 costs 2.01× Opus 4.8 per token, at a nearly identical usage shape — 97.9% versus 98.2% cache reads, so this is a fair comparison rather than two different kinds of work. And Opus 4.8’s own rate barely moved between June ($0.68) and July ($0.69), which makes it a clean control: the unit price of the old model held steady, the mix changed, and the mix is where the money went.
The counterfactual is stark. Had July’s Fable volume been billed at Opus 4.8’s rate, it would have cost $2,442.73 instead of $4,899.28 — putting the month at $3,268 total, below June, despite the beta push and a mobile app. The model swap added roughly $2,457.
Cache reads are still the volume story: 98% of every token is a re-read of context. But the cost story in July is a model swap, not a workload change. Worth knowing which one you’re looking at before optimising the wrong thing.
Which repo spent it
Both repos are built on the same machine, so July’s spend covers both. Splitting it took more care than expected. ccusage buckets a whole session under one project directory, and the session opened on July 28 — the day the mobile repo was created — ranges across domi, domi-mobile, and this website. Bucketed by session, mobile appears to have cost nothing, which is plainly false.
Every line of the session log carries its own working directory, so the honest split is done at line level: sum tokens per repo, then allocate each model’s known cost in proportion to its token volume. That reconciles to $5,713.22 against ccusage’s $5,724.90 — a 0.2% gap from deduplication edges.
| Repo (July) | All-in tokens | Cost | Share |
|---|---|---|---|
domi | 3.18B | $5,144.16 | 90.0% |
domi-mobile | 258M | $536.48 | 9.4% |
| website + other | 17M | $32.58 | 0.6% |
Mobile delivered 38 PRs for $536 — $14.12 per PR, slightly cheaper than the web repo’s $15.68, which is what a greenfield Expo app with no legacy to reason about should look like.
Cost per merged PR across everything: $14.16 in May, $13.83 in June, $15.64 in July — a 13% rise, entirely consistent with a model that costs twice as much per token doing the same shape of work.
Three months in, the build has cost $15,078.65 — roughly $164 a day across 92 days, for every repo on the machine.
One caveat worth stating plainly: ccusage reads local session logs that are pruned after about 40 days. May and June 1–19 were already gone by the time I started snapshotting on July 29, so those months are the figures I published at the time, and these posts are now their only record. July is captured. There’s a committed monthly snapshot and a runbook now, learned the expensive way: measure it while you still can.
Three months at a glance
Flow — what each month produced:
| Per month | May | June | July |
|---|---|---|---|
| PRs merged | 312 | 357 | 366 (328 + 38 mobile) |
| Commits | 696 | 348 | 390 (325 + 65 mobile) |
| Active working days | 29 | 30 | 22 |
| Build cost | $4,418.00 | $4,935.75 | $5,724.90 |
| Cost per merged PR | $14.16 | $13.83 | $15.64 |
Stock — where things stood at each month’s end:
| At month end | May | June | July (domi) | July (combined) |
|---|---|---|---|---|
| Authored LOC | ~109,700 | ~200,500 | ~279,000 | ~288,000 |
| Code (TS+TSX+SQL) | 81,510 | 155,402 | 221,553 | 229,843 |
| Test cases | 889 | 1,519 | 2,303 | 2,317 |
| Test files | 89 | 184 | 266 | 272 |
| Migrations | 48 | 111 | 154 | 154 |
| i18n keys (each locale) | 790 | 1,651 | 2,396 | 2,396 |
| Help pages | 74 | 144 | 268 | 268 |
| CI workflows | 9 | 10 | 13 | 15 |
| Cumulative PRs | 312 | 669 | 997 | 1,035 |
The trend across three months: throughput flat per calendar day but sharply up per working day, code growth decelerating as work moved from construction to hardening, tests outrunning code every single month, and cost per unit of work rising for a reason that has nothing to do with productivity — a more expensive model doing the same shape of work.
What it adds up to
Month one built the system. Month two made it safe to open. Month three opened it — a second person with a real account, encryption switched on over my own family’s data, assistants able to act rather than only look, and an app on a phone.
What I keep relearning is that the guardrails are what make the pace survivable. Fifteen CI gates, 2,317 tests, and a schema-drift check aren’t overhead on a small team; they’re the only reason this much can land in a month with nine idle days in it and still be trustworthy on the tenth.
Ninety-two evenings. $15,079 and counting. One household that has stopped being a demo.
Same commands next milestone. That’s the whole point of measuring instead of guessing.
Reproducible: the GitHub search API for PR counts (strategy-independent — the git-based counts in earlier posts are superseded), git log --oneline | wc -l for commits, null-delimited git ls-files -z … | xargs -0 cat | wc -l for lines of code (the spec filenames contain spaces), and ccusage daily --json for build cost, split per repo by reading the cwd on each session-log line (session-level bucketing misattributes a session that spans repos). Full methodology and the committed monthly cost snapshots live in the repo’s docs/metrics/.