Skip to content
mcpd
no daemon
Source

Cost accounting

A cost figure is only worth having if you can say where it came from. This is every step between a token count and a number on the screen.

Priced once, at ingest

When a span arrives carrying a model id and a token count, mcpd looks that model up in a price book, computes what the call cost and stores the result on the span. Nothing is recomputed on read.

That is the important decision on this page. Prices change. If cost were derived at query time from whatever book happened to be loaded that day, a rate cut next month would quietly rewrite what last month cost, and the number you reported to somebody would not be there when they went to check it. Costing at ingest freezes the figure against the rates that were true when the call was made. Every stored cost also records the id of the book that produced it, so a figure can be traced back to the rates behind it.

A span that already carries a cost is left exactly as it arrived. If your provider hands you a real figure and you record it, mcpd keeps yours and does not price over it. The is_estimated flag separates the two: anything mcpd derived from a book is marked estimated, because it is.

Rates are per million tokens

Every rate in a price book is currency units per million tokens, which is how every provider publishes them. A rate copied off a pricing page goes into a book unchanged, with no division to get wrong at three in the morning. The arithmetic for one meter is tokens × rate ÷ 1,000,000.

Cost lives on the step, not on the run

Each model call carries its own figure. A run's total is the sum of its spans, and mcpd recomputes that sum from the spans every time one of them arrives rather than incrementing a counter. Spans arrive out of order, get retried and get updated in place, and an incrementing total drifts under all three. A headline number that does not match the rows underneath it destroys trust in every other number on the page.

Trace b3d9e71a Fault
0ms 2.1s 4.2s 6.3s 8.4s
  1. agent support_agent fail
  2. model chat
  3. tool returns_policy
  4. model chat
  5. tool returns_policy
  6. model chat
  7. tool returns_policy

The model called one tool three times and never answered. That is visible in four seconds here, and invisible in a log file.

The three model calls above are priced at $0.00015, $0.00018 and $0.00021. The run total in the header is their sum and nothing else: the tool calls, the memory lookup and the guardrail consumed no tokens, so they carry no cost and contribute none.

The meters

A model call is not one number of tokens. Providers bill several meters at different rates, and a price book gives a rate for each of them.

RateApplies toWhen the book omits it
input Ordinary input tokens. Required.
output Ordinary completion tokens. Required.
cache_read Tokens served from a prompt cache. Charged at input.
cache_write Tokens written into a prompt cache. Charged at input.
cache_write_5m Writes to a five-minute cache, where the provider prices TTLs apart. Falls back to cache_write, then input.
cache_write_1h Writes to a one-hour cache. Falls back to cache_write, then input.
audio_input Audio input tokens. Charged at input.
audio_output Audio output tokens. Charged at output.
reasoning Hidden reasoning tokens, where a provider bills them apart. No fallback. They are charged at output as part of the output total, which is how the major providers bill them, unless the provider also reported a separate text count. See below.

A cached token is charged once

The classic way to overstate a bill is to charge a cached token twice: once at the cache rate, and again as part of the reported input total it is already inside. So when a provider reports a breakdown, that breakdown wins. mcpd prices each meter at its own rate and charges only the remainder at the plain input rate.

A claude-sonnet-4 call reporting one million input tokens, of which nine hundred thousand were read from cache, prices against the built-in book like this:

  • 900,000 cache reads at $0.30 per million, which is $0.27.
  • 100,000 remaining input tokens at $3.00 per million, which is $0.30.
  • $0.57 of input cost, rather than the $3.00 the reported total on its own would have produced.

Where a provider gives an explicit text count, that count is used as the remainder directly. Where it does not, the remainder is the reported total minus every metered field.

That leaves one gap worth naming. A meter the book holds no rate for is charged only as part of the remainder, so when the provider also sent a text count it is not charged at all. Reasoning tokens sit there whenever your book gives them no rate of their own, and declaring one fixes it. Image tokens sit there always, because a book has no image rate to give, so on a heavily multimodal workload read the figure as low rather than exact.

An unknown model has no cost, not a cost of zero

If the book holds no rates for the model, mcpd stores the span with no cost field at all. It does not store a zero.

Zero is a claim: this call was free. Absent is an admission: we do not know what this call cost. They lead to different decisions, and a tool that reports the second as the first is lying about a bill. The same rule runs through the read path, where a run whose spans total nothing is returned with no cost figure rather than a cost of zero, so a blank cost column means not priced and never free.

The practical consequence is worth internalising: a model missing from the book is missing from every total. Its spend does not appear in /v1/api/analytics/cost, and its runs are excluded by any positive min_cost filter. A weekly total that looks too low is usually a model id that is not resolving, not a quiet week.

Resolving a model id

The id on a span is whatever your client sent, and clients decorate it. A gateway prefixes its vendor, a cloud prefixes its region, a provider pins a release date. A book is keyed on bare model names, so lookup tries four things in order and stops at the first hit.

StepExample
The id as given, case folded GPT-4o finds gpt-4o
The part after the last slash openai/gpt-4o finds gpt-4o
The dotted tail of a cloud id us.anthropic.claude-sonnet-4 finds claude-sonnet-4
The id with a trailing date removed, with or without its prefix openai/gpt-4o-2024-08-06 finds gpt-4o

Both -YYYY-MM-DD and -YYYYMMDD suffixes are recognised. Failing all four steps is a normal outcome rather than an error: it means no cost, as above.

The book that ships

mcpd carries a small built-in book so that docker run produces real figures with no configuration at all. It is called builtin, it prices in USD, and every cost it produces records that name.

ModelInputOutputCache readCache write, 5 minCache write, 1 hour
claude-opus-415.0075.001.5018.7530.00
claude-opus-4-115.0075.001.5018.7530.00
claude-opus-4-55.0025.000.506.2510.00
claude-sonnet-43.0015.000.303.756.00
claude-sonnet-4-53.0015.000.303.756.00
claude-haiku-4-51.005.000.101.252.00
claude-3-5-haiku0.804.000.081.001.60
gpt-4o2.5010.001.25at inputat input
gpt-4o-mini0.150.600.075at inputat input
gpt-4.12.008.000.50at inputat input
gpt-4.1-mini0.401.600.10at inputat input
o32.008.000.50at inputat input
o4-mini1.104.400.275at inputat input
gemini-2.5-pro1.2510.000.31at inputat input
gemini-2.5-flash0.302.500.075at inputat input
text-embedding-3-small0.020.00at inputat inputat input
text-embedding-3-large0.130.00at inputat inputat input

USD per million tokens, as the book stood when it was last checked against a third-party model catalogue, which the book records as its as_of date. Anthropic's entries carry both cache write rates because the daemon falls back from the one-hour rate to the five-minute one, and a book that omitted the longer one would under-bill in silence. OpenAI charges nothing to write to its cache, so a cache write there is billed at the input rate. gemini-2.5-flash also carries an audio input rate of 1.00; every other entry prices audio at its text rate. There is no claude-haiku-4: the id Anthropic serves is claude-haiku-4-5, and an id that resolves to nothing is left unpriced rather than guessed.

These are published list prices, not your prices. If you have negotiated rates, or you run a model that is not in this table, the figures mcpd shows you are an estimate and should be read as one. The book is deliberately short: a long one would go stale in more places and give a stronger impression of accuracy than it deserves.

You cannot supply your own book. There is no flag, no config file key and no endpoint that loads one: every deployment prices with the built-in table above, and a model that table does not name stays unpriced. Negotiated rates, a model you host yourself and a currency other than US dollars are all out of reach today. That is a gap rather than a decision, and until it is closed the honest reading of every figure on this page is that it is an estimate at list prices.

What does exist is the shape a book has, which is worth knowing while you weigh whether these figures are close enough for what you need. A book is an id, an ISO 4217 currency and a map of model id to rates. Every figure is stamped with the id of the book that produced it, so two books can be told apart after the fact, and the rates in one project would have to share one currency: mcpd sums a run's spans arithmetically and converts between none of them.

Reading the money back

GET /v1/api/analytics/cost answers where the money went over a window, without exporting anything to a spreadsheet.

curl -s 'http://localhost:4317/v1/api/analytics/cost?from=-7d&group_by=model'
ParameterMeaning
from, to The window. RFC 3339 or the relative shorthand. Defaults to the last seven days, ending now.
group_by model by default. Also provider, entity_name, environment, status, session_id, user_id, service_name or name.
bucket 30s, 5m, 1h or 1d. Omit it for totals only; supply it to get a time series alongside them.
project Which project to read. Defaults to the daemon's default project.

where it went

# Where the money went last week, by model.
curl -s --get http://localhost:4317/v1/api/analytics/cost \
  --data-urlencode 'from=-7d' \
  --data-urlencode 'group_by=model'

# The same week as a daily series, so you can see the day it changed.
curl -s --get http://localhost:4317/v1/api/analytics/cost \
  --data-urlencode 'from=-7d' \
  --data-urlencode 'group_by=model' \
  --data-urlencode 'bucket=1d'

Groups come back largest first, capped at a hundred rows, and a bucketed series is capped at five thousand points.

response

{
  "from": "2026-08-20T09:00:00Z",
  "to": "2026-08-27T09:00:00Z",
  "currency": "USD",
  "total_cost": 12.418264,
  "groups": [
    {
      "key": "claude-sonnet-4",
      "count": 1841,
      "error_count": 12,
      "total_cost": 9.612055,
      "input_tokens": 2903118,
      "output_tokens": 214477,
      "latency": { "mean": 1840.2, "max": 24193.0 }
    },
    {
      "key": "gpt-4o-mini",
      "count": 6204,
      "error_count": 3,
      "total_cost": 2.806209,
      "input_tokens": 9114882,
      "output_tokens": 388310,
      "latency": { "mean": 612.4, "max": 8801.0 }
    }
  ]
}

One subtlety in count. Grouping by model, provider or entity_name reads the spans, so count is a number of model calls. Grouping by anything else reads the run projection, so count is a number of runs. Both are useful; they are not the same number, and comparing one against the other is the easiest mistake to make here.

The per-group latency in this response carries a mean and a maximum only. Percentiles come from GET /v1/api/analytics/summary, which computes them over the window.

Where to go next

Cost is one column in a run list, and the run list is where an investigation actually starts: Finding a run covers min_cost and sorting by spend. If your figures look wrong because payloads are being dropped before they are priced, check Sampling, which decides what is recorded at all. Every field named on this page is listed in the data model.