← Research

How the ledger chain works

This page explains the row hash and chain head on the Ledger surfaces: the per-period root, the rowHash on a receipt, and the export you download.

What it is

Every priced request we meter becomes one row in a per-workspace hash chain. A row's hash covers its own measured fields plus the previous row's hash, so changing, reordering, or deleting a row changes every hash after it. The recipe is published and versioned as recovea-chain-v1. You re-derive the chain from an export with only a SHA-256 implementation. Nothing calls back to us.

How it is computed

Money is integer micro-USD end to end. 1,000,000 micro-USD is one dollar. No float touches money on this path.

1. Cost. Rates are micro-USD per million tokens. Every rate x tokens product is summed in 128-bit integers, then floor-divided by 1,000,000 once at the end, never per component, so rounding cannot accumulate. An unknown model produces no cost, never a guess. Full arithmetic is on the spend page.

2. Canonical encoding.

Field kindRule
IntegersBase-10, no leading zeros, 0 for zero
cost_micro_usdInteger micro-USD, not a formatted currency string
timestamp_msUnix epoch milliseconds
provideropenai or anthropic, lowercase
basismeasured or applied
NullThe empty string

3. Field order. recipe_id, previous_hash, request_id, timestamp_ms, provider, model, route, tokens_in, tokens_out, tokens_cached, cost_micro_usd, price_list_version, basis, mandate_id.

4. Join and hash. Join the 14 parts with the single byte U+001F. SHA-256 the UTF-8 bytes. Render lowercase hex. That is row_hash, never an input to itself.

5. Link. The first row's previous_hash is genesis, 64 zeros; later rows use their predecessor's row_hash. Rows append in arrival order, inside one transaction holding a row lock on the workspace chain head, so the chain cannot fork. Each row also stores its position in the chain (chain_seq), and that position — not the timestamp — is the order an export walks. Ordering by time would be wrong: a receipt's timestamp is when the request started, but it is written when the request finishes, so a long streaming call legitimately arrives after shorter ones that started later. The chain head is the last row's hash.

Worked example

The first committed golden vector, which every implementation must reproduce. Parts 1 to 14:

recovea-chain-v1, 64 zeros, req_9f2c41d87ab34e60, 1750000000000, openai, gpt-4.1, /v1/chat/completions, 1200, 340, 0, 4180, rpl-2026-06-01, measured, empty.

Joined with U+001F that is 193 bytes. Its SHA-256:

2f542e00b0403c4e95403ef7fe6ac40d24c6598e0d100ee6cc6ec8a6d18152e2

Feed that in as part 2 of the next row and repeat.

What it does not include

  • Content. No prompt, completion, header, or secret. No field can hold one.
  • lever and row_hash. Neither is a hash input in v1.
  • Attribution identifiers. Key id, key prefix, project, tags, and workspace linkage sit outside the hash, so retention deletion can sever them and downloaded exports still verify.
  • Display strings. Formatted currency and ISO timestamps render at the edge.
  • Unpriced rows. A request we could not price, and a coalesced deduplication follower that made no provider call, carry a null cost and never chain. They settle later as a correction row under a new price list version. Corrections are new rows, never edits.
  • The invoice. Every export carries the line the meter measures; the provider invoice is the bill.

Limits and current status

Live. The recipe, the golden vectors, the transactional append, and both exports: the session route GET /platform/v1/ledger/export and the key-authed route GET /v1/ledger/export, each in JSONL, JSON, and CSV.

Drift we have not closed. Two export serializers exist, and they spell the same fields differently.

FieldKey-authed GET /v1/ledger/exportSession GET /platform/v1/ledger/export
recipe idreciperecipe
previous row hashprevious_hashpreviousHash
request idrequest_idrequestId
start timestamptimestamp_mstimestampMs
provider, model, routeprovider, model, routeprovider, model, route
token countstokens_in, tokens_out, tokens_cachedtokensIn, tokensOut, tokensCached
integer micro-USDcost in JSON and JSONL, column cost_micro_usd in CSVcostMicroUsd in all three
price list versionprice_list_versionpriceListVersion
basisbasisbasis
leverleverlever, a display companion after rowHash
mandate idmandate_idmandateId
row hashrow_hashrowHash
extra columnsnone, 16 in CSVts and cost, 18 in CSV

One trap follows from that table: cost is the integer micro-USD value on the key-authed route and a fixed-8-decimal display string on the session route. Hash the integer, never the string. The header block is camelCase on both routes (workspaceId, chainHead, rowCount, exportedAt, refPriceVersions), and the key-authed CSV puts lever between basis and mandate_id while the session CSV puts it last. Both re-derive to the same hashes, but the files are not byte-identical. Separately, the API's methodology registry cites /research/metering and /research/limits, which are not pages here.

Built, not activated. A second recipe, recovea-receipt-v1, is implemented and pinned by its own vectors, hashing a wider set including customer id, reasoning tokens, HTTP status, and traffic environment. Nothing writes it yet; every chained row today is recovea-chain-v1. The mandate_id slot is reserved: always null, always hashed as the empty string, so activating it later will not fork the recipe.

A gap we found ourselves. On 2026-07-16 our own census caught the console export coercing an unpriced row's null linkage to genesis, which made the downloaded file fail offline re-derivation. Fixed: unpriced rows are now excluded, matching the key-authed surface.

Header caveat. The header's genesis is always 64 zeros, even when the exported period starts partway through your chain. For any period other than your first, start from the first row's own previousHash.

Numbers next to this one.

SurfaceStatus today
BudgetsOnly workspace-scoped caps reach the gateway. Key- and project-scoped caps are not enforced yet.
AlertsThe sweep runs about once a minute, so a fast burst can reach a cap between checks. Alerts warn on the way up; they are not an ordering guarantee.
CounterfactualMeasured, never applied. No lever is activated in any environment we run, and only deduplication has a wired hot path.
Cost per successful outputServed at list price. Invoice reconciliation at period close is planned, not wired.
Recovea ScoreThe engine is not shipped. No workspace has a score.

Where to check us

Field names below are the session route's. On the key-authed route the same fields are previous_hash and row_hash; chainHead is in the header, which is camelCase on both.

  1. Download an export for a period. Any format carries everything needed.
  2. Take the first row. Its previousHash is your starting value.
  3. For each row, encode the 14 parts in order, join with U+001F, SHA-256, lowercase hex, and compare against that row's rowHash.
  4. Feed each derived hash forward as the next row's previousHash.
  5. Compare your final value against chainHead in the header and the root shown in the product.

If a row disagrees, tell us. A chain that does not re-derive falsifies the claim this page makes, and we treat that as a disclosable incident, not a support ticket.