Skip to content

Latest commit

 

History

History
937 lines (834 loc) · 47.1 KB

contracts.md

File metadata and controls

937 lines (834 loc) · 47.1 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. A provider that sets PaymentRequirement.metadata.settleAfterBackend to true has settle run after a successful backend call instead of before it; the authorization flow of x402 and MPP does. A backend failure then settles nothing. A rejected or unconfirmed settlement withholds the response. metadata.reuseRejectedAuthorization: true lets the pipeline reserve a rejected attempt's authorization again with reclaimRejected. x402 sets it; MPP's single-use credentials cannot be reused.
  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. Payment amounts are decimal strings in display units, such as "0.01". Providers convert them to base units. If a settled result includes metadata.amountBaseUnits, the HTTP route uses that value for PAYMENT-RESPONSE.amount.
  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 lists only those provider-backed methods, so its first entry is the rail a challenge offers.

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 when settle() throws, with or without a transaction hash. A throw whose details.settlementNotSubmitted is true records rejected instead and answers PAYMENT_PROVIDER_UNAVAILABLE: the provider knows nothing was submitted, so the authorization can be reserved again.
  • Optional PaymentAttemptReservation.reclaimRejected lets reservePaymentAttempt reclaim an attempt the same provider rejected (see Receipt store). This permits x402 retries after a backend failure or a refused settlement, which previously returned 402. A custom ReceiptStore that ignores the field still refuses these retries.
  • 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.
  • ReceiptStore.listReceipts accepts ReceiptListOptions; undelivered: true filters for receipts counted by countUndeliveredReceipts. Custom stores that ignore the flag also return delivered receipts.
  • ReceiptStore.countUncertainPaymentAttempts is required and counts settlement-uncertain and legacy failed attempts. Uncertain settlements leave no receipt, so countUndeliveredReceipts cannot find them. listPaymentAttempts accepts PaymentAttemptListOptions; uncertain: true filters for the same statuses. Custom stores that ignore the flag do not filter by settlement status.
  • 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.
  • CanonicalRequest.backendHeaders and BackendRequest.backendHeaders are optional: headers an adapter asks the backend executor to forward. The pipeline passes them through unchanged. In the HTTP backend, a header from backend config wins over a forwarded header of the same name, and so do the derived Idempotency-Key and the executor's Content-Type. The HTTP backend never forwards Authorization, Cookie, Set-Cookie, Host, Content-Length, Content-Type, Idempotency-Key, Connection, Keep-Alive, TE, Trailer, Transfer-Encoding, Upgrade or any Proxy-* header. An illegal forwarded name or value is INPUT_INVALID before pricing. ACP is the only adapter that sets the field. A custom BackendExecutor that ignores it forwards nothing.
  • PaymentProvider.selectRequirement optionally selects a proof's offer before verify. It returns the selected destination, network, asset and metadata while preserving id and the full challenge. Verification, authorization, replay reservation and settlement use this requirement. Providers without the hook keep their existing path.
  • Optional AdapterHealth.components lists AdapterHealthComponent results with a name and status, such as one per x402 offer. Discovery may publish these fields; they contain no detail. Omission preserves existing reporting.
  • PaymentRequirement.destination and PaymentRequiredEnvelope.payment.destination are optional. Multi-offer x402 challenges omit destination, network and asset; single-offer and selected x402 requirements include all three. The envelope fields are deprecated: read individual offers in accepts. With strictNullChecks, callers passing either destination as a required string must now handle undefined.
  • Optional PaymentContext.input carries validated resource input to createRequirement, allowing a rail to bind its challenge to the request. x402's ERC-8409 quotes hash this input with the resource ID. A rail must not send the input to a third party. Providers that ignore the field are unaffected.

Protocols and gateway

  • The x402 provider accepts X402MultiOfferProviderOptions with one shared facilitator. Multi-offer challenges use the offer-independent URL resource://agent-commerce/resources/<id>. GatewayConfig.payments.x402.options holds resolved options; single-option fields mirror the first. /.well-known includes payments.x402.options only for multiple offers and omits RPC URLs.

  • An x402 offer takes assetTransferMethod (X402AssetTransferMethod, eip3009 by default, or permit2). X402ProviderOptions now extends X402OfferOptions.

    • assetName and assetVersion became optional in both option types, in GatewayConfig.payments.x402 and its options, and in /.well-known. EIP-3009 still requires both: config load supplies the default asset's metadata or requires explicit values; provider construction rejects missing values. Permit2 may omit them. With strictNullChecks, readers must handle undefined.
    • /.well-known publishes assetTransferMethod: permit2 beside a Permit2 offer.
    • The provider no longer forwards a payload's extensions to the facilitator.
    • createPaymentProof refuses an offer naming a method other than eip3009 with INPUT_INVALID.
    • The invoke route returns 412 for a missing Permit2 allowance (permit2_allowance_required), preserving PAYMENT-REQUIRED as specified by the exact EVM scheme.
  • The x402 provider serves Solana offers (solana: networks) through a remote facilitator.

    • X402OfferOptions.asset and payTo widened from 0x${string} to string to carry a Solana mint and wallet. Code reading either field as 0x${string} must narrow it.
    • The descriptor's supportedSpec names family=eip155, family=solana or both; capabilities gain exact-svm, and unsupported no longer lists svm.
    • A settlement success must name the offer's network and a transaction in its family's form, for EVM offers too. Anything else unlocks nothing, and the pipeline records the attempt settlement-uncertain.
    • A settlement failure that names a transaction is a rejection only with the family's on-chain failure reason: invalid_exact_evm_transaction_failed, invalid_exact_svm_transaction_failed or NEAR's settlement_failed. Any other reason leaves the outcome unknown. A rejection keeps its transaction as the attempt's externalReference.
  • The x402 provider serves NEAR offers (near:mainnet, near:testnet) through a remote facilitator.

    • X402OfferOptions.asset and payTo carry NEAR account IDs for a NEAR offer. The descriptor's supportedSpec can name family=near, capabilities gain exact-near, and unsupported no longer lists a chain family.
    • @x402/near is the optional peer for NEAR offers, loaded only when one is configured.
    • Verification reasons that name the facilitator's failure to read the chain or its configuration, such as invalid_exact_near_access_key_lookup_failed, are no verdict: PAYMENT_PROVIDER_UNAVAILABLE. At settle the SDK returns them before submitting, so the provider throws with details.settlementNotSubmitted and the attempt is recorded rejected.
    • createX402PaymentProvider refuses a maxTimeoutSeconds that is not a positive integer with CONFIG_INVALID, as config load already did: the NEAR facilitator refuses every payment under such a value.
  • x402 verification and settlement currency uses the network's default token symbol when paying that asset, otherwise the resource's currency. Settlement preserves a differently labeled price, such as USD, in metadata.quote (amount, currency). Payment attempts, receipts and PAYMENT-RESPONSE now record USDC instead of repeating a USD quote.

  • A replay error's details.network names the network of the offer it paid.

  • /.well-known publishes each rail's paymentFlow, and resourcePaymentFlows when a resource that lists the rail sets its own; WellKnownDocument gains both fields for x402 and MPP. A rail's resourcePaymentFlows in GatewayConfig no longer includes resources that do not list that rail.

  • A resource's x402.accepts overrides its offers, matching the SDK's route-level accepts.

    • X402ProviderOptions.resourceOffers, keyed by resource id, replaces the provider's offers for those resources, under the same facilitator and settings. An offer that several lists repeat is built and health-checked once.
    • GatewayConfig.payments.x402.resourceOptions holds the resolved lists, and /.well-known publishes them as payments.x402.resourceOptions. WellKnownX402Offer names the offer shape both lists share.
  • x402 health checks all offers, reporting components named <network> <asset> <payTo> with lowercase EVM addresses. Any failing offer fails the provider. /.well-known copies statuses into options and resourceOptions; /ready is unchanged.

  • Experimental ERC-8409 quotes on x402 EVM offers. Without quote, challenges are unchanged.

    • X402ProviderOptions.quote enables signing. GatewayConfig.payments.x402.quote is present only when payments.x402.quote.enabled is true.
    • createX402PaymentProvider returns X402PaymentProvider, a PaymentProvider whose optional quote describes the issuer, request scheme and extension key. The descriptor gains the capability erc-8409-quotes.
    • /.well-known publishes payments.x402.quote (WellKnownX402Quote), read from the signing provider.
  • 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.

  • A resource's mimeType sets the media type its x402 challenges declare as resource.mimeType; application/json remains the default. X402ProviderOptions.resourceMimeTypes and GatewayConfig.payments.x402.resourceMimeTypes carry it by resource id.

  • POST /api/resources/:id/invoke delivers the backend's Content-Type, with charset=utf-8, when it is a text type that fits the relayed body; text no longer always goes out as text/plain. Every text delivery also carries Content-Security-Policy: sandbox and X-Content-Type-Options: nosniff.

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. It consumes, releases, or marks the reservation uncertain based on the outcome. If the backend fails before settlement, it releases the reservation without settling. 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.
  • MppProviderOptions and GatewayConfig.payments.mpp accept optional paymentFlow and resourcePaymentFlows with the x402 flow values.
  • MPP's default paymentFlow is upfront; it was authorization. The charge intent counts a backend call made before verification completes as partial access, and EVM charge verification includes the transfer. A deployment that wants the old order must set paymentFlow: authorization.

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/server
@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 @x402/core, @x402/evm, viem; @coinbase/x402 only for auth.type: cdp; @x402/svm and @solana/kit only for a Solana offer; @x402/near only for a NEAR offer

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 atomically claims a replayKey. On a duplicate, it throws CommerceError('PAYMENT_REPLAYED', …) with the existing attempt's status in details.attemptStatus if the store can read it. The pipeline returns 402 and a fresh challenge if that status is settled or rejected. It returns 409 for any other status or when the status is unavailable. That includes the legacy failed, which may hide a settlement with no verdict.

A rejected attempt settled nothing: its backend failed first, or the provider returned a verdict that no transfer occurred. With reclaimRejected: true, the same provider can replace it with a new reservation; only one concurrent claim wins. This lets the NEAR reference client retry using the access key's next nonce. The SQLite store first copies the replaced row into payment_attempt_history, so the earlier request and its rejection reason stay on record.

x402

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

/** One settlement option: network, token and merchant payee */
export interface X402OfferOptions {
  readonly network: string; // CAIP-2, for example 'eip155:84532'
  readonly rpcUrl: string;
  readonly asset: string; // ERC-20 contract, Solana mint or NEAR token account
  /** The token's EIP-712 domain; an EIP-3009 offer needs both */
  readonly assetName?: string;
  readonly assetVersion?: string;
  readonly assetDecimals: number;
  /** Merchant-controlled settlement destination. */
  readonly payTo: string; // on Solana the wallet, on NEAR the account
  /** EVM only. Default 'eip3009'. A Permit2 buyer approves Permit2 for the token first */
  readonly assetTransferMethod?: X402AssetTransferMethod;
}

export type X402AssetTransferMethod = 'eip3009' | 'permit2';

/** A single offer and the settings every offer shares */
export interface X402ProviderOptions extends X402OfferOptions {
  readonly maxTimeoutSeconds?: number;
  /** 'authorization' (default) settles after a successful backend call; 'upfront' first */
  readonly paymentFlow?: X402PaymentFlow;
  /** Per-resource overrides of `paymentFlow`, keyed by resource id */
  readonly resourcePaymentFlows?: Readonly<Record<string, X402PaymentFlow>>;
  /** Each resource's challenge `resource.mimeType`; defaults to 'application/json' */
  readonly resourceMimeTypes?: Readonly<Record<string, string>>;
  /** Offers that replace the provider's for a resource, keyed by resource id */
  readonly resourceOffers?: Readonly<Record<string, readonly X402OfferOptions[]>>;
  readonly facilitator: X402FacilitatorConfig;
  /** Must be true before mainnet settlement. */
  readonly allowMainnet?: boolean;
  /** Must be true for an unauthenticated mainnet facilitator. */
  readonly allowUnauthenticatedFacilitator?: boolean;
  /** EVM networks outside the built-in registry; `network` may name one */
  readonly customNetworks?: Readonly<
    Record<string, { readonly kind: 'testnet' | 'mainnet'; readonly displayName: string }>
  >;
  /** Experimental: signs ERC-8409 quotes for EVM offers with a dedicated key when request data can be canonicalized */
  readonly quote?: { readonly signerPrivateKey: string };
  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 };

/** Several offers per paid resource, settled through one facilitator */
export interface X402MultiOfferProviderOptions
  extends Omit<X402ProviderOptions, keyof X402OfferOptions> {
  readonly offers: readonly X402OfferOptions[];
}

/** Experimental `quote`: present when the provider signs ERC-8409 quotes */
export interface X402PaymentProvider extends PaymentProvider {
  readonly quote?: {
    readonly issuer: `0x${string}`;
    readonly requestScheme: string; // 'agent-commerce/request/v1'
    readonly extension: string; // 'erc-8409', the PaymentRequired.extensions key
  };
}

export function createX402PaymentProvider(
  options: X402ProviderOptions | X402MultiOfferProviderOptions,
): X402PaymentProvider;

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';
  /** The local Anvil dev chain, where the in-process facilitator runs */
  readonly local?: boolean;
  /** Default USDC deployment and EIP-712 domain */
  readonly canonicalAsset?: {
    readonly symbol: string;
    readonly address: string;
    readonly name: string;
    readonly version: string;
    readonly decimals: number;
  };
}

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:31337' (local), 'eip155:84532' (default) or 'eip155:8453' */
  readonly network?: string;
  /** 'upfront' (default) settles before the backend; 'authorization' after it */
  readonly paymentFlow?: X402PaymentFlow;
  /** Per-resource overrides of `paymentFlow`, keyed by resource id */
  readonly resourcePaymentFlows?: Readonly<Record<string, X402PaymentFlow>>;
  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:31337 (local), 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 accepts either the full Authorization: Payment ... value, including its scheme, or a JSON string containing the credential object from MPP's MCP _meta carrier. PaymentChallenge.envelope contains wwwAuthenticate for the HTTP header and challenges for the MCP carrier. A settled result with a transaction reference carries the Payment-Receipt value in metadata.receipt.

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 requireMandateAudience?: boolean;
      readonly requireMandateExpiry?: boolean;
      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. runGateway 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;
    };

/** One resolved x402 option */
export interface X402OptionConfig {
  readonly network: string;
  readonly rpcUrl: string; // networks.<id>.rpcUrl, else the shared rpcUrl
  readonly asset: string;
  readonly assetName?: string; // always present for EIP-3009
  readonly assetVersion?: string;
  readonly assetDecimals: number;
  readonly payTo: string;
  readonly assetTransferMethod?: 'permit2'; // absent for EIP-3009
}

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; // always present for EIP-3009
      readonly assetVersion?: string;
      readonly assetDecimals: number;
      readonly payTo: string;
      readonly assetTransferMethod?: 'permit2'; // absent for EIP-3009
      readonly maxTimeoutSeconds: number;
      readonly paymentFlow?: X402PaymentFlow; // default 'authorization'
      readonly resourcePaymentFlows?: Readonly<Record<string, X402PaymentFlow>>;
      readonly resourceMimeTypes?: Readonly<Record<string, string>>; // resources that set `mimeType`
      readonly facilitator: X402FacilitatorConfig;
      readonly allowMainnet?: boolean;
      readonly allowUnauthenticatedFacilitator?: boolean;
      readonly networks?: X402ProviderOptions['customNetworks']; // declared EVM networks
      readonly options?: readonly X402OptionConfig[]; // the fields above mirror the first
      // Each resource's own offers, replacing `options` for it
      readonly resourceOptions?: Readonly<Record<string, readonly X402OptionConfig[]>>;
      // Experimental; present only when `payments.x402.quote.enabled` is true
      readonly quote?: { readonly signerPrivateKey: string };
    };
    readonly mpp?: {
      readonly enabled: boolean;
      readonly network: 'eip155:31337' | '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 paymentFlow?: X402PaymentFlow; // default 'upfront'
      readonly resourcePaymentFlows?: Readonly<Record<string, X402PaymentFlow>>;
      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. A delivery relays the backend's status, body and text Content-Type; see security.md.
GET /api/receipts?limit= Lists recent receipts behind the operator-token gate; undelivered=true filters for failed deliveries.
GET /api/events?limit= Lists recent events through the operator-token gate.
GET /api/payment-attempts Lists recent payment attempts behind the operator-token gate. limit= bounds the result size; uncertain=true selects settlement-uncertain and legacy failed attempts for reconciliation.
Adapter mounts and fixed routes Delegated to enabled protocol adapters, with collision checks and per-mount limits.

Without server.adminToken, the 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": 31337,
  "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. It signs only EIP-3009; another extra.assetTransferMethod yields INPUT_INVALID. Use the SDK's x402Client for Permit2.

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

runGateway in src/gateway/run.ts loads configuration, builds providers and adapters, starts the gateway, and closes the server, AP2 providers and receipt store on SIGTERM or SIGINT. src/gateway/main.ts and agent-commerce start both call it. It imports MCP, AP2 and payment rail implementations only when enabled. An enabled rail can share another rail's peers: MPP uses x402 for settlement even with payments.x402 disabled. createGateway accepts injected implementations and does not import the composition root.