Third-Party Verification Guide

How to verify an AgnCred work receipt yourself, offline, without trusting AgnCred's database or API. Everything you need is in one public response:

GET /api/v1/receipts/:id/verification-bundle

The bundle (bundle_version: "agncred-verification-bundle.v2"; v1 bundles you saved earlier keep verifying under the v1 rules) contains the exact immutable canonical payload and its hash, the attestation (decision, approved public fields, signature, signing key ID), the full sequenced event chain, the company's complete public key history, the server's own verification report (which you are about to reproduce), and formula instructions. v2 bundles for build-identified receipts (work-receipt.v0.4) additionally carry registered_manifests (every manifest hash registered per version, with its registration time) and lineage_events (the per-lineage registry chain, hash-linked from genesis exactly like the receipt chain).

Five independent checks (the fifth applies to v0.4 receipts only), and two separate facts to read at the end:

1. Verify the payload hash

Canonicalization is RFC 8785 (JCS): object keys sorted recursively by UTF-16 code units, ECMAScript number formatting, undefined members dropped. Compliance vectors live in contracts/test-vectors/jcs-vectors.json — your serializer must reproduce every expected string byte-for-byte.

2. Verify the attestation signature

The signature is Ed25519. Find the public key in signing_keys by the attestation's signing_key_id.

3. Verify the event chain

Events are per-receipt, sequenced, and hash-chained; the chain starts at the literal string genesis.

4. Verify the manifest binding (v0.4 receipts, VF-1)

Build-identified receipts carry manifest_hash and version_ref inside the canonical payload, so the payload-hash check (step 1) already covers their integrity. The new fact to establish is that the manifest was registered against that version before the run started:

A v0.2 or v0.3 receipt has no manifest binding; this check reports not applicable.

5. Check key status and lifecycle status — separately

These are two different facts. Do not conflate them:

Runnable verifier

The script below uses only node:crypto for hashing and @noble/curves for Ed25519 (the same library and major version AgnCred signs with — v2 renamed its module paths, so pin v1):

npm install @noble/curves@1
node verify-receipt.mjs https://<agncred-host>/api/v1/receipts/<receipt-id>/verification-bundle
# or against a saved bundle:
node verify-receipt.mjs ./bundle.json
// verify-receipt.mjs — offline verifier for agncred-verification-bundle.v1 and .v2
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { ed25519 } from "@noble/curves/ed25519";

// RFC 8785 (JCS): keys sorted by UTF-16 code units, undefined dropped,
// ECMAScript number formatting (JSON.stringify provides both string
// escaping and number formatting per RFC 8785).
function jcs(value) {
  if (typeof value === "number" && !Number.isFinite(value)) throw new Error("non-finite number");
  if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
  if (Array.isArray(value)) return `[${value.map(jcs).join(",")}]`;
  const entries = Object.entries(value)
    .filter(([, v]) => v !== undefined)
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
  return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${jcs(v)}`).join(",")}}`;
}

const sha256 = (s) => "sha256:" + createHash("sha256").update(s, "utf8").digest("hex");
const hex = (s) => Uint8Array.from(Buffer.from(s, "hex"));
const utf8 = (s) => new TextEncoder().encode(s);
const results = [];
const check = (name, ok, detail = "") => results.push({ name, ok, detail });

// Bundle events arrive in the stored camelCase shape; hashing uses canonical snake_case.
function canonicalEvent(e) {
  const strip = (obj) => JSON.parse(JSON.stringify(obj)); // drops undefined members
  if (e.sequenceNumber !== undefined) {
    return strip({
      schema_version: e.schemaVersion, event_id: e.eventId, receipt_id: e.receiptId,
      sequence_number: e.sequenceNumber, previous_event_hash: e.previousEventHash,
      event_type: e.eventType, actor_type: e.actorType, actor_id: e.actorId,
      created_at: e.createdAt, event_hash: e.eventHash, metadata: e.metadata
    });
  }
  return strip({
    schema_version: e.schemaVersion, event_id: e.eventId, receipt_id: e.receiptId,
    previous_event_id: e.previousEventId, event_type: e.eventType, actor_type: e.actorType,
    actor_id: e.actorId, created_at: e.createdAt, event_hash: e.eventHash, metadata: e.metadata
  });
}

const source = process.argv[2];
if (!source) { console.error("usage: node verify-receipt.mjs <bundle-url-or-file>"); process.exit(2); }
const bundle = source.startsWith("http")
  ? await (await fetch(source)).json()
  : JSON.parse(readFileSync(source, "utf8"));

// --- 1. payload hash ---------------------------------------------------------
const { version, attestation, events, signing_keys: keys, receipt } = bundle;
const payloadMessage = version.schema_version === "work-receipt.v0.3"
  ? jcs(version.canonical_payload)
  : jcs((({ signature, ...rest }) => rest)(version.canonical_payload));
check("payload hash matches canonical payload", sha256(payloadMessage) === version.payload_hash);

// --- 2. attestation signature --------------------------------------------------
if (attestation) {
  const key = keys.find((k) => k.key_id === attestation.signing_key_id);
  check("signing key present in key history", Boolean(key), attestation.signing_key_id);
  if (key) {
    let ok = false;
    if (attestation.signature_scheme === "agncred-attestation.v1" || attestation.signature_scheme === "agncred-attestation.v2") {
      const base = {
        scheme: attestation.signature_scheme,
        receipt_id: receipt.receipt_id,
        receipt_version: attestation.receipt_version,
        payload_hash: version.payload_hash,
        company_id: attestation.company_id,
        decision: attestation.decision,
        approved_public_fields: attestation.approved_public_fields ?? {},
        attested_at: attestation.attested_at
      };
      const message = attestation.signature_scheme === "agncred-attestation.v2"
        ? jcs({
            ...base,
            confirmed_dimensions: attestation.approved_public_fields?.confirmed_dimensions ?? {},
            complexity_band: attestation.approved_public_fields?.complexity_band ?? null
          })
        : jcs(base);
      ok = ed25519.verify(hex(attestation.signature), utf8(message), hex(key.public_key));
      check("signed_payload_hash matches message", sha256(message) === attestation.signed_payload_hash);
    } else if (attestation.signature_scheme === "receipt.v0.2") {
      const sig = version.canonical_payload.signature?.signature_value;
      ok = Boolean(sig) && ed25519.verify(hex(sig), utf8(payloadMessage), hex(key.public_key));
    }
    check(`attestation signature valid (${attestation.signature_scheme})`, ok);
    check("key status (separate fact)", true, `${key.status}${key.revoked_at ? `, revoked_at ${key.revoked_at}` : ""}`);
  }
} else {
  check("attestation present", false, "receipt carries no company attestation — it proves nothing yet");
}

// --- 3. event chain -------------------------------------------------------------
let previousHash = "genesis";
let previousLegacy = null;
let chainOk = true;
events.forEach((stored, index) => {
  const e = canonicalEvent(stored);
  const { event_hash: recorded, ...unhashed } = e;
  if (e.schema_version === "receipt-event.v0.1") {
    if (e.previous_event_id !== (previousLegacy?.event_id ?? undefined)) chainOk = false;
    if (recorded !== sha256(`${jcs(unhashed)}\n${previousLegacy?.event_hash ?? "genesis"}`)) chainOk = false;
    previousLegacy = e;
  } else {
    if (e.sequence_number !== index + 1) chainOk = false;
    if (e.previous_event_hash !== previousHash) chainOk = false;
    if (recorded !== sha256(jcs(unhashed))) chainOk = false;
    previousLegacy = null;
  }
  previousHash = e.event_hash;
});
check(`event chain valid (${events.length} events)`, chainOk);

// --- 4. manifest binding (VF-1, v0.4 receipts in v2 bundles) ---------------------
const payload = version.canonical_payload;
if (payload.schema_version === "work-receipt.v0.4") {
  const registrations = bundle.registered_manifests ?? [];
  const entry = registrations.find(
    (item) => item.version_ref === payload.version_ref && item.manifest_hash === payload.manifest_hash
  );
  check("manifest registered against version_ref", Boolean(entry));
  if (entry) {
    check(
      "manifest registered before the run started",
      Date.parse(entry.registered_at) <= Date.parse(payload.started_at),
      `${entry.registered_at} vs ${payload.started_at}`
    );
  }
  const lineageEvents = bundle.lineage_events ?? [];
  let lineagePrevious = "genesis";
  let lineageChainOk = lineageEvents.length > 0;
  lineageEvents.forEach((event, index) => {
    const { event_hash: recorded, ...unhashed } = event;
    if (event.sequence_number !== index + 1) lineageChainOk = false;
    if (event.previous_event_hash !== lineagePrevious) lineageChainOk = false;
    if (recorded !== sha256(jcs(unhashed))) lineageChainOk = false;
    lineagePrevious = event.event_hash;
  });
  check(`lineage chain valid (${lineageEvents.length} events)`, lineageChainOk);
  const registeredEvent = lineageEvents.find(
    (event) =>
      (event.event_type === "version_registered" || event.event_type === "revision_registered") &&
      event.metadata?.manifest_hash === payload.manifest_hash
  );
  check(
    "registration event on the lineage chain at or before started_at",
    Boolean(registeredEvent) && Date.parse(registeredEvent.created_at) <= Date.parse(payload.started_at)
  );
} else {
  check("manifest binding (separate fact)", true, "not applicable before work-receipt.v0.4");
}

// --- 5. current state (separate facts) -------------------------------------------
check("lifecycle status (separate fact)", true,
  `${receipt.current_status}, version ${receipt.current_version}, privacy_erased=${receipt.privacy_erased}`);

// --- report ----------------------------------------------------------------------
let failed = 0;
for (const r of results) {
  if (!r.ok) failed += 1;
  console.log(`${r.ok ? "PASS" : "FAIL"}  ${r.name}${r.detail ? ` — ${r.detail}` : ""}`);
}
console.log(failed === 0 ? "\nVERIFIED" : `\nNOT VERIFIED (${failed} failed)`);
process.exit(failed === 0 ? 0 : 1);

Reading the result

VERIFIED means: this exact payload existed, this company's key signed this exact version with this exact decision and these exact public fields, and the lifecycle history has not been reordered or forked. It does not by itself mean the receipt is currently in good standing — read the separate facts: a disputed status or a revoked key changes what you should conclude, without changing what was historically signed.