@402md/x402
v0.1.1
Published
x402 protocol implementation — one-liner paywalls, auto-paying fetch, Base + Stellar support
Downloads
203
Maintainers
Readme
@402md/x402
One-liner paywalls and auto-paying fetch for the x402 protocol. Supports Base (EVM) and Stellar (Soroban) with built-in budget controls for AI agents.
// Server — one line to paywall any route
app.get('/api/premium', paywall({ price: '0.01', payTo: '0x...', network: 'base' }), handler)
// Client — auto-pays 402 responses transparently
const res = await client.fetch('https://api.example.com/premium')Table of Contents
- Install
- Quick Start
- Server Middleware
- Client
- How Payment Validation Works
- Supported Networks
- Budget System
- Dynamic Pricing
- Cart / E-commerce Example
- Subscription with Wallet Auth
- USDC Amount Handling
- API Reference
- Optional Dependencies
Install
npm install @402md/x402Chain SDKs are optional — install only what you need:
# For EVM networks (Base, Base Sepolia)
npm install viem
# For Stellar networks (Stellar, Stellar Testnet)
npm install @stellar/stellar-sdkIf you try to use a network without its SDK installed, you get a clear error:
Error: viem is required for EVM networks. Install it: npm install viemQuick Start
Paywall a route (server)
import express from 'express'
import { paywall } from '@402md/x402'
const app = express()
app.get(
'/api/weather',
paywall({
price: '0.001', // $0.001 USDC per request
payTo: '0xYourAddress', // your wallet
network: 'base' // Base mainnet
}),
(req, res) => {
// req.x402.payment contains { settled, txHash, payer, network }
res.json({ temperature: 22, unit: 'celsius' })
}
)
app.listen(3000)Consume a paywalled API (client)
import { createPaymentClient } from '@402md/x402'
const client = await createPaymentClient({
evmPrivateKey: process.env.PRIVATE_KEY,
network: 'base',
budget: { maxPerCall: '0.01', maxPerDay: '1.00' }
})
const res = await client.fetch('https://api.example.com/api/weather')
const data = await res.json()
console.log(data) // { temperature: 22, unit: 'celsius' }Server Middleware
All middleware functions accept a PaywallConfig object:
interface PaywallConfig {
price: string // USDC amount, e.g. '0.001'
payTo: string // recipient address (EVM or Stellar)
network: PaymentNetwork // 'base' | 'base-sepolia' | 'stellar' | 'stellar-testnet'
facilitatorUrl?: string // override default facilitator
description?: string // human-readable resource description
maxTimeoutSeconds?: number // payment validity window (default: 300)
onPayment?: (payment: VerifiedPayment) => void // post-payment callback
}Express / Connect
import express from 'express'
import { paywall } from '@402md/x402'
const app = express()
// Simple — one config object
app.get(
'/api/data',
paywall({ price: '0.01', payTo: '0xABC...', network: 'base' }),
(req, res) => {
// Payment verified and settled. Access details:
const { settled, txHash, payer, network } = req.x402.payment
res.json({ data: 'premium content', payer })
}
)
// With callback — log every payment
app.post(
'/api/generate',
paywall({
price: '0.05',
payTo: '0xABC...',
network: 'base',
description: 'AI text generation',
onPayment: (payment) => {
console.log(`Received payment from ${payment.payer}: ${payment.txHash}`)
}
}),
(req, res) => {
res.json({ text: 'generated content' })
}
)
// Testnet — same API, just change network
app.get(
'/api/test',
paywall({ price: '0.001', payTo: '0xABC...', network: 'base-sepolia' }),
handler
)Works with any Express-compatible framework (Connect, Polka, etc.).
Hono
import { Hono } from 'hono'
import { paywallHono, getPayment } from '@402md/x402'
const app = new Hono()
app.get(
'/api/data',
paywallHono({ price: '0.01', payTo: '0xABC...', network: 'base' }),
(c) => {
const payment = getPayment(c)
return c.json({ data: 'premium content', payer: payment?.payer })
}
)
export default appgetPayment(c) is a typed helper that retrieves the VerifiedPayment from Hono's context.
Next.js App Router
// app/api/premium/route.ts
import { paywallNextjs } from '@402md/x402'
export const GET = paywallNextjs(
{ price: '0.01', payTo: '0xABC...', network: 'base' },
async (req) => {
// req.x402.payment is available here
return Response.json({ data: 'premium content' })
}
)
export const POST = paywallNextjs(
{ price: '0.05', payTo: '0xABC...', network: 'base' },
async (req) => {
const body = await req.json()
return Response.json({ result: 'processed', input: body })
}
)Wraps your route handler — if no valid payment is present, returns 402 before your handler runs.
Client
Persistent Client
Best for agents or services that make multiple paid requests:
import { createPaymentClient } from '@402md/x402'
const client = await createPaymentClient({
evmPrivateKey: process.env.PRIVATE_KEY,
network: 'base',
budget: {
maxPerCall: '0.10',
maxPerDay: '5.00',
maxPerSession: '2.00'
}
})
// Auto-paying fetch — handles 402 transparently
const res = await client.fetch('https://api.example.com/premium')
// Check balance
const balance = await client.getBalance() // '42.50'
// Get wallet address
const address = client.getAddress() // '0x...'
// Manual payment (advanced)
const token = await client.pay(paymentRequirement)What client.fetch() does internally:
- Makes the HTTP request normally
- If server returns
200— returns the response as-is - If server returns
402— parses the payment requirements, checks budget, signs payment, retries withX-PAYMENTheader - Returns the paid response
One-shot Fetch
For single requests where you don't need to reuse the client:
import { x402Fetch } from '@402md/x402'
const res = await x402Fetch('https://api.example.com/premium', {
method: 'POST',
body: JSON.stringify({ prompt: 'hello' }),
headers: { 'Content-Type': 'application/json' },
paymentConfig: {
evmPrivateKey: process.env.PRIVATE_KEY,
network: 'base',
budget: { maxPerCall: '0.10' }
}
})Pass skipPayment: true to get the raw 402 response without auto-paying:
const res = await x402Fetch('https://api.example.com/premium', {
skipPayment: true
})
// res.status === 402How Payment Validation Works
The 402 Flow
The x402 protocol uses HTTP status 402 Payment Required to create a challenge-response payment flow. Every payment is gasless for the payer — the facilitator pays on-chain gas fees.
Agent Server Facilitator
| | |
| GET /api/data | |
|----------------------------->| |
| | |
| 402 + PaymentRequired JSON | |
|<-----------------------------| |
| | |
| [sign authorization] | |
| | |
| GET /api/data | |
| X-PAYMENT: <base64 token> | |
|----------------------------->| |
| | POST /verify |
| | {paymentPayload, reqs} |
| |----------------------------->|
| | { isValid: true } |
| |<-----------------------------|
| | |
| | POST /settle |
| | {paymentPayload, reqs} |
| |----------------------------->|
| | { success, txHash } |
| |<-----------------------------|
| | |
| 200 + response data | |
|<-----------------------------| |Step 1: Server returns 402
When a request arrives without a valid X-PAYMENT header, the middleware returns:
{
"x402Version": 2,
"accepts": [
{
"scheme": "exact",
"network": "eip155:8453",
"amount": "10000",
"payTo": "0xRecipientAddress",
"maxTimeoutSeconds": 300,
"asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"extra": {
"facilitator": "https://facilitator.x402.org",
"name": "USD Coin",
"version": "2"
}
}
],
"resource": {
"url": "/api/data",
"description": "Premium data access",
"mimeType": "application/json"
}
}Key fields:
amount— USDC in atomic units (6 decimals)."10000"= $0.01network— CAIP-2 chain identifierasset— USDC contract address on that networkextra.facilitator— URL of the service that will verify and settle
Step 2: Client signs authorization
The client signs a gasless authorization (not a transaction). The signing mechanism differs by chain:
- EVM: EIP-712
TransferWithAuthorization(ERC-3009) - Stellar: Soroban
SorobanAuthorizationEntry
The signed proof is base64-encoded and sent as the X-PAYMENT header.
Step 3: Server verifies and settles
The server middleware:
- Decodes the
X-PAYMENTheader (base64 → JSON) - Sends the payment payload to the facilitator for verification (
POST /verify) - If valid, sends it for settlement (
POST /settle) - The facilitator submits the on-chain transaction and pays gas
- Returns
{ settled: true, txHash, payer, network }to your handler
EVM (Base) — EIP-712 Gasless Signatures
On Base networks, the client signs an ERC-3009 TransferWithAuthorization using EIP-712 typed data:
EIP-712 Domain:
name: "USD Coin" (mainnet) / "USDC" (testnet)
version: "2"
chainId: 8453 (mainnet) / 84532 (testnet)
verifyingContract: USDC contract address
Message (TransferWithAuthorization):
from: agent's address
to: payTo (recipient)
value: amount in atomic units
validAfter: 0
validBefore: now + maxTimeoutSeconds
nonce: random 32 bytes (ERC-3009 uses random nonces)This signature authorizes a USDC transfer without submitting a transaction. The facilitator takes this signature and calls transferWithAuthorization() on the USDC contract, paying the gas itself.
Why this is gasless: The agent only signs data (free). The facilitator submits the on-chain transaction and covers gas fees.
Dependencies: Requires viem for private key management and EIP-712 signing.
Stellar — Soroban Auth Entries (Gasless)
On Stellar networks, the client signs a Soroban authorization entry — not a full transaction:
SorobanAuthorizedInvocation:
function: USDC contract "transfer"
args: [from (agent), to (recipient), amount (i128)]
SorobanAuthorizationEntry:
credentials:
address: agent's public key
nonce: random i64 (positive)
signatureExpirationLedger: current ledger + timeout/5
signature: ed25519 signature
rootInvocation: the invocation aboveThe signed auth entry is serialized to XDR (base64) and sent to the facilitator.
What the facilitator does:
- Receives the signed auth entry
- Builds a Stellar transaction with its own source account (pays gas ~$0.00001)
- Attaches the agent's auth entry to the transaction
- Submits to the Soroban network
Why this is gasless: The agent signs only the authorization to move USDC. The facilitator wraps it in a transaction and pays all fees.
Ledger-based expiration: Stellar doesn't use wall-clock time for auth expiration. Instead, it uses ledger numbers. With ~5 seconds per ledger, maxTimeoutSeconds: 300 translates to ~60 ledgers from the current sequence.
Dependencies: Requires @stellar/stellar-sdk for Keypair management, XDR encoding, and authorizeEntry().
Facilitator Verify + Settle
The facilitator is a third-party service that acts as the on-chain settlement layer:
| Network | Facilitator | Operator |
|---------|-------------|----------|
| Base, Base Sepolia | https://facilitator.x402.org | Coinbase |
| Stellar | https://channels.openzeppelin.com/x402 | OpenZeppelin |
| Stellar Testnet | https://channels.openzeppelin.com/x402/testnet | OpenZeppelin |
Verify (POST /verify):
- Validates the cryptographic signature
- Checks the authorization hasn't expired
- Checks the payer has sufficient USDC balance
- Returns
{ isValid: true, payer: '0x...' }or{ isValid: false, invalidReason: '...' }
Settle (POST /settle):
- Submits the on-chain transaction (calling
transferWithAuthorizationon EVM or the Soroban USDC contract on Stellar) - Pays all gas fees
- Returns
{ success: true, transaction: '0x...', network: 'eip155:8453' }
You can override the facilitator URL per route:
paywall({
price: '0.01',
payTo: '0x...',
network: 'base',
facilitatorUrl: 'https://my-custom-facilitator.com'
})Or use the FacilitatorClient directly:
import { FacilitatorClient } from '@402md/x402'
const facilitator = new FacilitatorClient('https://facilitator.x402.org')
const verifyResult = await facilitator.verify(paymentPayload, requirements)
const settleResult = await facilitator.settle(paymentPayload, requirements)Supported Networks
Base (Mainnet)
| Property | Value |
|----------|-------|
| Network key | 'base' |
| CAIP-2 | eip155:8453 |
| Chain ID | 8453 |
| USDC address | 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 |
| Facilitator | https://facilitator.x402.org |
| EIP-712 name | USD Coin |
| SDK | viem |
paywall({ price: '0.01', payTo: '0x...', network: 'base' })Base Sepolia (Testnet)
| Property | Value |
|----------|-------|
| Network key | 'base-sepolia' |
| CAIP-2 | eip155:84532 |
| Chain ID | 84532 |
| USDC address | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
| Facilitator | https://facilitator.x402.org |
| EIP-712 name | USDC |
| SDK | viem |
// Use for development and testing — faucet USDC available
paywall({ price: '0.01', payTo: '0x...', network: 'base-sepolia' })Stellar (Mainnet)
| Property | Value |
|----------|-------|
| Network key | 'stellar' |
| CAIP-2 | stellar:pubnet |
| USDC contract | CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA |
| Facilitator | https://channels.openzeppelin.com/x402 |
| Signing | Soroban auth entry (gasless) |
| SDK | @stellar/stellar-sdk |
paywall({ price: '0.01', payTo: 'GABC...XYZ', network: 'stellar' })Stellar Testnet
| Property | Value |
|----------|-------|
| Network key | 'stellar-testnet' |
| CAIP-2 | stellar:testnet |
| USDC contract | CBIELTK6YBZJU5UP2WWQEUCYKLPU6AUNZ2BQ4WWFEIE3USCIHMXQDAMA |
| Facilitator | https://channels.openzeppelin.com/x402/testnet |
| Signing | Soroban auth entry (gasless) |
| SDK | @stellar/stellar-sdk |
paywall({ price: '0.01', payTo: 'GABC...XYZ', network: 'stellar-testnet' })Mixing networks
Server and client networks are independent. A server on Base can coexist with a server on Stellar:
app.get('/api/base', paywall({ price: '0.01', payTo: '0x...', network: 'base' }), handler)
app.get('/api/stellar', paywall({ price: '0.01', payTo: 'GABC...', network: 'stellar' }), handler)Budget System
AI agents need spending limits. The BudgetTracker enforces three types of caps:
const client = await createPaymentClient({
evmPrivateKey: '0x...',
network: 'base',
budget: {
maxPerCall: '0.10', // rejects any single payment > $0.10
maxPerDay: '5.00', // rejects if cumulative daily spend would exceed $5.00
maxPerSession: '2.00' // rejects if cumulative session spend would exceed $2.00
}
})| Limit | Scope | Resets |
|-------|-------|--------|
| maxPerCall | Single payment | Per request |
| maxPerDay | Calendar day (midnight local time) | Daily at 00:00 |
| maxPerSession | Lifetime of this PaymentClient instance | Never (create a new client) |
All limits are optional. If no budget is configured, there are no spending restrictions.
Budget is checked before signing — if a payment would exceed any limit, an error is thrown and no signature is created. After successful signing, the amount is recorded.
try {
const res = await client.fetch('https://expensive-api.com/data')
} catch (e) {
// "Would exceed daily budget. Spent: $4.50, requested: $1.00, limit: $5.00"
console.error(e.message)
}Dynamic Pricing
paywall() already supports dynamic prices — just compute the amount at request time:
import express from 'express'
import { paywall } from '@402md/x402'
const app = express()
app.use(express.json())
app.post('/v1/checkout', async (req, res, next) => {
const cart = await getCart(req.body.cartId)
const shipping = await calculateShipping(req.body.address)
const total = (cart.subtotal + shipping).toFixed(6)
paywall({
price: total,
payTo: '0x...',
network: 'base',
description: `Order: $${total} USDC`
})(req, res, next)
}, async (req, res) => {
const order = await processOrder(req.body)
res.json({ orderId: order.id, status: 'confirmed' })
})The client doesn't need any changes — client.fetch() reads the actual price from the 402 response and pays whatever the server requires. Budget limits still apply.
Cart / E-commerce Example
A full e-commerce flow with free endpoints (search, cart, shipping) and a dynamic-priced checkout:
Server
import express from 'express'
import { paywall } from '@402md/x402'
const app = express()
app.use(express.json())
// Free — product search
app.get('/v1/products', async (req, res) => {
const results = await searchProducts(req.query.q as string)
res.json(results)
})
// Free — shipping quote
app.post('/v1/shipping/quote', async (req, res) => {
const quote = await calculateShipping(req.body.address, req.body.items)
res.json({ shipping: quote.toFixed(6), estimatedDays: 3 })
})
// Dynamic price — checkout
app.post('/v1/orders', async (req, res, next) => {
const { items, shipping, address } = req.body
const subtotal = items.reduce((sum, i) => sum + i.price * i.qty, 0)
const total = (subtotal + shipping).toFixed(6)
paywall({
price: total,
payTo: '0xYourAddress',
network: 'base',
description: `Order: ${items.length} items, $${total} USDC`,
onPayment: (payment) => {
console.log(`Order paid: ${payment.txHash} from ${payment.payer}`)
}
})(req, res, next)
}, async (req, res) => {
const order = await createOrder(req.body, req.x402.payment)
res.json({ orderId: order.id, status: 'confirmed' })
})Client (AI Agent)
import { createPaymentClient } from '@402md/x402'
const client = await createPaymentClient({
evmPrivateKey: process.env.PRIVATE_KEY,
network: 'base',
budget: { maxPerCall: '200.00', maxPerDay: '500.00' }
})
// 1. Search products (free)
const products = await client.fetch('https://shop.example.com/v1/products?q=keyboard')
const items = [
{ id: 'kb-01', name: 'Mechanical Keyboard', price: 89.99, qty: 1 },
{ id: 'usbc-01', name: 'USB-C Cable', price: 12.99, qty: 1 }
]
// 2. Get shipping quote (free)
const quoteRes = await client.fetch('https://shop.example.com/v1/shipping/quote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ address: '123 Main St', items })
})
const { shipping } = await quoteRes.json() // "7.500000"
// 3. Checkout (auto-pays $110.48 via x402)
const orderRes = await client.fetch('https://shop.example.com/v1/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items, shipping: 7.50, address: '123 Main St' })
})
const order = await orderRes.json() // { orderId: '...', status: 'confirmed' }The agent never calculates the total — the server computes it, returns a 402 with the exact price, and client.fetch() pays it automatically.
Subscription with Wallet Auth
For subscription-based services, combine x402 payment with wallet-signature authentication:
- Subscribe — agent pays via x402, server records the wallet
- Login — agent proves wallet ownership with a signed message
- Access — protected routes check the subscription via
walletAuth()middleware
Server
import express from 'express'
import { paywall, verifyWalletSignature, walletAuth } from '@402md/x402'
const app = express()
app.use(express.json())
// 1. Subscription payment (x402)
app.post('/v1/subscribe',
paywall({
price: '10.00',
payTo: '0xYourAddress',
network: 'base',
description: '30-day subscription'
}),
async (req, res) => {
const { payer } = req.x402.payment
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
await db.subscriptions.upsert({ wallet: payer, expiresAt })
res.json({ subscribedUntil: expiresAt.toISOString(), wallet: payer })
}
)
// 2. Wallet auth login (free)
app.post('/v1/auth', async (req, res) => {
const { message, signature, address } = req.body
const valid = await verifyWalletSignature({ message, signature, address })
if (!valid) return res.status(401).json({ error: 'Invalid signature' })
const sub = await db.subscriptions.findByWallet(address)
if (!sub || sub.expiresAt < new Date())
return res.status(403).json({ error: 'No active subscription' })
const token = signJwt({ wallet: address, exp: sub.expiresAt })
res.json({ token })
})
// 3. Protected route with walletAuth middleware
app.get('/v1/data',
walletAuth({
verifyAccess: async (addr) => {
const sub = await db.subscriptions.findByWallet(addr)
return !!sub && sub.expiresAt > new Date()
}
}),
(req, res) => {
res.json({ data: 'premium content', wallet: req.x402.wallet.address })
}
)Client (AI Agent)
import { createPaymentClient } from '@402md/x402'
const client = await createPaymentClient({
evmPrivateKey: process.env.PRIVATE_KEY,
network: 'base',
budget: { maxPerCall: '10.00' }
})
// Step 1: Subscribe (auto-pays $10.00 via x402)
await client.fetch('https://api.example.com/v1/subscribe', { method: 'POST' })
// Step 2: Login with wallet signature
const message = `Login: ${Date.now()}`
const signature = await client.signMessage(message)
const loginRes = await fetch('https://api.example.com/v1/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, signature, address: client.getAddress() })
})
const { token } = await loginRes.json()
// Step 3: Use protected endpoints with JWT
const data = await fetch('https://api.example.com/v1/data', {
headers: { Authorization: `Bearer ${token}` }
})The walletAuth() middleware can also be used directly (without JWT) by reading the Authorization: WalletAuth <base64> header. The base64 payload is a JSON WalletSignature object:
// Client sends WalletAuth header directly
const sig = await client.signMessage(`Login: ${Date.now()}`)
const payload = btoa(JSON.stringify({
message: `Login: ${Date.now()}`,
signature: sig,
address: client.getAddress()
}))
const res = await fetch('https://api.example.com/v1/data', {
headers: { Authorization: `WalletAuth ${payload}` }
})USDC Amount Handling
USDC has 6 decimals. The x402 protocol uses atomic units (integers) internally, but this package lets you use readable decimal strings everywhere:
| You write | Internal atomic value | USDC amount |
|-----------|----------------------|-------------|
| '1' | 1000000 | $1.00 |
| '0.01' | 10000 | $0.01 |
| '0.001' | 1000 | $0.001 |
| '0.000001' | 1 | $0.000001 |
Conversion uses string math only — no floating-point arithmetic:
import { usdcToAtomic, atomicToUsdc } from '@402md/x402'
usdcToAtomic('0.01') // '10000'
usdcToAtomic('1') // '1000000'
atomicToUsdc('10000') // '0.01'
atomicToUsdc('1000000') // '1'API Reference
Server Exports
paywall(config: PaywallConfig)
Express/Connect middleware. Returns a 402 response with payment requirements if no valid X-PAYMENT header is present. On valid payment, sets req.x402.payment and calls next().
paywallHono(config: PaywallConfig)
Hono middleware. Same behavior as paywall, but uses Hono's context API. Sets x402Payment in context.
getPayment(c: HonoContext): VerifiedPayment | undefined
Retrieves the verified payment from a Hono context after paywallHono runs.
paywallNextjs(config: PaywallConfig, handler: NextjsHandler)
Next.js App Router wrapper. Returns a route handler that validates payment before calling your handler. Sets req.x402.payment on the request object.
createPaymentRequired(config: PaywallConfig, resourceUrl: string): PaymentRequired
Builds a 402 response body manually. Useful if you need custom middleware logic.
verifyPaymentHeader(header: string, network: PaymentNetwork, facilitatorUrl?: string): Promise<VerifiedPayment>
Verifies and settles a payment header against the facilitator. Throws on failure.
decodePaymentHeader(header: string): Record<string, unknown>
Decodes a base64 X-PAYMENT header into a JSON object.
verifyWalletSignature(sig: WalletSignature): Promise<boolean>
Verifies a wallet signature. Detects network from address format (0x → EVM via EIP-191, G → Stellar via ed25519). Returns false on invalid signature (never throws).
walletAuth(config: WalletAuthConfig)
Express middleware for wallet-signature authentication. Reads Authorization: WalletAuth <base64> header, verifies the signature, and checks access via config.verifyAccess(). Sets req.x402.wallet.address on success. Returns 401 on missing/invalid auth, 403 on access denied.
usdcToAtomic(usdc: string): string
Converts a USDC decimal string to atomic units (string math, no floats).
atomicToUsdc(atomic: string): string
Converts atomic USDC units to a decimal string.
Client Exports
createPaymentClient(config: PaymentClientConfig): Promise<PaymentClient>
Creates a payment client with auto-paying fetch, budget tracking, and balance queries.
x402Fetch(url: string, options?: X402FetchOptions): Promise<Response>
One-shot auto-paying fetch. Creates an ephemeral client per request.
BudgetTracker
Class for tracking spending limits. Used internally by createPaymentClient, but can be used standalone:
import { BudgetTracker } from '@402md/x402'
const budget = new BudgetTracker({ maxPerCall: '0.10', maxPerDay: '5.00' })
budget.check('0.05') // ok
budget.record('0.05') // records the spend
budget.check('4.96') // throws: would exceed daily budgetFacilitatorClient
Low-level client for communicating with x402 facilitator services:
import { FacilitatorClient } from '@402md/x402'
// Auto-resolve URL from network name
const client = new FacilitatorClient('base')
// Or use a custom URL
const custom = new FacilitatorClient('https://my-facilitator.com')
const { isValid, payer } = await client.verify(payload, requirements)
const { success, transaction } = await client.settle(payload, requirements)getProvider(network: PaymentNetwork, config): Promise<ChainProvider>
Returns the chain-specific provider for signing payments and checking balances. Automatically selects Base or Stellar based on the network.
Constants
import {
USDC_DECIMALS, // 6
USDC_ADDRESSES, // { base: '0x833...', 'base-sepolia': '0x036...', ... }
CHAIN_IDS, // { base: 8453, 'base-sepolia': 84532, stellar: null, ... }
CAIP2_NETWORKS, // { base: 'eip155:8453', stellar: 'stellar:pubnet', ... }
FACILITATOR_URLS, // { base: 'https://facilitator.x402.org', ... }
X402_SCHEME, // 'exact'
X402_VERSION, // 2
X402_PAYMENT_HEADER, // 'X-PAYMENT'
X402_DEFAULT_TIMEOUT_SECONDS, // 300
isEvmNetwork, // (network) => boolean
isStellarNetwork // (network) => boolean
} from '@402md/x402'Types
import type {
PaymentNetwork, // 'base' | 'base-sepolia' | 'stellar' | 'stellar-testnet'
PaywallConfig, // server middleware config
PaymentClientConfig, // client config
BudgetConfig, // { maxPerCall?, maxPerDay?, maxPerSession? }
PaymentClient, // { pay, signMessage, getBalance, getAddress, fetch }
VerifiedPayment, // { settled, txHash?, payer?, network }
X402FetchOptions, // RequestInit + paymentConfig + skipPayment
WalletAuthConfig, // { verifyAccess, messageFormat? }
WalletSignature, // { message, signature, address }
ChainProvider, // { signPayment, signMessage, getBalance, getAddress }
// Re-exported from @x402/core
PaymentPayload,
PaymentRequired,
PaymentRequirements,
SettleResponse,
VerifyResponse
} from '@402md/x402'Optional Dependencies
| Dependency | Required for | What it does |
|-----------|--------------|--------------|
| viem | base, base-sepolia | EIP-712 signing, balance queries via JSON-RPC |
| @stellar/stellar-sdk | stellar, stellar-testnet | Keypair management, Soroban auth entry signing, XDR encoding |
Both are loaded dynamically (await import(...)) on first use. If you only use Base, you never load the Stellar SDK and vice versa. Bundle size stays minimal.
License
MIT
