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.
-
Take fourteen values, in this order:
Text1. 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_idThe first two are the recipe id and the previous row's hash; the other twelve are the receipt's fields.
-
Join them with the unit separator, U+001F, and encode the result as UTF-8.
-
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:
0000000000000000000000000000000000000000000000000000000000000000How each value is written
- Integers are base-10, with no leading zeros.
timestamp_msis Unix epoch milliseconds.cost_micro_usdis integer microdollars; 1000000 is one dollar.provideris hashed as the export writes it:openai,anthropicoropenai_compatible.basisismeasuredorapplied.- An absent
mandate_idis 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:
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 2f542e00b0403c4e95403ef7fe6ac40d24c6598e0d100ee6cc6ec8a6d18152e2In a shell, with printf '\037' as the separator:
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}" | sha256sum2f542e00b0403c4e95403ef7fe6ac40d24c6598e0d100ee6cc6ec8a6d18152e2 -The second row's previous hash is the first row's hash, and nothing else links them:
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())
# 131c454755e3ea86a890f82dac42d1b61a5a336ef2b228ac33ef7d49ed378281Change 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:
- For each row, join
recipe,previousHash,requestId,timestampMs,provider,model,route,tokensIn,tokensOut,tokensCached,costMicroUsd,priceListVersion,basisandmandateIdwith U+001F, treating a nullmandateIdas the empty string, and compare the SHA-256 hex withrowHash. - Check that each row's
previousHashequals therowHashof the row before it. - Check that the last row's
rowHashequals thechainHeadin 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.