@agentcash/router 1.4.1 → 1.5.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
@@ -46,7 +46,51 @@ export const env = createEnv({
46
46
  });
47
47
  ```
48
48
 
49
- Without these keys, x402 routes will fail to initialize (empty 402 responses, no payment header).
49
+ Without these keys, x402 routes that use the default EVM facilitator will fail to initialize.
50
+ Run `validateRouterConfig(config)` during startup if you want missing facilitator, MPP, or store
51
+ configuration to throw immediately in every environment.
52
+
53
+ ### Recommended strict setup
54
+
55
+ ```typescript
56
+ import {
57
+ createRouter,
58
+ mppFromEnv,
59
+ validateRouterConfig,
60
+ x402AcceptsFromEnv,
61
+ type ProtocolType,
62
+ } from '@agentcash/router';
63
+
64
+ const accepts = x402AcceptsFromEnv(process.env);
65
+ const protocols: ProtocolType[] = process.env.MPP_SECRET_KEY ? ['x402', 'mpp'] : ['x402'];
66
+
67
+ const config = {
68
+ payeeAddress: process.env.X402_WALLET_ADDRESS!,
69
+ baseUrl: process.env.NEXT_PUBLIC_BASE_URL!,
70
+ strictRoutes: true,
71
+ protocols,
72
+ x402: { accepts },
73
+ mpp: mppFromEnv(process.env, {
74
+ recipient: process.env.X402_WALLET_ADDRESS,
75
+ useDefaultStore: true,
76
+ }),
77
+ discovery: {
78
+ title: 'My API',
79
+ version: '1.0.0',
80
+ },
81
+ };
82
+
83
+ validateRouterConfig(config);
84
+ export const router = createRouter(config);
85
+ ```
86
+
87
+ `x402AcceptsFromEnv()` always adds Base (`BASE_NETWORK`) and also adds Solana
88
+ mainnet (`SOLANA_MAINNET_NETWORK`) when `SOLANA_PAYEE_ADDRESS` is set. Solana
89
+ addresses are case-sensitive and are preserved as-is.
90
+
91
+ `mppFromEnv()` returns `undefined` when no MPP env vars are present. If any MPP
92
+ env var is present, the full trio is required: `MPP_SECRET_KEY`, `MPP_CURRENCY`,
93
+ and `TEMPO_RPC_URL`.
50
94
 
51
95
  ## Quick Start
52
96
 
@@ -143,9 +187,36 @@ Creates a `ServiceRouter` instance.
143
187
  | `plugin` | `RouterPlugin` | `undefined` | Observability plugin |
144
188
  | `prices` | `Record<string, string>` | `undefined` | Central pricing map (auto-applied) |
145
189
  | `siwx.nonceStore` | `NonceStore` | `MemoryNonceStore` | Custom nonce store |
146
- | `mpp` | `{ secretKey, currency, recipient? }` | `undefined` | MPP config |
190
+ | `mpp` | `{ secretKey, currency, recipient?, rpcUrl?, useDefaultStore? }` | `undefined` | MPP config |
191
+ | `protocols` | `('x402' \| 'mpp')[]` | `['x402']` | Default protocols for auto-priced routes |
147
192
  | `strictRoutes` | `boolean` | `false` | Enforce `route({ path })` and prevent key/path divergence |
148
193
 
194
+ ### Config validation helpers
195
+
196
+ ```typescript
197
+ import {
198
+ BASE_NETWORK,
199
+ SOLANA_MAINNET_NETWORK,
200
+ TEMPO_USDC_CURRENCY,
201
+ getRouterConfigIssues,
202
+ mppFromEnv,
203
+ paidOptionsForProtocols,
204
+ validateRouterConfig,
205
+ x402AcceptsFromEnv,
206
+ } from '@agentcash/router';
207
+ ```
208
+
209
+ - `validateRouterConfig(config)` throws `RouterConfigError` with structured
210
+ issues. Use it when you want invalid env/config to fail at startup.
211
+ - `getRouterConfigIssues(config)` returns the same structured issues without
212
+ throwing.
213
+ - `x402AcceptsFromEnv(env)` builds Base and optional Solana x402 accepts from
214
+ `X402_WALLET_ADDRESS` and `SOLANA_PAYEE_ADDRESS`.
215
+ - `mppFromEnv(env)` builds MPP config only when MPP env is present, and rejects
216
+ partial MPP env.
217
+ - `paidOptionsForProtocols(protocols)` copies a protocol array into a
218
+ route-level `PaidOptions` object.
219
+
149
220
  ### Path-First Routing
150
221
 
151
222
  Use path-first route definitions to keep runtime, OpenAPI, and discovery aligned:
@@ -259,12 +330,19 @@ interface HandlerContext<TBody, TQuery> {
259
330
  query: TQuery; // Parsed + validated
260
331
  request: NextRequest; // Raw request
261
332
  wallet: string | null; // Verified wallet address
333
+ payment: HandlerPaymentContext | null; // Payment metadata for this request
262
334
  account: unknown; // From .apiKey() resolver
263
335
  alert: AlertFn; // Fire observability alerts
264
336
  setVerifiedWallet: (addr: string) => void;
265
337
  }
266
338
  ```
267
339
 
340
+ `payment` is `null` for unprotected, API-key-only, and SIWX-only requests. For
341
+ paid requests it includes `protocol`, `status`, `payer`, `amount`, `network`,
342
+ and best-effort recipient/transaction/receipt metadata when the protocol
343
+ provides it. x402 handlers currently see `status: 'verified'` because settlement
344
+ happens after a successful handler response.
345
+
268
346
  ### RouterPlugin
269
347
 
270
348
  Pluggable observability. All hooks are optional and fire-and-forget.
package/dist/index.cjs CHANGED
@@ -188,6 +188,18 @@ var init_x402_facilitators = __esm({
188
188
  }
189
189
  });
190
190
 
191
+ // src/constants.ts
192
+ var BASE_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_CURRENCY, ZERO_EVM_ADDRESS;
193
+ var init_constants = __esm({
194
+ "src/constants.ts"() {
195
+ "use strict";
196
+ BASE_NETWORK = "eip155:8453";
197
+ SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
198
+ TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
199
+ ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
200
+ }
201
+ });
202
+
191
203
  // src/x402-config.ts
192
204
  async function resolvePayToValue(payTo, request, fallback, body) {
193
205
  if (!payTo) return fallback;
@@ -201,7 +213,7 @@ function getConfiguredX402Accepts(config) {
201
213
  return [
202
214
  {
203
215
  scheme: "exact",
204
- network: config.network ?? "eip155:8453",
216
+ network: config.network ?? BASE_NETWORK,
205
217
  payTo: config.payeeAddress
206
218
  }
207
219
  ];
@@ -230,6 +242,7 @@ async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, b
230
242
  var init_x402_config = __esm({
231
243
  "src/x402-config.ts"() {
232
244
  "use strict";
245
+ init_constants();
233
246
  }
234
247
  });
235
248
 
@@ -376,17 +389,28 @@ var init_upstash_rest = __esm({
376
389
  // src/index.ts
377
390
  var index_exports = {};
378
391
  __export(index_exports, {
392
+ BASE_NETWORK: () => BASE_NETWORK,
379
393
  HttpError: () => HttpError,
380
394
  MemoryEntitlementStore: () => MemoryEntitlementStore,
381
395
  MemoryNonceStore: () => MemoryNonceStore,
382
396
  RouteBuilder: () => RouteBuilder,
383
397
  RouteRegistry: () => RouteRegistry,
398
+ RouterConfigError: () => RouterConfigError,
384
399
  SIWX_CHALLENGE_EXPIRY_MS: () => SIWX_CHALLENGE_EXPIRY_MS,
385
400
  SIWX_ERROR_MESSAGES: () => SIWX_ERROR_MESSAGES,
401
+ SOLANA_MAINNET_NETWORK: () => SOLANA_MAINNET_NETWORK,
402
+ TEMPO_USDC_CURRENCY: () => TEMPO_USDC_CURRENCY,
403
+ ZERO_EVM_ADDRESS: () => ZERO_EVM_ADDRESS,
386
404
  consolePlugin: () => consolePlugin,
387
405
  createRedisEntitlementStore: () => createRedisEntitlementStore,
388
406
  createRedisNonceStore: () => createRedisNonceStore,
389
- createRouter: () => createRouter
407
+ createRouter: () => createRouter,
408
+ formatRouterConfigIssues: () => formatRouterConfigIssues,
409
+ getRouterConfigIssues: () => getRouterConfigIssues,
410
+ mppFromEnv: () => mppFromEnv,
411
+ paidOptionsForProtocols: () => paidOptionsForProtocols,
412
+ validateRouterConfig: () => validateRouterConfig,
413
+ x402AcceptsFromEnv: () => x402AcceptsFromEnv
390
414
  });
391
415
  module.exports = __toCommonJS(index_exports);
392
416
 
@@ -1029,6 +1053,10 @@ function getRequirementNetwork(requirements, fallback) {
1029
1053
  const network = requirements?.network;
1030
1054
  return typeof network === "string" ? network : fallback;
1031
1055
  }
1056
+ function getRequirementRecipient(requirements) {
1057
+ const payTo = requirements?.payTo;
1058
+ return typeof payTo === "string" ? payTo : void 0;
1059
+ }
1032
1060
  function siwxSignatureType(network) {
1033
1061
  return network.startsWith("solana:") ? "ed25519" : "eip191";
1034
1062
  }
@@ -1047,7 +1075,7 @@ function getSupportedChains(x402Accepts, fallbackNetwork) {
1047
1075
  return chains;
1048
1076
  }
1049
1077
  function createRequestHandler(routeEntry, handler, deps) {
1050
- async function invoke(request, meta, pluginCtx, wallet, account, parsedBody) {
1078
+ async function invoke(request, meta, pluginCtx, wallet, account, parsedBody, payment) {
1051
1079
  const ctx = {
1052
1080
  body: parsedBody,
1053
1081
  query: parseQuery(request, routeEntry),
@@ -1055,6 +1083,7 @@ function createRequestHandler(routeEntry, handler, deps) {
1055
1083
  requestId: meta.requestId,
1056
1084
  route: routeEntry.key,
1057
1085
  wallet,
1086
+ payment,
1058
1087
  account,
1059
1088
  alert(level, message, alertMeta) {
1060
1089
  firePluginHook(deps.plugin, "onAlert", pluginCtx, {
@@ -1107,7 +1136,8 @@ function createRequestHandler(routeEntry, handler, deps) {
1107
1136
  pluginCtx,
1108
1137
  wallet,
1109
1138
  account2,
1110
- body2.data
1139
+ body2.data,
1140
+ null
1111
1141
  );
1112
1142
  finalize(response, rawResult, meta, pluginCtx, body2.data);
1113
1143
  return response;
@@ -1384,7 +1414,16 @@ function createRequestHandler(routeEntry, handler, deps) {
1384
1414
  return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
1385
1415
  const { payload: verifyPayload, requirements: verifyRequirements } = verify;
1386
1416
  const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
1417
+ const matchedRecipient = getRequirementRecipient(verifyRequirements);
1387
1418
  const wallet = normalizeWalletAddress(verify.payer);
1419
+ const payment = {
1420
+ protocol: "x402",
1421
+ status: "verified",
1422
+ payer: wallet,
1423
+ amount: price,
1424
+ network: matchedNetwork,
1425
+ ...matchedRecipient ? { recipient: matchedRecipient } : {}
1426
+ };
1388
1427
  pluginCtx.setVerifiedWallet(wallet);
1389
1428
  firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
1390
1429
  protocol: "x402",
@@ -1398,7 +1437,8 @@ function createRequestHandler(routeEntry, handler, deps) {
1398
1437
  pluginCtx,
1399
1438
  wallet,
1400
1439
  account,
1401
- body.data
1440
+ body.data,
1441
+ payment
1402
1442
  );
1403
1443
  if (response.status < 400) {
1404
1444
  try {
@@ -1497,7 +1537,15 @@ function createRequestHandler(routeEntry, handler, deps) {
1497
1537
  pluginCtx,
1498
1538
  wallet,
1499
1539
  account,
1500
- body.data
1540
+ body.data,
1541
+ {
1542
+ protocol: "mpp",
1543
+ status: "verified",
1544
+ payer: wallet,
1545
+ amount: price,
1546
+ network: "tempo:4217",
1547
+ recipient: deps.payeeAddress
1548
+ }
1501
1549
  );
1502
1550
  if (response2.status < 400) {
1503
1551
  let mppResult2;
@@ -1630,7 +1678,17 @@ function createRequestHandler(routeEntry, handler, deps) {
1630
1678
  pluginCtx,
1631
1679
  wallet,
1632
1680
  account,
1633
- body.data
1681
+ body.data,
1682
+ {
1683
+ protocol: "mpp",
1684
+ status: "settled",
1685
+ payer: wallet,
1686
+ amount: price,
1687
+ network: "tempo:4217",
1688
+ recipient: deps.payeeAddress,
1689
+ ...txHash ? { transaction: txHash } : {},
1690
+ ...receiptHeader ? { receipt: receiptHeader } : {}
1691
+ }
1634
1692
  );
1635
1693
  if (response.status < 400) {
1636
1694
  if (routeEntry.siwxEnabled) {
@@ -2590,61 +2648,282 @@ function createLlmsTxtHandler(discovery) {
2590
2648
 
2591
2649
  // src/index.ts
2592
2650
  init_x402_config();
2651
+ init_constants();
2652
+
2653
+ // src/config.ts
2654
+ init_constants();
2593
2655
  init_evm();
2594
2656
  init_solana();
2657
+ init_x402_config();
2658
+ var RouterConfigError = class extends Error {
2659
+ issues;
2660
+ constructor(issues) {
2661
+ super(formatRouterConfigIssues(issues));
2662
+ this.name = "RouterConfigError";
2663
+ this.issues = issues;
2664
+ }
2665
+ };
2666
+ function validateRouterConfig(config, options = {}) {
2667
+ const issues = getRouterConfigIssues(config, options);
2668
+ if (issues.length > 0) throw new RouterConfigError(issues);
2669
+ }
2670
+ function getRouterConfigIssues(config, options = {}) {
2671
+ const env = options.env ?? process.env;
2672
+ const issues = [];
2673
+ const protocols = config.protocols ?? ["x402"];
2674
+ if (!config.baseUrl) {
2675
+ issues.push({
2676
+ code: "missing_base_url",
2677
+ message: '[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.'
2678
+ });
2679
+ }
2680
+ if (config.protocols && config.protocols.length === 0) {
2681
+ issues.push({
2682
+ code: "empty_protocols",
2683
+ message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
2684
+ });
2685
+ }
2686
+ if (protocols.includes("x402")) {
2687
+ issues.push(...validateX402Config(config, env, options));
2688
+ }
2689
+ if (protocols.includes("mpp")) {
2690
+ issues.push(...validateMppConfig(config, env));
2691
+ }
2692
+ return issues;
2693
+ }
2694
+ function formatRouterConfigIssues(issues) {
2695
+ return issues.map((issue) => issue.message).join("\n");
2696
+ }
2697
+ function mppFromEnv(env, options = {}) {
2698
+ const secretKey = env.MPP_SECRET_KEY;
2699
+ const currency = env.MPP_CURRENCY;
2700
+ const rpcUrl = env.TEMPO_RPC_URL;
2701
+ const hasAnyMppEnv = Boolean(secretKey || currency || rpcUrl || options.require);
2702
+ if (!hasAnyMppEnv) return void 0;
2703
+ const missing = [
2704
+ secretKey ? null : "MPP_SECRET_KEY",
2705
+ currency ? null : "MPP_CURRENCY",
2706
+ rpcUrl ? null : "TEMPO_RPC_URL"
2707
+ ].filter(Boolean);
2708
+ if (missing.length > 0) {
2709
+ throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
2710
+ }
2711
+ return {
2712
+ secretKey,
2713
+ currency,
2714
+ rpcUrl,
2715
+ ...options.recipient ? { recipient: options.recipient } : {},
2716
+ ...options.feePayerKey ? { feePayerKey: options.feePayerKey } : {},
2717
+ ...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
2718
+ };
2719
+ }
2720
+ function x402AcceptsFromEnv(env, options = {}) {
2721
+ const payeeEnv = options.payeeEnv ?? "X402_WALLET_ADDRESS";
2722
+ const solanaPayeeEnv = options.solanaPayeeEnv ?? "SOLANA_PAYEE_ADDRESS";
2723
+ const payeeAddress = options.payeeAddress ?? env[payeeEnv];
2724
+ if (!payeeAddress) {
2725
+ throw new Error(`${payeeEnv} is required to build x402 accepts`);
2726
+ }
2727
+ const accepts = [
2728
+ {
2729
+ scheme: "exact",
2730
+ network: options.network ?? BASE_NETWORK,
2731
+ payTo: payeeAddress
2732
+ }
2733
+ ];
2734
+ const solanaPayeeAddress = options.solanaPayeeAddress ?? env[solanaPayeeEnv];
2735
+ if (solanaPayeeAddress) {
2736
+ accepts.push({
2737
+ scheme: "exact",
2738
+ network: SOLANA_MAINNET_NETWORK,
2739
+ payTo: solanaPayeeAddress
2740
+ });
2741
+ }
2742
+ return accepts;
2743
+ }
2744
+ function paidOptionsForProtocols(protocols) {
2745
+ return { protocols: [...protocols] };
2746
+ }
2747
+ function validateX402Config(config, env, options) {
2748
+ const issues = [];
2749
+ const accepts = getConfiguredX402Accepts(config);
2750
+ if (accepts.length === 0) {
2751
+ issues.push({
2752
+ code: "missing_x402_accepts",
2753
+ protocol: "x402",
2754
+ message: "x402 requires at least one accept configuration."
2755
+ });
2756
+ return issues;
2757
+ }
2758
+ const acceptWithoutNetwork = accepts.find((accept) => !accept.network);
2759
+ if (acceptWithoutNetwork) {
2760
+ issues.push({
2761
+ code: "missing_x402_network",
2762
+ protocol: "x402",
2763
+ message: "x402 accepts require a network."
2764
+ });
2765
+ }
2766
+ const unsupported = accepts.find(
2767
+ (accept) => accept.network && !isSupportedX402Network(accept.network)
2768
+ );
2769
+ if (unsupported) {
2770
+ issues.push({
2771
+ code: "unsupported_x402_network",
2772
+ protocol: "x402",
2773
+ message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
2774
+ });
2775
+ }
2776
+ const missingAsset = accepts.find(
2777
+ (accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset
2778
+ );
2779
+ if (missingAsset) {
2780
+ issues.push({
2781
+ code: "missing_x402_asset",
2782
+ protocol: "x402",
2783
+ message: "non-exact x402 accepts require an asset."
2784
+ });
2785
+ }
2786
+ const invalidDecimals = accepts.find(
2787
+ (accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
2788
+ );
2789
+ if (invalidDecimals) {
2790
+ issues.push({
2791
+ code: "invalid_x402_decimals",
2792
+ protocol: "x402",
2793
+ message: "x402 accept decimals must be a non-negative integer."
2794
+ });
2795
+ }
2796
+ if (accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
2797
+ issues.push({
2798
+ code: "missing_x402_payee",
2799
+ protocol: "x402",
2800
+ message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
2801
+ });
2802
+ }
2803
+ const placeholder = findPlaceholderPayee([
2804
+ config.payeeAddress,
2805
+ ...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
2806
+ ]);
2807
+ if (placeholder) {
2808
+ issues.push({
2809
+ code: "placeholder_payee",
2810
+ protocol: "x402",
2811
+ message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
2812
+ });
2813
+ }
2814
+ if (options.requireCdpKeys !== false && usesDefaultEvmFacilitator(config)) {
2815
+ const missing = [
2816
+ env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
2817
+ env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
2818
+ ].filter(Boolean);
2819
+ if (missing.length > 0) {
2820
+ issues.push({
2821
+ code: "missing_cdp_keys",
2822
+ protocol: "x402",
2823
+ message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
2824
+ });
2825
+ }
2826
+ }
2827
+ return issues;
2828
+ }
2829
+ function validateMppConfig(config, env) {
2830
+ const issues = [];
2831
+ const mpp = config.mpp;
2832
+ if (!mpp) {
2833
+ return [
2834
+ {
2835
+ code: "missing_mpp_config",
2836
+ protocol: "mpp",
2837
+ message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
2838
+ }
2839
+ ];
2840
+ }
2841
+ if (!mpp.secretKey) {
2842
+ issues.push({
2843
+ code: "missing_mpp_secret_key",
2844
+ protocol: "mpp",
2845
+ message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
2846
+ });
2847
+ }
2848
+ if (!mpp.currency) {
2849
+ issues.push({
2850
+ code: "missing_mpp_currency",
2851
+ protocol: "mpp",
2852
+ message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
2853
+ });
2854
+ }
2855
+ if (!mpp.recipient && !config.payeeAddress) {
2856
+ issues.push({
2857
+ code: "missing_mpp_recipient",
2858
+ protocol: "mpp",
2859
+ message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
2860
+ });
2861
+ }
2862
+ const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
2863
+ if (placeholder) {
2864
+ issues.push({
2865
+ code: "placeholder_payee",
2866
+ protocol: "mpp",
2867
+ message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
2868
+ });
2869
+ }
2870
+ if (!(mpp.rpcUrl ?? env.TEMPO_RPC_URL)) {
2871
+ issues.push({
2872
+ code: "missing_mpp_rpc_url",
2873
+ protocol: "mpp",
2874
+ message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
2875
+ });
2876
+ }
2877
+ if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
2878
+ issues.push({
2879
+ code: "missing_mpp_default_store_env",
2880
+ protocol: "mpp",
2881
+ message: "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
2882
+ });
2883
+ }
2884
+ return issues;
2885
+ }
2886
+ function usesDefaultEvmFacilitator(config) {
2887
+ return getConfiguredX402Networks(config).some(
2888
+ (network) => typeof network === "string" && isEvmNetwork(network)
2889
+ ) && config.x402?.facilitators?.evm === void 0;
2890
+ }
2891
+ function isSupportedX402Network(network) {
2892
+ return isEvmNetwork(network) || isSolanaNetwork(network);
2893
+ }
2894
+ function findPlaceholderPayee(values) {
2895
+ return values.find((value) => value?.toLowerCase() === ZERO_EVM_ADDRESS) ?? null;
2896
+ }
2897
+
2898
+ // src/index.ts
2899
+ init_constants();
2595
2900
  function createRouter(config) {
2596
2901
  const registry = new RouteRegistry();
2597
2902
  const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
2598
2903
  const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
2599
- const network = config.network ?? "eip155:8453";
2904
+ const network = config.network ?? BASE_NETWORK;
2600
2905
  const x402Accepts = getConfiguredX402Accepts(config);
2601
- if (!config.baseUrl) {
2602
- throw new Error(
2603
- '[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.'
2604
- );
2605
- }
2606
- if (config.protocols && config.protocols.length === 0) {
2607
- throw new Error(
2608
- "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
2609
- );
2610
- }
2611
- const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
2612
- let x402ConfigError;
2613
- let mppConfigError;
2614
- if (!config.protocols || config.protocols.includes("x402")) {
2615
- if (x402Accepts.length === 0) {
2616
- x402ConfigError = "x402 requires at least one accept configuration.";
2617
- } else if (x402Accepts.some((accept) => !accept.network)) {
2618
- x402ConfigError = "x402 accepts require a network.";
2619
- } else if (x402Accepts.some((accept) => !isSupportedX402Network(accept.network))) {
2620
- const unsupported = x402Accepts.find((accept) => !isSupportedX402Network(accept.network));
2621
- x402ConfigError = `unsupported x402 network '${unsupported?.network}'. Use eip155:* or solana:*.`;
2622
- } else if (x402Accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset)) {
2623
- x402ConfigError = "non-exact x402 accepts require an asset.";
2624
- } else if (x402Accepts.some(
2625
- (accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
2626
- )) {
2627
- x402ConfigError = "x402 accept decimals must be a non-negative integer.";
2628
- } else if (x402Accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
2629
- x402ConfigError = "x402 requires payeeAddress in router config or payTo on every x402 accept.";
2630
- }
2631
- }
2632
- if (config.protocols?.includes("mpp")) {
2633
- if (!config.mpp) {
2634
- mppConfigError = 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.';
2635
- } else if (!config.mpp.recipient && !config.payeeAddress) {
2636
- mppConfigError = "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config.";
2637
- } else if (!(config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL)) {
2638
- mppConfigError = "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object.";
2639
- }
2640
- }
2641
- const allConfigErrors = [x402ConfigError, mppConfigError].filter(Boolean);
2642
- if (allConfigErrors.length > 0) {
2643
- for (const err of allConfigErrors) console.error(`[router] ${err}`);
2906
+ const configIssues = getRouterConfigIssues(config, {
2907
+ requireCdpKeys: process.env.NODE_ENV === "production"
2908
+ });
2909
+ const baseUrlIssue = configIssues.find((issue) => issue.code === "missing_base_url");
2910
+ if (baseUrlIssue) throw new RouterConfigError([baseUrlIssue]);
2911
+ const emptyProtocolsIssue = configIssues.find((issue) => issue.code === "empty_protocols");
2912
+ if (emptyProtocolsIssue) throw new RouterConfigError([emptyProtocolsIssue]);
2913
+ const protocolConfigIssues = configIssues.filter(
2914
+ (issue) => issue.code !== "missing_base_url" && issue.code !== "empty_protocols"
2915
+ );
2916
+ const x402ConfigIssues = protocolConfigIssues.filter((issue) => issue.protocol === "x402");
2917
+ const mppConfigIssues = protocolConfigIssues.filter((issue) => issue.protocol === "mpp");
2918
+ const x402ConfigError = x402ConfigIssues.length > 0 ? formatRouterConfigIssues(x402ConfigIssues) : void 0;
2919
+ const mppConfigError = mppConfigIssues.length > 0 ? formatRouterConfigIssues(mppConfigIssues) : void 0;
2920
+ if (protocolConfigIssues.length > 0) {
2921
+ for (const issue of protocolConfigIssues) console.error(`[router] ${issue.message}`);
2644
2922
  if (process.env.NODE_ENV === "production") {
2645
- throw new Error(allConfigErrors.join("\n"));
2923
+ throw new RouterConfigError(protocolConfigIssues);
2646
2924
  }
2647
2925
  }
2926
+ const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
2648
2927
  if (config.plugin?.init) {
2649
2928
  try {
2650
2929
  const result = config.plugin.init({ origin: resolvedBaseUrl });
@@ -2789,9 +3068,6 @@ function createRouter(config) {
2789
3068
  registry
2790
3069
  };
2791
3070
  }
2792
- function isSupportedX402Network(network) {
2793
- return isEvmNetwork(network) || isSolanaNetwork(network);
2794
- }
2795
3071
  function normalizePath(path) {
2796
3072
  let normalized = path.trim();
2797
3073
  normalized = normalized.replace(/^\/+/, "");
@@ -2800,15 +3076,26 @@ function normalizePath(path) {
2800
3076
  }
2801
3077
  // Annotate the CommonJS export names for ESM import in node:
2802
3078
  0 && (module.exports = {
3079
+ BASE_NETWORK,
2803
3080
  HttpError,
2804
3081
  MemoryEntitlementStore,
2805
3082
  MemoryNonceStore,
2806
3083
  RouteBuilder,
2807
3084
  RouteRegistry,
3085
+ RouterConfigError,
2808
3086
  SIWX_CHALLENGE_EXPIRY_MS,
2809
3087
  SIWX_ERROR_MESSAGES,
3088
+ SOLANA_MAINNET_NETWORK,
3089
+ TEMPO_USDC_CURRENCY,
3090
+ ZERO_EVM_ADDRESS,
2810
3091
  consolePlugin,
2811
3092
  createRedisEntitlementStore,
2812
3093
  createRedisNonceStore,
2813
- createRouter
3094
+ createRouter,
3095
+ formatRouterConfigIssues,
3096
+ getRouterConfigIssues,
3097
+ mppFromEnv,
3098
+ paidOptionsForProtocols,
3099
+ validateRouterConfig,
3100
+ x402AcceptsFromEnv
2814
3101
  });
package/dist/index.d.cts CHANGED
@@ -260,6 +260,17 @@ interface PaidOptions {
260
260
  /** Override MPP protocol metadata in x-payment-info discovery. */
261
261
  mpp?: MppProtocolInfo;
262
262
  }
263
+ type PaymentStatus = 'verified' | 'settled';
264
+ interface HandlerPaymentContext {
265
+ protocol: ProtocolType;
266
+ status: PaymentStatus;
267
+ payer: string;
268
+ amount: string;
269
+ network: string;
270
+ recipient?: string;
271
+ transaction?: string;
272
+ receipt?: string;
273
+ }
263
274
  interface HandlerContext<TBody = undefined, TQuery = undefined> {
264
275
  body: TBody;
265
276
  query: TQuery;
@@ -267,6 +278,7 @@ interface HandlerContext<TBody = undefined, TQuery = undefined> {
267
278
  requestId: string;
268
279
  route: string;
269
280
  wallet: string | null;
281
+ payment: HandlerPaymentContext | null;
270
282
  account: unknown;
271
283
  alert: AlertFn;
272
284
  setVerifiedWallet: (addr: string) => void;
@@ -640,6 +652,44 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
640
652
  handler(fn: HandlerArg<TBody, TQuery, HasAuth, NeedsBody, HasBody, NeedsInputExample, NeedsOutputExample>): (request: NextRequest) => Promise<Response>;
641
653
  }
642
654
 
655
+ declare const BASE_NETWORK = "eip155:8453";
656
+ declare const SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
657
+ declare const TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
658
+ declare const ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
659
+
660
+ type RouterEnv = Record<string, string | undefined>;
661
+ type RouterConfigIssueCode = 'missing_base_url' | 'empty_protocols' | 'missing_x402_accepts' | 'missing_x402_network' | 'unsupported_x402_network' | 'missing_x402_asset' | 'invalid_x402_decimals' | 'missing_x402_payee' | 'missing_cdp_keys' | 'placeholder_payee' | 'missing_mpp_config' | 'missing_mpp_secret_key' | 'missing_mpp_currency' | 'missing_mpp_recipient' | 'missing_mpp_rpc_url' | 'missing_mpp_default_store_env';
662
+ interface RouterConfigIssue {
663
+ code: RouterConfigIssueCode;
664
+ message: string;
665
+ protocol?: ProtocolType;
666
+ }
667
+ interface RouterConfigValidationOptions {
668
+ env?: RouterEnv;
669
+ requireCdpKeys?: boolean;
670
+ }
671
+ declare class RouterConfigError extends Error {
672
+ readonly issues: RouterConfigIssue[];
673
+ constructor(issues: RouterConfigIssue[]);
674
+ }
675
+ declare function validateRouterConfig(config: RouterConfig, options?: RouterConfigValidationOptions): void;
676
+ declare function getRouterConfigIssues(config: RouterConfig, options?: RouterConfigValidationOptions): RouterConfigIssue[];
677
+ declare function formatRouterConfigIssues(issues: readonly RouterConfigIssue[]): string;
678
+ declare function mppFromEnv(env: RouterEnv, options?: {
679
+ recipient?: string;
680
+ require?: boolean;
681
+ useDefaultStore?: boolean;
682
+ feePayerKey?: string;
683
+ }): RouterConfig['mpp'] | undefined;
684
+ declare function x402AcceptsFromEnv(env: RouterEnv, options?: {
685
+ payeeAddress?: string;
686
+ payeeEnv?: string;
687
+ network?: string;
688
+ solanaPayeeAddress?: string;
689
+ solanaPayeeEnv?: string;
690
+ }): X402AcceptConfig[];
691
+ declare function paidOptionsForProtocols(protocols: readonly ProtocolType[]): PaidOptions;
692
+
643
693
  interface MonitorEntry {
644
694
  provider: string;
645
695
  route: string;
@@ -660,4 +710,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
660
710
  prices?: P;
661
711
  }): ServiceRouter<Extract<keyof P, string>>;
662
712
 
663
- export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type DiscoveryConfig, type EntitlementStore, type ErrorEvent, type HandlerContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type MppProtocolInfo, type NonceStore, type OveragePolicy, type PaidOptions, type PayToConfig, type PaymentEvent, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisEntitlementStoreOptions, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402AcceptConfig, type X402FacilitatorTarget, type X402FacilitatorsConfig, type X402ResolvedAccept, type X402RouterFacilitatorConfig, type X402Server, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter };
713
+ export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, BASE_NETWORK, type DiscoveryConfig, type EntitlementStore, type ErrorEvent, type HandlerContext, type HandlerPaymentContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type MppProtocolInfo, type NonceStore, type OveragePolicy, type PaidOptions, type PayToConfig, type PaymentEvent, type PaymentStatus, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisEntitlementStoreOptions, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, RouterConfigError, type RouterConfigIssue, type RouterConfigIssueCode, type RouterConfigValidationOptions, type RouterEnv, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, SOLANA_MAINNET_NETWORK, type ServiceRouter, type SettlementEvent, TEMPO_USDC_CURRENCY, type TierConfig, type X402AcceptConfig, type X402FacilitatorTarget, type X402FacilitatorsConfig, type X402ResolvedAccept, type X402RouterFacilitatorConfig, type X402Server, ZERO_EVM_ADDRESS, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter, formatRouterConfigIssues, getRouterConfigIssues, mppFromEnv, paidOptionsForProtocols, validateRouterConfig, x402AcceptsFromEnv };
package/dist/index.d.ts CHANGED
@@ -260,6 +260,17 @@ interface PaidOptions {
260
260
  /** Override MPP protocol metadata in x-payment-info discovery. */
261
261
  mpp?: MppProtocolInfo;
262
262
  }
263
+ type PaymentStatus = 'verified' | 'settled';
264
+ interface HandlerPaymentContext {
265
+ protocol: ProtocolType;
266
+ status: PaymentStatus;
267
+ payer: string;
268
+ amount: string;
269
+ network: string;
270
+ recipient?: string;
271
+ transaction?: string;
272
+ receipt?: string;
273
+ }
263
274
  interface HandlerContext<TBody = undefined, TQuery = undefined> {
264
275
  body: TBody;
265
276
  query: TQuery;
@@ -267,6 +278,7 @@ interface HandlerContext<TBody = undefined, TQuery = undefined> {
267
278
  requestId: string;
268
279
  route: string;
269
280
  wallet: string | null;
281
+ payment: HandlerPaymentContext | null;
270
282
  account: unknown;
271
283
  alert: AlertFn;
272
284
  setVerifiedWallet: (addr: string) => void;
@@ -640,6 +652,44 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
640
652
  handler(fn: HandlerArg<TBody, TQuery, HasAuth, NeedsBody, HasBody, NeedsInputExample, NeedsOutputExample>): (request: NextRequest) => Promise<Response>;
641
653
  }
642
654
 
655
+ declare const BASE_NETWORK = "eip155:8453";
656
+ declare const SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
657
+ declare const TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
658
+ declare const ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
659
+
660
+ type RouterEnv = Record<string, string | undefined>;
661
+ type RouterConfigIssueCode = 'missing_base_url' | 'empty_protocols' | 'missing_x402_accepts' | 'missing_x402_network' | 'unsupported_x402_network' | 'missing_x402_asset' | 'invalid_x402_decimals' | 'missing_x402_payee' | 'missing_cdp_keys' | 'placeholder_payee' | 'missing_mpp_config' | 'missing_mpp_secret_key' | 'missing_mpp_currency' | 'missing_mpp_recipient' | 'missing_mpp_rpc_url' | 'missing_mpp_default_store_env';
662
+ interface RouterConfigIssue {
663
+ code: RouterConfigIssueCode;
664
+ message: string;
665
+ protocol?: ProtocolType;
666
+ }
667
+ interface RouterConfigValidationOptions {
668
+ env?: RouterEnv;
669
+ requireCdpKeys?: boolean;
670
+ }
671
+ declare class RouterConfigError extends Error {
672
+ readonly issues: RouterConfigIssue[];
673
+ constructor(issues: RouterConfigIssue[]);
674
+ }
675
+ declare function validateRouterConfig(config: RouterConfig, options?: RouterConfigValidationOptions): void;
676
+ declare function getRouterConfigIssues(config: RouterConfig, options?: RouterConfigValidationOptions): RouterConfigIssue[];
677
+ declare function formatRouterConfigIssues(issues: readonly RouterConfigIssue[]): string;
678
+ declare function mppFromEnv(env: RouterEnv, options?: {
679
+ recipient?: string;
680
+ require?: boolean;
681
+ useDefaultStore?: boolean;
682
+ feePayerKey?: string;
683
+ }): RouterConfig['mpp'] | undefined;
684
+ declare function x402AcceptsFromEnv(env: RouterEnv, options?: {
685
+ payeeAddress?: string;
686
+ payeeEnv?: string;
687
+ network?: string;
688
+ solanaPayeeAddress?: string;
689
+ solanaPayeeEnv?: string;
690
+ }): X402AcceptConfig[];
691
+ declare function paidOptionsForProtocols(protocols: readonly ProtocolType[]): PaidOptions;
692
+
643
693
  interface MonitorEntry {
644
694
  provider: string;
645
695
  route: string;
@@ -660,4 +710,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
660
710
  prices?: P;
661
711
  }): ServiceRouter<Extract<keyof P, string>>;
662
712
 
663
- export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type DiscoveryConfig, type EntitlementStore, type ErrorEvent, type HandlerContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type MppProtocolInfo, type NonceStore, type OveragePolicy, type PaidOptions, type PayToConfig, type PaymentEvent, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisEntitlementStoreOptions, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402AcceptConfig, type X402FacilitatorTarget, type X402FacilitatorsConfig, type X402ResolvedAccept, type X402RouterFacilitatorConfig, type X402Server, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter };
713
+ export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, BASE_NETWORK, type DiscoveryConfig, type EntitlementStore, type ErrorEvent, type HandlerContext, type HandlerPaymentContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type MppProtocolInfo, type NonceStore, type OveragePolicy, type PaidOptions, type PayToConfig, type PaymentEvent, type PaymentStatus, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisEntitlementStoreOptions, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, RouterConfigError, type RouterConfigIssue, type RouterConfigIssueCode, type RouterConfigValidationOptions, type RouterEnv, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, SOLANA_MAINNET_NETWORK, type ServiceRouter, type SettlementEvent, TEMPO_USDC_CURRENCY, type TierConfig, type X402AcceptConfig, type X402FacilitatorTarget, type X402FacilitatorsConfig, type X402ResolvedAccept, type X402RouterFacilitatorConfig, type X402Server, ZERO_EVM_ADDRESS, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter, formatRouterConfigIssues, getRouterConfigIssues, mppFromEnv, paidOptionsForProtocols, validateRouterConfig, x402AcceptsFromEnv };
package/dist/index.js CHANGED
@@ -166,6 +166,18 @@ var init_x402_facilitators = __esm({
166
166
  }
167
167
  });
168
168
 
169
+ // src/constants.ts
170
+ var BASE_NETWORK, SOLANA_MAINNET_NETWORK, TEMPO_USDC_CURRENCY, ZERO_EVM_ADDRESS;
171
+ var init_constants = __esm({
172
+ "src/constants.ts"() {
173
+ "use strict";
174
+ BASE_NETWORK = "eip155:8453";
175
+ SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
176
+ TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
177
+ ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
178
+ }
179
+ });
180
+
169
181
  // src/x402-config.ts
170
182
  async function resolvePayToValue(payTo, request, fallback, body) {
171
183
  if (!payTo) return fallback;
@@ -179,7 +191,7 @@ function getConfiguredX402Accepts(config) {
179
191
  return [
180
192
  {
181
193
  scheme: "exact",
182
- network: config.network ?? "eip155:8453",
194
+ network: config.network ?? BASE_NETWORK,
183
195
  payTo: config.payeeAddress
184
196
  }
185
197
  ];
@@ -208,6 +220,7 @@ async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, b
208
220
  var init_x402_config = __esm({
209
221
  "src/x402-config.ts"() {
210
222
  "use strict";
223
+ init_constants();
211
224
  }
212
225
  });
213
226
 
@@ -990,6 +1003,10 @@ function getRequirementNetwork(requirements, fallback) {
990
1003
  const network = requirements?.network;
991
1004
  return typeof network === "string" ? network : fallback;
992
1005
  }
1006
+ function getRequirementRecipient(requirements) {
1007
+ const payTo = requirements?.payTo;
1008
+ return typeof payTo === "string" ? payTo : void 0;
1009
+ }
993
1010
  function siwxSignatureType(network) {
994
1011
  return network.startsWith("solana:") ? "ed25519" : "eip191";
995
1012
  }
@@ -1008,7 +1025,7 @@ function getSupportedChains(x402Accepts, fallbackNetwork) {
1008
1025
  return chains;
1009
1026
  }
1010
1027
  function createRequestHandler(routeEntry, handler, deps) {
1011
- async function invoke(request, meta, pluginCtx, wallet, account, parsedBody) {
1028
+ async function invoke(request, meta, pluginCtx, wallet, account, parsedBody, payment) {
1012
1029
  const ctx = {
1013
1030
  body: parsedBody,
1014
1031
  query: parseQuery(request, routeEntry),
@@ -1016,6 +1033,7 @@ function createRequestHandler(routeEntry, handler, deps) {
1016
1033
  requestId: meta.requestId,
1017
1034
  route: routeEntry.key,
1018
1035
  wallet,
1036
+ payment,
1019
1037
  account,
1020
1038
  alert(level, message, alertMeta) {
1021
1039
  firePluginHook(deps.plugin, "onAlert", pluginCtx, {
@@ -1068,7 +1086,8 @@ function createRequestHandler(routeEntry, handler, deps) {
1068
1086
  pluginCtx,
1069
1087
  wallet,
1070
1088
  account2,
1071
- body2.data
1089
+ body2.data,
1090
+ null
1072
1091
  );
1073
1092
  finalize(response, rawResult, meta, pluginCtx, body2.data);
1074
1093
  return response;
@@ -1345,7 +1364,16 @@ function createRequestHandler(routeEntry, handler, deps) {
1345
1364
  return await build402(request, routeEntry, deps, meta, pluginCtx, body.data);
1346
1365
  const { payload: verifyPayload, requirements: verifyRequirements } = verify;
1347
1366
  const matchedNetwork = getRequirementNetwork(verifyRequirements, deps.network);
1367
+ const matchedRecipient = getRequirementRecipient(verifyRequirements);
1348
1368
  const wallet = normalizeWalletAddress(verify.payer);
1369
+ const payment = {
1370
+ protocol: "x402",
1371
+ status: "verified",
1372
+ payer: wallet,
1373
+ amount: price,
1374
+ network: matchedNetwork,
1375
+ ...matchedRecipient ? { recipient: matchedRecipient } : {}
1376
+ };
1349
1377
  pluginCtx.setVerifiedWallet(wallet);
1350
1378
  firePluginHook(deps.plugin, "onPaymentVerified", pluginCtx, {
1351
1379
  protocol: "x402",
@@ -1359,7 +1387,8 @@ function createRequestHandler(routeEntry, handler, deps) {
1359
1387
  pluginCtx,
1360
1388
  wallet,
1361
1389
  account,
1362
- body.data
1390
+ body.data,
1391
+ payment
1363
1392
  );
1364
1393
  if (response.status < 400) {
1365
1394
  try {
@@ -1458,7 +1487,15 @@ function createRequestHandler(routeEntry, handler, deps) {
1458
1487
  pluginCtx,
1459
1488
  wallet,
1460
1489
  account,
1461
- body.data
1490
+ body.data,
1491
+ {
1492
+ protocol: "mpp",
1493
+ status: "verified",
1494
+ payer: wallet,
1495
+ amount: price,
1496
+ network: "tempo:4217",
1497
+ recipient: deps.payeeAddress
1498
+ }
1462
1499
  );
1463
1500
  if (response2.status < 400) {
1464
1501
  let mppResult2;
@@ -1591,7 +1628,17 @@ function createRequestHandler(routeEntry, handler, deps) {
1591
1628
  pluginCtx,
1592
1629
  wallet,
1593
1630
  account,
1594
- body.data
1631
+ body.data,
1632
+ {
1633
+ protocol: "mpp",
1634
+ status: "settled",
1635
+ payer: wallet,
1636
+ amount: price,
1637
+ network: "tempo:4217",
1638
+ recipient: deps.payeeAddress,
1639
+ ...txHash ? { transaction: txHash } : {},
1640
+ ...receiptHeader ? { receipt: receiptHeader } : {}
1641
+ }
1595
1642
  );
1596
1643
  if (response.status < 400) {
1597
1644
  if (routeEntry.siwxEnabled) {
@@ -2551,61 +2598,282 @@ function createLlmsTxtHandler(discovery) {
2551
2598
 
2552
2599
  // src/index.ts
2553
2600
  init_x402_config();
2601
+ init_constants();
2602
+
2603
+ // src/config.ts
2604
+ init_constants();
2554
2605
  init_evm();
2555
2606
  init_solana();
2607
+ init_x402_config();
2608
+ var RouterConfigError = class extends Error {
2609
+ issues;
2610
+ constructor(issues) {
2611
+ super(formatRouterConfigIssues(issues));
2612
+ this.name = "RouterConfigError";
2613
+ this.issues = issues;
2614
+ }
2615
+ };
2616
+ function validateRouterConfig(config, options = {}) {
2617
+ const issues = getRouterConfigIssues(config, options);
2618
+ if (issues.length > 0) throw new RouterConfigError(issues);
2619
+ }
2620
+ function getRouterConfigIssues(config, options = {}) {
2621
+ const env = options.env ?? process.env;
2622
+ const issues = [];
2623
+ const protocols = config.protocols ?? ["x402"];
2624
+ if (!config.baseUrl) {
2625
+ issues.push({
2626
+ code: "missing_base_url",
2627
+ message: '[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.'
2628
+ });
2629
+ }
2630
+ if (config.protocols && config.protocols.length === 0) {
2631
+ issues.push({
2632
+ code: "empty_protocols",
2633
+ message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
2634
+ });
2635
+ }
2636
+ if (protocols.includes("x402")) {
2637
+ issues.push(...validateX402Config(config, env, options));
2638
+ }
2639
+ if (protocols.includes("mpp")) {
2640
+ issues.push(...validateMppConfig(config, env));
2641
+ }
2642
+ return issues;
2643
+ }
2644
+ function formatRouterConfigIssues(issues) {
2645
+ return issues.map((issue) => issue.message).join("\n");
2646
+ }
2647
+ function mppFromEnv(env, options = {}) {
2648
+ const secretKey = env.MPP_SECRET_KEY;
2649
+ const currency = env.MPP_CURRENCY;
2650
+ const rpcUrl = env.TEMPO_RPC_URL;
2651
+ const hasAnyMppEnv = Boolean(secretKey || currency || rpcUrl || options.require);
2652
+ if (!hasAnyMppEnv) return void 0;
2653
+ const missing = [
2654
+ secretKey ? null : "MPP_SECRET_KEY",
2655
+ currency ? null : "MPP_CURRENCY",
2656
+ rpcUrl ? null : "TEMPO_RPC_URL"
2657
+ ].filter(Boolean);
2658
+ if (missing.length > 0) {
2659
+ throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
2660
+ }
2661
+ return {
2662
+ secretKey,
2663
+ currency,
2664
+ rpcUrl,
2665
+ ...options.recipient ? { recipient: options.recipient } : {},
2666
+ ...options.feePayerKey ? { feePayerKey: options.feePayerKey } : {},
2667
+ ...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
2668
+ };
2669
+ }
2670
+ function x402AcceptsFromEnv(env, options = {}) {
2671
+ const payeeEnv = options.payeeEnv ?? "X402_WALLET_ADDRESS";
2672
+ const solanaPayeeEnv = options.solanaPayeeEnv ?? "SOLANA_PAYEE_ADDRESS";
2673
+ const payeeAddress = options.payeeAddress ?? env[payeeEnv];
2674
+ if (!payeeAddress) {
2675
+ throw new Error(`${payeeEnv} is required to build x402 accepts`);
2676
+ }
2677
+ const accepts = [
2678
+ {
2679
+ scheme: "exact",
2680
+ network: options.network ?? BASE_NETWORK,
2681
+ payTo: payeeAddress
2682
+ }
2683
+ ];
2684
+ const solanaPayeeAddress = options.solanaPayeeAddress ?? env[solanaPayeeEnv];
2685
+ if (solanaPayeeAddress) {
2686
+ accepts.push({
2687
+ scheme: "exact",
2688
+ network: SOLANA_MAINNET_NETWORK,
2689
+ payTo: solanaPayeeAddress
2690
+ });
2691
+ }
2692
+ return accepts;
2693
+ }
2694
+ function paidOptionsForProtocols(protocols) {
2695
+ return { protocols: [...protocols] };
2696
+ }
2697
+ function validateX402Config(config, env, options) {
2698
+ const issues = [];
2699
+ const accepts = getConfiguredX402Accepts(config);
2700
+ if (accepts.length === 0) {
2701
+ issues.push({
2702
+ code: "missing_x402_accepts",
2703
+ protocol: "x402",
2704
+ message: "x402 requires at least one accept configuration."
2705
+ });
2706
+ return issues;
2707
+ }
2708
+ const acceptWithoutNetwork = accepts.find((accept) => !accept.network);
2709
+ if (acceptWithoutNetwork) {
2710
+ issues.push({
2711
+ code: "missing_x402_network",
2712
+ protocol: "x402",
2713
+ message: "x402 accepts require a network."
2714
+ });
2715
+ }
2716
+ const unsupported = accepts.find(
2717
+ (accept) => accept.network && !isSupportedX402Network(accept.network)
2718
+ );
2719
+ if (unsupported) {
2720
+ issues.push({
2721
+ code: "unsupported_x402_network",
2722
+ protocol: "x402",
2723
+ message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
2724
+ });
2725
+ }
2726
+ const missingAsset = accepts.find(
2727
+ (accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset
2728
+ );
2729
+ if (missingAsset) {
2730
+ issues.push({
2731
+ code: "missing_x402_asset",
2732
+ protocol: "x402",
2733
+ message: "non-exact x402 accepts require an asset."
2734
+ });
2735
+ }
2736
+ const invalidDecimals = accepts.find(
2737
+ (accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
2738
+ );
2739
+ if (invalidDecimals) {
2740
+ issues.push({
2741
+ code: "invalid_x402_decimals",
2742
+ protocol: "x402",
2743
+ message: "x402 accept decimals must be a non-negative integer."
2744
+ });
2745
+ }
2746
+ if (accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
2747
+ issues.push({
2748
+ code: "missing_x402_payee",
2749
+ protocol: "x402",
2750
+ message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
2751
+ });
2752
+ }
2753
+ const placeholder = findPlaceholderPayee([
2754
+ config.payeeAddress,
2755
+ ...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
2756
+ ]);
2757
+ if (placeholder) {
2758
+ issues.push({
2759
+ code: "placeholder_payee",
2760
+ protocol: "x402",
2761
+ message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
2762
+ });
2763
+ }
2764
+ if (options.requireCdpKeys !== false && usesDefaultEvmFacilitator(config)) {
2765
+ const missing = [
2766
+ env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
2767
+ env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
2768
+ ].filter(Boolean);
2769
+ if (missing.length > 0) {
2770
+ issues.push({
2771
+ code: "missing_cdp_keys",
2772
+ protocol: "x402",
2773
+ message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
2774
+ });
2775
+ }
2776
+ }
2777
+ return issues;
2778
+ }
2779
+ function validateMppConfig(config, env) {
2780
+ const issues = [];
2781
+ const mpp = config.mpp;
2782
+ if (!mpp) {
2783
+ return [
2784
+ {
2785
+ code: "missing_mpp_config",
2786
+ protocol: "mpp",
2787
+ message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
2788
+ }
2789
+ ];
2790
+ }
2791
+ if (!mpp.secretKey) {
2792
+ issues.push({
2793
+ code: "missing_mpp_secret_key",
2794
+ protocol: "mpp",
2795
+ message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
2796
+ });
2797
+ }
2798
+ if (!mpp.currency) {
2799
+ issues.push({
2800
+ code: "missing_mpp_currency",
2801
+ protocol: "mpp",
2802
+ message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
2803
+ });
2804
+ }
2805
+ if (!mpp.recipient && !config.payeeAddress) {
2806
+ issues.push({
2807
+ code: "missing_mpp_recipient",
2808
+ protocol: "mpp",
2809
+ message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
2810
+ });
2811
+ }
2812
+ const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
2813
+ if (placeholder) {
2814
+ issues.push({
2815
+ code: "placeholder_payee",
2816
+ protocol: "mpp",
2817
+ message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
2818
+ });
2819
+ }
2820
+ if (!(mpp.rpcUrl ?? env.TEMPO_RPC_URL)) {
2821
+ issues.push({
2822
+ code: "missing_mpp_rpc_url",
2823
+ protocol: "mpp",
2824
+ message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
2825
+ });
2826
+ }
2827
+ if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
2828
+ issues.push({
2829
+ code: "missing_mpp_default_store_env",
2830
+ protocol: "mpp",
2831
+ message: "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
2832
+ });
2833
+ }
2834
+ return issues;
2835
+ }
2836
+ function usesDefaultEvmFacilitator(config) {
2837
+ return getConfiguredX402Networks(config).some(
2838
+ (network) => typeof network === "string" && isEvmNetwork(network)
2839
+ ) && config.x402?.facilitators?.evm === void 0;
2840
+ }
2841
+ function isSupportedX402Network(network) {
2842
+ return isEvmNetwork(network) || isSolanaNetwork(network);
2843
+ }
2844
+ function findPlaceholderPayee(values) {
2845
+ return values.find((value) => value?.toLowerCase() === ZERO_EVM_ADDRESS) ?? null;
2846
+ }
2847
+
2848
+ // src/index.ts
2849
+ init_constants();
2556
2850
  function createRouter(config) {
2557
2851
  const registry = new RouteRegistry();
2558
2852
  const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
2559
2853
  const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
2560
- const network = config.network ?? "eip155:8453";
2854
+ const network = config.network ?? BASE_NETWORK;
2561
2855
  const x402Accepts = getConfiguredX402Accepts(config);
2562
- if (!config.baseUrl) {
2563
- throw new Error(
2564
- '[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.'
2565
- );
2566
- }
2567
- if (config.protocols && config.protocols.length === 0) {
2568
- throw new Error(
2569
- "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
2570
- );
2571
- }
2572
- const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
2573
- let x402ConfigError;
2574
- let mppConfigError;
2575
- if (!config.protocols || config.protocols.includes("x402")) {
2576
- if (x402Accepts.length === 0) {
2577
- x402ConfigError = "x402 requires at least one accept configuration.";
2578
- } else if (x402Accepts.some((accept) => !accept.network)) {
2579
- x402ConfigError = "x402 accepts require a network.";
2580
- } else if (x402Accepts.some((accept) => !isSupportedX402Network(accept.network))) {
2581
- const unsupported = x402Accepts.find((accept) => !isSupportedX402Network(accept.network));
2582
- x402ConfigError = `unsupported x402 network '${unsupported?.network}'. Use eip155:* or solana:*.`;
2583
- } else if (x402Accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset)) {
2584
- x402ConfigError = "non-exact x402 accepts require an asset.";
2585
- } else if (x402Accepts.some(
2586
- (accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
2587
- )) {
2588
- x402ConfigError = "x402 accept decimals must be a non-negative integer.";
2589
- } else if (x402Accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
2590
- x402ConfigError = "x402 requires payeeAddress in router config or payTo on every x402 accept.";
2591
- }
2592
- }
2593
- if (config.protocols?.includes("mpp")) {
2594
- if (!config.mpp) {
2595
- mppConfigError = 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.';
2596
- } else if (!config.mpp.recipient && !config.payeeAddress) {
2597
- mppConfigError = "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config.";
2598
- } else if (!(config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL)) {
2599
- mppConfigError = "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object.";
2600
- }
2601
- }
2602
- const allConfigErrors = [x402ConfigError, mppConfigError].filter(Boolean);
2603
- if (allConfigErrors.length > 0) {
2604
- for (const err of allConfigErrors) console.error(`[router] ${err}`);
2856
+ const configIssues = getRouterConfigIssues(config, {
2857
+ requireCdpKeys: process.env.NODE_ENV === "production"
2858
+ });
2859
+ const baseUrlIssue = configIssues.find((issue) => issue.code === "missing_base_url");
2860
+ if (baseUrlIssue) throw new RouterConfigError([baseUrlIssue]);
2861
+ const emptyProtocolsIssue = configIssues.find((issue) => issue.code === "empty_protocols");
2862
+ if (emptyProtocolsIssue) throw new RouterConfigError([emptyProtocolsIssue]);
2863
+ const protocolConfigIssues = configIssues.filter(
2864
+ (issue) => issue.code !== "missing_base_url" && issue.code !== "empty_protocols"
2865
+ );
2866
+ const x402ConfigIssues = protocolConfigIssues.filter((issue) => issue.protocol === "x402");
2867
+ const mppConfigIssues = protocolConfigIssues.filter((issue) => issue.protocol === "mpp");
2868
+ const x402ConfigError = x402ConfigIssues.length > 0 ? formatRouterConfigIssues(x402ConfigIssues) : void 0;
2869
+ const mppConfigError = mppConfigIssues.length > 0 ? formatRouterConfigIssues(mppConfigIssues) : void 0;
2870
+ if (protocolConfigIssues.length > 0) {
2871
+ for (const issue of protocolConfigIssues) console.error(`[router] ${issue.message}`);
2605
2872
  if (process.env.NODE_ENV === "production") {
2606
- throw new Error(allConfigErrors.join("\n"));
2873
+ throw new RouterConfigError(protocolConfigIssues);
2607
2874
  }
2608
2875
  }
2876
+ const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
2609
2877
  if (config.plugin?.init) {
2610
2878
  try {
2611
2879
  const result = config.plugin.init({ origin: resolvedBaseUrl });
@@ -2750,9 +3018,6 @@ function createRouter(config) {
2750
3018
  registry
2751
3019
  };
2752
3020
  }
2753
- function isSupportedX402Network(network) {
2754
- return isEvmNetwork(network) || isSolanaNetwork(network);
2755
- }
2756
3021
  function normalizePath(path) {
2757
3022
  let normalized = path.trim();
2758
3023
  normalized = normalized.replace(/^\/+/, "");
@@ -2760,15 +3025,26 @@ function normalizePath(path) {
2760
3025
  return normalized.replace(/\/+$/, "");
2761
3026
  }
2762
3027
  export {
3028
+ BASE_NETWORK,
2763
3029
  HttpError,
2764
3030
  MemoryEntitlementStore,
2765
3031
  MemoryNonceStore,
2766
3032
  RouteBuilder,
2767
3033
  RouteRegistry,
3034
+ RouterConfigError,
2768
3035
  SIWX_CHALLENGE_EXPIRY_MS,
2769
3036
  SIWX_ERROR_MESSAGES,
3037
+ SOLANA_MAINNET_NETWORK,
3038
+ TEMPO_USDC_CURRENCY,
3039
+ ZERO_EVM_ADDRESS,
2770
3040
  consolePlugin,
2771
3041
  createRedisEntitlementStore,
2772
3042
  createRedisNonceStore,
2773
- createRouter
3043
+ createRouter,
3044
+ formatRouterConfigIssues,
3045
+ getRouterConfigIssues,
3046
+ mppFromEnv,
3047
+ paidOptionsForProtocols,
3048
+ validateRouterConfig,
3049
+ x402AcceptsFromEnv
2774
3050
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Unified route builder for Next.js App Router APIs with x402, MPP, SIWX, and API key auth",
5
5
  "type": "module",
6
6
  "exports": {