This guide explains how to integrate PEAC receipts with x402 HTTP 402 payment flows.
Live demo: x402.peacprotocol.org | Visual demo repo
x402 is an open standard for HTTP 402 payments; its Signed Offers and Receipts extension defines payment-native signed artifacts. PEAC preserves those artifacts inside portable, offline-verifiable interaction records:
| Layer | What It Does |
|---|---|
| x402 | Defines the payment flow and payment-native signed offers/receipts |
| PEAC | Preserves those artifacts inside a portable, offline-verifiable record |
Together they enable:
- Instant payments via crypto rails
- Offline-verifiable payment and delivery artifacts
- Audit trail with payment evidence
pnpm add @peac/protocol @peac/crypto @peac/rails-x402import { issue } from '@peac/protocol';
const { jws } = await issue({
iss: 'https://your-api.com',
kind: 'evidence',
type: 'org.peacprotocol/payment',
pillars: ['commerce'],
extensions: {
'org.peacprotocol/commerce': {
payment_rail: 'x402',
amount_minor: '10000',
currency: 'USD',
asset: 'USDC',
env: 'live',
event: 'settlement',
reference: `x402_${txHash}`, // caller-assigned reference; e.g. bind the x402 transaction hash
},
},
privateKey,
kid: 'your-key-id',
});
// Set the PEAC-Receipt header in your response
response.setHeader('PEAC-Receipt', jws);The
org.peacprotocol/commerceextension is strict: it carries onlypayment_rail,amount_minor,currency,asset,reference,env, andevent. x402-specific transaction details such asnetwork, transaction hash, and recipient are not commerce fields. Preserve them through the x402 mapping path, for example@peac/adapter-x402's record-mapping helpers (toPeacRecord), which normalize x402-specific fields into the record evidence instead of adding them to the commerce extension.
import { verifyReceipt } from '@peac/protocol';
const result = await verifyReceipt(receiptJws);
if (result.ok) {
console.log('Payment verified:', result.claims.payment?.evidence?.tx_hash);
} else {
console.error('Invalid receipt:', result.error);
}Client Server
| |
| 1. GET /resource |
|----------------------------------->|
| |
| 2. 402 Payment Required |
| Payment-Required: {...} |
| PEAC-Issuer: https://... |
|<-----------------------------------|
| |
| 3. Pay via x402 SDK |
|----------------------------------->|
| |
| 4. 200 OK |
| PEAC-Receipt: eyJhbG... |
|<-----------------------------------|
| |
| 5. Verify receipt offline |
HTTP/1.1 402 Payment Required
Content-Type: application/problem+json
Payment-Required: {"network":"eip155:8453","asset":"USDC","amount":"100","recipient":"0x..."}
PEAC-Issuer: https://your-api.comHTTP/1.1 200 OK
Content-Type: application/json
PEAC-Receipt: eyJhbGciOiJFZERTQSIsInR5cCI6InBlYWMtcmVjZWlwdC8wLjEifQ...The @peac/rails-x402 adapter supports both x402 v1 and v2:
import { X402Adapter } from '@peac/rails-x402';
// Auto-detect v1 or v2 based on headers
const adapter = new X402Adapter({ dialect: 'auto' });
// Force v2 only
const v2Adapter = new X402Adapter({ dialect: 'v2' });| v1 Header | v2 Header |
|---|---|
X-PAYMENT |
Payment-Required |
X-PAYMENT-RESPONSE |
Payment-Response |
These are the CAIP-2 network identifiers commonly seen in x402 offers. PEAC
term-matches network as an opaque string and does not maintain a closed
allowlist of networks.
| Network | CAIP-2 ID |
|---|---|
| Base Mainnet | eip155:8453 |
| Base Sepolia | eip155:84532 |
| Polygon Mainnet | eip155:137 |
| Avalanche Mainnet | eip155:43114 |
| Avalanche Fuji | eip155:43113 |
| Solana Mainnet | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp |
| Solana Devnet | solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1 |
Solana CAIP-2 identifiers use the genesis-block hash prefix per
CAIP-2; the plain-label forms
solana:mainnet and solana:devnet are not canonical.
Upstream x402 distinguishes two payment schemes today: exact (single-shot
fixed amount) and upto (usage-based with an authorized maximum). PEAC is
scheme-agnostic: the adapter preserves the scheme identifier verbatim and
term-matches it alongside network, asset, payTo, and amount without
interpreting scheme-specific semantics.
PEAC's role for any x402 scheme, stated precisely:
- Preserves and surfaces the
schemestring identifier in the interaction record (available atproofs.x402.offerfor both v1 and v2, and atevidence.schemefor v2) - Term-matches required fields already present in the signed artifact
- Does NOT enforce scheme-specific invariants such as single-use authorization, time bounds, recipient binding, facilitator binding, or max-vs-actual settlement correctness
Those invariants are the x402 scheme layer's responsibility and are enforced on-chain or by the facilitator, not by PEAC.
For the authoritative compatibility matrix that distinguishes three truth
surfaces — upstream x402 protocol, upstream facilitator and SDK state, and
PEAC-tested behavior — see
docs/compatibility/x402-scheme-coverage.md.
The normative specification is
docs/specs/X402-PROFILE.md § 3.0.
Publish payment terms at /.well-known/peac.txt:
version: 0.9.18
usage: conditional
receipts: required
rate_limit: 1000/hour
price: 0.50
currency: USD
payment_methods: [x402]
payment_networks: [eip155:8453, solana:mainnet]
negotiate: https://your-api.com/negotiatePEAC receipts with x402 evidence:
{
"typ": "interaction-record+jwt",
"iss": "https://your-api.com",
"aud": "https://your-api.com/resource",
"iat": 1703001234,
"exp": 1703004834,
"amt": 100,
"cur": "USD",
"payment": {
"rail": "x402",
"asset": "USDC",
"env": "live",
"reference": "x402_abc123",
"evidence": {
"network": "eip155:8453",
"tx_hash": "0xabc...",
"recipient": "0x123...",
"x402_version": "v2"
}
}
}app.use(async (req, res, next) => {
const receipt = req.headers['peac-receipt'];
if (!receipt) {
return res.status(402).json({
type: 'https://www.peacprotocol.org/errors/payment-required',
title: 'Payment Required',
status: 402,
});
}
const result = await verifyReceipt(receipt);
if (!result.ok) {
return res.status(401).json({
type: 'https://www.peacprotocol.org/errors/invalid-receipt',
title: 'Invalid Receipt',
status: 401,
});
}
req.receipt = result.claims;
next();
});import { verify } from '@peac/crypto';
// Client can verify without network calls
const { valid, payload } = await verify(receiptJws, issuerPublicKey);
if (valid) {
console.log('Receipt is valid');
console.log('Paid:', payload.amt, payload.cur);
console.log('Expires:', new Date(payload.exp * 1000));
}Receipts can be stored and verified later for audit purposes:
// Store receipt with request log
await db.insert('audit_log', {
request_id: requestId,
receipt_jws: receiptJws,
timestamp: new Date(),
});
// Verify during audit
const receipts = await db.query('SELECT receipt_jws FROM audit_log WHERE ...');
for (const { receipt_jws } of receipts) {
const result = await verifyReceipt(receipt_jws);
// Check validity, amounts, timing, etc.
}import express from 'express';
import { issue, verifyReceipt } from '@peac/protocol';
import { generateKeypair } from '@peac/crypto';
const app = express();
const { privateKey, publicKey } = await generateKeypair();
app.get('/premium', async (req, res) => {
const receipt = req.headers['peac-receipt'] as string;
if (!receipt) {
return res.status(402).json({
type: 'https://www.peacprotocol.org/errors/payment-required',
title: 'Payment Required',
x402: {
network: 'eip155:8453',
asset: 'USDC',
amount: '100',
},
});
}
const result = await verifyReceipt(receipt);
if (!result.ok) {
return res.status(401).json({ error: 'Invalid receipt' });
}
res.json({ data: 'Premium content' });
});
// Endpoint called after x402 payment succeeds
app.post('/issue-receipt', async (req, res) => {
const { txHash } = req.body;
const { jws } = await issue({
iss: 'https://your-api.com',
kind: 'evidence',
type: 'org.peacprotocol/payment',
pillars: ['commerce'],
extensions: {
'org.peacprotocol/commerce': {
payment_rail: 'x402',
amount_minor: '10000',
currency: 'USD',
asset: 'USDC',
env: 'live',
event: 'settlement',
reference: `x402_${txHash}`,
},
},
privateKey,
kid: 'key-2025',
});
res.json({ receipt: jws });
});- Live demo: x402.peacprotocol.org
- x402 spec: x402.org
- Example code: examples/x402-node-server
- Stripe x402 crypto demo:
pnpm --filter @peac/example-stripe-x402-crypto demo(source | profile) - Package (x402): @peac/rails-x402
- Package (Stripe): @peac/rails-stripe