@devlab.group/agent-commerce 0.1.0-alpha.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,14 +10,8 @@
10
10
  <img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-strict-3178c6">
11
11
  <img alt="MCP" src="https://img.shields.io/badge/MCP-supported-6b4fbb">
12
12
  <img alt="x402" src="https://img.shields.io/badge/x402-supported-0052ff">
13
- <img alt="Status" src="https://img.shields.io/badge/status-alpha-orange">
14
13
  </p>
15
14
 
16
- > **Alpha.** `v0.1.0-alpha` is experimental. Do not use it with production funds
17
- > without an independent review. See [SECURITY.md](SECURITY.md).
18
-
19
- ---
20
-
21
15
  ## What it is, in ten seconds
22
16
 
23
17
  You already have an HTTP API. AI agents want to **discover** it, **call** it and
@@ -30,7 +24,7 @@ wallet — the gateway never holds it, and never holds your keys.
30
24
 
31
25
  ```text
32
26
  Your existing API → Agent Commerce Gateway → AI Agent
33
- MCP · x402 · receipts · doctor
27
+ MCP · x402 · receipts · doctor
34
28
  ```
35
29
 
36
30
  ## Demo
@@ -87,24 +81,25 @@ const gateway = await createGateway({
87
81
  paymentProviders: [],
88
82
  protocolAdapters: [],
89
83
  });
90
- const { url } = await gateway.listen;
84
+ const { url } = await gateway.listen();
91
85
  ```
92
86
 
93
87
  ### Optional peers — install only the rails you use
94
88
 
95
89
  The MCP adapter and the x402 provider live on their own subpaths, because each
96
- needs a dependency the rest of the package does not. x402 alone pulls a browser
97
- wallet stack (wagmi, WalletConnect, Reown) worth ~572 MB, which a gateway
98
- serving a free HTTP resource has no business installing.
90
+ needs a dependency the rest of the package does not — the x402 rail brings the
91
+ whole EVM signing and RPC stack, which a gateway serving a free HTTP resource
92
+ has no business installing.
99
93
 
100
- | You want | Install | Import |
101
- | ------------------------------ | ------------------------------ | ------------------------------------------ |
102
- | gateway, config, receipts, CLI | `@devlab.group/agent-commerce` | `from '@devlab.group/agent-commerce'` |
103
- | expose resources as MCP tools | `+ @modelcontextprotocol/sdk` | `from '@devlab.group/agent-commerce/mcp'` |
104
- | accept x402 payments | `+ x402 viem` | `from '@devlab.group/agent-commerce/x402'` |
94
+ | You want | Install | Import |
95
+ | --------------------------------- | ------------------------------ | ------------------------------------------ |
96
+ | gateway, config, receipts, CLI | `@devlab.group/agent-commerce` | `from '@devlab.group/agent-commerce'` |
97
+ | expose resources as MCP tools | `+ @modelcontextprotocol/sdk` | `from '@devlab.group/agent-commerce/mcp'` |
98
+ | accept x402 payments | `+ @x402/core @x402/evm viem` | `from '@devlab.group/agent-commerce/x402'` |
99
+ | authenticate to a CDP facilitator | `+ @coinbase/x402` | (no import — loaded on demand) |
105
100
 
106
101
  ```bash
107
- npm install @devlab.group/agent-commerce @modelcontextprotocol/sdk x402 viem
102
+ npm install @devlab.group/agent-commerce @modelcontextprotocol/sdk @x402/core @x402/evm viem
108
103
  ```
109
104
 
110
105
  ```ts
@@ -112,12 +107,19 @@ import { mcp } from '@devlab.group/agent-commerce/mcp';
112
107
  import { x402 } from '@devlab.group/agent-commerce/x402';
113
108
  ```
114
109
 
115
- Peers are pinned exactly: x402's
116
- schemas and EIP-712 domains cross this boundary, so a version skew is a
117
- correctness problem rather than a convenience one. Import a subpath without its
110
+ Peers are pinned exactly: x402's schemas and EIP-712 domains cross this
111
+ boundary, so a version skew is a correctness problem rather than a convenience
112
+ one. Import a subpath without its
118
113
  peer installed and Node fails at load naming the missing package — deliberately,
119
114
  rather than starting a gateway that silently serves nothing.
120
115
 
116
+ `@coinbase/x402` is the odd one out: it has no import of its own and is loaded
117
+ dynamically, only when `facilitator.auth.type: cdp` is configured. It is worth
118
+ avoiding if you can — it brings `@coinbase/cdp-sdk` and `axios`, which carry
119
+ high-severity advisories, while the package itself and the other three peers
120
+ audit clean. `auth.type: bearer` covers any facilitator with a static token and
121
+ installs nothing.
122
+
121
123
  ## Quickstart
122
124
 
123
125
  Requirements: **Node >= 22**, **npm 10**, **Docker**. Nothing else — no API
@@ -154,7 +156,7 @@ To stop and wipe state: `docker compose down -v`.
154
156
  ┌──────────────────────────────────────────────────────┐
155
157
  │ AI Agent │
156
158
  └──────────────┬───────────────────────────────────────┘
157
- │ MCP · HTTP + X-PAYMENT
159
+ │ MCP · HTTP + PAYMENT-SIGNATURE
158
160
  ┌──────────────▼───────────────────────────────────────┐
159
161
  │ Agent Commerce Gateway (yours) │
160
162
  │ │
@@ -206,13 +208,13 @@ See [docs/configuration.md](docs/configuration.md).
206
208
 
207
209
  ## Protocol support
208
210
 
209
- | Protocol | Status | Pinned revision |
210
- | --------------------- | --------- | ---------------------------------- |
211
- | **MCP** | Supported | `@modelcontextprotocol/sdk@1.30.0` |
212
- | **x402** | Supported | `x402@1.2.0`, scheme `exact`, EVM |
213
- | **HTTP** | Supported | native routes |
214
- | UCP | Planned | — |
215
- | ACP · MPP · A2A · AP2 | Planned | — |
211
+ | Protocol | Status | Pinned revision |
212
+ | --------------------- | --------- | -------------------------------------------------------- |
213
+ | **MCP** | Supported | `@modelcontextprotocol/sdk@1.30.0` |
214
+ | **x402** | Supported | x402 v2 (`@x402/core`, `@x402/evm`), scheme `exact`, EVM |
215
+ | **HTTP** | Supported | native routes |
216
+ | UCP | Planned | — |
217
+ | ACP · MPP · A2A · AP2 | Planned | — |
216
218
 
217
219
  "Planned" means **no code ships for it**. Each adapter reports its own
218
220
  `supportedSpec`, `capabilities` and `unsupported` list at runtime via
@@ -235,19 +237,123 @@ checkable, not marketing. Detail: [docs/protocols.md](docs/protocols.md).
235
237
 
236
238
  Detail: [docs/payment-flow.md](docs/payment-flow.md).
237
239
 
240
+ ## Public networks
241
+
242
+ Same gateway, same pipeline — a different `network` and a facilitator that is
243
+ not this process. No code changes, and no "live mode" to switch on.
244
+
245
+ ### It has actually settled
246
+
247
+ Not a roadmap entry. Both of these moved 0.01 USDC from a buyer to a merchant
248
+ through a remote facilitator:
249
+
250
+ | Network | Transaction |
251
+ | ------------ | --------------------------------------------------------------------------------------------------------------------- |
252
+ | Base Sepolia | [`0xea41b234c4…`](https://sepolia.basescan.org/tx/0xea41b234c4645a4d335589ec9753646aa7cccd1b97e9e15823b88bff7b54a247) |
253
+ | Base | [`0x57ec81c2a3…`](https://basescan.org/tx/0x57ec81c2a360d14d59a43cf4e24be09a6bd75cbe6185016372895bda73e42763) |
254
+
255
+ In both, the gateway held no key, signed nothing and paid no gas — the buyer
256
+ signed an EIP-3009 authorisation offline holding no ETH, and the facilitator
257
+ broadcast it. Each run reads the buyer and merchant balances and the
258
+ transaction receipt back off the chain afterwards; the gateway's own report of
259
+ success is not the proof.
260
+
261
+ Reproduce with `npm run test:testnet` / `npm run test:mainnet` — both spend
262
+ real funds, skip themselves without credentials, and never run in CI.
263
+
264
+ ### The facilitator model
265
+
266
+ A **facilitator** verifies the buyer's authorisation and broadcasts the
267
+ transfer. It is the only component that needs gas, and it is never this
268
+ gateway on a public network.
269
+
270
+ | `facilitator.mode` | Who signs | Where it is allowed |
271
+ | ------------------ | ----------------------------------- | ------------------------ |
272
+ | `local` | this process, with an Anvil dev key | the local dev chain only |
273
+ | `remote` | an HTTP facilitator you point at | anywhere |
274
+
275
+ With `remote`, the gateway holds **no signing key at all**. The buyer signs an
276
+ EIP-3009 authorisation offline — no ETH required — and the facilitator pays the
277
+ gas. A facilitator cannot redirect your money: the authorisation names its
278
+ recipient, amount and chain, so it can broadcast exactly that transfer or
279
+ nothing. What it can do is see every authorisation you handle, and stop
280
+ answering.
281
+
282
+ Three auth types: `none`, `bearer` (a static token, installs nothing) and `cdp`
283
+ (Coinbase Developer Platform, which signs a fresh JWT per request). Anything
284
+ else is refused at config load rather than sent nothing. You can also run your
285
+ own — `remote` does not care who operates the endpoint.
286
+
287
+ ### Base Sepolia
288
+
289
+ ```yaml
290
+ payments:
291
+ x402:
292
+ enabled: true
293
+ network: eip155:84532
294
+ rpcUrl: https://base-sepolia-rpc.publicnode.com # health checks only
295
+ asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e" # Circle USDC
296
+ assetName: USDC
297
+ assetVersion: "2"
298
+ assetDecimals: 6
299
+ payTo: ${MERCHANT_WALLET}
300
+ maxTimeoutSeconds: 300
301
+ facilitator:
302
+ mode: remote
303
+ url: https://x402.org/facilitator
304
+ auth: { type: none }
305
+ ```
306
+
307
+ Full config in [`examples/base-sepolia/`](examples/base-sepolia/). Test USDC
308
+ from [faucet.circle.com](https://faucet.circle.com); the buyer needs no ETH.
309
+ `npm run test:testnet` drives the whole flow and reads the result back off the
310
+ chain.
311
+
312
+ Chain id 84532 belongs to **both** Base Sepolia and this project's local dev
313
+ chain, deliberately. Nothing infers "public network" from it — `local`,
314
+ `testnet` and `mainnet` are derived from the network *and* the facilitator
315
+ together, and reported by `doctor`, `health()` and `/.well-known`.
316
+
317
+ ### Base mainnet
318
+
319
+ Real funds, so nothing is defaulted. Every one of these is checked at config
320
+ load, and the gateway will not start without them:
321
+
322
+ | Required | |
323
+ | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
324
+ | `allowMainnet: true` | mainnet is never a default |
325
+ | `facilitator.mode: remote` | `local` needs a funded gas key inside this process |
326
+ | an HTTPS `facilitator.url` | |
327
+ | `allowUnauthenticatedFacilitator: true` | only if that facilitator takes no credential |
328
+ | a non-development `payTo` | |
329
+ | `asset` = USDC on Base, `assetName: "USD Coin"` | **not** `"USDC"` — that deployment predates the rename, and the buyer signs the name into their EIP-712 domain |
330
+
331
+ Full config in [`examples/base-mainnet/`](examples/base-mainnet/), and
332
+ [`examples/base-mainnet-payai/`](examples/base-mainnet-payai/) for an
333
+ unauthenticated facilitator. `npm run test:mainnet` proves it end to end and
334
+ spends real USDC on every run.
335
+
336
+ `agent-commerce validate` reports any of the above before anything starts, and
337
+ `doctor` prints `LIVE MAINNET MODE — REAL FUNDS`.
338
+
339
+ > Neither public-network suite runs in CI — there is no workflow and there must
340
+ > not be one. A workflow means a funded key in repository secrets, spendable by
341
+ > anyone with write access. Both suites run from the machine that holds the
342
+ > wallet, and skip themselves without credentials.
343
+
238
344
  ## Diagnostics
239
345
 
240
346
  ```console
241
347
  $ npm run agent-commerce -- doctor --config config-demo.yaml
242
348
 
243
- PASS Config valid — 2 resource(s), merchant "Demo Data Store"
244
- PASS Gateway healthy and ready at http://127.0.0.1:8080
245
- PASS Backend 2/2 backend host(s) reachable
246
- PASS Protocols http=on mcp=on (/mcp)
247
- PASS Payments x402 enabled — network=base-sepolia, destination=0x7099…79C8, facilitator=local
248
- INFO Payments (MPP) planned — not implemented in v0.1
249
- PASS Storage sqlite schema v1 writable; receipts=2
250
- PASS Protocol versions reported by gateway /.well-known/agent-commerce
349
+ PASS Config valid — 2 resource(s), merchant "Demo Data Store" (using local chain manifest .deploy/local.json for X402_ASSET, X402_ASSET_NAME, X402_ASSET_VERSION, X402_ASSET_DECIMALS, MERCHANT_WALLET, X402_FACILITATOR_PRIVATE_KEY)
350
+ PASS Gateway healthy and ready at http://127.0.0.1:8080
351
+ PASS Backend 2/2 backend host(s) reachable
352
+ PASS Protocols http=on mcp=on (/mcp)
353
+ PASS Payments x402 v2 (scheme=exact) enabled — LOCAL dev chain (eip155:84532, chain id shared with Base Sepolia), destination=0x7099…79C8, facilitator=local
354
+ INFO Payments (MPP) planned — not implemented in this release
355
+ PASS Storage sqlite schema v1 writable; receipts=2
356
+ PASS Protocol versions reported by gateway /.well-known/agent-commerce
251
357
 
252
358
  Score: 7/7 checks passed
253
359
  ```
@@ -277,15 +383,6 @@ reachable by anyone else, know the split:
277
383
 
278
384
  [SECURITY.md](SECURITY.md) states plainly what this does and does not protect.
279
385
 
280
- ## Live settlement — not in this release
281
-
282
- **v0.1.0-alpha settles only against the local deterministic chain** (Anvil +
283
- MockUSDC). There is no live mode, no flag to enable one, and no partial path
284
- toward one: `facilitator.mode: "remote"` is rejected at config load, and the
285
- x402 provider's health check requires an Anvil-only RPC method, so `/ready`
286
- returns 503 against a real network. Settling real value is **planned**, not
287
- shipped — see [docs/payment-flow.md](docs/payment-flow.md).
288
-
289
386
  ## Development
290
387
 
291
388
  ```bash
@@ -298,7 +395,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
298
395
 
299
396
  ## Roadmap
300
397
 
301
- **Now (v0.1.0-alpha)** — MCP, x402, receipts, doctor, deterministic demo.
398
+ **Now (v1.0.0)** — MCP, x402 v2, settlement on the local chain, Base Sepolia
399
+ and Base mainnet, receipts, doctor, deterministic demo.
302
400
 
303
401
  **Next** — OpenAPI import · a stronger conformance suite · a `doctor` GitHub
304
402
  Action · UCP · MPP · ACP · A2A · AP2 · Shopify and WooCommerce examples ·
@@ -18,8 +18,9 @@ var DEFAULT_BACKEND_TIMEOUT_MS = 1e4;
18
18
 
19
19
  // src/core/domain/wire.ts
20
20
  var PAYMENT_INPUT_FIELD = "_payment";
21
- var PAYMENT_HEADER = "x-payment";
22
- var PAYMENT_RESPONSE_HEADER = "x-payment-response";
21
+ var PAYMENT_HEADER = "payment-signature";
22
+ var PAYMENT_RESPONSE_HEADER = "payment-response";
23
+ var PAYMENT_REQUIRED_HEADER = "payment-required";
23
24
  var DELIVERY_SUMMARY_META_KEY = "agent-commerce/delivery";
24
25
  function toDeliverySummary(outcome) {
25
26
  const p = outcome.payment;
@@ -56,7 +57,8 @@ function toPaymentRequiredEnvelope(outcome) {
56
57
  ...r.network !== void 0 ? { network: r.network } : {},
57
58
  ...r.asset !== void 0 ? { asset: r.asset } : {},
58
59
  ...r.expiresAt !== void 0 ? { expiresAt: r.expiresAt } : {},
59
- accepts: r.challenge.accepts
60
+ accepts: r.challenge.accepts,
61
+ ...r.challenge.envelope !== void 0 ? { envelope: r.challenge.envelope } : {}
60
62
  }
61
63
  };
62
64
  }
@@ -185,8 +187,8 @@ var systemClock = {
185
187
  nowIso: () => (/* @__PURE__ */ new Date()).toISOString(),
186
188
  monotonicMs: () => performance.now()
187
189
  };
188
- var PACKAGE_VERSION = "0.1.0-alpha.0" ;
190
+ var PACKAGE_VERSION = "1.0.0" ;
189
191
 
190
- export { COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, NOOP_LOGGER, PACKAGE_VERSION, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_RESPONSE_HEADER, RETRYABLE_ERROR_CODES, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope };
191
- //# sourceMappingURL=chunk-CZ6ZDAYD.js.map
192
- //# sourceMappingURL=chunk-CZ6ZDAYD.js.map
192
+ export { COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, NOOP_LOGGER, PACKAGE_VERSION, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, RETRYABLE_ERROR_CODES, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope };
193
+ //# sourceMappingURL=chunk-BQNE3A5H.js.map
194
+ //# sourceMappingURL=chunk-BQNE3A5H.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/domain/event.ts","../src/core/domain/resource.ts","../src/core/domain/wire.ts","../src/core/errors/codes.ts","../src/core/errors/commerce-error.ts","../src/core/interfaces/logger.ts","../src/core/interfaces/protocol-adapter.ts","../src/core/interfaces/runtime.ts","../src/version.ts"],"names":[],"mappings":"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","file":"chunk-BQNE3A5H.js","sourcesContent":["/**\n * Canonical commerce events.\n *\n * FROZEN CONTRACT. Every event in a single flow shares one `requestId` so that\n * logs, payment attempts, backend calls and receipts are correlatable.\n */\nimport type { IsoTimestamp } from './common.js';\n\nexport const COMMERCE_EVENT_TYPES = [\n 'resource.discovered',\n 'resource.requested',\n 'payment.required',\n 'payment.rejected',\n 'payment.verified',\n 'payment.settled',\n 'backend.called',\n 'backend.failed',\n 'resource.delivered',\n] as const;\n\nexport type CommerceEventType = (typeof COMMERCE_EVENT_TYPES)[number];\n\nexport interface CommerceEvent {\n readonly id: string;\n readonly type: CommerceEventType;\n readonly requestId: string;\n readonly resourceId?: string;\n readonly at: IsoTimestamp;\n /** Protocol adapter that originated the request, e.g. 'mcp' or 'http'. */\n readonly adapter?: string;\n readonly paymentProvider?: string;\n readonly durationMs?: number;\n readonly status?: 'ok' | 'error';\n /** Non-secret, structured detail. Must never contain keys, headers or proofs. */\n readonly data?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Sink for canonical events.\n *\n * Implementations must be non-throwing from the caller's perspective: an event\n * sink failure must never fail an otherwise successful commerce flow.\n */\nexport interface EventSink {\n emit(event: CommerceEvent): Promise<void>;\n}\n","/**\n * Canonical resource model.\n *\n * FROZEN CONTRACT.\n */\nimport type { DecimalAmount, JsonSchema, PaymentMethodName, ProtocolName } from './common.js';\n\n/** HTTP methods a backend handler may use. */\nexport type BackendMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n\n/**\n * Describes how the gateway calls the merchant's existing backend.\n *\n * Backend URLs are administrator-controlled configuration only. User- or\n * agent-controlled backend URLs are forbidden (see docs/security.md, SSRF).\n * Secret header values must come from `${ENV_VAR}` placeholders resolved by\n * the config loader, never from plaintext configuration.\n */\nexport interface BackendHandler {\n readonly type: 'http';\n readonly method: BackendMethod;\n readonly url: string;\n readonly headers?: Readonly<Record<string, string>>;\n /** Hard upper bound on the backend call. Defaults to DEFAULT_BACKEND_TIMEOUT_MS. */\n readonly timeoutMs?: number;\n}\n\n/** Default backend timeout when a resource does not specify one. */\nexport const DEFAULT_BACKEND_TIMEOUT_MS = 10_000;\n\n/**\n * Canonical pricing.\n *\n * `dynamic` exists in the type system for forward compatibility but is\n * REJECTED by config validation in this release.\n */\nexport type Pricing =\n | { readonly type: 'free' }\n | {\n readonly type: 'fixed';\n readonly amount: DecimalAmount;\n readonly currency: string;\n }\n | {\n readonly type: 'dynamic';\n readonly resolver: string;\n };\n\n/**\n * A merchant capability exposed to agents.\n *\n * The canonical resource is the single source of truth: protocol adapters map\n * it outward, they never define their own parallel notion of a resource.\n */\nexport interface CommerceResource {\n readonly id: string;\n readonly name: string;\n readonly description?: string;\n readonly inputSchema?: JsonSchema;\n readonly outputSchema?: JsonSchema;\n readonly handler: BackendHandler;\n readonly pricing: Pricing;\n readonly exposedVia: readonly ProtocolName[];\n readonly paymentMethods: readonly PaymentMethodName[];\n}\n\n/** Read-only view of every configured resource. */\nexport interface ResourceRegistry {\n get(id: string): CommerceResource | undefined;\n list(): readonly CommerceResource[];\n /** Resources exposed through a given protocol surface. */\n listExposedVia(protocol: ProtocolName): readonly CommerceResource[];\n has(id: string): boolean;\n}\n","/**\n * Gateway-defined wire envelopes.\n *\n * FROZEN CONTRACT. These are the *only* representations of \"payment required\"\n * and \"error\" that leave the gateway, whichever protocol carries them. The HTTP\n * routes, the MCP adapter and the demo buyer agent all use these helpers so the\n * three can never drift apart.\n */\n\nimport type { CommerceError, CommerceErrorCode } from '../errors/index.js';\nimport type { DecimalAmount, IsoTimestamp, PaymentMethodName } from './common.js';\nimport type { DeliveredOutcome, PaymentRequiredOutcome } from './request.js';\n\n/**\n * Reserved input property carrying a payment proof on protocols that have no\n * header channel (MCP tool calls). Over HTTP the `PAYMENT-SIGNATURE` header is\n * used.\n */\nexport const PAYMENT_INPUT_FIELD = '_payment';\n\n/**\n * HTTP request header carrying the payment proof.\n *\n * x402 v2 header names, lowercased because that is how Node presents incoming\n * headers. v1's `X-PAYMENT` / `X-PAYMENT-RESPONSE` pair is not accepted: this\n * gateway speaks one protocol version, and quietly honouring both would mean\n * two verification paths for the same money.\n */\nexport const PAYMENT_HEADER = 'payment-signature';\n\n/** HTTP response header carrying the settlement result. */\nexport const PAYMENT_RESPONSE_HEADER = 'payment-response';\n\n/**\n * HTTP response header carrying the base64 payment challenge on a 402.\n *\n * The 402 body still carries {@link PaymentRequiredEnvelope} — richer, and the\n * only channel MCP has — but an x402 v2 client reads the challenge from this\n * header and ignores the body, so both are sent.\n */\nexport const PAYMENT_REQUIRED_HEADER = 'payment-required';\n\n/**\n * Key under which a protocol adapter attaches a {@link DeliverySummary} to its\n * result metadata — MCP's `CallToolResult._meta`, and anywhere else a protocol\n * offers an out-of-band metadata channel.\n *\n * Frozen deliberately. The bug this whole mechanism exists to fix was two\n * surfaces disagreeing about what a payer gets back; a key name enforced only\n * by one side's tests would reintroduce exactly that drift. Producer and\n * consumer both import this constant.\n *\n * Follows MCP's `<namespace>/<name>` convention for `_meta` keys.\n */\nexport const DELIVERY_SUMMARY_META_KEY = 'agent-commerce/delivery';\n\nexport interface PaymentRequiredEnvelope {\n readonly status: 'payment-required';\n readonly code: 'PAYMENT_REQUIRED';\n readonly requestId: string;\n readonly resourceId: string;\n readonly message: string;\n readonly payment: {\n readonly provider: PaymentMethodName;\n readonly version: string;\n readonly amount: DecimalAmount;\n readonly currency: string;\n readonly destination: string;\n readonly network?: string;\n readonly asset?: string;\n readonly expiresAt?: IsoTimestamp;\n /** Provider-native requirement objects, passed through verbatim. */\n readonly accepts: readonly Readonly<Record<string, unknown>>[];\n /**\n * The provider's own challenge document, verbatim — see\n * {@link PaymentChallenge.envelope}. Present for providers that have one;\n * a buyer's protocol client can hand this straight to its SDK.\n */\n readonly envelope?: Readonly<Record<string, unknown>>;\n };\n}\n\nexport interface ErrorEnvelope {\n readonly status: 'error';\n readonly code: CommerceErrorCode;\n readonly message: string;\n readonly retryable: boolean;\n readonly requestId?: string;\n readonly resourceId?: string;\n readonly details?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * What a buyer learns about their own completed purchase.\n *\n * A payer is entitled to the record of their own transaction — the request it\n * belonged to, whether it settled, and the settlement reference they can check\n * on-chain. They are NOT entitled to the merchant's ledger, which is why\n * `/api/receipts` is an operator route behind `server.adminToken`.\n *\n * Before this existed the two surfaces disagreed: HTTP callers received a\n * settlement summary in the payment-response header while MCP callers received\n * nothing, so an MCP buyer's only route to their own receipt was the merchant's\n * ledger. Both surfaces now emit this same shape.\n */\nexport interface DeliverySummary {\n readonly requestId: string;\n readonly resourceId: string;\n readonly receiptId: string;\n readonly deliveredAt: IsoTimestamp;\n readonly payment?: {\n readonly status: 'verified' | 'settled' | 'rejected';\n readonly amount: DecimalAmount;\n readonly currency: string;\n /** Settlement reference, e.g. an on-chain transaction hash. */\n readonly externalReference?: string;\n readonly network?: string;\n };\n}\n\n/** Build the payer-facing summary of a delivered outcome. */\nexport function toDeliverySummary(outcome: DeliveredOutcome): DeliverySummary {\n const p = outcome.payment;\n return {\n requestId: outcome.requestId,\n resourceId: outcome.resourceId,\n receiptId: outcome.receipt.id,\n deliveredAt: outcome.receipt.deliveredAt,\n ...(p !== undefined\n ? {\n payment: {\n status: p.status,\n amount: p.amount,\n currency: p.currency,\n ...(p.externalReference !== undefined\n ? { externalReference: p.externalReference }\n : {}),\n ...(p.network !== undefined ? { network: p.network } : {}),\n },\n }\n : {}),\n };\n}\n\nexport function toPaymentRequiredEnvelope(\n outcome: PaymentRequiredOutcome,\n): PaymentRequiredEnvelope {\n const r = outcome.requirement;\n return {\n status: 'payment-required',\n code: 'PAYMENT_REQUIRED',\n requestId: outcome.requestId,\n resourceId: outcome.resourceId,\n message: `Payment of ${r.amount} ${r.currency} is required for resource \"${outcome.resourceId}\". Retry with a ${r.provider} payment proof.`,\n payment: {\n provider: r.provider,\n version: r.challenge.version,\n amount: r.amount,\n currency: r.currency,\n destination: r.destination,\n ...(r.network !== undefined ? { network: r.network } : {}),\n ...(r.asset !== undefined ? { asset: r.asset } : {}),\n ...(r.expiresAt !== undefined ? { expiresAt: r.expiresAt } : {}),\n accepts: r.challenge.accepts,\n ...(r.challenge.envelope !== undefined ? { envelope: r.challenge.envelope } : {}),\n },\n };\n}\n\nexport function toErrorEnvelope(error: CommerceError): ErrorEnvelope {\n const info = error.toInfo();\n return {\n status: 'error',\n code: info.code,\n message: info.message,\n retryable: info.retryable,\n ...(info.requestId !== undefined ? { requestId: info.requestId } : {}),\n ...(info.resourceId !== undefined ? { resourceId: info.resourceId } : {}),\n ...(info.details !== undefined ? { details: info.details } : {}),\n };\n}\n\nexport function isPaymentRequiredEnvelope(value: unknown): value is PaymentRequiredEnvelope {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as PaymentRequiredEnvelope).status === 'payment-required' &&\n typeof (value as PaymentRequiredEnvelope).payment === 'object'\n );\n}\n","/**\n * Typed domain error codes.\n *\n * FROZEN CONTRACT. Protocol adapters map these codes into their own response\n * format. Never throw a bare string or untyped Error across a package boundary.\n */\nexport const COMMERCE_ERROR_CODES = [\n 'CONFIG_INVALID',\n 'RESOURCE_NOT_FOUND',\n 'INPUT_INVALID',\n 'PAYMENT_REQUIRED',\n 'PAYMENT_INVALID',\n 'PAYMENT_REPLAYED',\n 'PAYMENT_PROVIDER_UNAVAILABLE',\n 'PAYMENT_SETTLEMENT_FAILED',\n 'BACKEND_TIMEOUT',\n 'BACKEND_ERROR',\n 'PROTOCOL_UNSUPPORTED',\n 'GATEWAY_BUSY',\n 'STORAGE_ERROR',\n 'INTERNAL_ERROR',\n] as const;\n\nexport type CommerceErrorCode = (typeof COMMERCE_ERROR_CODES)[number];\n\n/**\n * Canonical HTTP status for each error code.\n *\n * Paid resources fail closed: every payment failure maps to a 4xx/5xx status\n * and never to a successful delivery.\n */\nexport const COMMERCE_ERROR_HTTP_STATUS: Readonly<Record<CommerceErrorCode, number>> = {\n CONFIG_INVALID: 500,\n RESOURCE_NOT_FOUND: 404,\n INPUT_INVALID: 400,\n PAYMENT_REQUIRED: 402,\n PAYMENT_INVALID: 402,\n PAYMENT_REPLAYED: 409,\n PAYMENT_PROVIDER_UNAVAILABLE: 503,\n PAYMENT_SETTLEMENT_FAILED: 502,\n BACKEND_TIMEOUT: 504,\n BACKEND_ERROR: 502,\n PROTOCOL_UNSUPPORTED: 501,\n GATEWAY_BUSY: 503,\n STORAGE_ERROR: 500,\n INTERNAL_ERROR: 500,\n};\n\n/** Codes for which a client may reasonably retry the same request. */\nexport const RETRYABLE_ERROR_CODES: ReadonlySet<CommerceErrorCode> = new Set([\n 'PAYMENT_PROVIDER_UNAVAILABLE',\n 'BACKEND_TIMEOUT',\n // Load shedding is transient by definition: the caller should back off and\n // try again. Before this code existed the MCP adapter's queue-full path threw\n // PROTOCOL_UNSUPPORTED — HTTP 501, retryable:false — so an agent framework\n // keyed on the machine-readable flag (which exists precisely so clients need\n // not parse prose) would read a momentary throttle as a permanent capability\n // gap and stop calling the tool.\n 'GATEWAY_BUSY',\n]);\n","/**\n * The single error type crossing package boundaries.\n *\n * FROZEN CONTRACT.\n */\nimport {\n COMMERCE_ERROR_HTTP_STATUS,\n type CommerceErrorCode,\n RETRYABLE_ERROR_CODES,\n} from './codes.js';\n\nexport interface CommerceErrorOptions {\n /** Structured, non-secret detail safe to return to a client. */\n readonly details?: Readonly<Record<string, unknown>>;\n readonly cause?: unknown;\n readonly requestId?: string;\n readonly resourceId?: string;\n /** Override the default HTTP status for this code. */\n readonly httpStatus?: number;\n}\n\n/** Serialisable wire form of a CommerceError. */\nexport interface CommerceErrorInfo {\n readonly code: CommerceErrorCode;\n readonly message: string;\n readonly httpStatus: number;\n readonly retryable: boolean;\n readonly requestId?: string;\n readonly resourceId?: string;\n readonly details?: Readonly<Record<string, unknown>>;\n}\n\nexport class CommerceError extends Error {\n readonly code: CommerceErrorCode;\n readonly httpStatus: number;\n readonly retryable: boolean;\n readonly details?: Readonly<Record<string, unknown>>;\n readonly requestId?: string;\n readonly resourceId?: string;\n\n constructor(code: CommerceErrorCode, message: string, options: CommerceErrorOptions = {}) {\n super(message, options.cause !== undefined ? { cause: options.cause } : undefined);\n this.name = 'CommerceError';\n this.code = code;\n this.httpStatus = options.httpStatus ?? COMMERCE_ERROR_HTTP_STATUS[code];\n this.retryable = RETRYABLE_ERROR_CODES.has(code);\n if (options.details !== undefined) this.details = options.details;\n if (options.requestId !== undefined) this.requestId = options.requestId;\n if (options.resourceId !== undefined) this.resourceId = options.resourceId;\n Error.captureStackTrace?.(this, CommerceError);\n }\n\n /** Non-secret, client-safe representation. */\n toInfo(): CommerceErrorInfo {\n return {\n code: this.code,\n message: this.message,\n httpStatus: this.httpStatus,\n retryable: this.retryable,\n ...(this.requestId !== undefined ? { requestId: this.requestId } : {}),\n ...(this.resourceId !== undefined ? { resourceId: this.resourceId } : {}),\n ...(this.details !== undefined ? { details: this.details } : {}),\n };\n }\n}\n\nexport function isCommerceError(value: unknown): value is CommerceError {\n return value instanceof CommerceError;\n}\n\n/**\n * Normalise any thrown value into a CommerceError.\n *\n * Used at package boundaries so an unexpected failure never escapes untyped.\n *\n * SECURITY — do not \"improve\" this by propagating `value.message`.\n * `CommerceError.message` is client-visible: it is serialised by `toInfo()`\n * and `toErrorEnvelope()` and returned over HTTP and MCP. An arbitrary thrown\n * Error's message routinely carries internal detail — database connection\n * strings, internal hostnames, filesystem paths, backend URLs. Copying it into\n * a client-facing field turns every unexpected exception into an information\n * disclosure.\n *\n * The original value is preserved on `cause`, which is never serialised, so\n * logs and diagnostics keep the full detail while clients get only the\n * caller's deliberate, reviewed `fallbackMessage`.\n *\n * A caller that *wants* a specific client-facing message passes one:\n * toCommerceError(err, 'BACKEND_ERROR', 'Backend call failed')\n */\nexport function toCommerceError(\n value: unknown,\n fallbackCode: CommerceErrorCode = 'INTERNAL_ERROR',\n fallbackMessage = 'Unexpected internal error',\n): CommerceError {\n if (isCommerceError(value)) return value;\n return new CommerceError(fallbackCode, fallbackMessage, { cause: value });\n}\n","/**\n * Minimal structural logger interface.\n *\n * Core must not depend on pino directly; the gateway injects a pino instance\n * that satisfies this shape. Never log secrets: private keys, seed phrases,\n * Authorization headers or payment authorisation payloads.\n */\nexport interface Logger {\n debug(obj: Record<string, unknown>, msg?: string): void;\n info(obj: Record<string, unknown>, msg?: string): void;\n warn(obj: Record<string, unknown>, msg?: string): void;\n error(obj: Record<string, unknown>, msg?: string): void;\n child(bindings: Record<string, unknown>): Logger;\n}\n\n/** A logger that discards everything. Useful in unit tests. */\nexport const NOOP_LOGGER: Logger = {\n debug: () => {},\n info: () => {},\n warn: () => {},\n error: () => {},\n child: () => NOOP_LOGGER,\n};\n","/**\n * Protocol adapter boundary.\n *\n * FROZEN CONTRACT. An adapter translates a protocol's wire format into a\n * `CanonicalRequest`, hands it to the execution pipeline and maps the outcome\n * back. It must not call merchant backends, implement payment logic, or hold\n * its own copy of the canonical model.\n *\n * Adapter isolation: a failure while starting or serving one adapter must not\n * prevent the others from operating.\n */\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { AdapterDescriptor, AdapterHealth, ProtocolName } from '../domain/common.js';\nimport type { EventSink } from '../domain/event.js';\nimport type { ExecutionPipeline } from '../domain/request.js';\nimport type { ResourceRegistry } from '../domain/resource.js';\nimport type { Logger } from './logger.js';\nimport type { Clock, IdGenerator } from './runtime.js';\n\n/** Everything an adapter is given. Nothing else is available to it. */\nexport interface ProtocolAdapterContext {\n readonly pipeline: ExecutionPipeline;\n readonly resources: ResourceRegistry;\n readonly events: EventSink;\n readonly logger: Logger;\n readonly clock: Clock;\n readonly ids: IdGenerator;\n /** Externally reachable base URL of the gateway, from config. */\n readonly publicBaseUrl: string;\n}\n\nexport interface ProtocolAdapter {\n readonly name: ProtocolName;\n readonly descriptor: AdapterDescriptor;\n\n start(context: ProtocolAdapterContext): Promise<void>;\n health(): Promise<AdapterHealth>;\n stop(): Promise<void>;\n}\n\n/**\n * A protocol adapter served over HTTP.\n *\n * The gateway mounts `handleHttp` at `mountPath` using raw Node request and\n * response objects, so adapters stay independent of the HTTP framework.\n *\n * GUARANTEE — the request body is **unconsumed**.\n *\n * `req` is handed over with its stream unread: the gateway suppresses every\n * body parser on this mount, so an adapter whose SDK reads the body itself\n * (MCP's Streamable HTTP transport does) can do so. The adapter owns reading,\n * size-limiting beyond the gateway's own cap, and parsing.\n *\n * This is load-bearing and easy to break silently. Fastify pre-registers\n * exact-match parsers for `application/json` and `text/plain` and prefers them\n * over a `'*'` wildcard, so registering only a wildcard no-op leaves the\n * built-in JSON parser draining the body first — the adapter then reads an\n * empty stream and every request fails to parse. That exact regression shipped\n * once and no unit test caught it, because adapter conformance suites drive\n * `handleHttp` over a bare `node:http` server and never traverse the gateway.\n *\n * Any change to how adapters are mounted must be covered by a test that POSTs\n * a real JSON body through the real gateway and asserts the adapter received\n * it intact.\n */\nexport interface HttpProtocolAdapter extends ProtocolAdapter {\n /** Path prefix the gateway mounts this adapter at, e.g. '/mcp'. */\n readonly mountPath: string;\n handleHttp(req: IncomingMessage, res: ServerResponse): Promise<void>;\n}\n\nexport function isHttpProtocolAdapter(adapter: ProtocolAdapter): adapter is HttpProtocolAdapter {\n return typeof (adapter as HttpProtocolAdapter).handleHttp === 'function';\n}\n","/**\n * Injected runtime primitives.\n *\n * Everything non-deterministic (time, ids) is injected so tests can be exact.\n */\nimport type { IsoTimestamp } from '../domain/common.js';\n\nexport interface Clock {\n now(): Date;\n nowIso(): IsoTimestamp;\n /** Milliseconds since an arbitrary origin; use for durations only. */\n monotonicMs(): number;\n}\n\nexport interface IdGenerator {\n /** Opaque, collision-resistant identifier. */\n next(prefix?: string): string;\n}\n\nexport const systemClock: Clock = {\n now: () => new Date(),\n nowIso: () => new Date().toISOString(),\n monotonicMs: () => performance.now(),\n};\n","/**\n * The package version, resolved once for the whole package.\n *\n * Two paths, deliberately:\n *\n * - **Development** reads the root manifest, so the number is always live.\n * - **The published bundle cannot.** tsup collapses `src/**` into\n * `dist/index.js` and `dist/cli/index.js`, so any `require('../../package.json')`\n * written against a source path resolves from the wrong depth once bundled —\n * which is exactly how this broke during the single-package refactor, and how\n * it earlier produced a CLI reporting `v0.0.0-unknown`. The build injects\n * `__OAC_PACKAGE_VERSION__` instead (see tsup.config.ts).\n *\n * `typeof` on an undeclared identifier is safe in JS, so the dev path is\n * unaffected by the constant not existing.\n */\nimport { createRequire } from 'node:module';\n\ndeclare const __OAC_PACKAGE_VERSION__: string | undefined;\n\nfunction readFromManifest(): string {\n try {\n const require = createRequire(import.meta.url);\n // src/version.ts -> repository root\n return (require('../package.json') as { version?: string }).version ?? '0.0.0-unknown';\n } catch {\n return '0.0.0-unknown';\n }\n}\n\nexport const PACKAGE_VERSION: string =\n typeof __OAC_PACKAGE_VERSION__ === 'string' ? __OAC_PACKAGE_VERSION__ : readFromManifest();\n"]}
@@ -0,0 +1,255 @@
1
+ import { CommerceError } from './chunk-BQNE3A5H.js';
2
+
3
+ // src/payments/x402/networks.ts
4
+ var PROFILES = [
5
+ {
6
+ id: "eip155:84532",
7
+ chainId: 84532,
8
+ displayName: "Base Sepolia",
9
+ kind: "testnet",
10
+ canonicalAsset: {
11
+ symbol: "USDC",
12
+ address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
13
+ name: "USDC",
14
+ version: "2"
15
+ }
16
+ },
17
+ {
18
+ id: "eip155:8453",
19
+ chainId: 8453,
20
+ displayName: "Base",
21
+ kind: "mainnet",
22
+ canonicalAsset: {
23
+ symbol: "USDC",
24
+ address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
25
+ name: "USD Coin",
26
+ version: "2"
27
+ }
28
+ }
29
+ ];
30
+ var BY_ID = new Map(PROFILES.map((profile) => [profile.id, profile]));
31
+ var SUPPORTED_NETWORK_IDS = PROFILES.map((p) => p.id);
32
+ function requireNetworkProfile(id, path) {
33
+ const profile = BY_ID.get(id);
34
+ if (!profile) {
35
+ throw new CommerceError(
36
+ "CONFIG_INVALID",
37
+ `${path}: "${id}" is not a supported network. Supported CAIP-2 identifiers: ${SUPPORTED_NETWORK_IDS.join(", ")}`,
38
+ { details: { path, network: id } }
39
+ );
40
+ }
41
+ return profile;
42
+ }
43
+ function resolveDeploymentMode(profile, facilitatorMode) {
44
+ if (facilitatorMode === "local") return "local";
45
+ return profile.kind;
46
+ }
47
+ var LIVE_MAINNET_BANNER = "LIVE MAINNET MODE \u2014 REAL FUNDS";
48
+ function describeDeploymentMode(mode) {
49
+ return mode === "mainnet" ? LIVE_MAINNET_BANNER : mode.toUpperCase();
50
+ }
51
+
52
+ // src/payments/x402/local-chain/accounts.ts
53
+ var ANVIL_WELL_KNOWN_ACCOUNTS = [
54
+ {
55
+ address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
56
+ privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
57
+ },
58
+ {
59
+ address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
60
+ privateKey: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
61
+ },
62
+ {
63
+ address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
64
+ privateKey: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"
65
+ }
66
+ ];
67
+
68
+ // src/payments/x402/dev-key-guard.ts
69
+ var WELL_KNOWN_DEV_KEYS = new Set(
70
+ ANVIL_WELL_KNOWN_ACCOUNTS.map((a) => a.privateKey.toLowerCase())
71
+ );
72
+ var WELL_KNOWN_DEV_ADDRESSES = new Set(
73
+ ANVIL_WELL_KNOWN_ACCOUNTS.map((a) => a.address.toLowerCase())
74
+ );
75
+ var PRIVATE_KEY_SHAPE = /^0x[0-9a-fA-F]{64}$/;
76
+ function isWellKnownDevKey(privateKey) {
77
+ return WELL_KNOWN_DEV_KEYS.has(privateKey.toLowerCase());
78
+ }
79
+ function isWellKnownDevAddress(address) {
80
+ return WELL_KNOWN_DEV_ADDRESSES.has(address.toLowerCase());
81
+ }
82
+ function isLikelyLocalOrPrivateHost(hostname) {
83
+ const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
84
+ if (h === "localhost" || h === "127.0.0.1" || h === "::1") return true;
85
+ if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
86
+ if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
87
+ if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
88
+ if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
89
+ if (!h.includes(".") && !h.includes(":")) return true;
90
+ return false;
91
+ }
92
+ function parseHostnameOrThrow(rpcUrl) {
93
+ try {
94
+ return new URL(rpcUrl).hostname;
95
+ } catch (cause) {
96
+ throw new CommerceError("CONFIG_INVALID", "x402 provider: rpcUrl is not a valid URL", {
97
+ cause
98
+ });
99
+ }
100
+ }
101
+ function describeRpc(rpcUrl) {
102
+ try {
103
+ return new URL(rpcUrl).origin;
104
+ } catch {
105
+ return "[unparseable rpcUrl]";
106
+ }
107
+ }
108
+ function assertDevKeyIsLocalOnly(rpcUrl, signerPrivateKey) {
109
+ if (!PRIVATE_KEY_SHAPE.test(signerPrivateKey)) {
110
+ throw new CommerceError(
111
+ "CONFIG_INVALID",
112
+ 'x402 provider: facilitator.signerPrivateKey is not a well-formed private key (expected "0x" followed by exactly 64 hex characters).'
113
+ );
114
+ }
115
+ if (!isWellKnownDevKey(signerPrivateKey)) return;
116
+ const hostname = parseHostnameOrThrow(rpcUrl);
117
+ if (!isLikelyLocalOrPrivateHost(hostname)) {
118
+ throw new CommerceError(
119
+ "CONFIG_INVALID",
120
+ `x402 provider: facilitator.signerPrivateKey is a well-known Anvil development key, but rpcUrl ${describeRpc(rpcUrl)} does not look like a local/private chain. A public dev key must never sign against a public network \u2014 it is instantly drainable and can grief real settlements via nonce exhaustion. Use a real facilitator key for any non-local RPC, or point rpcUrl at your local/dev chain.`
121
+ );
122
+ }
123
+ }
124
+ function assertPayToIsNotDevAddress(rpcUrl, payTo) {
125
+ if (!isWellKnownDevAddress(payTo)) return;
126
+ const hostname = parseHostnameOrThrow(rpcUrl);
127
+ if (!isLikelyLocalOrPrivateHost(hostname)) {
128
+ throw new CommerceError(
129
+ "CONFIG_INVALID",
130
+ `x402 provider: "payTo" (${payTo}) is a well-known Anvil development address, but rpcUrl ${describeRpc(rpcUrl)} does not look like a local/private chain. The private key behind that address is public knowledge, so any revenue settled to it is immediately spendable by anyone. Set "payTo" to your own merchant wallet for any non-local RPC, or point rpcUrl at your local/dev chain.`
131
+ );
132
+ }
133
+ }
134
+
135
+ // src/payments/x402/guardrails.ts
136
+ function invalid(message, path) {
137
+ return new CommerceError("CONFIG_INVALID", message, { details: { path } });
138
+ }
139
+ function resolveX402Deployment(input) {
140
+ const profile = requireNetworkProfile(input.network, "payments.x402.network");
141
+ const mode = resolveDeploymentMode(profile, input.facilitator.mode);
142
+ if (profile.kind === "mainnet" && input.facilitator.mode === "local") {
143
+ throw invalid(
144
+ `payments.x402: network "${profile.id}" (${profile.displayName}) is a mainnet and cannot be served by facilitator.mode "local". A mainnet deployment must settle through a remote facilitator.`,
145
+ "payments.x402.facilitator.mode"
146
+ );
147
+ }
148
+ if (mode === "mainnet" && input.allowMainnet !== true) {
149
+ throw invalid(
150
+ `payments.x402: network "${profile.id}" (${profile.displayName}) settles real funds. Set payments.x402.allowMainnet: true to acknowledge this explicitly \u2014 it is never the default.`,
151
+ "payments.x402.allowMainnet"
152
+ );
153
+ }
154
+ if (input.facilitator.mode === "remote") {
155
+ assertFacilitatorUrlIsSafe(input.facilitator.url, mode);
156
+ if (mode === "mainnet" && input.facilitator.auth.type === "none" && input.allowUnauthenticatedFacilitator !== true) {
157
+ throw invalid(
158
+ `payments.x402: facilitator ${describeOrigin(input.facilitator.url)} takes no credential, and this is a mainnet deployment. It will see every payment authorisation you handle, with no account, terms or support behind it. Set payments.x402.allowUnauthenticatedFacilitator: true to accept that, or configure facilitator.auth.`,
159
+ "payments.x402.allowUnauthenticatedFacilitator"
160
+ );
161
+ }
162
+ for (const [field, value] of credentialFields(input.facilitator.auth)) {
163
+ if (value.trim() === "") {
164
+ throw invalid(
165
+ `payments.x402: facilitator.auth.type is "${input.facilitator.auth.type}" but ${field} is empty. An empty credential is refused rather than sent.`,
166
+ `payments.x402.facilitator.auth.${field}`
167
+ );
168
+ }
169
+ }
170
+ }
171
+ if (/^0x0{40}$/i.test(input.payTo)) {
172
+ throw invalid(
173
+ 'payments.x402: "payTo" is the zero address. Every payment settled there is destroyed.',
174
+ "payments.x402.payTo"
175
+ );
176
+ }
177
+ if (mode !== "local" && isWellKnownDevAddress(input.payTo)) {
178
+ throw invalid(
179
+ `payments.x402: "payTo" (${input.payTo}) is a well-known Anvil development address and this is a ${mode} deployment. Anyone can spend what settles there. Set payTo to your own merchant wallet.`,
180
+ "payments.x402.payTo"
181
+ );
182
+ }
183
+ const canonical = profile.canonicalAsset;
184
+ if (mode === "mainnet" && canonical) {
185
+ if (!sameAddress(input.asset, canonical.address)) {
186
+ throw invalid(
187
+ `payments.x402: asset ${input.asset} is not ${canonical.symbol} on ${profile.displayName} (expected ${canonical.address}). Settling a mainnet payment in an unintended token is refused.`,
188
+ "payments.x402.asset"
189
+ );
190
+ }
191
+ if (input.assetName !== void 0 && input.assetName !== canonical.name) {
192
+ throw invalid(
193
+ `payments.x402: assetName "${input.assetName}" is not the EIP-712 domain name ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.name}"). Every payment would be refused after the buyer signed.`,
194
+ "payments.x402.assetName"
195
+ );
196
+ }
197
+ if (input.assetVersion !== void 0 && input.assetVersion !== canonical.version) {
198
+ throw invalid(
199
+ `payments.x402: assetVersion "${input.assetVersion}" is not the EIP-712 domain version ${canonical.symbol} reports on ${profile.displayName} (expected "${canonical.version}").`,
200
+ "payments.x402.assetVersion"
201
+ );
202
+ }
203
+ }
204
+ return { profile, mode };
205
+ }
206
+ function assertFacilitatorUrlIsSafe(url, mode) {
207
+ let parsed;
208
+ try {
209
+ parsed = new URL(url);
210
+ } catch (cause) {
211
+ throw new CommerceError("CONFIG_INVALID", `payments.x402: facilitator.url is not a valid URL`, {
212
+ cause,
213
+ details: { path: "payments.x402.facilitator.url" }
214
+ });
215
+ }
216
+ if (parsed.protocol === "https:") return;
217
+ if (parsed.protocol !== "http:") {
218
+ throw invalid(
219
+ `payments.x402: facilitator ${describeOrigin(url)} must be reached over https (or http on a local/private host); got "${parsed.protocol}//"`,
220
+ "payments.x402.facilitator.url"
221
+ );
222
+ }
223
+ if (mode !== "mainnet" && isLikelyLocalOrPrivateHost(parsed.hostname)) return;
224
+ throw invalid(
225
+ `payments.x402: facilitator ${describeOrigin(url)} is reached over plain HTTP. Payment authorisations and settlement results would travel unencrypted. Use https, or point at a local/private host on a non-mainnet deployment.`,
226
+ "payments.x402.facilitator.url"
227
+ );
228
+ }
229
+ function describeOrigin(url) {
230
+ try {
231
+ return new URL(url).origin;
232
+ } catch {
233
+ return "[unparseable facilitator.url]";
234
+ }
235
+ }
236
+ function credentialFields(auth) {
237
+ switch (auth.type) {
238
+ case "bearer":
239
+ return [["token", auth.token]];
240
+ case "cdp":
241
+ return [
242
+ ["apiKeyId", auth.apiKeyId],
243
+ ["apiKeySecret", auth.apiKeySecret]
244
+ ];
245
+ default:
246
+ return [];
247
+ }
248
+ }
249
+ function sameAddress(a, b) {
250
+ return a.toLowerCase() === b.toLowerCase();
251
+ }
252
+
253
+ export { SUPPORTED_NETWORK_IDS, assertDevKeyIsLocalOnly, assertPayToIsNotDevAddress, describeDeploymentMode, requireNetworkProfile, resolveDeploymentMode, resolveX402Deployment };
254
+ //# sourceMappingURL=chunk-MEPETSP6.js.map
255
+ //# sourceMappingURL=chunk-MEPETSP6.js.map