Skip to main content
Back to blog

I Rebuilt the Counter I Threw Away

9 min read

I've published three counts of my own token usage. Every one of them came from a script I wrote in an afternoon and then deleted. I said so at the time, in the last one, in the closing paragraph, like it was charming: "by a script I wrote in an afternoon."

It isn't charming, and this post is the bill coming due.

So this time I wrote it properly and kept it: scripts/count_tokens.py. It walks ~/.claude/projects/**/*.jsonl, takes the assistant lines, sums the four usage buckets, groups by model and by project. The difference from its three predecessors is that it still exists tomorrow.

The count

3,503 transcript files. 3,350 of them carry usage. 121,810 assistant turns. 24.69 billion tokens.

Split: 207.5M uncached input, 118.4M output, 1.28B cache writes, and 23.08B cache reads.

One caveat the script prints on every run, because it's a real limit and not a footnote: 295 of those turns are counted twice. Resumed sessions re-read their parent's transcript, so a turn can appear in more than one file. There's a --dedupe flag that drops them. The default counts them, to match how the earlier posts counted, and the number is reported rather than quietly absorbed. It's a rounding error at this scale — 0.2% — but an instrument that hides its known error is the thing I'm trying to stop building.

The three patterns I'd found before all reproduced on the new instrument, which is the only reason re-measuring is interesting:

Pattern Last count This count
Bash is half my tool calls 52.8% 50.3% of 62,604
Cache reads are ~90% of everything 90.5% 93.5%
One project is half the work homelab ≈ half homelab 13.87B of 24.69B

Three findings, two instruments, both agreeing. Those are the ones I'll keep building on. RailMind is the surprise in second place at 7.64B — an iPad track planner is now nearly a third of my entire AI footprint, which tells you what I've actually been doing this summer regardless of what I thought I was doing.

Then I checked it against my own homework

The obvious test for a new instrument is to point it at a window you've already published and see if it agrees. My last post covered April 8 to July 7 and reported 91,908 turns and 15.58 billion tokens, from 2,608 files.

I ran the new counter over exactly that window. It said 78,050 turns. 13.82 billion. From 2,320 files.

Fifteen percent fewer turns. Eleven percent fewer tokens. Roughly 288 files short. That's not rounding.

My first assumption was a bug in my own parser — that I was skipping assistant lines the old script had counted. That one I could check, so I did: of every assistant line on disk inside that window, 78,050 of 78,050 carry a usage block. Zero without. There is nothing there for my parser to have missed.

So one of two things is true. Either there is less on disk than there was on July 7th, or my new script counts differently than the old one did.

And here is the punchline of this entire post: I cannot tell which.

I can't, because answering that question requires running the old script against today's files, and the old script does not exist. I deleted it. The one artifact that could distinguish "the world changed" from "my ruler changed" was the thing I threw away, and I threw it away three times in a row while writing three posts about measurement.

That's not a footnote. That's the whole lesson, and I earned it the expensive way. A number that moves is only information if the instrument held still. Mine didn't, so the movement means nothing. I have 15% and no idea what it's the size of.

The measurement I got wrong on the way

I nearly published something much worse than uncertainty.

Chasing the shortfall, I broke the corpus down by month — top-level session transcripts against the nested subagent files that parallel work leaves behind — and got a table with a hole in it. April: zero top-level sessions. May: zero. I had a whole section written about it. Every session from those months, gone; only orphaned subagent files left behind, limbs with no torso.

It was an artifact of my own measurement. I'd bucketed the files by mtime — when the file was last written — and called it "when that work happened." Those are not the same question. A session started in April and resumed in July carries a July mtime, and several of mine were bulk-touched on the first and ninth of July by something I haven't identified. Re-cut on the first content timestamp inside each file — when the conversation actually began — and the hole closes:

Month Top-level, by mtime Top-level, by content
2026-04 0 5
2026-05 0 8
2026-06 94 123
2026-07 296 254

Thirteen April and May sessions are sitting on my disk right now. I was about to tell you they'd been deleted.

I want to sit in that for a second, because it's the same bug as everything else I've fixed this week. My briefing reported a hardcoded zero. My health check reports healthy because npx is on PATH. And I, writing the post about those, produced a zero that was an artifact of the field I chose to sort by — and it was such a good zero. It was dramatic. It had a table. I'd have defended it.

What saved it wasn't my judgment. It was handing the draft to something whose only instruction was to refute me, which went and counted the files a different way. The same move that makes the pipeline trustworthy is the one that makes the writing trustworthy, and I don't get to skip it just because I'm the one holding the pen.

The record is thin in April — 5 sessions, 14 subagent files, for a month I know I worked in. Something is missing. But "thin" is what I can prove, and "gone" was a story I told because my sort key told it to me.

The part that actually stung

There's exactly one committed piece of code in my stack that reads these transcripts: thread_indexer_worker.py. Its docstring says it globs **/*.jsonl. Line 179 actually calls project_dir.glob("*.jsonl") — no recursion.

So it sees the top-level session files and nothing else. 390 of 3,503. Eleven percent. It has never once read a subagent transcript, which is 89% of everything I've generated, and every fan-out I've ever run.

Then I went looking for what it does with the 390, and it's worse than the glob. The worker is gated behind a CORTEX_WRITE env var, and that variable is set nowhere — not in the LaunchAgent plist, not in my env file, nowhere. Its own live report says would_write_to_cortex: false. It runs every hour, on the hour, reads eleven percent of my transcripts, and writes a JSON file to /tmp that nothing reads and I had forgotten existed.

I've been calling this thing my session indexer all week. It indexes nothing. It's an hourly no-op with a docstring describing a system I never finished, and I'd have kept citing it as infrastructure indefinitely, because it's green, and it's scheduled, and it never errors.

Fixed the glob tonight — one word, rglob, plus the tests the worker never had, which is precisely how a docstring and its implementation disagreed for the worker's entire life without anything noticing. The CORTEX_WRITE question is a different decision — whether I want that pipeline at all — and I'm not going to make it at midnight by flipping a variable.

What the instrument is for

I'm being careful with the dollar figures this time. The counter prints cost, and the arithmetic checks out, but the rates come from a table I maintain by hand — I typed them in from what my own earlier posts asserted. So it's an estimate resting on my memory of a pricing page, and I marked it that way in the code, with a comment telling the next reader to re-check it before quoting a number. The token counts are ground truth from the files. The dollars are a model. Two different epistemic animals, and the script now says which is which.

One thing it does refuse to do: price a model it doesn't know. If an unfamiliar model shows up, the function returns None rather than 0.0, the row prints unpriced, and it's excluded from the total with a loud warning. There's a model in my corpus right now — <synthetic>, 311 turns — that trips exactly this path. A zero would have slid into the total and looked like a fact.

I spent three posts measuring my tokens with an instrument I threw away each time. Each of those posts felt rigorous. They had tables. The numbers were real, in the sense that a script really did produce them from real files.

But I destroyed the ruler after every measurement, so the series they form is decorative. When this count came back 15% under the last one, I had exactly nothing to bring to the question of why — no way to distinguish a corpus that shrank from a script that counts differently, because the only object that could have answered it was gone. Three careful measurements, and I can't compare any two of them.

So the counter is committed now, and that's the actual deliverable — not the 24.69 billion. The 24.69 billion is worth something only from the next count onward, when there'll finally be a fixed ruler behind it and a moving number will mean the world moved.

Keep the instrument. Not because it's precious, but because a number without a stable instrument behind it isn't a measurement. It's an anecdote with a decimal point.