Reconciling an API bill, token by token
Four numbers have to agree, and you can check all four yourself: the usage block on the response, the ratio the gateway multiplies by, the integer quota on the log row, and the balance. The conversion is fixed -- a published dollar rate divided by two is the ratio, and 500,000 quota is one dollar -- so a GLM-5.2 request of 1,000 input and 500 output tokens is 1,047.5 quota, $0.002095, and that is what usage.cost_usd carries to eight decimal places.
Four numbers, and where each one lives
A bill on this platform is not one number in one place. It is four, and they are produced by different systems at different moments, which is exactly why reconciling them is worth doing rather than assuming.
The first is usage on the response, returned with the answer itself. The second is the pair of ratios the gateway multiplies by, which are derived from the published rate card by a script and never typed. The third is the integer quota recorded on the consumption row in your log. The fourth is the balance, which is those integers subtracted from what was credited.
If any two of them disagree, the pricing page is a lie, and that is a much more serious defect than a slow endpoint. So the conversion between them is published here in full.
- 1,000 input tokens at $0.82 per million$0.0008239%
- 500 output tokens at $2.55 per million$0.00127561%
| Number | Produced by | Unit | When you see it |
|---|---|---|---|
| usage.cost_usd | The gateway, on the response | USD, 8 decimal places | Immediately, on every request |
| modelRatio / completionRatio / cacheRatio | Derived from the catalogue by a script | Dimensionless | Never directly -- but reconstructible from the rate card |
| quota on the log row | The gateway, when the request settles | Integer quota, 500,000 to the dollar | In the usage log |
| Balance | Credits minus consumption | Integer quota | On the dashboard and GET /api/me |
A published rate is stored as a ratio, and the divisor is two
The gateway does not hold dollars per million tokens. It holds a multiplier, and computes quota as prompt tokens times the ratio, plus completion tokens times the ratio times a completion multiplier. Because 1,000,000 tokens at ratio 1 costs 1,000,000 quota, and 500,000 quota is a dollar, a ratio of 1 means two dollars per million.
So the conversion is: ratio equals the published dollar rate divided by two. Getting that divisor wrong by a factor of two is the entire failure mode of this design, and it would be invisible until somebody reconciled a bill -- which is why the catalogue is the source, the ratios are generated rather than edited, and the generator prints a round trip for one model and exits non-zero if converting back does not land within a millionth of the published price.
Everything else follows from the first ratio. The completion multiplier is the output rate divided by the input rate. The cache multiplier is the cached rate divided by the input rate, which is why a cached rate here structurally cannot exceed the uncached one.
| Field | GLM-5.2 | DeepSeek V4 Flash | How it is derived |
|---|---|---|---|
| modelRatio | 0.41 | 0.045 | Input rate per million, divided by 2 |
| completionRatio | 3.10975610 | 2 | Output rate divided by input rate |
| cacheRatio | 0.19512195 | 0.2 | Cached rate divided by input rate |
Reconciling one request by hand
Take a GLM-5.2 request that read 1,000 tokens and wrote 500. The ratio is 0.41 and the completion multiplier is 3.10975610.
Quota is 1,000 times 0.41, plus 500 times 0.41 times 3.10975610: 410 plus 637.5, or 1,047.5 quota. Divide by 500,000 and that is $0.002095, which is what the response carries in usage.cost_usd. The same figure computed straight off the rate card -- 1,000 tokens at $0.82 per million plus 500 at $2.55 per million -- is $0.00082 plus $0.001275, which is the same $0.002095.
That the two routes agree is the property being checked. The rate card and the billing system are not two descriptions of one intention here; one is generated from the other, and this arithmetic is what makes that checkable from outside.
QUOTA_PER_USD = 500_000
ratio = 0.82 / 2 # published input rate / 2
completion_ratio = 2.55 / 0.82 # output / input
quota = 1000 * ratio + 500 * ratio * completion_ratio
print(quota) # 1047.5
print(quota / QUOTA_PER_USD) # 0.002095 == usage.cost_usd
Three places rounding is allowed to land
Money is never a float below the interface, and there are two integer grids underneath a balance. The gateway holds quota, where 500,000 units are a dollar, so one unit is $0.000002. Our own append-only credit ledger holds micros, where one micro is $0.000001. The coarser of the two is the one that decides what actually moves, which makes $0.000002 the smallest amount this system can charge or refund.
The first rounding is therefore the gateway's, at that grid. The second is cosmetic: usage.cost_usd is fixed to eight decimal places, which is finer than the grid it describes, so it never loses a movable amount. The third is the interface, where precision follows magnitude rather than a fixed rule -- below a hundredth of a cent the UI says less than $0.00001 rather than picking a rounding that reads as zero.
That last one is a billing decision dressed as formatting. A genuine spend of $0.001386 rendered at two decimal places tells a customer they were charged nothing and an operator that consumption is zero. Both are false, and both fail in the direction that hides a problem.
| Model | Input rate / 1M | Cost of one input token | Tokens to move one quota unit |
|---|---|---|---|
| DeepSeek V4 Pro | $0.28 | $0.00000028 | 8 |
| DeepSeek V4 Flash | $0.09 | $0.00000009 | 23 |
Sum the integers, never the dollars
The usage endpoint returns both figures on every row: the raw quota, which is authoritative, and a dollar value, which is quota divided by 500,000 for rendering. The dollar value must never be summed by a client, and the reason is arithmetic rather than style.
Each dollar figure is a float derived from an integer. Adding a hundred of them accumulates the error of a hundred divisions; adding the integers and dividing once accumulates the error of one. On a hundred requests the difference is invisible, and on a month of an agent loop it is the kind of discrepancy that makes somebody stop trusting the whole system for a reason nobody can locate.
The same rule applies to your own logging. Log usage.cost_usd for the distribution -- it is the fastest way to catch one class of request generating far more output than the rest -- and reconcile totals from the token counts and the rate card.
rows = fetch("/api/usage")["entries"]
# Right: integers first, one division at the end.
total = sum(r["quota"] for r in rows) / 500_000
# Wrong: a hundred floats, a hundred roundings.
# total = sum(r["cost_usd"] for r in rows)
# Per-request logging is a different job, and cost_usd is right for it:
# a cost regression shows up in the distribution long before the total.
worst = max(rows, key=lambda r: r["cost_usd"])
What the log will not tell you
The gateway's own log is every event on an account, not just API calls. A $30 top-up arrives in it as a row with an empty model name, zero tokens and zero quota, plus a human-readable line in Chinese. Passed through unfiltered, that made a customer's history page show a blank, zero-cost, unexplained entry, so the usage endpoint filters to consumption rows and says so.
The rolled-up view has a sharper limitation. The gateway's aggregation endpoint returns one combined token count per bucket and does not split prompt from completion, so there is no honest split to expose. It was once surfaced as a prompt-token figure beside a permanently-zero completion figure, which made a correct total look like a prompt-only number and made completions look free. There is now one field, labelled as the total, because a wrong label on a right number is still wrong.
The per-request rows do carry both counts, along with the key name that produced them, so attribution by key is available even though a per-key spending cap is not.
The one place this design trusts somebody else
Consumption is rolled up from our own records rather than asked of an upstream, which means an upstream that under-reports or stops reporting cannot quietly change what we believe we sold. That is deliberate and it is most of the integrity of this system.
What we do take from upstream is the per-response usage block those records are built from. A provider returning wrong token counts would produce wrong charges here, and nothing downstream would notice. That is the single remaining place this design trusts a third party, and it is better stated than left for someone to discover.
The check available to you is the same one available to us: token counts scale with what you sent. A summarisation prompt whose input count is not roughly proportional to its length across a hundred requests is worth asking about.
If you stream, ask for the numbers
The usage block does not exist on a stream by default. Without stream_options.include_usage a streaming caller never learns what a call cost at all, so an evaluation harness that streams for progress output and then reports zero spend is a harness that surprises you at the end of the month rather than at the end of the run.
Set it and one extra frame arrives after the last content frame and before the terminator: empty choices, populated usage. While it is on, every earlier frame carries usage: null rather than omitting the key, so a client can read the field unconditionally and get its answer on exactly one frame.
Setting stream_options without stream: true is a 400 rather than a silent no-op, because accepting it quietly would let a caller who forgot stream: true see a usage block on a blocking response and conclude their configuration works.
A reconciliation checklist
Reconstruct one request by hand before you trust a hundred. Ratio is the published input rate over two; quota is prompt times ratio plus completion times ratio times the completion multiplier; quota over 500,000 is dollars.
Sum quota, not cost_usd. One division at the end, not one per row.
Compare the response's usage against the log row for the same request. They are produced at different moments by the same system, and a disagreement between them is the first thing worth reporting.
Expect nothing below $0.000002 to move. On DeepSeek V4 Flash that is 23 input tokens, so a very small request genuinely can round to no charge.
Check the rate card programmatically rather than reading it. GET https://router.xark.io/api/v1/pricing is public, key-free and CORS-open, and 42% below the publisher's own rate on GLM-5.2 is a claim you should be able to diff rather than believe.
# Every rate, no key, CORS-open. Diff it against what you were charged.
curl -s https://router.xark.io/api/v1/pricing \
| jq '.data[] | {model,
in: .ours.input, out: .ours.output, cached: .ours.cached_input,
official_in: .official.input,
saving: .savings_percent}'
AI Token Router is an OpenAI-compatible gateway for open-weight models. Every rate on the pricing page is printed next to the model’s official rate, so the numbers in this post are checkable rather than claimed.
Get an API keyRelated
- What a context window actually costs
K means 1,024 here, so 200K is 204,800 tokens. What filling each window costs once, what it costs every turn, and where retrieval wins.
- What a spending ceiling protects you from
The balance is the only hard limit here. Four things that look like spend controls and are not, and the exact arithmetic of how fast a ceiling can go.
- Why a payment cannot credit you twice
Two Stripe events describe one payment. Which one credits, what a redelivery does, and the append-only ledger that makes a second credit unrepresentable.