Track API Costs
From Tokens to Money
Lesson 185 measured tokens. A team cares about money, and money depends on the tokens multiplied by the price of each kind of token, for each model, on each day. The same token count costs very different amounts on different models, and output tokens are priced higher than input tokens. A dashboard that reports only tokens can hide a feature whose small volume runs on the most expensive model, and a dashboard that reports only the total bill cannot say which feature caused it.
This lesson covers how to turn usage records into cost, how to attribute cost to features and days, how to compare the application's estimate with the invoice, and how to set budget alarms that fire before a bill surprises anyone. The prices in the examples are examples: the current figures come from the pricing page, and a table in code must be updated when they change.
Cost Is Price Times Usage, Per Kind
Each call is billed for its input tokens and its output tokens, at prices that depend on the model. Cache reads and writes carry their own prices, as Lessons 125 and 126 described. The cost of one call is therefore a sum of products, and the price table needs one entry per model with one price per kind of token:
PRICES_PER_MILLION = {
"claude-sonnet-5-5": {"input": 2.00, "output": 10.00},
"claude-opus-5-5": {"input": 4.00, "output": 20.00},
}
def call_cost(model: str, input_tokens: int, output_tokens: int) -> float:
prices = PRICES_PER_MILLION[model]
return (input_tokens * prices["input"] + output_tokens * prices["output"]) / 1_000_000
The function fails loudly for an unknown model, through the dictionary lookup, rather than assuming a price. An unknown model in production should stop the cost report and raise an alarm, because a silent default would understate spending.
Attributing Cost
Attribution connects each cost to the feature that caused it, and to the day it was incurred. Those two labels are what turn a total into a decision. A daily report by feature shows which feature grew, which model it uses, and whether its spending matches its value.
The report below groups call records by day and feature, totals the cost, and compares each day's spending with a budget. Each record carries the model, so the cost of a feature is correct even when it uses more than one model:
"""cost_report.py: attribute call costs to days and features, and alarm on budget overruns."""
from collections import defaultdict
from dataclasses import dataclass
PRICES_PER_MILLION = {
"claude-sonnet-5-5": {"input": 2.00, "output": 10.00},
"claude-opus-5-5": {"input": 4.00, "output": 20.00},
}
@dataclass(frozen=True)
class CallRecord:
day: str
feature: str
model: str
input_tokens: int
output_tokens: int
def call_cost(record: CallRecord) -> float:
prices = PRICES_PER_MILLION[record.model]
return (record.input_tokens * prices["input"] + record.output_tokens * prices["output"]) / 1_000_000
def daily_report(records: list[CallRecord], daily_budget: float) -> list[str]:
by_day: dict[str, dict[str, float]] = defaultdict(lambda: defaultdict(float))
for record in records:
by_day[record.day][record.feature] += call_cost(record)
lines = []
for day, features in by_day.items():
for feature, total in features.items():
lines.append(f"{day} {feature}: ${total:.2f}")
day_total = sum(features.values())
lines.append(f"{day} total: ${day_total:.2f}")
if day_total > daily_budget:
lines.append(f"alert: {day} spend ${day_total:.2f} is over the ${daily_budget:.2f} budget")
return lines
if __name__ == "__main__":
records = [
CallRecord("2026-11-09", "summarize", "claude-sonnet-5-5", 1_000_000, 100_000),
CallRecord("2026-11-09", "classify", "claude-sonnet-5-5", 2_000_000, 50_000),
CallRecord("2026-11-09", "tutor", "claude-opus-5-5", 500_000, 200_000),
CallRecord("2026-11-10", "summarize", "claude-sonnet-5-5", 1_500_000, 150_000),
]
for line in daily_report(records, daily_budget=10.00):
print(line)
Output:
2026-11-09 summarize: $3.00
2026-11-09 classify: $4.50
2026-11-09 tutor: $6.00
2026-11-09 total: $13.50
alert: 2026-11-09 spend $13.50 is over the $10.00 budget
2026-11-10 total: $4.50
How It Works
- Each record's cost comes from its own model's price. The tutor feature runs on the more expensive model, so its cost is computed with that model's prices, and the report reflects it.
- The report is grouped by day, then by feature. The first day's three features add up to the day total, which is compared with the budget.
- The alert fires on the day that exceeds the budget. The second day, at $4.50, stays under the limit and produces no alert.
- The arithmetic is exact for these inputs. The summarize feature costs one million input tokens at $2 plus one hundred thousand output tokens at $10 per million, which is $2 plus $1, or $3.00.
- The budget is a parameter, so the same report serves different limits for different environments.
Reconciling With the Invoice
The application's cost figure is an estimate built from its own records and its own price table. The invoice is the authority, and the two can differ: a price table may be out of date, a record may be missing, a retry may have been billed that the application did not record, or a discount may apply. Reconciliation compares the two over a period and investigates the difference. A small, stable difference is expected, and a growing one means a record or a price is wrong.
The platform's usage and cost reporting, which the documentation describes, provides the figures to compare against. The comparison should run regularly, not only at billing time, because a discrepancy found early is much easier to explain.
Budget Alarms
An alarm is worth more when it fires early. A budget of ten dollars for a day is a limit, and an alarm at eighty percent of it gives time to act before the limit is reached. The alarm should name the feature responsible, since the response often depends on which feature grew. Lesson 179 described the per-user budget that enforces spending in code, and the alarms here are the warning that a person sees before that enforcement becomes necessary.
Keeping the Price Table Current
The price table is data that goes stale. Prices change, new models appear, and cache and batch rates differ from standard rates. A table that is not reviewed produces reports that are confidently wrong. A practical habit is to record the date the table was checked, to keep the table in configuration rather than in scattered code, and to reconcile against the invoice whenever prices are updated. Batch usage (Lesson 131) and cache usage (Lesson 125) need their own rates in the table, or their costs are misstated.
Summary
Cost is the sum of each token kind's count times its price, for the model used. Attributing cost to days and features turns a total into a decision, and the report should compare each day with a budget and alarm early. The application's estimate must be reconciled with the invoice regularly, because the price table and the records can both drift. Alarms should fire before limits, and name the feature responsible. The next lesson covers the logs that record how the application runs, which is the subject of configuring logging.