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):

  1. session — exists on-chain; facts (client, operator, deposit, settled spent).
  2. transcript / claimed-root — entries well-formed & gap-free; the package's own root matches its own calls.
  3. 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).
  4. vouchers — each signature authorized by the session client (EOA recover, ERC-1271 staticcall for smart accounts), cumulative amounts strictly increasing and ≤ deposit.
  5. 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.
  6. attestations — dispatched to registered verifiers by type; unrecognized types are reported ○ UNVERIFIED, never silently accepted.
  7. 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)

EnvDefaultMeaning
AIOUM_COMMIT_RESULTS1record transcripts + anchor the root before settle (0 = off; a commit-required service then cannot settle paid work)
AIOUM_TRANSCRIPT_RETENTION_MS7 dayshow long transcripts outlive their last call — keep it beyond settleChallengeWindow
AIOUM_NODE_DIR~/.aioum-nodewhere *-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.