Client payments
--- title: Client payments description: Configure handlers, PMI support, and client behavior for CEP-8 paid servers --- # Client payments Client payments are implemented as middleware around your client transport. When a server replies with `notifications/payment_required`, the payments layer: 1. selects a `PaymentHandler` for the PMI 2. asks the handler to pay `pay_req` 3. continues waiting for the original request completion ## Handlers A `PaymentHandler` implements one PMI (one payment rail). The built-in handler supports Lightning BOLT11 over NWC. ```ts import { LnBolt11NwcPaymentHandler, withClientPayments, } from "@contextvm/sdk/payments"; const handler = new LnBolt11NwcPaymentHandler({ nwcConnectionString: process.env.NWC_CLIENT_CONNECTION!, }); const paidTransport = withClientPayments(baseTransport, { handlers: [handler], }); ``` ## PMI advertisement (`pmi` tags) CEP-8 allows clients to advertise which PMIs they can pay by including `pmi` tags on requests. Why it matters: - If the server supports multiple PMIs, your tags help it select a compatible one. - The order expresses preference (first = most preferred). When you wrap a transport with `withClientPayments(...)`, the SDK will automatically inject `pmi` tags derived from your configured handler list. ## Payment rejection If the server rejects a request without charging, it emits `notifications/payment_rejected` correlated to the request. This is a policy outcome (no invoice, no settlement). Your app should typically: - stop retrying the same request (unless the policy might change) - surface the optional message to users/operators ## Client-side payment policy (`paymentPolicy`) You can optionally intercept `notifications/payment_required` locally and decide whether the client should proceed with payment. This is designed for: - interactive UX (UI prompts) - LLM-driven consent - headless "Code Mode" policies (budgets / allowlists) ```ts import { withClientPayments, type PaymentHandlerRequest, } from "@contextvm/sdk/payments"; const paidTransport = withClientPayments(baseTransport, { handlers: [handler], paymentPolicy: async (req: PaymentHandlerRequest, ctx): Promise<boolean> => { // ctx?.method examples: "tools/call", "prompts/get", "resources/read" // ctx?.capability examples: "tool:add", "prompt:welcome", "resource:greeting://alice" if (req.amount > 500) return false; if (ctx?.capability === "tool:expensive_tool") return false; return true; }, }); ``` ### Decline behavior (fail-fast) When `paymentPolicy` returns `false`, the SDK will **fail the original request immediately** (instead of hanging until timeout) on transports that support correlation (such as `NostrClientTransport`). The synthesized JSON-RPC error uses: - `error.code = -32000` - `error.message = "Payment declined by client policy"` - `error.data` includes `{ pmi, amount, method, capability }` when available Similarly, if a handler's `canHandle` returns `false`, the SDK will fail-fast with `"Payment declined by client handler"` when correlation exists. ## Multiple handlers You can configure multiple handlers if you support multiple payment rails. ```ts const paidTransport = withClientPayments(baseTransport, { handlers: [ lightningHandler, // futureHandler, ], }); ``` The first handler that matches the server-selected PMI is used.Client payments
Section titled “Client payments”Client payments are implemented as middleware around your client transport.
When a server replies with notifications/payment_required, the payments layer:
- selects a
PaymentHandlerfor the PMI - asks the handler to pay
pay_req - continues waiting for the original request completion
Handlers
Section titled “Handlers”A PaymentHandler implements one PMI (one payment rail). The built-in handler supports Lightning BOLT11 over NWC.
import { LnBolt11NwcPaymentHandler, withClientPayments,} from "@contextvm/sdk/payments";
const handler = new LnBolt11NwcPaymentHandler({ nwcConnectionString: process.env.NWC_CLIENT_CONNECTION!,});
const paidTransport = withClientPayments(baseTransport, { handlers: [handler],});PMI advertisement (pmi tags)
Section titled “PMI advertisement (pmi tags)”CEP-8 allows clients to advertise which PMIs they can pay by including pmi tags on requests.
Why it matters:
- If the server supports multiple PMIs, your tags help it select a compatible one.
- The order expresses preference (first = most preferred).
When you wrap a transport with withClientPayments(...), the SDK will automatically inject pmi tags derived from your configured handler list.
Payment rejection
Section titled “Payment rejection”If the server rejects a request without charging, it emits notifications/payment_rejected correlated to the request.
This is a policy outcome (no invoice, no settlement).
Your app should typically:
- stop retrying the same request (unless the policy might change)
- surface the optional message to users/operators
Client-side payment policy (paymentPolicy)
Section titled “Client-side payment policy (paymentPolicy)”You can optionally intercept notifications/payment_required locally and decide whether the client should proceed with payment.
This is designed for:
- interactive UX (UI prompts)
- LLM-driven consent
- headless “Code Mode” policies (budgets / allowlists)
import { withClientPayments, type PaymentHandlerRequest,} from "@contextvm/sdk/payments";
const paidTransport = withClientPayments(baseTransport, { handlers: [handler], paymentPolicy: async (req: PaymentHandlerRequest, ctx): Promise<boolean> => { // ctx?.method examples: "tools/call", "prompts/get", "resources/read" // ctx?.capability examples: "tool:add", "prompt:welcome", "resource:greeting://alice" if (req.amount > 500) return false; if (ctx?.capability === "tool:expensive_tool") return false; return true; },});Decline behavior (fail-fast)
Section titled “Decline behavior (fail-fast)”When paymentPolicy returns false, the SDK will fail the original request immediately (instead of hanging until timeout) on transports that support correlation (such as NostrClientTransport).
The synthesized JSON-RPC error uses:
error.code = -32000error.message = "Payment declined by client policy"error.dataincludes{ pmi, amount, method, capability }when available
Similarly, if a handler’s canHandle returns false, the SDK will fail-fast with "Payment declined by client handler" when correlation exists.
Multiple handlers
Section titled “Multiple handlers”You can configure multiple handlers if you support multiple payment rails.
const paidTransport = withClientPayments(baseTransport, { handlers: [ lightningHandler, // futureHandler, ],});The first handler that matches the server-selected PMI is used.