โ† Back to Blog
AI EngineeringAI for PMsLLMs

AI Agent Cost Attribution: Metering and Chargeback in Production

Your LLM bill is one number. Your finance team wants twelve. Here's how to meter agent spend per user, feature, and team โ€” and turn it into a chargeback model that survives a budget review.

By Joinloopยท21 September 2026ยท7 min read
ShareLinkedInXReddit
๐Ÿ’ณ

Somewhere around the third production agent, someone in finance asks: "which feature is driving the $40K LLM line item?" If your answer is "the OpenAI/Anthropic dashboard shows total spend," you don't have an answer. You have a bill.

Cost attribution is the practice of tagging every model call with enough metadata to reconstruct spend by dimension โ€” user, tenant, feature, agent, environment โ€” after the fact. Chargeback is what you do with that data: allocate it back to the teams or product lines that generated it, so cost becomes a input to product decisions instead of a surprise at month-end.

This matters more for agents than for a single chat endpoint because agents multiply calls. A single user request to a coding agent might trigger 15 tool calls, 3 sub-agent spawns, and 2 retries after a malformed tool-use response. Without per-call tagging, that entire cascade collapses into one undifferentiated number.

Why the provider dashboard isn't enough

Anthropic Console, OpenAI's usage dashboard, and most LLM gateways give you spend by API key and model. That's fine if you have one API key per feature. Almost nobody does โ€” you have one key per environment (prod/staging), shared across every agent and every customer.

The dashboard also can't see your business dimensions:

  • Which customer tenant generated this cost โ€” critical for usage-based pricing or margin analysis per account
  • Which feature or agent made the call โ€” a support-ticket triage agent and a code-review agent sharing one key look identical from the provider's side
  • Which step in a workflow โ€” was this the cheap classification call or the expensive final-answer call
  • Whether the call was a retry โ€” retries from malformed JSON or rate limits are pure waste, and they hide inside "total tokens" unless you flag them

You need this metadata at the point of the call, not reconstructed later from logs that don't have it.

The pattern: a metering middleware

Wrap every LLM call โ€” not the SDK, the call site โ€” in a thin layer that records cost alongside a tagged context. This works whether you're calling the API directly or going through a gateway like LiteLLM or Portkey.

// metering.ts
interface CallContext {
  tenantId: string;
  feature: string;      // e.g. "ticket-triage", "code-review-agent"
  agentStep?: string;    // e.g. "classify", "retrieve", "generate"
  userId?: string;
  isRetry?: boolean;
}

interface MeteredResult<T> {
  result: T;
  costUsd: number;
  inputTokens: number;
  outputTokens: number;
}

const PRICING: Record<string, { input: number; output: number }> = {
  "claude-sonnet-5": { input: 3.00, output: 15.00 },   // per 1M tokens
  "claude-haiku-4-5": { input: 0.80, output: 4.00 },
};

export async function meteredCall<T>(
  ctx: CallContext,
  model: string,
  fn: () => Promise<{ output: T; usage: { input_tokens: number; output_tokens: number } }>
): Promise<MeteredResult<T>> {
  const start = Date.now();
  const { output, usage } = await fn();
  const price = PRICING[model];
  const costUsd =
    (usage.input_tokens / 1_000_000) * price.input +
    (usage.output_tokens / 1_000_000) * price.output;

  await recordUsageEvent({
    ...ctx,
    model,
    costUsd,
    inputTokens: usage.input_tokens,
    outputTokens: usage.output_tokens,
    latencyMs: Date.now() - start,
    timestamp: new Date().toISOString(),
  });

  return { result: output, costUsd, inputTokens: usage.input_tokens, outputTokens: usage.output_tokens };
}

recordUsageEvent writes one row per call to an append-only table (ClickHouse, BigQuery, or even Postgres at moderate volume). Every downstream report โ€” daily spend by tenant, weekly spend by feature, retry-cost audit โ€” is a query against that table, not a re-derivation from provider logs.

Call sites become explicit about what they're spending on:

const { result } = await meteredCall(
  { tenantId: ticket.tenantId, feature: "ticket-triage", agentStep: "classify" },
  "claude-haiku-4-5",
  () => anthropic.messages.create({ model: "claude-haiku-4-5", ... })
);

The overhead is one wrapped function call. The payoff is a queryable ledger from day one, instead of a scramble to instrument everything after finance asks the question.

Attributing multi-step agent runs

A single user-facing request in an agent system is a tree, not a line. Tag every call in the tree with a shared runId so you can roll up cost per logical unit of work, not just per raw call:

const runId = crypto.randomUUID();

await meteredCall({ tenantId, feature: "code-review-agent", agentStep: "plan", runId }, ...);
await meteredCall({ tenantId, feature: "code-review-agent", agentStep: "retrieve-diff", runId }, ...);
await meteredCall({ tenantId, feature: "code-review-agent", agentStep: "generate-comments", runId }, ...);

Now SUM(costUsd) WHERE runId = X gives you the true cost of one review, including every sub-call. This is the number that should show up next to "average revenue per review" when someone asks whether the feature is worth running.

Chargeback models

Once you have tagged spend, you have to decide how to allocate it. Three common approaches, in order of complexity:

Model How it works Best for
Direct pass-through Each team's spend is billed to their own cost center, dollar for dollar Clear feature ownership, internal teams that control their own agent prompts
Shared-pool with usage weighting One central LLM budget, allocated proportionally by measured usage at month-end Shared infrastructure (e.g. one RAG pipeline serving multiple products)
Fixed allocation + overage billing Each team gets a budget; usage under it is free, overage is billed at marginal cost Encourages efficient prompting without micromanaging every call

Most orgs start with shared-pool because it's the least invasive, then move to direct pass-through once a feature's cost becomes large enough that its owning team needs the incentive to optimize it. Fixed allocation is worth adopting once you have 3+ teams competing for the same model quota, since it turns "please stop burning our rate limit" into a number instead of a Slack argument.

What to track beyond raw dollars

Cost attribution done well answers questions finance doesn't know to ask yet:

  • Cost per successful outcome, not cost per call. If your support agent costs $0.40/ticket but only resolves 60% without escalation, your real cost per resolution is $0.67. Track resolution alongside spend.
  • Retry and error cost. Tag isRetry: true on every retried call. A 5% malformed-tool-call rate on a workflow with expensive final-generation steps can be 15-20% of that workflow's total bill.
  • Cache hit savings. If you're using prompt caching, log the counterfactual cost (what it would have cost without the cache hit) alongside the actual cost, so the savings show up as a positive number someone can point to, not an invisible absence of spend.
  • Cost per model tier. If you route cheap classification to Haiku and expensive generation to Sonnet, report them as separate line items. A blended average hides whether your routing logic is actually working.

Common pitfalls

  • Metering after the gateway instead of before it. If you only log at the provider/gateway level, you lose your own business context (tenant, feature, step). Meter at the call site, in your own code.
  • Trusting client-reported token counts. Some SDKs estimate tokens before the call and don't reconcile with the actual usage object in the response. Always read usage.input_tokens / usage.output_tokens from the API response, never from a pre-call estimate.
  • Ignoring cached tokens in pricing. Cached input tokens are billed at a different (usually ~10%) rate. If your cost calculation treats all input tokens the same, you'll overstate cost on cache-heavy workloads and undersell the ROI of caching to stakeholders.
  • No sampling plan for high-volume, low-value calls. If you have a classification step running millions of times a day, don't store every raw prompt/response for cost auditing โ€” store the metered numbers, and sample full payloads for debugging separately. Storage costs compound the exact problem you're trying to control.
  • Treating attribution as a one-time project. New agents and features ship without the metering wrapper because someone forgot, or thought it was optional. Make meteredCall (or your gateway's equivalent) part of the PR template checklist for any new LLM call site, not a retrofit.

Next steps

Start small: wrap your highest-volume agent's calls this week, tag them with feature and tenantId, and ship a single dashboard query โ€” cost by feature, by day. That one query is usually enough to settle the first budget conversation. Chargeback models and cache-savings reporting can wait until the ledger itself is trustworthy.

If you're building or hardening production AI agents and want a structured, hands-on path through metering, evals, and the rest of the operational surface area, Joinloop's AI Engineer cohort covers this end to end.

ShareLinkedInXReddit

Want to build this yourself?

Join a live cohort and go from reading about AI to shipping it, with practitioners who do it daily.