Skip to content

How cost is counted

Every Claude session in spwn keeps a running account of what it has spent. After each turn, spwn reads the agent’s transcript, counts the tokens, prices them, and writes the result into the session’s PR doc, a file committed on the session’s own branch. The spend lands in the same commit as the work it paid for.

You get three things from this:

  • A number on every session. It shows in the sidebar, on the Fleet, in Compare, and in the session header, so you can see what each approach is costing while it runs.
  • Figures you can add up. Forking copies a conversation, so naive totals double-count. Each session also records its own share, and adding those up across a fork family gives the exact spend.
  • Cost that travels with the branch. It’s plain TOML in the repo. A teammate, a CI job, or a script with no spwn installed can read it. See Reading cost outside spwn.
Where What it shows
Session header (the chip in the agent bar) The whole conversation’s usd, or N req when the model has no price. Hover for requests and output tokens. Click it to open the PR tab.
Sidebar (a chip on each session row) The session’s own spend. Hovering the ↑N ahead chip also shows the cost summary.
Fleet rows The session’s own spend, with the total and own figures in the tooltip.
Fleet peek (Space) The same summary as text, in the facts line under the last message.
Compare lanes Each lane’s own spend in its header, so forks compare on what they did after the split.
Inspector → PR The full breakdown: dollars, requests, the model, transcript count, input, output, cache read and cache write, a per-model table, and the own/inherited split for a fork. Spend across this problem expands to a group rollup.
The pull request on GitHub When spwn opens or updates a PR, the body’s spwn section ends with a Cost to produce line.

Rows that show the own figure fall back to a request count (42 req) when there’s no dollar figure to show.

spwn scans the transcript files for the session’s agent session id: ~/.claude/projects/…/<id>.jsonl, plus any sibling agent-*.jsonl files that belong to it. Those siblings are where Claude Code writes subagent work. Skipping them would miss a large share of a session that delegates.

For every request in those files it records five token counts: input, output, cache read, and cache writes at the 5-minute and 1-hour TTLs. The two cache-write TTLs stay separate because they bill at different rates. Two details keep the counts honest:

  • Each API request counts once. Claude Code writes one response as several transcript entries, so spwn deduplicates by requestId. Where the entries disagree (subagent files record streaming partials), it keeps the largest value of each field.
  • Rewinds don’t lose spend. A rewind or resume can move a session to a fresh agent session id. spwn remembers every id the session has spent under, in the sessions list, and scans all of them together. Anything they share is still counted once.

A fork starts from a copy of its parent’s conversation, and the child’s transcript carries that shared history too. So the doc records two figures:

  • tokens is the conversation end to end, inherited prefix included. Use it when you’re looking at one branch on its own.
  • own is the part that first appeared in this session. Use it when you’re adding sessions up.

Say a parent makes 10 requests and forks. The parent then makes 5 more, and the child makes 3 of its own:

tokens.requests own.requests
Parent 15 15
Child 13 3
Sum 28, with the shared 10 counted twice 18, the real number

Summing own is exact because of how spwn measures the inherited part. On the child’s first turn after the fork, it intersects the child’s request ids with the parent’s, and the tokens of those shared requests become the child’s inherited baseline. That baseline is written into the doc once and then frozen. From then on, own = tokens − inherited.

Measuring by request id rather than by the parent’s total has two benefits:

  • Rewind-then-fork is exact. When you fork from an earlier turn, the child copies only the transcript up to that turn, which can end well before the parent’s head. The intersection finds exactly the requests that were copied, however far back that is.
  • The number doesn’t move afterwards. Freezing means the parent can keep working, or be deleted, without changing the child’s figures. No turn has to walk the ancestry again.

A root session inherited nothing, so its doc leaves own out. An absent own means it equals tokens.

If spwn can’t measure the inherited prefix, the doc says ownUnknown = true and records no own at all. That happens when the parent’s transcript is gone before the child’s first turn, or when the child shares no request ids with it. tokens is still recorded. A sum should leave such a session out rather than count its whole conversation, because part of that conversation is already counted on the parent.

ownUsd is the session’s usd multiplied by its share of the requests (own.requests / tokens.requests). The frozen baseline keeps tokens, not a per-model split, so dollars are divided by request count. Own tokens sum exactly. Own dollars are a close estimate, and so are the dollars themselves.

Most sessions use one model. The doc then names it with model = "…" and lets tokens stand for it.

A session that used more than one model, such as an Opus main thread plus Haiku subagents, gets a byModel table instead of model. It has one entry per model id, each with the same token fields and its own usd when the model has a rate. Fast mode shows up as a separate id with a #fast suffix, because it bills at its own rate (see Model prices).

spwn recomputes cost once per completed turn. It runs before the session-turn hooks, so the bundled per-turn commit picks up the new figures in that turn’s commit rather than a turn later. Each commit on a session branch carries the cost as of that commit, and git log -p .spwn/pr/ gives you the spend history.

The update is idempotent. The cost value has no timestamp, so if nothing new was spent the file isn’t touched and the diff is empty. The same step also tidies stale PR docs out of the worktree (see after landing), and that removal rides the same commit.

cost belongs to spwn. The PR tab leaves it out of the front-matter editor, and a hand edit would be overwritten on the next turn. Every other key in the file is yours.

Here is a real-shaped PR doc for a fork that used Opus and Haiku. The front matter sits between +++ fences and is TOML. problem is a key you add yourself (see rollups), and cost is written by spwn:

+++
problem = "checkout-latency"
[cost]
files = 3
ownUsd = 24.6861
sessions = ["0f3c9a52-7d1e-4b6a-9e2f-5c8d1a7b4e90"]
usd = 37.3339
[cost.byModel.claude-haiku-4-5]
cacheRead = 0
cacheWrite = 0
cacheWrite1h = 0
cacheWrite5m = 0
input = 38912
output = 6120
requests = 31
usd = 0.0695
[cost.byModel.claude-opus-5]
cacheRead = 41882905
cacheWrite = 1242631
cacheWrite1h = 1030587
cacheWrite5m = 212044
input = 1204
output = 187431
requests = 214
usd = 37.2644
[cost.inherited]
cacheRead = 12410773
cacheWrite = 490780
cacheWrite1h = 402660
cacheWrite5m = 88120
input = 540
output = 61208
requests = 83
[cost.own]
cacheRead = 29472132
cacheWrite = 751851
cacheWrite1h = 627927
cacheWrite5m = 123924
input = 39576
output = 132343
requests = 162
[cost.tokens]
cacheRead = 41882905
cacheWrite = 1242631
cacheWrite1h = 1030587
cacheWrite5m = 212044
input = 40116
output = 193551
requests = 245
+++
# Cut checkout latency by caching the tax lookup
…

A root session with one model is shorter. It has model instead of byModel, and no own, ownUsd or inherited:

[cost]
files = 1
model = "claude-opus-5"
sessions = ["6b254ae6-c327-43f6-a071-e19fc5baad4d"]
usd = 5.9289
[cost.tokens]
cacheRead = 6120455
cacheWrite = 188920
cacheWrite1h = 170512
cacheWrite5m = 18408
input = 310
output = 41877
requests = 58

Keys come out in alphabetical order. A small value is written inline (cost = { usd = 1.5 }), and anything wider than 120 characters becomes a [section], which a real cost almost always is. To a TOML parser both forms are the same, so read them with a parser rather than a regex.

Key Present Meaning
usd when at least one model had a rate Estimated dollars for the whole conversation, to four decimals.
tokens always The whole conversation: input, output, cacheRead, cacheWrite (= cacheWrite5m + cacheWrite1h), cacheWrite5m, cacheWrite1h, requests.
own forks whose prefix was measured The part that first appeared in this session. Same fields as tokens. Absent means equal to tokens.
ownUsd with own, when usd exists usd apportioned by request share.
inherited with own The frozen baseline copied from the parent.
ownUnknown forks spwn couldn’t measure true. Leave this session out of a sum.
model exactly one model used Its id.
byModel more than one model used Per-model token fields, plus usd for priced models.
unpriced when a model had no rate Model ids whose tokens are counted but not priced.
sessions always Every agent session id this doc has spent under. More than one after a rewind.
files always How many transcript files the scan read: the main one, plus one per subagent. A sudden drop means lost subagent transcripts, not a cheaper session.
  • Claude only. Cost comes from Claude Code’s transcript format. Codex and Gemini sessions get no cost key at all. A missing key means “not measured”, not a zero.
  • Nothing until the first turn. A session that hasn’t finished a turn has no transcript to read, so the PR tab says No cost recorded yet.
  • Some pricing modifiers aren’t applied. spwn prices fast mode separately, but it doesn’t apply batch or regional (inference_geo) rates. Neither shows up in Claude Code’s interactive sessions.
  • Rate changes apply going forward. The next turn reprices the whole session at the current table. Figures already committed stay as they were.