Skip to main content
Every selection response carries the usage receipt; the arithmetic below is the published definition of the number on it.One nuance if you are also checking payloads against the per-request ceiling: the ceiling counts your strings as sent, without NFC normalisation, while the meter normalises to NFC. For text already in NFC, which is nearly all text, the two agree.

Why this page exists

If you dispute a charge, we cannot show you your records. We never had them. So the audit story has to work without them, and it does — by making the meter a pure function of your request body:
1

You hold the payload

You sent it. We did not keep a copy.
2

We publish the function

In full, below, with a reference implementation.
3

The receipt names the version

meter_version pins which definition applied.
Which means you can arrive at the number yourself, without trusting a log you cannot see. Exactly, if you run the Python reference implementation below — it is the definition. A port to another language is exact for real text and has one stated residual on exotic characters, which is written down rather than left for a support ticket.

The definition

Those four field groups, and nothing else. (“Every field the engine reads” would be a wider set — keywords, tags and attributes are read as indexing hints and are deliberately outside the meter, because a number you have to be able to reproduce should be short enough to reproduce by hand.) Not counted: keywords, tags, attributes (structurally bounded; a size cap on them applies), record id, source, step_id, kind, anything in the response, and any JSON syntax — braces, quotes and commas in the envelope are not your text.
tokens_before on the response is a different number and is not the meter.On this deployment it works out to Σ estimate_tokens(record.text) — the record text only. The meter adds record.title, task.prompt and task.recent_prompts, so your input_tokens will be larger than tokens_before, and the gap is exactly those three field groups. That is expected, not a discrepancy.Do not reconcile an invoice against tokens_before: it is an engine-internal budgeting quantity and is free to move when engine internals do, while the meter is pinned by meter_version.

The counting rules

These are part of meter_version, not implementation detail. They are what make “you can recompute it” a true statement rather than an approximate one.
Unicode gives the same visible text more than one encoding. á can be one code point or two — a followed by a combining acute — and the counter sees them differently:
Both render as á clause. Only the second is what the meter counts.So the meter normalises first, and so should you. For text that is already in NFC — which is nearly all text — this is a no-op.
The character-count fallback divides by the number of code points. In JavaScript, "𝔘".length is 2 because the string is measured in UTF-16 code units, while the same character is one scalar value.A naïve port using .length therefore diverges on every astral-plane character — emoji, many CJK extensions, mathematical alphanumerics — and the same document would bill differently depending on which language you checked with. The TypeScript reference below uses [...text].length for this reason.
Rounding happens once, over the summed exact amount for the whole invoice period — never per request. Per-request rounding of sub-cent amounts is how a million small requests silently stop summing to their own receipts.So: sum input_tokens across your receipts first, then multiply, then round. Not the other way round.

The reference implementation

This is the published function, verbatim. It is a deterministic heuristic, not a model tokenizer — deliberately, so the number is reproducible with no model dependency and no per-provider drift.

How the count works

Two estimates, and the larger wins:
  • Lexical — the number of word runs and standalone punctuation marks.
  • Character fallback — code points ÷ 4, at least 1. This catches long compact strings with no word boundaries, such as a base64 blob or minified JSON, which the lexical pass would badly under-count.
max(lexical, char_based) means a normal sentence is counted lexically and a dense blob is counted by length.

Worked example

The quickstart payload, field by field: Note that task.keywords (["refund", "invoice"]) contributes nothing — it is capped, not counted. Then, from the receipt:
with charge_multiplier read off the receipt for that request, and rate_per_million from the rate card named by price_version.

Checking a whole invoice

1

Export your receipts for the period

One per request: input_tokens, outcome, charge_multiplier, meter_version, price_version.
2

Group by (price_version, charge_multiplier)

A rate change or a different outcome mix inside one period means more than one group. Do not average the multipliers.
3

Sum input_tokens within each group, then price the group

Exact arithmetic, not floats, if you can — a rational or decimal type. This is where the “round once” rule applies.
4

Round the total, once

Sum the groups, then round. The invoice does the same, in the same order.
Spot-check by re-deriving input_tokens for a sample of requests from your own stored payloads. If your recomputation matches the receipts and your arithmetic matches the invoice, the bill is verified end to end without us being involved.

If your number disagrees

Check that you are counting title as well as text, and recent_prompts as well as prompt. Those four are the whole surface, and omitting title or recent_prompts is the usual cause.Also check you are not counting keywords, tags or attributes, which are not metered, and not counting the JSON envelope.
Two likely causes:
  • You did not normalise to NFC. Decomposed accents count differently.
  • You ported the function to a language that measures strings in UTF-16 code units. Use code points.
There is one residual we know about and would rather name.The counting function classifies characters as word / non-word / whitespace, and every runtime carries those classification tables at whatever Unicode version it shipped with. Two runtimes at different Unicode versions can disagree about a recently-assigned character.In practice this is invisible, and we measured rather than assumed it. Over a 4.2 MB corpus of prose, Markdown and source code (7,106 samples) the two implementations agree on every sample, as they do across an adversarial set of whitespace and combining-character cases — including the three classifications a naive port gets wrong (U+001C–U+001F and U+0085, which Python treats as whitespace and JavaScript does not; and U+FEFF, the reverse).Where they can diverge is on code points assigned in a Unicode version one runtime has and the other does not. Against 3,027 strings of uniformly random code points — overwhelmingly unassigned and private-use, which is the worst case rather than a realistic one — they disagree on 116, always by one or two tokens, and always because one runtime classifies a recently-assigned character as a letter and the other does not.If you need a number with no residual at all, run the Python reference implementation. It is the one the meter is defined by.You do not have to take the measurement on trust either — it is a script, and re-running it is how we keep the claim true as runtimes move.
Tell us, with the request_id, the meter_version from the receipt, and your computed number. A disagreement between the published function and the meter is a defect on our side, and it is one we want to hear about — the whole point of publishing the function is that you can find it.

Checking a payload before you send it

The same function answers “will this 413?”. Your per-request ceiling is denominated in exactly this quantity:
Cheaper than a round trip, and it lets you split deliberately rather than by bisection.