Skip to content
← All posts
Product

Per-token billing you can audit

6 min read

CLF AI Gateway is prepaid: you top up credits, and every request draws them down. This post is the entire pricing model — the formula, the integer arithmetic behind it, and how to recompute any charge from numbers we already show you. Nothing here is simplified for marketing; this is what the settlement code does.

Three per-token prices, integer math

Every model has up to three per-token prices: input, cached input, and output. They are published per million tokens on the models page — for example, kimi-k2.6 bills $0.57/M input, $0.096/M cached input, and $2.40/M output. Today every model bills purely per token; there is no per-request fee line on any of them.

Those rates already include the 40% subsidy running since August 11, 2026: every model is priced 40% below its list price — the same numbers Cloudflare Workers AI publishes for these models — and the models page shows both figures, list struck through. There is nothing to activate; the discount is baked into every request automatically.

Internally, none of this is floating point. Prices are stored as integer nano-USD per token (1 nano-USD = 10⁻⁹ dollars), so $0.57 per million tokens is exactly 570 nano per token. A request cost is a sum of integer multiplications, your balance accrues exact nano amounts, and rounding to cents happens once — at statement time, half-even — never per request.

The formula

text
billable_input = prompt_tokens − cached_tokens

cost = billable_input    × input_price
     + cached_tokens     × cached_input_price
     + completion_tokens  × output_price
  • prompt_tokens, cached_tokens and completion_tokens come from the usage object of the response — the same one you see.
  • completion_tokens is billed in full at the output price. Reasoning tokens live inside it, not next to it (details below).
  • Prices are the per-token rates from the models page, fixed at the moment your request arrives — a price change mid-stream never touches a request that already started.

A real request, recomputed by hand

Here is a real request against kimi-k2.6: a long prompt that hit the prefix cache, and a short answer. The response reported:

json
"usage": {
  "prompt_tokens": 12907,
  "completion_tokens": 300,
  "total_tokens": 13207,
  "prompt_tokens_details": { "cached_tokens": 12864 }
}

Apply the formula with kimi-k2.6 rates (570 / 96 / 2400 nano per token):

text
billable_input = 12,907 − 12,864 =       43 tokens

    43 ×  570 =    24,510 nano
12,864 ×   96 = 1,234,944 nano
   300 × 2400 =   720,000 nano
                ─────────────
                1,979,454 nano  =  $0.001979454

That exact figure came back on the response as x-gw-cost-nano: 1979454, and the same row sits in the dashboard usage log. Without the cache hit, the same request would have cost 12,907 × 570 + 300 × 2400 = 8,076,990 nano ($0.0081) — the prefix cache cut this request by 75.5%.

Reasoning tokens: shown, never billed twice

Kimi, GLM and DeepSeek V4 are reasoning models: part of the completion is chain-of-thought that streams as reasoning_content before the visible answer. Those tokens are already inside completion_tokens, and that is the only place they are billed — at the normal output price.

We still count them separately and return usage.completion_tokens_details.reasoning_tokens, because when a two-line answer costs more than you expected, you deserve to see why. But there is deliberately no "reasoning price" dimension: adding one on top of completion_tokens would bill the same token twice.

Zero-completion insurance

If a request produces nothing, it costs nothing. The exact condition:

The condition matters. A request that ends with finish_reason: "stop" or "length" and zero completion — say, you sent max_tokens: 0 — is a request the model finished as instructed, and the prompt is billed normally. Without that carve-out, "free if no output" becomes an exploit: set max_tokens: 0 and have your prompts processed for free.

What caching does to your bill

Prefix cache — automatic

When your prompt shares a prefix with a recent request — same system prompt, same tool definitions, same few-shot examples — the inference platform reuses the computed prefix and reports it as cached_tokens. Those bill at the cached-input rate: on kimi-k2.6 that is 96 vs 570 nano per token, roughly 6× cheaper. That is why the 12.9k-token request above cost a fifth of a cent. To benefit, keep stable content at the front of your messages and variable content at the end.

One honest caveat: glm-4.7-flash has no cached-input rate (the pricing table shows — there), so its cached tokens bill at the normal input price. We do not get a cached discount on that model either, so we do not invent one for show.

Exact cache — opt-in

For deterministic repeated calls (temperature: 0) you can enable response caching per request with "cache": {"mode": "on", "ttl_seconds": 3600}. A full hit skips inference entirely and bills a flat 10% of the normal cost of the request it replayed. The response carries x-gw-cache: hit, so cache traffic is visible in your log, not blended in.

Streams, disconnects, partial output

  • You disconnect mid-stream: generation upstream does not stop the instant you hang up, so tokens generated up to the last chunk we received are billed. Set max_tokens — it is your hard per-request spend cap.
  • Upstream fails mid-stream: you get a final chunk with finish_reason: "error", then [DONE]. You pay only for tokens actually delivered, and the request is flagged partial in your log.
  • It fails before the first byte: zero-completion insurance applies — $0.

For streams, pass stream_options: {"include_usage": true} and read the final chunk’s usage — or fetch GET /v1/generation?id=<x-request-id> afterwards. Same numbers either way.

Prepaid: holds, settle, and a 402 you can parse

Before a request goes upstream, we place a temporary hold on your balance — an estimate from your prompt size and max_tokens, capped so that a 262K-context model does not lock up dollars for a half-cent request. When the request settles, the real cost replaces the hold. If your available balance cannot cover the estimate, you get a 402 with machine-readable numbers:

json
{
  "error": {
    "message": "Insufficient credits. Available: $0.0041. This request requires an estimated hold of $0.0269. Top up at https://app.clfaigateway.dev/billing/topup",
    "type": "insufficient_credits",
    "code": "insufficient_credits",
    "metadata": {
      "available_nano": 4100000,
      "required_estimate_nano": 26927600,
      "topup_url": "https://app.clfaigateway.dev/billing/topup"
    }
  }
}

Two behaviors worth knowing: a live stream is never cut because your balance hit zero mid-response — that request settles even if it dips your balance slightly negative. And new requests are refused until you top up. Prepaid means the worst case is bounded and visible, not that responses die mid-sentence.

Audit it yourself

  • Per request: the usage object, plus x-gw-cost-nano, x-request-id and x-gw-cache response headers.
  • Per account: the dashboard usage log — tokens, cache state and cost per request, exportable as CSV, matching the headers nano for nano.
  • Per model: current prices on /models, including the upstream model IDs (@cf/...), so you know exactly which model serves you.

That is the whole model. If you recompute a charge and get a different number, that is a bug — tell us and we will fix it, not the story.

FAQ

Am I charged for failed requests?

No — a request with zero completion tokens and finish_reason null or error settles at $0. If a stream fails after delivering tokens, you pay only for the delivered tokens and the request is marked partial.

What happens if my balance runs out mid-stream?

The stream finishes and settles normally; your balance can go slightly negative on that one request. New requests return 402 until you top up.

Where do I see cached_tokens and reasoning_tokens?

In every response: usage.prompt_tokens_details.cached_tokens and usage.completion_tokens_details.reasoning_tokens — the same figures the billing formula uses, also visible per request in the dashboard.