bolthub logobolthub
SDKs & Tools

@bolthub/pay (Tool-payment SDK)

Charge agents for an MCP tool or HTTP endpoint in a few lines, and pay for tools with a budgeted client. Rail-agnostic; L402 (Lightning) live today, x402 (stablecoins) behind the same interface.

@bolthub/pay is the tool-payment SDK: the seller side prices a tool, the buyer side pays for it. It is open source (MIT), free to self-host, and needs no bolthub account. The agent-to-tool protocol standardises what a tool does but has no slot for what it costs; this SDK fills that slot. A paid tool answers an unpaid call with a payment_required challenge and runs only once a valid proof comes back, over whatever rail you accept.

bun add @bolthub/pay
# or: npm install @bolthub/pay

The package follows SemVer from 0.1.0. The wire format it speaks (the Tool Payment Profile, TPP 0.1) is a draft and may evolve before 1.0.

Seller: charge for an MCP tool

A rail needs a signing secret (32+ characters) and something that makes invoices: your own wallet (NWC, LND, phoenixd, LNbits) or the hosted facilitator.

import { createPaywall, l402Rail } from "@bolthub/pay";

const pay = createPaywall({
  rails: [
    l402Rail({
      secret: process.env.PAY_SECRET!,
      invoiceProvider: {
        async createInvoice(amountSat, memo) {
          const { invoice, paymentHash } = await myWallet.makeInvoice(amountSat, memo);
          return { invoice, paymentHash };
        },
      },
    }),
  ],
});

// Register the tool. `resource` defaults to the tool name; a proof is
// accepted only for the resource it was minted against.
pay.tool(
  server,
  "get_satellite_image",
  "Recent high-res satellite imagery for a lat/lon and date.",
  schema,
  { price: { amount: 2000 } }, // 2000 sats per call
  async (args) => ({ content: [{ type: "text", text: await fetchImage(args) }] }),
);

Prefer to call server.tool yourself? Wrap just the handler:

server.tool(
  "get_satellite_image",
  schema,
  pay(
    { price: { amount: 2000 }, resource: "get_satellite_image" },
    async (args) => ({ content: [{ type: "text", text: await fetchImage(args) }] }),
  ),
);

What the buyer sees

  1. An unpaid call returns an error result whose _meta["ai.bolthub/payment"] holds the challenge: the price, the resource, and one offer per rail (an L402 offer carries a Lightning invoice and a token).
  2. The buyer pays an offer, then re-calls the tool with the proof in the request _meta: { "ai.bolthub/payment": { "scheme": "l402", "proof": "<token>:<preimageHex>" } }.
  3. The proof verifies and the handler runs.

A payment-blind client just sees a normal tool error ("Payment required: 2000 sat …") and moves on; nothing breaks.

Optional, for cost-aware agents that budget before calling:

const ad = pay.advertise({ amount: 2000 }); // → { version, price, model, rails }
// attach to the tool's _meta["ai.bolthub/payment"]

Buyer: pay for tools automatically

PayingClient calls a tool; when it gets a payment_required challenge it pays an offer it has a payer for and retries, all inside a per-asset budget. The budget is a hard cap: every payment is counted against maxTotal before it happens, and offers that would cross it are refused.

import { PayingClient, l402Payer, x402Payer } from "@bolthub/pay";

const client = new PayingClient({
  payers: [
    l402Payer({ wallet: myLightningWallet }), // pays L402 invoices
    x402Payer({ signer: myUsdcSigner }),      // signs x402 transfers
  ],
  maxTotal: { sat: 10_000, usdc: 5_000 },     // per-asset budget
  onPaid: (i) => console.log(`paid ${i.amount} ${i.asset} via ${i.scheme}`),
});

// callTool handles challenge → pay → retry transparently:
const result = await client.callTool(mcpClient, "get_satellite_image", { lat, lon });

Payers are tried in order, so the list is your rail preference. l402Payer's wallet is structurally @bolthub/agent's WalletAdapter, so existing wallets (NWC, LND, phoenixd) drop straight in. Delegating to a sub-agent? Give it its own PayingClient with a smaller cap.

Rails

RailStatusWhat it does
l402Rail / l402PayerLiveLightning. HMAC-signed, resource-scoped, time-limited tokens; constant-time verification.
x402Rail / x402PayerSDK-only todayStablecoins. Advertises x402 payment requirements; x402Facilitator speaks the standard facilitator API (x402.org, Coinbase CDP, self-hosted) and eip3009Signer signs through any viem-shaped account. Not yet live on bolthub's hosted path.
facilitatorRail + httpFacilitatorLive (hosted)Delegates mint/verify to a bolthub facilitator: at-most-once proof redemption, usage metering, analytics. See the hosted facilitator.

Price a tool in more than one asset and the challenge carries one offer per rail; the buyer pays in whichever they hold. Adding a rail is implementing one interface (assets, createOffer, verify); the paywall core never sees rail-specific bytes.

Security model

  • Tokens are HMAC-signed, scoped to a resource, and time-limited (default 15 minutes). A proof minted for one tool can never unlock another.
  • Signature and preimage checks are constant-time.
  • The wrapper fails closed: no resource, or any unverifiable proof, means no service.
  • Self-hosted l402Rail has no built-in replay dedup: a paid proof stays valid for the token TTL, so a buyer could re-call within it. Use the facilitator rail when you need strict at-most-once, per-call billing.

Which package do I need?

  • Charging for your own MCP tools or endpoints@bolthub/pay (this page).
  • Calling paid MCP tools from an agent@bolthub/pay (PayingClient).
  • Calling L402-gated HTTP APIs (the hosted gateway, the API Hub) → @bolthub/agent (TypeScript) or bolthub (Python).
  • Wiring every Hub API into an MCP client at once@bolthub/mcp-registry.