@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/dist/x402.js CHANGED
@@ -1,11 +1,49 @@
1
- import { CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION } from './chunk-CZ6ZDAYD.js';
1
+ import { assertPayToIsNotDevAddress, resolveX402Deployment, assertDevKeyIsLocalOnly, describeDeploymentMode } from './chunk-MEPETSP6.js';
2
+ export { SUPPORTED_NETWORK_IDS } from './chunk-MEPETSP6.js';
3
+ import { CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION } from './chunk-BQNE3A5H.js';
4
+ import { PaymentRequirementsV2Schema, PaymentPayloadV2Schema } from '@x402/core/schemas';
2
5
  import { getAddress, toHex, isAddress, createWalletClient, http, publicActions, createPublicClient, HttpRequestError, TimeoutError, WaitForTransactionReceiptTimeoutError, encodeAbiParameters, keccak256, ContractFunctionRevertedError, ContractFunctionExecutionError } from 'viem';
3
6
  import { privateKeyToAccount } from 'viem/accounts';
4
- import { encodePayment } from 'x402/schemes';
5
- import { getNetworkId } from 'x402/shared';
6
- import { PaymentRequirementsSchema, PaymentPayloadSchema } from 'x402/types';
7
- import { verify, settle } from 'x402/facilitator';
7
+ import { x402Facilitator } from '@x402/core/facilitator';
8
+ import { HTTPFacilitatorClient } from '@x402/core/http';
9
+ import { isEIP3009Payload, toFacilitatorEvmSigner } from '@x402/evm';
10
+ import { registerExactEvmScheme } from '@x402/evm/exact/facilitator';
8
11
 
12
+ var LOCAL_CHAIN_ID = 84532;
13
+ function chainIdFromCaip2(network) {
14
+ const match = /^eip155:(\d+)$/.exec(network);
15
+ if (!match?.[1]) return void 0;
16
+ const chainId = Number(match[1]);
17
+ return Number.isSafeInteger(chainId) && chainId > 0 ? chainId : void 0;
18
+ }
19
+ function buildLocalChain(rpcUrl, chainId = LOCAL_CHAIN_ID) {
20
+ return {
21
+ id: chainId,
22
+ name: "agent-commerce-local",
23
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
24
+ rpcUrls: {
25
+ default: { http: [rpcUrl] }
26
+ }
27
+ };
28
+ }
29
+ function createLocalPublicClient(rpcUrl, timeoutMs, chainId) {
30
+ return createPublicClient({
31
+ chain: buildLocalChain(rpcUrl, chainId),
32
+ transport: http(rpcUrl, { timeout: timeoutMs } )
33
+ });
34
+ }
35
+ function createLocalFacilitatorClient(rpcUrl, signerPrivateKey) {
36
+ const chain = buildLocalChain(rpcUrl);
37
+ const account = privateKeyToAccount(signerPrivateKey);
38
+ return createWalletClient({
39
+ account,
40
+ chain,
41
+ transport: http(rpcUrl)
42
+ }).extend(publicActions);
43
+ }
44
+
45
+ // src/payments/x402/client.ts
46
+ var X402_VERSION = 2;
9
47
  var TRANSFER_WITH_AUTHORIZATION_TYPES = {
10
48
  TransferWithAuthorization: [
11
49
  { name: "from", type: "address" },
@@ -16,23 +54,27 @@ var TRANSFER_WITH_AUTHORIZATION_TYPES = {
16
54
  { name: "nonce", type: "bytes32" }
17
55
  ]
18
56
  };
19
- var DEFAULT_VALID_AFTER_SKEW_SECONDS = 600;
20
57
  async function createPaymentProof(options) {
21
- const requirements = PaymentRequirementsSchema.parse(options.accepts);
58
+ const requirements = PaymentRequirementsV2Schema.parse(options.accepts);
22
59
  if (!requirements.extra || typeof requirements.extra["name"] !== "string" || typeof requirements.extra["version"] !== "string") {
23
60
  throw new Error(
24
61
  "createPaymentProof: payment requirement is missing extra.name/extra.version \u2014 cannot build the EIP-712 domain"
25
62
  );
26
63
  }
64
+ const chainId = chainIdFromCaip2(requirements.network);
65
+ if (chainId === void 0) {
66
+ throw new Error(
67
+ `createPaymentProof: network "${requirements.network}" is not a CAIP-2 eip155 identifier \u2014 cannot determine the chain id to sign against`
68
+ );
69
+ }
27
70
  const account = privateKeyToAccount(options.buyerPrivateKey);
28
71
  void options.rpcUrl;
29
- const chainId = getNetworkId(requirements.network);
30
72
  const nonce = options.overrides?.nonce ?? randomNonce();
31
73
  const nowSeconds = Math.floor(Date.now() / 1e3);
32
- const validAfter = options.overrides?.validAfter ?? nowSeconds - DEFAULT_VALID_AFTER_SKEW_SECONDS;
74
+ const validAfter = options.overrides?.validAfter ?? 0;
33
75
  const validBefore = options.overrides?.validBefore ?? nowSeconds + requirements.maxTimeoutSeconds;
34
76
  const to = getAddress(options.overrides?.payTo ?? requirements.payTo);
35
- const value = BigInt(options.overrides?.value ?? requirements.maxAmountRequired);
77
+ const value = BigInt(options.overrides?.value ?? requirements.amount);
36
78
  const signature = await account.signTypedData({
37
79
  domain: {
38
80
  name: requirements.extra["name"],
@@ -52,9 +94,8 @@ async function createPaymentProof(options) {
52
94
  }
53
95
  });
54
96
  const payload = {
55
- x402Version: 1,
56
- scheme: "exact",
57
- network: requirements.network,
97
+ x402Version: X402_VERSION,
98
+ accepted: requirements,
58
99
  payload: {
59
100
  signature,
60
101
  authorization: {
@@ -67,7 +108,7 @@ async function createPaymentProof(options) {
67
108
  }
68
109
  }
69
110
  };
70
- return encodePayment(payload);
111
+ return Buffer.from(JSON.stringify(payload), "utf8").toString("base64");
71
112
  }
72
113
  function randomNonce() {
73
114
  const bytes = new Uint8Array(32);
@@ -114,109 +155,111 @@ function formatCanonicalAmount(baseUnits, decimals) {
114
155
  const sign = negative ? "-" : "";
115
156
  return fractionPart.length > 0 ? `${sign}${integerPart}.${fractionPart}` : `${sign}${integerPart}`;
116
157
  }
117
- var LOCAL_CHAIN_ID = 84532;
118
- function buildLocalChain(rpcUrl) {
158
+ var SETTLEMENT_CONFIRMATION_TIMEOUT_MS = 6e4;
159
+ function createLocalFacilitatorBinding(client, network, isTransportError) {
160
+ const build = () => {
161
+ let failed = false;
162
+ const watch = (promise) => promise.catch((err) => {
163
+ if (isTransportError(err)) failed = true;
164
+ throw err;
165
+ });
166
+ const signer = {
167
+ address: client.account.address,
168
+ readContract: (args) => watch(client.readContract(args)),
169
+ verifyTypedData: (args) => watch(client.verifyTypedData(args)),
170
+ writeContract: (args) => watch(client.writeContract(args)),
171
+ sendTransaction: (args) => watch(client.sendTransaction(args)),
172
+ waitForTransactionReceipt: (args) => watch(client.waitForTransactionReceipt(args)),
173
+ getCode: (args) => watch(client.getCode(args))
174
+ };
175
+ const facilitator = registerExactEvmScheme(new x402Facilitator(), {
176
+ signer: toFacilitatorEvmSigner(signer, {
177
+ confirmationTimeoutMs: SETTLEMENT_CONFIRMATION_TIMEOUT_MS
178
+ }),
179
+ networks: network
180
+ });
181
+ return { facilitator, failed: () => failed };
182
+ };
119
183
  return {
120
- id: LOCAL_CHAIN_ID,
121
- name: "agent-commerce-local",
122
- nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
123
- rpcUrls: {
124
- default: { http: [rpcUrl] }
184
+ kind: "local",
185
+ describe: "local (in-process)",
186
+ open() {
187
+ const { facilitator, failed } = build();
188
+ return {
189
+ verify: (payload, requirements) => facilitator.verify(payload, requirements),
190
+ settle: (payload, requirements) => facilitator.settle(payload, requirements),
191
+ transportFailed: failed
192
+ };
193
+ },
194
+ async supported() {
195
+ return build().facilitator.getSupported().kinds;
125
196
  }
126
197
  };
127
198
  }
128
- function createLocalPublicClient(rpcUrl, timeoutMs) {
129
- return createPublicClient({
130
- chain: buildLocalChain(rpcUrl),
131
- transport: http(rpcUrl, timeoutMs !== void 0 ? { timeout: timeoutMs } : void 0)
199
+ function cdpAuthHeaders(apiKeyId, apiKeySecret) {
200
+ const loading = import('@coinbase/x402').then(
201
+ (mod) => mod.createCdpAuthHeaders(apiKeyId, apiKeySecret),
202
+ (cause) => {
203
+ throw new CommerceError(
204
+ "CONFIG_INVALID",
205
+ 'x402 provider: facilitator.auth.type is "cdp", which needs the optional peer "@coinbase/x402". Install it (npm install @coinbase/x402), or use a facilitator that accepts a static token with auth.type "bearer".',
206
+ { cause }
207
+ );
208
+ }
209
+ );
210
+ loading.catch(() => {
132
211
  });
212
+ return async () => {
213
+ const create = await loading;
214
+ if (!create) {
215
+ throw new CommerceError(
216
+ "PAYMENT_PROVIDER_UNAVAILABLE",
217
+ "x402 provider: @coinbase/x402 returned no auth-header factory"
218
+ );
219
+ }
220
+ return await create();
221
+ };
133
222
  }
134
- function createLocalFacilitatorClient(rpcUrl, signerPrivateKey) {
135
- const chain = buildLocalChain(rpcUrl);
136
- const account = privateKeyToAccount(signerPrivateKey);
137
- return createWalletClient({
138
- account,
139
- chain,
140
- transport: http(rpcUrl)
141
- }).extend(publicActions);
142
- }
143
-
144
- // src/payments/x402/local-chain/accounts.ts
145
- var ANVIL_WELL_KNOWN_ACCOUNTS = [
146
- {
147
- address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
148
- privateKey: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
149
- },
150
- {
151
- address: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
152
- privateKey: "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"
153
- },
154
- {
155
- address: "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC",
156
- privateKey: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"
157
- }
158
- ];
159
-
160
- // src/payments/x402/dev-key-guard.ts
161
- var WELL_KNOWN_DEV_KEYS = new Set(
162
- ANVIL_WELL_KNOWN_ACCOUNTS.map((a) => a.privateKey.toLowerCase())
163
- );
164
- var WELL_KNOWN_DEV_ADDRESSES = new Set(
165
- ANVIL_WELL_KNOWN_ACCOUNTS.map((a) => a.address.toLowerCase())
166
- );
167
- var PRIVATE_KEY_SHAPE = /^0x[0-9a-fA-F]{64}$/;
168
- function isWellKnownDevKey(privateKey) {
169
- return WELL_KNOWN_DEV_KEYS.has(privateKey.toLowerCase());
170
- }
171
- function isWellKnownDevAddress(address) {
172
- return WELL_KNOWN_DEV_ADDRESSES.has(address.toLowerCase());
173
- }
174
- function isLikelyLocalOrPrivateHost(hostname) {
175
- const h = hostname.toLowerCase().replace(/^\[|\]$/g, "");
176
- if (h === "localhost" || h === "127.0.0.1" || h === "::1") return true;
177
- if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
178
- if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
179
- if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
180
- if (/^172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
181
- if (!h.includes(".") && !h.includes(":")) return true;
182
- return false;
183
- }
184
- function parseHostnameOrThrow(rpcUrl) {
185
- try {
186
- return new URL(rpcUrl).hostname;
187
- } catch (cause) {
188
- throw new CommerceError(
189
- "CONFIG_INVALID",
190
- `x402 provider: rpcUrl "${rpcUrl}" is not a valid URL`,
191
- { cause }
192
- );
193
- }
194
- }
195
- function assertDevKeyIsLocalOnly(rpcUrl, signerPrivateKey) {
196
- if (!PRIVATE_KEY_SHAPE.test(signerPrivateKey)) {
197
- throw new CommerceError(
198
- "CONFIG_INVALID",
199
- 'x402 provider: facilitator.signerPrivateKey is not a well-formed private key (expected "0x" followed by exactly 64 hex characters).'
200
- );
201
- }
202
- if (!isWellKnownDevKey(signerPrivateKey)) return;
203
- const hostname = parseHostnameOrThrow(rpcUrl);
204
- if (!isLikelyLocalOrPrivateHost(hostname)) {
205
- throw new CommerceError(
206
- "CONFIG_INVALID",
207
- `x402 provider: facilitator.signerPrivateKey is a well-known Anvil development key, but rpcUrl "${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.`
208
- );
209
- }
210
- }
211
- function assertPayToIsNotDevAddress(rpcUrl, payTo) {
212
- if (!isWellKnownDevAddress(payTo)) return;
213
- const hostname = parseHostnameOrThrow(rpcUrl);
214
- if (!isLikelyLocalOrPrivateHost(hostname)) {
215
- throw new CommerceError(
216
- "CONFIG_INVALID",
217
- `x402 provider: "payTo" (${payTo}) is a well-known Anvil development address, but rpcUrl "${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.`
218
- );
223
+ function createRemoteFacilitatorBinding(options) {
224
+ const auth = options.auth;
225
+ let createAuthHeaders;
226
+ if (auth.type === "bearer") {
227
+ createAuthHeaders = async () => {
228
+ const headers = { Authorization: `Bearer ${auth.token}` };
229
+ return { verify: headers, settle: headers, supported: headers };
230
+ };
231
+ } else if (auth.type === "cdp") {
232
+ createAuthHeaders = cdpAuthHeaders(auth.apiKeyId, auth.apiKeySecret);
219
233
  }
234
+ const client = new HTTPFacilitatorClient({
235
+ url: options.url,
236
+ timeoutMs: options.timeoutMs ?? SETTLEMENT_CONFIRMATION_TIMEOUT_MS,
237
+ ...createAuthHeaders ? { createAuthHeaders } : {}
238
+ });
239
+ return {
240
+ kind: "remote",
241
+ describe: `remote ${new URL(options.url).origin} (auth=${auth.type})`,
242
+ open() {
243
+ let failed = false;
244
+ const watch = async (call) => {
245
+ try {
246
+ return await call();
247
+ } catch (err) {
248
+ failed = true;
249
+ throw err;
250
+ }
251
+ };
252
+ return {
253
+ verify: (payload, requirements) => watch(() => client.verify(payload, requirements)),
254
+ settle: (payload, requirements) => watch(() => client.settle(payload, requirements)),
255
+ transportFailed: () => failed
256
+ };
257
+ },
258
+ async supported() {
259
+ const response = await client.getSupported();
260
+ return response.kinds;
261
+ }
262
+ };
220
263
  }
221
264
  function decodePaymentSubmission(payloadBase64) {
222
265
  let json;
@@ -226,11 +269,11 @@ function decodePaymentSubmission(payloadBase64) {
226
269
  } catch {
227
270
  return void 0;
228
271
  }
229
- const result = PaymentPayloadSchema.safeParse(json);
272
+ const result = PaymentPayloadV2Schema.safeParse(json);
230
273
  return result.success ? result.data : void 0;
231
274
  }
232
275
  function isExactEvmPayload(payload) {
233
- return payload.scheme === "exact" && typeof payload.payload === "object" && payload.payload !== null && "authorization" in payload.payload && "signature" in payload.payload;
276
+ return payload.accepted.scheme === "exact" && typeof payload.payload === "object" && payload.payload !== null && isEIP3009Payload(payload.payload) && typeof payload.payload.signature === "string";
234
277
  }
235
278
  function computeReplayKey(input) {
236
279
  const encoded = encodeAbiParameters(
@@ -241,10 +284,20 @@ function computeReplayKey(input) {
241
284
  }
242
285
 
243
286
  // src/payments/x402/provider.ts
244
- var X402_PROTOCOL_VERSION = "1";
287
+ var X402_VERSION2 = 2;
288
+ var X402_PROTOCOL_VERSION = String(X402_VERSION2);
245
289
  var DEFAULT_MAX_TIMEOUT_SECONDS = 60;
246
290
  var DEFAULT_MIME_TYPE = "application/json";
247
291
  var HEALTH_TIMEOUT_MS = 4e3;
292
+ var SETTLEMENT_PENDING_REASON = "settlement_pending";
293
+ var MAX_REASON_LENGTH = 64;
294
+ var REASON_SHAPE = /^[a-z0-9_.-]+$/i;
295
+ function sanitiseReason(reason, fallback) {
296
+ if (reason === void 0) return fallback;
297
+ const trimmed = reason.trim();
298
+ if (trimmed.length === 0 || trimmed.length > MAX_REASON_LENGTH) return fallback;
299
+ return REASON_SHAPE.test(trimmed) ? trimmed : fallback;
300
+ }
248
301
  var DEFAULT_IDS = {
249
302
  next: (prefix) => `${prefix ? `${prefix}_` : ""}${crypto.randomUUID()}`
250
303
  };
@@ -268,11 +321,23 @@ function createX402PaymentProvider(options) {
268
321
  `x402 provider: "assetDecimals" must be a non-negative integer`
269
322
  );
270
323
  }
271
- let facilitatorClient;
324
+ const { profile, mode } = resolveX402Deployment({
325
+ network: options.network,
326
+ payTo: options.payTo,
327
+ asset: options.asset,
328
+ assetName: options.assetName,
329
+ assetVersion: options.assetVersion,
330
+ facilitator: options.facilitator,
331
+ ...options.allowMainnet !== void 0 ? { allowMainnet: options.allowMainnet } : {},
332
+ ...options.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: options.allowUnauthenticatedFacilitator } : {}
333
+ });
334
+ const network = options.network;
335
+ let binding;
272
336
  if (options.facilitator.mode === "local") {
273
337
  assertDevKeyIsLocalOnly(options.rpcUrl, options.facilitator.signerPrivateKey);
338
+ let client;
274
339
  try {
275
- facilitatorClient = createLocalFacilitatorClient(
340
+ client = createLocalFacilitatorClient(
276
341
  options.rpcUrl,
277
342
  options.facilitator.signerPrivateKey
278
343
  );
@@ -283,9 +348,18 @@ function createX402PaymentProvider(options) {
283
348
  { cause }
284
349
  );
285
350
  }
351
+ binding = createLocalFacilitatorBinding(client, network, isProviderUnavailableError);
352
+ } else {
353
+ binding = createRemoteFacilitatorBinding({
354
+ url: options.facilitator.url,
355
+ auth: options.facilitator.auth
356
+ });
286
357
  }
287
- const publicClient = createLocalPublicClient(options.rpcUrl);
288
- const healthPublicClient = createLocalPublicClient(options.rpcUrl, HEALTH_TIMEOUT_MS);
358
+ const healthPublicClient = createLocalPublicClient(
359
+ options.rpcUrl,
360
+ HEALTH_TIMEOUT_MS,
361
+ profile.chainId
362
+ );
289
363
  const clock = options.clock ?? systemClock;
290
364
  const ids = options.ids ?? DEFAULT_IDS;
291
365
  const logger = options.logger ?? NOOP_LOGGER;
@@ -294,43 +368,54 @@ function createX402PaymentProvider(options) {
294
368
  name: "x402",
295
369
  kind: "payment",
296
370
  implementationVersion: PACKAGE_VERSION,
297
- supportedSpec: "x402@1.2.0 scheme=exact family=evm",
298
- capabilities: ["exact-evm", "local-facilitator", "eip-3009"],
371
+ supportedSpec: `x402/v${X402_VERSION2} scheme=exact family=eip155`,
372
+ capabilities: [
373
+ "exact-evm",
374
+ `${binding.kind}-facilitator`,
375
+ "eip-3009",
376
+ "caip-2",
377
+ `mode=${mode}`
378
+ ],
299
379
  status: "stable",
300
- unsupported: ["svm", "deferred scheme", "remote facilitator"]
380
+ unsupported: [
381
+ "svm",
382
+ "permit2",
383
+ "upto scheme",
384
+ "per-request signed facilitator credentials (e.g. CDP JWT)"
385
+ ]
301
386
  };
302
387
  async function createRequirement(context) {
303
- const maxAmountRequired = parseCanonicalAmount(
304
- context.amount,
305
- options.assetDecimals
306
- ).toString();
388
+ const amount = parseCanonicalAmount(context.amount, options.assetDecimals).toString();
307
389
  const requirements = {
308
390
  scheme: "exact",
309
- // options.network is a canonical free-form string (frozen contract); the
310
- // x402 SDK's Network union is checked against the live chain in health()
311
- // and by verify()'s own network-mismatch guard.
312
- network: options.network,
313
- maxAmountRequired,
314
- // Descriptive metadata only: `resource` is not part of the EIP-3009
315
- // authorisation the buyer signs (`from`/`to`/`value`/`validAfter`/
316
- // `validBefore`/`nonce`), nothing verifies against it, and the replay
317
- // key is keyed on the authorisation rather than on this string. It is
318
- // still what a facilitator, wallet or buyer agent logs and shows, so it
319
- // carries `network` and `payTo` — both already public in this same
320
- // challenge — to stay unambiguous across the wider x402 ecosystem,
321
- // where the `agent-commerce` authority alone is not reserved to us.
322
- resource: `resource://agent-commerce/${encodeURIComponent(options.network)}/${options.payTo}/resources/${encodeURIComponent(context.resource.id)}`,
323
- description: context.resource.description ?? context.resource.name,
324
- mimeType: DEFAULT_MIME_TYPE,
391
+ network,
392
+ asset: options.asset,
393
+ amount,
325
394
  payTo: options.payTo,
326
395
  maxTimeoutSeconds,
327
- asset: options.asset,
328
- // REQUIRED.name/version explicitly means
329
- // the facilitator never needs an on-chain version() call to build the
330
- // EIP-712 domain, so verification stays deterministic even against a
331
- // token that has no on-chain version() (it doesn't matter here since
332
- // MockUSDC has one, but this is what makes the domain unambiguous).
333
- extra: { name: options.assetName, version: options.assetVersion }
396
+ // `name`/`version` are what let a buyer and the facilitator build the
397
+ // same EIP-712 domain without an on-chain `version()` call, so
398
+ // verification stays deterministic against a token that has none.
399
+ //
400
+ // `assetTransferMethod` is stated rather than left to default: x402 v2's
401
+ // `exact`/EVM scheme also has a Permit2 path, and a client that picked it
402
+ // would produce a payload this provider cannot settle. Declaring the one
403
+ // supported method turns that into a client-side non-choice instead of a
404
+ // server-side rejection.
405
+ extra: {
406
+ name: options.assetName,
407
+ version: options.assetVersion,
408
+ assetTransferMethod: "eip3009"
409
+ }
410
+ };
411
+ const envelope = {
412
+ x402Version: X402_VERSION2,
413
+ resource: {
414
+ url: `resource://agent-commerce/${encodeURIComponent(options.network)}/${options.payTo}/resources/${encodeURIComponent(context.resource.id)}`,
415
+ description: context.resource.description ?? context.resource.name,
416
+ mimeType: DEFAULT_MIME_TYPE
417
+ },
418
+ accepts: [requirements]
334
419
  };
335
420
  const expiresAt = new Date(clock.now().getTime() + maxTimeoutSeconds * 1e3).toISOString();
336
421
  return {
@@ -347,11 +432,12 @@ function createX402PaymentProvider(options) {
347
432
  challenge: {
348
433
  provider: "x402",
349
434
  version: X402_PROTOCOL_VERSION,
350
- accepts: [requirements]
435
+ accepts: [requirements],
436
+ envelope
351
437
  }
352
438
  };
353
439
  }
354
- async function verify$1(context) {
440
+ async function verify(context) {
355
441
  const { requirement, submission } = context;
356
442
  const rejected = (reason) => ({
357
443
  status: "rejected",
@@ -379,6 +465,9 @@ function createX402PaymentProvider(options) {
379
465
  if (!payload) {
380
466
  return rejected("malformed_payment_payload");
381
467
  }
468
+ if (payload.x402Version !== X402_VERSION2) {
469
+ return rejected("unsupported_x402_version");
470
+ }
382
471
  if (!isExactEvmPayload(payload)) {
383
472
  return rejected("unsupported_scheme");
384
473
  }
@@ -386,35 +475,32 @@ function createX402PaymentProvider(options) {
386
475
  if (!isAddress(authorization.from) || !isAddress(authorization.to)) {
387
476
  return rejected("malformed_payment_payload");
388
477
  }
389
- if (payload.network !== options.network) {
478
+ if (payload.accepted.network !== options.network) {
390
479
  return rejected("wrong_network");
391
480
  }
392
481
  if (getAddress(authorization.to) !== getAddress(options.payTo)) {
393
482
  return rejected("wrong_recipient");
394
483
  }
395
484
  let authorizedValue;
396
- let maxRequired;
485
+ let required;
397
486
  try {
398
487
  authorizedValue = BigInt(authorization.value);
399
- maxRequired = BigInt(requirements.maxAmountRequired);
488
+ required = BigInt(requirements.amount);
400
489
  } catch {
401
490
  return rejected("malformed_payment_payload");
402
491
  }
403
- if (authorizedValue < maxRequired) {
492
+ if (authorizedValue !== required) {
404
493
  return rejected("wrong_amount");
405
494
  }
495
+ const scope = binding.open();
406
496
  let sdkResult;
407
497
  try {
408
- sdkResult = await verify(
409
- publicClient,
410
- payload,
411
- requirements
412
- );
498
+ sdkResult = await scope.verify(payload, requirements);
413
499
  } catch (err) {
414
- if (isProviderUnavailableError(err)) {
500
+ if (isProviderUnavailableError(err) || scope.transportFailed()) {
415
501
  throw new CommerceError(
416
502
  "PAYMENT_PROVIDER_UNAVAILABLE",
417
- "x402 provider: RPC unreachable during verify()",
503
+ `x402 provider: ${binding.kind} facilitator unreachable during verify()`,
418
504
  {
419
505
  cause: err
420
506
  }
@@ -424,10 +510,27 @@ function createX402PaymentProvider(options) {
424
510
  return rejected("unexpected_verify_error");
425
511
  }
426
512
  if (!sdkResult.isValid) {
427
- return rejected(sdkResult.invalidReason ?? "invalid_payment");
513
+ if (scope.transportFailed()) {
514
+ throw new CommerceError(
515
+ "PAYMENT_PROVIDER_UNAVAILABLE",
516
+ `x402 provider: ${binding.kind} facilitator unreachable during verify()`,
517
+ { details: { reportedReason: sdkResult.invalidReason ?? "invalid_payment" } }
518
+ );
519
+ }
520
+ if (sdkResult.invalidReason !== void 0) {
521
+ logger.debug(
522
+ { reportedReason: sdkResult.invalidReason },
523
+ "x402 verify(): facilitator rejection reason"
524
+ );
525
+ }
526
+ return rejected(sanitiseReason(sdkResult.invalidReason, "invalid_payment"));
527
+ }
528
+ const payloadChainId = chainIdFromCaip2(payload.accepted.network);
529
+ if (payloadChainId === void 0) {
530
+ return rejected("wrong_network");
428
531
  }
429
532
  const replayKey = computeReplayKey({
430
- chainId: getNetworkId(payload.network),
533
+ chainId: payloadChainId,
431
534
  asset: getAddress(requirements.asset),
432
535
  from: getAddress(authorization.from),
433
536
  nonce: authorization.nonce
@@ -439,12 +542,12 @@ function createX402PaymentProvider(options) {
439
542
  payee: getAddress(authorization.to),
440
543
  amount: formatCanonicalAmount(authorizedValue, options.assetDecimals),
441
544
  currency: requirement.currency,
442
- network: payload.network,
545
+ network: payload.accepted.network,
443
546
  asset: getAddress(requirements.asset),
444
547
  replayKey
445
548
  };
446
549
  }
447
- async function settle$1(context) {
550
+ async function settle(context) {
448
551
  const { requirement, submission, verification } = context;
449
552
  if (verification.status !== "verified") {
450
553
  throw new CommerceError(
@@ -452,12 +555,6 @@ function createX402PaymentProvider(options) {
452
555
  "x402 provider: settle() called without a successful verify()"
453
556
  );
454
557
  }
455
- if (options.facilitator.mode !== "local" || facilitatorClient === void 0) {
456
- throw new CommerceError(
457
- "PROTOCOL_UNSUPPORTED",
458
- 'x402 provider: remote facilitator settlement is not implemented in v0.1.0-alpha; only { mode: "local" } is supported'
459
- );
460
- }
461
558
  const requirements = requirement.challenge.accepts[0];
462
559
  if (!requirements) {
463
560
  throw new CommerceError(
@@ -472,30 +569,26 @@ function createX402PaymentProvider(options) {
472
569
  "x402 provider: settle() received an undecodable payment payload"
473
570
  );
474
571
  }
475
- let broadcastTxHash;
476
- const settlementClient = {
477
- ...facilitatorClient,
478
- waitForTransactionReceipt: (args) => {
479
- broadcastTxHash = args.hash;
480
- return facilitatorClient.waitForTransactionReceipt(args);
481
- }
482
- };
572
+ const rejectedSettlement = (rejectionReason) => ({
573
+ status: "rejected",
574
+ provider: "x402",
575
+ amount: verification.amount,
576
+ currency: requirement.currency,
577
+ ...verification.network !== void 0 ? { network: verification.network } : {},
578
+ ...verification.asset !== void 0 ? { asset: verification.asset } : {},
579
+ ...verification.replayKey !== void 0 ? { replayKey: verification.replayKey } : {},
580
+ rejectionReason
581
+ });
582
+ const scope = binding.open();
483
583
  let sdkResult;
484
584
  try {
485
- sdkResult = await settle(
486
- settlementClient,
487
- payload,
488
- requirements
489
- );
585
+ sdkResult = await scope.settle(payload, requirements);
490
586
  } catch (err) {
491
- if (isProviderUnavailableError(err)) {
587
+ if (isProviderUnavailableError(err) || scope.transportFailed()) {
492
588
  throw new CommerceError(
493
589
  "PAYMENT_PROVIDER_UNAVAILABLE",
494
- "x402 provider: RPC unreachable during settle()",
495
- {
496
- cause: err,
497
- ...broadcastTxHash !== void 0 ? { details: { transactionHash: broadcastTxHash } } : {}
498
- }
590
+ `x402 provider: ${binding.kind} facilitator unreachable during settle()`,
591
+ { cause: err }
499
592
  );
500
593
  }
501
594
  const rejectionReason = isOnChainRevertError(err) ? "transaction_reverted" : "unexpected_settle_error";
@@ -503,28 +596,28 @@ function createX402PaymentProvider(options) {
503
596
  { err: describeError(err), rejectionReason },
504
597
  "x402 settle(): settlement transaction failed"
505
598
  );
506
- return {
507
- status: "rejected",
508
- provider: "x402",
509
- amount: verification.amount,
510
- currency: requirement.currency,
511
- ...verification.network !== void 0 ? { network: verification.network } : {},
512
- ...verification.asset !== void 0 ? { asset: verification.asset } : {},
513
- ...verification.replayKey !== void 0 ? { replayKey: verification.replayKey } : {},
514
- rejectionReason
515
- };
599
+ return rejectedSettlement(rejectionReason);
516
600
  }
517
601
  if (!sdkResult.success) {
602
+ if (sdkResult.errorReason === SETTLEMENT_PENDING_REASON || scope.transportFailed()) {
603
+ throw new CommerceError(
604
+ "PAYMENT_PROVIDER_UNAVAILABLE",
605
+ "x402 provider: settlement was broadcast but could not be confirmed",
606
+ {
607
+ ...sdkResult.transaction ? { details: { transactionHash: sdkResult.transaction } } : {}
608
+ }
609
+ );
610
+ }
611
+ if (sdkResult.errorReason !== void 0) {
612
+ logger.debug(
613
+ { reportedReason: sdkResult.errorReason },
614
+ "x402 settle(): facilitator failure reason"
615
+ );
616
+ }
518
617
  return {
519
- status: "rejected",
520
- provider: "x402",
521
- amount: verification.amount,
522
- currency: requirement.currency,
618
+ ...rejectedSettlement(sanitiseReason(sdkResult.errorReason, "settlement_failed")),
523
619
  network: sdkResult.network,
524
- ...verification.asset !== void 0 ? { asset: verification.asset } : {},
525
- ...sdkResult.payer !== void 0 ? { payer: sdkResult.payer } : {},
526
- ...verification.replayKey !== void 0 ? { replayKey: verification.replayKey } : {},
527
- rejectionReason: sdkResult.errorReason ?? "settlement_failed"
620
+ ...sdkResult.payer !== void 0 ? { payer: sdkResult.payer } : {}
528
621
  };
529
622
  }
530
623
  return {
@@ -546,10 +639,10 @@ function createX402PaymentProvider(options) {
546
639
  const checkedAt = clock.nowIso();
547
640
  try {
548
641
  const chainId = await healthPublicClient.getChainId();
549
- if (chainId !== LOCAL_CHAIN_ID) {
642
+ if (chainId !== profile.chainId) {
550
643
  return {
551
644
  status: "fail",
552
- detail: `RPC at ${options.rpcUrl} reports chain id ${chainId}, expected ${LOCAL_CHAIN_ID}`,
645
+ detail: `RPC at ${options.rpcUrl} reports chain id ${chainId}, expected ${profile.chainId} (${profile.displayName})`,
553
646
  checkedAt,
554
647
  durationMs: clock.monotonicMs() - startedAt
555
648
  };
@@ -563,10 +656,22 @@ function createX402PaymentProvider(options) {
563
656
  durationMs: clock.monotonicMs() - startedAt
564
657
  };
565
658
  }
566
- if (options.facilitator.mode === "remote") {
659
+ if (binding.kind === "remote") {
660
+ const kinds = await binding.supported();
661
+ const supportsUs = kinds.some(
662
+ (kind) => kind.x402Version === X402_VERSION2 && kind.scheme === "exact" && kind.network === options.network
663
+ );
664
+ if (!supportsUs) {
665
+ return {
666
+ status: "fail",
667
+ detail: `Facilitator ${binding.describe} does not advertise x402 v${X402_VERSION2} scheme=exact on ${options.network} (${profile.displayName}); no payment on this deployment can settle`,
668
+ checkedAt,
669
+ durationMs: clock.monotonicMs() - startedAt
670
+ };
671
+ }
567
672
  return {
568
- status: "warn",
569
- detail: "RPC and asset are reachable; remote facilitator mode is not implemented in v0.1.0-alpha",
673
+ status: "pass",
674
+ detail: `${describeDeploymentMode(mode)} \u2014 RPC ${options.rpcUrl} reachable, chain id ${profile.chainId} (${profile.displayName}), asset ${options.asset} has code, facilitator ${binding.describe} supports exact/${options.network}`,
570
675
  checkedAt,
571
676
  durationMs: clock.monotonicMs() - startedAt
572
677
  };
@@ -575,14 +680,14 @@ function createX402PaymentProvider(options) {
575
680
  if (!isAnvil) {
576
681
  return {
577
682
  status: "fail",
578
- detail: `RPC at ${options.rpcUrl} reports chain id ${LOCAL_CHAIN_ID} but does not answer "anvil_nodeInfo" \u2014 it does not look like a local Anvil dev node. Refusing to treat it as safe for a local-facilitator dev key.`,
683
+ detail: `RPC at ${options.rpcUrl} reports chain id ${profile.chainId} but does not answer "anvil_nodeInfo" \u2014 it does not look like a local Anvil dev node. Refusing to treat it as safe for a local-facilitator dev key.`,
579
684
  checkedAt,
580
685
  durationMs: clock.monotonicMs() - startedAt
581
686
  };
582
687
  }
583
688
  return {
584
689
  status: "pass",
585
- detail: `RPC ${options.rpcUrl} reachable, chain id ${LOCAL_CHAIN_ID}, asset ${options.asset} has code, confirmed Anvil dev node`,
690
+ detail: `${describeDeploymentMode(mode)} \u2014 RPC ${options.rpcUrl} reachable, chain id ${profile.chainId}, asset ${options.asset} has code, confirmed Anvil dev node`,
586
691
  checkedAt,
587
692
  durationMs: clock.monotonicMs() - startedAt
588
693
  };
@@ -616,8 +721,8 @@ function createX402PaymentProvider(options) {
616
721
  name: "x402",
617
722
  descriptor,
618
723
  createRequirement,
619
- verify: verify$1,
620
- settle: settle$1,
724
+ verify,
725
+ settle,
621
726
  health
622
727
  };
623
728
  }