Skip to content

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.

Terminal window
npm install @curless/agentbank-mcp-pay
import { 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.

The wrapper runs a three-step handshake, and the agent drives it:

  1. First call (no proof) → the wrapper returns a payment_required gate instead of running your tool. The terms ride in structuredContent (MCP, especially stdio, has no HTTP layer to return a 402 on): merchant, amount, currency, sku, and the payEndpoint to pay.
  2. The agent pays that endpoint and gets a paymentIntentId.
  3. Retry with the proof — the agent calls the tool again with _agentbankPayment: { paymentIntentId }. The wrapper verifies the payment (merchant + amount + currency + the per-tool sku), consumes it single-use, then runs your real handler and appends a payment: { …, 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.

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.

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.

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.

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.

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.