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.

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:

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

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.