Server payments
--- title: Server payments description: Configure priced capabilities, processors, dynamic pricing, and rejection on ContextVM servers --- # 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 You price individual capabilities by `method` + `name`. ```ts 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) 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. ```ts 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`) `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. ```ts withServerPayments(transport, { processors: [processor], pricedCapabilities, paymentInteraction: 'transparent', // restrict to the notification flow }); ``` :::caution[Behavioral change in 0.13.0] The default changed from implicit transparent-only to `'optional'`. A server that accepts payments now **also accepts `explicit_gating` requests** from clients that ask for it. Pass `paymentInteraction: 'transparent'` to restore the previous behavior. ::: 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](/how-to/payments/explicit-gating). For the API reference, see [Explicit Gating API](/reference/ts-sdk/payments/explicit-gating). ## Dynamic pricing: `resolvePrice` `resolvePrice` runs on every priced request and returns the final quote. ```ts 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. ## Rejecting requests without charging To reject a priced request without creating an invoice, return `{ reject: true, message? }` from `resolvePrice`. ```ts 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 ## Waiving payment (prepaid / subscription) To waive payment and allow the request to proceed without creating an invoice, return `{ waive: true }` from `resolvePrice`. ```ts 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 ## Notifications and correlation 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. :::note Notification correlation applies to the **transparent** lifecycle. In `explicit_gating` mode the server returns `-32042` / `-32043` JSON-RPC invocation errors instead of notifications. Enable it per session from the client side; see [Explicit payment gating](/how-to/payments/explicit-gating). :::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,});Payment interaction policy (paymentInteraction)
Section titled “Payment interaction policy (paymentInteraction)”withServerPayments accepts a paymentInteraction policy of type 'optional' | 'transparent':
'optional'(default): the server advertisesexplicit_gatingsupport and mirrors each client’s requested lifecycle. Clients that requestexplicit_gatingare gated; transparent clients keep the notification flow.'transparent': transparent-only. A client that requestsexplicit_gatingreceives a-32602negotiation 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.
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,});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.
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
Waiving payment (prepaid / subscription)
Section titled “Waiving payment (prepaid / subscription)”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_requiredis emitted - no processor method is called
- the request is forwarded immediately
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.