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 / 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 retained (org briefs); often null for 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);

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.