crab0.4.0

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:

FieldValueNote
prompt_tokens309,230everything sent
cache_read308,60899.8% of it was already cached
billed input622prompt − cache_read
cache_write622one fresh block written into the cache
completion_tokens3,646output
cost_usd0.006413what 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 since as 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 since or 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.

SituationWhat crab does
Model missing from every tableRow written, counted as unpriced, shown as −.
Provider returned no usageRow written with the counts it had; unpriced where input is unknown.
Price changed after the runRow keeps the cost computed at write time. Add a new since block for future turns.
Two runs concurrentlySeparate rows, each priced independently.
Call spanning a peak boundaryPriced 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.

esc