Skip to content

Latest commit

 

History

History
368 lines (291 loc) · 10.6 KB

File metadata and controls

368 lines (291 loc) · 10.6 KB

x402 + PEAC Integration Guide

This guide explains how to integrate PEAC receipts with x402 HTTP 402 payment flows.

Live demo: x402.peacprotocol.org | Visual demo repo

Overview

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

Quick Start

1. Install

pnpm add @peac/protocol @peac/crypto @peac/rails-x402

2. Issue a Receipt After Payment

import { 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/commerce extension is strict: it carries only payment_rail, amount_minor, currency, asset, reference, env, and event. x402-specific transaction details such as network, 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.

3. Verify a Receipt

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);
}

HTTP Flow

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          |

402 Response Headers (x402 v2)

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.com

200 Response with Receipt

HTTP/1.1 200 OK
Content-Type: application/json
PEAC-Receipt: eyJhbGciOiJFZERTQSIsInR5cCI6InBlYWMtcmVjZWlwdC8wLjEifQ...

x402 v2 Dialect

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' });

Header Differences

v1 Header v2 Header
X-PAYMENT Payment-Required
X-PAYMENT-RESPONSE Payment-Response

Common CAIP-2 identifiers

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.

Payment schemes

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 scheme string identifier in the interaction record (available at proofs.x402.offer for both v1 and v2, and at evidence.scheme for 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.

Discovery with peac.txt

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/negotiate

Receipt Structure

PEAC 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"
    }
  }
}

Verification Strategies

At Request Time (Server)

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();
});

Offline (Client)

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));
}

Audit (Later)

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.
}

Example: Express Server

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 });
});

Resources