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 / rule | Value before JCS |
|---|---|
| field_abac_decision | Omit (not part of signed body) |
| passport_scope_decision | Omit (not part of signed body) |
| signature.value | null before canonicalization |
| signature.signed_at | null before canonicalization |
| signature.signer | included (commits key_id) |
| anchor.digest | empty string before canonicalization |
| anchor.anchor_tx_hash | null 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):
- Decode the ML-DSA-65 public key from hex (
keys[]entry whereformat: "hex"). - Compute
SHA-256(public_key_bytes). - Take the first 16 lowercase hex characters of the digest.
- 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
| Field | Meaning |
|---|---|
| valid / verified | Server-computed ML-DSA synthesis signature verdict |
| reason | Human-readable status (e.g. valid, invalid_signature) |
| synthesis_id | Canonical synthesis identifier |
| payload | Signed synthesis payload object |
| brief_text | Stored output text when org retention settings require it; often null for anonymous x402 |
| signature.algorithm | ML-DSA-65 or Ed25519 (legacy) |
| signature.signature | Raw signature bytes (base64url) when available |
| signature.output_hash / input_hash | Committed hashes from signed payload |
| signature.public_key_url | Signing key fetch URL |
| base_anchor | Merkle batch / Base tx summary for synthesis artifact |
| receipt_payment / receipt_governance | GSR fields when receipt exists in store; else null |
| verification_urls | trust_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 hasheslocal_proof.leaf_hash— keccak256 leaf from synthesis_id, output_hash, timestampslocal_proof.merkle_root— batch rooton_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_rootcommits per-stepoutput_hashvalues (the GSR lineage from every tool step in the run) - The completion predicate —
binding.completion_attestation_hashover the signed pipeline summary, plusacceptance.*criteria hash and evaluation - Cost reconciliation —
cost.llm_cost_usd_total, token totals, and optionalsettlement_total_atomicrollup
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 / rule | Value before JCS |
|---|---|
| signature.value | null before canonicalization |
| signature.signed_at | null before canonicalization |
| anchor.digest | empty string before canonicalization |
| anchor.anchor_tx_hash | null 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.