This page covers where the dollar on a run comes from, what it means, and how far to trust it. Spending caps enforce against these same numbers, so the accuracy matters beyond display.
Why an agent costs more than it looks#
Billing happens per API call, not per agent. Every turn sends the whole
conversation so far back to the model, and you pay for all of it again. For a
context that grows evenly over n turns, billed tokens are about (n + 1) / 2
times the final context. A 38-turn agent bills roughly 19 times what its last
turn reads.
Caching discounts the charge but does not remove it. A cached prefix bills at a fraction of the base input rate on every turn that reads it. Cheap per token, paid many times over.
The four token buckets#
An API response reports four token counts, each billed at a different multiple of the model's base input rate.
| Bucket | Field | Rate |
|---|---|---|
| New input | input_tokens |
1x base input |
| Cache write | cache_creation_input_tokens |
~1.25x base, higher for a 1-hour TTL (time to live) |
| Cache read | cache_read_input_tokens |
~0.1x base, lower on some families |
| Output | output_tokens |
~5x base |
The multiples above are the shape of current published pricing, not fixed constants. Roscoe ships a rate table of list prices per million tokens, one entry per model family. Settings → Models shows the input and output rate for each model and lets you override either one; read the current prices there rather than from this page. The cache multipliers are built in and are not editable.
The spread matters more than the multipliers themselves. Cache reads bill at a small fraction of the base rate while output bills at a multiple of it, so the same token count can differ by well over an order of magnitude in price on a single model. A token count alone does not determine a price. You need the composition.
Two traps in the fields:
cache_creation.ephemeral_5m_input_tokensandephemeral_1h_input_tokensare a breakdown ofcache_creation_input_tokens, never an addition to it. Adding them on top doubles the cache-write charge.- The two TTL buckets bill at different multiples, and the populations that use them barely overlap. Subagent turns are almost entirely 5-minute; the driver's own turns are almost entirely 1-hour. Pricing every write at the 5-minute multiple therefore understates driver cost substantially, so Roscoe reads the split from the data rather than assuming it.
Where the token counts come from#
The host reports subagent_tokens when a subagent finishes. That number is the
final turn only, not a cumulative total, so it is roughly 10x low on a
typical multi-turn agent and useless on its own. Roscoe uses it as a checksum,
not as the answer.
The real figures come from the per-turn transcript Claude Code writes:
<config-root>/projects/<encoded-cwd>/<session-uuid>/subagents/agent-<id>.jsonl
<config-root>/projects/<encoded-cwd>/<session-uuid>/subagents/workflows/wf_<id>/agent-<id>.jsonlThe config root is CLAUDE_CONFIG_DIR when it holds an absolute path,
otherwise ~/.claude. A relative value is ignored rather than guessed at.
That file holds one line per content block, not per turn, and the early lines
of a message carry a partial output_tokens. Summing it naively roughly
doubles the total. The rule that reduces it correctly:
Group assistant lines by
message.id. Within each group take the input side once andmax(output_tokens). Then sum all four fields across groups.
Every clause carries weight. max rather than first, because early lines
freeze output at a partial count. Input once rather than summed, because the
input fields never vary within a group. Across groups rather than one group,
because a group is one turn and an agent is many.
On Claude Code Roscoe locates and reduces the file itself. On every other host it cannot reach the transcript, so the driver ships the raw lines and Roscoe reduces them. Drivers are asked to send the lines unreduced, and if they do reduce, to keep the last line of each group. The idiomatic way to deduplicate keeps the first, which is the partial count.
Provenance: reading usage_source#
node_runs.usage_source records how a row's usage was obtained.
| Value | Meaning |
|---|---|
exact |
Roscoe read and reduced the transcript itself. |
reconciled |
Same, but the final turn was topped up or closed in a state worth flagging. |
reported |
The driver shipped per-line records. Roscoe reduced them, never saw the file. |
scalar |
A total only, with no composition. Falls through to a text-length estimate. |
estimated |
No usage at all. Reconstructed from text length, blind to cache. |
main_loop |
Answered directly in the driving conversation, so no transcript ever exists to price it from. |
They rank in that order. Rows written before the column existed hold
legacy, which is not on the ladder: it reads as estimated, as does any
other unrecognised value.
reconciled outranks reported because custody dominates. A reconciled
row's uncertainty is one derived integer in a file Roscoe read. A reported
row's uncertainty is every digit a model typed into a tool call.
Read the field as a claim about tokens, not dollars. exact means the
tokens came from a per-token record. The dollar still passes through the rate
table.
Only the three measured rungs write the cache columns. A scalar row leaves
them NULL rather than 0, because 0 would assert that no cache traffic happened
rather than that nobody looked.
Metered and subscription dollars are different#
Metered. A server-side backend ran and
actually charged. The claude -p path reports an authoritative
total_cost_usd that Roscoe records verbatim, without consulting the rate
table. This is the only truly billed figure Roscoe holds.
Subscription. Every handback node runs on your own Claude plan. Out of pocket cost is $0. The dollar shown is an API-equivalent list price computed from token counts and Roscoe's rate table, answering "what would this have cost on the API".
So a rate-table error moves a subscription figure even when the token counts
are perfect, reconciling it against an Anthropic invoice is not meaningful, and
a spending cap on a handback run is a proxy control rather than a spend limit.
workflow_runs keeps them apart: metered_cost_usd,
subscription_equiv_cost_usd, and total_cost_usd as their sum.
The driver's turns are usually the larger half#
The conversation driving a run usually costs more than the run's steps, and it lands in no node row. Roscoe measures it separately, and the dollar cap binds on steps plus driver. The token cap deliberately stays on steps.
Every run has its own driver#
On Claude Code and Cowork, start_workflow asks which conversation will drive
the run before anything starts. The answer is almost always dedicated: a
subagent spawned to drive this one run and nothing else. Its transcript opens
and closes with the run, so everything in it is the run's cost.
A run driven from your main conversation (shared) gets no driver figure at
all. That conversation is doing other work too, and nothing in it says which
turns belonged to the run. Roscoe records driver_shared_unattributable in
workflow_runs.driver_unavailable_reason rather than guess.
Where the figure comes from#
On Claude Code Roscoe shares the driver's filesystem. Its hooks report which transcript is the driver's, and Roscoe reads that file itself.
On Cowork the driver runs in a sandbox Roscoe cannot read. Each paused result carries the windows still needing a figure and a ready-made command. The driver runs the command in its sandbox and sends what it prints on the next handback. The first response of a run also hands over a one-line hook install, so the command knows which file is the driver's.
A Cowork subagent cannot spawn subagents of its own. So the driver asks your main conversation to spawn each step's agents and pass back their ids and answers. Those relay turns are the run's cost too: the command counts the main conversation's turns from its status read of the paused run to its reply to the driver, and nothing else it does.
When a driver declines#
A driver can answer unavailable with a reason instead of a figure. Roscoe
accepts it and asks again at every later pause while the window is
unmeasured. A window grows, so a driver that could not read the file early may
manage later. A recorded figure is never revised downward.
Roscoe pushes back in two places. A handback that leaves a window out entirely
is refused once. no_turns_in_window is accepted at the first handback, when
the turn that started the run has not been written yet, and refused from the
second, because every handback since was one of the driver's turns.
The reason persists on workflow_runs.driver_unavailable_reason until a
driver figure is recorded, which clears it. So a finished run that still has a
reason was never measured.
How fan-out and sub-workflows aggregate#
The parent carries the aggregate. For a consensus, map, or round-robin
node, the per-agent costs sum into one figure on the parent node_run, and the
children are then excluded from the run rollup so cache traffic is not counted
twice.
The exception is a fan-out that has re-paused mid-round, which has priced children and an unpriced parent. That spend is real, so the rollup counts a row when it is top-level or when its parent has no cost yet. Without that, a 15-agent consensus round is invisible to the spending cap until the parent completes.
A sub-workflow's cost lives in its own run, and the rollup recurses into child runs. Caps live on the root run, so the same dollars are never charged against two limits.
The aggregate's provenance is its weakest part: one estimated agent makes
the whole parent estimated, rather than presenting an approximation as a
measurement.
What stays inexact#
- Caps bind at fan-out boundaries, not node boundaries, and on handback they fire after the money is spent.
- A price change on a recognised model family is silent. Roscoe matches a model to a family by name, so a superseded generation prices at the current rate with no warning. Set an exact rate in Settings → Models to pin one.
- The rate table is hand-maintained and carries the date it was last checked against published pricing. It is the one remaining source of dollar error once the token counts are right.
- An unrecognised family falls back to the sonnet rate and warns once.
- Output tokens are occasionally under-recorded. A message's completion line is sometimes never written, freezing that turn's output at a streaming snapshot. This was an upstream Claude Code bug: rates dropped from 50.0% of groups in 2.1.246–2.1.267 to 2.3% on 2.1.268 and later. Separately, the Cowork sandbox records a placeholder of a few output tokens on some subagent turns. Roscoe prices what the transcript says rather than estimating over it, so those rows run slightly low.
- The driver figure runs a little low. Turns the driver takes after its last handback are counted only if it calls Roscoe again afterwards, which its instructions tell it to do. Turns it takes before the run starts, such as loading its tools, are never counted.
- A driver that answers inline cannot be measured. On Claude Code and
Cowork every
ai_agentandai_judgestep is dispatched as its own subagent, and that is what creates the transcript the figure is read from. A driver that ignores the instruction and answers from its own context leaves the step with no transcript, so it falls back to a text-length estimate. Plain desktop chat has no subagents at all and always takes that path. - The host a run reports is self-reported, except that a Cowork connection is always recorded as Cowork. Every control here assumes an honest driver; none is a security boundary.
- Web search and fetch (
server_tool_use) are not priced. - Nested subagents are unmeasured. Roscoe never nests agent trees itself, so this only bites if a node prompt tells a subagent to delegate.
If you are debugging a cost figure#
-
Read
usage_sourcefirst.estimatedis approximated from text length, blind to cache, measured 30–125x low on short agents and far worse on long ones.scalaris a final-turn total, roughly 10x low. Only then is the number worth comparing to anything. -
Check
billing_mode. Asubscriptionrow is an API-equivalent price, $0 out of pocket. Ameteredrow fromclaude -pis a real charge that never touched the rate table. -
Check
priced_at_rates_versionagainst the table'slastVerified. Rows priced under different tables are not comparable. A null stamp means the host supplied the dollar. -
Check
modelresolves to the family you expect. -
Recompute by hand, every term divided by 1,000,000:
input * rate.input + output * rate.output + write5m * rate.input * cacheWriteMultiplier + write1h * rate.input * cacheWrite1hMultiplier + cacheRead * rate.input * cacheReadMultiplierKeep the two cache-write terms apart: folding them into the 5-minute multiple prices a driver row low. A mismatch points at a per-model rate override in
roscoe.yaml, not at the table. -
Check whether the cache columns are null or zero. Null means nobody looked. Zero asserts no cache traffic, which for a multi-turn agent is almost certainly wrong.
-
If a figure looks ~10x low, suspect a final-turn number used as a total. Orders of magnitude low means a text-length estimate. Roughly 2x high means a transcript summed per line instead of per
message.id. -
For a fan-out parent, confirm whether its children should be counted: if the parent has a
cost_usdthey must be excluded, if not they must be included. -
For a run total, the driver's turns are counted but the steps alone are not the whole story. Check both.
Where to next#
- Running on your Claude subscription — which nodes run where, and what the handback does.
- LLM backends — the metered paths and where an authoritative cost comes from.
- roscoe.yaml — per-model rate overrides.