Cost and usage
Cost and usage tracking is the accounting the Agent SDK hands back on every result message: the tokens a call spent, and an estimate of what those tokens cost. The estimate is worked out on your own machine, so it is a guide rather than a bill.
Last updated: 20 Oct, 2026 · Claude Agent SDK
Claude Code counts tokens as it goes and prices them from a table bundled with the release, then puts the total on the result that ends the call. The cost tracking page is firm about what that number is: a client-side estimate that can drift from the invoice when prices change or when the installed release does not recognise a model. Anthropic's Usage and Cost API is the authority; this field is for watching a run.
Saving the stand-in this part runs against
Every runnable example in this part hands query() a transport of its own, so nothing starts a real process and nothing needs a key. That transport is one file. Save the three tabs below, in order, into shop_desk.py beside your lesson files.
import asyncio
import itertools
import json
import re
from claude_agent_sdk import ClaudeAgentOptions, Transport
ORDER = re.compile(r"\b[A-Z]\d+\b")
SESSIONS = itertools.count(1)
class ShopModel:
"""Stands in for Claude: reads the conversation, picks the next step."""
def __init__(self, script=None):
self.script = list(script or [])
def reply(self, said, results):
# a scripted reply lets a lesson force a step the stand-in would not pick
if self.script:
return self.script.pop(0)
if results:
return {"text": " ".join(results)}
# the newest message first, then the last id named earlier in the chat
orders = ORDER.findall(said[-1]) or ORDER.findall(" ".join(said))[-1:]
return {"text": f"I cannot help with order {orders[0]} yet." if orders
else "Which order is this about?"}ShopModelstands in for Claude. It reads the conversation so far and returns either text to say or a list of tools to call.ShopDeskstands in for the Claude Code binary. It subclasses the SDK'sTransport, so the SDK writes JSON lines intowriteand reads message dictionaries back out ofread_messages.- The session id is a counter, not a UUID, so the output printed here is the output your own run prints.
- It calls no model. That is why the cost below is zero and the token counts are word counts, and the lesson says so wherever a number appears.
The fields a result carries
# all four are read off the ResultMessage the call ends with
result.usage # {"input_tokens": ..., "output_tokens": ..., cache counts}
result.total_cost_usd # an estimate in US dollars, or None
result.model_usage # per model: inputTokens, outputTokens, costUSD, cache counts
result.num_turns # how many rounds of the loop the call tookBoth a success and an error result carry them. In Python total_cost_usd and usage are typed as optional, so read them with a fallback rather than straight into arithmetic.
Reading the figures off one run
One question, one answer, and the four fields printed off the result that closes the call.
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
from shop_desk import ShopDesk
options = ClaudeAgentOptions()
# the result closes the call and carries the accounting for the whole of it
async for message in query(prompt="Where is my order A17?", options=options,
transport=ShopDesk(options)):
if isinstance(message, ResultMessage):
print("turns ", message.num_turns)
print("usage ", message.usage)
print("cost ", message.total_cost_usd)
print("model_usage", message.model_usage)turns 1
usage {'input_tokens': 5, 'output_tokens': 7}
cost 0.0
model_usage NoneWhat the four numbers say
- turns is 1. The desk answered without asking for a tool, so the loop went round once, which is what
num_turnscounts. - usage holds the two token counts. Against a real model these are tokens; here they are words, because the stand-in splits text instead of tokenising it.
- cost is 0.0. No request left the machine, so there is nothing to price. A real run puts a small positive estimate here.
- model_usage is None. Claude Code fills it in per model; the stand-in names no model, so the field stays empty.
Counting steps without counting one twice
When Claude calls several tools in one reply, every assistant message in that reply carries the same message_id and the same usage. Adding them up inflates the total, so keep the ids you have counted. The same page warns that a step's output_tokens is a placeholder written before the reply was generated, so take the output total from the result instead (why the per-step count is a placeholder).
seen, inputs = set(), 0
async for message in query(prompt=question, options=options):
# a subagent's messages carry a parent id; this total is the main loop only
if isinstance(message, AssistantMessage) and message.parent_tool_use_id is None:
if message.message_id not in seen:
seen.add(message.message_id)
inputs += (message.usage or {}).get("input_tokens", 0)
# the real output total is only ever on the result
if isinstance(message, ResultMessage):
outputs = (message.usage or {}).get("output_tokens", 0)Shown, not run: the stand-in gives every assistant message an id of its own, so there is nothing here for the deduplication to catch. Point it at the real Claude Code and the repeated ids appear.
Breaking the spend down per model
A run that gives subagents a cheaper model than the main agent splits its spend across two names. model_usage is the only field that shows that split, and it counts subagent requests; the plain usage field does not.
for model, spend in (result.model_usage or {}).items():
print(model, spend["costUSD"], spend["inputTokens"], spend["outputTokens"])
print(" cache", spend["cacheReadInputTokens"], spend["cacheCreationInputTokens"])Seeing what the prompt cache saved
Caching happens without being asked for: the SDK caches repeated content for you, and the usage dictionary grows two keys that say how much was written and how much was read back (cache token tracking). A cache write costs more than a plain input token and a cache read costs less, so the two are worth watching apart from the rest.
usage = result.usage or {}
print("written to the cache:", usage.get("cache_creation_input_tokens", 0))
print("read back from it: ", usage.get("cache_read_input_tokens", 0))Asking for a one-hour cache
Cache entries for your own turns live five minutes by default when you authenticate with an API key. A service that starts many short sessions against one long system prompt loses the cache between them and pays full price each time. Setting ENABLE_PROMPT_CACHING_1H asks for an hour instead, and the SDK passes an environment variable through to the Claude Code process for you (extending the cache TTL).
options = ClaudeAgentOptions(env={"ENABLE_PROMPT_CACHING_1H": "1"})An hour of cache is billed at a higher write rate than five minutes, so it pays only where the reads it saves outnumber the writes it costs. Shown, not run: proving it needs a real key and two sessions more than five minutes apart.
usage against total_cost_usd and model_usage
| Field | What it counts when a subagent runs |
|---|---|
usage | Left out. The top-level loop only, so tokens spent inside a subagent are missing |
total_cost_usd | Counted. Subagent requests are in the figure beside the main loop |
model_usage | Counted, and split by model, which is the only view of where the tokens went |
When these numbers are the right ones to read
- Watching a run while you build it, to see which prompt doubled the token count.
- Putting a soft ceiling on one call with a budget, and logging what each call spent against it.
- Deciding whether a cheaper model for subagents is paying for itself, which needs the per-model split.
total_cost_usd.Related
- Previous: Model, thinking and effort
- Next: Observability with OpenTelemetry
- See also: Turn and budget limits
- Reference: Track cost and usage
- Ask a question that names no order and watch the token counts fall.
- Print
message.usageon each assistant message and compare it with the result's. - Add
max_budget_usd=0.01to the options and read the subtype the result comes back with.
Slow is fine. Stopping is the only problem.