Extend

Write an adapter

Add a protocol or a payment rail without touching the core — and the rules every adapter follows.

Two extension points exist, and neither should require changing src/core. If yours does, raise it as a contract change before writing code.

A protocol adapter

Implement ProtocolAdapter, or HttpProtocolAdapter for a protocol served over HTTP. You get one thing to work with: a ProtocolAdapterContext — the pipeline, the resource registry, an event sink, a logger, a clock, an id generator and the public base URL.

example-adapter.tstypescript
import type { HttpProtocolAdapter, ProtocolAdapterContext } from '@devlab.group/agent-commerce';
import { toCommerceError, toErrorEnvelope, toPaymentRequiredEnvelope } from '@devlab.group/agent-commerce';

export function createExampleAdapter(): HttpProtocolAdapter {
  let ctx: ProtocolAdapterContext | undefined;

  return {
    name: 'example',
    mountPath: '/example',
    descriptor: {
      name: 'example',
      kind: 'protocol',
      implementationVersion: '1.0.0',
      supportedSpec: 'example-spec@2026-01-01',   // pin it
      capabilities: ['discovery', 'invoke'],
      unsupported: ['subscriptions', 'batch'],    // be explicit
      status: 'experimental',
    },

    async start(context) {
      ctx = context;
      for (const resource of context.resources.listExposedVia('example')) {
        // map resource.inputSchema into your protocol's schema language
      }
    },

    async handleHttp(req, res) {
      if (!ctx) throw new Error('adapter not started');
      try {
        const outcome = await ctx.pipeline.execute({
          requestId: ctx.ids.next('req'),
          resourceId: /* from the wire */ '',
          input: /* from the wire, payment field removed */ {},
          protocol: 'example',
          receivedAt: ctx.clock.nowIso(),
        });
        if (outcome.kind === 'payment-required') {
          return respond(res, 402, toPaymentRequiredEnvelope(outcome));
        }
        respond(res, 200, outcome.body);
      } catch (error) {
        const commerceError = toCommerceError(error);
        respond(res, commerceError.httpStatus, toErrorEnvelope(commerceError));
      }
    },

    async health() {
      return { status: ctx ? 'pass' : 'fail', checkedAt: new Date().toISOString() };
    },

    async stop() {
      ctx = undefined;
    },
  };
}

Rules

  • Never call a merchant backend. Everything goes through pipeline.execute; bypassing it bypasses payment enforcement.
  • Never implement payment logic. Surface the payment-required outcome; do not decide what is owed.
  • Map errors deterministically with toErrorEnvelope(toCommerceError(e)). No stack traces on the wire.
  • Fail in isolation, and be honest in the descriptor.

A payment provider

Implement PaymentProvider: createRequirement, verify, settle and health.

typescript
export function createExampleProvider(options: ExampleOptions): PaymentProvider {
  return {
    name: 'example-rail',
    descriptor: { /* honest */ },

    async createRequirement(context) {
      return {
        id: /* … */,
        requestId: context.requestId,
        resourceId: context.resource.id,
        provider: 'example-rail',
        amount: context.amount,          // decimal display units, unchanged
        currency: context.currency,
        destination: options.payTo,      // merchant-controlled
        challenge: { provider: 'example-rail', version: '1', accepts: [/* native, opaque */] },
      };
    },

    async verify(context) {
      // No side effects that move money. Ever.
      // Return { status: 'rejected', rejectionReason } for a bad proof.
      // MUST return a replayKey derived ONLY from the authorisation.
    },

    async settle(context) {
      // Runs only after verify succeeded AND the replay key was reserved.
    },

    async health() { /* fast, never throws */ },
  };
}
  • verify must not move funds; settle is the only place that does.
  • Distinguish rejected (bad payment, not retryable) from unavailable (rail down, retryable).
  • The gateway must never need a merchant or buyer private key to use your rail.

Before review

Protocol adapter checklistSchema mapping, request normalisation with _payment stripped, shared envelopes verbatim, conformance fixtures, tests through a real client.
Payment provider checklistNegative tests for every failure, a real settlement proof, no key material, an honest descriptor.

See CONTRIBUTING.md for the development loop.