TrustReceipt
September 20, 2026 · View on GitHub
English | Español | Français | Deutsch
TrustReceipt
Merchant-side evidence layer for agentic commerce: portable, signed receipts you can verify offline
What it is
TrustReceipt is an open receipt format for merchants. It records evidence of agentic commerce transactions in a form anyone can verify offline, across protocols such as ACP, AP2, x402, MCP, UCP and MCAP. It sits alongside those protocols. AP2 mandates, ACP checkout sessions, Visa TAP signatures and x402 settlements stay as they are, and TrustReceipt adds a portable cryptographic record of the policy decision applied to each one.
A TrustReceipt is a JSON payload signed as a JWS, which you can verify offline against a public JWKS endpoint. Each receipt records who the agent was, which protocol ran, which trust providers vouched for the transaction, which policy applied and what decision it reached. All of that fits in one self-contained token that any party can verify without calling the issuer.
This package is the reference verifier and issuer implementation. It is part of Trusteed's merchant-control stack (policy snapshots + agent control points + receipts), but the receipt format itself is open and portable across issuers.
Recent updates (2026-09-16)
This sync brings the public repo current with the reference implementation (previous sync: 2026-08-17) and adds an accuracy pass over the existing docs. New:
- Two new third-party identity verifiers, each working without the underlying protocol's transport. Merchant Identity Assertions (MIA) covers self-issued and DNS-authorized third-party issuance (
draft-anders-merchant-identity-assertions-01, 30 conformance vectors). AGTP merchant identity covers Agent Identity Document, Intent Assertion and Cart-Digest checks (draft-hood-agtp-merchant-identity-02, 50 conformance vectors). - Mandate and approval evidence, State Witness evidence and operation link: three new optional field groups (see Receipt anatomy and SPEC.md §3.2), injected identically into all three receipt shapes with no
schema_versionbump. - The ATEP passport reference verifier (
reference-verifier/verify-atep-passport.mjs), a zero-dependency script that uses only Node built-ins. It shows that a merchant's portable trust attestation can be verified offline without any Trusteed code. - A 7th
legacy-compactconformance vector (signer declarations). - Two accuracy fixes to existing docs. The "11 v1.1 vectors" line undercounted a table that already had 12 rows (SPEC.md §11.6). And
CONTRIBUTING.md,docs/architecture.mdand this README's Conformance section now say thatscripts/validate-vectors.tscurrently reports 9/10, not 10/10 (TC-007 /expired, issue #6). Code and spec disagree on that vector. - A packaging gap that was already there and is now tracked: the RFC 3161 capability row was marked optional, but it is non-functional for any external installer today (issue #5).
- CHANGELOG.md cleanup: two entries had sat under a stale "Unreleased" heading since before this repo's public launch even though they had already shipped. They are retitled to the real version and date. An internal Trusteed deployment-coupling paragraph and an unreachable sibling-package cross-reference, both leaked in from the source monorepo, were removed.
See CHANGELOG.md for the full version history.
Capability status
| Capability | Status | Notes |
|---|---|---|
| JWS verification (Ed25519) | ✅ Implemented | CLI + library, no custom crypto (uses jose v6) |
| JWKS-based public key resolution | ✅ Implemented | Direct fetch() per verification (5s timeout), no in-process cache; inline JWK set also supported |
| Schema v1.0 | ✅ Stable | 10 conformance vectors passing |
| Schema v1.1 (eIDAS-aligned fields) | 🟡 Code-complete / experimental | 16 additional vectors passing (12 in test-vectors/v11/ + 4 negative strict-mode vectors in test-vectors/v11-strict/, T-AUD-012); field set may evolve before v1.2 |
| RFC 8785 canonical JSON | ✅ Implemented | Used for signing + audit chain hashes |
Audit chain (hash_chain_prev) | ✅ Implemented | Per-merchant tamper-evident linkage |
Signer declarations (signers) | ✅ Implemented | Who signed, under which custody model, and how each signer relates to the subject; a platform-held key and a merchant-held key are no longer indistinguishable |
Evaluation identity (evaluation_id) | ✅ Implemented | Points at the enforcement record that produced the verdict, so correlation stops relying on timestamp proximity. Identifies the EVALUATION, not the operation |
| eIDAS Advanced Electronic Seal posture | 🟡 Candidate | Field-level support; not a Qualified Electronic Seal (no QTSP) |
| ESIGN / UETA evidence shape | 🟡 Partial | esign_disclosure_hash + consent context; full disclosure workflow in progress |
| RFC 3161 trusted timestamp evidence | 🔴 Hook present, not externally usable | verify-timestamp-evidence.ts imports the real implementation from @agenticmcpstores/trust-receipt-tsa-client, a Trusteed-internal package that is not published to npm and not vendored in this repo; npm install resolves everything else but that one dependency has no registry entry; tracked in issue #5 |
| Merchant Identity Assertions (MIA): third-party verifier | ✅ Implemented | Verifies draft-anders-merchant-identity-assertions-01 documents: self-issued and DNS-authorized third-party issuance, replay/tamper/redirect detection; 30 conformance vectors under conformance/mia-vectors/ |
| AGTP merchant identity: Intent Assertion / Cart-Digest | ✅ Implemented | Verifies exactly what draft-hood-agtp-merchant-identity-02 declares checkable without speaking AGTP, no AGTP transport client ships here, only the Agent Identity Document, Intent Assertion, and Cart-Digest checks; 50 conformance vectors under conformance/agtp-merchant-vectors/ |
Mandate & approval evidence (mandate-evidence.ts) | 🟡 Structural only | Lets a third party recompute mandate_claims_hash and confirm a charged amount fell inside what was authorized; mandate_verification is a closed enum and today only ever reports "structure_only"; no issuer verifies a mandate signature yet |
State Witness evidence (state-witness-evidence.ts) | ✅ Implemented | Declares what the issuer's state comparator resolved before money moved, and against which authoritative state. Closes the gap where an unchanged-state checkout left no signed record that the comparison ran |
Operation link (operation-link.ts) | ✅ Implemented (schema only) | Ties a corrected retry or a reconfirmed execution back to the receipt it supersedes, distinct from hash_chain_prev, which only orders receipts by time and makes no continuation claim; not re-exported from index.ts, import from trust-receipt-verifier/schema/operation-link.js |
| AWS KMS issuer-side signing | 🟡 Optional / issuer-side | Provided by sibling package trust-receipt-kms-signer; not required for verification |
| Reference ports (TS) / language ports (Python, Go, Java) | 🟡 TS only today | Ports welcome; see CONTRIBUTING.md |
AIVS proof-bundle export/verify (aivs-export.ts) | 🟡 Code-complete | Projects a signed v1.0 receipt into an AIVS-compatible { manifest_hash, session_sig, audit_log } bundle, offline-verifiable with no Trusteed code (spec-062 US1, alignment not escrow) |
Extension artifact verification (verify-extension-artifact.ts) | 🟡 Code-complete | Verifies developer-signed erasure receipts and extension manifests from the Trusteed Extension Marketplace ecosystem |
v1.0-legacy compact receipt shape (verifier.ts) | ✅ Implemented | verifyTrustReceipt also accepts the JWT-style compact payload emitted by the platform issuer since spec-040; surfaced as result.variant / result.legacyReceipt |
Declared trust-anchor degradation (accepted_degraded) | ✅ Implemented | Three-valued verdict for v1.1 envelopes; see Verification verdicts and SPEC.md §4.1 (NORMATIVE) |
Consumer-side revocation (revocation.ts) | ✅ Implemented | checkRevocation() against a merchant's published status list. Pure and offline: you fetch, it decides. Every failure resolves to unknown, never not_revoked |
Canonicalization reporting (result.canonicalization) | ✅ Implemented | "jcs" vs "json-stringify-legacy" on the legacy-compact path, independent of variant, which describes the payload shape |
✅ = production-grade implementation. 🟡 = present and tested but subject to change before v1.2 GA, or dependent on operator-side integration.
Verification verdicts
verifyReceiptEnvelope (v1.1) returns three values. Treating the
verdict as binary is a conformance failure either way: it reports a degraded
receipt as fully verified, or it discards a valid one.
| Verdict | Meaning |
|---|---|
accepted | Signature and structure verified, and so is the chain of trust. |
accepted_degraded | Signature and structure verified; the receipt declares its chain of trust unverifiable. |
rejected | Any check failed. |
accepted_degraded is emitted only when the issuer root behind the bundled JWKS
history could not be cryptographically verified and the receipt's signed
body carries a legal_posture_warnings[] entry with
reason: "trust_anchor_staging". A receipt that stays silent about an
unverifiable anchor is rejected, and the unsigned envelope_metadata mirror
alone can never unlock the downgrade.
It attests internal consistency and issuer intent, never issuer
authenticity. Consumers that branch on outcome === "accepted" keep refusing
it. Accepting the weaker guarantee has to be a conscious act.
How it works
Two independent operations handle a TrustReceipt, issuing and verifying. They can run in different systems at different times, and neither needs a shared secret.
Issuing a receipt
sequenceDiagram
autonumber
participant Agent as 🤖 Agent / Platform
participant Issuer as 🏭 Issuer (trusteed.xyz)
participant KMS as 🔑 KMS / Ed25519 Key
Agent->>Issuer: Transaction event<br/>(protocol, merchant_id, agent_id,<br/>cart_hash, user_intent_hash, …)
Issuer->>Issuer: Build 24-field payload<br/>(5 groups: core, participants,<br/>evidence, trust assertions, compliance)
Issuer->>Issuer: RFC 8785 canonical serialization<br/>(sorted keys, no whitespace)
Issuer->>KMS: Sign canonical bytes
KMS-->>Issuer: Ed25519 signature
Issuer->>Issuer: Encode as compact JWS<br/>header.payload.signature (base64url)
Issuer-->>Agent: 📄 Compact JWS token
Verifying a receipt
sequenceDiagram
autonumber
participant Verifier as 🔍 Verifier (any party)
participant JWKS as 🌐 JWKS Endpoint<br/>/.well-known/jwks.json
participant Schema as 📐 Zod Schema
Verifier->>Verifier: Parse JWS header<br/>extract kid + alg
Verifier->>JWKS: GET public keys<br/>(direct fetch, 5s timeout)
JWKS-->>Verifier: Public JWK set
Verifier->>Verifier: Match kid → public key
Verifier->>Verifier: Verify Ed25519 signature<br/>(jose, no custom crypto)
Verifier->>Schema: Validate decoded payload
Schema-->>Verifier: Zod parse result
Verifier->>Verifier: Check issued_at / expires_at<br/>(± clock tolerance)
Verifier-->>Verifier: ✅ VerifyResult { valid, receipt }<br/>or ❌ { valid: false, reason, errors }
Full picture
flowchart LR
subgraph Protocols
P1[x402]
P2[AP2]
P3[ACP]
P4[MCP]
P5[UCP]
P6[MCAP]
end
subgraph Issuer ["Issuer (trusteed.xyz)"]
direction TB
B1["Build payload\n24 fields · 5 groups"]
B2["RFC 8785 canonicalize"]
B3["Ed25519 sign\n(kid pinned)"]
B4["Compact JWS"]
B1 --> B2 --> B3 --> B4
end
subgraph Verifier ["Verifier (any party, offline-capable)"]
direction TB
V1["Parse header\nextract kid"]
V2["Fetch JWKS\n(or inline JWK set)"]
V3["Match kid → key\nverify signature"]
V4["Zod schema check\nexpiry check"]
V5{Result}
V1 --> V2 --> V3 --> V4 --> V5
end
Protocols --> Issuer
Issuer -->|"📄 JWS token"| Verifier
V5 -->|valid| R1["✅ receipt object\n(policy_decision, agent_id, …)"]
V5 -->|invalid| R2["❌ reason + errors\n(tampered / expired / schema_invalid / …)"]
Key properties:
- Verification works offline. It needs only the JWKS URL (publicly reachable and HTTP-cacheable at the edge) and never calls back to the issuer
- One receipt format covers x402, AP2, ACP, MCP, UCP and MCAP through
protocol_artifacts hash_chain_prevlinks receipts into a tamper-evident, per-merchant chain (RFC 8785)signersstates who signed and in whose custody the key livesevaluation_idpoints at the enforcement record behind the verdictlegal_posturetracks the eIDAS, ESIGN and UK-DIATF compliance posture of each receipt
Legal disclaimer
Verifiable seal for agentic commerce. Each TrustReceipt generates portable cryptographic evidence of origin, integrity, consent, agent authorization, and auditable retention. Designed to be aligned with ESIGN/UETA in the US, aligned with eIDAS in the EU as candidate advanced electronic seal evidence, and compatible with the UK Electronic Signatures and Trust Services framework. Qualified seals/signatures require issuance or validation by an applicable QTSP.
Disclaimer: TrustReceipt is cryptographically verifiable technical evidence. It does not by itself determine legal liability. Whether a given receipt is admissible or persuasive in a specific jurisdiction or proceeding depends on applicable local law, the consenting parties' agreements, and other facts beyond the scope of this record format.
The issuer maintains an internal claims policy that fixes the permitted and prohibited wording for every posture in this table. It is not published with this repository. Ask the issuer if you need the canonical list.
Regulatory compatibility status
| Framework | Jurisdiction | Status | v1.1 Fields |
|---|---|---|---|
| eIDAS (Regulation 910/2014) | EU | 🟡 Candidate. legal_posture progresses ades_candidate_no_tsa → ades_candidate_timestamped → ades_candidate_kms. Qualified seal (QeSeal) requires a QTSP. | legal_posture, legal_posture_warnings, timestamp_evidence, esign_disclosure_hash |
| ESIGN / UETA | US | 🟡 Partial. Verifiable seal with consent evidence, agent attribution, versioned disclosure, and auditable retention, designed to support ESIGN/UETA. Full disclosure workflow (withdrawal URI, version pinning) in progress. | esign_disclosure_hash, consent_context.consent_disclosure_version, consent_context.withdrawal_uri_hash |
| Electronic Communications Act 2000 / DIATF | UK | 🟡 Schema-compatible. Jurisdiction-aware retention (UK: 7 y default) and legal_posture field carry UK trust-service evidence. DIATF alignment is verified at schema level. Operational certification is pending. | legal_posture, privacy_classification.jurisdiction, export_bundle.retention_policy |
⚠️ None of the above constitutes legal advice. Regulatory qualification status may change as the implementation evolves. Consult qualified legal counsel for jurisdiction-specific requirements.
Quick verify
npm install trust-receipt-verifier
v1.0 receipt (compact JWS):
import { verifyTrustReceipt } from "trust-receipt-verifier";
const result = await verifyTrustReceipt(jwsToken, {
jwksUrl: "https://trusteed.xyz/.well-known/jwks.json",
});
if (result.valid) {
console.log(result.receipt.policy_decision); // "allow"
} else {
console.error(result.reason, result.errors);
}
v1.1 envelope (receipt + envelope_metadata + optional sidecars):
import { verifyReceiptEnvelope } from "trust-receipt-verifier";
import type { VerifyOptions } from "trust-receipt-verifier";
const opts: VerifyOptions = {
jwksHistory: {
jws_compact: "<SignedJwksHistory JWS>",
signed_by_root_sha256: "<issuer-root-sha256>",
},
trustAnchorPemSha256: "<64-hex-sha256-of-the-issuer-root-PEM>",
policyOidAllowlist: ["1.2.3.4.5.6.7.8.9"],
// toleranceSeconds: 30, // default clock-skew tolerance (seconds)
// mode: "strict", // default "compat"; see "Strict vs compat" below
// allowStagingRoots: true, // staging/CI only, never set in production
};
const result = await verifyReceiptEnvelope(envelope, opts);
if (result.outcome === "accepted") {
console.log(result.recomputedLegalPosture); // "ades_candidate_timestamped"
if (result.warnings.includes("unknown_trust_provider_present")) {
// envelope references a trust provider not yet recognised by this verifier version
}
} else {
console.error(result.errorCode, result.detail);
// errorCode may be: "receipt_expired" | "receipt_not_yet_valid" |
// "jwks_history_signature_invalid" | "unknown_kid" | "schema_invalid" | …
}
allowStagingRoots: defaults tofalse. Whenfalse(production default) anyjwksHistory.signed_by_root_sha256not present in the embedded trust anchor list causes immediate rejection (jwks_history_signature_invalid). Set totrueonly in staging or CI environments that use unsigned/stub JWKS history bundles.
⚠️ The trust anchor shipped in this package is a staging stub, not a production root.
EMBEDDED_ISSUER_ROOTS(src/embedded-issuer-root.ts) currently holds a single structurally-valid-but-non-verifying placeholder certificate whose subject CN is marked(STAGING).validateChain()deliberately fail-closes on it withroot_key_not_provisioned, so no caller can mistake the placeholder for authoritative trust. The real self-signed Ed25519 root is produced by an offline key ceremony that has not yet run. When it does, the constant is replaced and the verifier package gets a SemVer bump. Until then, pin your owntrustAnchorPemSha256. Do not rely on the embedded list, and treat everytrustAnchorPemSha256value in this README as a placeholder to substitute.
Receipt anatomy
A TrustReceipt payload contains 24 fields across five groups:
Core
| Field | Type | Description |
|---|---|---|
receipt_id | UUID v4 | Unique receipt identifier |
schema_version | "1.0" | Schema version literal |
issued_at | Unix seconds | When the receipt was created |
expires_at | Unix seconds | When the receipt expires |
issuer | string | Issuing platform domain |
Participants
| Field | Type | Description |
|---|---|---|
merchant_id | string | Merchant identifier |
agent_id | string | Agent session or instance identifier |
agent_provider | string | AI provider (anthropic, openai, google, …) |
Transaction Evidence
| Field | Type | Description |
|---|---|---|
user_intent_hash | string (non-empty) | Hash of the user's original intent text. Must be non-empty (SHA-256 hex recommended) |
cart_hash | SHA-256 hex | Hash of cart contents at decision time (optional) |
order_hash | SHA-256 hex | Hash of settled order object (optional) |
transaction_id | string | Platform transaction reference (optional) |
protocol | enum | x402 | AP2 | ACP | MCP | UCP | MCAP |
protocol_artifacts | array | Hashes of protocol-specific evidence objects |
payment_reference | object | PSP name + reference, no raw payment data (optional) |
Trust Assertions
| Field | Type | Description |
|---|---|---|
risk_signals | array | Normalized signals from issuer or providers |
trust_provider_assertions | array | Scored assertions from ClearSale, Trulioo, Mastercard, etc. |
policy_decision | enum | allow | deny | review | challenge |
Compliance
| Field | Type | Description |
|---|---|---|
liability_context | object | Assertor and scope (optional) |
consent_context | object | Consent hash, scope, timestamp (optional) |
privacy_classification | object | PII flag, retention days, jurisdiction (optional) |
verification_methods | array | JWKS URL or DID for key resolution; at least one entry required |
kid | string | Key ID used to sign this receipt |
hash_chain_prev | SHA-256 hex | Previous receipt in audit chain (optional) |
signers | array | Declared signers: party, kid, custody model, relation to subject (optional) |
evaluation_id | string | Identity of the evaluation that produced the verdict (optional); see caveats below |
rule_set_version | integer | Version of the policy catalogue the verdict was evaluated under (optional) |
evaluated_rules | array | Rule codes that RAN, as opposed to rules_triggered which fired (optional) |
mandate_id / mandate_claims_hash / mandate_max_amount_cents / mandate_currency / mandate_subject / mandate_audience / mandate_expires_at / mandate_verification | mixed | Mandate evidence (2026-09, optional): lets a third party confirm the charged amount fell inside what was authorized; see SPEC.md §3.2 for the full field-by-field reference |
approval_ref / approval_channel / approval_at | mixed | Out-of-band human approval evidence (2026-09, optional). Distinct from the mandate: the mandate says what an agent COULD spend, this says a person said yes to THIS purchase |
state_witness_resolution / state_witness_authoritative_hash / state_witness_reasons | mixed | State Witness evidence (2026-09, optional): what the comparator resolved before money moved, and against which authoritative state |
operation_id / supersedes_receipt_hash / superseded_reason / reconfirmed_state_hash | mixed | Operation link (2026-09, optional): ties a corrected retry or reconfirmed execution back to the receipt it supersedes; distinct from hash_chain_prev, which only orders by time |
attachments | array | Named, hashed file references (optional) |
Protocol support
| Protocol | Artifact mapping | Primary artifact types |
|---|---|---|
| MCAP | Defined | mcap_consent_hash, mcap_nonce |
| x402 | Defined | permit2_hash, settlement_hash, upto_envelope_hash |
| AP2 | Defined | mandate_hash, ap2_consent_hash |
| MCP | Defined | mcp_call_hash, tool_call_hash |
| ACP | Defined | acp_session_hash, acp_policy_hash |
| UCP | Defined | ucp_token_hash |
Conformance
A verifier implementation must pass all 10 test vectors (v1.0) to claim TrustReceipt conformance. Three levels are defined:
v1.1 status (2026-05-06). eIDAS hardening adds 12 v1.1 vectors under
test-vectors/v11/. This line used to say 11, but the table in SPEC.md §11.6 always listed 12 rows, including019b. The v1.1 schema drops legacymandate_hash/permit2/mcprail fields and introducespayment_authorization_hash,authorization_scheme,legal_posture_warnings, andesign_disclosure_hash.
⚠️ Conformance discrepancy, as of 2026-09-16 (issue #6): running
npx tsx scripts/validate-vectors.tstoday reports 9/10, not 10/10.verifyTrustReceipt's expiry check became informative-only (result.freshness.expired, not fatal) on 2026-07-28 to satisfy spec-049 FR-018's multi-year retention requirement, and TC-007'sexpected: "invalid"entry was never reconciled with that change. Read that issue before you write a port against this line.test-vectors/vectors.jsonis the cross-language verifier ABI (seePUBLISH.md), and the wrong side of this discrepancy could get frozen into an independent port.
2026-09-16 additions. Two new third-party identity verifiers, each with its own conformance suite: 50 vectors under
conformance/agtp-merchant-vectors/(draft-hood-agtp-merchant-identity-02) and 30 vectors underconformance/mia-vectors/(draft-anders-merchant-identity-assertions-01), plus a 7thlegacy-compactvector (L007-signers-declaration.json). Counting every vector file acrosstest-vectors/andconformance/(v1.0 core 10, v1.1 12, v1.1-strict 4, legacy-compact 7, x402-binding 11, agtp-merchant 50, mia 30) gives 124 conformance vectors on disk. The automated test suite as a whole (pnpm test) is 475 passing tests across 36 files, more than the raw vector count because several suites assert per-field and boundary behavior beyond the fixed vector set. Both figures were measured directly (pnpm vitest run/npx tsc --noEmit, both clean) against this exact tree, not carried over from an older release.
| Level | Name | Requirement |
|---|---|---|
| 1 | Verifier | Passes all 10 test vectors |
| 2 | Issuer | Level 1 + correctly issues valid receipts |
| 3 | Provider | Level 2 + co-authors ≥1 trust_provider_assertions type with real data |
This reference implementation is Level 2 conformant. There are two ways to run the conformance suite:
(a) Unit tests. Verifies all 10 vectors using pre-built test infrastructure (10 tests):
pnpm test
(b) End-to-end JWS conformance. Generates a fresh keypair, signs all 10 vectors, calls verifyTrustReceipt and reports pass or fail per vector:
# Via CLI (requires the package to be built first)
trust-receipt conformance
# Or directly with tsx (no build required)
npx tsx scripts/validate-vectors.ts
Add the badge to your project once all 10 pass:
[](https://github.com/Trusteedxyz/Trust-Receipt-Verifier)
Repo structure
trust-receipt-verifier/
├── SPEC.md # formal specification (authoritative)
├── CONTRIBUTING.md # how to contribute vectors, ports, and provider schemas
├── LICENSE # MIT
├── src/
│ ├── index.ts # package exports
│ ├── verifier.ts # verifyTrustReceipt() + parseTrustReceiptUnsafe() (v1.0, incl. legacy-compact shape)
│ ├── verify-1.0.ts # v1.0 verifier internals
│ ├── verify-1.1.ts # verifyReceiptEnvelope() (v1.1 eIDAS envelope) + typed trust-provider predicates
│ ├── zod-1.1.ts # v1.1 Zod schema (strict root, rejects unknown top-level keys)
│ ├── types-1.1.ts # typed trust-provider assertion shapes
│ ├── issuer.ts # issueTrustReceipt()
│ ├── embedded-issuer-root.ts # compile-time trust anchor + validateChain()
│ ├── verify-jwks-history.ts # JWKS history chain verification
│ ├── verify-timestamp-evidence.ts # RFC 3161 timestamp verification
│ ├── verify-export-bundle.ts # offline export-bundle verification
│ ├── verify-extension-artifact.ts # erasure receipt / extension manifest verification (Extension Marketplace)
│ ├── aivs-export.ts # AIVS proof-bundle export/verify (spec-062 US1)
│ ├── revocation.ts # consumer-side revocation check against a merchant status list
│ ├── agtp-merchant/ # Agent Identity Document / Intent Assertion / Cart-Digest (`draft-hood-agtp-merchant-identity-02`), no AGTP transport client
│ ├── mia/ # Merchant Identity Assertions, self-issued + third-party (`draft-anders-merchant-identity-assertions-01`)
│ ├── __tests__/ # unit + conformance tests
│ └── schema/
│ ├── trust-receipt.schema.ts # Zod schema (source of truth for v1.0 TypeScript types)
│ ├── trust-receipt-legacy.schema.ts # v1.0-legacy compact shape (issued by platform since spec-040)
│ ├── policy-evidence.ts # policy-evidence fields, shared by all three receipt shapes
│ ├── mandate-evidence.ts # mandate + approval evidence fields (2026-09)
│ ├── state-witness-evidence.ts # state-comparator evidence fields (2026-09)
│ └── operation-link.ts # receipt-to-receipt operation linkage, distinct from `hash_chain_prev` (2026-09)
├── schema/
│ ├── trust-receipt-v1.0-final.schema.json # NORMATIVE v1.0 JSON Schema (+ .sha256)
│ └── trust-receipt-v1.schema.json # superseded draft, retained for link stability; do NOT implement against it
├── test-vectors/
│ ├── README.md # how to use the vectors
│ ├── vectors.json # vector manifest with expected outcomes
│ ├── valid/ # TC-001 through TC-005
│ ├── invalid/ # TC-006 through TC-010
│ └── v11/, v11-strict/ # v1.1 + strict-mode conformance vectors
├── conformance/
│ ├── legacy-compact-vectors/ # L001–L007: both canonicalization regimes, expiry, tamper, unknown kid, signer declarations
│ ├── agtp-merchant-vectors/ # 50 vectors for `src/agtp-merchant/` (identity, intent, cart-digest)
│ └── mia-vectors/ # 30 vectors for `src/mia/` (self-issued + DNS-authorized third-party MIA)
├── reference-verifier/
│ ├── verify-aivs-bundle.mjs # verifies an exported AIVS bundle with no dependency on this package
│ └── verify-atep-passport.mjs # zero-dependency ATEP passport verifier (Node built-ins only, no `jose`, no npm install)
├── bin/
│ └── trust-receipt.ts (source) → dist/bin/trust-receipt.js (compiled) # CLI: verify, inspect, generate-key, conformance
└── demo/ # runnable demo scripts
Issue a receipt
import { issueTrustReceipt } from "trust-receipt-verifier";
const jws = await issueTrustReceipt({
payload: {
issuer: "trusteed.xyz",
merchant_id: "merchant-001",
agent_id: "agent-session-xyz",
agent_provider: "anthropic",
user_intent_hash: "<sha256-hex-of-user-intent>",
protocol: "MCP",
protocol_artifacts: [{ type: "mcp_call_hash", hash: "<sha256-hex>" }],
policy_decision: "allow",
verification_methods: [
{ type: "jwks", value: "https://trusteed.xyz/.well-known/jwks.json" },
],
kid: "tr-ed25519-2026-04-29",
},
privateKeyJwk: myEd25519PrivateKey,
kid: "tr-ed25519-2026-04-29",
});
Canonicalization: the payload is serialized with RFC 8785 (sorted keys, no whitespace) before signing, so
SHA-256(payload)is identical in any conforming implementation.
Related artifact verifiers
AIVS proof-bundle export. Projects a signed v1.0 receipt into an AIVS-compatible bundle (draft-stone-aivs-00). You can verify it offline with only the JWS and the issuer JWKS:
import {
exportAivsProofBundle,
verifyAivsProofBundle,
} from "trust-receipt-verifier";
const bundle = exportAivsProofBundle(receiptJws); // { manifest_hash, session_sig, kid, alg, audit_log }
const result = await verifyAivsProofBundle(bundle, { jwks: issuerJwks });
Extension Marketplace artifacts. Verify developer-signed erasure receipts (proof of data destruction after uninstall) or extension manifests:
import { verifyExtensionArtifact } from "trust-receipt-verifier";
const result = await verifyExtensionArtifact(jws, {
kind: "erasure", // or "manifest"; caller states which artifact this is
jwksUrl: "https://trusteed.xyz/.well-known/jwks.json",
});
// result.valid: boolean; result.reason on failure ("malformed_jws" | "unsupported_alg" | "missing_kid" | "jwks_unreachable" | "kid_not_found" | "signature_invalid" | "payload_not_json" | "shape_invalid")
ATEP passport (zero-dependency). A standalone script under reference-verifier/verify-atep-passport.mjs proves a merchant's portable trust attestation offline using only Node.js built-ins (no jose, no npm install, no code from this package):
node reference-verifier/verify-atep-passport.mjs passport.json jwks.json
Merchant Identity Assertions (MIA). Verify a third party's own signed identity document, self-issued or DNS-authorized (draft-anders-merchant-identity-assertions-01):
import { verifyMiaForDomain } from "trust-receipt-verifier";
const result = await verifyMiaForDomain("supplier.example.com", {
io: { fetchDocument: (url) => fetch(url).then(/* → FetchedDocument | null */ null) },
});
// result.valid: boolean; result.reason on failure (retrieval, tamper, expiry, DNS authorization mismatch, …)
AGTP merchant identity. The parts of draft-hood-agtp-merchant-identity-02 that are checkable without speaking the AGTP transport itself (no AGTP client ships here):
import {
verifyMerchantIdentityDocument,
verifyIntentAssertion,
verifyCartDigest,
} from "trust-receipt-verifier";
const identity = verifyMerchantIdentityDocument(agentIdentityDocument, { /* … */ });
const intent = await verifyIntentAssertion(compactJwt, { /* keyLookup, expected audience, … */ });
const cart = verifyCartDigest(presentedDigest, cartContents, canonicalizeJSON);
CLI
# Generate an Ed25519 key pair
trust-receipt generate-key
# Verify a v1.0 receipt (compact JWS)
trust-receipt verify receipt.jws --jwks-url https://trusteed.xyz/.well-known/jwks.json
# Verify a v1.1 envelope (JSON object with `receipt` + `envelope_metadata`)
trust-receipt verify envelope.json \
--type receipt-v11 \
--jwks-history-file issuer-jwks-history.json \
--trust-anchor-sha256 <64-hex-sha256-of-the-issuer-root-PEM> \
--policy-oid 1.2.3.4.5.6.7.8.9
# Verify a v1.1 envelope in STRICT mode (semantic trust-anchor enforcement)
trust-receipt verify envelope.json \
--type receipt-v11 \
--jwks-history-file issuer-jwks-history.json \
--trust-anchor-sha256 <64-hex-sha256-of-the-issuer-root-PEM> \
--strict
# Staging / CI only: skip root-anchor check (never use in production)
trust-receipt verify envelope.json --type receipt-v11 \
--jwks-history-file issuer-jwks-history.json \
--trust-anchor-sha256 <sha256> \
--allow-staging-roots
# Inspect a receipt without verifying the signature
trust-receipt inspect receipt.jws
# Run full end-to-end conformance suite (signs + verifies all 10 vectors)
trust-receipt conformance
--typeautodetection: when--typeis omitted, the CLI inspects the input shape. A JSON object with bothreceiptandenvelope_metadatakeys is treated asreceipt-v11automatically. A compactheader.payload.sigstring is treated asreceipt(v1.0).--typealso acceptserasure,manifestandjwks-historyfor the artifact verifiers described above. Pass it explicitly when auto-detection is ambiguous (both erasure and manifest payloads are compact JWS with noreceipt/envelope_metadatakeys).
Strict vs compat verification mode (v1.1)
At the schema layer, the v1.1 verifier checks verification_methods.trust_anchor_sha256 and
verification_methods.jwks_sha256 only by regex format (64 hex chars). That check lets an
opaque stub anchor through, meaning all zeros or a single-nibble value. The stub is well-formed
but has no real chain-of-trust binding, and issuers emit such stubs before a production anchor
ceremony. The mode option (library) and the --strict flag (CLI) add a semantic layer:
| Condition | compat (default, canary) | strict |
|---|---|---|
trust_anchor_sha256 is an opaque stub (all-zeros / single-nibble) | warn trust_anchor_sha256_stub | reject trust_anchor_stub_rejected |
jwks_sha256 is an opaque stub | warn jwks_sha256_stub | reject jwks_sha256_stub_rejected |
trust_anchor_sha256 ≠ operator-pinned trustAnchorPemSha256 | warn trust_anchor_sha256_mismatch | reject trust_anchor_mismatch |
| buyer_agent receipt with no agent-identity binding | warn agent_identity_absent | reject agent_identity_required_strict |
| RFC 3161 / LOTL timestamp degraded (e.g. TSA unavailable) | warn (tsa_unavailable) | warn (tsa_unavailable), accepted in both |
compat is the default so the rollout breaks nobody while observability data builds up.
Switch to strict once issuers have completed the production anchor ceremony. The four
named negative conformance vectors live in test-vectors/v11-strict/ and are regenerated by
scripts/generate-strict-mode-vectors.ts.
// Library usage
import { verifyReceiptEnvelope } from "trust-receipt-verifier";
const result = await verifyReceiptEnvelope(envelope, {
jwksHistory,
trustAnchorPemSha256: "<64-hex-pinned-anchor>",
policyOidAllowlist: ["1.2.3.4.5.6.7.8.9"],
mode: "strict", // default is "compat"
});
Documentation
| Document | Description |
|---|---|
| SPEC.md | Formal specification: wire format, field reference, conformance rules |
| docs/architecture.md | Internal architecture: signing envelope, key resolution, verification pipeline, security properties |
| CONTRIBUTING.md | How to add conformance vectors, language ports, or trust provider schemas |
| CHANGELOG.md | Version history and breaking changes |
Contributing
See CONTRIBUTING.md for how to add conformance vectors, port the verifier to another language, or co-author a trust_provider_assertions schema as a trust provider partner.
What a TrustReceipt does NOT prove
A receipt is technical evidence, not legal proof or operational guarantee. It deliberately does not assert:
- That the payment was captured or settled. A receipt with
policy_decision: "allow"records the decision and intent. Settlement is recorded by the underlying PSP / rail (Stripe charge, x402 on-chain tx, ACP completion, etc.) and referenced viapayment_referenceorprotocol_artifacts, not by the receipt itself. - That goods or services were delivered. Fulfilment evidence lives in the merchant's order system.
- KYC / KYA compliance. A receipt records that a trust provider asserted a level (e.g.
kya_status) at issuance time. It is not a substitute for independent KYC/KYA verification. - eIDAS Qualified Electronic Seal status. Even with
legal_posturepopulated, a TrustReceipt is at best an Advanced Electronic Seal candidate. Qualified seals require issuance by an EU-listed QTSP, which is out of scope for this package. - Legal liability or admissibility. A receipt is cryptographic evidence. Whether it is admissible or persuasive in a specific jurisdiction depends on local law, the parties' agreements, and facts beyond the receipt format.
- That the user actually intended what the agent did. The receipt records
user_intent_hash, which shows that an intent text existed and was hashed. It does not show that the hash matches a verified human utterance.
If your use case needs any of the above guarantees, use the receipt as an audit primitive next to those mechanisms. It does not replace them.
Threat model
The verifier is designed to detect the following classes of tampering. For each, the verifier returns a structured { valid: false, reason } rather than throwing.
| Threat | Defence | Verifier behaviour (v1.0 / v1.1) |
|---|---|---|
| Signature forgery / payload tamper | Ed25519 over RFC 8785 canonical bytes; kid pinned in header and payload | "signature_invalid" / "signature_invalid" |
| Wrong key used to sign | kid mismatch between JWS header and JWKS entry | "kid_not_found" / "unknown_kid" |
| Expired receipt | expires_at checked against verifier clock with configurable tolerance (default ±30 s) | "expired" / "receipt_expired" |
| Receipt issued in the future | issued_at checked against verifier clock with same tolerance | "not_yet_valid" / "receipt_not_yet_valid" |
| Schema downgrade / unknown fields | Zod schema validation strict on known fields; unknown top-level keys rejected | "schema_invalid" / "schema_invalid" |
| Forged / unsigned JWKS history | jwksHistory.signed_by_root_sha256 must match an embedded trust anchor; hard-fail when unknown unless allowStagingRoots | n/a (v1.0) / "jwks_history_signature_invalid" |
| Unknown trust provider assertion | Verifier warns but does not reject, preserving forward-compatibility | n/a (v1.0) / warning "unknown_trust_provider_present" |
| Replay of an old receipt | Out of scope for the verifier alone. Consumers must enforce uniqueness via receipt_id + issued_at + business rules | n/a. The verifier returns valid: true / outcome: "accepted" for not-yet-expired replays |
| JWKS rotation while a receipt is live | JWKS fetch refreshes on kid miss; old keys can be retained in the JWKS set during the rotation grace window | Verifies as long as kid is still published |
| Compromised issuer key | Key revocation is operator-side: remove the kid from the JWKS set; verifiers will fail closed | "kid_not_found" / "unknown_kid" once removed |
| Clock skew between issuer/verifier | toleranceSeconds option (default 30 s) | Within tolerance: pass. Outside: "expired" / "receipt_expired" or "receipt_not_yet_valid" |
| MITM on JWKS endpoint | TLS to JWKS host is the operator's responsibility; pinning the JWKS URL out-of-band defends against rogue substitution | n/a. The verifier trusts the configured URL |
Non-goals. The verifier does not validate: (a) whether the underlying payment cleared, (b) whether merchant policy was correctly configured, (c) jurisdictional admissibility, (d) revocation lists external to the JWKS endpoint, or (e) protocol-specific evidence inside protocol_artifacts (those are caller-validated against the relevant protocol's spec).
Versioning policy
This package follows Semantic Versioning for both the public API and the receipt wire format.
| Change type | Bump | Compatibility |
|---|---|---|
| Add optional payload field | minor | Older verifiers ignore unknown fields only if the field is namespaced or explicitly marked optional |
| Add required payload field | major | Older verifiers will reject. Coordinated cutover required |
| Remove or rename payload field | major | Breaking. Issuers must continue emitting v1.x receipts until verifier population catches up |
Add new protocol enum value | minor | Older verifiers will reject unknown enum values. Emit the new value only after verifiers support it |
| Tighten Zod constraint (e.g. format, length) | minor | Backward-compatible at parse time. The new constraint is forward-only |
| Verifier library API change (function signature) | major | Caller code must update |
| Verifier library API change (new optional argument) | minor | Existing callers unaffected |
Cross-version verification. Verifier v1.1.x verifies receipts issued under schema v1.0 and schema v1.1. v1.0 receipts will simply lack v1.1 fields (legal_posture, consent_context, etc.) and the verifier treats them as optional. There is no plan to drop v1.0 verification in any v1.x release. Dropping it requires a v2.0 major bump and a deprecation window of at least 12 months.
schema_version field. Receipts carry schema_version: "1.0" or schema_version: "1.1". The verifier dispatches schema validation on this field. Receipts without a schema_version are rejected (reason: "schema_invalid").
Acknowledgements
TrustReceipt is a cross-protocol evidence format. The following external parties define schemas, protocols, or infrastructure that TrustReceipt receipts can reference or attest to. None of these organisations is a formal collaborator on this repository. The relationships are interoperability integrations, not endorsements.
Protocol authors (define schema fields)
| Protocol | Author | TrustReceipt schema field |
|---|---|---|
| ACP (Agentic Commerce Protocol) | OpenAI + Stripe | authorization_scheme: "acp_session_token", protocol: "ACP" |
| AP2 (Agent Payment Protocol v2) | authorization_scheme: "ap2_mandate_jws", protocol: "AP2", ap2_consent_hash | |
| x402 (stablecoin payment) | Coinbase + Cloudflare | authorization_scheme: "evm_permit2" / "svm_token_authorization" / "x402_native", protocol: "x402" |
| MCAP (Mastercard Agent Pay) | Mastercard | authorization_scheme: "mcap_cart_binding", protocol: "MCAP", mcap_consent_hash |
| MCP (Model Context Protocol) | Anthropic | authorization_scheme: "mcp_tool_invocation", protocol: "MCP" |
| UCP (Universal Commerce Protocol) | authorization_scheme: "ucp_rule_set_plus_agent_token", protocol: "UCP" |
Active runtime providers (wired in trust_provider_assertions[])
These providers produce structured assertions that the recomputeLegalPosture logic in verify-1.1.ts reads when determining the verifier-authoritative LegalPosture. Use the exported type predicates (isRfc9421ProviderAssertion, isHumanProviderAssertion, isVisaTapProviderAssertion) to narrow to the typed shapes defined in types-1.1.ts.
| Provider | Assertion provider field | Integration |
|---|---|---|
| IETF RFC 9421 (HTTP Message Signatures) | "rfc9421-native" | Verifies HTTP Message Signatures from any agent with a public JWKS endpoint. The issuer wires this in optionally |
| HUMAN Security — AgenticTrust | "human" | Optional agent-identity integration. No HUMAN SDK is imported in this verifier package |
| Visa TAP (Trusted Agent Protocol) | "visa" | Validated when signer domain is *.visa.com or *.visa.net with tag "agent-browser-auth" or "agent-payer-auth" |
Issuer-side infrastructure (not used by this verifier package)
| Tool | Role |
|---|---|
| freeTSA | Default Phase-1 RFC 3161 timestamp authority. The URL is per receipt (tsa_endpoint field), not hardcoded here |
| AWS KMS | Ed25519 issuer signing keys and HMAC CMKs for PII-sourced hashes; handled by sibling package trust-receipt-kms-signer |
Trademark Notice
TrustReceipt is not affiliated with, endorsed by, or officially supported by Mastercard, Anthropic, Skyfire, Coinbase, HUMAN Security, Visa, or any other named protocol owner or company referenced in this specification. Protocol names (AP2, MCAP, ACP, MCP, x402, UCP) are used descriptively to indicate interoperability targets only. All trademarks and registered marks are the property of their respective owners.
License
MIT, see LICENSE. Copyright Trusteed (trusteed.xyz), 2026.
evaluation_id: two properties consumers must not assume
evaluation_id was added in 0.4.0. It makes a receipt point at the enforcement
record that produced its verdict, so an adjudicator can request that record by
identity instead of guessing from timestamp proximity. Two properties matter to consumers:
It identifies the evaluation, not the operation. A cached verdict is served
as-is, so several distinct operations can legitimately carry the same
evaluation_id. It is not unique per receipt and must never be used as an
idempotency key.
Its absence is a statement, not missing data. The reference issuer emits the field only when it resolves to a record that can actually be fetched. Two cases where it is deliberately omitted:
- the verdict came from cache, so the identity belongs to an earlier evaluation rather than to this operation;
- the decision was not written to the audit table (the ALLOW branch is sampled, so most permitted operations have no record to point at).
Emitting the identity anyway would invite a third party to request an audit
record that nobody wrote. An absent evaluation_id means the receipt does not
claim an evaluation. It does not mean the evaluation is hidden.