@agentcash/router 1.0.0 → 1.1.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/index.cjs CHANGED
@@ -30,6 +30,204 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // src/protocols/evm.ts
34
+ function isEvmNetwork(network) {
35
+ return network.startsWith("eip155:");
36
+ }
37
+ function filterEvmNetworks(networks) {
38
+ return networks.filter(isEvmNetwork);
39
+ }
40
+ function buildEvmExactOptions(accepts, price) {
41
+ return accepts.filter(
42
+ (accept) => accept.scheme === "exact" && isEvmNetwork(accept.network)
43
+ ).map(({ network, payTo }) => ({
44
+ scheme: "exact",
45
+ network,
46
+ price,
47
+ payTo
48
+ }));
49
+ }
50
+ var init_evm = __esm({
51
+ "src/protocols/evm.ts"() {
52
+ "use strict";
53
+ }
54
+ });
55
+
56
+ // src/protocols/solana.ts
57
+ function isSolanaNetwork(network) {
58
+ return network.startsWith("solana:");
59
+ }
60
+ function filterSolanaNetworks(networks) {
61
+ return networks.filter(isSolanaNetwork);
62
+ }
63
+ function buildSolanaExactOptions(accepts, price) {
64
+ return accepts.filter(
65
+ (accept) => accept.scheme === "exact" && isSolanaNetwork(accept.network)
66
+ ).map(({ network, payTo }) => ({
67
+ scheme: "exact",
68
+ network,
69
+ price,
70
+ payTo
71
+ }));
72
+ }
73
+ function hasSolanaAccepts(accepts) {
74
+ return accepts.some((accept) => isSolanaNetwork(accept.network));
75
+ }
76
+ async function enrichRequirementsWithFacilitatorAccepts(facilitator, resource, requirements) {
77
+ if (!facilitator.url) {
78
+ throw new Error(`Facilitator for ${facilitator.network} is missing a URL for /accepts`);
79
+ }
80
+ const authHeaders = await getAcceptsHeadersForFacilitator(facilitator);
81
+ const response = await fetch(`${facilitator.url.replace(/\/+$/, "")}/accepts`, {
82
+ method: "POST",
83
+ headers: {
84
+ ...authHeaders,
85
+ "content-type": "application/json"
86
+ },
87
+ body: JSON.stringify({
88
+ x402Version: 2,
89
+ resource,
90
+ accepts: requirements
91
+ })
92
+ });
93
+ if (!response.ok) {
94
+ throw new Error(`Facilitator /accepts failed with status ${response.status}`);
95
+ }
96
+ const body = await response.json();
97
+ if (!Array.isArray(body.accepts)) {
98
+ throw new Error("Facilitator /accepts response did not include accepts");
99
+ }
100
+ return body.accepts;
101
+ }
102
+ function isSolanaRequirement(requirement) {
103
+ return isSolanaNetwork(requirement.network);
104
+ }
105
+ var init_solana = __esm({
106
+ "src/protocols/solana.ts"() {
107
+ "use strict";
108
+ init_x402_facilitators();
109
+ }
110
+ });
111
+
112
+ // src/x402-facilitators.ts
113
+ function getResolvedX402Facilitator(config, network, defaultEvmFacilitator) {
114
+ const family = getNetworkFamily(network);
115
+ if (!family) return null;
116
+ const target = resolveX402FacilitatorTarget(config, network, defaultEvmFacilitator);
117
+ const resolvedConfig = normalizeFacilitatorTarget(target);
118
+ return {
119
+ family,
120
+ network,
121
+ url: resolvedConfig.url,
122
+ config: resolvedConfig
123
+ };
124
+ }
125
+ function getResolvedX402Facilitators(config, networks, defaultEvmFacilitator) {
126
+ return Object.fromEntries(
127
+ networks.flatMap((network) => {
128
+ const facilitator = getResolvedX402Facilitator(config, network, defaultEvmFacilitator);
129
+ return facilitator ? [[network, facilitator]] : [];
130
+ })
131
+ );
132
+ }
133
+ function getResolvedX402FacilitatorGroups(facilitatorsByNetwork) {
134
+ const groups = [];
135
+ for (const facilitator of Object.values(facilitatorsByNetwork)) {
136
+ const existing = groups.find(
137
+ (group) => group.family === facilitator.family && sameFacilitatorConfig(group.config, facilitator.config)
138
+ );
139
+ if (existing) {
140
+ existing.networks.push(facilitator.network);
141
+ continue;
142
+ }
143
+ groups.push({
144
+ family: facilitator.family,
145
+ config: facilitator.config,
146
+ networks: [facilitator.network]
147
+ });
148
+ }
149
+ return groups;
150
+ }
151
+ function getFacilitatorForRequirement(facilitatorsByNetwork, requirement) {
152
+ return facilitatorsByNetwork?.[requirement.network];
153
+ }
154
+ function sameResolvedX402Facilitator(a, b) {
155
+ return sameFacilitatorConfig(a.config, b.config);
156
+ }
157
+ async function getAcceptsHeadersForFacilitator(facilitator) {
158
+ if (facilitator.config.createAcceptsHeaders) {
159
+ return facilitator.config.createAcceptsHeaders();
160
+ }
161
+ if (facilitator.config.createAuthHeaders) {
162
+ const headers = await facilitator.config.createAuthHeaders();
163
+ return headers.supported;
164
+ }
165
+ return {};
166
+ }
167
+ function resolveX402FacilitatorTarget(config, network, defaultEvmFacilitator) {
168
+ return (isSolanaNetwork(network) ? config.x402?.facilitators?.solana : void 0) ?? (isEvmNetwork(network) ? config.x402?.facilitators?.evm : void 0) ?? (isSolanaNetwork(network) ? DEFAULT_SOLANA_FACILITATOR_URL : defaultEvmFacilitator);
169
+ }
170
+ function normalizeFacilitatorTarget(target) {
171
+ return typeof target === "string" ? { url: target } : target;
172
+ }
173
+ function getNetworkFamily(network) {
174
+ if (isEvmNetwork(network)) return "evm";
175
+ if (isSolanaNetwork(network)) return "solana";
176
+ return null;
177
+ }
178
+ function sameFacilitatorConfig(a, b) {
179
+ return a.url === b.url && a.createAuthHeaders === b.createAuthHeaders && a.createAcceptsHeaders === b.createAcceptsHeaders;
180
+ }
181
+ var DEFAULT_SOLANA_FACILITATOR_URL;
182
+ var init_x402_facilitators = __esm({
183
+ "src/x402-facilitators.ts"() {
184
+ "use strict";
185
+ init_evm();
186
+ init_solana();
187
+ DEFAULT_SOLANA_FACILITATOR_URL = "https://facilitator.corbits.dev";
188
+ }
189
+ });
190
+
191
+ // src/x402-config.ts
192
+ async function resolvePayToValue(payTo, request, fallback) {
193
+ if (!payTo) return fallback;
194
+ if (typeof payTo === "string") return payTo;
195
+ return payTo(request);
196
+ }
197
+ function getConfiguredX402Accepts(config) {
198
+ if (config.x402?.accepts?.length) {
199
+ return [...config.x402.accepts];
200
+ }
201
+ return [
202
+ {
203
+ scheme: "exact",
204
+ network: config.network ?? "eip155:8453",
205
+ payTo: config.payeeAddress
206
+ }
207
+ ];
208
+ }
209
+ function getConfiguredX402Networks(config) {
210
+ return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
211
+ }
212
+ async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo) {
213
+ return Promise.all(
214
+ accepts.map(async (accept) => ({
215
+ network: accept.network,
216
+ scheme: accept.scheme ?? "exact",
217
+ payTo: await resolvePayToValue(accept.payTo ?? routeEntry.payTo, request, fallbackPayTo),
218
+ ...accept.asset ? { asset: accept.asset } : {},
219
+ ...accept.decimals !== void 0 ? { decimals: accept.decimals } : {},
220
+ ...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
221
+ ...accept.extra ? { extra: accept.extra } : {}
222
+ }))
223
+ );
224
+ }
225
+ var init_x402_config = __esm({
226
+ "src/x402-config.ts"() {
227
+ "use strict";
228
+ }
229
+ });
230
+
33
231
  // src/server.ts
34
232
  var server_exports = {};
35
233
  __export(server_exports, {
@@ -41,32 +239,59 @@ async function createX402Server(config) {
41
239
  const { bazaarResourceServerExtension } = await import("@x402/extensions/bazaar");
42
240
  const { siwxResourceServerExtension } = await import("@x402/extensions/sign-in-with-x");
43
241
  const { facilitator: defaultFacilitator } = await import("@coinbase/x402");
44
- const raw = config.facilitatorUrl ?? defaultFacilitator;
45
- const facilitatorConfig = typeof raw === "string" ? { url: raw } : raw;
46
- const httpClient = new HTTPFacilitatorClient(facilitatorConfig);
47
- const network = config.network ?? "eip155:8453";
48
- const client = cachedClient(httpClient, network);
49
- const server = new x402ResourceServer(client);
50
- registerExactEvmScheme(server);
242
+ const configuredNetworks = getConfiguredX402Networks(config);
243
+ const facilitatorsByNetwork = getResolvedX402Facilitators(
244
+ config,
245
+ configuredNetworks,
246
+ defaultFacilitator
247
+ );
248
+ const evmNetworks = filterEvmNetworks(configuredNetworks);
249
+ const svmNetworks = filterSolanaNetworks(configuredNetworks);
250
+ const facilitatorClients = createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient);
251
+ const server = new x402ResourceServer(
252
+ facilitatorClients.length === 1 ? facilitatorClients[0] : facilitatorClients
253
+ );
254
+ if (evmNetworks.length > 0) {
255
+ registerExactEvmScheme(server, { networks: evmNetworks });
256
+ }
257
+ if (svmNetworks.length > 0) {
258
+ const { registerExactSvmScheme } = await import("@x402/svm/exact/server");
259
+ registerExactSvmScheme(server, { networks: svmNetworks });
260
+ }
51
261
  server.registerExtension(bazaarResourceServerExtension);
52
262
  server.registerExtension(siwxResourceServerExtension);
53
263
  const initPromise = server.initialize();
54
- return { server, initPromise };
264
+ return {
265
+ server,
266
+ initPromise,
267
+ facilitatorsByNetwork
268
+ };
55
269
  }
56
- function cachedClient(inner, network) {
270
+ function cachedClient(inner, networks) {
57
271
  return {
58
272
  verify: inner.verify.bind(inner),
59
273
  settle: inner.settle.bind(inner),
60
274
  getSupported: async () => ({
61
- kinds: [{ x402Version: 2, scheme: "exact", network }],
275
+ kinds: networks.map((network) => ({ x402Version: 2, scheme: "exact", network })),
62
276
  extensions: [],
63
277
  signers: {}
64
278
  })
65
279
  };
66
280
  }
281
+ function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
282
+ const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
283
+ return groups.map((group) => {
284
+ const inner = new HTTPFacilitatorClient(group.config);
285
+ return group.family === "evm" ? cachedClient(inner, group.networks) : inner;
286
+ });
287
+ }
67
288
  var init_server = __esm({
68
289
  "src/server.ts"() {
69
290
  "use strict";
291
+ init_evm();
292
+ init_solana();
293
+ init_x402_facilitators();
294
+ init_x402_config();
70
295
  }
71
296
  });
72
297
 
@@ -90,33 +315,46 @@ module.exports = __toCommonJS(index_exports);
90
315
  // src/registry.ts
91
316
  var RouteRegistry = class {
92
317
  routes = /* @__PURE__ */ new Map();
93
- // Silently overwrites on duplicate key. Next.js module loading order is
94
- // non-deterministic during build discovery stubs and real handlers may
95
- // register the same route key in either order. Last writer wins.
318
+ // Internal map key includes the HTTP method so that POST and DELETE on the
319
+ // same path coexist. Within the same path+method, last-write-wins is still
320
+ // intentional Next.js module loading order is non-deterministic during
321
+ // build and discovery stubs may register the same route in either order.
96
322
  // Prior art: ElysiaJS uses the same pattern (silent overwrite in router.history).
323
+ mapKey(entry) {
324
+ return `${entry.key}:${entry.method}`;
325
+ }
97
326
  register(entry) {
98
- if (this.routes.has(entry.key) && typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
327
+ const k = this.mapKey(entry);
328
+ if (this.routes.has(k) && typeof process !== "undefined" && process.env?.["NODE_ENV"] !== "production") {
99
329
  console.warn(
100
- `[agentcash/router] route '${entry.key}' registered twice \u2014 overwriting (this is expected for discovery stubs during next build)`
330
+ `[agentcash/router] route '${entry.key}' (${entry.method}) registered twice \u2014 overwriting (this is expected for discovery stubs during next build)`
101
331
  );
102
332
  }
103
- this.routes.set(entry.key, entry);
333
+ this.routes.set(k, entry);
104
334
  }
335
+ // Accepts either a compound key ("site/domain:DELETE") or a path-only key
336
+ // ("site/domain") — path-only returns the first registered method for that path.
105
337
  get(key) {
106
- return this.routes.get(key);
338
+ const direct = this.routes.get(key);
339
+ if (direct) return direct;
340
+ for (const entry of this.routes.values()) {
341
+ if (entry.key === key) return entry;
342
+ }
343
+ return void 0;
107
344
  }
108
345
  entries() {
109
346
  return this.routes.entries();
110
347
  }
111
348
  has(key) {
112
- return this.routes.has(key);
349
+ return this.get(key) !== void 0;
113
350
  }
114
351
  get size() {
115
352
  return this.routes.size;
116
353
  }
117
354
  validate(expectedKeys) {
118
355
  if (!expectedKeys) return;
119
- const missing = expectedKeys.filter((k) => !this.routes.has(k));
356
+ const registeredPathKeys = new Set([...this.routes.values()].map((e) => e.key));
357
+ const missing = expectedKeys.filter((k) => !registeredPathKeys.has(k));
120
358
  if (missing.length > 0) {
121
359
  throw new Error(
122
360
  `route${missing.length > 1 ? "s" : ""} ${missing.map((k) => `'${k}'`).join(", ")} in prices map but not registered \u2014 add to barrel imports`
@@ -374,51 +612,41 @@ var import_mppx = require("mppx");
374
612
  var import_viem = require("viem");
375
613
 
376
614
  // src/protocols/x402.ts
377
- async function buildX402Challenge(server, routeEntry, request, price, payeeAddress, network, extensions) {
615
+ init_x402_facilitators();
616
+ init_evm();
617
+ init_solana();
618
+ async function buildX402Challenge(opts) {
619
+ const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
378
620
  const { encodePaymentRequiredHeader } = await import("@x402/core/http");
379
- const options = {
380
- scheme: "exact",
381
- network,
621
+ const resource = buildChallengeResource(request, routeEntry);
622
+ const requirements = await buildChallengeRequirements(
623
+ server,
624
+ request,
382
625
  price,
383
- payTo: payeeAddress
384
- };
385
- const resource = {
386
- url: request.url,
387
- method: routeEntry.method,
388
- description: routeEntry.description,
389
- mimeType: "application/json"
390
- };
391
- const requirements = await server.buildPaymentRequirementsFromOptions([options], {
392
- request
393
- });
626
+ accepts,
627
+ resource,
628
+ facilitatorsByNetwork
629
+ );
394
630
  const paymentRequired = await server.createPaymentRequiredResponse(
395
631
  requirements,
396
632
  resource,
397
- null,
633
+ void 0,
398
634
  extensions
399
635
  );
400
636
  const encoded = encodePaymentRequiredHeader(paymentRequired);
401
637
  return { encoded, requirements };
402
638
  }
403
- async function verifyX402Payment(server, request, routeEntry, price, payeeAddress, network) {
404
- const { decodePaymentSignatureHeader } = await import("@x402/core/http");
405
- const paymentHeader = request.headers.get("PAYMENT-SIGNATURE") ?? request.headers.get("X-PAYMENT");
406
- if (!paymentHeader) return null;
407
- const payload = decodePaymentSignatureHeader(paymentHeader);
408
- const options = {
409
- scheme: "exact",
410
- network,
411
- price,
412
- payTo: payeeAddress
413
- };
414
- const requirements = await server.buildPaymentRequirementsFromOptions([options], {
415
- request
416
- });
417
- const matching = server.findMatchingRequirements(requirements, payload);
418
- const verify = await server.verifyPayment(payload, matching);
419
- if (!verify.isValid) {
420
- return { valid: false, payload: null, requirements: null, payer: null };
639
+ async function verifyX402Payment(opts) {
640
+ const { server, request, price, accepts } = opts;
641
+ const payload = await readPaymentPayload(request);
642
+ if (!payload) return null;
643
+ const requirements = await buildExpectedRequirements(server, request, price, accepts);
644
+ const matching = findVerifiableRequirements(server, requirements, payload);
645
+ if (!matching) {
646
+ return invalidPaymentVerification();
421
647
  }
648
+ const verify = await server.verifyPayment(payload, matching);
649
+ if (!verify.isValid) return invalidPaymentVerification();
422
650
  return {
423
651
  valid: true,
424
652
  payer: verify.payer,
@@ -426,14 +654,150 @@ async function verifyX402Payment(server, request, routeEntry, price, payeeAddres
426
654
  requirements: matching
427
655
  };
428
656
  }
657
+ function findVerifiableRequirements(server, requirements, payload) {
658
+ const strictMatch = server.findMatchingRequirements(requirements, payload);
659
+ if (strictMatch) {
660
+ return payload.x402Version === 2 ? payload.accepted : strictMatch;
661
+ }
662
+ if (payload.x402Version !== 2) {
663
+ return null;
664
+ }
665
+ const stableMatch = requirements.find(
666
+ (requirement) => matchesStableFields(requirement, payload.accepted)
667
+ );
668
+ return stableMatch ? payload.accepted : null;
669
+ }
670
+ function matchesStableFields(requirement, accepted) {
671
+ return requirement.scheme === accepted.scheme && requirement.network === accepted.network && requirement.payTo === accepted.payTo && requirement.asset === accepted.asset && requirement.amount === accepted.amount && requirement.maxTimeoutSeconds === accepted.maxTimeoutSeconds;
672
+ }
673
+ async function buildExpectedRequirements(server, request, price, accepts) {
674
+ const exactRequirements = await buildExactRequirements(server, request, price, accepts);
675
+ const customRequirements = buildCustomRequirements(price, accepts);
676
+ return [...exactRequirements, ...customRequirements];
677
+ }
678
+ async function buildExactRequirements(server, request, price, accepts) {
679
+ const exactOptions = [
680
+ ...buildEvmExactOptions(accepts, price),
681
+ ...buildSolanaExactOptions(accepts, price)
682
+ ];
683
+ if (exactOptions.length === 0) return [];
684
+ return server.buildPaymentRequirementsFromOptions(exactOptions, { request });
685
+ }
686
+ function buildCustomRequirements(price, accepts) {
687
+ return accepts.filter((accept) => accept.scheme !== "exact").map((accept) => buildCustomRequirement(price, accept));
688
+ }
689
+ async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork) {
690
+ const requirements = await buildExpectedRequirements(server, request, price, accepts);
691
+ if (!needsFacilitatorEnrichment(accepts)) return requirements;
692
+ return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork);
693
+ }
694
+ function needsFacilitatorEnrichment(accepts) {
695
+ return accepts.some((accept) => accept.scheme !== "exact") || hasSolanaAccepts(accepts);
696
+ }
697
+ async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork) {
698
+ const groups = collectEnrichmentGroups(requirements, facilitatorsByNetwork);
699
+ if (groups.length === 0) return requirements;
700
+ const enrichedRequirements = [...requirements];
701
+ await Promise.all(
702
+ groups.map(async (group) => {
703
+ const enriched = await enrichRequirementsWithFacilitatorAccepts(
704
+ group.facilitator,
705
+ resource,
706
+ group.items.map(({ requirement }) => requirement)
707
+ );
708
+ if (enriched.length !== group.items.length) {
709
+ throw new Error(
710
+ `Facilitator /accepts returned ${enriched.length} requirements for ${group.items.length} inputs on ${group.facilitator.url ?? group.facilitator.network}`
711
+ );
712
+ }
713
+ enriched.forEach((requirement, offset) => {
714
+ const index = group.items[offset]?.index;
715
+ if (index === void 0) return;
716
+ enrichedRequirements[index] = requirement;
717
+ });
718
+ })
719
+ );
720
+ return enrichedRequirements;
721
+ }
722
+ function collectEnrichmentGroups(requirements, facilitatorsByNetwork) {
723
+ const groups = [];
724
+ requirements.forEach((requirement, index) => {
725
+ if (!requiresFacilitatorEnrichment(requirement)) return;
726
+ const facilitator = getRequiredFacilitator(requirement, facilitatorsByNetwork);
727
+ const existing = groups.find(
728
+ (group) => sameResolvedX402Facilitator(group.facilitator, facilitator)
729
+ );
730
+ if (existing) {
731
+ existing.items.push({ index, requirement });
732
+ return;
733
+ }
734
+ groups.push({
735
+ facilitator,
736
+ items: [{ index, requirement }]
737
+ });
738
+ });
739
+ return groups;
740
+ }
741
+ function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
742
+ const facilitator = getFacilitatorForRequirement(facilitatorsByNetwork, requirement);
743
+ if (!facilitator) {
744
+ throw new Error(
745
+ `Missing x402 facilitator for ${requirement.scheme} requirement on ${requirement.network}`
746
+ );
747
+ }
748
+ return facilitator;
749
+ }
750
+ function requiresFacilitatorEnrichment(requirement) {
751
+ return requirement.scheme !== "exact" || isSolanaRequirement(requirement);
752
+ }
753
+ function buildCustomRequirement(price, accept) {
754
+ if (!accept.asset) {
755
+ throw new Error(
756
+ `Custom x402 accept '${accept.scheme}' on '${accept.network}' is missing asset`
757
+ );
758
+ }
759
+ return {
760
+ scheme: accept.scheme,
761
+ network: accept.network,
762
+ amount: decimalToAtomicUnits(price, accept.decimals ?? 6),
763
+ asset: accept.asset,
764
+ payTo: accept.payTo,
765
+ maxTimeoutSeconds: accept.maxTimeoutSeconds ?? 300,
766
+ extra: accept.extra ?? {}
767
+ };
768
+ }
769
+ function buildChallengeResource(request, routeEntry) {
770
+ return {
771
+ url: request.url,
772
+ method: routeEntry.method,
773
+ description: routeEntry.description,
774
+ mimeType: "application/json"
775
+ };
776
+ }
777
+ async function readPaymentPayload(request) {
778
+ const paymentHeader = request.headers.get("PAYMENT-SIGNATURE") ?? request.headers.get("X-PAYMENT");
779
+ if (!paymentHeader) return null;
780
+ const { decodePaymentSignatureHeader } = await import("@x402/core/http");
781
+ return decodePaymentSignatureHeader(paymentHeader);
782
+ }
783
+ function invalidPaymentVerification() {
784
+ return { valid: false, payload: null, requirements: null, payer: null };
785
+ }
786
+ function decimalToAtomicUnits(amount, decimals) {
787
+ const match = /^(?<whole>\d+)(?:\.(?<fraction>\d+))?$/.exec(amount);
788
+ if (!match?.groups) {
789
+ throw new Error(`Invalid decimal amount '${amount}'`);
790
+ }
791
+ const whole = match.groups.whole;
792
+ const fraction = match.groups.fraction ?? "";
793
+ if (fraction.length > decimals) {
794
+ throw new Error(`Amount '${amount}' exceeds ${decimals} decimal places`);
795
+ }
796
+ const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
797
+ return normalized === "" ? "0" : normalized;
798
+ }
429
799
  async function settleX402Payment(server, payload, requirements) {
430
800
  const { encodePaymentResponseHeader } = await import("@x402/core/http");
431
- const payloadKeys = typeof payload === "object" && payload !== null ? Object.keys(payload).sort().join(",") : "n/a";
432
- const reqKeys = typeof requirements === "object" && requirements !== null ? Object.keys(requirements).sort().join(",") : "n/a";
433
- console.info("x402 settle input", {
434
- payloadKeys,
435
- requirementsKeys: reqKeys
436
- });
437
801
  const result = await server.settlePayment(payload, requirements);
438
802
  const encoded = encodePaymentResponseHeader(result);
439
803
  return { encoded, result };
@@ -508,10 +872,10 @@ function extractBearerToken(header) {
508
872
  }
509
873
 
510
874
  // src/orchestrate.ts
511
- async function resolvePayTo(routeEntry, request, fallback) {
512
- if (!routeEntry.payTo) return fallback;
513
- if (typeof routeEntry.payTo === "string") return routeEntry.payTo;
514
- return routeEntry.payTo(request);
875
+ init_x402_config();
876
+ function getRequirementNetwork(requirements, fallback) {
877
+ const network = requirements?.network;
878
+ return typeof network === "string" ? network : fallback;
515
879
  }
516
880
  function createRequestHandler(routeEntry, handler, deps) {
517
881
  async function invoke(request, meta, pluginCtx, wallet, account, parsedBody) {
@@ -792,24 +1156,28 @@ function createRequestHandler(routeEntry, handler, deps) {
792
1156
  console.error(`[router] ${routeEntry.key}: ${reason}`);
793
1157
  return fail(500, reason, meta, pluginCtx, body.data);
794
1158
  }
795
- const payTo = await resolvePayTo(routeEntry, request, deps.payeeAddress);
796
- const verify = await verifyX402Payment(
797
- deps.x402Server,
1159
+ const accepts = await resolveX402Accepts(
798
1160
  request,
799
1161
  routeEntry,
800
- price,
801
- payTo,
802
- deps.network
1162
+ deps.x402Accepts,
1163
+ deps.payeeAddress
803
1164
  );
1165
+ const verify = await verifyX402Payment({
1166
+ server: deps.x402Server,
1167
+ request,
1168
+ price,
1169
+ accepts
1170
+ });
804
1171
  if (!verify?.valid) return await build402(request, routeEntry, deps, meta, pluginCtx);
805
1172
  const { payload: verifyPayload, requirements: verifyRequirements } = verify;
1173
+ const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
806
1174
  const wallet = verify.payer.toLowerCase();
807
1175
  pluginCtx.setVerifiedWallet(wallet);
808
1176
  firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
809
1177
  protocol: "x402",
810
1178
  payer: wallet,
811
1179
  amount: price,
812
- network: deps.network
1180
+ network: matchedNetwork
813
1181
  });
814
1182
  const { response, rawResult } = await invoke(
815
1183
  request,
@@ -821,15 +1189,6 @@ function createRequestHandler(routeEntry, handler, deps) {
821
1189
  );
822
1190
  if (response.status < 400) {
823
1191
  try {
824
- const payloadFingerprint = typeof verifyPayload === "object" && verifyPayload !== null ? {
825
- keys: Object.keys(verifyPayload).sort().join(","),
826
- payloadType: typeof verifyPayload
827
- } : { payloadType: typeof verifyPayload };
828
- console.info("Settlement attempt", {
829
- route: routeEntry.key,
830
- network: deps.network,
831
- ...payloadFingerprint
832
- });
833
1192
  const settle = await settleX402Payment(
834
1193
  deps.x402Server,
835
1194
  verifyPayload,
@@ -851,14 +1210,14 @@ function createRequestHandler(routeEntry, handler, deps) {
851
1210
  protocol: "x402",
852
1211
  payer: verify.payer,
853
1212
  transaction: String(settle.result?.transaction ?? ""),
854
- network: deps.network
1213
+ network: matchedNetwork
855
1214
  });
856
1215
  } catch (err) {
857
1216
  const errObj = err;
858
1217
  console.error("Settlement failed", {
859
1218
  message: err instanceof Error ? err.message : String(err),
860
1219
  route: routeEntry.key,
861
- network: deps.network,
1220
+ network: matchedNetwork,
862
1221
  facilitatorStatus: errObj.response?.status,
863
1222
  facilitatorBody: errObj.response?.data ?? errObj.response?.body
864
1223
  });
@@ -1073,16 +1432,21 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
1073
1432
  }
1074
1433
  if (routeEntry.protocols.includes("x402") && deps.x402Server) {
1075
1434
  try {
1076
- const payTo = await resolvePayTo(routeEntry, request, deps.payeeAddress);
1077
- const { encoded } = await buildX402Challenge(
1078
- deps.x402Server,
1435
+ const accepts = await resolveX402Accepts(
1436
+ request,
1437
+ routeEntry,
1438
+ deps.x402Accepts,
1439
+ deps.payeeAddress
1440
+ );
1441
+ const { encoded } = await buildX402Challenge({
1442
+ server: deps.x402Server,
1079
1443
  routeEntry,
1080
1444
  request,
1081
- challengePrice,
1082
- payTo,
1083
- deps.network,
1445
+ price: challengePrice,
1446
+ accepts,
1447
+ facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
1084
1448
  extensions
1085
- );
1449
+ });
1086
1450
  response.headers.set("PAYMENT-REQUIRED", encoded);
1087
1451
  } catch (err) {
1088
1452
  const message = `x402 challenge build failed: ${err instanceof Error ? err.message : String(err)}`;
@@ -1475,8 +1839,8 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
1475
1839
  const x402Set = /* @__PURE__ */ new Set();
1476
1840
  const mppSet = /* @__PURE__ */ new Set();
1477
1841
  const methodHints = discovery.methodHints ?? "non-default";
1478
- for (const [key, entry] of registry.entries()) {
1479
- const url = `${normalizedBase}/api/${entry.path ?? key}`;
1842
+ for (const [, entry] of registry.entries()) {
1843
+ const url = `${normalizedBase}/api/${entry.path ?? entry.key}`;
1480
1844
  const resource = toDiscoveryResource(entry.method, url, methodHints);
1481
1845
  if (entry.authMode !== "unprotected") x402Set.add(resource);
1482
1846
  if (entry.protocols.includes("mpp")) mppSet.add(resource);
@@ -1532,12 +1896,12 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
1532
1896
  const tagSet = /* @__PURE__ */ new Set();
1533
1897
  let requiresSiwxScheme = false;
1534
1898
  let requiresApiKeyScheme = false;
1535
- for (const [key, entry] of registry.entries()) {
1536
- const apiPath = `/api/${entry.path ?? key}`;
1899
+ for (const [, entry] of registry.entries()) {
1900
+ const apiPath = `/api/${entry.path ?? entry.key}`;
1537
1901
  const method = entry.method.toLowerCase();
1538
- const tag = deriveTag(key);
1902
+ const tag = deriveTag(entry.key);
1539
1903
  tagSet.add(tag);
1540
- const built = buildOperation(key, entry, tag);
1904
+ const built = buildOperation(entry.key, entry, tag);
1541
1905
  if (built.requiresSiwxScheme) requiresSiwxScheme = true;
1542
1906
  if (built.requiresApiKeyScheme) requiresApiKeyScheme = true;
1543
1907
  paths[apiPath] = { ...paths[apiPath], [method]: built.operation };
@@ -1706,11 +2070,15 @@ function createLlmsTxtHandler(discovery) {
1706
2070
  }
1707
2071
 
1708
2072
  // src/index.ts
2073
+ init_x402_config();
2074
+ init_evm();
2075
+ init_solana();
1709
2076
  function createRouter(config) {
1710
2077
  const registry = new RouteRegistry();
1711
2078
  const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
1712
2079
  const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
1713
2080
  const network = config.network ?? "eip155:8453";
2081
+ const x402Accepts = getConfiguredX402Accepts(config);
1714
2082
  if (!config.baseUrl) {
1715
2083
  throw new Error(
1716
2084
  '[router] baseUrl is required in RouterConfig. Set it to your production domain (e.g., "https://api.example.com"). The realm is used for payment matching and must be correct.'
@@ -1724,8 +2092,23 @@ function createRouter(config) {
1724
2092
  const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
1725
2093
  let x402ConfigError;
1726
2094
  let mppConfigError;
1727
- if ((!config.protocols || config.protocols.includes("x402")) && !config.payeeAddress) {
1728
- x402ConfigError = "x402 requires payeeAddress in router config.";
2095
+ if (!config.protocols || config.protocols.includes("x402")) {
2096
+ if (x402Accepts.length === 0) {
2097
+ x402ConfigError = "x402 requires at least one accept configuration.";
2098
+ } else if (x402Accepts.some((accept) => !accept.network)) {
2099
+ x402ConfigError = "x402 accepts require a network.";
2100
+ } else if (x402Accepts.some((accept) => !isSupportedX402Network(accept.network))) {
2101
+ const unsupported = x402Accepts.find((accept) => !isSupportedX402Network(accept.network));
2102
+ x402ConfigError = `unsupported x402 network '${unsupported?.network}'. Use eip155:* or solana:*.`;
2103
+ } else if (x402Accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset)) {
2104
+ x402ConfigError = "non-exact x402 accepts require an asset.";
2105
+ } else if (x402Accepts.some(
2106
+ (accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
2107
+ )) {
2108
+ x402ConfigError = "x402 accept decimals must be a non-negative integer.";
2109
+ } else if (x402Accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
2110
+ x402ConfigError = "x402 requires payeeAddress in router config or payTo on every x402 accept.";
2111
+ }
1729
2112
  }
1730
2113
  if (config.protocols?.includes("mpp")) {
1731
2114
  if (!config.mpp) {
@@ -1759,8 +2142,10 @@ function createRouter(config) {
1759
2142
  plugin: config.plugin,
1760
2143
  nonceStore,
1761
2144
  entitlementStore,
1762
- payeeAddress: config.payeeAddress,
2145
+ payeeAddress: config.payeeAddress ?? "",
1763
2146
  network,
2147
+ x402FacilitatorsByNetwork: void 0,
2148
+ x402Accepts,
1764
2149
  mppx: null
1765
2150
  };
1766
2151
  deps.initPromise = (async () => {
@@ -1771,6 +2156,7 @@ function createRouter(config) {
1771
2156
  const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
1772
2157
  const result = await createX402Server2(config);
1773
2158
  deps.x402Server = result.server;
2159
+ deps.x402FacilitatorsByNetwork = result.facilitatorsByNetwork;
1774
2160
  await result.initPromise;
1775
2161
  } catch (err) {
1776
2162
  deps.x402Server = null;
@@ -1861,6 +2247,9 @@ function createRouter(config) {
1861
2247
  registry
1862
2248
  };
1863
2249
  }
2250
+ function isSupportedX402Network(network) {
2251
+ return isEvmNetwork(network) || isSolanaNetwork(network);
2252
+ }
1864
2253
  function normalizePath(path) {
1865
2254
  let normalized = path.trim();
1866
2255
  normalized = normalized.replace(/^\/+/, "");