@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/payThe package follows SemVer from
0.1.0. The wire format it speaks (the Tool Payment Profile, TPP0.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
- 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). - 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>" } }. - 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.
Advertise the price
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
| Rail | Status | What it does |
|---|---|---|
l402Rail / l402Payer | Live | Lightning. HMAC-signed, resource-scoped, time-limited tokens; constant-time verification. |
x402Rail / x402Payer | SDK-only today | Stablecoins. 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 + httpFacilitator | Live (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
l402Railhas 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) orbolthub(Python). - Wiring every Hub API into an MCP client at once →
@bolthub/mcp-registry.