Contents · 18 sections↓
- 01How do you track the cost of an AI agent?
- 02How AI agent costs add up
- 03What should you track?
- 04What do LLM tokens cost?
- 05When to build your own cost tracking (and when not to)
- 06What you need
- 07Step 1: Write the price table and the cost of one call
- 08Step 2: Feed every model call into the ledger
- 09Step 3: Attribute each run to a user
- 10Step 4: Run it and read the numbers
- 11Step 5: See cost while you develop
- 12Step 6: Cap the cost of a single run
- 13The complete example
- 14Running it in production
- 15Common problems and fixes
- 16Limitations
- 17Frequently asked questions
- 18Next steps
AI agent cost tracking answers a question your provider's invoice can't: which run, user, agent or feature spent the money. This guide is for developers running agents for real users who need that answer before the bill arrives. You will learn how agent costs add up, what to record for every model call, how to roll it up per run, per user and per agent, and how to add alerts and limits. The implementation uses Promptise Foundry, and every output below comes from running the code.
How do you track the cost of an AI agent?
Record the input and output tokens of every model call, together with the model name, the run it belongs to and the user who started it. Multiply the tokens by your price table to get a cost per call. Then add the calls up per run, per user, per agent and per day. Store those totals where you can query them, compare them with a limit before each new run, and alert when a total crosses a threshold.
Count per model call, not per request. One agent request makes several model calls, and each one resends the growing conversation.
Attribute at the source. Tag every call with a run id and a user id when it happens. You can't split an invoice afterwards.
Keep your own price table. Providers report dated model names and change prices, so match by pattern and review the table when prices change.
Separate alerts from limits. An alert tells you a user is expensive. A limit refuses the next run. You usually want both.
Reconcile with the invoice. Token-based estimates miss cache discounts, batch pricing and tool costs. Use them for attribution and limits, and the invoice for accounting.
[02]
How AI agent costs add up
A chat completion has one cost: the prompt tokens times the input price, plus the response tokens times the output price. An agent multiplies that. Each request runs a loop, and every turn of the loop is a full model call that includes the system prompt, the tool schemas, the conversation so far and every tool result:
request "Is TENT-2P in stock, and what about order A1001?"
│
├─ model call 1 input: system + tools + question 211 tokens
│ output: two tool calls 124 tokens
├─ tools run check_stock, get_order (no model tokens, may have their own cost)
└─ model call 2 input: everything above + both tool results 331 tokens
output: the answer 177 tokens
─────────
cost of the run = Σ (input tokens × input price + output tokens × output price)Four things make agent cost hard to predict:
Input grows with every turn. The second call above carries everything the first one did, plus the tool results. A run with ten tool calls can resend the same system prompt ten times.
Output usually costs more per token. For the models in the table below, the output price is five to eight times the input price. Reasoning models also bill their hidden reasoning as output tokens, so a short answer can carry hundreds of output tokens.
Some input is discounted. Providers charge less for cached prompt prefixes and batch requests. An estimate that prices every input token at the full rate is an upper bound.
Tools have their own costs. A search API, a geocoder or a payment fee is billed by someone else. Model tokens don't include it.
[03]
What should you track?
Level | What it answers | Where the number comes from |
|---|---|---|
---|---|--- | ||
Per model call | Which step is expensive? Which model? | The usage the provider returns with each call |
Per run (one request) | What does one answer cost? Which requests loop? | The sum of its calls |
Per user or tenant | Who drives the bill? Who should be limited? | The runs attributed to that caller |
Per agent or feature | Is the triage agent cheaper than the support agent? | The runs of each agent |
Per day or month | Are we on budget? | All runs in the period, from your store |
Record the call level, and compute everything else from it. If you only keep totals, you can't answer the next question someone asks.
[04]
What do LLM tokens cost?
Prices are set per million tokens and change several times a year. These are list prices for standard text tokens on 2026-10-18, from the OpenAI pricing page and the Anthropic pricing page. Check the pages before you copy a number:
Model | Input (USD / 1M) | Cached input | Output (USD / 1M) |
|---|---|---|---|
---|---|---|--- | |||
gpt-5-nano | 0.05 | 0.005 | 0.40 |
gpt-5-mini | 0.25 | 0.025 | 2.00 |
gpt-5.4-mini | 0.75 | 0.075 | 4.50 |
gpt-5.5 | 5.00 | 0.50 | 30.00 |
Claude Haiku 5.5 (prompts up to 100k tokens) | 0.10 | 0.01 | 0.50 |
Claude Sonnet 5.5 | 2.00 | 0.10 | 10.00 |
Claude Opus 5.5 | 4.00 | 0.20 | 20.00 |
Two details matter for tracking. Providers report dated model names (gpt-5-mini-2025-08-07), so match your table by prefix or glob. And some models change price above a prompt length, like Claude Haiku 5.5 above 100,000 tokens, so a single rate per model is an estimate for long prompts.
[05]
When to build your own cost tracking (and when not to)
The provider's usage dashboard shows spend per API key and per model, which is enough for a single internal tool. An LLM gateway or proxy sees every call from every service and can meter them centrally. Build tracking into the agent when you need what only the application knows: which end user, tenant, conversation or feature a call belongs to, and the ability to refuse a run before it spends anything. Most production agents end up with the dashboard for accounting and in-app tracking for attribution and limits.
[06]
What you need
Python 3.10 or later and Promptise Foundry 1.4.1 or later: pip install "promptise>=1.4.1".
An OpenAI API key in OPENAI_API_KEY. The example uses gpt-5-mini, and the whole guide costs well under a cent to run.
A small shop MCP server, server.py, with two read-only tools (get_order, check_stock). It is in the example files, and any tools work the same way.
[07]
Step 1: Write the price table and the cost of one call
Start with the part every tracking system needs, whatever the framework: prices per million tokens and a function that prices one call.
"""Token cost tracking: price table, per-call cost, and a ledger per run, user and agent."""
import logging
from collections import defaultdict
from fnmatch import fnmatchcase
log = logging.getLogger("costs")
# USD per 1 million tokens: (input, output). Check your provider's pricing page:
# these change, and providers report dated model names (gpt-5-mini-2025-08-07).
PRICES = {
"gpt-5-mini*": (0.25, 2.00),
"gpt-5-nano*": (0.05, 0.40),
}
def call_cost(model: str, prompt_tokens: int, completion_tokens: int) -> float | None:
"""Estimated USD cost of one model call, or None when the model has no price."""
for pattern, (input_price, output_price) in PRICES.items():
if fnmatchcase(model, pattern):
return (prompt_tokens * input_price + completion_tokens * output_price) / 1_000_000
return None
class CostLedger:
"""Folds observability events into cost totals per run, per user and per agent."""
def __init__(self, user_budget_usd: float) -> None:
self.user_budget_usd = user_budget_usd
self.by_run: dict[str, float] = defaultdict(float)
self.by_user: dict[str, float] = defaultdict(float)
self.by_agent: dict[str, float] = defaultdict(float)
self.unpriced: set[str] = set()
self._alerted: set[str] = set()
def on_event(self, entry) -> None:
"""Called for every timeline event; only llm.end carries token usage."""
if entry.event_type.value != "llm.end":
return
meta = entry.metadata
model = meta.get("model", "unknown")
cost = call_cost(model, meta.get("prompt_tokens", 0), meta.get("completion_tokens", 0))
if cost is None:
self.unpriced.add(model)
return
user = entry.user_id or "anonymous"
self.by_run[entry.parent_id or "no-run"] += cost
self.by_user[user] += cost
self.by_agent[entry.agent_id or "agent"] += cost
if self.by_user[user] > self.user_budget_usd and user not in self._alerted:
self._alerted.add(user)
log.warning("user %s is over budget: $%.6f > $%.6f", user, self.by_user[user], self.user_budget_usd)call_cost returns None for a model without a price instead of guessing, and the ledger records such models in unpriced, so a new model shows up as a gap rather than as zero spend. The CostLedger adds each priced call to three totals: the run (parent_id), the user and the agent. It also logs one warning the first time a user crosses user_budget_usd.
[08]
Step 2: Feed every model call into the ledger
Promptise records an llm.end event for every model call when observability is on. Its metadata carries model, prompt_tokens and completion_tokens, and the event itself carries agent_id, user_id and parent_id (the run it belongs to). A callback transporter hands each event to the ledger as it happens:
"""The support agent, built by a factory so main.py and promptise run share it."""
import sys
from promptise import PromptiseAgent, build_agent
from promptise.config import StdioServerSpec
from promptise.observability_config import ObservabilityConfig, ObserveLevel, TransporterType
from costs import CostLedger
LEDGER = CostLedger(user_budget_usd=0.001)
async def create_agent(ctx=None) -> PromptiseAgent:
return await build_agent(
model="openai:gpt-5-mini",
instructions="You answer shop customers. Use the tools for facts. Keep answers short.",
servers={"shop": StdioServerSpec(command=sys.executable, args=["server.py"])},
observer_agent_id="support",
observe=ObservabilityConfig(
level=ObserveLevel.STANDARD, # one llm.end event per model call
transporters=[TransporterType.CALLBACK],
on_event=LEDGER.on_event,
),
)ObserveLevel.STANDARD is the level that records one event per model call. BASIC only records the run totals, which is enough for cost per run but not for cost per model or per step. observer_agent_id names the agent in every event, which is what the per-agent totals group by. The factory pattern (create_agent) lets the same agent run from main.py, from tests and from promptise run.
[09]
Step 3: Attribute each run to a user
Pass the caller with each request. Promptise stores it in a context variable for the duration of the run, and every event recorded during the run carries its user_id:
"""Answer a few customer questions and report what each run, user and agent cost."""
import asyncio
import logging
from promptise import CallerContext
from agent import LEDGER, create_agent
from costs import call_cost
logging.basicConfig(level=logging.WARNING, format="%(levelname)s %(name)s: %(message)s")
REQUESTS = [
("alice", "Where is my order A1002?"),
("bob", "Do you have STOVE-MINI in stock?"),
("alice", "Is TENT-2P in stock, and what about my order A1001?"),
("alice", "And is STOVE-MINI back in stock?"),
]
DAILY_LIMIT_USD = 0.0012 # per user; tiny on purpose, so the example hits it
async def main() -> None:
agent = await create_agent()
try:
for user, question in REQUESTS:
if LEDGER.by_user[user] >= DAILY_LIMIT_USD: # check before you spend
print(f"refused {user}: ${LEDGER.by_user[user]:.6f} spent today")
continue
await agent.ainvoke(
{"messages": [{"role": "user", "content": question}]},
caller=CallerContext(user_id=user), # attributes every event of the run
)
print("per call:")
for e in agent.collector.query():
if e.event_type.value == "llm.end":
m = e.metadata
cost = call_cost(m["model"], m["prompt_tokens"], m["completion_tokens"])
print(f" {e.user_id:<6} {m['model']} in={m['prompt_tokens']:>4} "
f"out={m['completion_tokens']:>4} ${cost:.6f}")
print("per run: ", {run[:8]: round(c, 6) for run, c in LEDGER.by_run.items()})
print("per user: ", {u: round(c, 6) for u, c in LEDGER.by_user.items()})
print("per agent:", {a: round(c, 6) for a, c in LEDGER.by_agent.items()})
stats = agent.get_stats()
print("get_stats:", {k: stats[k] for k in
("llm_call_count", "total_prompt_tokens", "total_completion_tokens", "total_tokens")})
finally:
await agent.shutdown()
if __name__ == "__main__":
asyncio.run(main())The loop also shows the simplest hard limit: check the user's spend before the run, and refuse when it is over. Checking first is the point. A limit checked after the run lets the expensive run happen.
[10]
Step 4: Run it and read the numbers
python main.pyWARNING costs: user alice is over budget: $0.001076 > $0.001000
refused alice: $0.001512 spent today
per call:
alice gpt-5-mini-2025-08-07 in= 201 out= 90 $0.000230
alice gpt-5-mini-2025-08-07 in= 267 out= 239 $0.000545
bob gpt-5-mini-2025-08-07 in= 203 out= 26 $0.000103
bob gpt-5-mini-2025-08-07 in= 249 out= 369 $0.000800
alice gpt-5-mini-2025-08-07 in= 211 out= 124 $0.000301
alice gpt-5-mini-2025-08-07 in= 331 out= 177 $0.000437
per run: {'fc8409b6': 0.000775, '560a0960': 0.000903, '7a897213': 0.000738}
per user: {'alice': 0.001512, 'bob': 0.000903}
per agent: {'support': 0.002415}
get_stats: {'llm_call_count': 6, 'total_prompt_tokens': 1462, 'total_completion_tokens': 1025, 'total_tokens': 2487}How to read it:
Per call: every run made two model calls, and the second always had more input (267 vs 201 tokens, 331 vs 211) because it carried the tool results. Output was the larger part of the cost on every call, often with fewer output tokens than input tokens, because gpt-5-mini charges eight times more per output token.
The alert and the limit: alice crossed the $0.001 alert threshold during her second run, so the ledger logged a warning. Her third request was refused before it started, because her total ($0.001512) was over the $0.0012 limit. Bob was never limited.
Per user and per agent: the per-user totals add up to the agent total ($0.001512 + $0.000903 = $0.002415).
Cross-check: get_stats() counts 1,462 input and 1,025 output tokens. At $0.25 and $2.00 per million, that is $0.000366 + $0.00205 = $0.002416, the same as the ledger apart from rounding. When the two disagree, a model is missing from your price table.
[11]
Step 5: See cost while you develop
You don't need the ledger to see what a single run costs. Put your prices in the project's promptise.toml, and the dev tools show tokens and cost for every run:
name = "support-bot"
main = "main.py"
agent = "agent.py:create_agent"
[env]
required = ["OPENAI_API_KEY"]
[pricing]
"gpt-5-mini*" = { input = 0.25, output = 2.00 }promptise run --devtools --report run.json -i "Where is my order A1002?"promptise run support-bot · agent.py:create_agent · once · devtools on (observe
full)
● support model openai:gpt-5-mini 2 tool(s) 1 MCP server(s) observe standard
⇄ shop (stdio) /home/you/support-bot/… · 2 tool(s)
▸ support
◆ llm 1.46s in 201 out 26 tok → get_order
⚙ get_order({'order_id': 'A1002'})
✓ get_order 5ms → {"order_id": "A1002", "status": "in_transit", "carrier": "DHL", "eta": "2026-10-21"}
◆ llm 2.82s in 267 out 239 tok
◂ support done · 4.30s · 2 llm call(s) · 1 tool call(s) · 733 tok
■ support shut down
promptise run · exit 0 · 5.5s · 2 llm call(s) · 733 tok (in 468 out 265) · 1
tool call(s) · 0 error(s) · cost $0.0006
agent model llm calls tokens tool calls errors cost
support openai:gpt-5-mini 2 733 1 0 $0.0006
tool calls errors avg ms
get_order 1 0 5
report: run.jsonEach ◆ llm line is one model call with its input and output tokens. The summary prices the run with [pricing] and breaks it down per agent and per tool. [pricing] keys are matched by exact name, then without the provider: prefix, then as globs, and a model with no entry shows — and is listed as unpriced. Promptise ships no built-in price list on purpose, because a stale price is worse than none.
--report writes the same numbers as JSON, which a CI job can check:
jq '.stats.totals | {llm_calls, prompt_tokens, completion_tokens, cost_usd, unpriced_models}' run.json{
"llm_calls": 2,
"prompt_tokens": 468,
"completion_tokens": 265,
"cost_usd": 0.000647,
"unpriced_models": []
}A CI step that runs your eval set with --report and fails when cost_usd grows by more than, say, 20% catches a prompt change that doubles the token count before it ships.
[12]
Step 6: Cap the cost of a single run
The per-user check in Step 3 stops the next run. To stop a run that is already too expensive, such as an agent stuck calling tools, cap its tokens. BudgetHook adds up the tokens of each run and ends the run when it goes over:
import asyncio
from promptise.engine.hooks import BudgetHook
from agent import create_agent
async def main():
agent = await create_agent()
agent.hooks.append(BudgetHook(max_tokens=300))
try:
r = await agent.ainvoke({"messages": [{"role": "user", "content": "Is TENT-2P in stock, and what about my order A1001?"}]})
print("answer:", r["messages"][-1].content)
except Exception as e:
print(type(e).__name__, e)
finally:
await agent.shutdown()
asyncio.run(main())Token budget exceeded: 335 > 300
GraphBudgetExceededError Graph 'react' failed at node 'reason': Token budget exceeded: 335/300 tokens; BudgetHook stopped the run before the model gave an answer, after a call to check_stock, get_order whose result the model never sawThe limit is tiny here so that it fires. A real cap sits at a few times your normal run (look at your per-run numbers from Step 4), high enough that ordinary requests never hit it. When it fires before the model answered, the run raises GraphBudgetExceededError, so catch it and return a clear message. BudgetHook also takes max_cost_usd with a single blended cost_per_1k_tokens rate, which is coarser than the per-model table above.
For long-running agents, Promptise's runtime has a separate BudgetConfig with limits on tool calls, LLM turns and irreversible actions per run and per day. Its max_cost_per_run and max_cost_per_day count abstract units you assign to tools, not dollars, so they control what the agent does rather than what the model costs. AI agent cost control covers those limits and runaway loops in depth.
[13]
The complete example
support-bot/
├── promptise.toml # [pricing] for the dev tools and run reports
├── server.py # the shop MCP server (two read-only tools)
├── costs.py # price table, call_cost(), CostLedger
├── agent.py # agent factory; feeds llm.end events to the ledger
├── main.py # four requests, per-user limit, per-call/run/user/agent report
└── hookcap.py # a per-run token cap with BudgetHookpip install "promptise>=1.4.1"
export OPENAI_API_KEY=sk-...
python main.py
promptise run --devtools --report run.json -i "Where is my order A1002?"[14]
Running it in production
Persist the ledger
The ledger above lives in memory and is gone when the process restarts, and two replicas each keep their own numbers. In production, write one row per model call (timestamp, run id, user, tenant, agent, model, input and output tokens, cost) to a database or your log pipeline, and compute the totals with queries. Then the per-user check before each run reads the store, not a dictionary. Redis with a daily key per user (INCRBYFLOAT cost:alice:2026-10-18) is a common choice for the hot limit, with the raw rows in your warehouse.
Use the numbers Promptise already logs
Two outputs carry usage without extra code. promptise run --prod writes a final stats log line with total_tokens and cost_usd for the process. In serve mode, every /v1/invoke response includes a usage object with the run's model calls and tokens, which a gateway can record per user. Run, debug and deploy an AI agent shows both.
Export metrics and alert from your monitoring
The PROMETHEUS transporter exposes token counters for Grafana, and the OTLP transporter puts gen_ai.usage.input_tokens and gen_ai.usage.output_tokens on every model-call span, with enduser.id from the caller. Alert on rate of spend (dollars per hour) as well as totals: a loop shows up as a spike long before it shows up in a daily total. AI agent observability sets up both.
Keep traces free of user data
Cost tracking needs counts, not content. Tool arguments are recorded by default because they make traces useful for debugging. When events leave your trust boundary, set record_tool_io=False. The token counts, model names and user ids are still recorded. Promptise also redacts API keys, card numbers and emails from events by default (redact_sensitive=True).
Track tenants as well as users
For B2B products, store the tenant with every cost row (you know it when you build the CallerContext, which also has a tenant_id field) and group by it, so you can see whether each customer's usage fits its plan. Observability events carry user_id and session_id, not the tenant, so add it when you write the row. Multi-tenant AI agents covers isolating tenants beyond cost.
[15]
Common problems and fixes
Symptom | Cause | Fix |
|---|---|---|
---|---|--- | ||
Cost shows — or None | The reported model name (gpt-5-mini-2025-08-07) has no entry in your price table | Match with a glob: "gpt-5-mini*" |
The estimate is higher than the invoice | Cached input and batch discounts are priced at the full rate | Treat the estimate as an upper bound, or price cached tokens separately from the provider's usage fields |
The estimate is lower than the invoice | Missing models, retries, embeddings or calls outside the agent | List unpriced models, and meter other model calls the same way |
No per-call events, only totals | Observability is at BASIC | Use ObserveLevel.STANDARD |
Every call shows user_id None | The request was invoked without a caller | Pass caller=CallerContext(user_id=...) to ainvoke |
The ledger stays at zero, and the logs show CallbackTransporter error | Your callback raised; Promptise logs the error and keeps the agent running | Fix the callback, and alert on that log line so cost data never goes missing silently |
A user goes far over the limit in one request | The limit is checked between runs only | Add a per-run token cap (BudgetHook) and a tool-call cap |
Totals reset on deploy, or differ per replica | The ledger lives in process memory | Write per-call rows to a shared store and check limits against it |
[16]
Limitations
The approach here estimates cost from tokens and your price table. It does not read the provider's billing API, so it can't see discounts, credits or price changes you haven't copied. Promptise's token events count prompt and completion tokens but don't split out cached input or reasoning tokens, and [pricing] takes one input and one output price per model. Tool and infrastructure costs aren't included. The ledger in this guide is in-process. For accounting, reconcile with the provider's usage export each month.
[17]
Frequently asked questions
How do I track token usage per user in an AI agent?
Attach the user to each request and record it with every model call. In Promptise, pass caller=CallerContext(user_id="alice") to ainvoke. Every observability event of that run, including each llm.end with its token counts, then carries user_id, so a callback or a query can sum tokens and cost per user.
How do I calculate the cost of an LLM call?
Multiply the prompt tokens by the input price and the completion tokens by the output price, using the price per million tokens, and divide by one million. For example, 331 input and 177 output tokens on gpt-5-mini cost (331 × 0.25 + 177 × 2.00) / 1,000,000 = $0.000437.
Why does my agent use more tokens than the conversation length suggests?
Every turn of the agent loop resends the system prompt, the tool schemas, the conversation and all earlier tool results. A request with five tool calls makes about six model calls, each larger than the last. Reasoning models also bill hidden reasoning tokens as output.
How do I set a cost limit for an AI agent?
Use two limits. Before each run, check the user's or tenant's spend for the period against a limit and refuse when it is over. During a run, cap tokens or tool calls so a single request can't run away. In Promptise, BudgetHook(max_tokens=...) caps tokens per run, and the runtime's BudgetConfig caps tool calls and turns.
Does get_stats() return the cost?
No. agent.get_stats() returns token counts (total_prompt_tokens, total_completion_tokens, total_tokens), call counts and latencies, but no dollar amount. Multiply the tokens by your prices yourself, or use promptise run with a [pricing] table, whose dev tools and run reports include cost_usd.
Should I use the provider dashboard or track cost in my app?
Both. The provider dashboard is the source of truth for what you pay. Only your application knows which user, tenant, conversation or feature a call belongs to, and only your application can refuse a run before it spends money. Use the dashboard for accounting and in-app tracking for attribution and limits.
[18]
Next steps
Stop runaway spend: AI agent cost control with budgets.
Find the step that loops: AI agent stuck in a loop.
Send token metrics to your monitoring: AI agent observability.
Cut cost with caching: Semantic cache and model fallback.
Watch cost per run while you develop: Run, debug and deploy an AI agent.
Reference: Observability docs and promptise run and pricing.