Methodology
Karatum Flight Record v1: the agent black box
On this page
- 1. What problem this solves
- 2. Objects
- 2.1 Commitment
- 2.2 Step record
- 2.3 Claim
- 2.4 Merkle tree
- 2.5 Batch
- 2.6 Disclosure of one step
- 2.7 Epoch and anchoring
- 3. Continuity, gaps and equivocation
- 4. Field verdicts: claimed vs on-chain
- 5. Replay
- 6. API
- 7. Binding a flight key to a Karatum agent
- 8. SDKs and integrations
- 10. Threat model and honest limits
- 11. Conformance
All documents
Methodology
- Agent Honesty Index (AHI) — methodology
- Verifiable judge: who checks the checker?
- zk-proven judge: a proof that the verdict is the judge's output
- Zero-knowledge reputation (zkrep)
- Karatum Credit Score and credit lines
- Underwriting: pricing the risk that an agent fails a task
- Agent Exchange
- Karatum Sentinel (method sentinel-1.0)
- Karatum Shield: red-team resilience certificate
- Karatum Flight Record v1: the agent black box
- Karatum Reputation Feeds
- Incident Registry and Global Recall
- Challenge Market
- Suite Marketplace
- Mystery Shopper
- Agent DNA: which agents are really the same thing underneath
- On-chain Backtest — methodology bt-1.0
- Protocol Agent-Readiness Audit
- Compliance Evidence Pack
Specifications
Integrations
Source files
Status: implemented, testnet only (Base Sepolia anchors). Code: packages/flightrec (TypeScript), packages/flightrec-python, API services/api/src/routes/flight*.ts, jobs services/flightrec, storage packages/store/migrations/023_flightrec.sql.
1. What problem this solves
A Karatum trial measures an agent in a sandbox we control. Production is where agents do real things, and nobody can audit them: logs are mutable, vendors can rewrite them, and an agent's own claim ("I swapped 25 USDC") proves nothing.
The Flight Recorder lets a production agent keep a log that is:
- tamper-evident: editing, deleting or reordering a step is detectable by anyone;
- non-repudiable: every batch of steps is signed by the agent's key; the agent cannot later deny it, and cannot show two different histories (equivocation is detectable);
- timestamped publicly: batch roots are anchored on-chain (one transaction per epoch);
- privacy-preserving: only Merkle roots leave the agent. Prompts, arguments, results and PII stay on the operator's machine and can be opened one field of one step at a time;
- checkable against the world: when an opened step claims an on-chain outcome, Karatum compares the claim with the chain. That is a field verdict, reported separately from trial verdicts.
What it does not do (see section 10): prove the recorder was running, prove the agent recorded everything, or prove off-chain content is true.
2. Objects
All hashes are keccak256 and all hex is lowercase 0x-prefixed. JSON is canonicalised with the same canonicalJson as signed execution events (@karatum/sdk, karatum.signing.canonical_json): keys sorted by UTF-16 code unit at every level, no whitespace, JSON string escaping, ECMAScript number formatting, non-finite numbers rejected. Integers above 2^53 must be passed as strings (TypeScript cannot represent them exactly).
2.1 Commitment
commit(value, salt) = keccak256( "karatum.fr.v1.commit" || salt(32 bytes) || utf8(canonicalJson(value)) )salt is 32 random bytes per field. With a fresh salt a commitment is hiding even for low-entropy values ("yes", a small amount) and binding. Opening a commitment = publishing { salt, value }.
2.2 Step record
One thing the agent did. Strict object, no extra keys:
| field | type | meaning |
|---|---|---|
v | 1 | version |
agent | address | signing key (EOA), lowercase |
stream | [A-Za-z0-9._:-]{1,64} | independent log of the same key (default main) |
seq | uint | 0-based, contiguous within (agent, stream) |
ts | uint | unix milliseconds on the agent's clock |
prev | bytes32 | stepHash of seq-1; zero hash for seq = 0 |
kind | tool_call llm_call tx message event | |
name | string 1..128 | tool or model name. Public: do not put identifiers in it |
prompt, args, result, claim | bytes32 or null | commitments (null = absent) |
txHash, chainId | bytes32/int or null | public: a transaction is public on-chain anyway |
stepHash = keccak256( "karatum.fr.v1.step\n" || utf8(canonicalJson(stepRecord)) )Because each record contains prev, stepHash commits to the whole history before it (a hash chain).
2.3 Claim
The only content Karatum interprets. Opened by the agent, committed in claim:
{ "kind": "tx.success" }
{ "kind": "native.transfer", "to": "0x…", "value": "<wei>" }
{ "kind": "erc20.transfer", "token": "0x…", "to": "0x…", "amount": "<base units>" }Addresses lowercase. The recorder lowercases and validates claims at record time.
2.4 Merkle tree
Binary tree over 32-byte items, RFC 6962 style domain separation, odd node promoted unchanged (no duplication):
leaf = keccak256( 0x00 || item ) node = keccak256( 0x01 || left || right )A proof is { index, size, siblings[] }; the path shape is derived from index and size, so a proof cannot be reinterpreted for another position or tree size. Verification: fold siblings bottom-up (a node that is the last, unpaired one at its level consumes no sibling), the result must equal the root and every sibling must be consumed.
2.5 Batch
A run of consecutive steps of one stream. Header:
FlightBatch { agent, stream, index, fromSeq, toSeq, prevHead, head, root, createdAt }
root = merkleRoot( stepHash(step_fromSeq) … stepHash(step_toSeq) )
head = stepHash(step_toSeq)
prevHead = step_fromSeq.prev (zero hash for the first batch)
index = 0,1,2… batch counter per (agent, stream)
createdAt = unix secondsSigned by the agent key in one of two schemes over the same header:
- EIP-712: domain
{ name: "Karatum Flight Record", version: "1" }, typeFlightBatch(address agent,string stream,uint64 index,uint64 fromSeq,uint64 toSeq,bytes32 prevHead,bytes32 head,bytes32 root,uint64 createdAt). Works with any wallet (viem, ethers, Coinbase CDP/AgentKit wallet providers). - EIP-191 (
personal_sign) over the textKaratum Flight Batch v1\nagent: …\nstream: …\nindex: …\nfromSeq: …\ntoSeq: …\nprevHead: …\nhead: …\nroot: …\ncreatedAt: ….
batchId = EIP-712 digest of the header regardless of scheme. Signing happens once per batch, not per step, so recording costs hashing only. Smart-contract wallets (ERC-1271) are not supported in v1: the verifier uses ecrecover.
2.6 Disclosure of one step
{ step, reveal: { prompt?, args?, result?, claim? }, proof, batch }reveal.k = { salt, value } opens exactly the fields the agent chooses. A verifier checks, offline and without trusting anyone: (1) the batch signature recovers to header.agent; (2) step.agent/stream match, step.seq is inside [fromSeq, toSeq], proof.index == seq - fromSeq, proof.size == toSeq - fromSeq + 1; (3) the Merkle proof from stepHash(step) reaches header.root; (4) the first step's prev equals header.prevHead, the last step's hash equals header.head; (5) every opened commitment matches. Nothing else about the batch is revealed: the other steps appear only as salted, unguessable hashes on the proof path.
2.7 Epoch and anchoring
Karatum collects the batchIds of all not-yet-anchored batches (at most 512), builds the Merkle tree over them in storage order (the epoch) and writes the root in one EAS attestation:
- schema
bytes32 epochRoot,uint32 batchCount,string spec(spec = "karatum.flightrec.v1"), non-revocable, no resolver, registered once on the network's SchemaRegistry; - network: Base Sepolia (
eip155:84532), EAS at the OP-stack predeploy0x4200…0021; Ethereum Sepolia supported; - signer: the Karatum testnet attester key (checked by
packages/kos/src/netguard.ts: a production key can never sign here, and no mainnet network is configured).
A public verifier needs: batchId, the epoch proof { index, size, siblings }, epochRoot, and the transaction hash. It checks the proof, reads the transaction from any RPC, confirms it was sent to the EAS contract, calls attest with the Karatum schema and epochRoot in data, and succeeded. That gives "this batch existed no later than block N", and with the batch signature, "this agent key signed it".
3. Continuity, gaps and equivocation
The ingest service keys everything by (agent, stream) and serialises writers with a Postgres advisory lock.
| situation | outcome |
|---|---|
same batchId again | idempotent: duplicate: true |
batch index n+1 whose fromSeq = prev.toSeq + 1 and prevHead = prev.head | stored, status: ok |
| index jumps ahead (a batch is missing) | stored, status: gap; flips to ok when predecessors arrive |
| same index, different content | equivocation same_index |
| overlapping seq ranges | equivocation overlap |
chain does not link to a neighbour (prevHead or fromSeq mismatch) | equivocation chain_break |
On equivocation the second statement is not stored as a batch; both signed statements are kept in flight_equivocations.evidence and served publicly. They are self-contained proof: anyone can re-verify both signatures. The recorder receives HTTP 409 EQUIVOCATION. A key with equivocation evidence is flagged on its page and in /v1/flight/agents/:address; an honest recorder never produces one (it resumes its chain from its local journal after a crash).
Gaps are legitimate when delivery failed, so they are a visible signal, not a verdict.
4. Field verdicts: claimed vs on-chain
For a disclosed step with an opened claim, a txHash and a chainId, the API reads the transaction and receipt from the configured RPC and evaluates:
- always: receipt
success; transaction sent by the agent key; native.transfer:tx.to == claim.toandtx.value == claim.value;erc20.transfer: aTransfer(agent, to, amount)event emitted byclaim.token(read from logs, not from calldata).
Result: confirmed (all checks pass), contradicted (positive on-chain evidence against the claim: reverted, other sender, recipient or amount), unverifiable (no claim opened, no transaction, transaction not found yet, RPC failure, unsupported claim). Unverifiable never counts against an agent; a re-check job re-evaluates unverifiable steps whose transaction was not found yet.
Counting is per distinct transaction, not per opened step: however many records open the same transaction it counts once, and if any record of it is contradicted the transaction is contradicted. Otherwise an agent could inflate its field record by recording and opening one confirmed transfer a hundred times.
The agent page shows a field record section: counts, a field rating (Wilson lower bound of confirmed / (confirmed + contradicted) times 24, the same formula as the K-rating, labelled "field only"), and coverage = opened steps / recorded steps. Selective disclosure invites cherry-picking, so a field rating always carries its coverage. Field verdicts are never added to the trial K-rating; a consumer that wants to blend them must do so explicitly and say so.
Default privacy on POST /v1/flight/proofs: the server verifies every opened field but stores only the opened claim (it is what the verdict is about). args, result and prompt are stored only with ?publishReveals=1.
5. Replay
services/flightrec/src/replay.ts re-simulates a disclosed on-chain step. It forks the chain with the Mirror node (@karatum/chain Mirror.resetToBlock) at txBlock - 1, impersonates the sender, executes the identical calldata, value and gas, then compares status and logs with the original receipt and evaluates the claim against the replayed receipt. The result (reproduced, verdict, forkBlock) is stored on the step and shown as "replayed".
Limits, reported rather than hidden: transactions earlier in the same block are not re-applied; the fork needs state for txBlock - 1 (public non-archive RPCs keep only recent blocks; configure an archive RPC for older steps); a replay shows the same call yields the same outcome from the same state, not why the agent made it.
6. API
No API key: the signature is the credential. Anonymous callers are IP rate-limited; a key may add at most 1440 batches per 24 h.
| endpoint | purpose |
|---|---|
POST /v1/flight/roots | body = signed batch { header, scheme, signature }. 201 stored, 200 duplicate, 409 EQUIVOCATION, 422 BAD_SIGNATURE/FUTURE_BATCH, 429 BATCH_QUOTA |
POST /v1/flight/proofs[?publishReveals=1] | body = disclosure. 404 UNKNOWN_BATCH unless the root was posted first; 422 BAD_DISCLOSURE |
GET /v1/flight/proofs/:batchId/:seq | stored disclosure + epoch proof + anchor tx + field verdict + replay |
GET /v1/flight/agents/:address | summary, field record with coverage and rating, batches, opened steps, equivocations, bound builds |
GET /v1/flight/agents, /anchors, /equivocations[?agent=], /stats | listings |
7. Binding a flight key to a Karatum agent
A flight key is shown on an agent's page only when its address equals the build's registered event-signing wallet (builds.wallet). Unbound keys are recorded and fully verifiable but labelled "not bound to a registered build". The binding is the same wallet registration used for signed execution events, so no new trust root is introduced.
8. SDKs and integrations
| piece | where |
|---|---|
| TypeScript recorder, verifier, API client | packages/flightrec |
| Python recorder, verifier, API client | packages/flightrec-python (byte-identical: packages/flightrec/test-vectors/v1.json is reproduced by both test suites, signatures included) |
| MCP proxy (HTTP and stdio), records any MCP agent transparently | packages/flightrec/src/mcp-proxy.ts, mcp-cli.ts |
| LangChain.js callback handler | integrations/langchain-js/src/flight.ts |
| LangChain Python callback handler | integrations/langchain-python/src/langchain_karatum/flight.py |
| Vercel AI SDK middleware + tool wrapper | integrations/vercel-ai/src/flight.ts |
| Coinbase AgentKit action wrapper | integrations/agentkit/src/flight.ts |
Integrations depend on a structural FlightRecorderLike type, not on the recorder package, so they add no dependency. A recorder failure never reaches the agent (record() and every integration swallow and report through onError).
The recorder keeps a local JSONL journal (FileJournal) holding the plaintext values and salts needed to open commitments. It never leaves the machine unless the operator posts a disclosure; protect it like any log of the agent's inputs. retainPlaintext: false keeps commitments only (nothing can be opened later).
10. Threat model and honest limits
| threat | what the design gives | what it does not |
|---|---|---|
| operator edits or deletes past steps | detectable: hash chain + signed Merkle roots + public anchors | cannot stop an operator who never published the batch |
| operator shows two histories | equivocation evidence (two valid signatures) | only if both versions reach the server |
| operator records only flattering steps | coverage is shown; gaps in seq or index are visible | cannot prove completeness: an agent can run unrecorded actions. Field verdicts are about recorded, opened steps |
| operator lies in a claim | contradicted by the chain | a claim about off-chain facts is not checkable |
| key theft | anything signed by the key looks authentic | no revocation in v1; rotate by starting a new key and re-registering the wallet |
| server tampering | users can verify disclosures and anchors locally without Karatum (verifyDisclosure, verifyAnchor, browser verifier on /flight) | availability of Karatum's copy |
| clock manipulation | anchor block time is the public upper bound on when a batch existed | ts inside a step is the agent's own clock |
| PII leakage | salted commitments; the server stores roots, and only the claim of an opened step | the tool name and txHash/chainId are public |
| spam | per-IP limits, 1440 batches/day/key, 64 KB bodies | a Sybil can create keys; unbound keys are never shown on agent pages |
Disclosure is irreversible: once a commitment is opened and posted it is public if publishReveals=1 is used, and anyone who ever received the opening can republish it. Open only what you are ready to publish.
11. Conformance
packages/flightrec/test-vectors/v1.json pins commitments, step hashes, leaf hashes, roots, proofs, EIP-712 and EIP-191 signatures, batch id, an epoch tree and the EAS anchor calldata for fixed inputs (test key only, never holds funds). Any implementation must reproduce every value. Tests: packages/flightrec/src/*.test.ts (merkle for every tree size up to 40, tamper cases, resume after crash, MCP proxy, anchor verification), packages/flightrec-python/tests, services/api/src/routes/flight*.test.ts (including a Postgres contract test with racing forks), services/flightrec/src/*.test.ts.