npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@402md/x402

v0.1.1

Published

x402 protocol implementation — one-liner paywalls, auto-paying fetch, Base + Stellar support

Downloads

203

Readme

@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