Skip to content

Latest commit

 

History

History
683 lines (587 loc) · 29.9 KB

File metadata and controls

683 lines (587 loc) · 29.9 KB

Public contract

src/core/public-types.ts is the frozen cross-area API, established at v0.1.0-alpha. Change it only through this sequence:

  1. Record the use case, proposed change, alternatives, and compatibility impact.
  2. Approve the change.
  3. Update the canonical type, this record, and affected consumers.
  4. Re-baseline with node scripts/contract-surface.mjs --write.
  5. Run npm run verify.

Do not resolve disagreements by creating duplicate types.

Sources of truth

Source Purpose
src/core/public-types.ts Canonical frozen barrel
contract-surface.txt Generated list of exported names and signatures
docs/contracts.md Invariants, decisions, and integration boundaries

contract-surface.txt is generated by scripts/contract-surface.mjs and checked by npm run check:contract. If its enumeration differs from this document, the generated file is authoritative.

Surface map

This table is for navigation, not enumeration.

Area Defining modules
Resources and pricing domain/resource.ts
Payments domain/payment.ts
Authorization domain/authorization.ts
Receipts and payment attempts domain/receipt.ts
Events domain/event.ts
Requests and outcomes domain/request.ts
Shared names, descriptors, and scalar types domain/common.ts
Wire envelopes, headers, and reserved inputs domain/wire.ts
Errors and retryability errors/**
Protocol adapter boundary interfaces/protocol-adapter.ts
Storage boundary interfaces/store.ts
Backend boundary interfaces/backend.ts
Runtime utilities interfaces/logger.ts, interfaces/runtime.ts

Required invariants

  1. Each ingress path generates CanonicalRequest.requestId; a client never chooses it. Over HTTP, a client x-request-id matching [A-Za-z0-9._:-]{1,64} is logged as clientRequestId and nothing more. Records created for the request, including events, payment attempts, and receipts, reuse the generated ID.
  2. PaymentRequirement.challenge.accepts is provider-native and opaque. Pass it through unchanged.
  3. A payment provider derives PaymentResult.replayKey only from the authorization, including payer, nonce, asset, and network. It must not use the request ID; replaying one authorization under another request must still collide.
  4. PaymentProvider.verify does not move funds. settle runs only after verification returns verified with a non-empty replayKey, any required authorization is verified and reserved, and reservePaymentAttempt succeeds.
  5. EventSink.emit and event persistence must not fail a commerce flow.
  6. HttpBackendExecutor is the only built-in outbound HTTP path to merchant backends and always applies a timeout. A custom GatewayOptions.backend replaces it and must enforce its own timeout.
  7. Amounts are decimal strings in display units, such as "0.01". Payment providers convert them to base units.
  8. exactOptionalPropertyTypes is enabled. Add optional properties conditionally instead of assigning undefined.
  9. Preserve AuthorizationSubmission.payload byte-for-byte as opaque provider input; transport layers must not decode, normalize, or reserialize it. AP2 hashes the issuer-signed token inside its presentation, not the full payload.
  10. Authorization providers must not use PAYMENT_* codes. The pipeline passes a provider's CommerceError through unchanged, so it cannot enforce this boundary itself. A 402 invites payment and retry, which cannot repair a missing or rejected mandate.

CommerceResource.paymentMethods is ordered. The pipeline charges through the first method with an enabled provider and does not retry another method after a rejection. createGateway lists each resource's provider-backed methods first, keeping declared order, so every ingress path labels a proof with the method the pipeline selects; GET /api/resources shows that order.

Store no secrets

Never place private keys, raw payment proofs, authorization headers, tokens, or credentials in receipt metadata, event data, or payment metadata. The SQLite store recursively redacts secret-shaped keys as defense in depth. It replaces the value with [REDACTED] instead of rejecting the write because event persistence must not fail the commerce flow.

Recorded changes

Core and wire contracts

  • The initial surface was frozen at v0.1.0-alpha. UCP was later removed; current protocol names are defined by ProtocolName and PROTOCOL_NAMES.
  • toCommerceError no longer exposes an arbitrary Error.message. It keeps the original error as the non-serialized cause.
  • PaymentAttempt.status gained settlement-uncertain, recorded whenever settle() throws, with or without a transaction hash.
  • GATEWAY_BUSY is a retryable 503 used for transient load shedding.
  • ReceiptStore gained countReceipts and countUndeliveredReceipts; list results are capped and cannot provide reliable totals.
  • DeliverySummary, toDeliverySummary, and DELIVERY_SUMMARY_META_KEY expose a buyer's settlement result. The key is agent-commerce/delivery.
  • x402 v2 changed the HTTP headers to payment-signature, payment-required, and payment-response. The v1 x-payment* headers are not accepted.
  • An unexpected throw from the x402 SDK during verify() maps to PAYMENT_PROVIDER_UNAVAILABLE, replacing the rejection reason unexpected_verify_error. A throw carries no verdict, so it is not recorded against the payer.
  • PaymentChallenge.envelope and PaymentRequiredEnvelope.payment.envelope carry the provider's challenge document unchanged.
  • BackendHandler.inputBindings optionally maps top-level input fields to path, query, and body. If absent, legacy mapping applies; if present, unmapped fields are not forwarded. The mapping is explicit because the backend shape belongs to operator config, not schema-name inference. Shape errors remain INPUT_INVALID before pricing.
  • CanonicalRequest.idempotencyKey and BackendRequest.idempotencyKey are optional. ACP derives a stable, operation-scoped value because requestId changes on retry. The HTTP backend sends it as Idempotency-Key, replacing a static header of the same name from backend config. Two clients using the same key for one deployment and endpoint share both the derived key and the local claim; key uniqueness remains the client's responsibility.

Protocols and gateway

  • ProtocolName gained a2a and acp; PROTOCOL_NAMES is the runtime list used by validation. It is derived from a record keyed by the union, so a missing or extra name fails to compile. The type remains hand-written so generated contract output uses its name instead of expanding a literal union everywhere.
  • AdapterHttpRoute and HttpProtocolAdapter.additionalHttpRoutes let an adapter register fixed routes outside its mount. Startup rejects conflicting cross-adapter route claims, and fixed routes inherit the mount's body limit, concurrency cap, and failure isolation.
  • /.well-known/agent-commerce no longer publishes the facilitator URL or RPC URL because either may contain credentials. It publishes the x402 deployment mode instead.
  • ACP added its config block, checkout adapter, discovery constants, and five checkout-operation mappings. Enabled ACP config is discriminated so an incomplete checkout lifecycle fails during config load.
  • A cancel resource whose input schema sets additionalProperties: false must declare body, which a cancel carries when the caller sends one. Config load rejects one that does not; before, it loaded and refused every cancel carrying intent_trace with INPUT_INVALID.
  • WellKnownDocument.authorizationProviders lists authorization descriptors separately from payment providers.
  • The operator SSE route GET /api/events/stream was removed. GET /api/events is the only event feed, and the demo dashboard polls it.
  • The main entry exports the A2A adapter: createA2aAdapter and its options type, plus A2A_SPEC_VERSION, A2A_PROTOCOL_VERSION and A2A_AGENT_CARD_PATH. It needs no optional peer.
  • CreatePaymentProofOptions dropped the deprecated rpcUrl, which the helper ignored. TypeScript rejects it on a fresh object literal; callers passing a variable with extra properties should remove it as well.

Authorization and AP2

  • The generic authorization contract added AP2 as an authorization method, provider lifecycle types, wire carriers, four AUTHORIZATION_* errors, and optional authorization fields on requests, resources, 402 outcomes, and receipts. Authorization is neither a transport nor a payment rail.
  • The pipeline verifies and reserves authorization before payment replay reservation, then consumes, releases, or marks it uncertain after settlement. Receipts store an AuthorizationRecord, not the live reservation handle or proof. An uncertain settlement moves the mandate to uncertain; only released mandates can be presented again, so an uncertain mandate cannot authorize a second charge. Receipt schema version 2 adds nullable authorization_json with ALTER TABLE, preserving rows in existing databases.
  • GatewayOptions.authorizationProviders and readiness reporting are additive. A failing authorization provider blocks readiness. /ready replaces its raw detail with authorization-provider-unreachable; thrown health errors are logged at error, while returned details are logged at debug.
  • The ./ap2 entry exports the AP2 provider and createCheckoutJwt. The signing helper runs in the merchant process; the gateway only verifies its result. It uses the same RFC 8785 input hash and rejects invalid keys or amounts before they become opaque verification failures.
  • AP2_REJECTION_REASONS dropped unsupported_algorithm. A digest algorithm other than sha-256 is malformed_presentation, and a signing algorithm other than the pinned one is invalid_signature.

MPP

  • PaymentMethodName gained mpp, and PAYMENT_METHOD_NAMES exposes payment names at runtime. This changes the frozen union but adds no MPP-specific wire fields to core.
  • GatewayConfig.payments gained an optional mpp block. A paid resource still needs at least one of its named rails enabled, so a resource that names only mpp needs payments.mpp enabled but no x402 block.
  • The ./mpp entry exports createMppPaymentProvider (also as mpp) beside the pinned profile metadata and descriptor. The provider runs local checks, then verifies and settles through the x402 facilitator named in its options, using x402-compatible replay keys.

Published entry points

Entry Surface Imported optional peers
@devlab.group/agent-commerce Core contract, config, gateway, receipt store, A2A and ACP adapters None
@devlab.group/agent-commerce/ap2 AP2 verification and checkout signing jose, @sd-jwt/core, canonicalize
@devlab.group/agent-commerce/mcp MCP adapter @modelcontextprotocol/sdk
@devlab.group/agent-commerce/mpp MPP provider and profile metadata @coinbase/x402 (only for auth.type: cdp), @x402/core, @x402/evm, mppx, viem
@devlab.group/agent-commerce/x402 x402 provider and client proof helper @coinbase/x402 (only for auth.type: cdp), @x402/core, @x402/evm, viem

The main entry and CLI must not import optional peers. package.json is the authoritative export and dependency map.

Integration contracts

The TypeScript declarations own the exact shapes. The excerpts below record the factory boundaries and constraints that consumers rely on.

Receipt store

export interface SqliteReceiptStoreOptions {
  /** File path, or ':memory:' for tests. Parent directory is created if missing. */
  readonly path: string;
  readonly logger?: Logger;
  readonly clock?: Clock;
  readonly ids?: IdGenerator;
}

export function createSqliteReceiptStore(
  options: SqliteReceiptStoreOptions,
): ReceiptStore;

reservePaymentAttempt is atomic and throws CommerceError('PAYMENT_REPLAYED', …) for a duplicate replayKey.

x402

Published as @devlab.group/agent-commerce/x402.

export interface X402ProviderOptions {
  readonly network: string; // CAIP-2, for example 'eip155:84532'
  readonly rpcUrl: string;
  readonly asset: `0x${string}`;
  readonly assetName: string;
  readonly assetVersion: string;
  readonly assetDecimals: number;
  /** Merchant-controlled settlement destination. */
  readonly payTo: `0x${string}`;
  readonly maxTimeoutSeconds?: number;
  readonly facilitator: X402FacilitatorConfig;
  /** Must be true before mainnet settlement. */
  readonly allowMainnet?: boolean;
  /** Must be true for an unauthenticated mainnet facilitator. */
  readonly allowUnauthenticatedFacilitator?: boolean;
  readonly logger?: Logger;
  readonly clock?: Clock;
  readonly ids?: IdGenerator;
}

export type FacilitatorAuth =
  | { readonly type: 'none' }
  | { readonly type: 'bearer'; readonly token: string }
  | { readonly type: 'cdp'; readonly apiKeyId: string; readonly apiKeySecret: string };

export type X402FacilitatorConfig =
  | { readonly mode: 'local'; readonly signerPrivateKey: string }
  | { readonly mode: 'remote'; readonly url: string; readonly auth: FacilitatorAuth };

export function createX402PaymentProvider(
  options: X402ProviderOptions,
): PaymentProvider;

export type DeploymentMode = 'local' | 'testnet' | 'mainnet';
export const SUPPORTED_NETWORK_IDS: readonly string[];

export interface NetworkProfile {
  readonly id: string; // CAIP-2
  readonly chainId: number;
  readonly displayName: string;
  readonly kind: 'testnet' | 'mainnet';
  /** Canonical USDC and its EIP-712 domain, enforced on mainnet only */
  readonly canonicalAsset?: {
    readonly symbol: string;
    readonly address: string;
    readonly name: string;
    readonly version: string;
  };
}

Local facilitator keys are for the development chain only and must never hold funds. Remote mode holds no facilitator signing key in the gateway.

MPP

Published as @devlab.group/agent-commerce/mpp.

export interface MppProviderOptions {
  /** Merchant-controlled settlement destination. Never gateway-owned */
  readonly recipient: `0x${string}`;
  /** EIP-3009 token the charge is paid in */
  readonly asset: `0x${string}`;
  readonly assetName: string; // EIP-712 domain name, e.g. 'USDC'
  readonly assetVersion: string; // EIP-712 domain version, e.g. '2'
  readonly realm: string;
  /** Key that binds each challenge to this gateway. Never logged or published */
  readonly challengeSecret: string;
  readonly rpcUrl: string;
  /** x402 facilitator that verifies and broadcasts the authorization */
  readonly facilitator: X402FacilitatorConfig;
  /** Must be true before mainnet settlement */
  readonly allowMainnet?: boolean;
  /** Must be true for an unauthenticated mainnet facilitator */
  readonly allowUnauthenticatedFacilitator?: boolean;
  readonly logger?: Logger;
  readonly challengeTtlSeconds?: number; // default 300
  /** CAIP-2 network: 'eip155:84532' (default) or 'eip155:8453' */
  readonly network?: string;
  readonly clock?: Clock;
  readonly ids?: IdGenerator;
}

export function createMppPaymentProvider(options: MppProviderOptions): PaymentProvider;

Construction fails with CONFIG_INVALID for a recipient or asset that is not an address, an empty EIP-712 domain name or version, an empty or multi-line realm, a challenge secret with length below 32, a TTL that is not a positive whole number, or an unsupported network. The supported networks are eip155:84532 and eip155:8453. The provider then builds an internal x402 provider from the same network, asset, recipient and facilitator options, so every x402 construction check applies, including the mainnet guardrails. X402FacilitatorConfig is the type exported by the ./x402 entry. createRequirement refuses a resource priced in anything but USDC (CONFIG_INVALID) and an amount that is not a positive decimal with at most 6 fractional digits (PAYMENT_INVALID). Each challenge binds the resource id in its HMAC-covered opaque field.

verify performs local checks, then the facilitator's read-only check through the internal x402 provider, and confirms that both derived the same replay key. The shared key makes an authorization collide across both rails. settle rewraps the credential and hands it to x402 settlement. Negative settlement results remain rejections; thrown settlement errors propagate, so the pipeline records settlement-uncertain and returns PAYMENT_SETTLEMENT_FAILED. Returned results name mpp; health returns the internal x402 provider's health unchanged.

PaymentSubmission.payload is the serialized credential: the value of an Authorization: Payment ... header, scheme included. PaymentChallenge.envelope is { wwwAuthenticate }, the challenge as a WWW-Authenticate value, and a settled result's metadata.receipt is the Payment-Receipt value.

AP2

Published as @devlab.group/agent-commerce/ap2.

export interface Ap2AuthorizationProviderOptions {
  readonly config: EnabledAp2Config;
  readonly clock?: Clock;
  readonly logger?: Logger;
  readonly replayStore?: Ap2ReplayStore;
}

export interface Ap2AuthorizationProvider extends AuthorizationProvider {
  close(): void;
}

export function createAp2AuthorizationProvider(
  options: Ap2AuthorizationProviderOptions,
): Ap2AuthorizationProvider;

export type Ap2AuthorizationConfig =
  | { readonly enabled: false }
  | {
      readonly enabled: true;
      readonly specVersion: '0.2.0';
      readonly mode: 'direct';
      readonly trust: {
        readonly mandateIssuers: readonly Ap2TrustedIssuer[];
        readonly checkoutIssuers: readonly Ap2TrustedIssuer[];
      };
      readonly clockSkewSeconds: number;
      readonly replay: { readonly path: string };
    };

export type EnabledAp2Config = Extract<Ap2AuthorizationConfig, { enabled: true }>;

export interface Ap2TrustedIssuer {
  readonly issuer: string;
  readonly audience: string;
  readonly keys: readonly Ap2TrustedKey[];
}

export interface Ap2TrustedKey {
  readonly kid: string;
  readonly jwk: Readonly<Record<string, string>>; // public P-256 JWK
}

The replay database is separate from payment replay storage. The caller that creates the provider must close it. main.ts calls close() after gateway.close(); gateway.close() does not close authorization providers.

The merchant-side checkout signer has this boundary:

export type Ap2SigningKey = Readonly<Record<string, unknown>> | string;

export interface CreateCheckoutJwtOptions {
  readonly privateKey: Ap2SigningKey; // private ES256 JWK or PKCS#8 PEM
  readonly kid: string;
  readonly issuer: string;
  readonly audience: string;
  readonly resourceId: string;
  readonly input: unknown;
  readonly amount: string;
  readonly currency: string;
  readonly paymentMethod: string;
  readonly destination?: string;
  readonly network?: string;
  readonly asset?: string;
  readonly jwtId?: string;
  readonly expiresInSeconds?: number;
  readonly now?: Date;
}

export function createCheckoutJwt(
  options: CreateCheckoutJwtOptions,
): Promise<string>;

input excludes reserved fields, request IDs, and transport metadata. amount is a decimal string and is compared as text, so 0.1 and 0.10 are distinct.

MCP

Published as @devlab.group/agent-commerce/mcp.

export interface McpAdapterOptions {
  readonly mountPath?: string; // default '/mcp'
  readonly serverName?: string; // default 'agent-commerce'
  readonly serverVersion?: string; // default package version
}

export function createMcpAdapter(
  options?: McpAdapterOptions,
): HttpProtocolAdapter;

Gateway

export interface GatewayOptions {
  readonly config: GatewayConfig;
  readonly store: ReceiptStore;
  readonly paymentProviders: readonly PaymentProvider[];
  readonly authorizationProviders?: readonly AuthorizationProvider[];
  readonly protocolAdapters: readonly ProtocolAdapter[];
  readonly logger?: Logger;
  readonly clock?: Clock;
  readonly ids?: IdGenerator;
  readonly backend?: BackendExecutor;
}

export interface GatewayInstance {
  readonly pipeline: ExecutionPipeline;
  readonly resources: ResourceRegistry;
  listen(): Promise<{ url: string }>;
  close(): Promise<void>;
  readonly server: FastifyInstance;
}

export function createGateway(
  options: GatewayOptions,
): Promise<GatewayInstance>;

Omit authorizationProviders when no resource requires authorization. backend is an injectable test boundary.

Configuration

export function loadConfig(options?: {
  path?: string;
  env?: NodeJS.ProcessEnv;
  cwd?: string;
}): Promise<GatewayConfig>;

export function parseConfig(
  raw: unknown,
  env: NodeJS.ProcessEnv,
): GatewayConfig;

export type AcpCheckoutOperation =
  | 'createCheckoutSession'
  | 'updateCheckoutSession'
  | 'getCheckoutSession'
  | 'completeCheckoutSession'
  | 'cancelCheckoutSession';

export interface AcpDiscoveryConfig {
  readonly documentationUrl?: string;
  readonly supportedCurrencies?: readonly string[];
  readonly supportedLocales?: readonly string[];
  readonly interventionTypes?: readonly string[];
}

export type AcpProtocolConfig =
  | { readonly enabled: false; readonly mountPath: string }
  | {
      readonly enabled: true;
      readonly mountPath: string;
      readonly auth: { readonly type: 'bearer'; readonly token: string };
      readonly idempotency: { readonly path: string; readonly retentionHours: number };
      readonly checkout: {
        readonly operations: Readonly<Record<AcpCheckoutOperation, string>>;
      };
      readonly discovery?: AcpDiscoveryConfig;
    };

export interface GatewayConfig {
  readonly version: 1;
  readonly merchant: {
    readonly id: string;
    readonly name: string;
    readonly publicBaseUrl: string;
  };
  readonly server: {
    readonly port: number;
    readonly host: string;
    readonly adminToken?: string;
    readonly allowedOrigins: readonly string[];
  };
  readonly storage: {
    readonly receipts: { readonly driver: 'sqlite'; readonly path: string };
  };
  readonly protocols: {
    readonly http: { readonly enabled: boolean };
    readonly mcp: { readonly enabled: boolean; readonly mountPath: string };
    readonly a2a: { readonly enabled: boolean; readonly mountPath: string };
    readonly acp: AcpProtocolConfig;
  };
  readonly resources: readonly CommerceResource[];
  readonly payments: {
    readonly x402?: {
      readonly enabled: boolean;
      readonly network: string;
      readonly rpcUrl: string;
      readonly asset: string;
      readonly assetName: string;
      readonly assetVersion: string;
      readonly assetDecimals: number;
      readonly payTo: string;
      readonly maxTimeoutSeconds: number;
      readonly facilitator: X402FacilitatorConfig;
      readonly allowMainnet?: boolean;
      readonly allowUnauthenticatedFacilitator?: boolean;
    };
    readonly mpp?: {
      readonly enabled: boolean;
      readonly network: 'eip155:84532' | 'eip155:8453';
      readonly rpcUrl: string;
      readonly asset: string;
      readonly assetName: string;
      readonly assetVersion: string;
      readonly recipient: string;
      readonly realm: string;
      readonly challengeSecret: string;
      readonly challengeTtlSeconds?: number;
      readonly facilitator: X402FacilitatorConfig;
      readonly allowMainnet?: boolean;
      readonly allowUnauthenticatedFacilitator?: boolean;
    };
  };
  readonly authorization?: {
    readonly ap2: Ap2AuthorizationConfig;
  };
}

GatewayConfig is validated, environment-substituted, and normalized. An unset adminToken makes operator routes return 404; an empty allowedOrigins permits no cross-origin browser access.

Gateway HTTP surface

Route Behavior
GET /health Returns 200 after the Host check and, when present, the Origin check pass; a rejected Host or Origin returns 403.
GET /ready Returns 503 if the store, an adapter, or either provider kind reports fail; warn remains ready. Results are briefly cached and concurrent probes share one check.
GET /.well-known/agent-commerce Publishes merchant and sanitized adapter, provider, store, and protocol metadata.
GET /api/resources Lists canonical resources without secrets.
POST /api/resources/:id/invoke Invokes the HTTP surface. Payment and authorization use their defined headers; an unpaid request returns 402.
GET /api/receipts?limit= Lists recent receipts through the operator-token gate.
GET /api/events?limit= Lists recent events through the operator-token gate.
Adapter mounts and fixed routes Delegated to enabled protocol adapters, with collision checks and per-mount limits.

Without server.adminToken, both operator routes return 404. With a token, they require Authorization: Bearer <token>; query-string tokens are not accepted.

Local chain deployment manifest

npm run chain:deploy writes the git-ignored .deploy/local.json:

{
  "chainId": 84532,
  "rpcUrl": "http://127.0.0.1:8545",
  "hostRpcUrl": "http://127.0.0.1:8545",
  "asset": "0x...",
  "assetName": "MockUSDC",
  "assetVersion": "2",
  "assetDecimals": 6,
  "merchant": {
    "address": "0x...",
    "privateKeyLabel": "LOCAL DEVELOPMENT ONLY - DO NOT FUND"
  },
  "buyer": {
    "address": "0x...",
    "privateKey": "0x...",
    "note": "LOCAL DEVELOPMENT ONLY - DO NOT FUND"
  },
  "facilitator": {
    "address": "0x...",
    "privateKey": "0x...",
    "note": "LOCAL DEVELOPMENT ONLY - DO NOT FUND"
  },
  "buyerInitialBalance": "100.00"
}

src/payments/x402/local-chain/manifest.ts owns LocalChainManifest, LOCAL_CHAIN_MANIFEST_PATH, and readLocalChainManifest(cwd?). The deployment script imports that module directly, and the internal testing surface re-exports it.

hostRpcUrl is the host-reachable address of the same chain. Host-side tools use hostRpcUrl ?? rpcUrl, which supports older manifests without the optional field. readLocalChainManifest throws when the file is absent or malformed.

The internal src/payments/x402/testing.ts surface is for repository tests and the demo buyer; it is not a published package entry.

Client-side payment helper

createPaymentProof is the x402 client helper exported from the ./x402 entry. The demo buyer and the tests use it. Signing is offline: the chain id comes from the requirement's CAIP-2 network. The gateway does not call it or hold the buyer key.

export interface CreatePaymentProofOptions {
  /** The buyer's private key. It signs locally and is never sent anywhere */
  readonly buyerPrivateKey: `0x${string}`;
  /** One entry from PaymentRequiredEnvelope.payment.accepts, verbatim */
  readonly accepts: Readonly<Record<string, unknown>>;
  /** For negative tests only: a wrong amount, recipient, nonce or validity window */
  readonly overrides?: {
    readonly value?: string;
    readonly payTo?: string;
    readonly nonce?: `0x${string}`;
    readonly validBefore?: number;
    readonly validAfter?: number;
  };
}

export function createPaymentProof(
  options: CreatePaymentProofOptions,
): Promise<string>;

The result is the base64 PAYMENT-SIGNATURE value returned to the gateway.

Composition root

src/gateway/main.ts loads configuration, creates concrete providers and adapters, starts the gateway, and closes owned resources during shutdown. createGateway remains usable with injected fakes and does not depend on the composition root.