@axiru/x402-receipt-verifier
v0.1.1
Published
Verify x402 SignedOffers and SignedReceipts (JWS and EIP-712) independently of the party that issued them. Zero runtime dependencies beyond the Axiru spec types.
Maintainers
Readme
@axiru/x402-receipt-verifier
Independent verification of x402 Signed Offers and Signed Receipts, the evidence records defined by the upstream x402 offer and receipt extension.
Apache-2.0. Zero runtime dependencies beyond @axiru/spec.
Why this exists
A payment receipt that only the payer can verify is not evidence. It is a claim.
x402 settlement produces two signed artifacts: an offer from the resource server describing what is being sold and for how much, and a receipt confirming that settlement happened. If your auditor, your counterparty, or a regulator has to take your word for the contents of those artifacts, the audit trail is worth very little. This package lets anyone with the envelopes and a network connection check both signatures and confirm that the receipt actually settles the offer it claims to settle. No Axiru account, no API key, no hosted service.
That is the point. A format becomes a standard when third parties can verify it without asking the issuer for permission.
Install
npm install @axiru/x402-receipt-verifierNode 18 or later. Works in Edge and Workers runtimes when you inject fetchDidDocument.
What it verifies
Two wire formats:
| Format | Identity | Signature check |
| ----------- | ----------------------------------------- | ------------------------------------------------------------ |
| JWS | did:web:<host> | Resolved from https://<host>/.well-known/did.json. ES256 (P-256), EdDSA (Ed25519), and ES256K (secp256k1). |
| EIP-712 | did:pkh:eip155:<chainId>:<address> | Recovered from the secp256k1 signature. Injected verifier, see below. |
The JWS path is fully self-contained and uses only the Node crypto and fetch globals. The EIP-712 path requires you to inject an Eip712Verifier, because this package deliberately carries no keccak or secp256k1 dependency; a zero-dependency verifier is easier to audit and easier to trust.
Quickstart
import {
verifySignedOffer,
verifySignedReceipt,
verifyEvidenceChain
} from "@axiru/x402-receipt-verifier";
// Verify one artifact.
const offer = await verifySignedOffer(signedOffer);
if (!offer.ok) {
console.error("offer rejected:", offer.reason, offer.detail);
} else {
console.log("signed by", offer.value.signerDid, offer.value.payload);
}
// Or verify the whole chain in one call: both signatures, plus the
// receipt-matches-offer check, plus the payer allowlist.
const chain = await verifyEvidenceChain(
signedOffer,
signedReceipt,
["did:pkh:eip155:8453:0xAgentWalletAddress"], // authorized payers
);
if (!chain.ok) {
// `stage` tells you exactly where it failed: "offer" | "receipt" | "match".
console.error(`rejected at ${chain.stage}: ${chain.reason}`);
}Nothing throws on adversarial input. Every public function returns a Result-shaped union, because this code sits on an ingestion path where an unhandled exception turns a hostile payload into a 5xx.
Failure reasons
structural_invalid, unsupported_algorithm, unsupported_format, did_resolution_failed, did_key_not_found, signature_invalid, kid_did_mismatch, eip712_not_implemented, internal.
These are stable identifiers. Log them, alert on them, and assert on them in tests.
Binding evidence to an authorization
Verifying that an offer and receipt are well formed and correctly signed is necessary but not sufficient. A valid receipt for the wrong amount is still the wrong payment. Pass an expected settlement to bind the evidence to what you actually authorized:
const chain = await verifyEvidenceChain(signedOffer, signedReceipt, authorizedPayers, deps, {
amount_minor_units: "12000000",
asset: "USDC",
pay_to: "0xMerchantAddress"
});Without that argument the chain check confirms internal consistency only. With it, the chain check confirms that the settled value is the value your policy allowed.
Injecting dependencies
const deps = {
// Cache did:web documents however your host caches things.
fetchDidDocument: async (did: string) => myCache.getOrFetch(did),
// Bring your own keccak/secp256k1 for EIP-712 envelopes.
verifyEip712: async (typedData, signature) => recoverAddress(typedData, signature),
// Deterministic clock for tests and replay.
now: () => 1_760_000_000
};There is no default singleton. did:web caching strategy is host specific and a bad default here would quietly shadow yours.
Related packages
@axiru/specdefines the offer, receipt, and verification result types this package consumes.@axiru/x402-policy-middlewareis the pre-authorization half of the same loop: decide before you pay, verify after you paid.@axiru/agent-spend-guardrailsevaluates spend policy in process.
License
Apache-2.0. Copyright 2026 Axiru. See LICENSE.
