Skip to content

Server payments

Server payments are implemented as middleware that sits between:

  • the inbound client request, and
  • the underlying MCP server handler.

For priced requests, the middleware ensures no unpaid forwarding.

You price individual capabilities by method + name.

import type { PricedCapability } from '@contextvm/sdk/payments';
const pricedCapabilities: PricedCapability[] = [
{ method: 'tools/call', name: 'add', amount: 10, currencyUnit: 'sats' },
{
method: 'resources/read',
name: 'private://*',
amount: 5,
currencyUnit: 'sats',
},
];

Notes:

  • Only requests that match a priced capability are gated.
  • Matching is designed for predictable policy. Prefer explicit entries over overly-broad wildcards.

A PaymentProcessor is responsible for:

  • creating a pay_req for a given amount
  • verifying that a previously issued pay_req has been paid

You can configure multiple processors (multiple PMIs). The server selects a processor based on client/server PMI compatibility.

import {
LnBolt11NwcPaymentProcessor,
withServerPayments,
} from '@contextvm/sdk/payments';
const processor = new LnBolt11NwcPaymentProcessor({
nwcConnectionString: process.env.NWC_SERVER_CONNECTION!,
});
withServerPayments(transport, {
processors: [processor],
pricedCapabilities,
});

resolvePrice runs on every priced request and returns the final quote.

import type { ResolvePriceFn } from '@contextvm/sdk/payments';
const resolvePrice: ResolvePriceFn = async ({
capability,
request,
clientPubkey,
}) => {
// Example: price based on request size.
const requestSize = JSON.stringify(request.params ?? {}).length;
const extra = Math.ceil(requestSize / 1024);
const amount = Math.max(1, Math.round(capability.amount + extra));
return {
amount,
description: `Request size: ${requestSize} bytes`,
_meta: { requestSize },
};
};
withServerPayments(transport, {
processors: [processor],
pricedCapabilities,
resolvePrice,
});

Guidance:

  • Keep it fast and deterministic.
  • Treat it like authorization + pricing logic.
  • If you run multiple server instances, store any usage/quota state in a durable store.

To reject a priced request without creating an invoice, return { reject: true, message? } from resolvePrice.

import type { ResolvePriceFn } from '@contextvm/sdk/payments';
const resolvePrice: ResolvePriceFn = async ({ capability, clientPubkey }) => {
const isBlocked = await isUserBlocked(clientPubkey);
if (isBlocked) {
return { reject: true, message: 'Access denied' };
}
return { amount: capability.amount };
};

When rejected:

  • the server emits notifications/payment_rejected correlated to the request
  • no processor method is called
  • the request is not forwarded

Payment notifications are correlated to the original request using an e tag (the request event id).

  • notifications/payment_required
  • notifications/payment_accepted
  • notifications/payment_rejected

This is how clients know which in-flight request a payment notification belongs to.