How to meter AI agent and MCP tool calls per customer
AI agents call your API and MCP tools on behalf of your customers, often many times per task and with retries. Those calls are real usage. Here's how to count each one exactly once, keep agent traffic in its own bucket, and get a per-customer total you can bill.
Agent and bot calls are real usage
An agent acting for a customer consumes the same compute, tokens and downstream API calls as a person clicking a button. Count it. Don't drop requests because the User-Agent looks like a bot, an AI crawler or a script: that undercounts usage and undercharges your heaviest customers.
- Never filter. Every tool call made for a customer becomes a usage event.
- Give it its own traffic class. Send agent calls to their own meter (
agent_tool_calls) next to human requests (human_requests). Usage Lab rolls up each meter per customer per month, so you get two clean totals to price, cap or report separately. - Record context as properties.
traffic_class, tool name, MCP server, run id, step and the agent's User-Agent are stored with each event for audit and CSV. Properties are not aggregated.
We do the same on our own API: requests from AI agents, bots and scripts are counted in their own class next to browser traffic, and never blocked.
Idempotency keys for agent retries
Agent frameworks retry: a step times out, the model re-plans, the MCP client reconnects. Every event needs a required idempotency key, and Usage Lab never generates one for you. Build it deterministically from the things that identify one tool call:
- run id: the agent run or task, from your orchestrator;
- step: the step number inside that run;
- tool call id: the id the model or MCP client gives the call (for example a
tool_call_idor JSON-RPC request id).
Hash run_id:step:tool_call_id (SHA-256 is fine) and send it as idempotency_key, up to 255 characters. A retry of the same call sends the same key and comes back as a duplicate, so it's counted once. A different tool call gets a different key, so it's counted. The same key with a different body returns 409 and keeps the original. Never use a random UUID made at send time: each retry would get a new one and count again. More in idempotent usage events.
The event
One agent tool call as a Usage Lab event (one item of POST /api/v1/usage {"events": [...]}). Not a command.
{
"meter": "agent_tool_calls",
"subscription": "cust_acme",
"idempotency_key": "tc_<sha256 of run_id:step:tool_call_id, 40 hex chars>",
"properties": {
"traffic_class": "ai_agent",
"tool": "get_invoice",
"mcp_server": "billing-mcp",
"run_id": "run_2026-10_7f3a",
"step": 2,
"agent": "ClaudeBot/1.0"
}
}
Use a count meter for calls. If you bill by tokens or tool cost instead, use a sum meter and send the amount as value. Batches take up to 100 events; late events are accepted up to 30 days back.
Per-customer rollups
Send your customer id as subscription. GET /api/v1/meters/agent_tool_calls/rollups?month=YYYY-MM returns each customer's quantity and event count for the month, recomputed from the raw events, so late events and retries are already handled. Push the totals to Stripe with Stripe push (beta, Pro and Ultra) or read them into your own billing.
Runnable sample
This script creates two count meters (one per traffic class), meters one agent run with three MCP tool calls plus one human request for customer cust_acme, re-sends everything as an agent retry would, and prints this month's per-customer totals. It needs only Python's standard library and USAGE_LAB_KEY (a free key from /signup/; it uses 2 of the Free plan's 3 meters). Save it as meter_agent_tool_calls.py and run python3 meter_agent_tool_calls.py.
# meter_agent_tool_calls.py: Python 3.8+, standard library only. Needs USAGE_LAB_KEY in the environment.
# Meters AI agent / MCP tool calls per customer. Safe to re-run: ids come from run id + step + tool call id.
import hashlib, json, os, sys, urllib.error, urllib.request
from datetime import datetime, timezone
API = "https://usage-lab.pages.dev/api/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["USAGE_LAB_KEY"], "Content-Type": "application/json",
"User-Agent": "usage-lab-quickstart/1.0"} # any descriptive User-Agent; see the note below the sample
# One meter per traffic class, so agent calls get their own total next to human requests.
METERS = {"agent": "agent_tool_calls", "human": "human_requests"}
def call(method, path, body=None):
data = None if body is None else json.dumps(body).encode()
req = urllib.request.Request(API + path, data=data, headers=HEADERS, method=method)
try:
with urllib.request.urlopen(req, timeout=20) as r:
return r.status, json.load(r)
except urllib.error.HTTPError as e:
return e.code, json.load(e)
def tool_call_key(run_id, step, tool_call_id):
# Deterministic: the agent framework retries the same step with the same tool_call_id -> same key.
raw = f"{run_id}:{step}:{tool_call_id}"
return "tc_" + hashlib.sha256(raw.encode()).hexdigest()[:40]
def agent_event(customer, run_id, step, tool_call_id, tool, server, agent_ua):
return {"meter": METERS["agent"], "subscription": customer,
"idempotency_key": tool_call_key(run_id, step, tool_call_id),
"properties": {"traffic_class": "ai_agent", "tool": tool, "mcp_server": server,
"run_id": run_id, "step": step, "agent": agent_ua}}
def human_event(customer, request_id, route):
return {"meter": METERS["human"], "subscription": customer, "idempotency_key": "req_" + request_id,
"properties": {"traffic_class": "browser", "route": route}}
# 1. Create the meters (409 meter_exists on a re-run is fine)
for key in METERS.values():
status, body = call("POST", "/meters", {"key": key, "aggregation": "count", "name": key})
if status not in (201, 409):
sys.exit(f"create meter {key}: HTTP {status} {body}")
# 2. One agent run for customer cust_acme: three tool calls, plus one human request.
month = datetime.now(timezone.utc).strftime("%Y-%m")
run = f"run_{month}_7f3a"
events = [
agent_event("cust_acme", run, 1, "call_01", "search_docs", "docs-mcp", "ClaudeBot/1.0"),
agent_event("cust_acme", run, 2, "call_02", "get_invoice", "billing-mcp", "ClaudeBot/1.0"),
agent_event("cust_acme", run, 3, "call_03", "send_summary", "mail-mcp", "ClaudeBot/1.0"),
human_event("cust_acme", f"{month}_9c21", "/dashboard"),
]
# 3. Send, then re-send as an agent retry would: every call is counted exactly once
for attempt in ("first send", "agent retry"):
status, body = call("POST", "/usage", {"events": events})
if status >= 400:
sys.exit(f"{attempt}: HTTP {status} {body}")
print(attempt, body["summary"])
# 4. Per-customer totals for this month, one line per traffic class
for cls, key in METERS.items():
status, body = call("GET", f"/meters/{key}/rollups?month={month}")
if status != 200:
sys.exit(f"rollups {key}: HTTP {status} {body}")
for r in body["rollups"]:
print(cls, key, r["subscription"], r["quantity"])
Example output, not a command.
first send {'created': 4, 'duplicate': 0, 'conflict': 0}
agent retry {'created': 0, 'duplicate': 4, 'conflict': 0}
agent agent_tool_calls cust_acme 3
human human_requests cust_acme 1
User-Agent note: the Cloudflare edge in front of *.pages.dev rejects Python's default Python-urllib User-Agent (error 1010) before the request reaches us, so the sample sets usage-lab-quickstart/1.0. Any descriptive User-Agent works, including AI agents, bots and scripts, which are welcome.
More guides
- How to meter LLM token usage per customer
- Deduping usage events with idempotency keys
- Backfilling late usage events
- Usage metering compared
FAQ
Do AI agent and bot calls count as real usage?
Yes. Every call an AI agent, MCP client, bot or script makes on behalf of a customer is real, billable usage. Usage Lab never blocks, filters or discounts agent traffic. Send it to its own meter (for example agent_tool_calls) so it gets its own per-customer total next to human requests.
How do I stop agent retries from double counting?
Give each tool call a deterministic idempotency key built from the run id, the step number and the tool call id, for example a SHA-256 of run_id:step:tool_call_id. A retry sends the same key, and Usage Lab counts it once and reports it as a duplicate. The same key with a different body returns 409 and keeps the original.
Should agent calls and human requests share one meter?
Use one meter per traffic class. Usage Lab rolls up each meter per customer per month, and properties are stored but not aggregated, so separate meters (agent_tool_calls, human_requests) give you separate totals to price or report on.
What can I record about each tool call?
Anything useful as properties: traffic_class, tool name, MCP server, run id, step and the agent's User-Agent. Properties are stored with the event for audit and CSV export.
How do I get per-customer totals?
Send the customer id as subscription. GET /api/v1/meters/{meter}/rollups?month=YYYY-MM returns each subscription's quantity and event count for that month, recomputed from the raw events.
What limits apply?
Up to 100 events per batch, idempotency keys up to 255 characters, late events up to 30 days back. The Free plan allows 3 meters and 100,000 events a month.
What does Usage Lab cost?
Free: CA$0 for 100,000 events and 3 meters a month. Pro: CA$12 a month for 2M events and 10 meters. Ultra: CA$36 a month for 20M events and 100 meters. Flat prices, never a percentage of your billings.
- Playground: validate a usage event in your browser
- Usage event validator: check event JSON and idempotency keys
- Drop-in LLM metering helper + margin calculator
- Usage-based pricing calculator (LLM tokens or events)
- Live end-to-end demo
- API docs
- Quickstart: meter an AI app
- Meter Vercel AI SDK, OpenAI and Anthropic tokens (input + output)
- Code examples
- Usage metering compared: Usage Lab vs Stripe meters, Orb, Lago, Metronome, OpenMeter
- OpenMeter alternative: Usage Lab vs OpenMeter (Kong)
- Lago alternative: Usage Lab vs Lago
- Amberflo alternative: Usage Lab vs Amberflo
- Metering usage for Stripe usage-based billing
- Deduping usage events with idempotency keys
- Backfilling late usage events: replays, late windows and adjustments
- Usage Lab vs UsageBox: an honest comparison
- Metronome and Orb alternatives for small SaaS teams
- How to meter LLM token usage per customer for billing
- Stripe Billing Meters vs a usage ledger: when you need both