Trust

Governed receipt verification (GSR & ACR)

Stratalize issues two sibling receipt profiles under one family: GSR-JCS-1 for x402 settlement and ACR-JCS-1 for agent-run completion. This page is the public specification for offline verification — no Stratalize account required.

Configurable retention model

By default, Stratalize releases source tool output after delivery. What persists are cryptographic commitments — output_hash digests, signed _stratalize envelopes, and governed receipt records (GSR and ACR) — not necessarily the full response body.

Retention beyond that default is configurable per organization and per workflow. Where a workspace needs history to function — for example org briefs, audit replay, or agent execution review — Stratalize persists artifacts under that org's retention setting. Signed output records may be kept for a configurable window; raw upstream source data is not archived by default.

Consequence for verifiers: you can always verify receipt signatures and digest binding offline. To prove that a specific output body matches synthesis.output_hash, retain the original tool response JSON at call time and recompute SHA-256 locally when Stratalize is not holding the body under your retention tier.

Tool-response attestation via the _stratalize envelope is documented separately on /docs/attestation. GSR receipts cover settlement and governance proof; ACR receipts cover agent-run completion proof; save the artifacts you need for full audit replay.

GSR-JCS-1 canonicalization profile

Profile ID: GSR-JCS-1 (embedded as canonicalization_profile on newly issued receipts). Serialization: RFC 8785 JSON Canonicalization Scheme via the canonicalize npm package.

The _stratalize.version field ( "1") identifies the MCP/x402 attestation envelope format — it is not the GSR canonicalization profile. Receipts use canonicalization_profile: "GSR-JCS-1" for the governed settlement body.

Pre-canonicalization field rules

Apply these mutations to a deep copy of the receipt object, then JCS-canonicalize the result. Signing input and anchor digest preimage use the same canonical string.

Field / ruleValue before JCS
field_abac_decisionOmit (not part of signed body)
passport_scope_decisionOmit (not part of signed body)
signature.valuenull before canonicalization
signature.signed_atnull before canonicalization
signature.signerincluded (commits key_id)
anchor.digestempty string before canonicalization
anchor.anchor_tx_hashnull before canonicalization
anchor.anchor_status"pending" before canonicalization

All other receipt fields (version, canonicalization_profile, receipt_id, payment, governance, synthesis, provenance, anchor.contract, anchor.chain, signature.algorithm) are included unchanged. Anchor digest after signing equals SHA-256(UTF-8(canonical string)) hex.

key_id derivation (ML-DSA-65)

Receipt signature.signer carries the signing key identifier. Derivation (also returned by /api/trust/signing-key as current_key_id and per-key keys[].key_id):

  1. Decode the ML-DSA-65 public key from hex (keys[] entry where format: "hex").
  2. Compute SHA-256(public_key_bytes).
  3. Take the first 16 lowercase hex characters of the digest.
  4. Prefix with mldsa65-.

Example shape: mldsa65-a1b2c3d4e5f67890. This is additive metadata over the existing NIST FIPS 204 key material — no change to signing algorithms.

Public API: GET /api/trust/signing-key

Returns the active ML-DSA-65 public key and legacy Ed25519 material. No authentication. CORS *. Cache up to 1 hour.

GET https://www.stratalize.com/api/trust/signing-key

{
  "public_key": "<ML-DSA-65 hex>",
  "current_algorithm": "ML-DSA-65",
  "current_key_id": "mldsa65-xxxxxxxxxxxxxxxx",
  "key_id_derivation": {
    "algorithm": "SHA-256",
    "input": "ML-DSA-65 public key bytes (hex-decoded)",
    "fingerprint": "first 16 lowercase hex characters of SHA-256 digest",
    "prefix": "mldsa65-"
  },
  "keys": [
    {
      "algorithm": "ML-DSA-65",
      "format": "hex",
      "key": "<hex>",
      "key_id": "mldsa65-xxxxxxxxxxxxxxxx",
      "active": true
    }
  ],
  "verification_docs": "https://www.stratalize.com/docs/gsr"
}

Public API: GET /api/trust/verify-unified/{synthesis_id}

Server-side synthesis verification aggregate. Returns signature status, payload, optional brief text, Base anchor summary, ZK proof status, and receipt payment/governance fields when a GSR exists for the synthesis ID.

  • Authentication: none for public x402 / public synthesis records. Org-scoped records return 403 only when an authenticated session belongs to a different org.
  • Rate limit: 60 requests per minute per client IP (429 with Retry-After).
  • CORS: allowed origin https://trust.stratalize.com (use server-side fetch from agents).

Core response fields

FieldMeaning
valid / verifiedServer-computed ML-DSA synthesis signature verdict
reasonHuman-readable status (e.g. valid, invalid_signature)
synthesis_idCanonical synthesis identifier
payloadSigned synthesis payload object
brief_textStored output text when org retention settings require it; often null for anonymous x402
signature.algorithmML-DSA-65 or Ed25519 (legacy)
signature.signatureRaw signature bytes (base64url) when available
signature.output_hash / input_hashCommitted hashes from signed payload
signature.public_key_urlSigning key fetch URL
base_anchorMerkle batch / Base tx summary for synthesis artifact
receipt_payment / receipt_governanceGSR fields when receipt exists in store; else null
verification_urlstrust_page, merkle_proof, public_key links

For GSR-only verification, prefer POST /api/trust/verify-receipt with the _receipt object from your saved x402 response, or verify offline using the walkthrough below.

Public API: POST /api/trust/verify-receipt

Accepts a full GovernedSettlementReceipt JSON body. Returns layered verification (signature, digest binding, anchor status). CORS *. No auth. Use when you saved _receipt at payment time and want Stratalize to confirm anchor state against Base mainnet.

POST https://www.stratalize.com/api/trust/verify-receipt
Content-Type: application/json

{ "receipt": { ... full _receipt from x402 200 response ... } }

Merkle inclusion proofs (synthesis artifacts)

Yes — per-synthesis Merkle inclusion proofs exist today for synthesis artifacts batched on Base mainnet (hourly job). This is separate from the GSR AttestationAnchor.sol digest anchor on receipts.

Fetch: GET /api/verify/merkle-proof/{synthesis_id} (also linked as verification_urls.merkle_proof from verify-unified). When anchored, the response includes:

  • local_proof.proof_path — sibling hashes
  • local_proof.leaf_hash — keccak256 leaf from synthesis_id, output_hash, timestamps
  • local_proof.merkle_root — batch root
  • on_chain_proof — Base tx hash and contract root check

Verify locally with merkletreejs: MerkleTree.verify(proof_path, leaf_hash, merkle_root, keccak256, { sortPairs: true }). Pending batches return reason: pending_anchor without a proof path yet.

GSR receipt anchor (anchor.anchor_tx_hash) attests the receipt digest on-chain via a different contract path — confirm with Base RPC and POST /api/trust/verify-receipt, not the Merkle synthesis endpoint.

Walkthrough: offline GSR signature verification (Node 20+)

Uses the published GSR-JCS-1 rules above. Install: npm install canonicalize @noble/post-quantum.

import canonicalize from "canonicalize";
import { createHash } from "node:crypto";
import { ml_dsa65 } from "@noble/post-quantum/ml-dsa.js";

const GSR_JCS_1 = "GSR-JCS-1";

function computeKeyId(publicKeyHex) {
  const bytes = Buffer.from(publicKeyHex, "hex");
  const fp = createHash("sha256").update(bytes).digest("hex").slice(0, 16);
  return `mldsa65-${fp}`;
}

/** Apply GSR-JCS-1 pre-canonicalization rules (see table above). */
function canonicalReceiptForm(receipt) {
  if ((receipt.canonicalization_profile ?? GSR_JCS_1) !== GSR_JCS_1) {
    throw new Error("unsupported profile");
  }
  const clone = structuredClone(receipt);
  delete clone.field_abac_decision;
  delete clone.passport_scope_decision;
  clone.signature.value = null;
  clone.signature.signed_at = null;
  clone.anchor.digest = "";
  clone.anchor.anchor_tx_hash = null;
  clone.anchor.anchor_status = "pending";
  const canonical = canonicalize(clone);
  if (canonical === undefined) throw new Error("JCS failed");
  return canonical;
}

async function verifyGsrReceipt(receipt) {
  const keyRes = await fetch("https://www.stratalize.com/api/trust/signing-key");
  if (!keyRes.ok) throw new Error(`signing-key HTTP ${keyRes.status}`);
  const keys = await keyRes.json();
  const entry = keys.keys?.find((k) => k.algorithm === "ML-DSA-65" && k.active);
  if (!entry?.key) throw new Error("no active ML-DSA-65 key");
  const keyId = computeKeyId(entry.key);
  if (receipt.signature.signer !== keyId) {
    return { valid: false, reason: "signer/key_id mismatch" };
  }
  const canonical = canonicalReceiptForm(receipt);
  const digestOk =
    createHash("sha256").update(Buffer.from(canonical, "utf8")).digest("hex") ===
    receipt.anchor.digest;
  const sigOk = ml_dsa65.verify(
    Buffer.from(receipt.signature.value, "base64url"),
    Buffer.from(canonical, "utf8"),
    Buffer.from(entry.key, "hex"),
  );
  return { valid: sigOk && digestOk, signature: sigOk, digest_binding: digestOk, key_id: keyId };
}

// Usage: const receipt = x402Response._receipt;
// const result = await verifyGsrReceipt(receipt);

Agent Completion Receipt (ACR-JCS-1)

Profile ID: ACR-JCS-1. ACR is the sibling receipt to GSR: one receipt family, two profiles. GSR attests a single governed tool settlement; ACR attests that an agent run performed the terms of its mandate.

An ACR attests mandate execution. It does not attest that outputs are substantively correct, factually true, or optimal — only that the run completed under the declared governance bindings, scope decisions, acceptance predicate, and cost rollup.

An ACR cryptographically binds:

  • The mandate mandate.mandate_hash, type, and verification flag
  • The agent passport context — scope decision leaves in binding.scope_decisions_merkle_root (PSR-JCS-1 passport scope decisions per step)
  • The hash-chain of step settlements binding.step_receipts_merkle_root commits per-step output_hash values (the GSR lineage from every tool step in the run)
  • The completion predicate binding.completion_attestation_hash over the signed pipeline summary, plus acceptance.* criteria hash and evaluation
  • Cost reconciliation cost.llm_cost_usd_total, token totals, and optional settlement_total_atomic rollup

Implementation reference: lib/receipt/acr-builder.ts, lib/receipt/acr-emit.ts. Rows are stored with permission_model: agent_completion.

ACR-JCS-1 canonicalization profile

Serialization: RFC 8785 JCS via the canonicalize npm package. Unlike GSR-JCS-1, ACR does not strip sibling decision blobs — it carries binding hashes only.

Field / ruleValue before JCS
signature.valuenull before canonicalization
signature.signed_atnull before canonicalization
anchor.digestempty string before canonicalization
anchor.anchor_tx_hashnull before canonicalization
anchor.anchor_status"pending" before canonicalization

Empty Merkle trees use sha256("") — hex digest of UTF-8 empty string. Scope leaves with no decision commit sha256('ACR-NO-SCOPE-DECISION:' + step_index).

Offline ACR verification

Server helper: verifyAcrReceipt() in lib/receipt/acr-verifier.ts. Verifies ML-DSA-65 signature, digest binding, and optionally recomputes Merkle roots when you supply step output hashes and scope decision leaves.

import { verifyAcrReceipt } from "@/lib/receipt/acr-verifier";

const result = await verifyAcrReceipt(receipt, {
  merkleLeaves: {
    stepOutputHashes: ["<sha256-hex per step, index order>"],
    scopeSteps: [
      {
        step_index: 0,
        scope_decision_hash: "<hex or null>",
        scope_decision_kind: "psr",
      },
    ],
  },
});

// result.overall: "verified" | "signature_invalid" | "digest_invalid" | "merkle_invalid" | "key_unresolved"

Proof URLs on x402 responses

Paid x402 JSON includes verified_at (trust page URL with ?synthesis_id=), receipt_id, and _receipt. The trust page is browser-oriented; agents should save _receipt and verify using this spec. Legacy links using ?id= remain supported on the trust page (mapped to receipt_id or synthesis_id).

Related: _stratalize attestation, x402, trust.stratalize.com/verify.