@curless/agentbank-mcp-pay
v0.6.5
Published
Paywall an MCP tool / API so AI agents pay per call. withPaywall(): unpaid calls return a payment gate, the agent pays via agentbank (MPP/HTTP-402), the call runs once verified. withMeteredPaywall(): sub-cent usage accrues against a meter that settles one
Readme
@curless/agentbank-mcp-pay
Paywall an MCP tool (or any API) so AI agents pay per call. Wrap a tool
handler with withPaywall(): an unpaid call returns a payment gate, the
agent pays via agentbank's MPP HTTP-402 endpoint, and the call runs once the
payment is verified — single-use. Settlement happens on agentbank's
double-entry ledger + rails (card / stablecoin / Curless); this package only
gates the tool on a verified, not-yet-used payment.
Because MCP (especially stdio) has no HTTP layer to return a 402 on, the gate
is a normal tool result carrying the terms in structuredContent
(the SolvaPay / ATXP pattern).
Seller side
import { withPaywall, createHttpBackend } from '@curless/agentbank-mcp-pay';
const backend = createHttpBackend({
baseUrl: 'https://api.agentbank…',
apiKey: process.env.AGENTBANK_KEY!, // agent:execute; verifies via merchant-scoped /verify
});
server.tool(
'deep_search',
schema,
withPaywall(deepSearch, { backend, merchantId: 'curless_mch_…', price: 200, currency: 'USD', sku: 'deep_search' }),
);
// unpaid call → { structuredContent: { payment_required: { amount: 200, … } } }
// paid retry → runs deepSearch, returns its result + a receiptprice is in minor units (cents / token base units). The default one-time-use
store is in-process — back it with Redis/DB in production via the consumed
option.
Sub-cent pricing — withMeteredPaywall()
A per-call payment can't be a fraction of a minor unit, so withPaywall can't
price below one cent. For micro-priced tools (e.g. $0.0002 / call), meter
usage instead: each call accrues against a meter (priced in a high-precision
asset like USDC, 6 dp) and one aggregate PaymentIntent settles when the
balance crosses a threshold. Post-paid: the call runs on the authenticated
agentbank key's credit, so use it where the caller is trusted (your own metered
API) — the aggregate settles through the agent's rail.
import { withMeteredPaywall, createHttpBackend } from '@curless/agentbank-mcp-pay';
const backend = createHttpBackend({ baseUrl: 'https://api.agentbank…', apiKey: process.env.AGENTBANK_KEY! });
// Open a meter once per agent/customer (POST /v1/meters):
const { meterId } = await backend.openMeter({
merchantId: 'curless_mch_…',
currency: 'USDC', // settle the aggregate in a 6-dp asset
thresholdMinor: 1_000_000, // flush at $1.00 (1e6 USDC base units)
});
server.tool('embed', schema, withMeteredPaywall(embed, {
backend,
meterId,
unitPriceMinor: 200, // $0.0002 / call, exact — never rounded
idempotencyKey: (args) => String(args.requestId), // retried call won't double-meter
}));
// each call → runs + { structuredContent: { metered: { accruedMinor, settledPaymentIntentId } } }Only successful (non-error) calls meter; a metering transport error is reported
in the metered block, not masked over the handler's result. Flush an open tab
early with backend.flushMeter(meterId).
Post-paid vs prepaid. The above is post-paid (credit) — fine for your own /
trusted metered APIs. For untrusted agents, open a prepaid meter (backed by
an @agentbank/pay budget hold) and pass prepaid: true: usage is reserved
before the call runs and rejected past the funded budget, so the agent can
never consume beyond what it funded. An exhausted budget returns a top-up gate
(structuredContent.payment_required.status === 'insufficient_budget') and the
handler doesn't run; a metering error fails closed (also gates). Settlement is a
single capture at close (agent budget → merchant).
const { meterId } = await backend.openMeter({ merchantId, currency: 'USDC', thresholdMinor: 0,
// open prepaid via the API: POST /v1/meters { prepaid: { agentId, budgetMinor } }
});
server.tool('embed', schema, withMeteredPaywall(embed, { backend, meterId, unitPriceMinor: 200, prepaid: true }));Agent side
import { payMcpGate } from '@curless/agentbank-mcp-pay/agent';
const r1 = await callTool('deep_search', { q: '…' });
const gate = r1.structuredContent.payment_required;
const { paymentIntentId } = await payMcpGate(gate, {
baseUrl: 'https://api.agentbank…',
token: agentAccessToken, // agent:execute
source: 'did:agent:buyer-1',
});
const r2 = await callTool('deep_search', { q: '…', _agentbankPayment: { paymentIntentId } });MIT
