Skip to content
← All writing

Metering tokens when the bill is the product

5 min read
.md
Cover illustration for Metering tokens when the bill is the product

TL;DR

Bill on input plus output tokens rather than message count, because message cost varies by orders of magnitude. Record every provider call as a usage event with a stable idempotency key, check the remaining allowance before expensive work starts, and enforce limits at a boundary the user understands.

Two customers, same plan, same price. One asked short questions and cost me almost nothing. The other uploaded 40-page contracts all day and cost me more than they paid.

I was charging per message. A message from the first customer and a message from the second were the same line on the invoice and nowhere near the same line on my bill.

Message count is a proxy for cost that stops tracking cost the moment your product does anything interesting. So I moved to tokens, which is what the providers charge me for.

Billing unitTracks provider costCustomer intuitionFails when
Per messagePoorlyGoodMessage size varies, which it always does
Per conversationWorseGoodOne conversation runs all afternoon
Per seatNot at allBestUsage per seat varies by orders of magnitude
Input plus output tokensDirectlyWeak, needs translatingNever, but you have to explain it

Count model calls, not user messages

One question from a user is not one call to a model. It is a routing call to decide who handles it, three rounds of tool use, and a summarisation at the end. Five calls, five bills from my provider.

So that is what I record: one row per model call, tagged with the tenant, the agent and the role it was playing. Adding them up for an invoice is easy afterwards. Throwing the detail away first is not something you can undo.

type UsageEvent = {
  idempotencyKey: string   // stable per provider call
  tenantId: string
  agentId: string
  role: 'orchestrator' | 'classifier' | 'summariser' | 'reasoner'
  model: string
  tokensIn: number
  tokensOut: number
  at: number
}

The role tag paid for itself immediately. Aggregate token counts told me I was spending a lot. Per-role counts told me a third of it was going to a routing step whose entire output was a sub-agent name, which is the finding that led to routing that role to a cheaper model.

Why does the idempotency key matter so much?

Because writes fail after the work is done, and a retry without a key charges twice.

The sequence that bites: the model call succeeds, you write the usage event, the write times out, your retry logic runs it again. Now the tenant has been billed for one call twice. Nobody notices until a customer reconciles their invoice, and at that point every number you have produced is suspect.

The key has to be derived from the call, not generated at write time, or a retry produces a fresh key and the guard does nothing. I use the provider’s response id where one exists and a hash of the request otherwise. Stripe’s idempotent requests document the same contract from the other side.

Do this from the first line of billing code. Adding idempotency to a metering pipeline that has been running for months means auditing every event you have already written.

Check the allowance before you spend it

Metering tells you what happened. It does not stop a tenant blowing through their plan, and recording an overage you cannot collect is not a billing system.

Anything expensive gets an allowance check before it starts. A website import that will cost around 51,000 tokens checks that estimate against the remaining balance and refuses up front, rather than failing partway with half an import written and a partial charge.

CheckWhenOn failure
Hard balanceBefore any model callRefuse, prompt to top up
Estimated costBefore a bulk operationRefuse with the estimate shown
Soft thresholdOn crossing 80%Notify, do not block
Plan feature limitOn configuration changeRefuse, explain the plan limit

The soft threshold is the one that reduces support load. A tenant who gets a warning at 80% tops up. A tenant whose agent goes silent mid-conversation opens a ticket.

Where the limit bites is a product decision

A conversation is not atomic. When the balance runs out three tool calls into a five-call task, the technically correct answer is to stop immediately, and it produces a terrible experience: a half-finished task and no explanation.

Where you cut offOverage you absorbWhat the user sees
Mid tool callNoneThe agent stops mid-sentence, with no explanation
At the next turn boundaryBounded by one turnThe task finishes, then a clear message about the balance
At the end of the conversationUnboundedNothing, until the invoice

I check at turn boundaries and let an in-flight turn complete, accepting a small overage. The overage is bounded by the cost of one turn, which is a number I can look at and decide is acceptable. It is not free and I would not pretend the alternative is obviously wrong. It is a choice between a small unbilled cost and a user watching an agent stop mid-sentence.

Storage for counters wants low-latency reads on every request. I use Workers KV for the wallet, which is eventually consistent, so two concurrent requests can both see enough balance. Under-counting by one turn is a trade I took deliberately over putting a strongly consistent read on the hot path of every message.

Connecting it to money

Usage-based billing has a shape that predates AI, and Stripe’s usage-based billing documentation covers the meter and subscription mechanics.

The AI-specific parts are that your cost basis moves under you, and that tokens are not a unit customers have intuition about. When a provider changes prices or you route a role to a cheaper model, your margin changes without anything in your billing code changing. Store the model and the provider on every usage event so you can recompute historical margin. Without that you can see what you charged and not what it cost you.

Sources

Common questions

Should you bill an AI product per message or per token?

Per token. Message count stops tracking cost as soon as the product does anything interesting: one customer asks short questions, another uploads 40-page contracts, and both produce the same line on the invoice while producing wildly different lines on your provider bill.

What should a token usage event record?

One row per model call rather than per user message, tagged with tenant, agent, role, model, input tokens and output tokens. A single user question is often a routing call, several rounds of tool use and a summarisation, which is five provider bills. Aggregating later is easy; recovering detail you discarded is not.

Why does a metering pipeline need an idempotency key?

Because writes fail after the work is done. The model call succeeds, the usage write times out, the retry runs it again, and the tenant is billed twice. Nobody notices until a customer reconciles an invoice, at which point every number the system has produced is suspect.

How should the idempotency key be generated?

Derive it from the call itself, using the provider response id where one exists and a hash of the request otherwise. A key generated at write time changes on retry, which means the guard does nothing at exactly the moment it is needed.

What happens when a tenant runs out of balance mid-conversation?

Stopping immediately is technically correct and produces a half-finished task with no explanation. I check at turn boundaries and let an in-flight turn complete, which bounds the overage to the cost of one turn. That is a deliberate trade of a small unbilled cost against a user watching an agent stop mid-sentence.

Why store the model and provider on every usage event?

Because your cost basis moves under you. When a provider changes prices or you route a role to a cheaper model, margin changes without any billing code changing. Storing the model and provider lets you recompute historical margin; without it you can see what you charged but not what it cost you.

Written by Elson Tan, Head of Technology and co-founder at Nedex Group, working on AI harness and agent infrastructure.

AboutRSS
  • 4 min read

    Route cheap models to orchestration, not to the work

    Most agent platforms pick one model and use it everywhere. Splitting model selection by role rather than by task is where the cost curve actually bends.

  • 17 min read

    The product was the easy part

    What a SaaS needs before it can charge anyone: credit billing in Stripe, invoicing and sales tax, email unsubscribe law, terms and privacy, and an admin panel.

  • 5 min read

    Citations that survive the question

    A RAG answer with a source name under it is not a citation. If a teacher cannot open the page and see the sentence, the system has not shown its work. Carrying page numbers through retrieval is most of the job.

Get in touch

Tell me who you are and what you are working on.

Your details are used only to reply to this message.