Trust22
Ledger & reports
Every decision the oracle or the operator makes is one entry in a single hash chain, and the chain is periodically rooted on Arc. Screens, re-screens, tier changes, EDD answers, payouts, operator ticks, tips, refunds, reports and the anchors themselves all share the same formula and the same verify endpoint.
Hash chain
// src/lib/ledger/decisionLog.ts
hash = keccak256( prevHash ‖ utf8(canonical(payload)) ‖ uint64be(seq) ‖ utf8(kind) )
genesis prevHash = 0x00…00 (32 bytes) · seq starts at 1 and is contiguous
canonical() = keys sorted recursively, arrays in order, no whitespace, undefined dropped, bigint → decimal string
// src/lib/ledger/anchor.ts - Merkle root over the hashes of every unanchored entry, in seq order
pair(a, b) = keccak256(min(a, b) ‖ max(a, b)) // sorted pairs, so proofs need no position bits
odd level → last leaf is duplicated
root → AttestationRegistry.anchorRoot(root, fromSeq, toSeq) // every ANCHOR_EVERY_N = 50 entries
→ ANCHOR ledger entry { root, fromSeq, toSeq, tx, count } and anchorTx set on each entry in the range- Kinds
- SCREEN · RESCREEN · TIER_CHANGE · EDD_RESPONSE · NOOP (yield plans) · OPERATOR (operator ticks, executed YIELD_SWEEP / YIELD_REDEEM) · ANCHOR · PAYOUT · OPERATOR · TIP · REFUND · REPORT
- Append
- serialised in-process so seq is contiguous; id = dec_ + base36 time + seq + random
- Privacy
- payloads never include another client's book; EDD answers are logged only as keccak(answers); tip submitters are anonymised
- Anchoring today
- pending: anchorRoot needs the deployed registry and the attester key; entries accumulate with anchorTx null and anchor in one batch after the deploy
One entry
GET /api/v1/decision/dec_…
{
"id": "dec_…", "seq": 1042, "kind": "SCREEN", "subject": "0x…", "clientId": "treasury-7",
"payload": { "address", "score", "tier", "reasons", "recommendedLimits", "rationale", "eddRequested", "nextRescreenAt",
"evidenceDepth", "collectedAt", "analyst": { "model", "fallback", "latencyMs" }, "guards": [ … ], "screenedAt",
"priorTier", "clientId", "decisionHash", "attestation": { "signature", "attester", "validUntil" } },
"prevHash": "0x…", "hash": "0x…", "createdAt": 1760000000, "anchorTx": null
}The payload is the decision core that was hashed into decisionHash and signed; the evidence bundle lives on the attestation record and the verdict, so the ledger stays small. /ledger lists entries and /log/:id renders one with a replay of the verdict.
Verify
- GET /api/v1/ledger/verify
- re-walks the chain from genesis: { ok, brokenAt, length }; brokenAt is the first seq whose prevHash, seq or recomputed hash does not match
- GET /api/v1/ledger?limit=50
- newest entries
- GET /api/v1/decision/:id
- one entry with its anchorTx
- A verdict
- decisionHash in the attestation == keccak256(canonical(payload)) of its ledger entry; signature recovers to the published attester
- An anchor
- recompute the Merkle root over hashes fromSeq..toSeq and compare with the Anchored(root, fromSeq, toSeq) event on Arc
Signed daily report
Once per UTC day the agent generates a report from its own database, canonicalises and hashes the JSON, signs the hash string with the attester key as an EIP-191 personal_sign, stores it, and appends a REPORT ledger entry carrying the same hash. The entry is anchored with the rest of the chain.
| Where | What |
|---|---|
| /report | rendered latest report, “Copy markdown”, signature, archive |
GET /api/v1/report | { date, hash, signature, signer, ledgerId, createdAt, report, archive[] }; ?format=md |
GET /api/v1/report/:date · :date.md | one day as JSON or markdown; latest is an alias |
POST /api/internal/report?date=yesterday&force=1 | regenerate (Bearer CRON_SECRET) |
How to verify a signature
import { keccak256, stringToHex, verifyMessage } from "viem";
import { canonicalize } from "./src/lib/attest/canonical"; // or re-implement: sorted keys, no whitespace
const { report, hash, signature, signer } = await (await fetch("https://trycomplir.xyz/api/v1/report")).json();
const recomputed = keccak256(stringToHex(canonicalize(report)));
console.log(recomputed === hash); // the hash is over the canonical JSON
console.log(await verifyMessage({ address: signer, message: hash, signature })); // EIP-191 personal_sign over the hash stringsigner is the attester address, the same key that signs attestations. If the report is later regenerated with force=1, the hash changes and a new REPORT entry is appended; the old entry stays in the chain. The refunds and report page lists what a report contains.