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

pay402

v0.2.0

Published

Universal HTTP 402 payment SDK for AI agents, MCP servers, and automated pipelines

Readme

pay402

A universal payment layer for machine-to-machine commerce.

HTTP 402 ("Payment Required") has been a reserved status code since 1997 — a placeholder for a future where software could pay for services the same way it follows redirects. That future is here. Multiple payment protocols now use 402, but they're incompatible with each other: Lightning uses macaroons and invoices, x402 uses EIP-712 signatures on Base, Solana has its own flow, and Arkade uses Bitcoin VTXOs. A client built for one can't talk to the others.

pay402 is the universal adapter. One SDK that speaks every 402 dialect, so your code — whether it's an AI agent, an MCP server, a backend service, or a script — can pay any 402-gated endpoint regardless of which payment rail it uses. Configure your wallets once, set spend limits, and client.fetch() handles everything: challenge parsing, rail selection, payment execution, proof caching, and automatic retry.

But pay402 goes further than just client-side payments:

  • Bridge layer — Don't have the right wallet? pay402 routes payments across rails. An Arkade wallet can pay a Lightning invoice via Boltz atomic swap, or a USDC wallet can pay one via LendaSat. The server never knows the difference.
  • Server middleware — Gate your own Express routes or MCP tools behind payment with a few lines of config. Multi-rail, multi-price, plug in your own verification.
  • Agent skill — Register pay402 as MCP tools so AI agents can discover, estimate, and execute paid API calls autonomously — with spending controls that keep them on a leash.

The net effect: any software with a wallet can pay any service with a price. The payment rail becomes an implementation detail, not a compatibility barrier.

Supported Rails

| Rail | Protocol | Currency | Network | |------|----------|----------|---------| | L402 | Lightning Labs | Bitcoin (sats) | Lightning Network | | x402 | Coinbase | USDC | Base, Solana |

Arkade and EVM (USDC) wallets are also supported as funding sources via the bridge layer — they can pay L402 invoices through atomic swaps (Boltz for Arkade, LendaSat for USDC), but neither is a server-facing payment rail for L402.

Install

npm install pay402

Optional peer dependencies for bridge support:

npm install @arkade-os/sdk            # Arkade wallet
npm install @arkade-os/boltz-swap     # Arkade→Lightning bridge
npm install @lendasat/lendaswap-sdk-pure  # USDC→Lightning bridge

Quick Start

The fastest way to get running — configure wallets via environment variables:

# Set at least one wallet
export EVM_PRIVATE_KEY=0x...           # for x402 (Base/USDC)
# or
export LND_HOST=https://localhost:8080  # for L402 (Lightning)
export LND_MACAROON=hex-encoded-macaroon
# or
export ARKADE_MNEMONIC="your twelve word mnemonic phrase here"
export ARKADE_SERVER_URL=https://arkade.computer
export ARKADE_NETWORK=mainnet

# Set spend limits
export PAY402_MAX_DAILY=10.00
export PAY402_MAX_PER_REQUEST=1.00
import { fromEnv } from "pay402";

const client = fromEnv();

const res = await client.fetch("https://api.example.com/premium-data");
const data = await res.json();

That's it. The client auto-detects which wallets are configured, handles 402 responses, pays, retries, and caches tokens. See .env.example for all available options.

Try it locally

git clone https://github.com/RDMoutlaw/pay402.git && cd pay402
npm install
npx tsx examples/mock-server.ts   # Terminal 1: starts a 402-gated server
npx tsx examples/client-test.ts   # Terminal 2: pays and gets access

Manual Configuration

For full control, configure the client directly:

import { pay402Fetch } from "pay402";

const fetch402 = pay402Fetch({
  wallets: [
    {
      type: "lightning",
      lndHost: process.env.LND_HOST!,
      lndMacaroon: process.env.LND_MACAROON!,
    },
    {
      type: "evm",
      privateKey: process.env.EVM_PRIVATE_KEY! as `0x${string}`,
      chain: "base",
    },
    {
      type: "solana",
      secretKey: process.env.SOLANA_SECRET_KEY!,
      cluster: "mainnet-beta",
    },
    {
      type: "arkade",
      mnemonic: process.env.ARKADE_MNEMONIC!,
      arkServerUrl: process.env.ARKADE_SERVER_URL!,
      network: "mainnet",
    },
  ],
  autoFetchBtcPrice: true,
  logLevel: "info",
  spendControls: {
    global: { maxDaily: 10.0, maxPerRequest: 2.0 },
    denylist: ["https://*.untrusted.com/**"],
  },
});

const res = await fetch402("https://api.example.com/premium-data");
const data = await res.json();

Class API

import { Pay402Client } from "pay402";

const client = new Pay402Client({
  wallets: [{ type: "lightning", lndHost: "...", lndMacaroon: "..." }],
  btcPriceUsd: 60000,
  maxSinglePaymentUsd: 5,
  onPayment: (record) => {
    console.log(`Paid $${record.amountUsd} via ${record.rail} to ${record.endpoint}`);
  },
});

const res = await client.fetch("https://api.example.com/data");

// Call destroy() when done to stop background tasks (BTC price refresh)
client.destroy();

Axios Interceptor

import axios from "axios";
import { Pay402Client } from "pay402";

const client = new Pay402Client({ wallets: [...] });

axios.interceptors.request.use((config) => client.intercept(config));

Cross-Rail Bridging

When a server requires a rail your wallet doesn't natively support, pay402 can bridge the payment. For example, an Arkade wallet can pay a Lightning (L402) invoice via a Boltz submarine swap, or a USDC (EVM) wallet can pay one via a LendaSat atomic swap.

Bridging is opt-in and disabled by default:

// Arkade → Lightning
const client = new Pay402Client({
  wallets: [
    {
      type: "arkade",
      mnemonic: process.env.ARKADE_MNEMONIC!,
      arkServerUrl: "https://arkade.computer",
      network: "mainnet",
    },
  ],
  bridging: {
    enabled: true,
    maxBridgeFeeUsd: 0.50,              // max bridge fee you're willing to pay
    allowedPaths: ["arkade->l402"],      // only allow specific bridge paths
  },
  autoFetchBtcPrice: true,
});

// This works even though the server only accepts L402 (Lightning):
const res = await client.fetch("https://lightning-only-api.com/data");
// USDC → Lightning
const client = new Pay402Client({
  wallets: [
    {
      type: "evm",
      privateKey: process.env.EVM_PRIVATE_KEY! as `0x${string}`,
      chain: "base",
    },
  ],
  bridging: {
    enabled: true,
    maxBridgeFeeUsd: 0.50,
    allowedPaths: ["x402-base->l402"],
    lendasat: {
      chainId: 137,                      // Polygon (default)
      tokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // Polygon USDC (default)
    },
  },
  autoFetchBtcPrice: true,
});

// EVM wallet pays a Lightning invoice via LendaSat gasless swap:
const res = await client.fetch("https://lightning-only-api.com/data");

The server sees a valid L402 preimage — it doesn't know or care how the payment was funded. Bridge fees are included in spend control checks.

Currently supported bridge paths:

| Source | Target | Provider | Peer Dependency | |--------|--------|----------|-----------------| | arkade | l402 | Boltz submarine swap | @arkade-os/boltz-swap | | x402-base | l402 | LendaSat gasless atomic swap | @lendasat/lendaswap-sdk-pure |

Agent Skill (MCP Client Tools)

Register pay402 as a set of tools on an MCP server so AI agents can discover and use paid APIs:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerPay402Tools } from "pay402/mcp-tool";

const server = new McpServer({ name: "my-agent", version: "1.0" });
registerPay402Tools(server);

This registers four tools:

| Tool | Description | Key Input | |------|-------------|-----------| | pay402_fetch | Fetch a URL with automatic 402 payment | url, method?, headers?, body? | | pay402_estimate | Dry-run cost estimation | url, method? | | pay402_spending | View spending summary by period | period? (hour, day, all) | | pay402_balance | Check wallet balances | — |

You can pass a pre-built client or let it auto-configure from environment variables:

// With a pre-built client
const client = new Pay402Client({ wallets: [...] });
registerPay402Tools(server, { client });

// Or auto-configure from env
registerPay402Tools(server);

The package includes a SKILL.md file for agent skill discovery.

Programmatic Spending & Balance API

The client also exposes these methods directly:

// Spending summary
const summary = client.getSpendingSummary("hour");
// { totalUsd: 3.50, count: 5, byRail: { l402: { totalUsd: 2.0, count: 3 }, ... } }

// Wallet balances (currently supported for Arkade wallets)
const balances = await client.getBalances();
// [{ type: "arkade", balanceSats: 50000 }, { type: "lightning", error: "balance check not supported" }]

Server Middleware — Express

import express from "express";
import { pay402Middleware } from "pay402";

const app = express();

app.use(
  pay402Middleware({
    pricing: {
      "/api/premium/*": { l402: 1000, x402: 500000 }, // 1000 sats or 0.50 USDC
      "/api/data": { x402: 100000 },                                 // 0.10 USDC only
    },
    acceptedRails: ["l402", "x402"],
    verifyL402: (macaroon, preimage) => { /* your verification logic */ },
    verifyX402: (payload) => { /* your verification logic */ },
    x402PayTo: "0xYourAddress",
    x402Asset: "0xUSDCContractAddress",
    x402Network: "base",
    onPaymentReceived: ({ rail, route, amount }) => {
      console.log(`Received payment on ${rail} for ${route}`);
    },
  })
);

app.get("/api/premium/report", (req, res) => {
  res.json({ data: "premium content" });
});

MCP Payment Wrapper

Gate MCP tools behind payment without modifying tool implementations.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { mcpPaymentWrapper } from "pay402";

const server = new McpServer({ name: "my-server", version: "1.0.0" });

// Wrap BEFORE registering tools
mcpPaymentWrapper({
  server,
  pricing: {
    "premium-analysis": { l402: 500, x402: 250000 },
    "generate-report": { x402: 1000000 },
  },
  acceptedRails: ["l402", "x402"],
  verifyL402: (macaroon, preimage) => true,
  verifyX402: (payload) => true,
  x402PayTo: "0xYourAddress",
  x402Asset: "0xUSDC",
});

// Register tools as normal — payment is handled by the wrapper
server.registerTool("premium-analysis", { description: "..." }, async (args) => {
  return { content: [{ type: "text", text: "analysis result" }] };
});

When a tool is called without payment, the wrapper returns a structured error:

{
  "error": "payment_required",
  "version": "pay402/1.0",
  "challenges": [
    { "rail": "l402", "amountSats": 500 },
    { "rail": "x402", "network": "base", "amountSmallestUnit": 250000, "payTo": "0x...", "asset": "0x...", "maxTimeoutSeconds": 60 },
  ]
}

The calling client retries with proof in the _payment_proof parameter.

Configuration Reference

Wallet Types

| Type | Required Fields | Optional | |------|----------------|----------| | lightning | lndHost, lndMacaroon | tlsCert | | evm | privateKey (0x-prefixed), chain | rpcUrl, facilitatorUrl | | solana | secretKey, cluster | facilitatorUrl | | arkade | mnemonic, arkServerUrl, network | — |

Environment Variables

| Variable | Wallet | Description | |----------|--------|-------------| | LND_HOST | Lightning | LND REST API host | | LND_MACAROON | Lightning | Hex-encoded admin macaroon | | LND_TLS_CERT | Lightning | Base64 TLS cert for self-signed nodes | | EVM_PRIVATE_KEY | EVM | 0x-prefixed hex private key | | EVM_CHAIN | EVM | base or base-sepolia (default: base) | | EVM_FACILITATOR_URL | EVM | x402 facilitator URL | | SOLANA_SECRET_KEY | Solana | Base58-encoded keypair | | SOLANA_CLUSTER | Solana | mainnet-beta or devnet (default: mainnet-beta) | | SOLANA_FACILITATOR_URL | Solana | x402 facilitator URL | | ARKADE_MNEMONIC | Arkade | BIP-39 mnemonic phrase | | ARKADE_SERVER_URL | Arkade | Arkade server URL | | ARKADE_NETWORK | Arkade | mainnet or testnet (default: mainnet) | | PAY402_MAX_PER_REQUEST | — | Max USD per request | | PAY402_MAX_HOURLY | — | Max USD per rolling hour | | PAY402_MAX_DAILY | — | Max USD per rolling day | | PAY402_BTC_PRICE_USD | — | Static BTC price in USD | | PAY402_AUTO_BTC_PRICE | — | true to auto-fetch from CoinGecko | | PAY402_LOG_LEVEL | — | Logging level |

Spend Controls

{
  perEndpoint: {
    "https://api.example.com/*": { maxPerRequest: 0.50, maxDaily: 5.00 }
  },
  global: {
    maxPerRequest: 2.00,  // USD
    maxHourly: 10.00,     // rolling window
    maxDaily: 50.00,      // rolling window
  },
  railPreference: ["x402-base", "l402"],  // or "cheapest"
  allowlist: ["https://trusted.com/**"],
  denylist: ["https://*.evil.com/**"],
  dryRun: false,
}

For a complete guide on configuring spend policies for autonomous agents — budget tiers, approval workflows, multi-agent setups, and emergency controls — see Agent Spend Policy Guide.

Bridging Config

{
  bridging: {
    enabled: true,                       // default: false
    maxBridgeFeeUsd: 1.00,              // default: $1
    allowedPaths: ["arkade->l402", "x402-base->l402"], // restrict which bridge paths are allowed
    lendasat: {                          // optional LendaSat config
      chainId: 137,                      // default: 137 (Polygon)
      tokenAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", // default: Polygon USDC
    },
  },
}

Dry Run Mode

const client = new Pay402Client({
  wallets: [...],
  spendControls: { dryRun: true },
});

const res = await client.fetch("https://api.example.com/data");
const estimate = await res.json();
// { rail: "x402-base", estimatedCostUsd: 1.0, wouldExceedLimits: false }

Error Handling

All errors extend Pay402Error:

| Error | When | |-------|------| | NoCompatibleRailError | No adapter matches any advertised rail, or no wallet configured | | SpendLimitExceededError | Any spend control check fails | | PaymentFailedError | Adapter pay() threw — no auto-retry (money at stake) | | PaymentInFlightError | LND returned IN_FLIGHT — outcome unknown | | PaymentVerificationError | Server still returned 402 after payment | | InvoiceExpiredError | BOLT11 invoice expired before payment attempt | | BridgePaymentFailedError | Bridge swap failed (includes bridgePath field) |

import { SpendLimitExceededError, PaymentFailedError, BridgePaymentFailedError } from "pay402";

try {
  await client.fetch("https://api.example.com/data");
} catch (err) {
  if (err instanceof SpendLimitExceededError) {
    console.log(`Limit hit: ${err.limitType}, tried $${err.attemptedAmountUsd}`);
  }
  if (err instanceof PaymentFailedError) {
    console.log(`Payment failed on ${err.rail}: ${err.underlyingError.message}`);
  }
  if (err instanceof BridgePaymentFailedError) {
    console.log(`Bridge failed on ${err.bridgePath}: ${err.underlyingError.message}`);
  }
}

Logging

Structured JSON logging via pino. Set via config or env var:

const client = new Pay402Client({ wallets: [...], logLevel: "debug" });

Or: PAY402_LOG_LEVEL=debug (silent | fatal | error | warn | info | debug | trace)

Logs payment events, cache hits, rail selection, and challenge parsing at appropriate levels.

Live BTC Price

// Auto-fetch and refresh every 5 minutes (CoinGecko)
const client = new Pay402Client({
  wallets: [...],
  autoFetchBtcPrice: true,
  btcPriceUsd: 60000, // fallback until first fetch completes
});

// Or use the provider directly
import { createBtcPriceProvider } from "pay402";

const provider = createBtcPriceProvider({ initialPrice: 60000 });
console.log(provider.getPrice()); // latest price
provider.stop(); // cleanup

How It Works

  1. Client makes a request to a 402-gated endpoint
  2. Server returns 402 with WWW-Authenticate (L402) and/or X-Payment-Required (x402) headers
  3. SDK parses challenges from all headers
  4. Checks token cache — if valid cached proof exists, skips payment
  5. Runs spend control checks — blocks if any limit would be exceeded
  6. Selects optimal rail based on wallet availability and preference config
  7. If no direct match and bridging is enabled, attempts cross-rail bridge (e.g. Arkade -> L402)
  8. Executes payment via the matching rail adapter (or bridge provider)
  9. Retries the original request with proof-of-payment headers
  10. Caches the token for future requests
  11. Returns the response — caller never sees the 402

Security

This SDK handles private keys and real money. Keep these in mind:

  • Never hardcode private keys or mnemonics. Load them from environment variables or a secret manager.
  • The x402 facilitator is a trusted third party. When using x402, your signed EIP-3009 authorization is submitted to a facilitator (default: x402.org). The facilitator executes the on-chain transfer. A malicious facilitator could withhold or front-run transactions. Use facilitatorUrl to point to a facilitator you trust.
  • Bridge swaps involve a third party. The Arkade→L402 bridge uses Boltz for submarine swaps; the USDC→L402 bridge uses LendaSat for atomic swaps. Bridge fees are included in spend control checks, but the swaps themselves are trust relationships with the respective services.
  • Spend controls are your safety net. Always configure maxSinglePaymentUsd and global.maxDaily for autonomous agents. The default hard ceiling is $10 per payment.
  • No auto-retry after payment failure. If a payment fails, the SDK throws immediately. It does not try another rail — money may have already left your wallet.
  • No auto-fallback between rails. Rail selection happens before payment. Once a rail is chosen and payment begins, there's no switching.

To report a security vulnerability, please open a private issue or email the maintainers directly.

Contributing

git clone https://github.com/RDMoutlaw/pay402.git
cd pay402
npm install
npm test          # run tests
npm run typecheck # type-check without emitting

PRs welcome. Please include tests for new functionality.

License

MIT