In short: every Claude response reports how many tokens it used. Read those numbers, price each kind at its own rate, add them to a total keyed by your user ID, and compare it with what that user pays you.
The Anthropic console shows usage by workspace, key and model. It can't tell you which of your users caused it, which is the number you need when everyone pays the same flat price.
A Messages API response has a usage object. Claude splits input into three separate counts, which matters for pricing:
const message = await anthropic.messages.create({ model, max_tokens, messages });
const u = message.usage;
// u.input_tokens new input tokens (NOT including cache reads or writes)
// u.cache_read_input_tokens input served from the prompt cache
// u.cache_creation_input_tokens input written to the cache on this call
// u.output_tokens output tokens (includes extended-thinking tokens)Streaming: usage arrives in the stream events: input counts at the start and the final output count at the end. The SDK's helper that returns the final assembled message gives you the complete usage in one place, so use that instead of adding up events by hand.
Anthropic publishes prices per million tokens. Cache reads are far cheaper than normal input, and cache writes cost a bit more than normal input, so each of the four counts gets its own rate:
cost = input_tokens × input_rate
+ cache_read_input_tokens × cache_read_rate
+ cache_creation_input_tokens × cache_write_rate
+ output_tokens × output_rate (rate = price per million ÷ 1,000,000)// Example rates only. Check the current numbers on Anthropic's pricing page.
const PRICE_PER_MTOK = { "claude-sonnet-5-5": { input: 3, cacheRead: 0.3, cacheWrite: 3.75, output: 15 } };
function costUsd(model: string, u: Anthropic.Usage) {
const p = PRICE_PER_MTOK[model];
if (!p) return null; // unknown model: flag it, don't count it as $0
return (
(u.input_tokens * p.input +
(u.cache_read_input_tokens ?? 0) * p.cacheRead +
(u.cache_creation_input_tokens ?? 0) * p.cacheWrite +
u.output_tokens * p.output) / 1e6
);
}Mistakes that make Claude numbers wrong:
input_tokens and output_tokens, you miss the cache costs entirely and under-count every call that wrote to the cache. Always include both cache counts.output_tokens. A user who triggers long reasoning can cost far more than the visible answer suggests.input_tokens here.Insert one row per call with a unique event ID (so a retry can't count twice) and keep a per-user, per-day rollup that dashboards read. Do the write off the request path so a database problem never slows or fails a Claude call.
events(id uuid primary key, user_id, ts, model, input_tokens, cache_read_tokens, cache_write_tokens, output_tokens, cost_usd, feature)
daily_user_cost(user_id, day, cost_usd, requests)A user who costs $8 a month is fine on a $30 plan and a loss on a $7.49 plan. Bring in net revenue per user (after store fees and tax) under the same user ID, and margin is net revenue − AI cost. Sort by it and the users losing you money come first.
FiscaLLM does steps 2 to 4. One line after each call sends the model and the token counts, including the cache fields. It never reads the answer text or your prompts.
import { createMeter } from "fiscallm";
const meter = createMeter({ apiKey: process.env.FISCALLM_KEY, waitUntil });
const message = await anthropic.messages.create({ model, max_tokens, messages });
meter.trackAnthropic(message, { userId, feature: "summary" });from fiscallm import Meter
meter = Meter(api_key=os.environ["FISCALLM_KEY"])
message = client.messages.create(model=model, max_tokens=1024, messages=messages)
meter.track_anthropic(message, user_id=user_id, feature="summary")Free and open source. Try the demo dashboard (no sign-up), see how it works, or start free. Using OpenAI too? Read the OpenAI version.
Not by your own users. It groups by workspace, API key and model. Per-user cost needs usage tracked in your own code.
Yes. Cache reads and writes are billed at different rates from normal input, so they have to be counted separately.
No. Cost depends only on the model and the token counts. Never send message content.