How the price of a text request is calculated, start to finish

Between sending a request and paying for it there are several steps, and nearly every one is a place to either save money or lose track of spend.

5 min · · Updated · payments

Call /v1/messages or /v1/chat/completions, and before a single token shows up in the response, the gateway runs through several internal steps. Walking through them explains why a request sometimes fails with 402 instantly, and why X-Zukko-Cost-Micros in the response doesn't always match a rough estimate.

Step 1: counting input tokens

Everything that goes into the messages field — the system prompt, conversation history, file contents pasted in, tool descriptions for an agent — gets tokenized for the specific model you picked. Each model has its own tokenizer, so the same piece of text produces a different token count for claude-opus-5-5 than for gpt-5.6-sol. If you need the token count without sending a real request or paying for it, there's a dedicated free method:

bash
curl https://api.zukko.pro/v1/messages/count_tokens \
  -H "Authorization: Bearer fetch_your_key" \
  -H "Content-Type: application/json" \
  -H "anthropic-version: 2023-06-01" \
  -d '{"model":"claude-opus-5-5","messages":[{"role":"user","content":"hello"}]}'

This is handy when context is assembled programmatically from file fragments and history, and you want to estimate size before sending rather than after paying for it.

Step 2: the balance check happens before the model call

Next the gateway checks the account balance and any limits set on the specific key, if you configured them in the cabinet on the key's page. This happens before the request ever reaches the model provider. If there's no money left, the response comes back instantly:

bash
{"error":{"message":"Insufficient balance","code":"insufficient_balance"}}

The important detail: since the check runs before the model call, a failed request with a 402 costs nothing. The same step checks key limits — the daily and monthly dollar cap and the list of allowed models. If a particular model is blocked by the key's policy, you get a 403 model_not_allowed_for_key instead of a charge followed by a refusal.

Step 3: the request goes to the model — but the cache is checked first

If balance and limits are fine, the gateway checks the cache. With an explicit X-Zukko-Cache: true header (the primary way, works for any model) or temperature: 0 (only for models that accept that value; newer Claude models may reject a temperature other than 1 with a 400), the same request sent earlier may already be stored — for chat, the cache lives for up to 7 days. The match has to be exact: the whole request body, not a "nearly identical" request or a shared prefix. On a hit, the response comes back instantly and free, with X-Zukko-Cache: hit in the headers; such hits are not recorded in /v1/usage. If there's no match, the request body is converted into the target provider's format and actually sent.

Step 4: output tokens and the final price

Once the model responds, the gateway counts output tokens and adds up the cost: input and output tokens are multiplied by the model's rates. For the models currently enabled in Zukko, input and output cost the same — claude-sonnet-5 and gpt-5.6-sol at $0.30 per million, glm-5.3-flash at $0.04, for example (prices are at GET /v1/models/info). Images work differently — pricing there is a fixed amount per generated image, not per token, and it's billed separately for nano-banana-2-* and openai-gpt-image-* (image generation through Zukko is temporarily unavailable right now). The total is deducted from the account balance right after the model's response arrives, with no separate confirmation step.

Step 5: what comes back in the response

The headers of a regular (non-streaming) HTTP response carry two fields worth logging on your side instead of recalculating manually:

  • X-Zukko-Cost-Micros — the cost of this specific request in microdollars (1,000,000 micros = $1);
  • X-Fetch-Balance-Cents — the balance remaining after the charge; despite the name, also in microdollars (1,000,000 = $1), so a value of 187654 is about $0.19, not $1876.54.

Streamed responses (stream: true) don't carry these headers — check their spend in the dashboard, /v1/usage or /v1/balance. The exact balance in cents is the balance_cents field in the /v1/balance response.

If you're calling the API from a script or an agent, reading these headers as you go is cheaper than polling a separate balance endpoint on a schedule:

bash
curl https://api.zukko.pro/v1/balance \
  -H "Authorization: Bearer fetch_your_key"

The full history of charges, with cursor-based pagination (next_cursor), comes from /v1/usage — useful when you need to reconcile spend over a period rather than just check the current balance. Each record has created_at, model, input_tokens, output_tokens, total_tokens and cost_cents (fractional cents). The history covers the whole account: all keys together, with no per-key filter.

Idempotency protects against double charges

If the connection drops and you're not sure whether the request landed, don't guess and retry blindly. Send an Idempotency-Key header with a unique value per logical request (and the same value on its retries — never regenerate it when retrying): retrying with the same key within 24 hours returns the same stored response and doesn't charge twice. If a different request body arrives with the same Idempotency-Key, the gateway responds with 422 idempotency_conflict — a guard against accidental mix-ups, not an arbitrary restriction.

A full cycle, end to end

bash
curl -i https://api.zukko.pro/v1/messages \
  -H "Authorization: Bearer fetch_your_key" \
  -H "Content-Type: application/json" \
  -H "anthropic-version: 2023-06-01" \
  -H "Idempotency-Key: session-42-step-1" \
  -d '{"model":"claude-sonnet-5","max_tokens":200,"messages":[{"role":"user","content":"Explain the difference between TCP and UDP in two sentences"}]}'

The -i flag prints response headers along with the body — that's where the price of this exact request and the remaining balance show up. For a full catalog of models with limits and current prices, without needing a key at all, there's a dedicated public endpoint, /v1/models/info; the plain model list without details is GET /v1/models.

The whole chain — counting input tokens, checking balance and key limits, checking the cache, calling the model, counting output tokens, deducting the charge, and writing it into both the response headers and the /v1/usage history — takes milliseconds and works the same way whether you go through the Anthropic-compatible /v1/messages path or the OpenAI-compatible /v1/chat/completions and /v1/responses. Image models use a different path, /v1/images/generations (image generation is temporarily unavailable right now), but the balance and key-limit checks before generation starts follow the same logic.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

How API request pricing works, step by step · Zukko