src/core/public-types.ts is the frozen cross-area API, established at
v0.1.0-alpha. Change it only through this sequence:
- Record the use case, proposed change, alternatives, and compatibility impact.
- Approve the change.
- Update the canonical type, this record, and affected consumers.
- Re-baseline with
node scripts/contract-surface.mjs --write. - Run
npm run verify.
Do not resolve disagreements by creating duplicate types.
| 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.
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 |
- Each ingress path generates
CanonicalRequest.requestId; a client never chooses it. Over HTTP, a clientx-request-idmatching[A-Za-z0-9._:-]{1,64}is logged asclientRequestIdand nothing more. Records created for the request, including events, payment attempts, and receipts, reuse the generated ID. PaymentRequirement.challenge.acceptsis provider-native and opaque. Pass it through unchanged.- A payment provider derives
PaymentResult.replayKeyonly 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. PaymentProvider.verifydoes not move funds.settleruns only after verification returnsverifiedwith a non-emptyreplayKey, any required authorization is verified and reserved, andreservePaymentAttemptsucceeds. A provider that setsPaymentRequirement.metadata.settleAfterBackendtotruehassettlerun after a successful backend call instead of before it; theauthorizationflow of x402 and MPP does. A backend failure then settles nothing. A rejected or unconfirmed settlement withholds the response.metadata.reuseRejectedAuthorization: truelets the pipeline reserve a rejected attempt's authorization again withreclaimRejected. x402 sets it; MPP's single-use credentials cannot be reused.EventSink.emitand event persistence must not fail a commerce flow.HttpBackendExecutoris the only built-in outbound HTTP path to merchant backends and always applies a timeout. A customGatewayOptions.backendreplaces it and must enforce its own timeout.- Payment amounts are decimal strings in display units, such as
"0.01". Providers convert them to base units. If a settled result includesmetadata.amountBaseUnits, the HTTP route uses that value forPAYMENT-RESPONSE.amount. exactOptionalPropertyTypesis enabled. Add optional properties conditionally instead of assigningundefined.- Preserve
AuthorizationSubmission.payloadbyte-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. - Authorization providers must not use
PAYMENT_*codes. The pipeline passes a provider'sCommerceErrorthrough 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.
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.
- The initial surface was frozen at v0.1.0-alpha. UCP was later removed; current
protocol names are defined by
ProtocolNameandPROTOCOL_NAMES. toCommerceErrorno longer exposes an arbitraryError.message. It keeps the original error as the non-serializedcause.PaymentAttempt.statusgainedsettlement-uncertain, recorded whensettle()throws, with or without a transaction hash. A throw whosedetails.settlementNotSubmittedistruerecordsrejectedinstead and answersPAYMENT_PROVIDER_UNAVAILABLE: the provider knows nothing was submitted, so the authorization can be reserved again.- Optional
PaymentAttemptReservation.reclaimRejectedletsreservePaymentAttemptreclaim 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 customReceiptStorethat ignores the field still refuses these retries. GATEWAY_BUSYis a retryable 503 used for transient load shedding.ReceiptStoregainedcountReceiptsandcountUndeliveredReceipts; list results are capped and cannot provide reliable totals.ReceiptStore.listReceiptsacceptsReceiptListOptions;undelivered: truefilters for receipts counted bycountUndeliveredReceipts. Custom stores that ignore the flag also return delivered receipts.ReceiptStore.countUncertainPaymentAttemptsis required and countssettlement-uncertainand legacyfailedattempts. Uncertain settlements leave no receipt, socountUndeliveredReceiptscannot find them.listPaymentAttemptsacceptsPaymentAttemptListOptions;uncertain: truefilters for the same statuses. Custom stores that ignore the flag do not filter by settlement status.DeliverySummary,toDeliverySummary, andDELIVERY_SUMMARY_META_KEYexpose a buyer's settlement result. The key isagent-commerce/delivery.- x402 v2 changed the HTTP headers to
payment-signature,payment-required, andpayment-response. The v1x-payment*headers are not accepted. - An unexpected throw from the x402 SDK during
verify()maps toPAYMENT_PROVIDER_UNAVAILABLE, replacing the rejection reasonunexpected_verify_error. A throw carries no verdict, so it is not recorded against the payer. PaymentChallenge.envelopeandPaymentRequiredEnvelope.payment.envelopecarry the provider's challenge document unchanged.BackendHandler.inputBindingsoptionally 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 remainINPUT_INVALIDbefore pricing.CanonicalRequest.idempotencyKeyandBackendRequest.idempotencyKeyare optional. ACP derives a stable, operation-scoped value becauserequestIdchanges on retry. The HTTP backend sends it asIdempotency-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.backendHeadersandBackendRequest.backendHeadersare 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 derivedIdempotency-Keyand the executor'sContent-Type. The HTTP backend never forwardsAuthorization,Cookie,Set-Cookie,Host,Content-Length,Content-Type,Idempotency-Key,Connection,Keep-Alive,TE,Trailer,Transfer-Encoding,Upgradeor anyProxy-*header. An illegal forwarded name or value isINPUT_INVALIDbefore pricing. ACP is the only adapter that sets the field. A customBackendExecutorthat ignores it forwards nothing.PaymentProvider.selectRequirementoptionally selects a proof's offer beforeverify. It returns the selected destination, network, asset and metadata while preservingidand the fullchallenge. Verification, authorization, replay reservation and settlement use this requirement. Providers without the hook keep their existing path.- Optional
AdapterHealth.componentslistsAdapterHealthComponentresults with anameandstatus, such as one per x402 offer. Discovery may publish these fields; they contain no detail. Omission preserves existing reporting. PaymentRequirement.destinationandPaymentRequiredEnvelope.payment.destinationare 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 inaccepts. WithstrictNullChecks, callers passing eitherdestinationas a requiredstringmust now handleundefined.- Optional
PaymentContext.inputcarries validated resource input tocreateRequirement, 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.
-
The x402 provider accepts
X402MultiOfferProviderOptionswith one shared facilitator. Multi-offer challenges use the offer-independent URLresource://agent-commerce/resources/<id>.GatewayConfig.payments.x402.optionsholds resolved options; single-option fields mirror the first./.well-knownincludespayments.x402.optionsonly for multiple offers and omits RPC URLs. -
An x402 offer takes
assetTransferMethod(X402AssetTransferMethod,eip3009by default, orpermit2).X402ProviderOptionsnow extendsX402OfferOptions.assetNameandassetVersionbecame optional in both option types, inGatewayConfig.payments.x402and itsoptions, 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. WithstrictNullChecks, readers must handleundefined./.well-knownpublishesassetTransferMethod: permit2beside a Permit2 offer.- The provider no longer forwards a payload's
extensionsto the facilitator. createPaymentProofrefuses an offer naming a method other thaneip3009withINPUT_INVALID.- The invoke route returns 412 for a missing Permit2 allowance
(
permit2_allowance_required), preservingPAYMENT-REQUIREDas specified by theexactEVM scheme.
-
The x402 provider serves Solana offers (
solana:networks) through a remote facilitator.X402OfferOptions.assetandpayTowidened from0x${string}tostringto carry a Solana mint and wallet. Code reading either field as0x${string}must narrow it.- The descriptor's
supportedSpecnamesfamily=eip155,family=solanaor both; capabilities gainexact-svm, andunsupportedno longer listssvm. - 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_failedor NEAR'ssettlement_failed. Any other reason leaves the outcome unknown. A rejection keeps its transaction as the attempt'sexternalReference.
-
The x402 provider serves NEAR offers (
near:mainnet,near:testnet) through a remote facilitator.X402OfferOptions.assetandpayTocarry NEAR account IDs for a NEAR offer. The descriptor'ssupportedSpeccan namefamily=near, capabilities gainexact-near, andunsupportedno longer lists a chain family.@x402/nearis 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 withdetails.settlementNotSubmittedand the attempt is recordedrejected. createX402PaymentProviderrefuses amaxTimeoutSecondsthat is not a positive integer withCONFIG_INVALID, as config load already did: the NEAR facilitator refuses every payment under such a value.
-
x402 verification and settlement
currencyuses the network's default token symbol when paying that asset, otherwise the resource's currency. Settlement preserves a differently labeled price, such asUSD, inmetadata.quote(amount,currency). Payment attempts, receipts andPAYMENT-RESPONSEnow recordUSDCinstead of repeating aUSDquote. -
A replay error's
details.networknames the network of the offer it paid. -
/.well-knownpublishes each rail'spaymentFlow, andresourcePaymentFlowswhen a resource that lists the rail sets its own;WellKnownDocumentgains both fields for x402 and MPP. A rail'sresourcePaymentFlowsinGatewayConfigno longer includes resources that do not list that rail. -
A resource's
x402.acceptsoverrides its offers, matching the SDK's route-levelaccepts.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.resourceOptionsholds the resolved lists, and/.well-knownpublishes them aspayments.x402.resourceOptions.WellKnownX402Offernames the offer shape both lists share.
-
x402 health checks all offers, reporting
componentsnamed<network> <asset> <payTo>with lowercase EVM addresses. Any failing offer fails the provider./.well-knowncopies statuses intooptionsandresourceOptions;/readyis unchanged. -
Experimental ERC-8409 quotes on x402 EVM offers. Without
quote, challenges are unchanged.X402ProviderOptions.quoteenables signing.GatewayConfig.payments.x402.quoteis present only whenpayments.x402.quote.enabledis true.createX402PaymentProviderreturnsX402PaymentProvider, aPaymentProviderwhose optionalquotedescribes the issuer, request scheme and extension key. The descriptor gains the capabilityerc-8409-quotes./.well-knownpublishespayments.x402.quote(WellKnownX402Quote), read from the signing provider.
-
ProtocolNamegaineda2aandacp;PROTOCOL_NAMESis 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. -
AdapterHttpRouteandHttpProtocolAdapter.additionalHttpRouteslet 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-commerceno 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: falsemust declarebody, which a cancel carries when the caller sends one. Config load rejects one that does not; before, it loaded and refused every cancel carryingintent_tracewithINPUT_INVALID. -
WellKnownDocument.authorizationProviderslists authorization descriptors separately from payment providers. -
The operator SSE route
GET /api/events/streamwas removed.GET /api/eventsis the only event feed, and the demo dashboard polls it. -
The main entry exports the A2A adapter:
createA2aAdapterand its options type, plusA2A_SPEC_VERSION,A2A_PROTOCOL_VERSIONandA2A_AGENT_CARD_PATH. It needs no optional peer. -
CreatePaymentProofOptionsdropped the deprecatedrpcUrl, 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
mimeTypesets the media type its x402 challenges declare asresource.mimeType;application/jsonremains the default.X402ProviderOptions.resourceMimeTypesandGatewayConfig.payments.x402.resourceMimeTypescarry it by resource id. -
POST /api/resources/:id/invokedelivers the backend'sContent-Type, withcharset=utf-8, when it is a text type that fits the relayed body; text no longer always goes out astext/plain. Every text delivery also carriesContent-Security-Policy: sandboxandX-Content-Type-Options: nosniff.
- 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 touncertain; onlyreleasedmandates can be presented again, so an uncertain mandate cannot authorize a second charge. Receipt schema version 2 adds nullableauthorization_jsonwithALTER TABLE, preserving rows in existing databases. GatewayOptions.authorizationProvidersand readiness reporting are additive. A failing authorization provider blocks readiness./readyreplaces its raw detail withauthorization-provider-unreachable; thrown health errors are logged aterror, while returned details are logged atdebug.- The
./ap2entry exports the AP2 provider andcreateCheckoutJwt. 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_REASONSdroppedunsupported_algorithm. A digest algorithm other than sha-256 ismalformed_presentation, and a signing algorithm other than the pinned one isinvalid_signature.
PaymentMethodNamegainedmpp, andPAYMENT_METHOD_NAMESexposes payment names at runtime. This changes the frozen union but adds no MPP-specific wire fields to core.GatewayConfig.paymentsgained an optionalmppblock. A paid resource still needs at least one of its named rails enabled, so a resource that names onlymppneedspayments.mppenabled but no x402 block.- The
./mppentry exportscreateMppPaymentProvider(also asmpp) 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. MppProviderOptionsandGatewayConfig.payments.mppaccept optionalpaymentFlowandresourcePaymentFlowswith the x402 flow values.- MPP's default
paymentFlowisupfront; it wasauthorization. Thechargeintent 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 setpaymentFlow: authorization.
| 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.
The TypeScript declarations own the exact shapes. The excerpts below record the factory boundaries and constraints that consumers rely on.
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.
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.
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.
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.
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;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.
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.
| 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.
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.
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.
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.