Build your own payment rail
Build your own payment rail
Section titled “Build your own payment rail”The payments layer is designed so you can add new settlement methods without changing transports.
To add a new payment rail you implement:
- a server-side
PaymentProcessor(issue + verify) - a client-side
PaymentHandler(pay)
Both are keyed by a PMI string (for example my-rail-v1).
1) Choose a PMI
Section titled “1) Choose a PMI”Pick a stable string identifier.
- Good:
acme-checkout-v1 - Avoid: version-less strings that you can’t evolve later
2) Implement a processor
Section titled “2) Implement a processor”Processors must be able to:
- create a
pay_reqthat encodes enough information for a client handler to pay - later verify settlement for that
pay_req
Skeleton:
import type { PaymentProcessor, PaymentProcessorCreateParams, PaymentProcessorVerifyParams,} from '@contextvm/sdk/payments';
export class MyRailPaymentProcessor implements PaymentProcessor { public readonly pmi = 'my-rail-v1';
public async createPaymentRequired( params: PaymentProcessorCreateParams, ): Promise<{ amount: number; pay_req: string; description?: string; pmi: string; }> { // 1) Create a provider checkout/invoice // 2) Encode whatever the client needs to complete payment // 3) Return an opaque pay_req understood by the handler return { amount: params.amount, pay_req: JSON.stringify({ invoiceId: '...', requestEventId: params.requestEventId, }), description: params.description, pmi: this.pmi, }; }
public async verifyPayment( params: PaymentProcessorVerifyParams, ): Promise<{ _meta?: Record<string, unknown> }> { // Check provider for invoice status and fail if unpaid. return { _meta: { verifiedAt: Date.now() } }; }}Guidance:
- The processor runs on the server; never embed server secrets in
pay_req. - Make verification idempotent per
requestEventId.
3) Implement a handler
Section titled “3) Implement a handler”Handlers must be able to pay a pay_req for their PMI.
Skeleton:
import type { PaymentHandler, PaymentHandlerRequest,} from '@contextvm/sdk/payments';
export class MyRailPaymentHandler implements PaymentHandler { public readonly pmi = 'my-rail-v1';
public async canHandle(_req: PaymentHandlerRequest): Promise<boolean> { // Optional: enforce client policy (max amount, disabled rail, etc.) return true; }
public async handle(req: PaymentHandlerRequest): Promise<void> { const decoded = JSON.parse(req.pay_req) as { invoiceId: string }; // Pay invoice using your local wallet/provider. await payInvoice(decoded.invoiceId); }}4) Wire into server and client
Section titled “4) Wire into server and client”On the server:
withServerPayments(transport, { processors: [new MyRailPaymentProcessor()], pricedCapabilities,});On the client:
withClientPayments(baseTransport, { handlers: [new MyRailPaymentHandler()],});5) Test your rail
Section titled “5) Test your rail”Recommended tests:
- server emits
payment_requiredwith your PMI - client selects your handler
- server emits
payment_acceptedafter verification - rejection path works (
resolvePrice→payment_rejected)