Claude Agent SDKclaude-agent-sdk 0.2.157 · Python 3.10+
Dashboard
0%
1
Curious builder0 XP earned · 300 to level 2
0 daysFinish a lesson to begin
Badge collection0 of 6 unlocked
46 small wins to finish your pathNext lesson →

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?"}
  • ShopModel stands in for Claude. It reads the conversation so far and returns either text to say or a list of tools to call.
  • ShopDesk stands in for the Claude Code binary. It subclasses the SDK's Transport, so the SDK writes JSON lines into write and reads message dictionaries back out of read_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

python
# 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 took

Both 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.

Example
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)

What 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_turns counts.
  • 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).

python
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.

python
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.

python
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).

python
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

FieldWhat it counts when a subagent runs
usageLeft out. The top-level loop only, so tokens spent inside a subagent are missing
total_cost_usdCounted. Subagent requests are in the figure beside the main loop
model_usageCounted, 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.
Watch out. These fields are an estimate the SDK works out locally, not an invoice. They drift when prices change, when the installed release does not recognise a model, and when a billing rule applies that the client cannot model. Bill a customer from the Usage and Cost API, never from total_cost_usd.
Try it yourself
  • Ask a question that names no order and watch the token counts fall.
  • Print message.usage on each assistant message and compare it with the result's.
  • Add max_budget_usd=0.01 to the options and read the subtype the result comes back with.

Slow is fine. Stopping is the only problem.