Evidence format — results transcripts & dispute packages
Two values live off-chain: the resultsRoot an operator anchors through
PaymentManager.commitResults, and the evidenceURI a client hands to
DisputeManager.raiseDispute. Both rest on the same bytes: every served call recorded, those
records hashed into the root the contract stores, a dispute package that carries them, and
verify-evidence to prove what it can from that package. Most sessions never need any of it.
After settlement the agent rates the operator on-chain, 0–100, from the errors and slow answers it
met, and that verdict is the marketplace's everyday quality signal. A dispute is the stronger
claim, that an operator took payment without honestly delivering, and such a claim needs bytes
both sides can rebuild independently and a stranger can check. The source of truth is
offchain/src/sdk/results-core.ts (pure; ethers binding results.ts, byte-identical viem copy in
@aioum/sdk, drift-guarded), with pinned cross-implementation test vectors in both suites. Design
rationale: dispute-verifiability.md.
1. Call transcripts
Every served call is recorded twice, independently. The node keeps it in a durable
TranscriptStore (retained ≥ AIOUM_TRANSCRIPT_RETENTION_MS, default 7 days) and the client
keeps it in PaidSession.calls. Both write the same entry:
{ "index": 0, // 0-based position in the session, gap-free
"toolId": "wordcount", // the tool name exactly as called
"request": "…", // canonical request JSON (below)
"response": "…" } // the raw text the tool returned
The canonical request is stableStringify({ arguments, tool }): JSON with object keys
sorted at every depth, arrays in order, no whitespace, undefined values dropped,
bigints/non-finite numbers rejected. Only client-visible data enters the transcript, which is
what makes the root reproducible by both sides independently: neither trusts the other's record
to arrive at the same bytes.
2. Hashes and the Merkle root
All hashes keccak256; all strings UTF-8.
requestHash = keccak(utf8(request))
responseHash = keccak(utf8(response))
toolIdHash = keccak(utf8(toolId))
leaf_i = keccak( uint256(index) ++ requestHash ++ responseHash ++ toolIdHash )
(++ = concatenation; the leaf preimage is the abi.encoding of four static 32-byte words.)
Tree: binary Merkle over leaves in call order; parent = keccak(left ++ right); an odd
trailing node is promoted unchanged (never duplicated); a single leaf is its own root;
an empty transcript has no root (0x000…0 = absent, never committed on-chain).
Inclusion proofs are bottom-up sibling lists; a level where the node is the promoted odd
one contributes no sibling (merkleProof/verifyProof).
The node updates the session record's root after every call, and the settle loop anchors it
on-chain (PaymentManager.commitResults, write-once, Open-only) before settling. For services
flagged commitRequired in the ServiceRegistry, settle with a nonzero amount reverts without
an anchored root; the flag is snapshotted per session at open, so an operator cannot settle paid
work on such a service without first committing to what it served.
3. The evidence package
A dispute's evidenceURI should point to (e.g. IPFS-pinned) JSON:
{
"formatVersion": 1,
"sessionId": "42",
"chainId": 31337,
"paymentManager": "0x…", // the vouchers' EIP-712 verifying contract
"claim": "non-delivery" | "wrong-result" | "quality",
"narrative": "free text for the subjective residue",
"calls": [ /* transcript entries, §1, index-ordered from 0 */ ],
"vouchers": [ { "cumulativeAmount": "1000000", "signature": "0x…" }, … ],
"resultsRoot": "0x…", // assembler's own root over `calls`
"attestations": [ /* §5, optional */ ]
}
Either side can assemble one. The client calls session.evidencePackage(claim, narrative?)
from @aioum/sdk; the session recorded everything, so the package is one call away. The
operator exports its own record of the same session with
npm run verify-evidence -- --export --session <id> [--store node|multinode].
4. Verification (verify-evidence)
cd offchain
npm run verify-evidence -- evidence.json [--exec "<command>"] [--json]
Point it at a package and it runs seven checks, each reported ✓ (proven), ✗ (failed) or ○ (not applicable or unverifiable):
- session — exists on-chain; facts (client, operator, deposit, settled spent).
- transcript / claimed-root — entries well-formed & gap-free; the package's own root matches its own calls.
- anchored-root — recomputed root vs
sessionResultsRoot: match proves the operator anchored exactly this history; mismatch proves someone's record isn't what was anchored; absent root on a commit-required session is itself a failure (the operator cannot have settled paid work). - vouchers — each signature authorized by the session client (EOA recover, ERC-1271 staticcall for smart accounts), cumulative amounts strictly increasing and ≤ deposit.
- re-execution (
--exec, deterministic services only) — the command gets each call's canonical request JSON on stdin and must print the exact response to stdout (one trailing newline tolerated); divergence fails the check. - attestations — dispatched to registered verifiers by type; unrecognized types are reported ○ UNVERIFIED, never silently accepted.
- quality claims — always ○: quality is arbitrator judgment; the report says so instead of green-checking it.
Exit codes: 0 = nothing failed · 1 = at least one ✗ · 2 = usage/IO error.
5. Attestations (Tier 3 hooks)
{ "type": "tee-nitro" | "tee-sgx-dcap" | "zkml" | …, // selects the verifier
"format": "…", // verifier-defined
"payload": "base64…",
"coverage": [0, 1, 2], // call indexes covered (absent = all)
"note": "…" }
The verifier registry is an injection point (VerifyHooks.attestationVerifiers): TEE quote
verification (vendor endorsement chains, measurement policies) and zkML proof systems come from
the verifiers registered there; format, dispatch and reporting are common to all. An attestation
with no registered verifier is always surfaced as UNVERIFIED, never trusted.
6. Declaring a deterministic service
In the service's descriptor card (the descriptorURI JSON), add:
{ "deterministic": { "kind": "docker", "image": "ghcr.io/…@sha256:…", "entry": "…" } }
Any re-executor that can reproduce the environment may then wire --exec. A service
without this declaration must NOT be judged by re-execution, because LLM outputs legitimately
vary; its disputes rest on the anchored-transcript and voucher checks plus the arbitrator's
judgment.
7. Environment knobs (node)
| Env | Default | Meaning |
|---|---|---|
AIOUM_COMMIT_RESULTS | 1 | record transcripts + anchor the root before settle (0 = off; a commit-required service then cannot settle paid work) |
AIOUM_TRANSCRIPT_RETENTION_MS | 7 days | how long transcripts outlive their last call — keep it beyond settleChallengeWindow |
AIOUM_NODE_DIR | ~/.aioum-node | where *-transcripts/ lives (one file per session; an earlier single *-transcripts.json is split once at boot and kept as .migrated), next to the voucher store |
Local development network: stores are keyed by session id, which restarts at 1 when you
reset the local development network — wipe AIOUM_NODE_DIR whenever you redeploy it, or old
transcripts/vouchers will be attributed to new sessions. The AIOUM network never reuses ids.
That is the whole verifiability layer at byte level: a transcript both sides record independently, a root the contract stores before any paid work settles, and a package anyone can verify without trusting either party. It stands behind every rating an agent issues and every dispute a person raises, and none of it asks the person to keep records.
Next: Example services — the catalogue of services agents actually buy, and how an agent composes them in one run.