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

pseudo
// 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

json
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.

WhereWhat
/reportrendered 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.mdone day as JSON or markdown; latest is an alias
POST /api/internal/report?date=yesterday&force=1regenerate (Bearer CRON_SECRET)

How to verify a signature

ts
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 string

signer 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.