Verifying decisions

Check that a published decision record is exactly what the chain acted on, step by step.

Every decision exists in two places: a summary on chain in DecisionLog, and the full record at the location the summary points to. Checking that they match takes five steps.

The record

{
  "kind": "grade",
  "subject": "214",
  "question": "How well does the result meet the brief? ...",
  "request": { "state": "...", "questions": { "decision": { "type": "score" } } },
  "model": "jev-1.14.0",
  "answer": "accept",
  "confidence": 0.95,
  "scoreConfidence": 0.64,
  "probabilities": { "0": 0.05, "1": 0.14, "2": 0.81 }
}

Grade probabilities are keyed by level index, counted from the lowest level as 0. The brief lists its levels best first, so the keys run in the reverse order of the brief.

A grade's confidence is the probability on the paid levels, so you can recompute it. In the record above, the brief has three levels and pays from the second, so the paid levels are keys 1 and 2: 0.14 + 0.81 = 0.95. scoreConfidence is Jev's own measure of how concentrated the score is. It is kept for reference, and the chain does not gate on it.

Steps

  1. Read the decision. Call DecisionLog.get(id) (ids start at 1). Find its Recorded event by filtering on the indexed id, for example client.getContractEvents({ address, abi, eventName: "Recorded", args: { id }, fromBlock: 0n }), and read payloadHash and payloadURI from it.
  2. Fetch the record from payloadURI.
  3. Check the record. Serialize it canonically, with object keys sorted and no whitespace, and compute keccak256 of the UTF-8 bytes. It must equal payloadHash.
  4. Check the input. Serialize the record's request field the same way. Its keccak256 must equal the on-chain inputHash.
  5. Check the confidence. confidence × 10,000, rounded, must equal the on-chain confidenceBps.
import { keccak256, toBytes } from "viem";

function canonical(value: unknown): string {
  if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
  if (value && typeof value === "object") {
    return `{${Object.keys(value)
      .sort()
      .map((key) => `${JSON.stringify(key)}:${canonical((value as Record<string, unknown>)[key])}`)
      .join(",")}}`;
  }
  return JSON.stringify(value);
}

const ok =
  keccak256(toBytes(canonical(record))) === payloadHash &&
  keccak256(toBytes(canonical(record.request))) === decision.inputHash &&
  Math.round(record.confidence * 10_000) === decision.confidenceBps;

What this proves

That the published question, request, answer and probabilities are the ones recorded on chain before the decision was applied, and that the confidence the contract compared against its threshold is the one in the record.