Counting Tokens Without an API
There's been a lot of hype around AI Usage, tokenmaxxing and the like. You've probably seen a lot of articles similar to these:
I don't think people should be using AI for the sake of using AI. That's a flawed concept. More AI usage doesn't equal a productive worker.
Nonetheless, I was just interested in knowing how many tokens I've been using since I integrated generative AI usage into my daily work as a developer and what that trend would look like. The shape of this was inspired by Duncan's portfolio. However I didn't want to use an API.
There is a page on this site at /token-usage. It shows one bar per day for every day I have used a coding agent since May, stacked by model. Right now it sits at about 4.5 billion tokens across 21,906 assistant messages and 91 active days.

The numbers are already on my laptop
I did not build an API integration for this. Claude Code and Codex both write session transcripts to disk as JSONL, one line per event, under ~/.claude/projects and ~/.codex/sessions. Every assistant message in a Claude transcript carries a usage object with the token counts already in it. So the whole thing is a file walk.
A script reads every .jsonl file it can find, pulls the token counts out, buckets them by UTC day and by model, and writes one summary file to data/token-usage.json. The page imports that JSON at build time. There is no database and nothing to fetch at runtime.
Here is the core of the Claude reader:
if (obj.type !== "assistant") continue;
const message = obj.message;
const usage = message?.usage;
const model = message?.model;
if (!usage || !model || model === "<synthetic>") continue;
// Dedup retries: the same request can be logged more than once.
const dedupKey =
obj.requestId && message.id ? `${obj.requestId}:${message.id}` : obj.uuid;
if (dedupKey) {
if (state.seen.has(dedupKey)) continue;
state.seen.add(dedupKey);
}
const input = usage.input_tokens || 0;
const output = usage.output_tokens || 0;
const cacheRead = usage.cache_read_input_tokens || 0;
const cacheCreation = usage.cache_creation_input_tokens || 0;
The dedup step matters more than it looks. A single request can land in the log twice when a call retries, and without a key on requestId plus the message id you count the same tokens again. My first run was inflated by a few percent for exactly that reason.
The part that was hard
Claude Code does not keep transcripts forever. cleanupPeriodDays defaults to 30, so the logs are a rolling window. If the script just recomputed the totals from whatever is on disk, my chart would quietly eat its own history. Every day I would gain a day at the front and lose one at the back, and after a month I would have a chart that always shows thirty days and claims that is everything.
So the file is not a cache. It is the record. Each run recomputes the days still in the logs and merges them into the days already saved:
function mergeModelUsage(fresh, prev) {
if (fresh && prev) {
if (fresh.messageCount !== prev.messageCount) {
return fresh.messageCount > prev.messageCount ? fresh : prev;
}
return fresh.totalTokens >= prev.totalTokens ? fresh : prev;
}
return fresh || prev;
}
The rule is that a day can never shrink. Logs only lose data to pruning, they never gain it after the fact, so on a conflict the copy with more messages wins. A day that is halfway through being deleted from disk cannot overwrite the complete copy I already saved.
Delete data/token-usage.json and everything older than the last 30 days is gone for good. That is a slightly uncomfortable thing to know about a file in your own repo, but at least it is clear where the data actually lives.
Keeping it fresh
A launchd agent runs a shell script every two hours. The script checks whether it already succeeded today and exits if it did, checks that GitHub is reachable, runs the sync, and commits data/token-usage.json if the file changed. Then it merges dev into main and pushes, and Vercel redeploys.
"$NODE_BIN" scripts/sync-token-usage.mjs if ! git diff --quiet -- "$DATA_FILE"; then git add "$DATA_FILE" git commit -m "chore: update token usage" git push "$REMOTE" "$DEV_BRANCH" fi
Every two hours rather than once a day, because my laptop is not reliably awake at any given hour. The state file records the last successful date, so a missed window retries on the next tick instead of skipping the day. My commit history is now full of chore: update token usage, which I have made peace with.
The part I actually care about
My busiest single day was July 28: 581 million tokens across 1,509 assistant messages. That is a comically large number and it does not mean I shipped a large amount of software. It mostly means I was doing something the model had to re-read a lot of context for.
The mechanics of why are boring. Long agent sessions re-send the conversation on every turn, so context gets billed again as it grows. A repo with big files costs more per question than a repo with small ones. A model that reads twelve files before answering costs more than one that guesses, and the one that reads twelve files is usually the one I want.
What I used 4.5 Billion tokens for

Mostly hobby projects that let me creatively explore what frontier models can do.
- Trove, a Tauri desktop app for keeping and revisiting personal photos and memories. Rust core, encrypted storage, and it never touches the network.
- Trackr, a much smaller Tauri habit tracker with a SQLite database and a pile of streak math.
- A legacy codebase I spent weeks refactoring. Refactoring is mostly reading, and reading is the expensive operation.
- Exploring, which is a polite word for trying an idea and then throwing it away.
Both desktop apps are Rust, and I was learning Rust while writing them.
In case you'd want to checkout the full script, here it is: https://gist.github.com/okraks/e261332e589627f955f494eda7642299