Skip to content

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

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

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.

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

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.

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.