Trust
Independently verifying a Governed Settlement Receipt
Every paid x402 call returns a _receipt object signed under profile GSR-JCS-1. This page is the public specification for offline verification — no Stratalize account required.
Zero-storage output model (by design)
Stratalize does not retain original tool output content after delivery. Receipts commit an output_hash (SHA-256 of the synthesis bytes) and governance metadata, but not the payload itself. This is intentional: governed intelligence is delivered once to the caller; the receipt proves payment, permissions, and tamper-evidence without building a durable content archive.
Consequence for verifiers: you can always verify the receipt signature and digest binding offline. To verify that a specific output body matches synthesis.output_hash, you must retain the original tool response JSON yourself at call time and recompute SHA-256 locally. Stratalize cannot reconstruct that content later.
Tool-response attestation via the _stratalize envelope is documented separately on /docs/attestation. GSR receipts cover settlement and governance proof; save both artifacts if you need 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 retained (org briefs); often null for 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);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.