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,
});

Payment interaction policy (paymentInteraction)

Section titled “Payment interaction policy (paymentInteraction)”

withServerPayments accepts a paymentInteraction policy of type 'optional' | 'transparent':

  • 'optional' (default): the server advertises explicit_gating support and mirrors each client’s requested lifecycle. Clients that request explicit_gating are gated; transparent clients keep the notification flow.
  • 'transparent': transparent-only. A client that requests explicit_gating receives a -32602 negotiation error.
withServerPayments(transport, {
processors: [processor],
pricedCapabilities,
paymentInteraction: 'transparent', // restrict to the notification flow
});

Under 'optional', priced requests are routed to exactly one lifecycle per session based on the negotiated mode. For the explicit-gating end-to-end setup, see Explicit payment gating. For the API reference, see Explicit Gating API.

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,
});

The quote object uses meta (not _meta); it is merged into the emitted payment request’s _meta. Prefer the quotePrice, rejectPrice, and waivePrice helper factories over raw object literals — the reject: true / waive: true discriminants are easy to mistype.```

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

To waive payment and allow the request to proceed without creating an invoice, return { waive: true } from resolvePrice.

const resolvePrice: ResolvePriceFn = async ({ capability, clientPubkey }) => {
const hasPrepaid = await checkPrepaidBalance(clientPubkey);
if (hasPrepaid) {
return { waive: true };
}
return { amount: capability.amount };
};

When waived:

  • no notifications/payment_required is emitted
  • no processor method is called
  • the request is forwarded immediately

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.