@parallel-protocol/x402
v0.5.2
Published
Parallel Protocol x402 payment middleware — Express, Next.js, Fastify, Hono
Downloads
494
Maintainers
Readme
@parallel-protocol/x402
HTTP-402 payment middleware for AI agents — Express, Next.js, Fastify, Hono.
Wrap any route with a price tag. When an agent (or any HTTP client) hits the route without a valid payment, it receives a 402 Payment Required response describing exactly what is owed and to whom. The agent signs an EIP-3009 authorization off-chain and retries the request with a payment-signature header; the middleware has the payment verified by the facilitator before the handler runs, and settled on-chain only after the handler returns a 2xx.
Zero smart-contract interaction on the merchant side. The Parallel facilitator verifies signatures and handles all on-chain execution.
Building the agent side instead? @parallel-protocol/x402-fetch is the payer counterpart — a fetch that settles these 402 challenges automatically.
How it works
Agent Merchant (this SDK) Facilitator
| | |
|── GET /api/data ──────────────>| |
| | (route matches, no header) |
|<─ 402 + payment-required ──────| |
| (base64 PaymentRequired) | |
| | |
| [agent signs EIP-3009 auth] | |
| | |
|── GET /api/data ──────────────>| |
| payment-signature: <base64> |── POST /x402/verify ───────>|
| |<─ { isValid: true } ────────|
| | |
| | [handler runs] |
| | |
| |── POST /x402/settle ───────>|
| |<─ { txHash, route, ... } ───|
| | |
|<─ 200 + payment-response ──────| |
| (base64 settlement proof) | |Key properties:
- The handler only runs after the signature is verified.
- The on-chain settlement happens after the handler produces a 2xx response — the agent is not charged on errors.
- If settlement fails (on-chain revert, facilitator timeout), the middleware returns
402and discards the handler's response. - Handlers are bounded by a 30-second timeout on Express, Hono and Next.js — a slow handler returns
504and the agent is not charged. (Fastify manages handler execution itself, so no timeout is applied there.) - CORS preflight (
OPTIONS) is always passed through without challenge.
Install
npm install @parallel-protocol/x402
# or
bun add @parallel-protocol/x402Install your framework separately (all are optional peer dependencies):
npm install express # Express 4 or 5
npm install next # Next.js 14 or 15
npm install fastify # Fastify 4 or 5
npm install hono # Hono 4+TypeScript consumers should also install viem (peer dependency) — the public types use its Address type. Node ≥ 18 is required.
Quick start
Express
import express from "express";
import { paymentMiddleware } from "@parallel-protocol/x402/express";
const app = express();
app.use(
paymentMiddleware({
facilitator: {
url: "https://agents.parallel.best",
},
routes: {
"/api/data": {
price: "0.01", // 0.01 USDp
network: "base",
payTo: "0xYourAddress",
},
},
}),
);
app.get("/api/data", (req, res) => {
res.json({ data: "protected content" });
});Next.js (App Router)
// app/api/data/route.ts
import { NextResponse } from "next/server";
import { withPayment } from "@parallel-protocol/x402/next";
const config = {
facilitator: { url: "https://agents.parallel.best" },
routes: {
"/api/data": {
price: "0.10",
network: "ethereum",
payTo: "0xYourAddress",
},
},
};
export const GET = withPayment(config, async (req) => {
return NextResponse.json({ data: "protected content" });
});Fastify
import Fastify from "fastify";
import { paymentMiddleware } from "@parallel-protocol/x402/fastify";
const app = Fastify();
await app.register(
paymentMiddleware({
facilitator: { url: "https://agents.parallel.best" },
routes: {
"/api/data": {
price: "0.05",
network: "avalanche",
payTo: "0xYourAddress",
},
},
}),
);
app.get("/api/data", async () => {
return { data: "protected content" };
});Hono
import { Hono } from "hono";
import { paymentMiddleware } from "@parallel-protocol/x402/hono";
const app = new Hono();
app.use(
paymentMiddleware({
facilitator: { url: "https://agents.parallel.best" },
routes: {
"/api/data": {
price: "0.01",
network: "base",
payTo: "0xYourAddress",
},
},
}),
);
app.get("/api/data", (c) => c.json({ data: "protected content" }));Configuration
PaymentMiddlewareConfig
interface PaymentMiddlewareConfig {
facilitator: FacilitatorConfig;
routes: Record<string, RouteConfig>;
}FacilitatorConfig
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| url | string | Yes | Base URL of the Parallel facilitator (e.g. https://agents.parallel.best). The SDK internally appends /x402/verify and /x402/settle to this value. |
| apiKey | string | No | Optional API key, sent as the X-API-Key header on facilitator requests. |
RouteConfig
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| price | string \| bigint | Yes | Amount required. A string like "0.01" is parsed with each accepted token's own decimals (or with decimals when set explicitly). Pass a bigint to skip parsing. |
| decimals | number | No | Override for the decimals used to parse a string price. By default each accepted token is priced with its own decimals from the @parallel-protocol/chains catalog — a "0.01" price is correct whether paid in USDp (18) or USDC (6), no configuration. Only needed for a custom token outside the catalog; without it, an unknown token raises X402ConfigError rather than being silently guessed. |
| network | string | Yes | Chain slug: "ethereum", "base", "avalanche", "hyperevm". |
| payTo | Address | Yes | The merchant's receiving address. |
| acceptedTokens | Address[] | No | List of accepted token addresses. Defaults to [USDp, USDC, sUSDp] for known networks (see Default tokens). |
| description | string | No | Human-readable description of the resource, included in the 402 body. |
Route matching uses longest prefix wins: /api/v1/users will match a /api/v1 route config before a /api one.
Default tokens
For the four natively supported networks, acceptedTokens defaults to [USDp, USDC, sUSDp]:
| Network | USDp | USDC | sUSDp |
|---------|------|------|-------|
| ethereum | 0x9B3a8f7CEC208e247d97dEE13313690977e24459 | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 | 0xd3a452B305C8285c0Dd7b8537665c734d3D279eF |
| base | 0x76A9A0062ec6712b99B4f63bD2b4270185759dd5 | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 | 0x472eD57b376fE400259FB28e5C46eB53f0E3e7E7 |
| avalanche | 0x9eE1963f05553eF838604Dd39403be21ceF26AA4 | 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E | 0x9d92c21205383651610f90722131655a5b8ed3e0 |
| hyperevm | 0xBE65F0F410A72BeC163dC65d46c83699e957D588 | 0xb88339cb7199b77e23db6e890353e22632ba630f | 0x9B3a8f7CEC208e247d97dEE13313690977e24459 |
For any other network (from the full Parallel 25-chain catalog), you must provide acceptedTokens explicitly — the middleware throws X402ConfigError on the first request to that route otherwise.
You can also import the token list directly:
import { PARALLEL_TOKENS, getDefaultAcceptedTokens } from "@parallel-protocol/x402";
const baseTokens = PARALLEL_TOKENS.base;
// { usdp: "0x76A9...", susdp: "0x472e...", usdc: "0x8335...", USDS: "0x820C...", sUSDS: "0x5875..." }
const defaults = getDefaultAcceptedTokens("base");
// ["0x76A9...", "0x8335...", "0x472e..."] (USDp + USDC + sUSDp)sUSDp is included in the defaults (routes H, I, J, K, L are enabled out of the box). To opt out and accept only USDp and USDC, pass acceptedTokens explicitly:
import { PARALLEL_TOKENS } from "@parallel-protocol/x402";
routes: {
"/api/data": {
price: "0.01",
network: "base",
payTo: "0xYourAddress",
acceptedTokens: [
PARALLEL_TOKENS.base.usdp,
PARALLEL_TOKENS.base.usdc,
// sUSDp omitted → routes H, I, J, K, L disabled for this route
],
},
}Accepting Parallelizer collateral payments: Each supported network exposes additional collateral tokens whitelisted in the Parallelizer contract. They are available in PARALLEL_TOKENS but excluded from the defaults. Add them to acceptedTokens to let merchants receive them directly:
| Network | Token | Key | Yield-bearing |
|---------|-------|-----|---------------|
| ethereum | frxUSD | PARALLEL_TOKENS.ethereum.frxUSD | No |
| ethereum | sfrxUSD | PARALLEL_TOKENS.ethereum.sfrxUSD | Yes |
| ethereum | USDe | PARALLEL_TOKENS.ethereum.USDe | No |
| ethereum | sUSDe | PARALLEL_TOKENS.ethereum.sUSDe | Yes |
| base | USDS | PARALLEL_TOKENS.base.USDS | No |
| base | sUSDS | PARALLEL_TOKENS.base.sUSDS | Yes |
| avalanche | ygamiUSDC | PARALLEL_TOKENS.avalanche.ygamiUSDC | Yes |
| hyperevm | USDe | PARALLEL_TOKENS.hyperevm.USDe | No |
| hyperevm | sUSDe | PARALLEL_TOKENS.hyperevm.sUSDe | Yes |
import { PARALLEL_TOKENS } from "@parallel-protocol/x402";
routes: {
"/api/data": {
price: "0.01",
network: "ethereum",
payTo: "0xYourAddress",
acceptedTokens: [
PARALLEL_TOKENS.ethereum.usdp,
PARALLEL_TOKENS.ethereum.usdc,
PARALLEL_TOKENS.ethereum.sUSDe, // opt-in → agents can pay with sUSDe
PARALLEL_TOKENS.ethereum.sfrxUSD, // opt-in → agents can pay with sfrxUSD
],
},
}Note: the facilitator validates that
tokenOutis a whitelisted collateral on the target chain. If you add an address that is not in the Parallelizer's collateral list, payments to that route will be rejected at settlement.
Payment headers
402 response: payment-required
When no valid payment is present, the middleware returns HTTP 402 with a payment-required header containing a base64-encoded PaymentRequired object. The same JSON is also sent as the response body, and access-control-expose-headers: payment-required is set so browsers can read the header:
interface PaymentRequired {
x402Version: 2;
error?: string; // present on re-challenge (e.g. "INVALID_SIGNATURE")
resource: {
url: string;
description?: string;
mimeType: "application/json";
};
accepts: Array<{ // one entry per accepted token
scheme: "exact";
network: string; // EIP-155 format: "eip155:8453"
asset: string; // token address
amount: string; // smallest unit of `asset`, per its own decimals
payTo: string;
maxTimeoutSeconds: 300;
extra: { decimals: number }; // decimals of `asset` — payers need not guess
}>;
}Request header: payment-signature
The agent attaches a payment-signature (or X-PAYMENT) header with a base64-encoded SignedPaymentPayload:
// Minimal Route A payload (USDp transfer)
const signedPayload = {
scheme: "exact",
network: "base",
method: "transferWithAuthorization",
payload: {
signature: "0x...",
authorization: {
from: "0xAgentAddress",
to: "0xMerchantAddress",
value: "10000000000000000", // 0.01 USDp in wei
validAfter: "0",
validBefore: "1234567890", // Unix timestamp
nonce: "0x...",
},
},
};
const header = Buffer.from(JSON.stringify(signedPayload)).toString("base64");See @parallel-protocol/payment-core for EIP-3009 signing helpers.
200 response: payment-response
On success, the middleware sets a payment-response header with a base64-encoded settlement confirmation:
interface PaymentConfirmation {
success: true;
txHash: string;
route: string; // "A", "B", "C", ...
chain: string; // "base"
gasSponsored: boolean;
networkId: string; // "eip155:8453"
}The access-control-expose-headers header is set automatically so browsers can read payment-response.
Payment routes
The facilitator supports 12 settlement routes. The route is selected automatically based on the signed payload:
| Route | Credential method(s) | tokenIn (agent pays) | tokenOut (merchant receives) | Gas est. | Notes |
|-------|--------------------------|---------------------|------------------------------|----------|-------|
| A | transferWithAuthorization | USDp | USDp | ~65K | Direct USDp transfer |
| B | swapExactOutputWithAuthorization | USDp | USDC (exact) | ~120K | Parallelizer swaps agent's USDp → exact USDC for merchant |
| C | depositWithAuthorization | USDp | sUSDp | ~90K | Agent's USDp deposited into savings vault; merchant receives sUSDp shares |
| D | swapExactOutputWithAuthorization | USDp | non-USDC backing collateral (exact) | ~120K | Same as B but for other Parallelizer collaterals |
| E | swapExactInputWithAuthorization | USDC (exact) | USDp (min) | ~120K | Agent pays exact USDC; Parallelizer delivers minimum USDp to merchant |
| F | transferWithAuthorization | USDC (or any EIP-3009 token) | same token | ~65K | Direct EIP-3009 token push |
| G | swapExactInputWithAuthorization | USDC (exact) | sUSDp | ~200K | USDC → USDp swap, then USDp deposited as sUSDp for merchant (atomic multicall3) |
| H | redeemWithAuthorization | sUSDp shares | USDp | ~130K | Agent burns sUSDp shares; merchant receives USDp assets |
| I | redeemWithAuthorization + swapExactOutputWithAuthorization | sUSDp | USDC (exact) | ~180K | Agent redeems sUSDp → USDp, then swaps USDp → exact USDC for merchant (atomic multicall3) |
| J | transferWithAuthorization | sUSDp | sUSDp | ~65K | Direct sUSDp share transfer |
| K | redeemWithAuthorization + swapExactOutputWithAuthorization | sUSDp | non-USDC backing (exact) | ~180K | Same as I but for other Parallelizer collaterals (atomic multicall3) |
| L | partialRedeemWithAuthorization | sUSDp (partial) + USDp | USDp or backing collateral | ~180K | Partial sUSDp redeem combined with existing USDp; delivers to merchant (atomic multicall3) |
Chain requirements:
- Routes B, D, E, G, I, K, L require the Parallelizer to be deployed on the target chain (Ethereum, Base, Avalanche, HyperEVM).
- Routes C, G, H, I, J, K, L require sUSDp (savings module) to be deployed on the target chain.
- Route A and F work on all chains where the respective token is deployed.
- Gas sponsorship is decided by the facilitator per payment and reported as
gasSponsoredin the settlement receipt.
Exports
Main entry (@parallel-protocol/x402)
import {
// Middleware (framework-agnostic)
createPaymentGate,
createPaymentMiddleware,
// Facilitator client
FacilitatorClient,
// Errors
X402ConfigError,
X402RuntimeError,
X402_ERROR_CODES,
type X402ErrorCode,
// Tokens
PARALLEL_TOKENS,
getDefaultAcceptedTokens,
type ParallelNetwork,
// Types
type FacilitatorConfig,
type FacilitatorResponse,
type HTTPAdapter,
type MiddlewareResult,
type PassResult,
type PaymentConfirmation,
type PaymentFailure,
type PaymentGateResult,
type PaymentMiddlewareConfig,
type PaymentRequired,
type PaymentRequirements,
type ResourceInfo,
type RouteConfig,
type RunHandlerResult,
// Utils
encodeBase64,
parsePrice,
toEip155Network,
validateAddress,
} from "@parallel-protocol/x402";Framework adapters
| Import path | Export | Framework |
|-------------|--------|-----------|
| @parallel-protocol/x402/express | paymentMiddleware(config) | Express 4/5 |
| @parallel-protocol/x402/next | withPayment(config, handler) | Next.js 14/15 |
| @parallel-protocol/x402/fastify | paymentMiddleware(config) | Fastify 4/5 |
| @parallel-protocol/x402/hono | paymentMiddleware(config) | Hono 4+ |
Each subpath also exports its adapter class (ExpressAdapter, NextAdapter, FastifyAdapter, HonoAdapter) for use with createPaymentGate.
Advanced: createPaymentGate
For frameworks not covered above, or for custom integrations, you can use the lower-level createPaymentGate function:
import { createPaymentGate } from "@parallel-protocol/x402";
const gate = createPaymentGate(config);
// In your request handler:
const result = await gate({
getHeader: (name) => request.headers[name],
getMethod: () => request.method,
getPath: () => request.path,
getUrl: () => request.url,
});
if (result.type === "pass") {
// Route not configured for payment, proceed normally
} else if (result.type === "error") {
// Send 402: result.result.status / result.result.headers / result.result.body
} else {
// result.type === "verified"
// Run your handler...
const handlerStatus = await runHandler();
if (handlerStatus >= 200 && handlerStatus < 300) {
const settlement = await result.settle();
if (settlement.success) {
// settlement.txHash / settlement.route / settlement.gasSponsored
}
}
}createPaymentGate only verifies; it does not run the handler. Use createPaymentMiddleware if you want the full verify → run → settle flow managed automatically.
FacilitatorClient
Low-level client for the facilitator's x402 endpoints:
import { FacilitatorClient, PARALLEL_TOKENS, type FacilitatorPaymentRequirements } from "@parallel-protocol/x402";
const client = new FacilitatorClient({
url: "https://agents.parallel.best",
});
const requirements: FacilitatorPaymentRequirements = {
maxAmountRequired: "10000000000000000", // 0.01 USDp, smallest unit
asset: PARALLEL_TOKENS.base.usdp,
payTo: "0xYourAddress",
network: "base", // plain chain slug
};
// Phase 1: verify signature + reserve nonce (no on-chain tx)
await client.verify(paymentHeader, requirements); // throws X402RuntimeError on failure
// Phase 2: submit on-chain, return settlement result
const result = await client.settle(paymentHeader, requirements);
if (result.success) {
console.log(result.txHash); // on-chain tx hash
console.log(result.route); // "A", "B", ...
console.log(result.gasSponsored); // boolean
} else {
console.error(result.error.code); // e.g. "SUBMISSION_FAILED"
}verify uses a 10-second timeout. settle uses a 60-second timeout to accommodate on-chain confirmation latency. verify throws on any failure; settle returns a FacilitatorResponse union on facilitator errors and throws X402RuntimeError on connectivity issues, an undecodable payment header, or a malformed facilitator response.
A single-call client.pay(paymentHeader, requirements) (POST /x402/pay) also exists — it verifies and settles in one round trip, without the two-phase guarantees the middleware relies on.
Error codes
Thrown by the SDK
| Error class | When |
|-------------|------|
| X402ConfigError | Invalid config at construction (bad payTo, bad facilitator URL, empty routes, non-positive price) — plus, on the first request to a route, an unknown network with no acceptedTokens, or an accepted token whose decimals are neither in the catalog nor set via decimals |
| X402RuntimeError | Runtime failure; .code is one of the SDK codes below, or a facilitator error code propagated verbatim (see next table) |
| X402RuntimeError.code | Meaning |
|-------------------------|---------|
| FACILITATOR_UNAVAILABLE | Facilitator timed out (10s on verify, 60s on settle) or was unreachable |
| FACILITATOR_INVALID_RESPONSE | Facilitator returned an unexpected response shape |
| INVALID_PAYMENT | payment-signature / X-PAYMENT header could not be base64-decoded |
Propagated from the facilitator
When the facilitator rejects a payment, its error code is forwarded as the error field on the re-issued 402 body. Common values:
| Code | Meaning |
|------|---------|
| INVALID_SIGNATURE | EIP-712 signature recovery failed |
| INVALID_NONCE | Nonce already used (replay detected) |
| PAYMENT_EXPIRED | validBefore has passed |
| INSUFFICIENT_AMOUNT | Signed amount < required amount |
| NETWORK_MISMATCH | Payload chain ≠ route config chain |
| RATE_LIMIT_EXCEEDED | Signer has exceeded the hourly transaction limit |
| PARALLELIZER_PAUSED | On-chain router temporarily paused (swap routes only) |
Utilities
import {
parsePrice,
validateAddress,
toEip155Network,
encodeBase64,
getDefaultAcceptedTokens,
PARALLEL_TOKENS,
} from "@parallel-protocol/x402";| Function | Signature | Description |
|----------|-----------|-------------|
| parsePrice | (price: string \| bigint, decimals?: number) => bigint | Parse "0.01" → 10000000000000000n (18 decimals by default) |
| validateAddress | (address: string) => address is Address | Check 0x + 40-hex-char format |
| toEip155Network | (network: string) => string | "base" → "eip155:8453" |
| encodeBase64 | (obj: unknown) => string | JSON.stringify → base64 |
| getDefaultAcceptedTokens | (network: string) => [Address, Address, Address] \| undefined | [USDp, USDC, sUSDp] for known networks |
Comparison with MPP
| | @parallel-protocol/x402 | @parallel-protocol/mpp |
|-|--------------------------|--------------------------|
| Challenge status | 402 | 402 |
| Challenge header | payment-required: <base64> | WWW-Authenticate: Payment … |
| Credential header | payment-signature: <base64> | Authorization: Payment <base64url> |
| Receipt header | payment-response: <base64> | Payment-Receipt: <base64url> |
| Token per route | list (acceptedTokens) | single (currency) |
| Decimals | per accepted token (chains catalog) | 6 (fixed default) |
| Auth standard | x402 / Coinbase | RFC 9110 Payment auth-scheme |
| Facilitator endpoints | /x402/verify, /x402/settle | /mpp/verify, /mpp/settle |
| On-chain routes | A–L | A–L (same) |
Both protocols share the same facilitator backend, the same SignedPaymentPayload authorization format, and the same settlement routes.
Related packages
| Package | Purpose |
|---------|---------|
| @parallel-protocol/payment-core | EIP-3009 authorization builders and EIP-712 signing helpers (agent-side) |
| @parallel-protocol/x402-types | The facilitator's HTTP wire contract (Zod schemas) |
| @parallel-protocol/mpp | Machine Payments Protocol middleware (IETF Payment auth-scheme) |
| @parallel-protocol/chains | 25-chain catalog with contract addresses |
License
MIT © Parallel Protocol
