Single-agent cost attribution is a solved problem. Attach a customer and a feature to the run, record what it cost, and every awkward question becomes a query. We have written that up more than once.
Multi-agent systems break it in a way that is not obvious until your numbers stop reconciling.
The shape of the problem
Say a customer request hits an orchestrator. The orchestrator calls a research agent, which calls a summarisation agent twice, and then calls a drafting agent. Four agents, six model calls, one customer request.
Now: what did that request cost?
The spend physically happened inside the research, summarisation, and drafting agents. But the reason it happened lives in the orchestrator, and the thing you bill for is the customer request. If you attribute cost only to the agent that made the call, you can tell which components are expensive but not which workflows or customers are. If you attribute only to the top-level request, you know what customers cost but have no idea which component to fix when one gets expensive.
Teams usually pick one, discover the other question a month later, and find the data to answer it was never recorded.
Why the totals stop adding up
Three ways per-agent totals drift from the provider invoice, and it is worth knowing which one you have because the fixes are unrelated.
Shared sub-agents get double counted. The summarisation agent is called by three different workflows. If each workflow's cost report includes the summarisation spend it triggered, summing the three workflows counts that agent's spend once per caller. Nobody notices until someone adds up the workflow reports and gets a number larger than the bill.
Failed runs vanish. A sub-agent that threw partway through still consumed input tokens and possibly output tokens, and the provider charged for them. If instrumentation only emits on success, that spend exists on the invoice and nowhere in your data. On a system with a meaningful error rate this is a persistent gap that grows exactly when things are going badly.
Cache reads are billed differently. Cached input tokens cost a fraction of uncached ones. A cost calculation that multiplies total input tokens by the standard rate will overstate spend, sometimes substantially, on agents with long stable system prompts. In a multi-agent system the orchestrator's prompt is usually the most cached thing you have, so this hits the component you are most likely to be scrutinising. Prompt caching goes into where the discount actually applies.
The pattern that works
Two identifiers on every run, and cost recorded once.
import uuid, contextvars
root_run = contextvars.ContextVar("root_run", default=None)
def run_agent(name, payload, customer_id):
root = root_run.get() or str(uuid.uuid4())
token = root_run.set(root)
started = time.time()
outcome = "success"
try:
result = agents[name](payload)
return result
except Exception:
outcome = "error"
raise
finally:
emit({
"agent": name,
"root_run_id": root, # the customer request this belongs to
"parent_agent": current_agent.get(),
"customer_id": customer_id,
"outcome": outcome,
"cost": cost_of(result if outcome == "success" else partial_usage()),
})
root_run.reset(token)
The root_run_id is the whole trick. It is created by whichever agent starts first and inherited by everything downstream, so every event knows both what spent the money and which customer request caused it. Context variables handle the propagation cleanly in async code, where a global would give you the wrong answer under concurrency.
parent_agent is a smaller addition that pays off when you have shared sub-agents, because it lets you ask which caller is responsible for the summarisation agent's growth rather than just noting that it grew.
With those two fields, both questions become ordinary queries. Group by agent for component cost. Group by root_run_id for request cost. Group by customer_id for margin. No double counting, because each unit of spend is stored exactly once and the rollups are computed rather than written.
Cost per root run is the metric
Once the data has the right shape, the number worth putting on a dashboard is cost per root run, split by workflow.
Total cost per agent moves with volume, which means it goes up when business is good and it hides efficiency regressions inside growth. Cost per root run holds still when volume changes and moves when something about the work changed, which is exactly the sensitivity you want from an alerting metric.
It also localises quickly. If the research workflow's cost per root run steps from 0.14 to 0.31 on a Tuesday, grouping that workflow's runs by agent for the days either side will normally name the component in a couple of minutes. Usually it is a sub-agent being called more times per request rather than each call getting more expensive, which is a distinction that only exists if you kept both dimensions.
The failure this catches
The one I would most like people to avoid is the recursive call that is not quite a loop.
An orchestrator calls a research agent. Under some inputs the research agent decides it needs clarification and calls back into the orchestrator. That is not infinite, and it terminates, so nothing errors and no timeout fires. It just means a small slice of requests do four times the work.
Per-agent totals show both agents up slightly, which reads like growth. Cost per root run for that workflow shows a bimodal distribution with a second cluster at four times the cost, which reads like a bug, because it is one. Same underlying data, and only one of the two views makes it visible.
Multi-agent systems are worth the complexity when the decomposition is real. They just move cost attribution from something you get for free to something you have to design, and the design is two identifiers and an emit in a finally block. AgentPing takes root_run_id and parent_agent as first-class dimensions on the spend side, so the rollups are queries rather than a pipeline you maintain. Free on one workflow if you want to see what your own cost-per-request distribution looks like.