Token accounting: what Claude Code really used

This page is a deep dive into how Claude Code Session Analytics counts tokens and cost. Each number is easy to get wrong, and I got most of them wrong first. Here is what the data looks like, what went wrong, and how the tool counts now.

The transcript grain

My first model was simple: one .jsonl file is one session. The first ingest found 103 of 250 transcripts.

The missing files were sub-agents. Claude Code writes each sub-agent to its own file under <project>/<session_id>/subagents/agent-<id>.jsonl. These files repeat the parent’s sessionId, but they contain a different set of request IDs: zero overlap. They are real, additional API calls.

So the unit that maps one-to-one onto a report is the transcript, not the session. The constructed schema has a transcripts table between sessions and api_calls, and a session is a rollup over its transcripts. After a little over one month of use, sub-agents were 49% of all billed input tokens.

The data model

The schema claude has one fact table and some metadata and audit tables:

  • api_calls — one Claude API call (one request ID group): tokens, model, agent, agent type, timing. total_input_tokens is a generated column: input + cache write + cache read.
  • projects, sessions, transcripts — the hierarchy. A transcript has its size, hash, time window, models, CLI versions and git branches.
  • model_pricing — one price point per model and date.
  • ingest_runs, csv_files, parquet_exports, migrations — what was loaded, when, and from which file.

The views do the rest. Each dashboard panel and each report reads a view, not the raw tables, so the counting rules live in one place.

The sub-agent calls that were missing

In October I checked the integrity rule again: the loaded rows for each transcript must equal the call count from the transcript itself. It failed. Only 3 rows were linked to 511 sub-agent transcripts, against 8,701 expected calls. The sub-agent calls were in the database, but they were credited to the parent session.

The fix stamps each call with its own sub-agent transcript, and a relink step fixes the history from the database alone. After the relink, 8,684 calls moved to their sub-agent transcripts.

A second defect was hidden behind the first. A resumed or forked session can contain sub-agent records whose file lives under another session. Those calls stayed on the main transcript, and the same calls were also loaded from the sub-agent file: 46 duplicate rows, approximately 2.85 million input tokens counted two times. The relink now matches sub-agent transcripts across sessions and removes the duplicates.

With correct links, the sub-agent share is 23.9% of input tokens all time (438 million of 1.83 billion, 8,741 calls). In August, on a much smaller data set, it was 49%: as my usage grew, the share halved.

Why /stats shows a bigger number

One API call can write more than one record to the transcript: one record for each content block of the answer, under the same request ID. The input, cache-write and cache-read counts are the same in each record. The output count is different.

The tool deduplicates the group: input, cache write and cache read once, and the maximum output. Claude Code’s own /stats does not group the records. It adds the tokens of every record.

To compare, the tool keeps three extra columns that reproduce the /stats arithmetic: the number of records in the group, the raw output sum, and a generated duplicated_total_tokens. They do not change the deduplicated columns. For my last 30 days (dashboard defaults):

Tracked (deduplicated)/stats arithmetic
Tokens468 M956 M
Estimated API cost$194.24$407.98

The /stats number is approximately two times the real one.

The cost model

The cost is an estimate at Claude API list prices. The prices live in data/reference/model_pricing.csv that is updated as new models are deployed. A price change is a new row, never an edit, so an old call keeps the price that was valid on its day.

  • acp pricing refresh compares a snapshot of the pricing page with the CSV and appends only the changed rows.
  • acp pricing sync-db loads the CSV into claude.model_pricing.
  • The cost views take the newest price with effective_from <= called_at. A model with no price gives NULL, never 0 — so an unknown model can not make the total look smaller.
  • Long-context tiers. Some models like Haiku 5.5 have a higher rate above a prompt-length threshold. When a call’s input (input + cache write + cache read) is above the threshold, the full request is priced at the long rates.

All time, my usage would cost $852.34 at list prices, and $1,560.19 with the /stats arithmetic. The 96.74% cache-read share is the main reason it is not much higher: cache reads cost a fraction of the normal input price.

Sessions that start sessions

Some of my sessions hand work to a new session. The handoff prompt carries an Origin session_id: line. The loader stores that ID as the claimed parent, and the view v_session_lineage resolves the root and the depth of each chain, with a guard against cycles.

The Session lineage and Multi-agent dashboards show these chains.

How the numbers are checked

  • Loaded rows per transcript = the call count in the transcript.
  • One CSV for each transcript with calls, and no orphan calls.
  • A Parquet round-trip (export, COPY FROM, sum again) matches exactly.
  • Each sub-agent transcript is owned by one path, by file hash, so a symlinked file is not loaded two times.

Links