Paywall (mcp-pay)
@curless/agentbank-mcp-pay turns any API or MCP tool into pay-to-call. The tool is
unchanged — an unpaid call returns a “pay first” prompt; once the agent pays, your tool
runs and the money lands in your account. Each call is a single-use, per-call charge —
one payment unlocks exactly one call.
npm install @curless/agentbank-mcp-payimport { withPaywall, createHttpBackend, httpConsumedStore } from '@curless/agentbank-mcp-pay';
const baseUrl = 'https://mcp.curless.ai';const apiKey = process.env.AGENTBANK_MERCHANT_TOKEN!;const merchantId = 'your-merchant-id';
const paidTool = withPaywall(yourExistingToolHandler, { backend: createHttpBackend({ baseUrl, apiKey }), merchantId, price: 50, // minor units (e.g. cents) currency: 'USD', sku: 'premium-lookup', consumed: httpConsumedStore({ baseUrl, apiKey, merchantId }),});withPaywall(handler, opts) returns a handler with the same signature as the one you
passed in, so it drops straight into McpServer.tool(...) — no cast, no shape change.
How a paid call works
Section titled “How a paid call works”The wrapper runs a three-step handshake, and the agent drives it:
- First call (no proof) → the wrapper returns a
payment_requiredgate instead of running your tool. The terms ride instructuredContent(MCP, especially stdio, has no HTTP layer to return a 402 on): merchant, amount, currency,sku, and thepayEndpointto pay. - The agent pays that endpoint and gets a
paymentIntentId. - Retry with the proof — the agent calls the tool again with
_agentbankPayment: { paymentIntentId }. The wrapper verifies the payment (merchant + amount + currency + the per-toolsku), consumes it single-use, then runs your real handler and appends apayment: { …, status: 'paid' }block to the result.
Verification is server-authoritative — agentbank’s ledger and rail did the settlement; the
wrapper only checks the PaymentIntent actually paid this call. If verification fails, the
agent gets the gate back with a machine-routable reasonCode (not_captured → wait and
retry; insufficient_amount / currency_mismatch / reference_mismatch → the payment
doesn’t fit, stop; not_found → re-pay) so it can route without parsing prose.
Single-use is enforced — pass a shared store in production
Section titled “Single-use is enforced — pass a shared store in production”A payment unlocks one call. The consumed store is what guarantees that: consume(pi)
is atomic and returns true only for the first redemption — a replay (e.g. a lost response
retried) gets an already_redeemed result, never a second charge or a second run.
The default store is an in-process Set — single-instance only. Across replicas the
same payment could be redeemed once per instance, so withPaywall throws at startup
when NODE_ENV=production and no consumed store is passed rather than silently handing
you that footgun. Back it with a shared store:
httpConsumedStore({ baseUrl, apiKey, merchantId })— server-authoritative, the default choice (shown above).- Redis
SET NX, or a unique DB insert on the PaymentIntent id — any atomic check-and-set works.
Bind each tool with sku
Section titled “Bind each tool with sku”Set sku (strongly recommended): it binds the payment to this specific tool, so a
same-price payment made for a different tool can’t unlock this one (reference_mismatch).
Without it, any payment of the right amount to your merchant opens the gate.
withPaywall options
Section titled “withPaywall options”| Option | Meaning |
|---|---|
backend |
Verifies the presented payment — createHttpBackend({ baseUrl, apiKey }) over agentbank’s /verify endpoint. |
merchantId |
Your merchant id — the charge lands here. |
price / currency |
The price, in minor units, and ISO currency. Authoritative. |
sku |
Per-tool reference bound into the payment (per-call binding). Recommended. |
consumed |
Single-use store. Required in production (see above). |
productRef |
Optional Curless product ref (prd_…), surfaced in the gate for display/traceability. |
Sub-cent pricing: metered mode
Section titled “Sub-cent pricing: metered mode”A per-call payment can’t charge a fraction of a minor unit, so for sub-cent pricing use
withMeteredPaywall instead. It accrues usage against a meter you pre-open
(POST /v1/meters) and settles one aggregate PaymentIntent when the meter crosses its
threshold.
import { withMeteredPaywall } from '@curless/agentbank-mcp-pay';
const meteredTool = withMeteredPaywall(yourToolHandler, { backend: meteringBackend, meterId, // pre-opened for this agent/customer unitPriceMinor: 200, // e.g. 200 USDC base units = $0.0002 / unit quantity: (args) => args.tokens ?? 1, idempotencyKey: (args) => args.requestId, // never double-meter a retry});Two trust models:
- Post-paid (default) — the call runs, then usage meters against the agent’s credit; the aggregate settles through the agent’s rail at the threshold. Use it where the caller is trusted (your own metered API). Only successful calls meter.
- Prepaid (
prepaid: true) — usage is reserved before the handler runs against a funded budget hold, and the call is gated (top-up prompt) if the budget can’t cover it. Safe for untrusted agents: the call only runs within funded budget, and a failed call is refunded rather than billed.
HTTP, not just MCP
Section titled “HTTP, not just MCP”The paywall isn’t MCP-only. createHttpBackend verifies over plain HTTP, so the same
per-call gate works behind any framework — the agent pays the payEndpoint the gate
returns and presents the paymentIntentId on retry. For a scaffolded HTTP integration
(Next.js / Express) use @curless/cli init, which writes the verify
handshake in for you. @modelcontextprotocol/sdk is a type-only optional peer —
HTTP-only users don’t need it installed.
No code at all?
Section titled “No code at all?”npx @curless/cli init drops a paywall into an existing project (paywall is its default
role), and npm create @curless/agentbank -- --template paywall scaffolds a new one.

