Server payments
Server payments
Section titled “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.
Priced capability matching
Section titled “Priced capability matching”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.
Processors (server-side payment rails)
Section titled “Processors (server-side payment rails)”A PaymentProcessor is responsible for:
- creating a
pay_reqfor a given amount - verifying that a previously issued
pay_reqhas 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,});Dynamic pricing: resolvePrice
Section titled “Dynamic pricing: resolvePrice”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.
Rejecting requests without charging
Section titled “Rejecting requests without charging”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_rejectedcorrelated to the request - no processor method is called
- the request is not forwarded
Notifications and correlation
Section titled “Notifications and correlation”Payment notifications are correlated to the original request using an e tag (the request event id).
notifications/payment_requirednotifications/payment_acceptednotifications/payment_rejected
This is how clients know which in-flight request a payment notification belongs to.