Sign in

Concepts

The hash-chained ledger

The recipe recovea-chain-v1: fourteen fields in order, joined by U+001F, hashed with SHA-256 from a genesis of 64 zeros, and how to re-derive one receipt.

On this page

Every receipt in your organization's ledger carries a hash, and every hash is computed from the hash before it. Change one field of one row and that row's hash no longer re-derives, and neither does any hash after it. The recipe is published so you can do the recomputation yourself, with no Recovea code.

The recipe

The recipe's id is recovea-chain-v1.

  1. Take fourteen values, in this order:

    Text
    1. recipe_id
    2. previous_hash
    3. request_id
    4. timestamp_ms
    5. provider
    6. model
    7. route
    8. tokens_in
    9. tokens_out
    10. tokens_cached
    11. cost_micro_usd
    12. price_list_version
    13. basis
    14. mandate_id

    The first two are the recipe id and the previous row's hash; the other twelve are the receipt's fields.

  2. Join them with the unit separator, U+001F, and encode the result as UTF-8.

  3. Hash the bytes with SHA-256. The row's hash is the digest in lowercase hex.

The first row of a chain uses sixty-four zeros as its previous hash:

Text
0000000000000000000000000000000000000000000000000000000000000000

How each value is written

  • Integers are base-10, with no leading zeros.
  • timestamp_ms is Unix epoch milliseconds.
  • cost_micro_usd is integer microdollars; 1000000 is one dollar.
  • provider is hashed as the export writes it: openai, anthropic or openai_compatible.
  • basis is measured or applied.
  • An absent mandate_id is the empty string, so the input then ends with the separator.
  • A row's own hash is never one of its inputs, and neither is lever.

Re-derive one receipt

Take the first two rows of the recipe's test vector. The first row's previous hash is the genesis:

Text
request_id           req_9f2c41d87ab34e60
timestamp_ms         1750000000000
provider             openai
model                gpt-4.1
route                /v1/chat/completions
tokens_in            1200
tokens_out           340
tokens_cached        0
cost_micro_usd       4180
price_list_version   rpl-2026-06-01
basis                measured
mandate_id           (absent)
row_hash             2f542e00b0403c4e95403ef7fe6ac40d24c6598e0d100ee6cc6ec8a6d18152e2

In a shell, with printf '\037' as the separator:

Re-derive row one
S=$(printf '\037')
G=0000000000000000000000000000000000000000000000000000000000000000
printf '%s' "recovea-chain-v1${S}${G}${S}req_9f2c41d87ab34e60${S}1750000000000${S}openai${S}gpt-4.1${S}/v1/chat/completions${S}1200${S}340${S}0${S}4180${S}rpl-2026-06-01${S}measured${S}" | sha256sum
Text
2f542e00b0403c4e95403ef7fe6ac40d24c6598e0d100ee6cc6ec8a6d18152e2  -

The second row's previous hash is the first row's hash, and nothing else links them:

Re-derive row two from row one
import hashlib

previous = "2f542e00b0403c4e95403ef7fe6ac40d24c6598e0d100ee6cc6ec8a6d18152e2"
fields = [
    "recovea-chain-v1", previous,
    "req_b81d5c2e40f97a13", "1750000000750", "anthropic", "claude-sonnet-4",
    "/v1/messages", "2048", "512", "1024", "15750", "rpl-2026-06-01",
    "measured", "",
]
print(hashlib.sha256("\x1f".join(fields).encode("utf-8")).hexdigest())
# 131c454755e3ea86a890f82dac42d1b61a5a336ef2b228ac33ef7d49ed378281

Change any one of those values, tokens_out from 512 to 513 for example, and the second row's hash changes, and with it the previous hash of every row after it.

Re-derive your own export

Export a period from the platform's Ledger page (Prove). Each row of the export carries the fourteen values under camelCase names, in the recipe's order, followed by its rowHash:

  1. For each row, join recipe, previousHash, requestId, timestampMs, provider, model, route, tokensIn, tokensOut, tokensCached, costMicroUsd, priceListVersion, basis and mandateId with U+001F, treating a null mandateId as the empty string, and compare the SHA-256 hex with rowHash.
  2. Check that each row's previousHash equals the rowHash of the row before it.
  3. Check that the last row's rowHash equals the chainHead in the export's header.

A period's first row carries its stored previousHash, the last hash of the period before, so one month re-derives on its own; only the first period of a chain starts from the genesis.

What the chain shows

  • That a row has not changed since it was written, and none was reordered. An edit anywhere breaks the hashes from that row on.
  • What the row recorded, not that it is true of the world. The token counts in a receipt are the ones your provider reported on its own response.
  • Routed traffic only. A call that never went through the gateway has no receipt to chain.