@agentcash/router 1.5.2 → 1.6.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
@@ -105,6 +105,18 @@ function buildEvmExactOptions(accepts, price) {
105
105
  payTo
106
106
  }));
107
107
  }
108
+ function buildEvmUptoOptions(accepts, price) {
109
+ return accepts.filter(
110
+ (accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
111
+ ).map((accept) => ({
112
+ scheme: "upto",
113
+ network: accept.network,
114
+ payTo: accept.payTo,
115
+ price,
116
+ ...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
117
+ ...accept.extra ? { extra: accept.extra } : {}
118
+ }));
119
+ }
108
120
  var init_evm = __esm({
109
121
  "src/protocols/x402/evm.ts"() {
110
122
  "use strict";
@@ -289,40 +301,40 @@ async function createX402Server(config) {
289
301
  facilitatorsByNetwork
290
302
  };
291
303
  }
292
- function cachedClient(inner, kinds) {
304
+ function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
305
+ const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
306
+ return groups.map((group) => {
307
+ const inner = new HTTPFacilitatorClient(group.config);
308
+ const kinds = buildSupportedKinds(group);
309
+ return hardcodedSupportedClient(inner, kinds);
310
+ });
311
+ }
312
+ function hardcodedSupportedClient(inner, kinds) {
293
313
  return {
294
314
  verify: inner.verify.bind(inner),
295
315
  settle: inner.settle.bind(inner),
296
- getSupported: async () => ({
297
- kinds,
298
- extensions: [],
299
- signers: {}
300
- })
316
+ getSupported: async () => ({ kinds, extensions: [], signers: {} })
301
317
  };
302
318
  }
303
- function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
304
- const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
305
- return groups.map((group) => {
306
- const inner = new HTTPFacilitatorClient(group.config);
307
- const kinds = group.networks.flatMap((network) => {
308
- const exactKind = {
309
- x402Version: 2,
310
- scheme: "exact",
311
- network,
312
- ...group.family === "solana" ? {
313
- extra: {
314
- features: {
315
- xSettlementAccountSupported: true
316
- }
319
+ function buildSupportedKinds(group) {
320
+ return group.networks.flatMap((network) => {
321
+ const exactKind = {
322
+ x402Version: 2,
323
+ scheme: "exact",
324
+ network,
325
+ ...group.family === "solana" ? {
326
+ extra: {
327
+ features: {
328
+ xSettlementAccountSupported: true
317
329
  }
318
- } : {}
319
- };
320
- if (group.family === "evm") {
321
- return [exactKind, { x402Version: 2, scheme: "upto", network }];
322
- }
323
- return [exactKind];
324
- });
325
- return cachedClient(inner, kinds);
330
+ }
331
+ } : {}
332
+ };
333
+ const uptoKind = { x402Version: 2, scheme: "upto", network };
334
+ if (group.family === "evm") {
335
+ return [exactKind, uptoKind];
336
+ }
337
+ return [exactKind, uptoKind];
326
338
  });
327
339
  }
328
340
  var init_server = __esm({
@@ -335,57 +347,6 @@ var init_server = __esm({
335
347
  }
336
348
  });
337
349
 
338
- // src/upstash-rest.ts
339
- var upstash_rest_exports = {};
340
- __export(upstash_rest_exports, {
341
- createUpstashRest: () => createUpstashRest
342
- });
343
- function createUpstashRest(url, token) {
344
- const base = url.replace(/\/+$/, "");
345
- const headers = { Authorization: `Bearer ${token}` };
346
- async function get(key) {
347
- const res = await fetch(`${base}/get/${key}`, { headers });
348
- if (!res.ok) throw new Error(`[upstash-rest] GET ${key}: ${res.status}`);
349
- const { result } = await res.json();
350
- return result ?? null;
351
- }
352
- async function set(key, value) {
353
- const res = await fetch(`${base}`, {
354
- method: "POST",
355
- headers: { ...headers, "Content-Type": "application/json" },
356
- body: JSON.stringify(["SET", key, JSON.stringify(value)])
357
- });
358
- if (!res.ok) throw new Error(`[upstash-rest] SET ${key}: ${res.status}`);
359
- return await res.json();
360
- }
361
- async function del(key) {
362
- const res = await fetch(`${base}`, {
363
- method: "POST",
364
- headers: { ...headers, "Content-Type": "application/json" },
365
- body: JSON.stringify(["DEL", key])
366
- });
367
- if (!res.ok) throw new Error(`[upstash-rest] DEL ${key}: ${res.status}`);
368
- return await res.json();
369
- }
370
- return {
371
- get,
372
- set,
373
- del,
374
- async update(key, fn) {
375
- const current = await get(key);
376
- const change = fn(current);
377
- if (change.op === "set") await set(key, change.value);
378
- if (change.op === "delete") await del(key);
379
- return change.result;
380
- }
381
- };
382
- }
383
- var init_upstash_rest = __esm({
384
- "src/upstash-rest.ts"() {
385
- "use strict";
386
- }
387
- });
388
-
389
350
  // src/index.ts
390
351
  var index_exports = {};
391
352
  __export(index_exports, {
@@ -402,14 +363,16 @@ __export(index_exports, {
402
363
  TEMPO_USDC_CURRENCY: () => TEMPO_USDC_CURRENCY,
403
364
  ZERO_EVM_ADDRESS: () => ZERO_EVM_ADDRESS,
404
365
  consolePlugin: () => consolePlugin,
405
- createRedisEntitlementStore: () => createRedisEntitlementStore,
406
- createRedisNonceStore: () => createRedisNonceStore,
366
+ createKvEntitlementStore: () => createKvEntitlementStore,
367
+ createKvMppStore: () => createKvMppStore,
368
+ createKvNonceStore: () => createKvNonceStore,
407
369
  createRouter: () => createRouter,
408
370
  formatRouterConfigIssues: () => formatRouterConfigIssues,
409
371
  getRouterConfigIssues: () => getRouterConfigIssues,
410
372
  mppFromEnv: () => mppFromEnv,
411
373
  paidOptionsForProtocols: () => paidOptionsForProtocols,
412
374
  validateRouterConfig: () => validateRouterConfig,
375
+ withPrefix: () => withPrefix,
413
376
  x402AcceptsFromEnv: () => x402AcceptsFromEnv
414
377
  });
415
378
  module.exports = __toCommonJS(index_exports);
@@ -417,11 +380,6 @@ module.exports = __toCommonJS(index_exports);
417
380
  // src/registry.ts
418
381
  var RouteRegistry = class {
419
382
  routes = /* @__PURE__ */ new Map();
420
- // Internal map key includes the HTTP method so that POST and DELETE on the
421
- // same path coexist. Within the same path+method, last-write-wins is still
422
- // intentional — Next.js module loading order is non-deterministic during
423
- // build and discovery stubs may register the same route in either order.
424
- // Prior art: ElysiaJS uses the same pattern (silent overwrite in router.history).
425
383
  mapKey(entry) {
426
384
  return `${entry.key}:${entry.method}`;
427
385
  }
@@ -434,8 +392,6 @@ var RouteRegistry = class {
434
392
  }
435
393
  this.routes.set(k, entry);
436
394
  }
437
- // Accepts either a compound key ("site/domain:DELETE") or a path-only key
438
- // ("site/domain") — path-only returns the first registered method for that path.
439
395
  get(key) {
440
396
  const direct = this.routes.get(key);
441
397
  if (direct) return direct;
@@ -467,24 +423,17 @@ var RouteRegistry = class {
467
423
 
468
424
  // src/headers.ts
469
425
  var HEADERS = {
470
- // ---- Standard HTTP ----
471
426
  AUTHORIZATION: "Authorization",
472
427
  WWW_AUTHENTICATE: "WWW-Authenticate",
473
- // ---- Auth ----
474
428
  API_KEY: "X-API-Key",
475
- // ---- Request meta (used by plugin/observability) ----
476
429
  WALLET_ADDRESS: "X-Wallet-Address",
477
430
  CLIENT_ID: "X-Client-ID",
478
431
  SESSION_ID: "X-Session-ID",
479
- // ---- SIWX ----
480
432
  SIWX: "SIGN-IN-WITH-X",
481
- // ---- x402 (payment) ----
482
433
  X402_PAYMENT_SIGNATURE: "PAYMENT-SIGNATURE",
483
- /** Legacy x402 payment header — accepted alongside PAYMENT-SIGNATURE. */
484
434
  X402_PAYMENT_LEGACY: "X-PAYMENT",
485
435
  X402_PAYMENT_REQUIRED: "PAYMENT-REQUIRED",
486
436
  X402_PAYMENT_RESPONSE: "PAYMENT-RESPONSE",
487
- // ---- MPP (payment) ----
488
437
  MPP_PAYMENT_RECEIPT: "Payment-Receipt"
489
438
  };
490
439
  var AUTH_SCHEME = {
@@ -567,11 +516,31 @@ function consolePlugin() {
567
516
  };
568
517
  }
569
518
 
519
+ // src/alert.ts
520
+ function createReporter(plugin, pluginCtx, route) {
521
+ return (level, message, meta) => {
522
+ firePluginHook(plugin, "onAlert", pluginCtx, {
523
+ level,
524
+ message,
525
+ route,
526
+ ...meta ? { meta } : {}
527
+ });
528
+ };
529
+ }
530
+
570
531
  // src/pipeline/context/preflight.ts
571
532
  function preflight(routeEntry, handler, deps, request) {
572
533
  const meta = buildMeta(request, routeEntry);
573
534
  const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
574
- return { routeEntry, handler, deps, request, meta, pluginCtx };
535
+ return {
536
+ routeEntry,
537
+ handler,
538
+ deps,
539
+ request,
540
+ meta,
541
+ pluginCtx,
542
+ report: createReporter(deps.plugin, pluginCtx, routeEntry.key)
543
+ };
575
544
  }
576
545
  function buildMeta(request, routeEntry) {
577
546
  return {
@@ -616,19 +585,38 @@ function validateBody(parsed, schema) {
616
585
  };
617
586
  }
618
587
 
588
+ // src/pipeline/context/fire-plugin-response.ts
589
+ function firePluginResponse(ctx, response, requestBody, responseBody) {
590
+ firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
591
+ statusCode: response.status,
592
+ statusText: response.statusText,
593
+ duration: Date.now() - ctx.meta.startTime,
594
+ contentType: response.headers.get("content-type"),
595
+ headers: Object.fromEntries(response.headers.entries()),
596
+ requestBody,
597
+ responseBody
598
+ });
599
+ if (response.status >= 400 && response.status !== 402) {
600
+ firePluginHook(ctx.deps.plugin, "onError", ctx.pluginCtx, {
601
+ status: response.status,
602
+ message: response.statusText || `HTTP ${response.status}`,
603
+ settled: false
604
+ });
605
+ }
606
+ }
607
+
619
608
  // src/pipeline/context/parse-body.ts
620
- async function parseBody(request, routeEntry) {
621
- if (!routeEntry.bodySchema) return { ok: true, data: void 0 };
609
+ async function parseBody(ctx, request = ctx.request) {
610
+ if (!ctx.routeEntry.bodySchema) return { ok: true, data: void 0 };
622
611
  const raw = await bufferBody(request);
623
- const result = validateBody(raw, routeEntry.bodySchema);
612
+ const result = validateBody(raw, ctx.routeEntry.bodySchema);
624
613
  if (result.success) return { ok: true, data: result.data };
625
- return {
626
- ok: false,
627
- response: import_server.NextResponse.json(
628
- { success: false, error: result.error, issues: result.issues },
629
- { status: 400 }
630
- )
631
- };
614
+ const response = import_server.NextResponse.json(
615
+ { success: false, error: result.error, issues: result.issues },
616
+ { status: 400 }
617
+ );
618
+ firePluginResponse(ctx, response);
619
+ return { ok: false, response };
632
620
  }
633
621
 
634
622
  // src/pipeline/context/parse-query.ts
@@ -654,28 +642,6 @@ function handlerFailureError(response) {
654
642
 
655
643
  // src/pipeline/context/fail.ts
656
644
  var import_server2 = require("next/server");
657
-
658
- // src/pipeline/context/fire-plugin-response.ts
659
- function firePluginResponse(ctx, response, requestBody, responseBody) {
660
- firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
661
- statusCode: response.status,
662
- statusText: response.statusText,
663
- duration: Date.now() - ctx.meta.startTime,
664
- contentType: response.headers.get("content-type"),
665
- headers: Object.fromEntries(response.headers.entries()),
666
- requestBody,
667
- responseBody
668
- });
669
- if (response.status >= 400 && response.status !== 402) {
670
- firePluginHook(ctx.deps.plugin, "onError", ctx.pluginCtx, {
671
- status: response.status,
672
- message: response.statusText || `HTTP ${response.status}`,
673
- settled: false
674
- });
675
- }
676
- }
677
-
678
- // src/pipeline/context/fail.ts
679
645
  function fail(ctx, status, message, requestBody) {
680
646
  const response = import_server2.NextResponse.json({ success: false, error: message }, { status });
681
647
  firePluginResponse(ctx, response, requestBody);
@@ -693,7 +659,7 @@ async function runValidate(ctx, body) {
693
659
  }
694
660
  }
695
661
 
696
- // src/handler.ts
662
+ // src/pipeline/flows/static/static-invoke.ts
697
663
  var import_server3 = require("next/server");
698
664
 
699
665
  // src/types.ts
@@ -705,23 +671,15 @@ var HttpError = class extends Error {
705
671
  }
706
672
  };
707
673
 
708
- // src/handler.ts
709
- async function safeCallHandler(handler, ctx, options = {}) {
710
- try {
711
- const result = await handler(ctx);
712
- if (result instanceof Response) return result;
713
- return import_server3.NextResponse.json(result);
714
- } catch (error) {
715
- options.onError?.(error);
716
- const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
717
- const message = error instanceof Error ? error.message : "Internal error";
718
- return import_server3.NextResponse.json({ success: false, error: message }, { status });
719
- }
674
+ // src/pipeline/flows/static/static-invoke.ts
675
+ function invokePaidStatic(ctx, wallet, account, body, payment) {
676
+ return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
720
677
  }
721
-
722
- // src/pipeline/context/invoke.ts
723
- async function invoke(ctx, wallet, account, body, payment) {
724
- const handlerCtx = {
678
+ function invokeUnauthed(ctx, wallet, account, body) {
679
+ return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, null));
680
+ }
681
+ function buildHandlerCtx(ctx, wallet, account, body, payment) {
682
+ return {
725
683
  body,
726
684
  query: parseQuery(ctx.request, ctx.routeEntry),
727
685
  request: ctx.request,
@@ -740,21 +698,45 @@ async function invoke(ctx, wallet, account, body, payment) {
740
698
  },
741
699
  setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
742
700
  };
701
+ }
702
+ async function runHandler(ctx, handlerCtx) {
703
+ let returned;
704
+ try {
705
+ returned = ctx.handler(handlerCtx);
706
+ } catch (error) {
707
+ return errorResult(error);
708
+ }
709
+ if (isAsyncIterable(returned) && !isThenable(returned)) {
710
+ return errorResult(
711
+ new HttpError(
712
+ `route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
713
+ 500
714
+ )
715
+ );
716
+ }
743
717
  let rawResult;
744
- let handlerError;
745
- const response = await safeCallHandler(
746
- async (c) => {
747
- rawResult = await ctx.handler(c);
748
- return rawResult;
749
- },
750
- handlerCtx,
751
- {
752
- onError(error) {
753
- handlerError = error;
754
- }
755
- }
756
- );
757
- return { response, rawResult, handlerError };
718
+ try {
719
+ rawResult = await returned;
720
+ } catch (error) {
721
+ return errorResult(error);
722
+ }
723
+ const response = rawResult instanceof Response ? rawResult : import_server3.NextResponse.json(rawResult);
724
+ return { response, rawResult };
725
+ }
726
+ function errorResult(error) {
727
+ const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
728
+ const message = error instanceof Error ? error.message : "Internal error";
729
+ return {
730
+ response: import_server3.NextResponse.json({ success: false, error: message }, { status }),
731
+ rawResult: void 0,
732
+ handlerError: error
733
+ };
734
+ }
735
+ function isAsyncIterable(value) {
736
+ return value != null && typeof value === "object" && Symbol.asyncIterator in value;
737
+ }
738
+ function isThenable(value) {
739
+ return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
758
740
  }
759
741
 
760
742
  // src/pipeline/context/fire-provider-quota.ts
@@ -788,24 +770,24 @@ function computeQuotaLevel(remaining, warn, critical) {
788
770
  return "healthy";
789
771
  }
790
772
 
791
- // src/pipeline/context/finalize.ts
773
+ // src/pipeline/context/finalize/response.ts
792
774
  function finalize(ctx, response, rawResult, requestBody) {
793
775
  fireProviderQuota(ctx, response, rawResult);
794
776
  firePluginResponse(ctx, response, requestBody, rawResult);
795
777
  return response;
796
778
  }
797
779
 
798
- // src/pipeline/context/run-handler-only.ts
799
- async function runHandlerOnly(ctx, wallet, account) {
800
- const body = await parseBody(ctx.request, ctx.routeEntry);
801
- if (!body.ok) {
802
- firePluginResponse(ctx, body.response);
803
- return body.response;
780
+ // src/pipeline/context/grant-entitlement.ts
781
+ async function grantEntitlementIfSiwx(ctx, wallet) {
782
+ if (!ctx.routeEntry.siwxEnabled) return;
783
+ try {
784
+ await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
785
+ } catch (error) {
786
+ ctx.report(
787
+ "warn",
788
+ `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`
789
+ );
804
790
  }
805
- const validateErr = await runValidate(ctx, body.data);
806
- if (validateErr) return validateErr;
807
- const result = await invoke(ctx, wallet, account, body.data, null);
808
- return finalize(ctx, result.response, result.rawResult, body.data);
809
791
  }
810
792
 
811
793
  // src/pipeline/context/settlement-context.ts
@@ -822,23 +804,6 @@ function settlementContext(ctx, scope) {
822
804
  };
823
805
  }
824
806
 
825
- // src/pipeline/context/run-before-settle.ts
826
- async function runBeforeSettle(ctx, scope) {
827
- const hook = ctx.routeEntry.settlement?.beforeSettle;
828
- if (!hook) return null;
829
- try {
830
- await hook(settlementContext(ctx, scope));
831
- return null;
832
- } catch (error) {
833
- return fail(
834
- ctx,
835
- errorStatus(error, 500),
836
- errorMessage(error, "Pre-settlement validation failed"),
837
- scope.body
838
- );
839
- }
840
- }
841
-
842
807
  // src/pipeline/context/run-settlement-error.ts
843
808
  async function runSettlementError(ctx, scope, error, phase) {
844
809
  const hook = ctx.routeEntry.settlement?.onSettlementError;
@@ -847,12 +812,7 @@ async function runSettlementError(ctx, scope, error, phase) {
847
812
  await hook({ ...settlementContext(ctx, scope), error, phase });
848
813
  } catch (hookError) {
849
814
  const message = errorMessage(hookError, "Settlement error hook failed");
850
- console.error(`[router] ${ctx.routeEntry.key}: onSettlementError failed: ${message}`);
851
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
852
- level: "error",
853
- message: `Settlement error hook failed: ${message}`,
854
- route: ctx.routeEntry.key
855
- });
815
+ ctx.report("error", `Settlement error hook failed: ${message}`);
856
816
  }
857
817
  }
858
818
 
@@ -864,81 +824,145 @@ async function runAfterSettle(ctx, scope) {
864
824
  await hook(settlementContext(ctx, scope));
865
825
  } catch (error) {
866
826
  const message = errorMessage(error, "Post-settlement hook failed");
867
- console.error(`[router] ${ctx.routeEntry.key}: afterSettle failed: ${message}`);
868
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
869
- level: "error",
870
- message: `Post-settlement hook failed: ${message}`,
871
- route: ctx.routeEntry.key
872
- });
827
+ ctx.report("error", `Post-settlement hook failed: ${message}`);
873
828
  await runSettlementError(ctx, scope, error, "afterSettle");
874
829
  }
875
830
  }
876
831
 
877
- // src/pipeline/context/run-settled-handler-error.ts
878
- async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
879
- const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
880
- if (!hook) return;
881
- try {
882
- await hook({ ...settlementContext(ctx, scope), error });
883
- } catch (hookError) {
884
- const message = errorMessage(hookError, "Settled handler error hook failed");
885
- console.error(`[router] ${ctx.routeEntry.key}: onSettledHandlerError failed: ${message}`);
886
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
887
- level: "error",
888
- message: `Settled handler error hook failed: ${message}`,
889
- route: ctx.routeEntry.key
890
- });
891
- }
892
- }
893
-
894
- // src/pipeline/context/grant-entitlement.ts
895
- async function grantEntitlementIfSiwx(ctx, wallet) {
896
- if (!ctx.routeEntry.siwxEnabled) return;
897
- try {
898
- await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
899
- } catch (error) {
900
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
901
- level: "warn",
902
- message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
903
- route: ctx.routeEntry.key
904
- });
905
- }
832
+ // src/pipeline/context/finalize/epilogue.ts
833
+ async function runPostSettleEpilogue(args) {
834
+ const { ctx, strategy, wallet, settle, afterSettleScope, rawResult, body } = args;
835
+ await grantEntitlementIfSiwx(ctx, wallet);
836
+ firePluginHook(ctx.deps.plugin, "onPaymentSettled", ctx.pluginCtx, {
837
+ protocol: strategy.protocol,
838
+ payer: wallet,
839
+ transaction: settle.settledPayment.transaction ?? "",
840
+ network: settle.settledPayment.network
841
+ });
842
+ await runAfterSettle(ctx, afterSettleScope);
843
+ return finalize(ctx, settle.response, rawResult, body);
906
844
  }
907
845
 
908
- // src/pipeline/context/settle-and-finalize.ts
909
- async function settleAndFinalize(args) {
910
- const { ctx, strategy, verifyOutcome, scope, rawResult, body, onSettleError } = args;
911
- const { request, routeEntry, deps } = ctx;
846
+ // src/pipeline/context/finalize/request.ts
847
+ async function settleAndFinalizeRequest(args) {
848
+ const { ctx, strategy, verifyOutcome, scope, rawResult, body, billedAmount, onSettleError } = args;
849
+ const { request, routeEntry, deps, report } = ctx;
912
850
  const settle = await strategy.settle({
913
851
  request,
914
852
  response: scope.response,
915
853
  payment: verifyOutcome.payment,
916
854
  token: verifyOutcome.token,
917
855
  routeEntry,
918
- deps
856
+ deps,
857
+ billedAmount,
858
+ report
919
859
  });
920
860
  if (!settle.ok) {
921
861
  if (onSettleError) await onSettleError(settle.error, settle.failMessage);
922
862
  return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
923
863
  }
924
- await grantEntitlementIfSiwx(ctx, verifyOutcome.wallet);
925
- firePluginHook(deps.plugin, "onPaymentSettled", ctx.pluginCtx, {
926
- protocol: strategy.protocol,
927
- payer: verifyOutcome.wallet,
928
- transaction: settle.settledPayment.transaction ?? "",
929
- network: settle.settledPayment.network
864
+ return runPostSettleEpilogue({
865
+ ctx,
866
+ strategy,
867
+ wallet: verifyOutcome.wallet,
868
+ settle,
869
+ afterSettleScope: {
870
+ ...scope,
871
+ payment: settle.settledPayment,
872
+ response: settle.response
873
+ },
874
+ rawResult,
875
+ body
930
876
  });
931
- await runAfterSettle(ctx, {
932
- ...scope,
933
- payment: settle.settledPayment,
934
- response: settle.response
877
+ }
878
+
879
+ // src/pipeline/context/finalize/stream.ts
880
+ async function settleAndFinalizeStream(args) {
881
+ const { ctx, strategy, verifyOutcome, source, account, body, bindChannelCharge } = args;
882
+ const { request, routeEntry, deps, report } = ctx;
883
+ if (!strategy.settleStream) {
884
+ return fail(ctx, 500, `${strategy.protocol} does not support streaming handlers`, body);
885
+ }
886
+ const settle = await strategy.settleStream({
887
+ request,
888
+ source,
889
+ payment: verifyOutcome.payment,
890
+ token: verifyOutcome.token,
891
+ routeEntry,
892
+ deps,
893
+ bindChannelCharge,
894
+ report
935
895
  });
936
- return finalize(ctx, settle.response, rawResult, body);
896
+ if (!settle.ok) {
897
+ return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
898
+ }
899
+ return runPostSettleEpilogue({
900
+ ctx,
901
+ strategy,
902
+ wallet: verifyOutcome.wallet,
903
+ settle,
904
+ afterSettleScope: {
905
+ wallet: verifyOutcome.wallet,
906
+ account,
907
+ body,
908
+ payment: settle.settledPayment,
909
+ response: settle.response,
910
+ rawResult: void 0
911
+ },
912
+ rawResult: void 0,
913
+ body
914
+ });
915
+ }
916
+
917
+ // src/pipeline/context/run-handler-only.ts
918
+ async function runHandlerOnly(ctx, wallet, account) {
919
+ const body = await parseBody(ctx);
920
+ if (!body.ok) return body.response;
921
+ const validateErr = await runValidate(ctx, body.data);
922
+ if (validateErr) return validateErr;
923
+ const result = await invokeUnauthed(ctx, wallet, account, body.data);
924
+ return finalize(ctx, result.response, result.rawResult, body.data);
925
+ }
926
+
927
+ // src/pipeline/context/run-before-settle.ts
928
+ async function runBeforeSettle(ctx, scope) {
929
+ const hook = ctx.routeEntry.settlement?.beforeSettle;
930
+ if (!hook) return null;
931
+ try {
932
+ await hook(settlementContext(ctx, scope));
933
+ return null;
934
+ } catch (error) {
935
+ return fail(
936
+ ctx,
937
+ errorStatus(error, 500),
938
+ errorMessage(error, "Pre-settlement validation failed"),
939
+ scope.body
940
+ );
941
+ }
942
+ }
943
+
944
+ // src/pipeline/context/run-settled-handler-error.ts
945
+ async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
946
+ const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
947
+ if (!hook) return;
948
+ try {
949
+ await hook({ ...settlementContext(ctx, scope), error });
950
+ } catch (hookError) {
951
+ const message = errorMessage(hookError, "Settled handler error hook failed");
952
+ ctx.report("error", `Settled handler error hook failed: ${message}`);
953
+ }
937
954
  }
938
955
 
939
956
  // src/auth/normalize-wallet.ts
940
957
  function normalizeWalletAddress(address) {
941
- return /^0x/i.test(address) ? address.toLowerCase() : address;
958
+ const isEvm = /^0x/i.test(address);
959
+ return isEvm ? normalizeEvmWalletAddress(address) : normalizeSolanaWalletAddress(address);
960
+ }
961
+ function normalizeEvmWalletAddress(address) {
962
+ return address.toLowerCase();
963
+ }
964
+ function normalizeSolanaWalletAddress(address) {
965
+ return address;
942
966
  }
943
967
 
944
968
  // src/auth/siwx.ts
@@ -1018,20 +1042,18 @@ function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
1018
1042
  return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
1019
1043
  }
1020
1044
 
1021
- // src/pipeline/context/protocol-init-error.ts
1022
- function protocolInitError(routeEntry, deps) {
1023
- if (!routeEntry.pricing) return null;
1024
- const errors = [];
1025
- for (const protocol of routeEntry.protocols) {
1026
- if (protocol === "x402" && deps.x402InitError) {
1027
- errors.push(`x402: ${deps.x402InitError}`);
1028
- }
1029
- if (protocol === "mpp" && deps.mppInitError) {
1030
- errors.push(`mpp: ${deps.mppInitError}`);
1031
- }
1032
- }
1033
- if (errors.length === 0) return null;
1034
- return `Payment protocol initialization failed. ${errors.join("; ")}`;
1045
+ // src/pipeline/context/resolve-early-body.ts
1046
+ async function resolveEarlyBody(args) {
1047
+ const { ctx, pricing, incomingStrategy } = args;
1048
+ if (!shouldParseBodyEarly(incomingStrategy, ctx.routeEntry, pricing)) {
1049
+ return { ok: true, earlyBody: void 0 };
1050
+ }
1051
+ const earlyClone = ctx.request.clone();
1052
+ const earlyResult = await parseBody(ctx, earlyClone);
1053
+ if (!earlyResult.ok) return { ok: false, response: earlyResult.response };
1054
+ const validateErr = await runValidate(ctx, earlyResult.data);
1055
+ if (validateErr) return { ok: false, response: validateErr };
1056
+ return { ok: true, earlyBody: earlyResult.data };
1035
1057
  }
1036
1058
 
1037
1059
  // src/auth/api-key.ts
@@ -1052,6 +1074,39 @@ function extractBearerToken(header) {
1052
1074
  return null;
1053
1075
  }
1054
1076
 
1077
+ // src/pipeline/context/run-api-key-gate.ts
1078
+ async function runApiKeyGate(ctx) {
1079
+ const { request, routeEntry, deps } = ctx;
1080
+ if (!routeEntry.apiKeyResolver) return { ok: true, account: void 0 };
1081
+ const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
1082
+ if (!apiKeyResult.valid) {
1083
+ return { ok: false, response: fail(ctx, 401, "Invalid or missing API key") };
1084
+ }
1085
+ firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
1086
+ authMode: "apiKey",
1087
+ wallet: null,
1088
+ route: routeEntry.key,
1089
+ account: apiKeyResult.account
1090
+ });
1091
+ return { ok: true, account: apiKeyResult.account };
1092
+ }
1093
+
1094
+ // src/pipeline/context/protocol-init-error.ts
1095
+ function protocolInitError(routeEntry, deps) {
1096
+ if (!routeEntry.pricing) return null;
1097
+ const errors = [];
1098
+ for (const protocol of routeEntry.protocols) {
1099
+ if (protocol === "x402" && deps.x402InitError) {
1100
+ errors.push(`x402: ${deps.x402InitError}`);
1101
+ }
1102
+ if (protocol === "mpp" && deps.mppInitError) {
1103
+ errors.push(`mpp: ${deps.mppInitError}`);
1104
+ }
1105
+ }
1106
+ if (errors.length === 0) return null;
1107
+ return `Payment protocol initialization failed. ${errors.join("; ")}`;
1108
+ }
1109
+
1055
1110
  // src/pipeline/flows/api-key-only.ts
1056
1111
  async function runApiKeyOnlyFlow(ctx) {
1057
1112
  if (!ctx.routeEntry.apiKeyResolver) {
@@ -1221,13 +1276,22 @@ function selectPricing(raw, deps = {}) {
1221
1276
  // src/protocols/mpp/credential.ts
1222
1277
  var import_mppx = require("mppx");
1223
1278
  var import_viem = require("viem");
1279
+ var SESSION_ACTIONS = /* @__PURE__ */ new Set(["open", "topUp", "voucher", "close"]);
1224
1280
  function readMppCredential(request) {
1225
1281
  const credential = import_mppx.Credential.fromRequest(request);
1226
1282
  if (!credential) return null;
1227
1283
  const wallet = walletFromDid(credential.source ?? "");
1228
- const rawType = credential.payload?.type;
1284
+ const payload = credential.payload;
1285
+ const rawType = payload?.type;
1229
1286
  const payloadType = rawType === "transaction" ? "transaction" : rawType === "hash" ? "hash" : "unknown";
1230
- return { credential, wallet, payloadType };
1287
+ const rawAction = payload?.action;
1288
+ const sessionAction = typeof rawAction === "string" && SESSION_ACTIONS.has(rawAction) ? rawAction : void 0;
1289
+ return {
1290
+ credential,
1291
+ wallet,
1292
+ payloadType,
1293
+ ...sessionAction ? { sessionAction } : {}
1294
+ };
1231
1295
  }
1232
1296
  function walletFromDid(rawSource) {
1233
1297
  const parts = rawSource.split(":");
@@ -1235,6 +1299,168 @@ function walletFromDid(rawSource) {
1235
1299
  return normalizeWalletAddress((0, import_viem.isAddress)(last) ? (0, import_viem.getAddress)(last) : rawSource);
1236
1300
  }
1237
1301
 
1302
+ // src/protocols/mpp/session-mode.ts
1303
+ async function verifySessionMode(args, info) {
1304
+ const { request, deps, price, routeEntry } = args;
1305
+ if (!deps.mppx?.sessionRequest || !deps.mppx?.sessionStream || !deps.mppSessionConfig) {
1306
+ return {
1307
+ ok: false,
1308
+ kind: "config",
1309
+ message: "MPP sessions not configured on this server (set RouterConfig.mpp.session)"
1310
+ };
1311
+ }
1312
+ const tickCost = routeEntry.tickCost;
1313
+ const unitType = routeEntry.unitType;
1314
+ const streaming = routeEntry.streaming === true;
1315
+ const middleware = streaming ? deps.mppx.sessionStream : deps.mppx.sessionRequest;
1316
+ const middlewareRequest = isChannelOnlyAction(info, request) ? new Request(request.url, { method: request.method, headers: request.headers }) : request;
1317
+ let result;
1318
+ try {
1319
+ result = await middleware({
1320
+ amount: tickCost,
1321
+ unitType,
1322
+ suggestedDeposit: price,
1323
+ ...streaming ? { meta: { streaming: "true" } } : {}
1324
+ })(middlewareRequest);
1325
+ } catch (err) {
1326
+ const message = err instanceof Error ? err.message : String(err);
1327
+ return {
1328
+ ok: false,
1329
+ kind: "config",
1330
+ message: `MPP session verify failed: ${message}`
1331
+ };
1332
+ }
1333
+ if (result.status === 402) {
1334
+ const failure = await readMppxProblemDetails(result.challenge);
1335
+ return { ok: false, kind: "invalid", failure };
1336
+ }
1337
+ const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
1338
+ const payment = {
1339
+ protocol: "mpp",
1340
+ status: "verified",
1341
+ payer: info.wallet,
1342
+ amount: price,
1343
+ network: "tempo:4217",
1344
+ ...mppRecipient ? { recipient: mppRecipient } : {}
1345
+ };
1346
+ const token = {
1347
+ mode: "session",
1348
+ streaming,
1349
+ sessionResult: result,
1350
+ info,
1351
+ tickCost
1352
+ };
1353
+ return {
1354
+ ok: true,
1355
+ wallet: info.wallet,
1356
+ payment,
1357
+ token,
1358
+ alreadySettled: false
1359
+ };
1360
+ }
1361
+ async function settleSessionMode(args) {
1362
+ const { request, response, payment, token, billedAmount } = args;
1363
+ const sessionToken = token;
1364
+ if (isChannelOnlyAction(sessionToken.info, request)) {
1365
+ const wrapped2 = sessionToken.sessionResult.withReceipt(
1366
+ new Response(null, { status: 200 })
1367
+ );
1368
+ return {
1369
+ ok: true,
1370
+ response: wrapped2,
1371
+ settledPayment: { ...payment, status: "settled", amount: billedAmount }
1372
+ };
1373
+ }
1374
+ if (sessionToken.streaming) {
1375
+ return {
1376
+ ok: false,
1377
+ error: new Error("streaming session content settled via request path"),
1378
+ failMessage: "streaming session content settled via request path",
1379
+ failStatus: 500
1380
+ };
1381
+ }
1382
+ const wrapped = sessionToken.sessionResult.withReceipt(
1383
+ response
1384
+ );
1385
+ wrapped.headers.set("Cache-Control", "private");
1386
+ const receiptHeader = wrapped.headers.get(HEADERS.MPP_PAYMENT_RECEIPT) ?? void 0;
1387
+ const settledPayment = {
1388
+ ...payment,
1389
+ status: "settled",
1390
+ amount: billedAmount,
1391
+ ...receiptHeader ? { receipt: receiptHeader } : {}
1392
+ };
1393
+ return { ok: true, response: wrapped, settledPayment };
1394
+ }
1395
+ async function buildSessionChallenge(args) {
1396
+ const { request, deps, suggestedDeposit, routeEntry, report } = args;
1397
+ if (!deps.mppSessionConfig) return {};
1398
+ const streaming = routeEntry.streaming === true;
1399
+ const middleware = streaming ? deps.mppx?.sessionStream : deps.mppx?.sessionRequest;
1400
+ if (!middleware) return {};
1401
+ const tickCost = routeEntry.tickCost;
1402
+ const unitType = routeEntry.unitType;
1403
+ try {
1404
+ const result = await middleware({
1405
+ amount: tickCost,
1406
+ unitType,
1407
+ suggestedDeposit,
1408
+ ...streaming ? { meta: { streaming: "true" } } : {}
1409
+ })(request);
1410
+ if (result.status === 402) {
1411
+ const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
1412
+ if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
1413
+ }
1414
+ } catch (err) {
1415
+ report(
1416
+ "warn",
1417
+ `MPP session challenge build failed: ${err instanceof Error ? err.message : String(err)}`
1418
+ );
1419
+ throw err;
1420
+ }
1421
+ return {};
1422
+ }
1423
+ function isChannelOnlyAction(info, request) {
1424
+ const action = info.sessionAction;
1425
+ if (!action) return false;
1426
+ if (action === "close" || action === "topUp") return true;
1427
+ if ((action === "open" || action === "voucher") && !hasRequestBody(request)) return true;
1428
+ return false;
1429
+ }
1430
+ async function readMppxProblemDetails(challenge) {
1431
+ let body;
1432
+ try {
1433
+ body = await challenge.clone().text();
1434
+ } catch {
1435
+ return { reason: "mpp_session_invalid" };
1436
+ }
1437
+ if (!body) return { reason: "mpp_session_invalid" };
1438
+ let parsed;
1439
+ try {
1440
+ parsed = JSON.parse(body);
1441
+ } catch {
1442
+ return { reason: "mpp_session_invalid", message: body.slice(0, 500) };
1443
+ }
1444
+ if (!parsed || typeof parsed !== "object") {
1445
+ return { reason: "mpp_session_invalid" };
1446
+ }
1447
+ const details = parsed;
1448
+ const typeUri = typeof details.type === "string" ? details.type : void 0;
1449
+ const slug = typeUri ? typeUri.split("/").pop() : void 0;
1450
+ const reason = slug ? slug.replace(/-/g, "_") : "mpp_session_invalid";
1451
+ const message = typeof details.detail === "string" && details.detail.length > 0 ? details.detail : typeof details.title === "string" ? details.title : void 0;
1452
+ return message ? { reason, message } : { reason };
1453
+ }
1454
+ function hasRequestBody(request) {
1455
+ const cl = request.headers.get("content-length");
1456
+ if (cl !== null) {
1457
+ const n = Number.parseInt(cl.trim(), 10);
1458
+ return Number.isFinite(n) && n > 0;
1459
+ }
1460
+ if (request.headers.get("transfer-encoding") !== null) return true;
1461
+ return false;
1462
+ }
1463
+
1238
1464
  // src/protocols/mpp/transaction-mode.ts
1239
1465
  var import_tempo = require("viem/tempo");
1240
1466
  var import_actions = require("viem/actions");
@@ -1262,7 +1488,7 @@ async function readChallengeReason(challenge) {
1262
1488
 
1263
1489
  // src/protocols/mpp/transaction-mode.ts
1264
1490
  async function verifyTxMode(args, info) {
1265
- const { deps, price, routeEntry } = args;
1491
+ const { deps, price, report } = args;
1266
1492
  if (!deps.tempoClient) {
1267
1493
  return {
1268
1494
  ok: false,
@@ -1280,7 +1506,7 @@ async function verifyTxMode(args, info) {
1280
1506
  });
1281
1507
  } catch (err) {
1282
1508
  const message = err instanceof Error ? err.message : String(err);
1283
- console.warn(`[router] ${routeEntry.key}: MPP simulation failed \u2014 ${message}`);
1509
+ report("warn", `MPP simulation failed: ${message}`);
1284
1510
  return { ok: false, kind: "invalid" };
1285
1511
  }
1286
1512
  const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
@@ -1301,7 +1527,7 @@ async function verifyTxMode(args, info) {
1301
1527
  };
1302
1528
  }
1303
1529
  async function settleTxMode(args) {
1304
- const { request, response, payment, deps, routeEntry } = args;
1530
+ const { request, response, payment, deps, report } = args;
1305
1531
  if (!deps.mppx) {
1306
1532
  return {
1307
1533
  ok: false,
@@ -1315,7 +1541,7 @@ async function settleTxMode(args) {
1315
1541
  result = await deps.mppx.charge({ amount: payment.amount })(request);
1316
1542
  } catch (err) {
1317
1543
  const message = err instanceof Error ? err.message : String(err);
1318
- console.error(`[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`);
1544
+ report("error", `MPP broadcast failed after handler: ${message}`);
1319
1545
  return {
1320
1546
  ok: false,
1321
1547
  error: err,
@@ -1332,7 +1558,7 @@ async function settleTxMode(args) {
1332
1558
  mppResult: result,
1333
1559
  challenge: result.challenge
1334
1560
  });
1335
- console.error(`[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`);
1561
+ report("error", `MPP payment failed after handler: ${detail}`);
1336
1562
  return {
1337
1563
  ok: false,
1338
1564
  error: settlementError,
@@ -1355,10 +1581,10 @@ async function settleTxMode(args) {
1355
1581
 
1356
1582
  // src/protocols/mpp/hash-mode.ts
1357
1583
  async function verifyHashMode(args, info) {
1358
- const { deps, price, routeEntry, request } = args;
1584
+ const { deps, price, request, report } = args;
1359
1585
  if (!deps.mppx) {
1360
1586
  const reason = deps.mppInitError ? `MPP initialization failed: ${deps.mppInitError}` : "MPP not initialized \u2014 ensure mppx is installed and mpp config (secretKey, currency, recipient) is correct";
1361
- console.error(`[router] ${routeEntry.key}: ${reason}`);
1587
+ report("error", reason);
1362
1588
  return { ok: false, kind: "config", message: reason };
1363
1589
  }
1364
1590
  let chargeResult;
@@ -1366,13 +1592,13 @@ async function verifyHashMode(args, info) {
1366
1592
  chargeResult = await deps.mppx.charge({ amount: price })(request);
1367
1593
  } catch (err) {
1368
1594
  const message = err instanceof Error ? err.message : String(err);
1369
- console.error(`[router] ${routeEntry.key}: MPP charge failed: ${message}`);
1595
+ report("error", `MPP charge failed: ${message}`);
1370
1596
  return { ok: false, kind: "config", message: `MPP payment processing failed: ${message}` };
1371
1597
  }
1372
1598
  if (chargeResult.status === 402) {
1373
1599
  const reason = await readChallengeReason(chargeResult.challenge);
1374
1600
  const detail = reason || "credential may be invalid, or check TEMPO_RPC_URL configuration";
1375
- console.warn(`[router] ${routeEntry.key}: MPP credential rejected \u2014 ${detail}`);
1601
+ report("warn", `MPP credential rejected: ${detail}`);
1376
1602
  return { ok: false, kind: "invalid" };
1377
1603
  }
1378
1604
  const receiptHeader = chargeResult.withReceipt(new Response()).headers.get(
@@ -1417,9 +1643,20 @@ var mppStrategy = {
1417
1643
  const auth = request.headers.get(HEADERS.AUTHORIZATION);
1418
1644
  return Boolean(auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT));
1419
1645
  },
1646
+ preflight(request, _routeEntry) {
1647
+ const info = readMppCredential(request);
1648
+ if (!info?.sessionAction) return null;
1649
+ if (!isChannelOnlyAction(info, request)) return null;
1650
+ return { skipBody: true, skipHandler: true };
1651
+ },
1420
1652
  async verify(args) {
1421
1653
  const info = readMppCredential(args.request);
1422
1654
  if (!info) return { ok: false, kind: "invalid" };
1655
+ if (args.routeEntry.dynamicPrice) {
1656
+ if (!info.sessionAction) return { ok: false, kind: "invalid" };
1657
+ return verifySessionMode(args, info);
1658
+ }
1659
+ if (info.sessionAction) return { ok: false, kind: "invalid" };
1423
1660
  if (info.payloadType === "transaction" && args.deps.tempoClient) {
1424
1661
  return verifyTxMode(args, info);
1425
1662
  }
@@ -1427,28 +1664,88 @@ var mppStrategy = {
1427
1664
  },
1428
1665
  async settle(args) {
1429
1666
  const token = args.token;
1667
+ if (token.mode === "session") return settleSessionMode(args);
1430
1668
  if (token.mode === "transaction") return settleTxMode(args);
1431
1669
  return settleHashMode(args);
1432
1670
  },
1671
+ async settleStream(args) {
1672
+ const token = args.token;
1673
+ if (token.mode !== "session" || !token.streaming) {
1674
+ return {
1675
+ ok: false,
1676
+ error: new Error("streaming requires a streaming-mode MPP session credential"),
1677
+ failMessage: "streaming requires a streaming-mode MPP session credential",
1678
+ failStatus: 400
1679
+ };
1680
+ }
1681
+ const sessionToken = token;
1682
+ const sseResult = sessionToken.sessionResult;
1683
+ const { bindChannelCharge, source: handlerStream } = args;
1684
+ async function* forwardHandlerStreamWithChannelDebit(channel) {
1685
+ bindChannelCharge(channel.charge);
1686
+ try {
1687
+ for await (const chunk of handlerStream) {
1688
+ yield typeof chunk === "string" ? chunk : JSON.stringify(chunk);
1689
+ }
1690
+ } finally {
1691
+ bindChannelCharge(null);
1692
+ }
1693
+ }
1694
+ const sse = sseResult.withReceipt(forwardHandlerStreamWithChannelDebit);
1695
+ sse.headers.set("Cache-Control", "private");
1696
+ const settledPayment = {
1697
+ ...args.payment,
1698
+ status: "settled",
1699
+ amount: args.payment.amount
1700
+ };
1701
+ return { ok: true, response: sse, settledPayment };
1702
+ },
1433
1703
  async buildChallenge(args) {
1434
1704
  if (!args.deps.mppx) return {};
1435
- try {
1436
- const result = await args.deps.mppx.charge({ amount: args.price })(args.request);
1437
- if (result.status === 402) {
1438
- const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
1439
- if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
1440
- }
1441
- } catch (err) {
1442
- console.warn(
1443
- `[router] MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
1444
- );
1445
- throw err;
1705
+ const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
1706
+ if (args.routeEntry.dynamicPrice && sessionsConfigured) {
1707
+ const tickCost = args.routeEntry.tickCost;
1708
+ const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
1709
+ const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
1710
+ return buildSessionChallenge({
1711
+ ...args,
1712
+ suggestedDeposit
1713
+ });
1446
1714
  }
1447
- return {};
1715
+ return buildChargeChallenge(args);
1448
1716
  }
1449
1717
  };
1718
+ function multiplyDecimal(decimal, factor) {
1719
+ if (!Number.isFinite(factor) || factor <= 0) return decimal;
1720
+ const [whole, fraction = ""] = decimal.split(".");
1721
+ const scaled = (BigInt(whole + fraction) * BigInt(factor)).toString();
1722
+ const decimals = fraction.length;
1723
+ if (decimals === 0) return scaled;
1724
+ const padded = scaled.padStart(decimals + 1, "0");
1725
+ const intPart = padded.slice(0, padded.length - decimals);
1726
+ const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
1727
+ return fracPart ? `${intPart}.${fracPart}` : intPart;
1728
+ }
1729
+ async function buildChargeChallenge(args) {
1730
+ if (!args.deps.mppx) return {};
1731
+ try {
1732
+ const result = await args.deps.mppx.charge({ amount: args.price })(args.request);
1733
+ if (result.status === 402) {
1734
+ const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
1735
+ if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
1736
+ }
1737
+ } catch (err) {
1738
+ args.report(
1739
+ "warn",
1740
+ `MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
1741
+ );
1742
+ throw err;
1743
+ }
1744
+ return {};
1745
+ }
1450
1746
 
1451
1747
  // src/protocols/x402/strategy.ts
1748
+ var import_evm3 = require("@x402/evm");
1452
1749
  init_accepts();
1453
1750
 
1454
1751
  // src/protocols/x402/challenge.ts
@@ -1458,20 +1755,27 @@ init_solana();
1458
1755
  // src/protocols/x402/requirements.ts
1459
1756
  init_evm();
1460
1757
  init_solana();
1461
- async function buildExpectedRequirements(server, request, price, accepts) {
1462
- const exactRequirements = await buildExactRequirements(server, request, price, accepts);
1758
+ async function buildExpectedRequirements(server, request, price, accepts, report) {
1759
+ const sdkRequirements = await buildSdkHandledRequirements(
1760
+ server,
1761
+ request,
1762
+ price,
1763
+ accepts,
1764
+ report
1765
+ );
1463
1766
  const customRequirements = buildCustomRequirements(price, accepts);
1464
- return [...exactRequirements, ...customRequirements];
1767
+ return [...sdkRequirements, ...customRequirements];
1465
1768
  }
1466
- async function buildExactRequirements(server, request, price, accepts) {
1467
- const exactGroups = [
1769
+ async function buildSdkHandledRequirements(server, request, price, accepts, report) {
1770
+ const groups = [
1468
1771
  buildEvmExactOptions(accepts, price),
1772
+ buildEvmUptoOptions(accepts, price),
1469
1773
  buildSolanaExactOptions(accepts, price)
1470
1774
  ].filter((options) => options.length > 0);
1471
- if (exactGroups.length === 0) return [];
1775
+ if (groups.length === 0) return [];
1472
1776
  const requirements = [];
1473
1777
  const failures = [];
1474
- for (const options of exactGroups) {
1778
+ for (const options of groups) {
1475
1779
  try {
1476
1780
  requirements.push(
1477
1781
  ...await server.buildPaymentRequirementsFromOptions(options, { request })
@@ -1479,21 +1783,31 @@ async function buildExactRequirements(server, request, price, accepts) {
1479
1783
  } catch (error) {
1480
1784
  const err = error instanceof Error ? error : new Error(String(error));
1481
1785
  failures.push(err);
1482
- if (exactGroups.length === 1) {
1786
+ if (groups.length === 1) {
1483
1787
  throw err;
1484
1788
  }
1485
- console.warn(
1486
- `[router] Failed to build x402 exact requirements for ${options[0]?.network}: ${err.message}`
1789
+ report?.(
1790
+ "warn",
1791
+ `Failed to build x402 ${options[0]?.scheme} requirements for ${options[0]?.network}: ${err.message}`
1487
1792
  );
1488
1793
  }
1489
1794
  }
1490
1795
  if (requirements.length > 0) {
1491
1796
  return requirements;
1492
1797
  }
1493
- throw failures[0] ?? new Error("Failed to build x402 exact requirements");
1798
+ throw failures[0] ?? new Error("Failed to build x402 SDK-handled requirements");
1494
1799
  }
1495
1800
  function buildCustomRequirements(price, accepts) {
1496
- return accepts.filter((accept) => accept.scheme !== "exact").map((accept) => buildCustomRequirement(price, accept));
1801
+ return accepts.filter((accept) => !isSdkHandled(accept)).map((accept) => buildCustomRequirement(price, accept));
1802
+ }
1803
+ function isSdkHandled(accept) {
1804
+ if (isEvmNetwork(accept.network)) {
1805
+ return accept.scheme === "exact" || accept.scheme === "upto";
1806
+ }
1807
+ if (isSolanaRequirement({ network: accept.network })) {
1808
+ return accept.scheme === "exact";
1809
+ }
1810
+ return false;
1497
1811
  }
1498
1812
  function buildCustomRequirement(price, accept) {
1499
1813
  if (!accept.asset) {
@@ -1527,7 +1841,7 @@ function decimalToAtomicUnits(amount, decimals) {
1527
1841
 
1528
1842
  // src/protocols/x402/challenge.ts
1529
1843
  async function buildX402Challenge(opts) {
1530
- const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
1844
+ const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions, report } = opts;
1531
1845
  const { encodePaymentRequiredHeader } = await import("@x402/core/http");
1532
1846
  const resource = buildChallengeResource(request, routeEntry);
1533
1847
  const requirements = await buildChallengeRequirements(
@@ -1536,7 +1850,8 @@ async function buildX402Challenge(opts) {
1536
1850
  price,
1537
1851
  accepts,
1538
1852
  resource,
1539
- facilitatorsByNetwork
1853
+ facilitatorsByNetwork,
1854
+ report
1540
1855
  );
1541
1856
  const paymentRequired = await server.createPaymentRequiredResponse(
1542
1857
  requirements,
@@ -1547,13 +1862,13 @@ async function buildX402Challenge(opts) {
1547
1862
  const encoded = encodePaymentRequiredHeader(paymentRequired);
1548
1863
  return { encoded, requirements };
1549
1864
  }
1550
- async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork) {
1551
- const requirements = await buildExpectedRequirements(server, request, price, accepts);
1865
+ async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork, report) {
1866
+ const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
1552
1867
  if (!needsFacilitatorEnrichment(accepts)) return requirements;
1553
- return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork);
1868
+ return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork, report);
1554
1869
  }
1555
1870
  function needsFacilitatorEnrichment(accepts) {
1556
- return accepts.some((accept) => accept.scheme !== "exact") || hasSolanaAccepts(accepts);
1871
+ return hasSolanaAccepts(accepts);
1557
1872
  }
1558
1873
  async function enrichGroup(group, resource) {
1559
1874
  const accepted = await enrichRequirementsWithFacilitatorAccepts(
@@ -1568,7 +1883,7 @@ async function enrichGroup(group, resource) {
1568
1883
  }
1569
1884
  return accepted;
1570
1885
  }
1571
- async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork) {
1886
+ async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork, report) {
1572
1887
  const groups = collectEnrichmentGroups(requirements, facilitatorsByNetwork);
1573
1888
  if (groups.length === 0) return requirements;
1574
1889
  const results = await Promise.all(
@@ -1578,8 +1893,9 @@ async function enrichChallengeRequirements(requirements, resource, facilitatorsB
1578
1893
  } catch (err) {
1579
1894
  const label = group.facilitator.url ?? group.facilitator.network;
1580
1895
  const reason = err instanceof Error ? err.message : String(err);
1581
- console.warn(
1582
- `[router] ${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
1896
+ report?.(
1897
+ "warn",
1898
+ `${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
1583
1899
  );
1584
1900
  return { success: false, group };
1585
1901
  }
@@ -1632,7 +1948,7 @@ function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
1632
1948
  return facilitator;
1633
1949
  }
1634
1950
  function requiresFacilitatorEnrichment(requirement) {
1635
- return requirement.scheme !== "exact" || isSolanaRequirement(requirement);
1951
+ return isSolanaRequirement(requirement);
1636
1952
  }
1637
1953
  function buildChallengeResource(request, routeEntry) {
1638
1954
  return {
@@ -1644,33 +1960,68 @@ function buildChallengeResource(request, routeEntry) {
1644
1960
  }
1645
1961
 
1646
1962
  // src/protocols/x402/settle.ts
1647
- async function settleX402Payment(server, payload, requirements) {
1963
+ async function settleX402Payment(server, payload, requirements, amountOverride) {
1648
1964
  const { encodePaymentResponseHeader } = await import("@x402/core/http");
1965
+ if (amountOverride?.amount !== void 0) {
1966
+ const upstreamTaggedAmount = tagBareDecimalAsDollars(amountOverride.amount);
1967
+ const result2 = await server.settlePayment(payload, requirements, void 0, void 0, {
1968
+ amount: upstreamTaggedAmount
1969
+ });
1970
+ return {
1971
+ encoded: encodePaymentResponseHeader(result2),
1972
+ result: result2
1973
+ };
1974
+ }
1649
1975
  const result = await server.settlePayment(payload, requirements);
1650
- const encoded = encodePaymentResponseHeader(result);
1651
- return { encoded, result };
1976
+ return {
1977
+ encoded: encodePaymentResponseHeader(result),
1978
+ result
1979
+ };
1980
+ }
1981
+ function tagBareDecimalAsDollars(amount) {
1982
+ if (/^\d+\.\d+$/.test(amount)) return `$${amount}`;
1983
+ return amount;
1652
1984
  }
1653
1985
 
1654
1986
  // src/protocols/x402/verify.ts
1987
+ var import_types2 = require("@x402/core/types");
1655
1988
  async function verifyX402Payment(opts) {
1656
- const { server, request, price, accepts } = opts;
1989
+ const { server, request, price, accepts, report } = opts;
1657
1990
  const payload = await readPaymentPayload(request);
1658
1991
  if (!payload) return null;
1659
- const requirements = await buildExpectedRequirements(server, request, price, accepts);
1992
+ const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
1660
1993
  const matching = findVerifiableRequirements(server, requirements, payload);
1994
+ const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
1661
1995
  if (!matching) {
1662
- return invalidPaymentVerification();
1996
+ return invalidPaymentVerification({
1997
+ reason: "requirements_mismatch",
1998
+ message: "Signed payment requirements did not match any server-built requirement",
1999
+ ...accepted ? { accepted } : {}
2000
+ });
1663
2001
  }
1664
2002
  let verify;
1665
2003
  try {
1666
2004
  verify = await server.verifyPayment(payload, matching);
1667
2005
  } catch (err) {
1668
- const sc = err.statusCode;
1669
- if (sc && sc >= 400 && sc < 500) return invalidPaymentVerification();
2006
+ if (err instanceof import_types2.VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
2007
+ return invalidPaymentVerification({
2008
+ reason: err.invalidReason ?? "verify_error",
2009
+ ...err.invalidMessage ? { message: err.invalidMessage } : {},
2010
+ ...err.payer ? { payer: err.payer } : {},
2011
+ ...accepted ? { accepted } : {}
2012
+ });
2013
+ }
1670
2014
  throw err;
1671
2015
  }
1672
- if (!verify.isValid) return invalidPaymentVerification();
1673
- if (typeof verify.payer !== "string" || verify.payer.length === 0) {
2016
+ if (!verify.isValid) {
2017
+ return invalidPaymentVerification({
2018
+ reason: verify.invalidReason ?? "unknown",
2019
+ ...verify.invalidMessage ? { message: verify.invalidMessage } : {},
2020
+ ...verify.payer ? { payer: verify.payer } : {},
2021
+ ...accepted ? { accepted } : {}
2022
+ });
2023
+ }
2024
+ if (!verify.payer) {
1674
2025
  throw new Error("x402 verification succeeded without a payer address");
1675
2026
  }
1676
2027
  return {
@@ -1702,11 +2053,36 @@ async function readPaymentPayload(request) {
1702
2053
  const { decodePaymentSignatureHeader } = await import("@x402/core/http");
1703
2054
  return decodePaymentSignatureHeader(paymentHeader);
1704
2055
  }
1705
- function invalidPaymentVerification() {
1706
- return { valid: false, payload: null, requirements: null, payer: null };
2056
+ function invalidPaymentVerification(failure) {
2057
+ return {
2058
+ valid: false,
2059
+ payload: null,
2060
+ requirements: null,
2061
+ payer: null,
2062
+ ...failure ? { failure } : {}
2063
+ };
1707
2064
  }
1708
2065
 
1709
2066
  // src/protocols/x402/strategy.ts
2067
+ function formatVerifyFailureMessage(failure) {
2068
+ if (failure.reason === "permit2_allowance_required") {
2069
+ const wallet = failure.payer ?? "<the payer wallet>";
2070
+ const asset = failure.accepted?.asset ?? "<the asset>";
2071
+ const amount = failure.accepted?.amount ?? "<the required amount>";
2072
+ const network = failure.accepted?.network ?? "<the payment network>";
2073
+ return [
2074
+ `Payment rejected: In order for Upto to charge, the wallet ${wallet} MUST approve Permit2 to spend ${asset} on ${network}.`,
2075
+ `Required call (one-time, on-chain): ${asset}.approve(${import_evm3.PERMIT2_ADDRESS}, MAX_UINT256) from ${wallet}.`,
2076
+ `Permit2 contract address: ${import_evm3.PERMIT2_ADDRESS}.`,
2077
+ `Minimum allowance for this request: ${amount} (smallest units of ${asset}); use MAX_UINT256 to avoid re-approving on every future call.`,
2078
+ `Alternative without an on-chain transaction: the merchant can adopt the EIP-2612 gas-sponsoring extension (https://docs.x402.org/extensions/eip2612-gas-sponsoring).`
2079
+ ].join(" ");
2080
+ }
2081
+ if (failure.message) {
2082
+ return `Payment rejected (${failure.reason}): ${failure.message}`;
2083
+ }
2084
+ return `Payment rejected: ${failure.reason}`;
2085
+ }
1710
2086
  var x402Strategy = {
1711
2087
  protocol: "x402",
1712
2088
  detects(request) {
@@ -1714,114 +2090,123 @@ var x402Strategy = {
1714
2090
  request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)
1715
2091
  );
1716
2092
  },
1717
- async verify(args) {
1718
- const { request, body, price, routeEntry, deps } = args;
1719
- if (!deps.x402Server) {
1720
- const reason = deps.x402InitError ? `x402 facilitator initialization failed: ${deps.x402InitError}` : "x402 server not initialized \u2014 ensure @x402/core, @x402/evm, and @coinbase/x402 are installed";
1721
- console.error(`[router] ${routeEntry.key}: ${reason}`);
1722
- return { ok: false, kind: "config", message: reason };
1723
- }
1724
- const accepts = await resolveX402Accepts(
1725
- request,
1726
- routeEntry,
1727
- deps.x402Accepts,
1728
- deps.payeeAddress,
1729
- body
1730
- );
1731
- const verifyResult = await verifyX402Payment({
1732
- server: deps.x402Server,
1733
- request,
1734
- price,
1735
- accepts
1736
- });
1737
- if (!verifyResult?.valid) return { ok: false, kind: "invalid" };
1738
- const wallet = normalizeWalletAddress(verifyResult.payer);
1739
- const matchedNetwork = getRequirementNetwork(verifyResult.requirements, deps.network);
1740
- const matchedRecipient = getRequirementRecipient(verifyResult.requirements);
1741
- const payment = {
1742
- protocol: "x402",
1743
- status: "verified",
1744
- payer: wallet,
1745
- amount: price,
1746
- network: matchedNetwork,
1747
- ...matchedRecipient ? { recipient: matchedRecipient } : {}
1748
- };
1749
- return {
1750
- ok: true,
1751
- wallet,
1752
- payment,
1753
- token: {
1754
- payload: verifyResult.payload,
1755
- requirements: verifyResult.requirements
1756
- }
1757
- };
1758
- },
1759
- async settle(args) {
1760
- const { response, payment, token, deps } = args;
1761
- const x402Token = token;
1762
- try {
1763
- const settle = await settleX402Payment(
1764
- deps.x402Server,
1765
- x402Token.payload,
1766
- x402Token.requirements
1767
- );
1768
- if (!settle.result?.success) {
1769
- const reason = settle.result?.errorReason || "x402 settlement returned success=false";
1770
- const error = new Error(reason);
1771
- error.errorReason = reason;
1772
- throw error;
1773
- }
1774
- response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
1775
- response.headers.set("Cache-Control", "private");
1776
- const transaction = String(settle.result?.transaction ?? "");
1777
- const settledPayment = {
1778
- ...payment,
1779
- status: "settled",
1780
- ...transaction ? { transaction } : {}
2093
+ verify: (args) => verifyX402(args),
2094
+ settle: (args) => settleX402(args),
2095
+ buildChallenge: (args) => buildX402ChallengeContribution(args)
2096
+ };
2097
+ async function verifyX402(args) {
2098
+ const { request, body, price, routeEntry, deps, report } = args;
2099
+ if (!deps.x402Server) {
2100
+ const reason = deps.x402InitError ? `x402 facilitator initialization failed: ${deps.x402InitError}` : "x402 server not initialized \u2014 ensure @x402/core, @x402/evm, and @coinbase/x402 are installed";
2101
+ report("error", reason);
2102
+ return { ok: false, kind: "config", message: reason };
2103
+ }
2104
+ const accepts = await resolveX402Accepts(
2105
+ request,
2106
+ routeEntry,
2107
+ deps.x402Accepts,
2108
+ deps.payeeAddress,
2109
+ body
2110
+ );
2111
+ const verifyResult = await verifyX402Payment({
2112
+ server: deps.x402Server,
2113
+ request,
2114
+ price,
2115
+ accepts,
2116
+ report
2117
+ });
2118
+ if (!verifyResult?.valid) {
2119
+ const failure = verifyResult?.failure;
2120
+ if (failure) {
2121
+ return {
2122
+ ok: false,
2123
+ kind: "invalid",
2124
+ failure: {
2125
+ reason: failure.reason,
2126
+ message: formatVerifyFailureMessage(failure)
2127
+ }
1781
2128
  };
1782
- return { ok: true, response, settledPayment };
1783
- } catch (err) {
1784
- const errObj = err;
1785
- console.error("Settlement failed", {
1786
- message: err instanceof Error ? err.message : String(err),
1787
- route: args.routeEntry.key,
1788
- network: payment.network,
1789
- errorReason: errObj.errorReason,
1790
- facilitatorStatus: errObj.response?.status,
1791
- facilitatorBody: errObj.response?.data ?? errObj.response?.body
1792
- });
1793
- return { ok: false, error: err, failMessage: "Settlement failed" };
1794
2129
  }
1795
- },
1796
- async buildChallenge(args) {
1797
- const { request, routeEntry, body, price, extensions, deps } = args;
1798
- if (!deps.x402Server) return {};
1799
- const accepts = await resolveX402Accepts(
1800
- request,
1801
- routeEntry,
1802
- deps.x402Accepts,
1803
- deps.payeeAddress,
1804
- body
1805
- );
1806
- const { encoded } = await buildX402Challenge({
1807
- server: deps.x402Server,
1808
- routeEntry,
1809
- request,
1810
- price,
1811
- accepts,
1812
- facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
1813
- extensions
1814
- });
1815
- return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
2130
+ return { ok: false, kind: "invalid" };
2131
+ }
2132
+ const wallet = normalizeWalletAddress(verifyResult.payer);
2133
+ const { network, payTo } = verifyResult.requirements;
2134
+ const payment = {
2135
+ protocol: "x402",
2136
+ status: "verified",
2137
+ payer: wallet,
2138
+ amount: price,
2139
+ network,
2140
+ ...payTo ? { recipient: payTo } : {}
2141
+ };
2142
+ return {
2143
+ ok: true,
2144
+ wallet,
2145
+ payment,
2146
+ token: {
2147
+ payload: verifyResult.payload,
2148
+ requirements: verifyResult.requirements
2149
+ }
2150
+ };
2151
+ }
2152
+ async function settleX402(args) {
2153
+ const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
2154
+ const { payload, requirements } = token;
2155
+ const override = routeEntry.dynamicPrice ? { amount: billedAmount } : void 0;
2156
+ try {
2157
+ const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2158
+ if (!settle.result?.success) {
2159
+ throw Object.assign(
2160
+ new Error(settle.result?.errorReason ?? "x402 settlement returned success=false"),
2161
+ { errorReason: settle.result?.errorReason }
2162
+ );
2163
+ }
2164
+ response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
2165
+ response.headers.set("Cache-Control", "private");
2166
+ const transaction = String(settle.result.transaction ?? "");
2167
+ const settledPayment = {
2168
+ ...payment,
2169
+ status: "settled",
2170
+ amount: billedAmount,
2171
+ ...transaction ? { transaction } : {}
2172
+ };
2173
+ return { ok: true, response, settledPayment };
2174
+ } catch (err) {
2175
+ reportSettleFailure(report, err, payment.network);
2176
+ return { ok: false, error: err, failMessage: "Settlement failed" };
1816
2177
  }
1817
- };
1818
- function getRequirementNetwork(requirements, fallback) {
1819
- const network = requirements?.network;
1820
- return typeof network === "string" ? network : fallback;
1821
2178
  }
1822
- function getRequirementRecipient(requirements) {
1823
- const payTo = requirements?.payTo;
1824
- return typeof payTo === "string" ? payTo : void 0;
2179
+ async function buildX402ChallengeContribution(args) {
2180
+ const { request, routeEntry, body, price, extensions, deps, report } = args;
2181
+ if (!deps.x402Server) return {};
2182
+ const accepts = await resolveX402Accepts(
2183
+ request,
2184
+ routeEntry,
2185
+ deps.x402Accepts,
2186
+ deps.payeeAddress,
2187
+ body
2188
+ );
2189
+ const { encoded } = await buildX402Challenge({
2190
+ server: deps.x402Server,
2191
+ routeEntry,
2192
+ request,
2193
+ price,
2194
+ accepts,
2195
+ facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
2196
+ extensions,
2197
+ report
2198
+ });
2199
+ return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
2200
+ }
2201
+ function reportSettleFailure(report, err, network) {
2202
+ const facilitator = err ?? {};
2203
+ report("error", "Settlement failed", {
2204
+ error: err instanceof Error ? err.message : String(err),
2205
+ network,
2206
+ errorReason: facilitator.errorReason,
2207
+ facilitatorStatus: facilitator.response?.status,
2208
+ facilitatorBody: facilitator.response?.data ?? facilitator.response?.body
2209
+ });
1825
2210
  }
1826
2211
 
1827
2212
  // src/protocols/detect.ts
@@ -1855,23 +2240,75 @@ function getAllowedStrategies(allowed) {
1855
2240
  return allowed.map((name) => STRATEGIES[name]);
1856
2241
  }
1857
2242
 
1858
- // src/pipeline/challenge.ts
2243
+ // src/pipeline/flows/build402.ts
1859
2244
  var import_server4 = require("next/server");
1860
- async function build402(ctx, pricing, body) {
1861
- let challengePrice;
2245
+
2246
+ // src/pipeline/challenge-extensions.ts
2247
+ async function buildChallengeExtensions(ctx) {
2248
+ const { routeEntry } = ctx;
2249
+ let extensions;
1862
2250
  try {
1863
- challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
2251
+ const { z } = await import("zod");
2252
+ const { declareDiscoveryExtension } = await import("@x402/extensions/bazaar");
2253
+ const toJSON = (schema) => z.toJSONSchema(schema, {
2254
+ target: "draft-2020-12",
2255
+ unrepresentable: "any"
2256
+ });
2257
+ const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
2258
+ const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
2259
+ if (inputSchema) {
2260
+ const config = {
2261
+ method: routeEntry.method,
2262
+ bodyType: routeEntry.bodySchema ? "json" : void 0,
2263
+ inputSchema
2264
+ };
2265
+ if (routeEntry.inputExample !== void 0) {
2266
+ config.input = routeEntry.inputExample;
2267
+ }
2268
+ if (outputSchema && routeEntry.outputExample !== void 0) {
2269
+ config.output = { schema: outputSchema, example: routeEntry.outputExample };
2270
+ }
2271
+ extensions = declareDiscoveryExtension(config);
2272
+ }
1864
2273
  } catch (err) {
1865
- const message = errorMessage(err, "Price calculation failed");
1866
- const errorResponse = import_server4.NextResponse.json(
1867
- { success: false, error: message },
1868
- { status: errorStatus(err, 500) }
1869
- );
1870
- firePluginResponse(ctx, errorResponse);
1871
- return errorResponse;
1872
- }
1873
- const extensions = await buildChallengeExtensions(ctx);
1874
- const response = new import_server4.NextResponse(null, {
2274
+ firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
2275
+ level: "warn",
2276
+ message: `Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`,
2277
+ route: routeEntry.key
2278
+ });
2279
+ }
2280
+ if (routeEntry.siwxEnabled) {
2281
+ try {
2282
+ const siwxExtension = await buildSIWXExtension();
2283
+ if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
2284
+ extensions = {
2285
+ ...extensions ?? {},
2286
+ ...siwxExtension
2287
+ };
2288
+ }
2289
+ } catch {
2290
+ }
2291
+ }
2292
+ return extensions;
2293
+ }
2294
+
2295
+ // src/pipeline/flows/build402.ts
2296
+ async function build402(ctx, pricing, body, failure) {
2297
+ let challengePrice;
2298
+ try {
2299
+ challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
2300
+ } catch (err) {
2301
+ const message = errorMessage(err, "Price calculation failed");
2302
+ const errorResponse = import_server4.NextResponse.json(
2303
+ { success: false, error: message },
2304
+ { status: errorStatus(err, 500) }
2305
+ );
2306
+ firePluginResponse(ctx, errorResponse);
2307
+ return errorResponse;
2308
+ }
2309
+ const extensions = await buildChallengeExtensions(ctx);
2310
+ const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
2311
+ const response = new import_server4.NextResponse(responseBody, {
1875
2312
  status: 402,
1876
2313
  headers: {
1877
2314
  "Content-Type": "application/json",
@@ -1886,7 +2323,8 @@ async function build402(ctx, pricing, body) {
1886
2323
  body,
1887
2324
  price: challengePrice,
1888
2325
  extensions,
1889
- deps: ctx.deps
2326
+ deps: ctx.deps,
2327
+ report: ctx.report
1890
2328
  });
1891
2329
  if (contribution.headers) {
1892
2330
  for (const [name, value] of Object.entries(contribution.headers)) {
@@ -1895,11 +2333,7 @@ async function build402(ctx, pricing, body) {
1895
2333
  }
1896
2334
  } catch (err) {
1897
2335
  const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
1898
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
1899
- level: "critical",
1900
- message,
1901
- route: ctx.routeEntry.key
1902
- });
2336
+ ctx.report("critical", message);
1903
2337
  if (strategy.protocol === "x402") {
1904
2338
  const errorResponse = import_server4.NextResponse.json(
1905
2339
  { success: false, error: message },
@@ -1913,97 +2347,313 @@ async function build402(ctx, pricing, body) {
1913
2347
  firePluginResponse(ctx, response);
1914
2348
  return response;
1915
2349
  }
1916
- async function buildChallengeExtensions(ctx) {
1917
- const { routeEntry } = ctx;
1918
- let extensions;
2350
+
2351
+ // src/pipeline/flows/dynamic/dynamic-body-and-price.ts
2352
+ async function resolveDynamicBodyAndPrice(args) {
2353
+ const { ctx, pricing, skipBody } = args;
2354
+ if (skipBody) {
2355
+ return {
2356
+ ok: true,
2357
+ parsedBody: void 0,
2358
+ price: surrogatePriceForSkippedBody(ctx.routeEntry)
2359
+ };
2360
+ }
2361
+ const body = await parseBody(ctx);
2362
+ if (!body.ok) return { ok: false, response: body.response };
2363
+ const validateErr = await runValidate(ctx, body.data);
2364
+ if (validateErr) {
2365
+ return { ok: false, response: validateErr };
2366
+ }
2367
+ if (!pricing) {
2368
+ return { ok: false, response: fail(ctx, 500, "Pricing not configured", body.data) };
2369
+ }
1919
2370
  try {
1920
- const { z } = await import("zod");
1921
- const { declareDiscoveryExtension } = await import("@x402/extensions/bazaar");
1922
- const toJSON = (schema) => z.toJSONSchema(schema, {
1923
- target: "draft-2020-12",
1924
- unrepresentable: "any"
1925
- });
1926
- const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
1927
- const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
1928
- if (inputSchema) {
1929
- const config = {
1930
- method: routeEntry.method,
1931
- bodyType: routeEntry.bodySchema ? "json" : void 0,
1932
- inputSchema
1933
- };
1934
- if (routeEntry.inputExample !== void 0) {
1935
- config.input = routeEntry.inputExample;
1936
- }
1937
- if (outputSchema && routeEntry.outputExample !== void 0) {
1938
- config.output = { schema: outputSchema, example: routeEntry.outputExample };
1939
- }
1940
- extensions = declareDiscoveryExtension(config);
1941
- }
2371
+ const price = await pricing.quote(body.data);
2372
+ return { ok: true, parsedBody: body.data, price };
1942
2373
  } catch (err) {
1943
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
1944
- level: "warn",
1945
- message: `Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`,
1946
- route: routeEntry.key
1947
- });
2374
+ return {
2375
+ ok: false,
2376
+ response: fail(
2377
+ ctx,
2378
+ errorStatus(err, 500),
2379
+ errorMessage(err, "Price calculation failed"),
2380
+ body.data
2381
+ )
2382
+ };
1948
2383
  }
1949
- if (routeEntry.siwxEnabled) {
1950
- try {
1951
- const siwxExtension = await buildSIWXExtension();
1952
- if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
1953
- extensions = {
1954
- ...extensions ?? {},
1955
- ...siwxExtension
1956
- };
1957
- }
1958
- } catch {
2384
+ }
2385
+ function surrogatePriceForSkippedBody(routeEntry) {
2386
+ return routeEntry.maxPrice ?? routeEntry.minPrice ?? "0";
2387
+ }
2388
+
2389
+ // src/pipeline/flows/dynamic/dynamic-channel-mgmt.ts
2390
+ var import_server5 = require("next/server");
2391
+ async function runDynamicChannelMgmtFlow(args) {
2392
+ const { ctx, strategy, account, pricing, skipBody } = args;
2393
+ const { request, routeEntry, deps, report } = ctx;
2394
+ const bodyAndPrice = await resolveDynamicBodyAndPrice({ ctx, pricing, skipBody });
2395
+ if (!bodyAndPrice.ok) return bodyAndPrice.response;
2396
+ const { parsedBody, price } = bodyAndPrice;
2397
+ const verifyOutcome = await strategy.verify({
2398
+ request,
2399
+ body: parsedBody,
2400
+ price,
2401
+ routeEntry,
2402
+ deps,
2403
+ report
2404
+ });
2405
+ if (verifyOutcome.ok === false) {
2406
+ if (verifyOutcome.kind === "config") {
2407
+ return fail(ctx, 500, verifyOutcome.message, parsedBody);
1959
2408
  }
2409
+ return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
1960
2410
  }
1961
- return extensions;
2411
+ ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2412
+ firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
2413
+ protocol: strategy.protocol,
2414
+ payer: verifyOutcome.wallet,
2415
+ amount: price,
2416
+ network: verifyOutcome.payment.network
2417
+ });
2418
+ const synthetic = new import_server5.NextResponse(null, { status: 200 });
2419
+ const settleScope = {
2420
+ wallet: verifyOutcome.wallet,
2421
+ account,
2422
+ body: parsedBody,
2423
+ payment: verifyOutcome.payment,
2424
+ response: synthetic,
2425
+ rawResult: void 0
2426
+ };
2427
+ const beforeErr = await runBeforeSettle(ctx, settleScope);
2428
+ if (beforeErr) return beforeErr;
2429
+ return settleAndFinalizeRequest({
2430
+ ctx,
2431
+ strategy,
2432
+ verifyOutcome,
2433
+ scope: settleScope,
2434
+ rawResult: void 0,
2435
+ body: parsedBody,
2436
+ billedAmount: "0",
2437
+ onSettleError: async (error, failMessage) => {
2438
+ await runSettlementError(ctx, settleScope, error, "settle");
2439
+ report("critical", `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`);
2440
+ }
2441
+ });
1962
2442
  }
1963
2443
 
1964
- // src/pipeline/flows/paid.ts
1965
- async function runPaidFlow(ctx) {
1966
- const { request, routeEntry, deps } = ctx;
1967
- let account = void 0;
1968
- if (routeEntry.apiKeyResolver) {
1969
- const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
1970
- if (!apiKeyResult.valid) return fail(ctx, 401, "Invalid or missing API key");
1971
- account = apiKeyResult.account;
1972
- firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
1973
- authMode: "apiKey",
1974
- wallet: null,
1975
- route: routeEntry.key,
1976
- account
2444
+ // src/pipeline/flows/dynamic/dynamic-invoke.ts
2445
+ var import_server6 = require("next/server");
2446
+
2447
+ // src/pricing/atomic.ts
2448
+ var USDC_DECIMALS = 6;
2449
+ function decimalToAtomic(amount) {
2450
+ const m = /^(\d+)(?:\.(\d+))?$/.exec(amount.trim());
2451
+ if (!m) {
2452
+ throw Object.assign(new Error(`'${amount}' is not a valid decimal-dollar string`), {
2453
+ status: 400
1977
2454
  });
1978
2455
  }
1979
- const alertFn = (level, message, meta) => {
1980
- firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
1981
- level,
1982
- message,
1983
- route: routeEntry.key,
1984
- meta
1985
- });
2456
+ const whole = m[1];
2457
+ const fraction = (m[2] ?? "").slice(0, USDC_DECIMALS).padEnd(USDC_DECIMALS, "0");
2458
+ return BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0");
2459
+ }
2460
+ function atomicToDecimal(atomic) {
2461
+ const whole = atomic / 10n ** BigInt(USDC_DECIMALS);
2462
+ const fraction = atomic % 10n ** BigInt(USDC_DECIMALS);
2463
+ if (fraction === 0n) return whole.toString();
2464
+ const fractionStr = fraction.toString().padStart(USDC_DECIMALS, "0").replace(/0+$/, "");
2465
+ return `${whole}.${fractionStr}`;
2466
+ }
2467
+
2468
+ // src/pricing/charge-context.ts
2469
+ function createChargeContext(args) {
2470
+ const { tickCost, maxPrice, route } = args;
2471
+ const tickAtomic = decimalToAtomic(tickCost);
2472
+ if (tickAtomic <= 0n) {
2473
+ throw new Error(`route '${route}': tickCost '${tickCost}' must be a positive decimal string`);
2474
+ }
2475
+ const capAtomic = maxPrice !== void 0 ? decimalToAtomic(maxPrice) : null;
2476
+ let ticks = 0;
2477
+ let atomic = 0n;
2478
+ let channelCharge = null;
2479
+ const charge = async () => {
2480
+ const nextAtomic = atomic + tickAtomic;
2481
+ if (capAtomic !== null && nextAtomic > capAtomic) {
2482
+ throw Object.assign(
2483
+ new Error(
2484
+ `route '${route}': charge() running total ($${atomicToDecimal(nextAtomic)}) exceeds maxPrice ($${atomicToDecimal(capAtomic)})`
2485
+ ),
2486
+ { status: 400, code: "CHARGE_OVER_CAP" }
2487
+ );
2488
+ }
2489
+ ticks += 1;
2490
+ atomic = nextAtomic;
2491
+ if (channelCharge) await channelCharge();
1986
2492
  };
2493
+ return {
2494
+ charge,
2495
+ bindChannelCharge: (fn) => {
2496
+ channelCharge = fn;
2497
+ },
2498
+ tickCount: () => ticks,
2499
+ atomicTotal: () => atomic
2500
+ };
2501
+ }
2502
+
2503
+ // src/pipeline/flows/dynamic/dynamic-invoke.ts
2504
+ async function invokeDynamic(ctx, wallet, account, body, payment) {
2505
+ const streaming = ctx.routeEntry.streaming === true;
2506
+ const chargeContext = streaming ? createChargeContext({
2507
+ tickCost: ctx.routeEntry.tickCost,
2508
+ maxPrice: ctx.routeEntry.maxPrice,
2509
+ route: ctx.routeEntry.key
2510
+ }) : null;
2511
+ const baseHandlerCtx = {
2512
+ body,
2513
+ query: parseQuery(ctx.request, ctx.routeEntry),
2514
+ request: ctx.request,
2515
+ requestId: ctx.meta.requestId,
2516
+ route: ctx.routeEntry.key,
2517
+ wallet,
2518
+ payment,
2519
+ account,
2520
+ alert(level, message, alertMeta) {
2521
+ firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
2522
+ level,
2523
+ message,
2524
+ route: ctx.routeEntry.key,
2525
+ meta: alertMeta
2526
+ });
2527
+ },
2528
+ setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
2529
+ };
2530
+ const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
2531
+ let returned;
2532
+ try {
2533
+ returned = ctx.handler(handlerCtx);
2534
+ } catch (error) {
2535
+ return errorResult2(error, chargeContext);
2536
+ }
2537
+ if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2538
+ if (!chargeContext) {
2539
+ return errorResult2(
2540
+ new HttpError(
2541
+ "route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
2542
+ 500
2543
+ ),
2544
+ null
2545
+ );
2546
+ }
2547
+ return {
2548
+ kind: "stream",
2549
+ source: returned,
2550
+ chargeContext
2551
+ };
2552
+ }
2553
+ let rawResult;
2554
+ try {
2555
+ rawResult = await returned;
2556
+ } catch (error) {
2557
+ return errorResult2(error, chargeContext);
2558
+ }
2559
+ const response = rawResult instanceof Response ? rawResult : import_server6.NextResponse.json(rawResult);
2560
+ return { kind: "request", response, rawResult };
2561
+ }
2562
+ function errorResult2(error, chargeContext) {
2563
+ const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
2564
+ const message = error instanceof Error ? error.message : "Internal error";
2565
+ void chargeContext;
2566
+ return {
2567
+ kind: "request",
2568
+ response: import_server6.NextResponse.json({ success: false, error: message }, { status }),
2569
+ rawResult: void 0,
2570
+ handlerError: error
2571
+ };
2572
+ }
2573
+ function isAsyncIterable2(value) {
2574
+ return value != null && typeof value === "object" && Symbol.asyncIterator in value;
2575
+ }
2576
+ function isThenable2(value) {
2577
+ return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
2578
+ }
2579
+
2580
+ // src/pipeline/flows/dynamic/dynamic-preflight.ts
2581
+ function resolveDynamicPreflight(strategy, request, routeEntry) {
2582
+ const outcome = strategy.preflight?.(request, routeEntry) ?? null;
2583
+ return {
2584
+ skipBody: outcome?.skipBody ?? false,
2585
+ skipHandler: outcome?.skipHandler ?? false
2586
+ };
2587
+ }
2588
+
2589
+ // src/pipeline/flows/dynamic/dynamic-request.ts
2590
+ async function runDynamicRequestFlow(args) {
2591
+ const { ctx, strategy, verifyOutcome, account, body, result } = args;
2592
+ const { deps, routeEntry } = ctx;
2593
+ const settleScope = {
2594
+ wallet: verifyOutcome.wallet,
2595
+ account,
2596
+ body,
2597
+ payment: verifyOutcome.payment,
2598
+ response: result.response,
2599
+ rawResult: result.rawResult,
2600
+ handlerError: result.handlerError
2601
+ };
2602
+ if (result.response.status >= 400) {
2603
+ return finalize(ctx, result.response, result.rawResult, body);
2604
+ }
2605
+ const beforeErr = await runBeforeSettle(ctx, settleScope);
2606
+ if (beforeErr) return beforeErr;
2607
+ const billedAmount = routeEntry.tickCost;
2608
+ return settleAndFinalizeRequest({
2609
+ ctx,
2610
+ strategy,
2611
+ verifyOutcome,
2612
+ scope: settleScope,
2613
+ rawResult: result.rawResult,
2614
+ body,
2615
+ billedAmount,
2616
+ onSettleError: async (error, failMessage) => {
2617
+ await runSettlementError(ctx, settleScope, error, "settle");
2618
+ firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2619
+ level: "critical",
2620
+ message: `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
2621
+ route: routeEntry.key
2622
+ });
2623
+ }
2624
+ });
2625
+ }
2626
+
2627
+ // src/pipeline/flows/dynamic/dynamic-stream.ts
2628
+ async function runDynamicStreamFlow(args) {
2629
+ const { ctx, strategy, verifyOutcome, account, body, result } = args;
2630
+ return settleAndFinalizeStream({
2631
+ ctx,
2632
+ strategy,
2633
+ verifyOutcome,
2634
+ source: result.source,
2635
+ account,
2636
+ body,
2637
+ bindChannelCharge: result.chargeContext.bindChannelCharge
2638
+ });
2639
+ }
2640
+
2641
+ // src/pipeline/flows/dynamic/dynamic-paid.ts
2642
+ async function runDynamicPaidFlow(ctx) {
2643
+ const { request, routeEntry, deps, report } = ctx;
2644
+ const apiKeyGate = await runApiKeyGate(ctx);
2645
+ if (!apiKeyGate.ok) return apiKeyGate.response;
2646
+ const { account } = apiKeyGate;
1987
2647
  const pricing = selectPricing(routeEntry.pricing, {
1988
- alert: alertFn,
2648
+ alert: report,
1989
2649
  maxPrice: routeEntry.maxPrice,
1990
2650
  minPrice: routeEntry.minPrice,
1991
2651
  route: routeEntry.key
1992
2652
  });
1993
2653
  const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
1994
- let earlyBody = void 0;
1995
- if (shouldParseBodyEarly(incomingStrategy, routeEntry, pricing)) {
1996
- const earlyClone = request.clone();
1997
- const earlyResult = await parseBody(earlyClone, routeEntry);
1998
- if (earlyResult.ok) {
1999
- earlyBody = earlyResult.data;
2000
- const validateErr2 = await runValidate(ctx, earlyBody);
2001
- if (validateErr2) return validateErr2;
2002
- } else {
2003
- firePluginResponse(ctx, earlyResult.response);
2004
- return earlyResult.response;
2005
- }
2006
- }
2654
+ const earlyResolution = await resolveEarlyBody({ ctx, pricing, incomingStrategy });
2655
+ if (!earlyResolution.ok) return earlyResolution.response;
2656
+ const { earlyBody } = earlyResolution;
2007
2657
  const siwxFastPath = await trySiwxFastPath(ctx, account);
2008
2658
  if (siwxFastPath) return siwxFastPath;
2009
2659
  if (!incomingStrategy) {
@@ -2011,39 +2661,32 @@ async function runPaidFlow(ctx) {
2011
2661
  if (initError) return fail(ctx, 500, initError);
2012
2662
  return build402(ctx, pricing, earlyBody);
2013
2663
  }
2014
- const body = await parseBody(request, routeEntry);
2015
- if (!body.ok) {
2016
- firePluginResponse(ctx, body.response);
2017
- return body.response;
2018
- }
2019
- const validateErr = await runValidate(ctx, body.data);
2020
- if (validateErr) return validateErr;
2021
- if (!pricing) {
2022
- return fail(ctx, 500, "Pricing not configured", body.data);
2023
- }
2024
- let price;
2025
- try {
2026
- price = await pricing.quote(body.data);
2027
- } catch (err) {
2028
- return fail(
2664
+ const { skipBody, skipHandler } = resolveDynamicPreflight(incomingStrategy, request, routeEntry);
2665
+ if (skipHandler) {
2666
+ return runDynamicChannelMgmtFlow({
2029
2667
  ctx,
2030
- errorStatus(err, 500),
2031
- errorMessage(err, "Price calculation failed"),
2032
- body.data
2033
- );
2668
+ strategy: incomingStrategy,
2669
+ account,
2670
+ pricing,
2671
+ skipBody
2672
+ });
2034
2673
  }
2674
+ const bodyAndPrice = await resolveDynamicBodyAndPrice({ ctx, pricing, skipBody });
2675
+ if (!bodyAndPrice.ok) return bodyAndPrice.response;
2676
+ const { parsedBody, price } = bodyAndPrice;
2035
2677
  const verifyOutcome = await incomingStrategy.verify({
2036
2678
  request,
2037
- body: body.data,
2679
+ body: parsedBody,
2038
2680
  price,
2039
2681
  routeEntry,
2040
- deps
2682
+ deps,
2683
+ report
2041
2684
  });
2042
2685
  if (verifyOutcome.ok === false) {
2043
2686
  if (verifyOutcome.kind === "config") {
2044
- return fail(ctx, 500, verifyOutcome.message, body.data);
2687
+ return fail(ctx, 500, verifyOutcome.message, parsedBody);
2045
2688
  }
2046
- return build402(ctx, pricing, body.data);
2689
+ return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
2047
2690
  }
2048
2691
  ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2049
2692
  firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
@@ -2052,11 +2695,71 @@ async function runPaidFlow(ctx) {
2052
2695
  amount: price,
2053
2696
  network: verifyOutcome.payment.network
2054
2697
  });
2055
- const result = await invoke(ctx, verifyOutcome.wallet, account, body.data, verifyOutcome.payment);
2698
+ const result = await invokeDynamic(
2699
+ ctx,
2700
+ verifyOutcome.wallet,
2701
+ account,
2702
+ parsedBody,
2703
+ verifyOutcome.payment
2704
+ );
2705
+ switch (result.kind) {
2706
+ case "stream":
2707
+ return runDynamicStreamFlow({
2708
+ ctx,
2709
+ strategy: incomingStrategy,
2710
+ verifyOutcome,
2711
+ account,
2712
+ body: parsedBody,
2713
+ result
2714
+ });
2715
+ case "request":
2716
+ return runDynamicRequestFlow({
2717
+ ctx,
2718
+ strategy: incomingStrategy,
2719
+ verifyOutcome,
2720
+ account,
2721
+ body: parsedBody,
2722
+ result
2723
+ });
2724
+ }
2725
+ }
2726
+
2727
+ // src/pipeline/flows/static/static-body-and-price.ts
2728
+ async function resolveStaticBodyAndPrice(args) {
2729
+ const { ctx, pricing } = args;
2730
+ const body = await parseBody(ctx);
2731
+ if (!body.ok) return { ok: false, response: body.response };
2732
+ const validateErr = await runValidate(ctx, body.data);
2733
+ if (validateErr) {
2734
+ return { ok: false, response: validateErr };
2735
+ }
2736
+ if (!pricing) {
2737
+ return { ok: false, response: fail(ctx, 500, "Pricing not configured", body.data) };
2738
+ }
2739
+ try {
2740
+ const price = await pricing.quote(body.data);
2741
+ return { ok: true, parsedBody: body.data, price };
2742
+ } catch (err) {
2743
+ return {
2744
+ ok: false,
2745
+ response: fail(
2746
+ ctx,
2747
+ errorStatus(err, 500),
2748
+ errorMessage(err, "Price calculation failed"),
2749
+ body.data
2750
+ )
2751
+ };
2752
+ }
2753
+ }
2754
+
2755
+ // src/pipeline/flows/static/static-request.ts
2756
+ async function runStaticRequestFlow(args) {
2757
+ const { ctx, strategy, verifyOutcome, account, body, price, result } = args;
2758
+ const { deps, routeEntry } = ctx;
2056
2759
  const settleScope = {
2057
2760
  wallet: verifyOutcome.wallet,
2058
2761
  account,
2059
- body: body.data,
2762
+ body,
2060
2763
  payment: verifyOutcome.payment,
2061
2764
  response: result.response,
2062
2765
  rawResult: result.rawResult,
@@ -2066,44 +2769,198 @@ async function runPaidFlow(ctx) {
2066
2769
  if (result.response.status >= 400) {
2067
2770
  const settledScope = settleScope;
2068
2771
  await runSettledHandlerError(ctx, settledScope);
2069
- return finalize(ctx, result.response, result.rawResult, body.data);
2772
+ return finalize(ctx, result.response, result.rawResult, body);
2070
2773
  }
2071
- return settleAndFinalize({
2774
+ return settleAndFinalizeRequest({
2072
2775
  ctx,
2073
- strategy: incomingStrategy,
2776
+ strategy,
2074
2777
  verifyOutcome,
2075
2778
  scope: settleScope,
2076
2779
  rawResult: result.rawResult,
2077
- body: body.data
2780
+ body,
2781
+ billedAmount: price
2078
2782
  });
2079
2783
  }
2080
2784
  if (result.response.status >= 400) {
2081
- return finalize(ctx, result.response, result.rawResult, body.data);
2785
+ return finalize(ctx, result.response, result.rawResult, body);
2082
2786
  }
2083
2787
  const beforeErr = await runBeforeSettle(ctx, settleScope);
2084
2788
  if (beforeErr) return beforeErr;
2085
- return settleAndFinalize({
2789
+ return settleAndFinalizeRequest({
2086
2790
  ctx,
2087
- strategy: incomingStrategy,
2791
+ strategy,
2088
2792
  verifyOutcome,
2089
2793
  scope: settleScope,
2090
2794
  rawResult: result.rawResult,
2091
- body: body.data,
2795
+ body,
2796
+ billedAmount: price,
2092
2797
  onSettleError: async (error, failMessage) => {
2093
2798
  await runSettlementError(ctx, settleScope, error, "settle");
2094
2799
  firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2095
2800
  level: "critical",
2096
- message: `${incomingStrategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
2801
+ message: `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
2097
2802
  route: routeEntry.key
2098
2803
  });
2099
2804
  }
2100
2805
  });
2101
2806
  }
2102
2807
 
2808
+ // src/pipeline/flows/static/static-paid.ts
2809
+ async function runStaticPaidFlow(ctx) {
2810
+ const { request, routeEntry, deps, report } = ctx;
2811
+ const apiKeyGate = await runApiKeyGate(ctx);
2812
+ if (!apiKeyGate.ok) return apiKeyGate.response;
2813
+ const { account } = apiKeyGate;
2814
+ const pricing = selectPricing(routeEntry.pricing, {
2815
+ alert: report,
2816
+ maxPrice: routeEntry.maxPrice,
2817
+ minPrice: routeEntry.minPrice,
2818
+ route: routeEntry.key
2819
+ });
2820
+ const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
2821
+ const earlyResolution = await resolveEarlyBody({ ctx, pricing, incomingStrategy });
2822
+ if (!earlyResolution.ok) return earlyResolution.response;
2823
+ const { earlyBody } = earlyResolution;
2824
+ const siwxFastPath = await trySiwxFastPath(ctx, account);
2825
+ if (siwxFastPath) return siwxFastPath;
2826
+ if (!incomingStrategy) {
2827
+ const initError = protocolInitError(routeEntry, deps);
2828
+ if (initError) return fail(ctx, 500, initError);
2829
+ return build402(ctx, pricing, earlyBody);
2830
+ }
2831
+ const bodyAndPrice = await resolveStaticBodyAndPrice({ ctx, pricing });
2832
+ if (!bodyAndPrice.ok) return bodyAndPrice.response;
2833
+ const { parsedBody, price } = bodyAndPrice;
2834
+ const verifyOutcome = await incomingStrategy.verify({
2835
+ request,
2836
+ body: parsedBody,
2837
+ price,
2838
+ routeEntry,
2839
+ deps,
2840
+ report
2841
+ });
2842
+ if (verifyOutcome.ok === false) {
2843
+ if (verifyOutcome.kind === "config") {
2844
+ return fail(ctx, 500, verifyOutcome.message, parsedBody);
2845
+ }
2846
+ return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
2847
+ }
2848
+ ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2849
+ firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
2850
+ protocol: incomingStrategy.protocol,
2851
+ payer: verifyOutcome.wallet,
2852
+ amount: price,
2853
+ network: verifyOutcome.payment.network
2854
+ });
2855
+ const result = await invokePaidStatic(
2856
+ ctx,
2857
+ verifyOutcome.wallet,
2858
+ account,
2859
+ parsedBody,
2860
+ verifyOutcome.payment
2861
+ );
2862
+ return runStaticRequestFlow({
2863
+ ctx,
2864
+ strategy: incomingStrategy,
2865
+ verifyOutcome,
2866
+ account,
2867
+ body: parsedBody,
2868
+ price,
2869
+ result
2870
+ });
2871
+ }
2872
+
2873
+ // src/pipeline/flows/paid.ts
2874
+ async function runPaidFlow(ctx) {
2875
+ const dynamicPrice = ctx.routeEntry.dynamicPrice ?? false;
2876
+ switch (dynamicPrice) {
2877
+ case true:
2878
+ return runDynamicPaidFlow(ctx);
2879
+ case false:
2880
+ return runStaticPaidFlow(ctx);
2881
+ }
2882
+ }
2883
+
2103
2884
  // src/pipeline/flows/siwx-only.ts
2104
- var import_server5 = require("next/server");
2885
+ var import_server7 = require("next/server");
2886
+
2887
+ // src/kv-store/client.ts
2888
+ function restKvStore(url, token) {
2889
+ const base = url.replace(/\/+$/, "");
2890
+ const authHeader = { Authorization: `Bearer ${token}` };
2891
+ const jsonHeaders = { ...authHeader, "Content-Type": "application/json" };
2892
+ async function exec(command) {
2893
+ const res = await fetch(base, {
2894
+ method: "POST",
2895
+ headers: jsonHeaders,
2896
+ body: JSON.stringify(command)
2897
+ });
2898
+ if (!res.ok) {
2899
+ throw new Error(`[kv-store] ${command[0]} ${command[1] ?? ""}: ${res.status}`);
2900
+ }
2901
+ const body = await res.json();
2902
+ if (body.error) throw new Error(`[kv-store] ${command[0]}: ${body.error}`);
2903
+ return body.result ?? null;
2904
+ }
2905
+ async function get(key) {
2906
+ const res = await fetch(`${base}/get/${encodeURIComponent(key)}`, { headers: authHeader });
2907
+ if (!res.ok) throw new Error(`[kv-store] GET ${key}: ${res.status}`);
2908
+ const { result } = await res.json();
2909
+ return result ?? null;
2910
+ }
2911
+ async function set(key, value) {
2912
+ await exec(["SET", key, JSON.stringify(value)]);
2913
+ }
2914
+ async function del(key) {
2915
+ await exec(["DEL", key]);
2916
+ }
2917
+ async function setNxEx(key, value, ttlSeconds) {
2918
+ const result = await exec(["SET", key, JSON.stringify(value), "EX", ttlSeconds, "NX"]);
2919
+ return result === "OK";
2920
+ }
2921
+ async function sadd(key, member) {
2922
+ await exec(["SADD", key, member]);
2923
+ }
2924
+ async function sismember(key, member) {
2925
+ const result = await exec(["SISMEMBER", key, member]);
2926
+ return result === 1;
2927
+ }
2928
+ async function update(key, fn) {
2929
+ const current = await get(key);
2930
+ const change = fn(current);
2931
+ if (change.op === "set") await set(key, change.value);
2932
+ if (change.op === "delete") await del(key);
2933
+ return change.result;
2934
+ }
2935
+ return { get, set, del, setNxEx, sadd, sismember, update };
2936
+ }
2937
+ function isRestConfig(input) {
2938
+ return typeof input === "object" && input !== null && typeof input.url === "string" && typeof input.token === "string" && typeof input.get !== "function";
2939
+ }
2940
+ function resolveKvStore(input, env = process.env) {
2941
+ if (input) {
2942
+ if (isRestConfig(input)) return restKvStore(input.url, input.token);
2943
+ return input;
2944
+ }
2945
+ const url = env.KV_REST_API_URL;
2946
+ const token = env.KV_REST_API_TOKEN;
2947
+ if (url && token) return restKvStore(url, token);
2948
+ return void 0;
2949
+ }
2950
+ function withPrefix(kv, prefix) {
2951
+ const k = (key) => `${prefix}${key}`;
2952
+ return {
2953
+ get: (key) => kv.get(k(key)),
2954
+ set: (key, value) => kv.set(k(key), value),
2955
+ del: (key) => kv.del(k(key)),
2956
+ setNxEx: (key, value, ttl) => kv.setNxEx(k(key), value, ttl),
2957
+ sadd: (key, member) => kv.sadd(k(key), member),
2958
+ sismember: (key, member) => kv.sismember(k(key), member),
2959
+ update: (key, fn) => kv.update(k(key), fn)
2960
+ };
2961
+ }
2105
2962
 
2106
- // src/auth/nonce.ts
2963
+ // src/kv-store/nonce.ts
2107
2964
  var SIWX_CHALLENGE_EXPIRY_MS = 5 * 60 * 1e3;
2108
2965
  var MemoryNonceStore = class {
2109
2966
  seen = /* @__PURE__ */ new Map();
@@ -2120,48 +2977,59 @@ var MemoryNonceStore = class {
2120
2977
  }
2121
2978
  }
2122
2979
  };
2123
- function detectRedisClientType(client) {
2124
- if (!client || typeof client !== "object") {
2125
- throw new Error(
2126
- "createRedisNonceStore requires a Redis client. Supported: @upstash/redis, ioredis. Pass your Redis client instance as the first argument."
2127
- );
2128
- }
2129
- if ("options" in client && "status" in client) {
2130
- return "ioredis";
2131
- }
2132
- const constructor = client.constructor?.name;
2133
- if (constructor === "Redis" && "url" in client) {
2134
- return "upstash";
2980
+ function createKvNonceStore(kv, options) {
2981
+ const prefix = options?.prefix ?? "siwx:nonce:";
2982
+ const ttlSeconds = Math.ceil((options?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
2983
+ return {
2984
+ async check(nonce) {
2985
+ return kv.setNxEx(`${prefix}${nonce}`, 1, ttlSeconds);
2986
+ }
2987
+ };
2988
+ }
2989
+
2990
+ // src/kv-store/entitlement.ts
2991
+ var MemoryEntitlementStore = class {
2992
+ routeToWallets = /* @__PURE__ */ new Map();
2993
+ async has(route, wallet) {
2994
+ const wallets = this.routeToWallets.get(route);
2995
+ if (!wallets) return false;
2996
+ return wallets.has(normalizeWalletAddress(wallet));
2135
2997
  }
2136
- if (typeof client.set === "function") {
2137
- return "upstash";
2998
+ async grant(route, wallet) {
2999
+ const normalized = normalizeWalletAddress(wallet);
3000
+ let wallets = this.routeToWallets.get(route);
3001
+ if (!wallets) {
3002
+ wallets = /* @__PURE__ */ new Set();
3003
+ this.routeToWallets.set(route, wallets);
3004
+ }
3005
+ wallets.add(normalized);
2138
3006
  }
2139
- throw new Error(
2140
- "Unrecognized Redis client. Supported: @upstash/redis, ioredis. If using a different client, implement NonceStore interface directly."
2141
- );
2142
- }
2143
- function createRedisNonceStore(client, opts) {
2144
- const prefix = opts?.prefix ?? "siwx:nonce:";
2145
- const ttlSeconds = Math.ceil((opts?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
2146
- const clientType = detectRedisClientType(client);
3007
+ };
3008
+ function createKvEntitlementStore(kv, options) {
3009
+ const prefix = options?.prefix ?? "siwx:ent:";
2147
3010
  return {
2148
- async check(nonce) {
2149
- const key = `${prefix}${nonce}`;
2150
- if (clientType === "upstash") {
2151
- const redis = client;
2152
- const result = await redis.set(key, "1", { ex: ttlSeconds, nx: true });
2153
- return result !== null;
2154
- }
2155
- if (clientType === "ioredis") {
2156
- const redis = client;
2157
- const result = await redis.set(key, "1", "EX", ttlSeconds, "NX");
2158
- return result === "OK";
2159
- }
2160
- throw new Error("Unknown Redis client type");
3011
+ async has(route, wallet) {
3012
+ return kv.sismember(`${prefix}${route}`, normalizeWalletAddress(wallet));
3013
+ },
3014
+ async grant(route, wallet) {
3015
+ await kv.sadd(`${prefix}${route}`, normalizeWalletAddress(wallet));
2161
3016
  }
2162
3017
  };
2163
3018
  }
2164
3019
 
3020
+ // src/kv-store/mpp.ts
3021
+ async function createKvMppStore(kv, options) {
3022
+ const prefix = options?.prefix ?? "mpp:";
3023
+ const namespaced = withPrefix(kv, prefix);
3024
+ const { Store: StoreNs } = await import("mppx");
3025
+ return StoreNs.upstash({
3026
+ get: (key) => namespaced.get(key),
3027
+ set: (key, value) => namespaced.set(key, value),
3028
+ del: (key) => namespaced.del(key),
3029
+ update: (key, fn) => namespaced.update(key, fn)
3030
+ });
3031
+ }
3032
+
2165
3033
  // src/protocols/mpp/siwx-mode.ts
2166
3034
  var import_mppx3 = require("mppx");
2167
3035
  async function verifyMppSiwx(request, mppx) {
@@ -2180,12 +3048,11 @@ async function runSiwxOnlyFlow(ctx) {
2180
3048
  const { request, routeEntry, deps } = ctx;
2181
3049
  if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get(HEADERS.SIWX)) {
2182
3050
  const earlyClone = request.clone();
2183
- const earlyBody = await parseBody(earlyClone, routeEntry);
3051
+ const earlyBody = await parseBody(ctx, earlyClone);
2184
3052
  if (earlyBody.ok) {
2185
3053
  const validateErr = await runValidate(ctx, earlyBody.data);
2186
3054
  if (validateErr) return validateErr;
2187
3055
  } else {
2188
- firePluginResponse(ctx, earlyBody.response);
2189
3056
  return earlyBody.response;
2190
3057
  }
2191
3058
  }
@@ -2197,11 +3064,7 @@ async function runSiwxOnlyFlow(ctx) {
2197
3064
  mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
2198
3065
  } catch (err) {
2199
3066
  const message = err instanceof Error ? err.message : String(err);
2200
- firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2201
- level: "critical",
2202
- message: `MPP SIWX verification failed: ${message}`,
2203
- route: routeEntry.key
2204
- });
3067
+ ctx.report("critical", `MPP SIWX verification failed: ${message}`);
2205
3068
  return fail(ctx, 500, `MPP SIWX verification failed: ${message}`);
2206
3069
  }
2207
3070
  if (mppSiwxResult.valid) {
@@ -2223,7 +3086,7 @@ async function runSiwxOnlyFlow(ctx) {
2223
3086
  }
2224
3087
  const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
2225
3088
  if (!siwx.valid) {
2226
- const response = import_server5.NextResponse.json(
3089
+ const response = import_server7.NextResponse.json(
2227
3090
  { error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
2228
3091
  { status: 402 }
2229
3092
  );
@@ -2273,7 +3136,6 @@ async function buildSiwxChallenge(ctx) {
2273
3136
  extensions: {
2274
3137
  "sign-in-with-x": {
2275
3138
  info: siwxInfo,
2276
- // Required by MCP tools at the top level for chain detection.
2277
3139
  supportedChains,
2278
3140
  ...siwxSchema ? { schema: siwxSchema } : {}
2279
3141
  }
@@ -2284,13 +3146,12 @@ async function buildSiwxChallenge(ctx) {
2284
3146
  const { encodePaymentRequiredHeader } = await import("@x402/core/http");
2285
3147
  encoded = encodePaymentRequiredHeader(paymentRequired);
2286
3148
  } catch (err) {
2287
- firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2288
- level: "warn",
2289
- message: `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`,
2290
- route: routeEntry.key
2291
- });
3149
+ ctx.report(
3150
+ "warn",
3151
+ `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`
3152
+ );
2292
3153
  }
2293
- const response = new import_server5.NextResponse(JSON.stringify(paymentRequired), {
3154
+ const response = new import_server7.NextResponse(JSON.stringify(paymentRequired), {
2294
3155
  status: 402,
2295
3156
  headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
2296
3157
  });
@@ -2396,6 +3257,12 @@ var RouteBuilder = class {
2396
3257
  /** @internal */
2397
3258
  _minPrice;
2398
3259
  /** @internal */
3260
+ _dynamicPrice = false;
3261
+ /** @internal */
3262
+ _tickCost;
3263
+ /** @internal */
3264
+ _unitType;
3265
+ /** @internal */
2399
3266
  _payTo;
2400
3267
  /** @internal */
2401
3268
  _bodySchema;
@@ -2440,7 +3307,8 @@ var RouteBuilder = class {
2440
3307
  next._protocols = [...this._protocols];
2441
3308
  return next;
2442
3309
  }
2443
- paid(pricing, options) {
3310
+ paid(pricingOrOptions, options) {
3311
+ const { pricing, resolvedOptions } = resolvePaidArgs(this._key, pricingOrOptions, options);
2444
3312
  if (this._authMode === "unprotected") {
2445
3313
  throw new Error(
2446
3314
  `route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
@@ -2454,16 +3322,24 @@ var RouteBuilder = class {
2454
3322
  const next = this.fork();
2455
3323
  next._authMode = "paid";
2456
3324
  next._pricing = pricing;
2457
- if (options?.protocols) {
2458
- next._protocols = [...options.protocols];
3325
+ if (resolvedOptions?.protocols) {
3326
+ next._protocols = [...resolvedOptions.protocols];
2459
3327
  } else if (next._protocols.length === 0) {
2460
3328
  next._protocols = ["x402"];
2461
3329
  }
2462
- if (options?.maxPrice) next._maxPrice = options.maxPrice;
2463
- if (options?.minPrice) next._minPrice = options.minPrice;
2464
- if (options?.payTo) next._payTo = options.payTo;
2465
- if (options?.mpp) next._mppInfo = options.mpp;
3330
+ if (resolvedOptions?.maxPrice) next._maxPrice = resolvedOptions.maxPrice;
3331
+ if (resolvedOptions?.minPrice) next._minPrice = resolvedOptions.minPrice;
3332
+ if (resolvedOptions?.payTo) next._payTo = resolvedOptions.payTo;
3333
+ if (resolvedOptions?.mpp) next._mppInfo = resolvedOptions.mpp;
3334
+ if (resolvedOptions?.dynamic) next._dynamicPrice = true;
3335
+ if (resolvedOptions?.tickCost) next._tickCost = resolvedOptions.tickCost;
3336
+ if (resolvedOptions?.unitType) next._unitType = resolvedOptions.unitType;
2466
3337
  if (typeof pricing === "object" && "tiers" in pricing) {
3338
+ if (next._dynamicPrice) {
3339
+ throw new Error(
3340
+ `route '${this._key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
3341
+ );
3342
+ }
2467
3343
  for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
2468
3344
  if (!tierKey) {
2469
3345
  throw new Error(`route '${this._key}': tier key cannot be empty`);
@@ -2476,16 +3352,40 @@ var RouteBuilder = class {
2476
3352
  }
2477
3353
  }
2478
3354
  }
2479
- if (options?.maxPrice !== void 0) {
2480
- const parsed = parseFloat(options.maxPrice);
3355
+ if (resolvedOptions?.maxPrice !== void 0) {
3356
+ const parsed = parseFloat(resolvedOptions.maxPrice);
2481
3357
  if (isNaN(parsed) || parsed <= 0) {
2482
3358
  throw new Error(
2483
- `route '${this._key}': maxPrice '${options.maxPrice}' must be a positive decimal string`
3359
+ `route '${this._key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
2484
3360
  );
2485
3361
  }
2486
3362
  }
3363
+ if (resolvedOptions?.tickCost !== void 0) {
3364
+ const parsed = parseFloat(resolvedOptions.tickCost);
3365
+ if (isNaN(parsed) || parsed <= 0) {
3366
+ throw new Error(
3367
+ `route '${this._key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
3368
+ );
3369
+ }
3370
+ }
3371
+ if (next._dynamicPrice && !next._maxPrice) {
3372
+ throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires maxPrice`);
3373
+ }
3374
+ if (next._dynamicPrice && !next._tickCost) {
3375
+ throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires tickCost`);
3376
+ }
2487
3377
  return next;
2488
3378
  }
3379
+ /**
3380
+ * Require Sign-In-with-X wallet identity on this route — clients prove
3381
+ * control of a wallet via a signed challenge. Combine with `.paid()` to gate
3382
+ * a paid route on a verified wallet identity.
3383
+ *
3384
+ * @example
3385
+ * ```ts
3386
+ * router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
3387
+ * ```
3388
+ */
2489
3389
  siwx() {
2490
3390
  if (this._authMode === "unprotected") {
2491
3391
  throw new Error(
@@ -2508,6 +3408,19 @@ var RouteBuilder = class {
2508
3408
  next._protocols = [];
2509
3409
  return next;
2510
3410
  }
3411
+ /**
3412
+ * Require an `X-API-Key` header (or `Authorization: Bearer <key>`); the
3413
+ * resolver returns the account record, or `null` for 401. Composes with
3414
+ * `.paid()` — key is checked first, payment second.
3415
+ *
3416
+ * @example
3417
+ * ```ts
3418
+ * router
3419
+ * .route('admin/users')
3420
+ * .apiKey(async (key) => db.admin.findByKey(key))
3421
+ * .handler(async ({ account }) => db.user.list(account.orgId));
3422
+ * ```
3423
+ */
2511
3424
  apiKey(resolver) {
2512
3425
  if (this._siwxEnabled) {
2513
3426
  throw new Error(
@@ -2519,6 +3432,15 @@ var RouteBuilder = class {
2519
3432
  next._apiKeyResolver = resolver;
2520
3433
  return next;
2521
3434
  }
3435
+ /**
3436
+ * Mark the route as public — no auth, no payment, no SIWX. The handler
3437
+ * receives `null` for `wallet`, `payment`, and `account`.
3438
+ *
3439
+ * @example
3440
+ * ```ts
3441
+ * router.route('health').unprotected().handler(async () => ({ status: 'ok' }));
3442
+ * ```
3443
+ */
2522
3444
  unprotected() {
2523
3445
  if (this._authMode && this._authMode !== "unprotected") {
2524
3446
  throw new Error(
@@ -2535,60 +3457,82 @@ var RouteBuilder = class {
2535
3457
  next._protocols = [];
2536
3458
  return next;
2537
3459
  }
2538
- // -------------------------------------------------------------------------
2539
- // Provider monitoring
2540
- // -------------------------------------------------------------------------
3460
+ /**
3461
+ * Tag the route with an upstream provider for discovery and provider-side
3462
+ * monitoring. The provider name and config surface in `well-known` and
3463
+ * OpenAPI output.
3464
+ *
3465
+ * @example
3466
+ * ```ts
3467
+ * router
3468
+ * .route('search')
3469
+ * .paid('0.01')
3470
+ * .provider('exa', { quotaPerMonth: 1000 })
3471
+ * .handler(handler);
3472
+ * ```
3473
+ */
2541
3474
  provider(name, config) {
2542
3475
  const next = this.fork();
2543
3476
  next._providerName = name;
2544
3477
  next._providerConfig = config ?? {};
2545
3478
  return next;
2546
3479
  }
2547
- body(schema, example) {
3480
+ /**
3481
+ * Declare the request body's Zod schema. Parsed body is typed as `ctx.body`
3482
+ * in the handler. Use `.inputExample()` to attach a discovery example.
3483
+ *
3484
+ * @example
3485
+ * ```ts
3486
+ * .body(z.object({ query: z.string() }))
3487
+ * .handler(async ({ body }) => search(body.query));
3488
+ * ```
3489
+ */
3490
+ body(schema) {
2548
3491
  const next = this.fork();
2549
3492
  next._bodySchema = schema;
2550
- if (example !== void 0) {
2551
- next._inputExample = example;
2552
- next._hasInputExample = true;
2553
- }
2554
3493
  return next;
2555
3494
  }
2556
- query(schema, example) {
3495
+ /**
3496
+ * Declare a query-string Zod schema and switch the route to `GET`. Parsed
3497
+ * query is typed as `ctx.query` in the handler. Use `.inputExample()` to
3498
+ * attach a discovery example.
3499
+ *
3500
+ * @example
3501
+ * ```ts
3502
+ * .query(z.object({ id: z.string() }))
3503
+ * .handler(async ({ query }) => getById(query.id));
3504
+ * ```
3505
+ */
3506
+ query(schema) {
2557
3507
  const next = this.fork();
2558
3508
  next._querySchema = schema;
2559
- if (example !== void 0) {
2560
- next._inputExample = example;
2561
- next._hasInputExample = true;
2562
- }
2563
3509
  next._method = "GET";
2564
3510
  return next;
2565
3511
  }
2566
- output(schema, example) {
3512
+ /**
3513
+ * Declare the response output's Zod schema for OpenAPI generation. The
3514
+ * runtime does not validate handler return values — use Zod's `.parse()`
3515
+ * inside the handler if strict output validation is required. Use
3516
+ * `.outputExample()` to attach a discovery example.
3517
+ *
3518
+ * @example
3519
+ * ```ts
3520
+ * .output(z.object({ result: z.string() }))
3521
+ * .handler(async () => ({ result: 'ok' }));
3522
+ * ```
3523
+ */
3524
+ output(schema) {
2567
3525
  const next = this.fork();
2568
3526
  next._outputSchema = schema;
2569
- if (example !== void 0) {
2570
- next._outputExample = example;
2571
- next._hasOutputExample = true;
2572
- }
2573
3527
  return next;
2574
3528
  }
2575
3529
  /**
2576
- * Provide a conforming example of the request input (body or query params).
2577
- *
2578
- * Optional. When provided, the example is validated against the request schema
2579
- * at route registration and embedded in the bazaar discovery extension so
2580
- * indexers can advertise a working sample call.
2581
- *
2582
- * For the common case, pass the example directly to `.body(schema, example)` or
2583
- * `.query(schema, example)` instead.
3530
+ * Attach an example of the request body or query for discovery output,
3531
+ * validated against the registered schema at registration.
2584
3532
  *
2585
3533
  * @example
2586
3534
  * ```ts
2587
- * router.route('search')
2588
- * .paid('0.01')
2589
- * .body(z.object({ q: z.string() }))
2590
- * .inputExample({ q: 'hello world' })
2591
- * .handler(async ({ body }) => { ... });
3535
+ * .body(searchSchema).inputExample({ query: 'cats' });
2592
3536
  * ```
2593
3537
  */
2594
3538
  inputExample(example) {
@@ -2598,32 +3542,12 @@ var RouteBuilder = class {
2598
3542
  return next;
2599
3543
  }
2600
3544
  /**
2601
- * Provide a conforming example of the response output.
2602
- *
2603
- * Optional. When provided, the example is validated against the output schema
2604
- * at route registration and embedded in the bazaar discovery extension so
2605
- * indexers can advertise the response shape.
2606
- *
2607
- * For the common case, pass the example directly to `.output(schema, example)` instead.
2608
- *
2609
- * Accepts any JSON value (objects, arrays, or primitives) — top-level array
2610
- * or primitive responses (e.g. `z.array(...)`) are supported alongside the
2611
- * common object case.
3545
+ * Attach an example response for discovery output, validated against the
3546
+ * registered output schema at registration.
2612
3547
  *
2613
3548
  * @example
2614
3549
  * ```ts
2615
- * router.route('search')
2616
- * .paid('0.01')
2617
- * .output(z.object({ results: z.array(z.string()) }))
2618
- * .outputExample({ results: ['a', 'b'] })
2619
- * .handler(async () => { ... });
2620
- *
2621
- * // Top-level array response
2622
- * router.route('chains')
2623
- * .paid('0.01')
2624
- * .output(z.array(z.object({ name: z.string() })))
2625
- * .outputExample([{ name: 'Ethereum' }])
2626
- * .handler(async () => { ... });
3550
+ * .output(resultSchema).outputExample({ result: 'ok' });
2627
3551
  * ```
2628
3552
  */
2629
3553
  outputExample(example) {
@@ -2632,43 +3556,60 @@ var RouteBuilder = class {
2632
3556
  next._hasOutputExample = true;
2633
3557
  return next;
2634
3558
  }
3559
+ /**
3560
+ * Set a human-readable summary of the route. Surfaces in OpenAPI,
3561
+ * `well-known`, and `llms.txt` discovery output.
3562
+ *
3563
+ * @example
3564
+ * ```ts
3565
+ * .description('Search indexed web pages by full-text query');
3566
+ * ```
3567
+ */
2635
3568
  description(text) {
2636
3569
  const next = this.fork();
2637
3570
  next._description = text;
2638
3571
  return next;
2639
3572
  }
3573
+ /**
3574
+ * Override the URL path advertised in discovery output. Defaults to the
3575
+ * registry key passed to `.route()`.
3576
+ *
3577
+ * @example
3578
+ * ```ts
3579
+ * router.route('search').path('/v2/search').handler(handler);
3580
+ * ```
3581
+ */
2640
3582
  path(p) {
2641
3583
  const next = this.fork();
2642
3584
  next._path = p;
2643
3585
  return next;
2644
3586
  }
3587
+ /**
3588
+ * Override the HTTP method advertised in discovery. Defaults to `POST`, or
3589
+ * `GET` when `.query()` has been called.
3590
+ *
3591
+ * @example
3592
+ * ```ts
3593
+ * router.route('items/delete').method('DELETE').handler(handler);
3594
+ * ```
3595
+ */
2645
3596
  method(m) {
2646
3597
  const next = this.fork();
2647
3598
  next._method = m;
2648
3599
  return next;
2649
3600
  }
2650
- // -------------------------------------------------------------------------
2651
- // Pre-payment validation
2652
- // -------------------------------------------------------------------------
2653
3601
  /**
2654
- * Add pre-payment validation that runs after body parsing but before the 402
2655
- * challenge is shown. Use this for async business logic like "is this resource
2656
- * available?" or "has this user hit their rate limit?".
2657
- *
2658
- * Requires `.body()` — call `.body()` before `.validate()` for type inference.
3602
+ * Run validation against the parsed body before the 402 challenge. Throw
3603
+ * `Object.assign(new Error('...'), { status })` to reject with a custom
3604
+ * status code; defaults to 400. Requires `.body()` to be called first.
2659
3605
  *
2660
3606
  * @example
2661
- * ```typescript
2662
- * router
2663
- * .route('domain/register')
2664
- * .paid(calculatePrice)
2665
- * .body(RegisterSchema) // .body() first for type inference
2666
- * .validate(async (body) => {
2667
- * if (await isDomainTaken(body.domain)) {
2668
- * throw Object.assign(new Error('Domain taken'), { status: 409 });
2669
- * }
2670
- * })
2671
- * .handler(async ({ body }) => { ... });
3607
+ * ```ts
3608
+ * .body(RegisterSchema).validate(async (body) => {
3609
+ * if (await isTaken(body.name)) {
3610
+ * throw Object.assign(new Error('taken'), { status: 409 });
3611
+ * }
3612
+ * });
2672
3613
  * ```
2673
3614
  */
2674
3615
  validate(fn) {
@@ -2676,27 +3617,64 @@ var RouteBuilder = class {
2676
3617
  next._validateFn = fn;
2677
3618
  return next;
2678
3619
  }
2679
- // -------------------------------------------------------------------------
2680
- // Settlement lifecycle
2681
- // -------------------------------------------------------------------------
2682
3620
  /**
2683
- * Add route-specific settlement hooks.
3621
+ * Hook into the settlement lifecycle. `beforeSettle` runs after the handler
3622
+ * succeeds but before on-chain settlement and can cancel the charge;
3623
+ * `afterSettle` runs after settlement completes (success or failure).
2684
3624
  *
2685
- * `beforeSettle` runs after a successful handler response but before
2686
- * router-controlled settlement/broadcast, so it can still prevent the charge
2687
- * for x402 and MPP transaction-payload flows. `afterSettle` runs after
2688
- * settlement and is intended for durable ledgers or app-owned refund queues.
3625
+ * @example
3626
+ * ```ts
3627
+ * .settlement({
3628
+ * beforeSettle: ({ result }) => (result.refund ? 'skip' : 'continue'),
3629
+ * afterSettle: ({ tx }) => analytics.track('settled', { tx }),
3630
+ * });
3631
+ * ```
2689
3632
  */
2690
3633
  settlement(lifecycle) {
2691
3634
  const next = this.fork();
2692
3635
  next._settlement = lifecycle;
2693
3636
  return next;
2694
3637
  }
2695
- // -------------------------------------------------------------------------
2696
- // Terminal method
2697
- // -------------------------------------------------------------------------
3638
+ /**
3639
+ * Register the request handler and return the Next.js route function. The
3640
+ * handler receives a typed context and may return a value (serialized to
3641
+ * JSON), a raw `Response`, or throw an `HttpError` for a non-2xx status.
3642
+ *
3643
+ * @example
3644
+ * ```ts
3645
+ * export const POST = router
3646
+ * .route('search')
3647
+ * .paid('0.01')
3648
+ * .body(schema)
3649
+ * .handler(async ({ body, wallet }) => searchService(body, wallet));
3650
+ * ```
3651
+ */
2698
3652
  handler(fn) {
2699
- const handlerFn = fn;
3653
+ return this.register(fn, false);
3654
+ }
3655
+ /**
3656
+ * Register a streaming handler (`async function*`) and return the Next.js
3657
+ * route function. Each `charge()` call bills one tick (`tickCost` USDC) up
3658
+ * to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
3659
+ *
3660
+ * @example
3661
+ * ```ts
3662
+ * export const POST = router
3663
+ * .route('llm/stream')
3664
+ * .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
3665
+ * .body(schema)
3666
+ * .stream(async function* ({ body, charge }) {
3667
+ * for await (const token of streamLLM(body.prompt)) {
3668
+ * await charge();
3669
+ * yield token;
3670
+ * }
3671
+ * });
3672
+ * ```
3673
+ */
3674
+ stream(fn) {
3675
+ return this.register(fn, true);
3676
+ }
3677
+ register(handlerFn, streaming) {
2700
3678
  if (!this._authMode) {
2701
3679
  throw new Error(
2702
3680
  `route '${this._key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
@@ -2710,6 +3688,26 @@ var RouteBuilder = class {
2710
3688
  if (this._settlement && !this._pricing) {
2711
3689
  throw new Error(`route '${this._key}': .settlement() requires a paid route`);
2712
3690
  }
3691
+ if (this._dynamicPrice && this._protocols.includes("x402")) {
3692
+ const hasUpto = this._deps.x402Accepts.some((accept) => accept.scheme === "upto");
3693
+ if (!hasUpto) {
3694
+ throw new Error(
3695
+ `route '${this._key}': .paid({ dynamic: true }) on an x402 route requires an 'upto' accept on at least one configured network. Add { scheme: 'upto', network, asset } to RouterConfig.x402.accepts.`
3696
+ );
3697
+ }
3698
+ }
3699
+ if (this._dynamicPrice && this._protocols.includes("mpp")) {
3700
+ if (!this._deps.mppSessionConfig) {
3701
+ throw new Error(
3702
+ `route '${this._key}': .paid({ dynamic: true }) on an MPP route requires session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
3703
+ );
3704
+ }
3705
+ }
3706
+ if (streaming && !this._dynamicPrice) {
3707
+ throw new Error(
3708
+ `route '${this._key}': .stream() requires .paid({ dynamic: true }) \u2014 static/free routes can't meter per-chunk billing.`
3709
+ );
3710
+ }
2713
3711
  validateExamples(
2714
3712
  this._key,
2715
3713
  this._bodySchema,
@@ -2725,6 +3723,8 @@ var RouteBuilder = class {
2725
3723
  authMode: this._authMode,
2726
3724
  siwxEnabled: this._siwxEnabled,
2727
3725
  pricing: this._pricing,
3726
+ dynamicPrice: this._dynamicPrice ? true : void 0,
3727
+ streaming: streaming ? true : void 0,
2728
3728
  protocols: this._protocols,
2729
3729
  bodySchema: this._bodySchema,
2730
3730
  querySchema: this._querySchema,
@@ -2742,81 +3742,28 @@ var RouteBuilder = class {
2742
3742
  providerConfig: this._providerConfig,
2743
3743
  validateFn: this._validateFn,
2744
3744
  settlement: this._settlement,
2745
- mppInfo: this._mppInfo
3745
+ mppInfo: this._mppInfo,
3746
+ tickCost: this._tickCost,
3747
+ unitType: this._unitType
2746
3748
  };
2747
3749
  this._registry.register(entry);
2748
- return createRequestHandler(
2749
- entry,
2750
- handlerFn,
2751
- this._deps
2752
- );
3750
+ return createRequestHandler(entry, handlerFn, this._deps);
2753
3751
  }
2754
3752
  };
2755
-
2756
- // src/auth/entitlement.ts
2757
- var MemoryEntitlementStore = class {
2758
- routeToWallets = /* @__PURE__ */ new Map();
2759
- async has(route, wallet) {
2760
- const wallets = this.routeToWallets.get(route);
2761
- if (!wallets) return false;
2762
- return wallets.has(normalizeWalletAddress(wallet));
2763
- }
2764
- async grant(route, wallet) {
2765
- const normalized = normalizeWalletAddress(wallet);
2766
- let wallets = this.routeToWallets.get(route);
2767
- if (!wallets) {
2768
- wallets = /* @__PURE__ */ new Set();
2769
- this.routeToWallets.set(route, wallets);
3753
+ function resolvePaidArgs(routeKey, pricingOrOptions, options) {
3754
+ const isHandlerDynamicShape = typeof pricingOrOptions === "object" && pricingOrOptions !== null && typeof pricingOrOptions !== "function" && !("tiers" in pricingOrOptions) && "dynamic" in pricingOrOptions && pricingOrOptions.dynamic;
3755
+ if (isHandlerDynamicShape) {
3756
+ const opts = pricingOrOptions;
3757
+ if (!opts.maxPrice) {
3758
+ throw new Error(`route '${routeKey}': .paid({ dynamic: true }) requires maxPrice`);
2770
3759
  }
2771
- wallets.add(normalized);
3760
+ return { pricing: opts.maxPrice, resolvedOptions: opts };
2772
3761
  }
2773
- };
2774
- function detectRedisClientType2(client) {
2775
- if (!client || typeof client !== "object") {
2776
- throw new Error(
2777
- "createRedisEntitlementStore requires a Redis client. Supported: @upstash/redis, ioredis."
2778
- );
2779
- }
2780
- if ("options" in client && "status" in client) return "ioredis";
2781
- const constructor = client.constructor?.name;
2782
- if (constructor === "Redis" && "url" in client) return "upstash";
2783
- if (typeof client.sadd === "function" && typeof client.sismember === "function") {
2784
- return "upstash";
2785
- }
2786
- throw new Error("Unrecognized Redis client for entitlement store.");
2787
- }
2788
- function createRedisEntitlementStore(client, options) {
2789
- const clientType = detectRedisClientType2(client);
2790
- const prefix = options?.prefix ?? "siwx:entitlement:";
2791
- return {
2792
- async has(route, wallet) {
2793
- const key = `${prefix}${route}`;
2794
- const normalized = normalizeWalletAddress(wallet);
2795
- if (clientType === "upstash") {
2796
- const redis2 = client;
2797
- const result2 = await redis2.sismember(key, normalized);
2798
- return result2 === 1 || result2 === true;
2799
- }
2800
- const redis = client;
2801
- const result = await redis.sismember(key, normalized);
2802
- return result === 1;
2803
- },
2804
- async grant(route, wallet) {
2805
- const key = `${prefix}${route}`;
2806
- const normalized = normalizeWalletAddress(wallet);
2807
- if (clientType === "upstash") {
2808
- const redis2 = client;
2809
- await redis2.sadd(key, normalized);
2810
- return;
2811
- }
2812
- const redis = client;
2813
- await redis.sadd(key, normalized);
2814
- }
2815
- };
3762
+ return { pricing: pricingOrOptions, resolvedOptions: options };
2816
3763
  }
2817
3764
 
2818
3765
  // src/discovery/well-known.ts
2819
- var import_server6 = require("next/server");
3766
+ var import_server8 = require("next/server");
2820
3767
 
2821
3768
  // src/discovery/utils/guidance.ts
2822
3769
  async function resolveGuidance(discovery) {
@@ -2860,7 +3807,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
2860
3807
  if (instructions) {
2861
3808
  body.instructions = instructions;
2862
3809
  }
2863
- return import_server6.NextResponse.json(body, {
3810
+ return import_server8.NextResponse.json(body, {
2864
3811
  headers: {
2865
3812
  "Access-Control-Allow-Origin": "*",
2866
3813
  "Access-Control-Allow-Methods": "GET",
@@ -2877,14 +3824,14 @@ function toDiscoveryResource(method, url, mode) {
2877
3824
  }
2878
3825
 
2879
3826
  // src/discovery/openapi.ts
2880
- var import_server7 = require("next/server");
3827
+ var import_server9 = require("next/server");
2881
3828
  init_constants();
2882
3829
  function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2883
3830
  const normalizedBase = baseUrl.replace(/\/+$/, "");
2884
3831
  let cached = null;
2885
3832
  let validated = false;
2886
3833
  return async (_request) => {
2887
- if (cached) return import_server7.NextResponse.json(cached);
3834
+ if (cached) return import_server9.NextResponse.json(cached);
2888
3835
  if (!validated && pricesKeys) {
2889
3836
  registry.validate(pricesKeys);
2890
3837
  validated = true;
@@ -2947,7 +3894,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2947
3894
  paths
2948
3895
  };
2949
3896
  cached = createDocument(openApiDocument);
2950
- return import_server7.NextResponse.json(cached);
3897
+ return import_server9.NextResponse.json(cached);
2951
3898
  };
2952
3899
  }
2953
3900
  function deriveTag(routeKey) {
@@ -3009,97 +3956,336 @@ function buildOperation(routeKey, entry, tag) {
3009
3956
  };
3010
3957
  }
3011
3958
  return {
3012
- operation,
3013
- requiresSiwxScheme,
3014
- requiresApiKeyScheme
3959
+ operation,
3960
+ requiresSiwxScheme,
3961
+ requiresApiKeyScheme
3962
+ };
3963
+ }
3964
+ function toProtocolObject(protocol, mppInfo) {
3965
+ if (protocol === "mpp") {
3966
+ return {
3967
+ mpp: {
3968
+ method: mppInfo?.method ?? "tempo",
3969
+ intent: mppInfo?.intent ?? "charge",
3970
+ currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
3971
+ }
3972
+ };
3973
+ }
3974
+ return { [protocol]: {} };
3975
+ }
3976
+ function buildPricingInfo(entry) {
3977
+ if (!entry.pricing) return void 0;
3978
+ if (typeof entry.pricing === "string") {
3979
+ return {
3980
+ price: { mode: "fixed", currency: "USD", amount: entry.pricing }
3981
+ };
3982
+ }
3983
+ if (typeof entry.pricing === "function") {
3984
+ return {
3985
+ price: {
3986
+ mode: "dynamic",
3987
+ currency: "USD",
3988
+ min: entry.minPrice ?? "0",
3989
+ max: entry.maxPrice ?? "0"
3990
+ }
3991
+ };
3992
+ }
3993
+ if ("tiers" in entry.pricing) {
3994
+ const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
3995
+ const min = Math.min(...tierPrices);
3996
+ const max = Math.max(...tierPrices);
3997
+ if (Number.isFinite(min) && Number.isFinite(max)) {
3998
+ if (min === max) {
3999
+ return {
4000
+ price: { mode: "fixed", currency: "USD", amount: String(min) }
4001
+ };
4002
+ }
4003
+ return {
4004
+ price: { mode: "dynamic", currency: "USD", min: String(min), max: String(max) }
4005
+ };
4006
+ }
4007
+ return {
4008
+ price: {
4009
+ mode: "dynamic",
4010
+ currency: "USD",
4011
+ min: "0",
4012
+ max: entry.maxPrice ?? "0"
4013
+ }
4014
+ };
4015
+ }
4016
+ return void 0;
4017
+ }
4018
+
4019
+ // src/discovery/llms-txt.ts
4020
+ var import_server10 = require("next/server");
4021
+ function createLlmsTxtHandler(discovery) {
4022
+ return async (_request) => {
4023
+ const guidance = await resolveGuidance(discovery) ?? "";
4024
+ return new import_server10.NextResponse(guidance, {
4025
+ headers: {
4026
+ "Content-Type": "text/plain; charset=utf-8",
4027
+ "Access-Control-Allow-Origin": "*"
4028
+ }
4029
+ });
4030
+ };
4031
+ }
4032
+
4033
+ // src/index.ts
4034
+ init_accepts();
4035
+ init_constants();
4036
+
4037
+ // src/config/error.ts
4038
+ var RouterConfigError = class extends Error {
4039
+ issues;
4040
+ constructor(issues) {
4041
+ super(formatRouterConfigIssues(issues));
4042
+ this.name = "RouterConfigError";
4043
+ this.issues = issues;
4044
+ }
4045
+ };
4046
+ function formatRouterConfigIssues(issues) {
4047
+ return issues.map((issue) => issue.message).join("\n");
4048
+ }
4049
+
4050
+ // src/config/validators/x402.ts
4051
+ init_accepts();
4052
+
4053
+ // src/config/validators/shared.ts
4054
+ init_evm();
4055
+ init_solana();
4056
+ init_accepts();
4057
+ function isEvmAddress(value) {
4058
+ return /^0x[a-fA-F0-9]{40}$/.test(value);
4059
+ }
4060
+ function isEvmPrivateKey(value) {
4061
+ return /^0x[a-fA-F0-9]{64}$/.test(value);
4062
+ }
4063
+ function isSupportedX402Network(network) {
4064
+ return isEvmNetwork(network) || isSolanaNetwork(network);
4065
+ }
4066
+ function findPlaceholderPayee(values) {
4067
+ return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
4068
+ }
4069
+ function usesDefaultEvmFacilitator(config) {
4070
+ return getConfiguredX402Networks(config).some(
4071
+ (network) => typeof network === "string" && isEvmNetwork(network)
4072
+ ) && config.x402?.facilitators?.evm === void 0;
4073
+ }
4074
+
4075
+ // src/config/validators/x402.ts
4076
+ var CHECKS = [
4077
+ checkAcceptNetworkPresent,
4078
+ checkAcceptNetworkSupported,
4079
+ checkNonExactRequiresAsset,
4080
+ checkDecimalsAreValid,
4081
+ checkPayee,
4082
+ checkPlaceholderPayeeAddress,
4083
+ checkCdpKeys
4084
+ ];
4085
+ function validateX402Config(config, env, options) {
4086
+ const accepts = getConfiguredX402Accepts(config);
4087
+ if (accepts.length === 0) {
4088
+ return [
4089
+ {
4090
+ code: "missing_x402_accepts",
4091
+ protocol: "x402",
4092
+ message: "x402 requires at least one accept configuration."
4093
+ }
4094
+ ];
4095
+ }
4096
+ const args = { config, accepts, env, options };
4097
+ return CHECKS.map((check) => check(args)).filter(
4098
+ (issue) => issue !== null
4099
+ );
4100
+ }
4101
+ function checkAcceptNetworkPresent({ accepts }) {
4102
+ return accepts.some((accept) => !accept.network) ? { code: "missing_x402_network", protocol: "x402", message: "x402 accepts require a network." } : null;
4103
+ }
4104
+ function checkAcceptNetworkSupported({ accepts }) {
4105
+ const unsupported = accepts.find(
4106
+ (accept) => accept.network && !isSupportedX402Network(accept.network)
4107
+ );
4108
+ if (!unsupported) return null;
4109
+ return {
4110
+ code: "unsupported_x402_network",
4111
+ protocol: "x402",
4112
+ message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
4113
+ };
4114
+ }
4115
+ function checkNonExactRequiresAsset({ accepts }) {
4116
+ return accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset) ? {
4117
+ code: "missing_x402_asset",
4118
+ protocol: "x402",
4119
+ message: "non-exact x402 accepts require an asset."
4120
+ } : null;
4121
+ }
4122
+ function checkDecimalsAreValid({ accepts }) {
4123
+ const invalid = accepts.find(
4124
+ (accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
4125
+ );
4126
+ if (!invalid) return null;
4127
+ return {
4128
+ code: "invalid_x402_decimals",
4129
+ protocol: "x402",
4130
+ message: "x402 accept decimals must be a non-negative integer."
4131
+ };
4132
+ }
4133
+ function checkPayee({ config, accepts }) {
4134
+ if (config.payeeAddress) return null;
4135
+ return accepts.some((accept) => !accept.payTo) ? {
4136
+ code: "missing_x402_payee",
4137
+ protocol: "x402",
4138
+ message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
4139
+ } : null;
4140
+ }
4141
+ function checkPlaceholderPayeeAddress({
4142
+ config,
4143
+ accepts
4144
+ }) {
4145
+ const placeholder = findPlaceholderPayee([
4146
+ config.payeeAddress,
4147
+ ...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
4148
+ ]);
4149
+ if (!placeholder) return null;
4150
+ return {
4151
+ code: "placeholder_payee",
4152
+ protocol: "x402",
4153
+ message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
4154
+ };
4155
+ }
4156
+ function checkCdpKeys({ config, env, options }) {
4157
+ if (options.requireCdpKeys === false) return null;
4158
+ if (!usesDefaultEvmFacilitator(config)) return null;
4159
+ const missing = [
4160
+ env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
4161
+ env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
4162
+ ].filter(Boolean);
4163
+ if (missing.length === 0) return null;
4164
+ return {
4165
+ code: "missing_cdp_keys",
4166
+ protocol: "x402",
4167
+ message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
4168
+ };
4169
+ }
4170
+
4171
+ // src/config/validators/mpp.ts
4172
+ var import_accounts = require("viem/accounts");
4173
+ var CHECKS2 = [
4174
+ checkSecretKey,
4175
+ checkCurrency,
4176
+ checkRecipient,
4177
+ checkPlaceholderRecipient,
4178
+ checkRpcUrl,
4179
+ checkFeePayerKey,
4180
+ checkOperatorKey,
4181
+ checkOperatorMatchesFeePayer
4182
+ ];
4183
+ function validateMppConfig(config, env) {
4184
+ const mpp = config.mpp;
4185
+ if (!mpp) {
4186
+ return [
4187
+ {
4188
+ code: "missing_mpp_config",
4189
+ protocol: "mpp",
4190
+ message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
4191
+ }
4192
+ ];
4193
+ }
4194
+ const args = { config, mpp, env };
4195
+ return CHECKS2.map((check) => check(args)).filter(
4196
+ (issue) => issue !== null
4197
+ );
4198
+ }
4199
+ function checkSecretKey({ mpp }) {
4200
+ if (mpp.secretKey) return null;
4201
+ return {
4202
+ code: "missing_mpp_secret_key",
4203
+ protocol: "mpp",
4204
+ message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
3015
4205
  };
3016
4206
  }
3017
- function toProtocolObject(protocol, mppInfo) {
3018
- if (protocol === "mpp") {
4207
+ function checkCurrency({ mpp }) {
4208
+ if (!mpp.currency) {
3019
4209
  return {
3020
- mpp: {
3021
- method: mppInfo?.method ?? "tempo",
3022
- intent: mppInfo?.intent ?? "charge",
3023
- currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
3024
- }
4210
+ code: "missing_mpp_currency",
4211
+ protocol: "mpp",
4212
+ message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
3025
4213
  };
3026
4214
  }
3027
- return { [protocol]: {} };
3028
- }
3029
- function buildPricingInfo(entry) {
3030
- if (!entry.pricing) return void 0;
3031
- if (typeof entry.pricing === "string") {
4215
+ if (!isEvmAddress(mpp.currency)) {
3032
4216
  return {
3033
- price: { mode: "fixed", currency: "USD", amount: entry.pricing }
4217
+ code: "invalid_mpp_currency",
4218
+ protocol: "mpp",
4219
+ message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
3034
4220
  };
3035
4221
  }
3036
- if (typeof entry.pricing === "function") {
4222
+ return null;
4223
+ }
4224
+ function checkRecipient({ config, mpp }) {
4225
+ const recipient = mpp.recipient ?? config.payeeAddress;
4226
+ if (!recipient) {
3037
4227
  return {
3038
- price: {
3039
- mode: "dynamic",
3040
- currency: "USD",
3041
- min: entry.minPrice ?? "0",
3042
- max: entry.maxPrice ?? "0"
3043
- }
4228
+ code: "missing_mpp_recipient",
4229
+ protocol: "mpp",
4230
+ message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
3044
4231
  };
3045
4232
  }
3046
- if ("tiers" in entry.pricing) {
3047
- const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
3048
- const min = Math.min(...tierPrices);
3049
- const max = Math.max(...tierPrices);
3050
- if (Number.isFinite(min) && Number.isFinite(max)) {
3051
- if (min === max) {
3052
- return {
3053
- price: { mode: "fixed", currency: "USD", amount: String(min) }
3054
- };
3055
- }
3056
- return {
3057
- price: { mode: "dynamic", currency: "USD", min: String(min), max: String(max) }
3058
- };
3059
- }
4233
+ if (!isEvmAddress(recipient)) {
3060
4234
  return {
3061
- price: {
3062
- mode: "dynamic",
3063
- currency: "USD",
3064
- min: "0",
3065
- max: entry.maxPrice ?? "0"
3066
- }
4235
+ code: "invalid_mpp_recipient",
4236
+ protocol: "mpp",
4237
+ message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
3067
4238
  };
3068
4239
  }
3069
- return void 0;
4240
+ return null;
3070
4241
  }
3071
-
3072
- // src/discovery/llms-txt.ts
3073
- var import_server8 = require("next/server");
3074
- function createLlmsTxtHandler(discovery) {
3075
- return async (_request) => {
3076
- const guidance = await resolveGuidance(discovery) ?? "";
3077
- return new import_server8.NextResponse(guidance, {
3078
- headers: {
3079
- "Content-Type": "text/plain; charset=utf-8",
3080
- "Access-Control-Allow-Origin": "*"
3081
- }
3082
- });
4242
+ function checkPlaceholderRecipient({ config, mpp }) {
4243
+ const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
4244
+ if (!placeholder) return null;
4245
+ return {
4246
+ code: "placeholder_payee",
4247
+ protocol: "mpp",
4248
+ message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
4249
+ };
4250
+ }
4251
+ function checkRpcUrl({ mpp, env }) {
4252
+ if (mpp.rpcUrl ?? env.TEMPO_RPC_URL) return null;
4253
+ return {
4254
+ code: "missing_mpp_rpc_url",
4255
+ protocol: "mpp",
4256
+ message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
4257
+ };
4258
+ }
4259
+ function checkFeePayerKey({ mpp }) {
4260
+ if (!mpp.feePayerKey || isEvmPrivateKey(mpp.feePayerKey)) return null;
4261
+ return {
4262
+ code: "invalid_mpp_fee_payer_key",
4263
+ protocol: "mpp",
4264
+ message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
4265
+ };
4266
+ }
4267
+ function checkOperatorKey({ mpp }) {
4268
+ if (!mpp.operatorKey || isEvmPrivateKey(mpp.operatorKey)) return null;
4269
+ return {
4270
+ code: "invalid_mpp_operator_key",
4271
+ protocol: "mpp",
4272
+ message: "MPP operatorKey must be a 0x-prefixed 32-byte EVM private key."
4273
+ };
4274
+ }
4275
+ function checkOperatorMatchesFeePayer({ mpp }) {
4276
+ if (!mpp.operatorKey || !mpp.feePayerKey) return null;
4277
+ if (!isEvmPrivateKey(mpp.operatorKey) || !isEvmPrivateKey(mpp.feePayerKey)) return null;
4278
+ const opAddr = (0, import_accounts.privateKeyToAccount)(mpp.operatorKey).address.toLowerCase();
4279
+ const fpAddr = (0, import_accounts.privateKeyToAccount)(mpp.feePayerKey).address.toLowerCase();
4280
+ if (opAddr !== fpAddr) return null;
4281
+ return {
4282
+ code: "mpp_operator_equals_fee_payer",
4283
+ protocol: "mpp",
4284
+ message: `MPP operatorKey and feePayerKey resolve to the same address (${opAddr}). Tempo rejects fee-delegated txs with sender === feePayer, so channel close/settle would fail at runtime. Either use two distinct wallets, or omit feePayerKey to disable gas sponsorship (clients then pay their own gas).`
3083
4285
  };
3084
4286
  }
3085
4287
 
3086
- // src/index.ts
3087
- init_accepts();
3088
- init_constants();
3089
-
3090
- // src/config.ts
3091
- init_constants();
3092
- init_evm();
3093
- init_solana();
3094
- init_accepts();
3095
- var RouterConfigError = class extends Error {
3096
- issues;
3097
- constructor(issues) {
3098
- super(formatRouterConfigIssues(issues));
3099
- this.name = "RouterConfigError";
3100
- this.issues = issues;
3101
- }
3102
- };
4288
+ // src/config/validate.ts
3103
4289
  function validateRouterConfig(config, options = {}) {
3104
4290
  const issues = getRouterConfigIssues(config, options);
3105
4291
  if (issues.length > 0) throw new RouterConfigError(issues);
@@ -3128,9 +4314,9 @@ function getRouterConfigIssues(config, options = {}) {
3128
4314
  }
3129
4315
  return issues;
3130
4316
  }
3131
- function formatRouterConfigIssues(issues) {
3132
- return issues.map((issue) => issue.message).join("\n");
3133
- }
4317
+
4318
+ // src/config/env.ts
4319
+ init_constants();
3134
4320
  function mppFromEnv(env, options = {}) {
3135
4321
  const secretKey = env.MPP_SECRET_KEY;
3136
4322
  const currency = env.MPP_CURRENCY;
@@ -3161,8 +4347,7 @@ function mppFromEnv(env, options = {}) {
3161
4347
  currency,
3162
4348
  rpcUrl,
3163
4349
  ...options.recipient ? { recipient: options.recipient } : {},
3164
- ...feePayerKey ? { feePayerKey } : {},
3165
- ...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
4350
+ ...feePayerKey ? { feePayerKey } : {}
3166
4351
  };
3167
4352
  }
3168
4353
  function x402AcceptsFromEnv(env, options = {}) {
@@ -3192,189 +4377,145 @@ function x402AcceptsFromEnv(env, options = {}) {
3192
4377
  function paidOptionsForProtocols(protocols) {
3193
4378
  return { protocols: [...protocols] };
3194
4379
  }
3195
- function validateX402Config(config, env, options) {
3196
- const issues = [];
3197
- const accepts = getConfiguredX402Accepts(config);
3198
- if (accepts.length === 0) {
3199
- issues.push({
3200
- code: "missing_x402_accepts",
3201
- protocol: "x402",
3202
- message: "x402 requires at least one accept configuration."
3203
- });
3204
- return issues;
3205
- }
3206
- const acceptWithoutNetwork = accepts.find((accept) => !accept.network);
3207
- if (acceptWithoutNetwork) {
3208
- issues.push({
3209
- code: "missing_x402_network",
3210
- protocol: "x402",
3211
- message: "x402 accepts require a network."
3212
- });
3213
- }
3214
- const unsupported = accepts.find(
3215
- (accept) => accept.network && !isSupportedX402Network(accept.network)
3216
- );
3217
- if (unsupported) {
3218
- issues.push({
3219
- code: "unsupported_x402_network",
3220
- protocol: "x402",
3221
- message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
3222
- });
3223
- }
3224
- const missingAsset = accepts.find(
3225
- (accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset
3226
- );
3227
- if (missingAsset) {
3228
- issues.push({
3229
- code: "missing_x402_asset",
3230
- protocol: "x402",
3231
- message: "non-exact x402 accepts require an asset."
3232
- });
3233
- }
3234
- const invalidDecimals = accepts.find(
3235
- (accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
3236
- );
3237
- if (invalidDecimals) {
3238
- issues.push({
3239
- code: "invalid_x402_decimals",
3240
- protocol: "x402",
3241
- message: "x402 accept decimals must be a non-negative integer."
3242
- });
3243
- }
3244
- if (accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
3245
- issues.push({
3246
- code: "missing_x402_payee",
3247
- protocol: "x402",
3248
- message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
3249
- });
3250
- }
3251
- const placeholder = findPlaceholderPayee([
3252
- config.payeeAddress,
3253
- ...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
3254
- ]);
3255
- if (placeholder) {
3256
- issues.push({
3257
- code: "placeholder_payee",
3258
- protocol: "x402",
3259
- message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
3260
- });
4380
+
4381
+ // src/init/x402.ts
4382
+ async function initX402(config, configError) {
4383
+ if (configError) return { initError: configError };
4384
+ try {
4385
+ const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4386
+ const result = await createX402Server2(config);
4387
+ await result.initPromise;
4388
+ return {
4389
+ server: result.server,
4390
+ facilitatorsByNetwork: result.facilitatorsByNetwork
4391
+ };
4392
+ } catch (err) {
4393
+ return { initError: err instanceof Error ? err.message : String(err) };
3261
4394
  }
3262
- if (options.requireCdpKeys !== false && usesDefaultEvmFacilitator(config)) {
3263
- const missing = [
3264
- env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
3265
- env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
3266
- ].filter(Boolean);
3267
- if (missing.length > 0) {
3268
- issues.push({
3269
- code: "missing_cdp_keys",
3270
- protocol: "x402",
3271
- message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
3272
- });
4395
+ }
4396
+
4397
+ // src/mppx-init.ts
4398
+ function getMppxRequestContext(args) {
4399
+ const {
4400
+ Mppx,
4401
+ tempo,
4402
+ mppConfig,
4403
+ payeeAddress,
4404
+ getClient,
4405
+ feePayerAccount,
4406
+ resolvedStore,
4407
+ sessionEnabled,
4408
+ sharedSessionParams,
4409
+ realm
4410
+ } = args;
4411
+ const instance = Mppx.create({
4412
+ methods: [
4413
+ tempo.charge({
4414
+ currency: mppConfig.currency,
4415
+ recipient: mppConfig.recipient ?? payeeAddress,
4416
+ getClient,
4417
+ ...feePayerAccount ? { feePayer: feePayerAccount } : {},
4418
+ ...resolvedStore ? { store: resolvedStore } : {}
4419
+ }),
4420
+ ...sessionEnabled ? [
4421
+ tempo.session({
4422
+ ...sharedSessionParams,
4423
+ sse: false
4424
+ })
4425
+ ] : []
4426
+ ],
4427
+ secretKey: mppConfig.secretKey,
4428
+ realm
4429
+ });
4430
+ return instance;
4431
+ }
4432
+ function getMppxStreamingContext(args) {
4433
+ if (!args.sessionEnabled) return null;
4434
+ const { Mppx, tempo, mppConfig, sharedSessionParams, realm } = args;
4435
+ const instance = Mppx.create({
4436
+ methods: [
4437
+ tempo.session({
4438
+ ...sharedSessionParams,
4439
+ sse: true
4440
+ })
4441
+ ],
4442
+ secretKey: mppConfig.secretKey,
4443
+ realm
4444
+ });
4445
+ return instance;
4446
+ }
4447
+
4448
+ // src/init/mpp.ts
4449
+ async function initMpp(config, resolvedBaseUrl, kvStore, configError) {
4450
+ if (configError) return { initError: configError };
4451
+ if (!config.mpp) return {};
4452
+ try {
4453
+ const { Mppx, tempo } = await import("mppx/server");
4454
+ const { createClient, http } = await import("viem");
4455
+ const { tempo: tempoChain } = await import("viem/chains");
4456
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
4457
+ const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
4458
+ const tempoClient = createClient({ chain: tempoChain, transport: http(rpcUrl) });
4459
+ const getClient = async () => tempoClient;
4460
+ const operatorAccount = config.mpp.operatorKey ? privateKeyToAccount2(config.mpp.operatorKey) : void 0;
4461
+ const feePayerAccount = config.mpp.feePayerKey ? privateKeyToAccount2(config.mpp.feePayerKey) : void 0;
4462
+ if (config.mpp.session && operatorAccount) {
4463
+ assertOperatorMatchesRecipient(config, operatorAccount.address);
3273
4464
  }
4465
+ const resolvedStore = kvStore ? await createKvMppStore(kvStore) : void 0;
4466
+ const realm = new URL(resolvedBaseUrl).host;
4467
+ const mppConfig = config.mpp;
4468
+ const sessionEnabled = !!(mppConfig.session && operatorAccount);
4469
+ const sharedSessionParams = {
4470
+ currency: mppConfig.currency,
4471
+ decimals: 6,
4472
+ recipient: mppConfig.recipient ?? config.payeeAddress,
4473
+ getClient,
4474
+ ...operatorAccount ? { account: operatorAccount } : {},
4475
+ ...feePayerAccount ? { feePayer: feePayerAccount } : {},
4476
+ ...resolvedStore ? { store: resolvedStore } : {}
4477
+ };
4478
+ const mppxArgs = {
4479
+ Mppx,
4480
+ tempo,
4481
+ mppConfig,
4482
+ payeeAddress: config.payeeAddress ?? "",
4483
+ getClient,
4484
+ feePayerAccount,
4485
+ resolvedStore,
4486
+ sessionEnabled,
4487
+ sharedSessionParams,
4488
+ realm
4489
+ };
4490
+ const primary = getMppxRequestContext(mppxArgs);
4491
+ const streaming = getMppxStreamingContext(mppxArgs);
4492
+ const mppx = {
4493
+ charge: primary.charge,
4494
+ ...primary.session ? { sessionRequest: primary.session } : {},
4495
+ ...streaming?.session ? { sessionStream: streaming.session } : {}
4496
+ };
4497
+ return { mppx, tempoClient };
4498
+ } catch (err) {
4499
+ return { initError: err instanceof Error ? err.message : String(err) };
3274
4500
  }
3275
- return issues;
3276
4501
  }
3277
- function validateMppConfig(config, env) {
3278
- const issues = [];
3279
- const mpp = config.mpp;
3280
- if (!mpp) {
3281
- return [
3282
- {
3283
- code: "missing_mpp_config",
3284
- protocol: "mpp",
3285
- message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
3286
- }
3287
- ];
3288
- }
3289
- if (!mpp.secretKey) {
3290
- issues.push({
3291
- code: "missing_mpp_secret_key",
3292
- protocol: "mpp",
3293
- message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
3294
- });
3295
- }
3296
- if (!mpp.currency) {
3297
- issues.push({
3298
- code: "missing_mpp_currency",
3299
- protocol: "mpp",
3300
- message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
3301
- });
3302
- } else if (!isEvmAddress(mpp.currency)) {
3303
- issues.push({
3304
- code: "invalid_mpp_currency",
3305
- protocol: "mpp",
3306
- message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
3307
- });
3308
- }
3309
- const mppRecipient = mpp.recipient ?? config.payeeAddress;
3310
- if (!mppRecipient) {
3311
- issues.push({
3312
- code: "missing_mpp_recipient",
3313
- protocol: "mpp",
3314
- message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
3315
- });
3316
- } else if (!isEvmAddress(mppRecipient)) {
3317
- issues.push({
3318
- code: "invalid_mpp_recipient",
3319
- protocol: "mpp",
3320
- message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
3321
- });
3322
- }
3323
- const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
3324
- if (placeholder) {
3325
- issues.push({
3326
- code: "placeholder_payee",
3327
- protocol: "mpp",
3328
- message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
3329
- });
3330
- }
3331
- if (!(mpp.rpcUrl ?? env.TEMPO_RPC_URL)) {
3332
- issues.push({
3333
- code: "missing_mpp_rpc_url",
3334
- protocol: "mpp",
3335
- message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
3336
- });
3337
- }
3338
- if (mpp.feePayerKey && !isEvmPrivateKey(mpp.feePayerKey)) {
3339
- issues.push({
3340
- code: "invalid_mpp_fee_payer_key",
3341
- protocol: "mpp",
3342
- message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
3343
- });
3344
- }
3345
- if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
3346
- issues.push({
3347
- code: "missing_mpp_default_store_env",
3348
- protocol: "mpp",
3349
- message: "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
3350
- });
4502
+ function assertOperatorMatchesRecipient(config, operatorAddress) {
4503
+ const recipient = (config.mpp?.recipient ?? config.payeeAddress)?.toLowerCase();
4504
+ const opAddr = operatorAddress.toLowerCase();
4505
+ if (recipient && opAddr !== recipient) {
4506
+ throw new Error(
4507
+ `MPP session config mismatch: operator address ${operatorAddress} must equal recipient/payee ${recipient}. mppx's channel-close handler asserts sender === payee. Set mpp.operatorKey to the private key for ${recipient}, or set mpp.recipient/payeeAddress to ${operatorAddress}.`
4508
+ );
3351
4509
  }
3352
- return issues;
3353
- }
3354
- function usesDefaultEvmFacilitator(config) {
3355
- return getConfiguredX402Networks(config).some(
3356
- (network) => typeof network === "string" && isEvmNetwork(network)
3357
- ) && config.x402?.facilitators?.evm === void 0;
3358
- }
3359
- function isSupportedX402Network(network) {
3360
- return isEvmNetwork(network) || isSolanaNetwork(network);
3361
- }
3362
- function isEvmAddress(value) {
3363
- return /^0x[a-fA-F0-9]{40}$/.test(value);
3364
- }
3365
- function isEvmPrivateKey(value) {
3366
- return /^0x[a-fA-F0-9]{64}$/.test(value);
3367
- }
3368
- function findPlaceholderPayee(values) {
3369
- return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
3370
4510
  }
3371
4511
 
3372
4512
  // src/index.ts
3373
4513
  init_constants();
3374
4514
  function createRouter(config) {
3375
4515
  const registry = new RouteRegistry();
3376
- const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
3377
- const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
4516
+ const kvStore = resolveKvStore(config.kvStore);
4517
+ const nonceStore = kvStore ? createKvNonceStore(kvStore) : new MemoryNonceStore();
4518
+ const entitlementStore = kvStore ? createKvEntitlementStore(kvStore) : new MemoryEntitlementStore();
3378
4519
  const network = config.network ?? BASE_NETWORK;
3379
4520
  const x402Accepts = getConfiguredX402Accepts(config);
3380
4521
  const configIssues = getRouterConfigIssues(config, {
@@ -3420,69 +4561,20 @@ function createRouter(config) {
3420
4561
  x402FacilitatorsByNetwork: void 0,
3421
4562
  x402Accepts,
3422
4563
  mppx: null,
3423
- tempoClient: null
4564
+ tempoClient: null,
4565
+ mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
3424
4566
  };
3425
4567
  deps.initPromise = (async () => {
3426
- if (x402ConfigError) {
3427
- deps.x402InitError = x402ConfigError;
3428
- } else {
3429
- try {
3430
- const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
3431
- const result = await createX402Server2(config);
3432
- deps.x402Server = result.server;
3433
- deps.x402FacilitatorsByNetwork = result.facilitatorsByNetwork;
3434
- await result.initPromise;
3435
- } catch (err) {
3436
- deps.x402Server = null;
3437
- deps.x402InitError = err instanceof Error ? err.message : String(err);
3438
- }
3439
- }
3440
- if (mppConfigError) {
3441
- deps.mppInitError = mppConfigError;
3442
- } else if (config.mpp) {
3443
- try {
3444
- const { Mppx, tempo } = await import("mppx/server");
3445
- const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
3446
- const { createClient, http } = await import("viem");
3447
- const { tempo: tempoChain } = await import("viem/chains");
3448
- deps.tempoClient = createClient({ chain: tempoChain, transport: http(rpcUrl) });
3449
- const getClient = async () => deps.tempoClient;
3450
- let feePayerAccount;
3451
- if (config.mpp.feePayerKey) {
3452
- const { privateKeyToAccount } = await import("viem/accounts");
3453
- feePayerAccount = privateKeyToAccount(config.mpp.feePayerKey);
3454
- }
3455
- let resolvedStore = config.mpp.store;
3456
- if (!resolvedStore && config.mpp.useDefaultStore) {
3457
- const kvUrl = process.env.KV_REST_API_URL;
3458
- const kvToken = process.env.KV_REST_API_TOKEN;
3459
- if (!kvUrl || !kvToken) {
3460
- throw new Error(
3461
- "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
3462
- );
3463
- }
3464
- const { Store } = await import("mppx");
3465
- const { createUpstashRest: createUpstashRest2 } = await Promise.resolve().then(() => (init_upstash_rest(), upstash_rest_exports));
3466
- resolvedStore = Store.upstash(createUpstashRest2(kvUrl, kvToken));
3467
- }
3468
- deps.mppx = Mppx.create({
3469
- methods: [
3470
- tempo.charge({
3471
- currency: config.mpp.currency,
3472
- recipient: config.mpp.recipient ?? config.payeeAddress,
3473
- getClient,
3474
- ...feePayerAccount ? { feePayer: feePayerAccount } : {},
3475
- ...resolvedStore ? { store: resolvedStore } : {}
3476
- })
3477
- ],
3478
- secretKey: config.mpp.secretKey,
3479
- realm: new URL(resolvedBaseUrl).host
3480
- });
3481
- } catch (err) {
3482
- deps.mppx = null;
3483
- deps.mppInitError = err instanceof Error ? err.message : String(err);
3484
- console.error(`[router] MPP initialization failed: ${deps.mppInitError}`);
3485
- }
4568
+ const x402Result = await initX402(config, x402ConfigError);
4569
+ deps.x402Server = x402Result.server ?? null;
4570
+ deps.x402FacilitatorsByNetwork = x402Result.facilitatorsByNetwork;
4571
+ if (x402Result.initError) deps.x402InitError = x402Result.initError;
4572
+ const mppResult = await initMpp(config, resolvedBaseUrl, kvStore, mppConfigError);
4573
+ deps.mppx = mppResult.mppx ?? null;
4574
+ deps.tempoClient = mppResult.tempoClient ?? null;
4575
+ if (mppResult.initError) {
4576
+ deps.mppInitError = mppResult.initError;
4577
+ console.error(`[router] MPP initialization failed: ${mppResult.initError}`);
3486
4578
  }
3487
4579
  })();
3488
4580
  const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
@@ -3564,13 +4656,15 @@ function normalizePath(path) {
3564
4656
  TEMPO_USDC_CURRENCY,
3565
4657
  ZERO_EVM_ADDRESS,
3566
4658
  consolePlugin,
3567
- createRedisEntitlementStore,
3568
- createRedisNonceStore,
4659
+ createKvEntitlementStore,
4660
+ createKvMppStore,
4661
+ createKvNonceStore,
3569
4662
  createRouter,
3570
4663
  formatRouterConfigIssues,
3571
4664
  getRouterConfigIssues,
3572
4665
  mppFromEnv,
3573
4666
  paidOptionsForProtocols,
3574
4667
  validateRouterConfig,
4668
+ withPrefix,
3575
4669
  x402AcceptsFromEnv
3576
4670
  });