Pricing
The bill, and how to check it.
crab has no subscription and no hosted service marking anything up. It has a price table with version history and peak/valley windows, a four-term formula, and a local ledger. Everything here is auditable against your own provider invoices.
crab computes money from four numbers the provider gives it and a table you can edit. This page is the arithmetic, the file format, and the provider quirks that make naive cost estimates wrong.
The arithmetic
A turn reports prompt, completion, cache_read and
cache_write. The cost is:
billed_input = prompt - cache_read
cost = billed_input * input_rate
+ cache_read * cache_read_rate
+ cache_write * cache_write_rate
+ completion * output_rate
# rates are per token; tables are usually quoted per million
That subtraction is the whole trick. A cached prompt token is not billed at the input rate — it is billed at the cache-read rate, which is typically a tenth of it. Omit the subtraction and a session that is 99% cache hits gets priced as if every token were fresh.
A real row
One turn from crab metrics --json on a long session:
| Field | Value | Note |
|---|---|---|
prompt_tokens | 309,230 | everything sent |
cache_read | 308,608 | 99.8% of it was already cached |
| billed input | 622 | prompt − cache_read |
cache_write | 622 | one fresh block written into the cache |
completion_tokens | 3,646 | output |
cost_usd | 0.006413 | what crab recorded |
The lesson is in the ratio, not the total. The prompt was 309k tokens and cost almost nothing because
the prefix was stable; had that run broken its prefix every turn, the same conversation would have cost
orders of magnitude more. That is why crab trace prints cr and cw
next to every model call.
You can verify any row yourself:
sqlite3 .crab/metrics.sqlite \
"select model, prompt_tokens - cache_read as billed_in,
cache_read, cache_write, completion_tokens, round(cost_usd,6)
from turns order by ts desc limit 5;
Peak and valley windows
Several providers price by time of day. A price version can therefore carry windows, and crab picks the rate that applies to the moment of that individual call — not to the report, and not to the day:
[[models."some-model".versions]]
since = "2026-09-01T00:00:00+08:00"
input = 4.00
output = 16.00
# local-time windows within this version
[[models."some-model".versions.windows]]
from = "00:30" # minutes since local midnight
to = "08:30"
input = 2.00 # valley rate
output = 8.00
Because each call is priced at its own timestamp, a single session that spans a pricing boundary is priced correctly in both halves. This is also why the ledger stores a cost per row rather than recomputing from today's rates.
Version history
A model's price is a chain of versions sorted by since. To record a change you verified,
append a block with the new effective time; crab uses the latest version whose since is
not after the call:
[models."deepseek-chat"]
[[models."deepseek-chat".versions]]
since = "2026-10-01T00:00:00+08:00"
input = 0.27
output = 1.10
cache_read = 0.07
cache_write = 0.0 # DeepSeek folds writes into the miss price
# a later, verified price change
[[models."deepseek-chat".versions]]
since = "2027-01-01T00:00:00+08:00"
input = 0.30
output = 1.20
cache_read = 0.08
cache_write = 0.0
Old turns keep the cost computed when they ran, so changing the table never rewrites history.
pricing.toml, and what it is for
The built-in table covers the models crab ships knowledge of. ~/.crab/pricing.toml is an
overlay for three situations: a model the built-in table does not know, a price change you have
verified, and a field you want to override. It is merged at load time; nothing is patched in the
binary.
Merge rules
- Merge by
since - A version block with the same
sinceas a built-in one overrides its non-zero fields. windows- A non-empty window list replaces the built-in windows entirely rather than merging.
- Unknown model
- No built-in entry means your file is the only source. Absent rates stay absent.
- Bad syntax
- A version whose
sinceor window cannot be parsed is warned about on stderr and skipped — never silently treated as zero.
Two provider quirks worth encoding
DeepSeek does not bill cache writes separately
Writing into the cache is included in the miss price, so cache_write must be
0.0. A table that charges for writes over-reports every DeepSeek turn by the full write
volume — which on a long session is most of the cost.
Kimi prices cache writes by TTL
Cache writes are billed at the default 5-minute TTL tier. A session with long gaps between turns costs more than one that streams continuously, even with identical token counts. That is the provider's pricing, not a crab artefact.
The alias trap
crab prices the model name in your config, not the model a gateway actually
routed to. If you set an alias that is not an official identifier, crab cannot find it and the turn
comes back unpriced — visible as priced_turns < turns in crab metrics --json,
and as a lower-bound total.
Two fixes, and the first is better: use the provider's real model id. If the alias is genuinely
valid, give it its own [models."alias"] block.
When a turn is unpriced
crab writes the token counts and leaves cost_usd out of the sum. It does not estimate
from a similar model, and it does not fall back to the default provider's rates. The reports surface
this so a total is never presented as complete when it is not — in the text report an unpriced model
shows −, not $0.
| Situation | What crab does |
|---|---|
| Model missing from every table | Row written, counted as unpriced, shown as −. |
| Provider returned no usage | Row written with the counts it had; unpriced where input is unknown. |
| Price changed after the run | Row keeps the cost computed at write time. Add a new since block for future turns. |
| Two runs concurrently | Separate rows, each priced independently. |
| Call spanning a peak boundary | Priced at the window that applied to that call's timestamp. |
Why a local ledger at all
Because the interesting question is never "what did this turn cost" — it is "what did this repository cost me over the last month, and which model" — and every gateway answers that only for traffic that went through it. crab is a client, so it keeps its own books. The file is SQLite; the schema is on the observability page; nothing about it is proprietary.
And because the price table is a file, the answer can be wrong in a way you can fix. That is a strictly better failure mode than a number you cannot audit.