Claude Code spend tracking tools compared
Claude Code spend trackers answer different questions. Compare local log parsers against the built-in usage screens and your own OpenTelemetry stack.
What a Claude Code spend tracker actually reads
Every Claude Code spend tracker reads one of three data sources, and the source decides which question it can answer. A log parser reads the session transcript files on your own disk. A dashboard reads the usage records Anthropic keeps for your account or your organisation. A metrics backend reads the OpenTelemetry (OTel) stream that Claude Code emits when you switch it on. All three can be correct at the same time and still disagree, because they are counting different things.
This guide does not re-explain tokens. how Claude Code counts token usage covers input, output, cache writes and cache reads, and no dashboard means much until that part is clear. The question here is narrower: for each shape of tool, what can it see, and what can it never see.
Why three Claude Code spend trackers appeared on one day
Three separate Claude Code spend trackers were posted on the same day. They were not three versions of the same tool, and that is the useful part. One parsed the local session files. One wrapped the account usage screens. One was a hosted tracing backend you run yourself.
They arrived together because the cost of an agent session stopped being obvious. A chat costs roughly what you can see on screen. An agent reads twenty files, runs the test suite, and re-sends the whole conversation on every turn, so the bill is driven by context you never typed. On a subscription there is no dollar figure at all, only a usage bar that empties faster on some days than others. Each of the three shapes fills a different part of that gap.
Shape 1: a local log parser tells you what today cost
Claude Code stores each conversation as JSON Lines (JSONL) at ~/.claude/projects/<project>/<session-id>.jsonl, where <project> is your working directory path with non-alphanumeric characters replaced by -. Every assistant turn in that file carries the token counts for its request. A log parser adds them up and prices them.
ccusage is the one most people land on. It needs no install:
npx ccusage@latest daily
npx ccusage@latest daily --breakdown
npx ccusage@latest blocks
npx ccusage@latest session --jsondaily totals by date. --breakdown splits each row by model, which is how you find out that one Opus afternoon is most of the week. blocks groups by the five-hour window a subscription resets on. session totals per conversation, and --instances groups by project so you can see which repository is expensive. Add --since and --until to bound the range, and run npx ccusage@latest daily --help for the date format your version expects. As of August 2026 it also reads other agent CLIs, including Codex and OpenCode, which matters if you are comparing them.
Pricing comes from a model price table, and the tool has three cost modes. --mode auto uses the costUSD value Claude Code wrote into the file when it is present, and computes from token counts when it is not. --mode calculate always computes from tokens and ignores any recorded cost. --mode display shows only recorded costs and prints $0.00 for rows that have none. If a total looks wrong, run the same report under calculate and then under display. A large gap between them means most entries carry no recorded cost, so everything you are reading is an estimate.
The same data can feed your prompt. ccusage statusline prints a compact line for the Claude Code status bar, wired into ~/.claude/settings.json like any other status line command. See building a Claude Code statusline for the settings block and the fields it receives.
What a log parser cannot see is anything that did not happen on this machine. A second laptop, a session on claude.ai, a teammate's work: those transcripts live on those disks. Old data is missing too, because transcripts are cleaned up after 30 days by default under the cleanupPeriodDays setting, so last quarter is gone unless you archived it.
There is one more risk, and it is structural. Anthropic's documentation states that the entry format is internal to Claude Code and changes between versions, so scripts that parse these files directly can break on any release. That applies to every tool of this shape. It is also the reason a hand-rolled jq one-liner over the JSONL is a worse idea than it looks: the maintained parsers track the format changes for you, and your one-liner will report a confident wrong number the day a field is renamed.
Finally, the dollar figure needs a caveat on a subscription. You are not billed per token on Pro or Max, so the number is what your tokens would have cost at list API rates. It measures how heavy your usage is. It is not your bill. If the real question is which plan to be on, that comparison is its own exercise: see API billing against a Claude subscription.
Shape 2: the built-in usage screens tell you which model burned the budget
Claude Code ships its own reporting and most people never open it. Run /usage inside a session. The Session block at the top shows tokens by model and a dollar figure for the current session, computed locally from token counts at standard list rates. That figure does not reflect a discount or promotional pricing, so it can differ from your invoice. The totals reset when /clear starts a new conversation.
On a Pro, Max, Team or Enterprise plan the same screen shows how much of your plan limit you have used, and attributes recent usage to skills, subagents, plugins and individual MCP servers as a percentage of the total. It flags behaviours that account for 10% or more of recent usage, such as long context or cache misses. Press d or w to switch between the last 24 hours and the last 7 days. These figures are approximate and computed from local session history on this machine, so a second device is not counted.
Above one developer, the numbers move to the account. An API organisation gets the Console usage page, a Claude Code dashboard with spend and accepted lines per member, and a Claude Code Analytics API that returns the same daily per-user metrics with an admin key. Teams and Enterprise plans get a spend report in the admin console with CSV export, updated daily, and Enterprise adds an analytics API. Which of these you see depends on how each developer signed in, so a mixed organisation reads two reports and adds them up by hand.
For sizing a budget, the published figure in Anthropic's cost documentation as of August 2026 is an average near $13 per developer per active day and $150 to $250 per developer per month, with 90% of users under $30 per active day. Treat that as a published benchmark from enterprise deployments, not as a prediction for your team. Run a pilot group and measure before you extrapolate.
What the dashboards cannot see is anything below the day and the person. They will tell you that Opus was most of Tuesday. They will not tell you which prompt, which repository or which CI job did it. They also lag, because the organisation reports update daily, so they are a review tool rather than a way to catch a runaway agent this afternoon. Catching the runaway needs limits, not reports, which is the subject of keeping agent costs bounded on a VPS.
Shape 3: your own OpenTelemetry stack tells you which prompt regressed
Claude Code emits OpenTelemetry metrics and events once you set one environment variable, and this is the only option that streams per-user token and cost data into a system you control in near real time. The metrics include claude_code.cost.usage in USD, claude_code.token.usage in tokens, claude_code.session.count and claude_code.active_time.total.
The token metric is the interesting one, because of its attributes. Each data point carries type, which is input, output, cacheRead or cacheCreation, plus model and query_source, which is main, subagent or auxiliary. It also carries agent.name, skill.name, mcp_server.name and mcp_tool.name. That is enough to answer questions no dashboard can reach: how much of the bill is subagents rather than your own turns, whether one MCP server doubled your input tokens, whether cache reads collapsed after someone edited CLAUDE.md. Cache behaviour is usually where the surprise hides, and when prompt caching pays for itself explains what you are looking at.
One correction worth making, because it comes up in every thread on this. Langfuse is a good self-hosted tracing backend, and running it on a VPS is covered in self-hosting Langfuse for agent tracing. Its OTLP endpoint accepts traces only. Claude Code exports metrics and log events, not spans, so pointing OTEL_EXPORTER_OTLP_ENDPOINT at Langfuse leaves the project empty and gives you no error worth reading. Langfuse is the right tool for agents you build on the API yourself, where your own code creates each span with its prompt, model and cost. For the Claude Code CLI, a metrics store is the match.
Set up Claude Code spend tracking on your own VPS
Two services are enough: a collector to receive the metrics, and Prometheus to store them. Keep both off the public internet, because an open OTLP port accepts writes from anyone who finds it. Write /opt/ccmetrics/compose.yaml:
services:
collector:
image: otel/opentelemetry-collector-contrib:latest
command: ["--config=/etc/otel/config.yaml"]
volumes:
- ./collector.yaml:/etc/otel/config.yaml:ro
ports:
- "10.8.0.1:4318:4318"
restart: unless-stopped
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prom-data:/prometheus
ports:
- "127.0.0.1:9090:9090"
restart: unless-stopped
volumes:
prom-data:10.8.0.1 is the server's address inside a WireGuard tunnel, so the collector is reachable from your machines and from nowhere else. The address in front of the port is doing real work here, because published Docker ports are not filtered by ufw: see why Docker published ports bypass ufw. Setting up the tunnel itself is a WireGuard VPN on your own VPS.
/opt/ccmetrics/collector.yaml:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
processors:
batch:
exporters:
prometheus:
endpoint: 0.0.0.0:8889
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus]/opt/ccmetrics/prometheus.yml. Port 8889 is never published to the host, because Prometheus reaches the collector over the Compose network by service name:
global:
scrape_interval: 30s
scrape_configs:
- job_name: claude-code
static_configs:
- targets: ["collector:8889"]cd /opt/ccmetrics
docker compose up -d
docker compose logs collectorThe collector log should end with Everything is ready. Begin running and processing data. A log that stops on a config error means the YAML did not parse, and the container will restart in a loop.
Now point Claude Code at it. On each machine that runs Claude Code, add this to ~/.claude/settings.json:
{
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"OTEL_METRICS_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "none",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://10.8.0.1:4318",
"OTEL_METRIC_EXPORT_INTERVAL": "10000"
}
}Start a session, send one prompt, wait for the export interval (10 seconds here, 60 seconds by default), then ask Prometheus what it has learned:
curl -s http://localhost:9090/api/v1/label/__name__/values | grep -o 'claude_code[a-z_]*'You should get several names beginning with claude_code_. The exporter rewrites the dots to underscores and appends the unit, so the exact strings depend on your collector version. An empty result means nothing has arrived. Check that the protocol and the port agree, because http/protobuf goes to 4318 and grpc goes to 4317, and a mismatch fails quietly. Run claude --debug and the debug log reports OTel export errors.
For one machine and no server, skip all of the above. Set OTEL_METRICS_EXPORTER=prometheus and Claude Code exposes a scrape endpoint at http://localhost:9464/metrics itself. When prometheus is the only exporter listed, Claude Code omits the USD, tokens and s units from the metric names so the scrape stays valid Prometheus text format.
One privacy decision comes with this shape. By default only counts leave the machine, no prompt text and no tool output. OTEL_LOG_USER_PROMPTS=1 and OTEL_LOG_TOOL_CONTENT=1 change that, and then your metrics box holds source code and whatever else was in context. Turn those on deliberately, and read keeping secrets out of agent context first.
Tracking spend for scripted and CI runs
Non-interactive runs are the ones that surprise people, because nobody is watching the screen. claude -p with --output-format json reports the cost of that run in its result payload:
claude -p "summarise the failing tests" --output-format json | jq '.total_cost_usd'The payload carries total_cost_usd plus a per-model breakdown, so a CI job can record its own spend with no dashboard involved. Append the value to a file, or push it as a metric to the collector above. This is the cheapest useful spend tracking available, and it costs one jq call per run.
Failure modes and what you will see
The report is empty. npx ccusage@latest daily printing no rows means it is not reading where Claude Code writes. CLAUDE_CONFIG_DIR moves that location, and the parser has to be told about it. If rows exist but stop about a month back, that is cleanupPeriodDays working as designed: transcripts are removed after 30 days by default.
Two machines report different totals. Expected, and not a bug. Both /usage and any log parser read local session history only, so usage from another device or from claude.ai is absent from both.
The local total does not match the invoice. Local figures are computed from token counts at standard list rates. They know nothing about promotional pricing or a contracted discount, and on a subscription your tokens are not billed individually at all. The Console usage page is authoritative for API billing.
Cost climbed while you did the same work. Check the cache columns before anything else. A long session re-sends its whole history on every turn, priced at the cached rate while the cache is warm and at the full input rate once it goes cold, so one long break re-processes the entire conversation. That shows up as a large input number next to a small output number, and input against output token pricing explains why the two move independently.
A day with subagents looks impossible. Each subagent runs its own context window, so token use scales with how many ran and how long each one lasted. Only the OTel data separates them, through the query_source attribute on claude_code.token.usage. A log parser will show you the total and leave you guessing.
FAQ
Does ccusage show what I am actually billed on a Max plan?
No. On a subscription you are not billed per token, so a log parser prices your tokens at standard list API rates and shows what the same work would have cost through the API. It is a good relative measure of how heavy a day was, and it is useful for comparing projects or models against each other. For what you owe, the Console usage page covers API billing and the plan billing page covers a subscription.
Where does Claude Code store the session files these tools read?
In ~/.claude/projects/<project>/<session-id>.jsonl, where <project> is the working directory path with non-alphanumeric characters replaced by -. Each line is a JSON object for one message, tool use or metadata entry. CLAUDE_CONFIG_DIR moves the whole directory, and cleanupPeriodDays in settings.json controls the 30-day retention. Anthropic documents the entry format as internal and subject to change between versions, so parse it with a maintained tool rather than your own script.
Can I send Claude Code telemetry to Langfuse?
Not directly. The Langfuse OTLP endpoint accepts traces, and Claude Code exports metrics and log events rather than spans, so the data has nowhere to land. Send Claude Code metrics to an OpenTelemetry collector and store them in Prometheus. Use Langfuse for agents you build yourself on the API, where your own code emits spans that carry the prompt, the model and the cost.
Why do my local numbers not match the Console usage page?
Because they are computed differently. /usage and log parsers add up token counts from session files on the machine you are sitting at, then price them at standard list rates. The Console reports what your organisation was actually charged, across every machine and every key, after any discount. A mismatch is normal. A very large one usually means a second device, a CI runner, or another team member is billing to the same account.
How do I track the cost of a claude -p run in CI?
Run it with --output-format json and read total_cost_usd from the result, for example with claude -p "..." --output-format json | jq '.total_cost_usd'. The same payload includes a per-model breakdown and the session ID. Record that value per job and you have per-pipeline spend without any agent, dashboard or extra service.