Skip to content

402md/x402

Repository files navigation

@402md/x402

npm version License: MIT x402 Base Stellar TypeScript

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

npm install @402md/x402

Chain 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-sdk

If 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 viem

Quick 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 app

getPayment(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:

  1. Makes the HTTP request normally
  2. If server returns 200 — returns the response as-is
  3. If server returns 402 — parses the payment requirements, checks budget, signs payment, retries with X-PAYMENT header
  4. 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 === 402

How 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.01
  • networkCAIP-2 chain identifier
  • asset — USDC contract address on that network
  • extra.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:

  1. Decodes the X-PAYMENT header (base64 → JSON)
  2. Sends the payment payload to the facilitator for verification (POST /verify)
  3. If valid, sends it for settlement (POST /settle)
  4. The facilitator submits the on-chain transaction and pays gas
  5. 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 above

The signed auth entry is serialized to XDR (base64) and sent to the facilitator.

What the facilitator does:

  1. Receives the signed auth entry
  2. Builds a Stellar transaction with its own source account (pays gas ~$0.00001)
  3. Attaches the agent's auth entry to the transaction
  4. 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 transferWithAuthorization on 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:

  1. Subscribe — agent pays via x402, server records the wallet
  2. Login — agent proves wallet ownership with a signed message
  3. 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 budget

FacilitatorClient

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

About

x402 payment protocol for AI agents — one-liner paywalls, auto-paying fetch, Base + Stellar

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors