@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.js CHANGED
@@ -83,6 +83,18 @@ function buildEvmExactOptions(accepts, price) {
83
83
  payTo
84
84
  }));
85
85
  }
86
+ function buildEvmUptoOptions(accepts, price) {
87
+ return accepts.filter(
88
+ (accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
89
+ ).map((accept) => ({
90
+ scheme: "upto",
91
+ network: accept.network,
92
+ payTo: accept.payTo,
93
+ price,
94
+ ...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
95
+ ...accept.extra ? { extra: accept.extra } : {}
96
+ }));
97
+ }
86
98
  var init_evm = __esm({
87
99
  "src/protocols/x402/evm.ts"() {
88
100
  "use strict";
@@ -267,40 +279,40 @@ async function createX402Server(config) {
267
279
  facilitatorsByNetwork
268
280
  };
269
281
  }
270
- function cachedClient(inner, kinds) {
282
+ function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
283
+ const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
284
+ return groups.map((group) => {
285
+ const inner = new HTTPFacilitatorClient(group.config);
286
+ const kinds = buildSupportedKinds(group);
287
+ return hardcodedSupportedClient(inner, kinds);
288
+ });
289
+ }
290
+ function hardcodedSupportedClient(inner, kinds) {
271
291
  return {
272
292
  verify: inner.verify.bind(inner),
273
293
  settle: inner.settle.bind(inner),
274
- getSupported: async () => ({
275
- kinds,
276
- extensions: [],
277
- signers: {}
278
- })
294
+ getSupported: async () => ({ kinds, extensions: [], signers: {} })
279
295
  };
280
296
  }
281
- function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
282
- const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
283
- return groups.map((group) => {
284
- const inner = new HTTPFacilitatorClient(group.config);
285
- const kinds = group.networks.flatMap((network) => {
286
- const exactKind = {
287
- x402Version: 2,
288
- scheme: "exact",
289
- network,
290
- ...group.family === "solana" ? {
291
- extra: {
292
- features: {
293
- xSettlementAccountSupported: true
294
- }
297
+ function buildSupportedKinds(group) {
298
+ return group.networks.flatMap((network) => {
299
+ const exactKind = {
300
+ x402Version: 2,
301
+ scheme: "exact",
302
+ network,
303
+ ...group.family === "solana" ? {
304
+ extra: {
305
+ features: {
306
+ xSettlementAccountSupported: true
295
307
  }
296
- } : {}
297
- };
298
- if (group.family === "evm") {
299
- return [exactKind, { x402Version: 2, scheme: "upto", network }];
300
- }
301
- return [exactKind];
302
- });
303
- return cachedClient(inner, kinds);
308
+ }
309
+ } : {}
310
+ };
311
+ const uptoKind = { x402Version: 2, scheme: "upto", network };
312
+ if (group.family === "evm") {
313
+ return [exactKind, uptoKind];
314
+ }
315
+ return [exactKind, uptoKind];
304
316
  });
305
317
  }
306
318
  var init_server = __esm({
@@ -313,65 +325,9 @@ var init_server = __esm({
313
325
  }
314
326
  });
315
327
 
316
- // src/upstash-rest.ts
317
- var upstash_rest_exports = {};
318
- __export(upstash_rest_exports, {
319
- createUpstashRest: () => createUpstashRest
320
- });
321
- function createUpstashRest(url, token) {
322
- const base = url.replace(/\/+$/, "");
323
- const headers = { Authorization: `Bearer ${token}` };
324
- async function get(key) {
325
- const res = await fetch(`${base}/get/${key}`, { headers });
326
- if (!res.ok) throw new Error(`[upstash-rest] GET ${key}: ${res.status}`);
327
- const { result } = await res.json();
328
- return result ?? null;
329
- }
330
- async function set(key, value) {
331
- const res = await fetch(`${base}`, {
332
- method: "POST",
333
- headers: { ...headers, "Content-Type": "application/json" },
334
- body: JSON.stringify(["SET", key, JSON.stringify(value)])
335
- });
336
- if (!res.ok) throw new Error(`[upstash-rest] SET ${key}: ${res.status}`);
337
- return await res.json();
338
- }
339
- async function del(key) {
340
- const res = await fetch(`${base}`, {
341
- method: "POST",
342
- headers: { ...headers, "Content-Type": "application/json" },
343
- body: JSON.stringify(["DEL", key])
344
- });
345
- if (!res.ok) throw new Error(`[upstash-rest] DEL ${key}: ${res.status}`);
346
- return await res.json();
347
- }
348
- return {
349
- get,
350
- set,
351
- del,
352
- async update(key, fn) {
353
- const current = await get(key);
354
- const change = fn(current);
355
- if (change.op === "set") await set(key, change.value);
356
- if (change.op === "delete") await del(key);
357
- return change.result;
358
- }
359
- };
360
- }
361
- var init_upstash_rest = __esm({
362
- "src/upstash-rest.ts"() {
363
- "use strict";
364
- }
365
- });
366
-
367
328
  // src/registry.ts
368
329
  var RouteRegistry = class {
369
330
  routes = /* @__PURE__ */ new Map();
370
- // Internal map key includes the HTTP method so that POST and DELETE on the
371
- // same path coexist. Within the same path+method, last-write-wins is still
372
- // intentional — Next.js module loading order is non-deterministic during
373
- // build and discovery stubs may register the same route in either order.
374
- // Prior art: ElysiaJS uses the same pattern (silent overwrite in router.history).
375
331
  mapKey(entry) {
376
332
  return `${entry.key}:${entry.method}`;
377
333
  }
@@ -384,8 +340,6 @@ var RouteRegistry = class {
384
340
  }
385
341
  this.routes.set(k, entry);
386
342
  }
387
- // Accepts either a compound key ("site/domain:DELETE") or a path-only key
388
- // ("site/domain") — path-only returns the first registered method for that path.
389
343
  get(key) {
390
344
  const direct = this.routes.get(key);
391
345
  if (direct) return direct;
@@ -417,24 +371,17 @@ var RouteRegistry = class {
417
371
 
418
372
  // src/headers.ts
419
373
  var HEADERS = {
420
- // ---- Standard HTTP ----
421
374
  AUTHORIZATION: "Authorization",
422
375
  WWW_AUTHENTICATE: "WWW-Authenticate",
423
- // ---- Auth ----
424
376
  API_KEY: "X-API-Key",
425
- // ---- Request meta (used by plugin/observability) ----
426
377
  WALLET_ADDRESS: "X-Wallet-Address",
427
378
  CLIENT_ID: "X-Client-ID",
428
379
  SESSION_ID: "X-Session-ID",
429
- // ---- SIWX ----
430
380
  SIWX: "SIGN-IN-WITH-X",
431
- // ---- x402 (payment) ----
432
381
  X402_PAYMENT_SIGNATURE: "PAYMENT-SIGNATURE",
433
- /** Legacy x402 payment header — accepted alongside PAYMENT-SIGNATURE. */
434
382
  X402_PAYMENT_LEGACY: "X-PAYMENT",
435
383
  X402_PAYMENT_REQUIRED: "PAYMENT-REQUIRED",
436
384
  X402_PAYMENT_RESPONSE: "PAYMENT-RESPONSE",
437
- // ---- MPP (payment) ----
438
385
  MPP_PAYMENT_RECEIPT: "Payment-Receipt"
439
386
  };
440
387
  var AUTH_SCHEME = {
@@ -517,11 +464,31 @@ function consolePlugin() {
517
464
  };
518
465
  }
519
466
 
467
+ // src/alert.ts
468
+ function createReporter(plugin, pluginCtx, route) {
469
+ return (level, message, meta) => {
470
+ firePluginHook(plugin, "onAlert", pluginCtx, {
471
+ level,
472
+ message,
473
+ route,
474
+ ...meta ? { meta } : {}
475
+ });
476
+ };
477
+ }
478
+
520
479
  // src/pipeline/context/preflight.ts
521
480
  function preflight(routeEntry, handler, deps, request) {
522
481
  const meta = buildMeta(request, routeEntry);
523
482
  const pluginCtx = firePluginHook(deps.plugin, "onRequest", meta) ?? createDefaultContext(meta);
524
- return { routeEntry, handler, deps, request, meta, pluginCtx };
483
+ return {
484
+ routeEntry,
485
+ handler,
486
+ deps,
487
+ request,
488
+ meta,
489
+ pluginCtx,
490
+ report: createReporter(deps.plugin, pluginCtx, routeEntry.key)
491
+ };
525
492
  }
526
493
  function buildMeta(request, routeEntry) {
527
494
  return {
@@ -566,19 +533,38 @@ function validateBody(parsed, schema) {
566
533
  };
567
534
  }
568
535
 
536
+ // src/pipeline/context/fire-plugin-response.ts
537
+ function firePluginResponse(ctx, response, requestBody, responseBody) {
538
+ firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
539
+ statusCode: response.status,
540
+ statusText: response.statusText,
541
+ duration: Date.now() - ctx.meta.startTime,
542
+ contentType: response.headers.get("content-type"),
543
+ headers: Object.fromEntries(response.headers.entries()),
544
+ requestBody,
545
+ responseBody
546
+ });
547
+ if (response.status >= 400 && response.status !== 402) {
548
+ firePluginHook(ctx.deps.plugin, "onError", ctx.pluginCtx, {
549
+ status: response.status,
550
+ message: response.statusText || `HTTP ${response.status}`,
551
+ settled: false
552
+ });
553
+ }
554
+ }
555
+
569
556
  // src/pipeline/context/parse-body.ts
570
- async function parseBody(request, routeEntry) {
571
- if (!routeEntry.bodySchema) return { ok: true, data: void 0 };
557
+ async function parseBody(ctx, request = ctx.request) {
558
+ if (!ctx.routeEntry.bodySchema) return { ok: true, data: void 0 };
572
559
  const raw = await bufferBody(request);
573
- const result = validateBody(raw, routeEntry.bodySchema);
560
+ const result = validateBody(raw, ctx.routeEntry.bodySchema);
574
561
  if (result.success) return { ok: true, data: result.data };
575
- return {
576
- ok: false,
577
- response: NextResponse.json(
578
- { success: false, error: result.error, issues: result.issues },
579
- { status: 400 }
580
- )
581
- };
562
+ const response = NextResponse.json(
563
+ { success: false, error: result.error, issues: result.issues },
564
+ { status: 400 }
565
+ );
566
+ firePluginResponse(ctx, response);
567
+ return { ok: false, response };
582
568
  }
583
569
 
584
570
  // src/pipeline/context/parse-query.ts
@@ -604,28 +590,6 @@ function handlerFailureError(response) {
604
590
 
605
591
  // src/pipeline/context/fail.ts
606
592
  import { NextResponse as NextResponse2 } from "next/server";
607
-
608
- // src/pipeline/context/fire-plugin-response.ts
609
- function firePluginResponse(ctx, response, requestBody, responseBody) {
610
- firePluginHook(ctx.deps.plugin, "onResponse", ctx.pluginCtx, {
611
- statusCode: response.status,
612
- statusText: response.statusText,
613
- duration: Date.now() - ctx.meta.startTime,
614
- contentType: response.headers.get("content-type"),
615
- headers: Object.fromEntries(response.headers.entries()),
616
- requestBody,
617
- responseBody
618
- });
619
- if (response.status >= 400 && response.status !== 402) {
620
- firePluginHook(ctx.deps.plugin, "onError", ctx.pluginCtx, {
621
- status: response.status,
622
- message: response.statusText || `HTTP ${response.status}`,
623
- settled: false
624
- });
625
- }
626
- }
627
-
628
- // src/pipeline/context/fail.ts
629
593
  function fail(ctx, status, message, requestBody) {
630
594
  const response = NextResponse2.json({ success: false, error: message }, { status });
631
595
  firePluginResponse(ctx, response, requestBody);
@@ -643,7 +607,7 @@ async function runValidate(ctx, body) {
643
607
  }
644
608
  }
645
609
 
646
- // src/handler.ts
610
+ // src/pipeline/flows/static/static-invoke.ts
647
611
  import { NextResponse as NextResponse3 } from "next/server";
648
612
 
649
613
  // src/types.ts
@@ -655,23 +619,15 @@ var HttpError = class extends Error {
655
619
  }
656
620
  };
657
621
 
658
- // src/handler.ts
659
- async function safeCallHandler(handler, ctx, options = {}) {
660
- try {
661
- const result = await handler(ctx);
662
- if (result instanceof Response) return result;
663
- return NextResponse3.json(result);
664
- } catch (error) {
665
- options.onError?.(error);
666
- const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
667
- const message = error instanceof Error ? error.message : "Internal error";
668
- return NextResponse3.json({ success: false, error: message }, { status });
669
- }
622
+ // src/pipeline/flows/static/static-invoke.ts
623
+ function invokePaidStatic(ctx, wallet, account, body, payment) {
624
+ return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
670
625
  }
671
-
672
- // src/pipeline/context/invoke.ts
673
- async function invoke(ctx, wallet, account, body, payment) {
674
- const handlerCtx = {
626
+ function invokeUnauthed(ctx, wallet, account, body) {
627
+ return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, null));
628
+ }
629
+ function buildHandlerCtx(ctx, wallet, account, body, payment) {
630
+ return {
675
631
  body,
676
632
  query: parseQuery(ctx.request, ctx.routeEntry),
677
633
  request: ctx.request,
@@ -690,21 +646,45 @@ async function invoke(ctx, wallet, account, body, payment) {
690
646
  },
691
647
  setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
692
648
  };
649
+ }
650
+ async function runHandler(ctx, handlerCtx) {
651
+ let returned;
652
+ try {
653
+ returned = ctx.handler(handlerCtx);
654
+ } catch (error) {
655
+ return errorResult(error);
656
+ }
657
+ if (isAsyncIterable(returned) && !isThenable(returned)) {
658
+ return errorResult(
659
+ new HttpError(
660
+ `route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
661
+ 500
662
+ )
663
+ );
664
+ }
693
665
  let rawResult;
694
- let handlerError;
695
- const response = await safeCallHandler(
696
- async (c) => {
697
- rawResult = await ctx.handler(c);
698
- return rawResult;
699
- },
700
- handlerCtx,
701
- {
702
- onError(error) {
703
- handlerError = error;
704
- }
705
- }
706
- );
707
- return { response, rawResult, handlerError };
666
+ try {
667
+ rawResult = await returned;
668
+ } catch (error) {
669
+ return errorResult(error);
670
+ }
671
+ const response = rawResult instanceof Response ? rawResult : NextResponse3.json(rawResult);
672
+ return { response, rawResult };
673
+ }
674
+ function errorResult(error) {
675
+ const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
676
+ const message = error instanceof Error ? error.message : "Internal error";
677
+ return {
678
+ response: NextResponse3.json({ success: false, error: message }, { status }),
679
+ rawResult: void 0,
680
+ handlerError: error
681
+ };
682
+ }
683
+ function isAsyncIterable(value) {
684
+ return value != null && typeof value === "object" && Symbol.asyncIterator in value;
685
+ }
686
+ function isThenable(value) {
687
+ return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
708
688
  }
709
689
 
710
690
  // src/pipeline/context/fire-provider-quota.ts
@@ -738,24 +718,24 @@ function computeQuotaLevel(remaining, warn, critical) {
738
718
  return "healthy";
739
719
  }
740
720
 
741
- // src/pipeline/context/finalize.ts
721
+ // src/pipeline/context/finalize/response.ts
742
722
  function finalize(ctx, response, rawResult, requestBody) {
743
723
  fireProviderQuota(ctx, response, rawResult);
744
724
  firePluginResponse(ctx, response, requestBody, rawResult);
745
725
  return response;
746
726
  }
747
727
 
748
- // src/pipeline/context/run-handler-only.ts
749
- async function runHandlerOnly(ctx, wallet, account) {
750
- const body = await parseBody(ctx.request, ctx.routeEntry);
751
- if (!body.ok) {
752
- firePluginResponse(ctx, body.response);
753
- return body.response;
728
+ // src/pipeline/context/grant-entitlement.ts
729
+ async function grantEntitlementIfSiwx(ctx, wallet) {
730
+ if (!ctx.routeEntry.siwxEnabled) return;
731
+ try {
732
+ await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
733
+ } catch (error) {
734
+ ctx.report(
735
+ "warn",
736
+ `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`
737
+ );
754
738
  }
755
- const validateErr = await runValidate(ctx, body.data);
756
- if (validateErr) return validateErr;
757
- const result = await invoke(ctx, wallet, account, body.data, null);
758
- return finalize(ctx, result.response, result.rawResult, body.data);
759
739
  }
760
740
 
761
741
  // src/pipeline/context/settlement-context.ts
@@ -772,23 +752,6 @@ function settlementContext(ctx, scope) {
772
752
  };
773
753
  }
774
754
 
775
- // src/pipeline/context/run-before-settle.ts
776
- async function runBeforeSettle(ctx, scope) {
777
- const hook = ctx.routeEntry.settlement?.beforeSettle;
778
- if (!hook) return null;
779
- try {
780
- await hook(settlementContext(ctx, scope));
781
- return null;
782
- } catch (error) {
783
- return fail(
784
- ctx,
785
- errorStatus(error, 500),
786
- errorMessage(error, "Pre-settlement validation failed"),
787
- scope.body
788
- );
789
- }
790
- }
791
-
792
755
  // src/pipeline/context/run-settlement-error.ts
793
756
  async function runSettlementError(ctx, scope, error, phase) {
794
757
  const hook = ctx.routeEntry.settlement?.onSettlementError;
@@ -797,12 +760,7 @@ async function runSettlementError(ctx, scope, error, phase) {
797
760
  await hook({ ...settlementContext(ctx, scope), error, phase });
798
761
  } catch (hookError) {
799
762
  const message = errorMessage(hookError, "Settlement error hook failed");
800
- console.error(`[router] ${ctx.routeEntry.key}: onSettlementError failed: ${message}`);
801
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
802
- level: "error",
803
- message: `Settlement error hook failed: ${message}`,
804
- route: ctx.routeEntry.key
805
- });
763
+ ctx.report("error", `Settlement error hook failed: ${message}`);
806
764
  }
807
765
  }
808
766
 
@@ -814,81 +772,145 @@ async function runAfterSettle(ctx, scope) {
814
772
  await hook(settlementContext(ctx, scope));
815
773
  } catch (error) {
816
774
  const message = errorMessage(error, "Post-settlement hook failed");
817
- console.error(`[router] ${ctx.routeEntry.key}: afterSettle failed: ${message}`);
818
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
819
- level: "error",
820
- message: `Post-settlement hook failed: ${message}`,
821
- route: ctx.routeEntry.key
822
- });
775
+ ctx.report("error", `Post-settlement hook failed: ${message}`);
823
776
  await runSettlementError(ctx, scope, error, "afterSettle");
824
777
  }
825
778
  }
826
779
 
827
- // src/pipeline/context/run-settled-handler-error.ts
828
- async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
829
- const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
830
- if (!hook) return;
831
- try {
832
- await hook({ ...settlementContext(ctx, scope), error });
833
- } catch (hookError) {
834
- const message = errorMessage(hookError, "Settled handler error hook failed");
835
- console.error(`[router] ${ctx.routeEntry.key}: onSettledHandlerError failed: ${message}`);
836
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
837
- level: "error",
838
- message: `Settled handler error hook failed: ${message}`,
839
- route: ctx.routeEntry.key
840
- });
841
- }
842
- }
843
-
844
- // src/pipeline/context/grant-entitlement.ts
845
- async function grantEntitlementIfSiwx(ctx, wallet) {
846
- if (!ctx.routeEntry.siwxEnabled) return;
847
- try {
848
- await ctx.deps.entitlementStore.grant(ctx.routeEntry.key, wallet);
849
- } catch (error) {
850
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
851
- level: "warn",
852
- message: `Entitlement grant failed: ${error instanceof Error ? error.message : String(error)}`,
853
- route: ctx.routeEntry.key
854
- });
855
- }
780
+ // src/pipeline/context/finalize/epilogue.ts
781
+ async function runPostSettleEpilogue(args) {
782
+ const { ctx, strategy, wallet, settle, afterSettleScope, rawResult, body } = args;
783
+ await grantEntitlementIfSiwx(ctx, wallet);
784
+ firePluginHook(ctx.deps.plugin, "onPaymentSettled", ctx.pluginCtx, {
785
+ protocol: strategy.protocol,
786
+ payer: wallet,
787
+ transaction: settle.settledPayment.transaction ?? "",
788
+ network: settle.settledPayment.network
789
+ });
790
+ await runAfterSettle(ctx, afterSettleScope);
791
+ return finalize(ctx, settle.response, rawResult, body);
856
792
  }
857
793
 
858
- // src/pipeline/context/settle-and-finalize.ts
859
- async function settleAndFinalize(args) {
860
- const { ctx, strategy, verifyOutcome, scope, rawResult, body, onSettleError } = args;
861
- const { request, routeEntry, deps } = ctx;
794
+ // src/pipeline/context/finalize/request.ts
795
+ async function settleAndFinalizeRequest(args) {
796
+ const { ctx, strategy, verifyOutcome, scope, rawResult, body, billedAmount, onSettleError } = args;
797
+ const { request, routeEntry, deps, report } = ctx;
862
798
  const settle = await strategy.settle({
863
799
  request,
864
800
  response: scope.response,
865
801
  payment: verifyOutcome.payment,
866
802
  token: verifyOutcome.token,
867
803
  routeEntry,
868
- deps
804
+ deps,
805
+ billedAmount,
806
+ report
869
807
  });
870
808
  if (!settle.ok) {
871
809
  if (onSettleError) await onSettleError(settle.error, settle.failMessage);
872
810
  return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
873
811
  }
874
- await grantEntitlementIfSiwx(ctx, verifyOutcome.wallet);
875
- firePluginHook(deps.plugin, "onPaymentSettled", ctx.pluginCtx, {
876
- protocol: strategy.protocol,
877
- payer: verifyOutcome.wallet,
878
- transaction: settle.settledPayment.transaction ?? "",
879
- network: settle.settledPayment.network
812
+ return runPostSettleEpilogue({
813
+ ctx,
814
+ strategy,
815
+ wallet: verifyOutcome.wallet,
816
+ settle,
817
+ afterSettleScope: {
818
+ ...scope,
819
+ payment: settle.settledPayment,
820
+ response: settle.response
821
+ },
822
+ rawResult,
823
+ body
880
824
  });
881
- await runAfterSettle(ctx, {
882
- ...scope,
883
- payment: settle.settledPayment,
884
- response: settle.response
825
+ }
826
+
827
+ // src/pipeline/context/finalize/stream.ts
828
+ async function settleAndFinalizeStream(args) {
829
+ const { ctx, strategy, verifyOutcome, source, account, body, bindChannelCharge } = args;
830
+ const { request, routeEntry, deps, report } = ctx;
831
+ if (!strategy.settleStream) {
832
+ return fail(ctx, 500, `${strategy.protocol} does not support streaming handlers`, body);
833
+ }
834
+ const settle = await strategy.settleStream({
835
+ request,
836
+ source,
837
+ payment: verifyOutcome.payment,
838
+ token: verifyOutcome.token,
839
+ routeEntry,
840
+ deps,
841
+ bindChannelCharge,
842
+ report
885
843
  });
886
- return finalize(ctx, settle.response, rawResult, body);
844
+ if (!settle.ok) {
845
+ return fail(ctx, settle.failStatus ?? 500, settle.failMessage, body);
846
+ }
847
+ return runPostSettleEpilogue({
848
+ ctx,
849
+ strategy,
850
+ wallet: verifyOutcome.wallet,
851
+ settle,
852
+ afterSettleScope: {
853
+ wallet: verifyOutcome.wallet,
854
+ account,
855
+ body,
856
+ payment: settle.settledPayment,
857
+ response: settle.response,
858
+ rawResult: void 0
859
+ },
860
+ rawResult: void 0,
861
+ body
862
+ });
863
+ }
864
+
865
+ // src/pipeline/context/run-handler-only.ts
866
+ async function runHandlerOnly(ctx, wallet, account) {
867
+ const body = await parseBody(ctx);
868
+ if (!body.ok) return body.response;
869
+ const validateErr = await runValidate(ctx, body.data);
870
+ if (validateErr) return validateErr;
871
+ const result = await invokeUnauthed(ctx, wallet, account, body.data);
872
+ return finalize(ctx, result.response, result.rawResult, body.data);
873
+ }
874
+
875
+ // src/pipeline/context/run-before-settle.ts
876
+ async function runBeforeSettle(ctx, scope) {
877
+ const hook = ctx.routeEntry.settlement?.beforeSettle;
878
+ if (!hook) return null;
879
+ try {
880
+ await hook(settlementContext(ctx, scope));
881
+ return null;
882
+ } catch (error) {
883
+ return fail(
884
+ ctx,
885
+ errorStatus(error, 500),
886
+ errorMessage(error, "Pre-settlement validation failed"),
887
+ scope.body
888
+ );
889
+ }
890
+ }
891
+
892
+ // src/pipeline/context/run-settled-handler-error.ts
893
+ async function runSettledHandlerError(ctx, scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
894
+ const hook = ctx.routeEntry.settlement?.onSettledHandlerError;
895
+ if (!hook) return;
896
+ try {
897
+ await hook({ ...settlementContext(ctx, scope), error });
898
+ } catch (hookError) {
899
+ const message = errorMessage(hookError, "Settled handler error hook failed");
900
+ ctx.report("error", `Settled handler error hook failed: ${message}`);
901
+ }
887
902
  }
888
903
 
889
904
  // src/auth/normalize-wallet.ts
890
905
  function normalizeWalletAddress(address) {
891
- return /^0x/i.test(address) ? address.toLowerCase() : address;
906
+ const isEvm = /^0x/i.test(address);
907
+ return isEvm ? normalizeEvmWalletAddress(address) : normalizeSolanaWalletAddress(address);
908
+ }
909
+ function normalizeEvmWalletAddress(address) {
910
+ return address.toLowerCase();
911
+ }
912
+ function normalizeSolanaWalletAddress(address) {
913
+ return address;
892
914
  }
893
915
 
894
916
  // src/auth/siwx.ts
@@ -968,20 +990,18 @@ function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
968
990
  return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
969
991
  }
970
992
 
971
- // src/pipeline/context/protocol-init-error.ts
972
- function protocolInitError(routeEntry, deps) {
973
- if (!routeEntry.pricing) return null;
974
- const errors = [];
975
- for (const protocol of routeEntry.protocols) {
976
- if (protocol === "x402" && deps.x402InitError) {
977
- errors.push(`x402: ${deps.x402InitError}`);
978
- }
979
- if (protocol === "mpp" && deps.mppInitError) {
980
- errors.push(`mpp: ${deps.mppInitError}`);
981
- }
982
- }
983
- if (errors.length === 0) return null;
984
- return `Payment protocol initialization failed. ${errors.join("; ")}`;
993
+ // src/pipeline/context/resolve-early-body.ts
994
+ async function resolveEarlyBody(args) {
995
+ const { ctx, pricing, incomingStrategy } = args;
996
+ if (!shouldParseBodyEarly(incomingStrategy, ctx.routeEntry, pricing)) {
997
+ return { ok: true, earlyBody: void 0 };
998
+ }
999
+ const earlyClone = ctx.request.clone();
1000
+ const earlyResult = await parseBody(ctx, earlyClone);
1001
+ if (!earlyResult.ok) return { ok: false, response: earlyResult.response };
1002
+ const validateErr = await runValidate(ctx, earlyResult.data);
1003
+ if (validateErr) return { ok: false, response: validateErr };
1004
+ return { ok: true, earlyBody: earlyResult.data };
985
1005
  }
986
1006
 
987
1007
  // src/auth/api-key.ts
@@ -1002,6 +1022,39 @@ function extractBearerToken(header) {
1002
1022
  return null;
1003
1023
  }
1004
1024
 
1025
+ // src/pipeline/context/run-api-key-gate.ts
1026
+ async function runApiKeyGate(ctx) {
1027
+ const { request, routeEntry, deps } = ctx;
1028
+ if (!routeEntry.apiKeyResolver) return { ok: true, account: void 0 };
1029
+ const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
1030
+ if (!apiKeyResult.valid) {
1031
+ return { ok: false, response: fail(ctx, 401, "Invalid or missing API key") };
1032
+ }
1033
+ firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
1034
+ authMode: "apiKey",
1035
+ wallet: null,
1036
+ route: routeEntry.key,
1037
+ account: apiKeyResult.account
1038
+ });
1039
+ return { ok: true, account: apiKeyResult.account };
1040
+ }
1041
+
1042
+ // src/pipeline/context/protocol-init-error.ts
1043
+ function protocolInitError(routeEntry, deps) {
1044
+ if (!routeEntry.pricing) return null;
1045
+ const errors = [];
1046
+ for (const protocol of routeEntry.protocols) {
1047
+ if (protocol === "x402" && deps.x402InitError) {
1048
+ errors.push(`x402: ${deps.x402InitError}`);
1049
+ }
1050
+ if (protocol === "mpp" && deps.mppInitError) {
1051
+ errors.push(`mpp: ${deps.mppInitError}`);
1052
+ }
1053
+ }
1054
+ if (errors.length === 0) return null;
1055
+ return `Payment protocol initialization failed. ${errors.join("; ")}`;
1056
+ }
1057
+
1005
1058
  // src/pipeline/flows/api-key-only.ts
1006
1059
  async function runApiKeyOnlyFlow(ctx) {
1007
1060
  if (!ctx.routeEntry.apiKeyResolver) {
@@ -1171,13 +1224,22 @@ function selectPricing(raw, deps = {}) {
1171
1224
  // src/protocols/mpp/credential.ts
1172
1225
  import { Credential } from "mppx";
1173
1226
  import { getAddress, isAddress } from "viem";
1227
+ var SESSION_ACTIONS = /* @__PURE__ */ new Set(["open", "topUp", "voucher", "close"]);
1174
1228
  function readMppCredential(request) {
1175
1229
  const credential = Credential.fromRequest(request);
1176
1230
  if (!credential) return null;
1177
1231
  const wallet = walletFromDid(credential.source ?? "");
1178
- const rawType = credential.payload?.type;
1232
+ const payload = credential.payload;
1233
+ const rawType = payload?.type;
1179
1234
  const payloadType = rawType === "transaction" ? "transaction" : rawType === "hash" ? "hash" : "unknown";
1180
- return { credential, wallet, payloadType };
1235
+ const rawAction = payload?.action;
1236
+ const sessionAction = typeof rawAction === "string" && SESSION_ACTIONS.has(rawAction) ? rawAction : void 0;
1237
+ return {
1238
+ credential,
1239
+ wallet,
1240
+ payloadType,
1241
+ ...sessionAction ? { sessionAction } : {}
1242
+ };
1181
1243
  }
1182
1244
  function walletFromDid(rawSource) {
1183
1245
  const parts = rawSource.split(":");
@@ -1185,6 +1247,168 @@ function walletFromDid(rawSource) {
1185
1247
  return normalizeWalletAddress(isAddress(last) ? getAddress(last) : rawSource);
1186
1248
  }
1187
1249
 
1250
+ // src/protocols/mpp/session-mode.ts
1251
+ async function verifySessionMode(args, info) {
1252
+ const { request, deps, price, routeEntry } = args;
1253
+ if (!deps.mppx?.sessionRequest || !deps.mppx?.sessionStream || !deps.mppSessionConfig) {
1254
+ return {
1255
+ ok: false,
1256
+ kind: "config",
1257
+ message: "MPP sessions not configured on this server (set RouterConfig.mpp.session)"
1258
+ };
1259
+ }
1260
+ const tickCost = routeEntry.tickCost;
1261
+ const unitType = routeEntry.unitType;
1262
+ const streaming = routeEntry.streaming === true;
1263
+ const middleware = streaming ? deps.mppx.sessionStream : deps.mppx.sessionRequest;
1264
+ const middlewareRequest = isChannelOnlyAction(info, request) ? new Request(request.url, { method: request.method, headers: request.headers }) : request;
1265
+ let result;
1266
+ try {
1267
+ result = await middleware({
1268
+ amount: tickCost,
1269
+ unitType,
1270
+ suggestedDeposit: price,
1271
+ ...streaming ? { meta: { streaming: "true" } } : {}
1272
+ })(middlewareRequest);
1273
+ } catch (err) {
1274
+ const message = err instanceof Error ? err.message : String(err);
1275
+ return {
1276
+ ok: false,
1277
+ kind: "config",
1278
+ message: `MPP session verify failed: ${message}`
1279
+ };
1280
+ }
1281
+ if (result.status === 402) {
1282
+ const failure = await readMppxProblemDetails(result.challenge);
1283
+ return { ok: false, kind: "invalid", failure };
1284
+ }
1285
+ const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
1286
+ const payment = {
1287
+ protocol: "mpp",
1288
+ status: "verified",
1289
+ payer: info.wallet,
1290
+ amount: price,
1291
+ network: "tempo:4217",
1292
+ ...mppRecipient ? { recipient: mppRecipient } : {}
1293
+ };
1294
+ const token = {
1295
+ mode: "session",
1296
+ streaming,
1297
+ sessionResult: result,
1298
+ info,
1299
+ tickCost
1300
+ };
1301
+ return {
1302
+ ok: true,
1303
+ wallet: info.wallet,
1304
+ payment,
1305
+ token,
1306
+ alreadySettled: false
1307
+ };
1308
+ }
1309
+ async function settleSessionMode(args) {
1310
+ const { request, response, payment, token, billedAmount } = args;
1311
+ const sessionToken = token;
1312
+ if (isChannelOnlyAction(sessionToken.info, request)) {
1313
+ const wrapped2 = sessionToken.sessionResult.withReceipt(
1314
+ new Response(null, { status: 200 })
1315
+ );
1316
+ return {
1317
+ ok: true,
1318
+ response: wrapped2,
1319
+ settledPayment: { ...payment, status: "settled", amount: billedAmount }
1320
+ };
1321
+ }
1322
+ if (sessionToken.streaming) {
1323
+ return {
1324
+ ok: false,
1325
+ error: new Error("streaming session content settled via request path"),
1326
+ failMessage: "streaming session content settled via request path",
1327
+ failStatus: 500
1328
+ };
1329
+ }
1330
+ const wrapped = sessionToken.sessionResult.withReceipt(
1331
+ response
1332
+ );
1333
+ wrapped.headers.set("Cache-Control", "private");
1334
+ const receiptHeader = wrapped.headers.get(HEADERS.MPP_PAYMENT_RECEIPT) ?? void 0;
1335
+ const settledPayment = {
1336
+ ...payment,
1337
+ status: "settled",
1338
+ amount: billedAmount,
1339
+ ...receiptHeader ? { receipt: receiptHeader } : {}
1340
+ };
1341
+ return { ok: true, response: wrapped, settledPayment };
1342
+ }
1343
+ async function buildSessionChallenge(args) {
1344
+ const { request, deps, suggestedDeposit, routeEntry, report } = args;
1345
+ if (!deps.mppSessionConfig) return {};
1346
+ const streaming = routeEntry.streaming === true;
1347
+ const middleware = streaming ? deps.mppx?.sessionStream : deps.mppx?.sessionRequest;
1348
+ if (!middleware) return {};
1349
+ const tickCost = routeEntry.tickCost;
1350
+ const unitType = routeEntry.unitType;
1351
+ try {
1352
+ const result = await middleware({
1353
+ amount: tickCost,
1354
+ unitType,
1355
+ suggestedDeposit,
1356
+ ...streaming ? { meta: { streaming: "true" } } : {}
1357
+ })(request);
1358
+ if (result.status === 402) {
1359
+ const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
1360
+ if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
1361
+ }
1362
+ } catch (err) {
1363
+ report(
1364
+ "warn",
1365
+ `MPP session challenge build failed: ${err instanceof Error ? err.message : String(err)}`
1366
+ );
1367
+ throw err;
1368
+ }
1369
+ return {};
1370
+ }
1371
+ function isChannelOnlyAction(info, request) {
1372
+ const action = info.sessionAction;
1373
+ if (!action) return false;
1374
+ if (action === "close" || action === "topUp") return true;
1375
+ if ((action === "open" || action === "voucher") && !hasRequestBody(request)) return true;
1376
+ return false;
1377
+ }
1378
+ async function readMppxProblemDetails(challenge) {
1379
+ let body;
1380
+ try {
1381
+ body = await challenge.clone().text();
1382
+ } catch {
1383
+ return { reason: "mpp_session_invalid" };
1384
+ }
1385
+ if (!body) return { reason: "mpp_session_invalid" };
1386
+ let parsed;
1387
+ try {
1388
+ parsed = JSON.parse(body);
1389
+ } catch {
1390
+ return { reason: "mpp_session_invalid", message: body.slice(0, 500) };
1391
+ }
1392
+ if (!parsed || typeof parsed !== "object") {
1393
+ return { reason: "mpp_session_invalid" };
1394
+ }
1395
+ const details = parsed;
1396
+ const typeUri = typeof details.type === "string" ? details.type : void 0;
1397
+ const slug = typeUri ? typeUri.split("/").pop() : void 0;
1398
+ const reason = slug ? slug.replace(/-/g, "_") : "mpp_session_invalid";
1399
+ const message = typeof details.detail === "string" && details.detail.length > 0 ? details.detail : typeof details.title === "string" ? details.title : void 0;
1400
+ return message ? { reason, message } : { reason };
1401
+ }
1402
+ function hasRequestBody(request) {
1403
+ const cl = request.headers.get("content-length");
1404
+ if (cl !== null) {
1405
+ const n = Number.parseInt(cl.trim(), 10);
1406
+ return Number.isFinite(n) && n > 0;
1407
+ }
1408
+ if (request.headers.get("transfer-encoding") !== null) return true;
1409
+ return false;
1410
+ }
1411
+
1188
1412
  // src/protocols/mpp/transaction-mode.ts
1189
1413
  import { Transaction as TempoTransaction } from "viem/tempo";
1190
1414
  import { call as viemCall } from "viem/actions";
@@ -1212,7 +1436,7 @@ async function readChallengeReason(challenge) {
1212
1436
 
1213
1437
  // src/protocols/mpp/transaction-mode.ts
1214
1438
  async function verifyTxMode(args, info) {
1215
- const { deps, price, routeEntry } = args;
1439
+ const { deps, price, report } = args;
1216
1440
  if (!deps.tempoClient) {
1217
1441
  return {
1218
1442
  ok: false,
@@ -1230,7 +1454,7 @@ async function verifyTxMode(args, info) {
1230
1454
  });
1231
1455
  } catch (err) {
1232
1456
  const message = err instanceof Error ? err.message : String(err);
1233
- console.warn(`[router] ${routeEntry.key}: MPP simulation failed \u2014 ${message}`);
1457
+ report("warn", `MPP simulation failed: ${message}`);
1234
1458
  return { ok: false, kind: "invalid" };
1235
1459
  }
1236
1460
  const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
@@ -1251,7 +1475,7 @@ async function verifyTxMode(args, info) {
1251
1475
  };
1252
1476
  }
1253
1477
  async function settleTxMode(args) {
1254
- const { request, response, payment, deps, routeEntry } = args;
1478
+ const { request, response, payment, deps, report } = args;
1255
1479
  if (!deps.mppx) {
1256
1480
  return {
1257
1481
  ok: false,
@@ -1265,7 +1489,7 @@ async function settleTxMode(args) {
1265
1489
  result = await deps.mppx.charge({ amount: payment.amount })(request);
1266
1490
  } catch (err) {
1267
1491
  const message = err instanceof Error ? err.message : String(err);
1268
- console.error(`[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`);
1492
+ report("error", `MPP broadcast failed after handler: ${message}`);
1269
1493
  return {
1270
1494
  ok: false,
1271
1495
  error: err,
@@ -1282,7 +1506,7 @@ async function settleTxMode(args) {
1282
1506
  mppResult: result,
1283
1507
  challenge: result.challenge
1284
1508
  });
1285
- console.error(`[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`);
1509
+ report("error", `MPP payment failed after handler: ${detail}`);
1286
1510
  return {
1287
1511
  ok: false,
1288
1512
  error: settlementError,
@@ -1305,10 +1529,10 @@ async function settleTxMode(args) {
1305
1529
 
1306
1530
  // src/protocols/mpp/hash-mode.ts
1307
1531
  async function verifyHashMode(args, info) {
1308
- const { deps, price, routeEntry, request } = args;
1532
+ const { deps, price, request, report } = args;
1309
1533
  if (!deps.mppx) {
1310
1534
  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";
1311
- console.error(`[router] ${routeEntry.key}: ${reason}`);
1535
+ report("error", reason);
1312
1536
  return { ok: false, kind: "config", message: reason };
1313
1537
  }
1314
1538
  let chargeResult;
@@ -1316,13 +1540,13 @@ async function verifyHashMode(args, info) {
1316
1540
  chargeResult = await deps.mppx.charge({ amount: price })(request);
1317
1541
  } catch (err) {
1318
1542
  const message = err instanceof Error ? err.message : String(err);
1319
- console.error(`[router] ${routeEntry.key}: MPP charge failed: ${message}`);
1543
+ report("error", `MPP charge failed: ${message}`);
1320
1544
  return { ok: false, kind: "config", message: `MPP payment processing failed: ${message}` };
1321
1545
  }
1322
1546
  if (chargeResult.status === 402) {
1323
1547
  const reason = await readChallengeReason(chargeResult.challenge);
1324
1548
  const detail = reason || "credential may be invalid, or check TEMPO_RPC_URL configuration";
1325
- console.warn(`[router] ${routeEntry.key}: MPP credential rejected \u2014 ${detail}`);
1549
+ report("warn", `MPP credential rejected: ${detail}`);
1326
1550
  return { ok: false, kind: "invalid" };
1327
1551
  }
1328
1552
  const receiptHeader = chargeResult.withReceipt(new Response()).headers.get(
@@ -1367,9 +1591,20 @@ var mppStrategy = {
1367
1591
  const auth = request.headers.get(HEADERS.AUTHORIZATION);
1368
1592
  return Boolean(auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT));
1369
1593
  },
1594
+ preflight(request, _routeEntry) {
1595
+ const info = readMppCredential(request);
1596
+ if (!info?.sessionAction) return null;
1597
+ if (!isChannelOnlyAction(info, request)) return null;
1598
+ return { skipBody: true, skipHandler: true };
1599
+ },
1370
1600
  async verify(args) {
1371
1601
  const info = readMppCredential(args.request);
1372
1602
  if (!info) return { ok: false, kind: "invalid" };
1603
+ if (args.routeEntry.dynamicPrice) {
1604
+ if (!info.sessionAction) return { ok: false, kind: "invalid" };
1605
+ return verifySessionMode(args, info);
1606
+ }
1607
+ if (info.sessionAction) return { ok: false, kind: "invalid" };
1373
1608
  if (info.payloadType === "transaction" && args.deps.tempoClient) {
1374
1609
  return verifyTxMode(args, info);
1375
1610
  }
@@ -1377,28 +1612,88 @@ var mppStrategy = {
1377
1612
  },
1378
1613
  async settle(args) {
1379
1614
  const token = args.token;
1615
+ if (token.mode === "session") return settleSessionMode(args);
1380
1616
  if (token.mode === "transaction") return settleTxMode(args);
1381
1617
  return settleHashMode(args);
1382
1618
  },
1619
+ async settleStream(args) {
1620
+ const token = args.token;
1621
+ if (token.mode !== "session" || !token.streaming) {
1622
+ return {
1623
+ ok: false,
1624
+ error: new Error("streaming requires a streaming-mode MPP session credential"),
1625
+ failMessage: "streaming requires a streaming-mode MPP session credential",
1626
+ failStatus: 400
1627
+ };
1628
+ }
1629
+ const sessionToken = token;
1630
+ const sseResult = sessionToken.sessionResult;
1631
+ const { bindChannelCharge, source: handlerStream } = args;
1632
+ async function* forwardHandlerStreamWithChannelDebit(channel) {
1633
+ bindChannelCharge(channel.charge);
1634
+ try {
1635
+ for await (const chunk of handlerStream) {
1636
+ yield typeof chunk === "string" ? chunk : JSON.stringify(chunk);
1637
+ }
1638
+ } finally {
1639
+ bindChannelCharge(null);
1640
+ }
1641
+ }
1642
+ const sse = sseResult.withReceipt(forwardHandlerStreamWithChannelDebit);
1643
+ sse.headers.set("Cache-Control", "private");
1644
+ const settledPayment = {
1645
+ ...args.payment,
1646
+ status: "settled",
1647
+ amount: args.payment.amount
1648
+ };
1649
+ return { ok: true, response: sse, settledPayment };
1650
+ },
1383
1651
  async buildChallenge(args) {
1384
1652
  if (!args.deps.mppx) return {};
1385
- try {
1386
- const result = await args.deps.mppx.charge({ amount: args.price })(args.request);
1387
- if (result.status === 402) {
1388
- const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
1389
- if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
1390
- }
1391
- } catch (err) {
1392
- console.warn(
1393
- `[router] MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
1394
- );
1395
- throw err;
1653
+ const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
1654
+ if (args.routeEntry.dynamicPrice && sessionsConfigured) {
1655
+ const tickCost = args.routeEntry.tickCost;
1656
+ const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
1657
+ const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
1658
+ return buildSessionChallenge({
1659
+ ...args,
1660
+ suggestedDeposit
1661
+ });
1396
1662
  }
1397
- return {};
1663
+ return buildChargeChallenge(args);
1398
1664
  }
1399
1665
  };
1666
+ function multiplyDecimal(decimal, factor) {
1667
+ if (!Number.isFinite(factor) || factor <= 0) return decimal;
1668
+ const [whole, fraction = ""] = decimal.split(".");
1669
+ const scaled = (BigInt(whole + fraction) * BigInt(factor)).toString();
1670
+ const decimals = fraction.length;
1671
+ if (decimals === 0) return scaled;
1672
+ const padded = scaled.padStart(decimals + 1, "0");
1673
+ const intPart = padded.slice(0, padded.length - decimals);
1674
+ const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
1675
+ return fracPart ? `${intPart}.${fracPart}` : intPart;
1676
+ }
1677
+ async function buildChargeChallenge(args) {
1678
+ if (!args.deps.mppx) return {};
1679
+ try {
1680
+ const result = await args.deps.mppx.charge({ amount: args.price })(args.request);
1681
+ if (result.status === 402) {
1682
+ const wwwAuth = result.challenge.headers.get(HEADERS.WWW_AUTHENTICATE);
1683
+ if (wwwAuth) return { headers: { [HEADERS.WWW_AUTHENTICATE]: wwwAuth } };
1684
+ }
1685
+ } catch (err) {
1686
+ args.report(
1687
+ "warn",
1688
+ `MPP challenge build failed: ${err instanceof Error ? err.message : String(err)}`
1689
+ );
1690
+ throw err;
1691
+ }
1692
+ return {};
1693
+ }
1400
1694
 
1401
1695
  // src/protocols/x402/strategy.ts
1696
+ import { PERMIT2_ADDRESS } from "@x402/evm";
1402
1697
  init_accepts();
1403
1698
 
1404
1699
  // src/protocols/x402/challenge.ts
@@ -1408,20 +1703,27 @@ init_solana();
1408
1703
  // src/protocols/x402/requirements.ts
1409
1704
  init_evm();
1410
1705
  init_solana();
1411
- async function buildExpectedRequirements(server, request, price, accepts) {
1412
- const exactRequirements = await buildExactRequirements(server, request, price, accepts);
1706
+ async function buildExpectedRequirements(server, request, price, accepts, report) {
1707
+ const sdkRequirements = await buildSdkHandledRequirements(
1708
+ server,
1709
+ request,
1710
+ price,
1711
+ accepts,
1712
+ report
1713
+ );
1413
1714
  const customRequirements = buildCustomRequirements(price, accepts);
1414
- return [...exactRequirements, ...customRequirements];
1715
+ return [...sdkRequirements, ...customRequirements];
1415
1716
  }
1416
- async function buildExactRequirements(server, request, price, accepts) {
1417
- const exactGroups = [
1717
+ async function buildSdkHandledRequirements(server, request, price, accepts, report) {
1718
+ const groups = [
1418
1719
  buildEvmExactOptions(accepts, price),
1720
+ buildEvmUptoOptions(accepts, price),
1419
1721
  buildSolanaExactOptions(accepts, price)
1420
1722
  ].filter((options) => options.length > 0);
1421
- if (exactGroups.length === 0) return [];
1723
+ if (groups.length === 0) return [];
1422
1724
  const requirements = [];
1423
1725
  const failures = [];
1424
- for (const options of exactGroups) {
1726
+ for (const options of groups) {
1425
1727
  try {
1426
1728
  requirements.push(
1427
1729
  ...await server.buildPaymentRequirementsFromOptions(options, { request })
@@ -1429,21 +1731,31 @@ async function buildExactRequirements(server, request, price, accepts) {
1429
1731
  } catch (error) {
1430
1732
  const err = error instanceof Error ? error : new Error(String(error));
1431
1733
  failures.push(err);
1432
- if (exactGroups.length === 1) {
1734
+ if (groups.length === 1) {
1433
1735
  throw err;
1434
1736
  }
1435
- console.warn(
1436
- `[router] Failed to build x402 exact requirements for ${options[0]?.network}: ${err.message}`
1737
+ report?.(
1738
+ "warn",
1739
+ `Failed to build x402 ${options[0]?.scheme} requirements for ${options[0]?.network}: ${err.message}`
1437
1740
  );
1438
1741
  }
1439
1742
  }
1440
1743
  if (requirements.length > 0) {
1441
1744
  return requirements;
1442
1745
  }
1443
- throw failures[0] ?? new Error("Failed to build x402 exact requirements");
1746
+ throw failures[0] ?? new Error("Failed to build x402 SDK-handled requirements");
1444
1747
  }
1445
1748
  function buildCustomRequirements(price, accepts) {
1446
- return accepts.filter((accept) => accept.scheme !== "exact").map((accept) => buildCustomRequirement(price, accept));
1749
+ return accepts.filter((accept) => !isSdkHandled(accept)).map((accept) => buildCustomRequirement(price, accept));
1750
+ }
1751
+ function isSdkHandled(accept) {
1752
+ if (isEvmNetwork(accept.network)) {
1753
+ return accept.scheme === "exact" || accept.scheme === "upto";
1754
+ }
1755
+ if (isSolanaRequirement({ network: accept.network })) {
1756
+ return accept.scheme === "exact";
1757
+ }
1758
+ return false;
1447
1759
  }
1448
1760
  function buildCustomRequirement(price, accept) {
1449
1761
  if (!accept.asset) {
@@ -1477,7 +1789,7 @@ function decimalToAtomicUnits(amount, decimals) {
1477
1789
 
1478
1790
  // src/protocols/x402/challenge.ts
1479
1791
  async function buildX402Challenge(opts) {
1480
- const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions } = opts;
1792
+ const { server, routeEntry, request, price, accepts, facilitatorsByNetwork, extensions, report } = opts;
1481
1793
  const { encodePaymentRequiredHeader } = await import("@x402/core/http");
1482
1794
  const resource = buildChallengeResource(request, routeEntry);
1483
1795
  const requirements = await buildChallengeRequirements(
@@ -1486,7 +1798,8 @@ async function buildX402Challenge(opts) {
1486
1798
  price,
1487
1799
  accepts,
1488
1800
  resource,
1489
- facilitatorsByNetwork
1801
+ facilitatorsByNetwork,
1802
+ report
1490
1803
  );
1491
1804
  const paymentRequired = await server.createPaymentRequiredResponse(
1492
1805
  requirements,
@@ -1497,13 +1810,13 @@ async function buildX402Challenge(opts) {
1497
1810
  const encoded = encodePaymentRequiredHeader(paymentRequired);
1498
1811
  return { encoded, requirements };
1499
1812
  }
1500
- async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork) {
1501
- const requirements = await buildExpectedRequirements(server, request, price, accepts);
1813
+ async function buildChallengeRequirements(server, request, price, accepts, resource, facilitatorsByNetwork, report) {
1814
+ const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
1502
1815
  if (!needsFacilitatorEnrichment(accepts)) return requirements;
1503
- return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork);
1816
+ return enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork, report);
1504
1817
  }
1505
1818
  function needsFacilitatorEnrichment(accepts) {
1506
- return accepts.some((accept) => accept.scheme !== "exact") || hasSolanaAccepts(accepts);
1819
+ return hasSolanaAccepts(accepts);
1507
1820
  }
1508
1821
  async function enrichGroup(group, resource) {
1509
1822
  const accepted = await enrichRequirementsWithFacilitatorAccepts(
@@ -1518,7 +1831,7 @@ async function enrichGroup(group, resource) {
1518
1831
  }
1519
1832
  return accepted;
1520
1833
  }
1521
- async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork) {
1834
+ async function enrichChallengeRequirements(requirements, resource, facilitatorsByNetwork, report) {
1522
1835
  const groups = collectEnrichmentGroups(requirements, facilitatorsByNetwork);
1523
1836
  if (groups.length === 0) return requirements;
1524
1837
  const results = await Promise.all(
@@ -1528,8 +1841,9 @@ async function enrichChallengeRequirements(requirements, resource, facilitatorsB
1528
1841
  } catch (err) {
1529
1842
  const label = group.facilitator.url ?? group.facilitator.network;
1530
1843
  const reason = err instanceof Error ? err.message : String(err);
1531
- console.warn(
1532
- `[router] ${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
1844
+ report?.(
1845
+ "warn",
1846
+ `${label} /accepts failed, dropping ${group.items.length} requirement(s): ${reason}`
1533
1847
  );
1534
1848
  return { success: false, group };
1535
1849
  }
@@ -1582,7 +1896,7 @@ function getRequiredFacilitator(requirement, facilitatorsByNetwork) {
1582
1896
  return facilitator;
1583
1897
  }
1584
1898
  function requiresFacilitatorEnrichment(requirement) {
1585
- return requirement.scheme !== "exact" || isSolanaRequirement(requirement);
1899
+ return isSolanaRequirement(requirement);
1586
1900
  }
1587
1901
  function buildChallengeResource(request, routeEntry) {
1588
1902
  return {
@@ -1594,33 +1908,68 @@ function buildChallengeResource(request, routeEntry) {
1594
1908
  }
1595
1909
 
1596
1910
  // src/protocols/x402/settle.ts
1597
- async function settleX402Payment(server, payload, requirements) {
1911
+ async function settleX402Payment(server, payload, requirements, amountOverride) {
1598
1912
  const { encodePaymentResponseHeader } = await import("@x402/core/http");
1913
+ if (amountOverride?.amount !== void 0) {
1914
+ const upstreamTaggedAmount = tagBareDecimalAsDollars(amountOverride.amount);
1915
+ const result2 = await server.settlePayment(payload, requirements, void 0, void 0, {
1916
+ amount: upstreamTaggedAmount
1917
+ });
1918
+ return {
1919
+ encoded: encodePaymentResponseHeader(result2),
1920
+ result: result2
1921
+ };
1922
+ }
1599
1923
  const result = await server.settlePayment(payload, requirements);
1600
- const encoded = encodePaymentResponseHeader(result);
1601
- return { encoded, result };
1924
+ return {
1925
+ encoded: encodePaymentResponseHeader(result),
1926
+ result
1927
+ };
1928
+ }
1929
+ function tagBareDecimalAsDollars(amount) {
1930
+ if (/^\d+\.\d+$/.test(amount)) return `$${amount}`;
1931
+ return amount;
1602
1932
  }
1603
1933
 
1604
1934
  // src/protocols/x402/verify.ts
1935
+ import { VerifyError } from "@x402/core/types";
1605
1936
  async function verifyX402Payment(opts) {
1606
- const { server, request, price, accepts } = opts;
1937
+ const { server, request, price, accepts, report } = opts;
1607
1938
  const payload = await readPaymentPayload(request);
1608
1939
  if (!payload) return null;
1609
- const requirements = await buildExpectedRequirements(server, request, price, accepts);
1940
+ const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
1610
1941
  const matching = findVerifiableRequirements(server, requirements, payload);
1942
+ const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
1611
1943
  if (!matching) {
1612
- return invalidPaymentVerification();
1944
+ return invalidPaymentVerification({
1945
+ reason: "requirements_mismatch",
1946
+ message: "Signed payment requirements did not match any server-built requirement",
1947
+ ...accepted ? { accepted } : {}
1948
+ });
1613
1949
  }
1614
1950
  let verify;
1615
1951
  try {
1616
1952
  verify = await server.verifyPayment(payload, matching);
1617
1953
  } catch (err) {
1618
- const sc = err.statusCode;
1619
- if (sc && sc >= 400 && sc < 500) return invalidPaymentVerification();
1954
+ if (err instanceof VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
1955
+ return invalidPaymentVerification({
1956
+ reason: err.invalidReason ?? "verify_error",
1957
+ ...err.invalidMessage ? { message: err.invalidMessage } : {},
1958
+ ...err.payer ? { payer: err.payer } : {},
1959
+ ...accepted ? { accepted } : {}
1960
+ });
1961
+ }
1620
1962
  throw err;
1621
1963
  }
1622
- if (!verify.isValid) return invalidPaymentVerification();
1623
- if (typeof verify.payer !== "string" || verify.payer.length === 0) {
1964
+ if (!verify.isValid) {
1965
+ return invalidPaymentVerification({
1966
+ reason: verify.invalidReason ?? "unknown",
1967
+ ...verify.invalidMessage ? { message: verify.invalidMessage } : {},
1968
+ ...verify.payer ? { payer: verify.payer } : {},
1969
+ ...accepted ? { accepted } : {}
1970
+ });
1971
+ }
1972
+ if (!verify.payer) {
1624
1973
  throw new Error("x402 verification succeeded without a payer address");
1625
1974
  }
1626
1975
  return {
@@ -1652,11 +2001,36 @@ async function readPaymentPayload(request) {
1652
2001
  const { decodePaymentSignatureHeader } = await import("@x402/core/http");
1653
2002
  return decodePaymentSignatureHeader(paymentHeader);
1654
2003
  }
1655
- function invalidPaymentVerification() {
1656
- return { valid: false, payload: null, requirements: null, payer: null };
2004
+ function invalidPaymentVerification(failure) {
2005
+ return {
2006
+ valid: false,
2007
+ payload: null,
2008
+ requirements: null,
2009
+ payer: null,
2010
+ ...failure ? { failure } : {}
2011
+ };
1657
2012
  }
1658
2013
 
1659
2014
  // src/protocols/x402/strategy.ts
2015
+ function formatVerifyFailureMessage(failure) {
2016
+ if (failure.reason === "permit2_allowance_required") {
2017
+ const wallet = failure.payer ?? "<the payer wallet>";
2018
+ const asset = failure.accepted?.asset ?? "<the asset>";
2019
+ const amount = failure.accepted?.amount ?? "<the required amount>";
2020
+ const network = failure.accepted?.network ?? "<the payment network>";
2021
+ return [
2022
+ `Payment rejected: In order for Upto to charge, the wallet ${wallet} MUST approve Permit2 to spend ${asset} on ${network}.`,
2023
+ `Required call (one-time, on-chain): ${asset}.approve(${PERMIT2_ADDRESS}, MAX_UINT256) from ${wallet}.`,
2024
+ `Permit2 contract address: ${PERMIT2_ADDRESS}.`,
2025
+ `Minimum allowance for this request: ${amount} (smallest units of ${asset}); use MAX_UINT256 to avoid re-approving on every future call.`,
2026
+ `Alternative without an on-chain transaction: the merchant can adopt the EIP-2612 gas-sponsoring extension (https://docs.x402.org/extensions/eip2612-gas-sponsoring).`
2027
+ ].join(" ");
2028
+ }
2029
+ if (failure.message) {
2030
+ return `Payment rejected (${failure.reason}): ${failure.message}`;
2031
+ }
2032
+ return `Payment rejected: ${failure.reason}`;
2033
+ }
1660
2034
  var x402Strategy = {
1661
2035
  protocol: "x402",
1662
2036
  detects(request) {
@@ -1664,114 +2038,123 @@ var x402Strategy = {
1664
2038
  request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)
1665
2039
  );
1666
2040
  },
1667
- async verify(args) {
1668
- const { request, body, price, routeEntry, deps } = args;
1669
- if (!deps.x402Server) {
1670
- 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";
1671
- console.error(`[router] ${routeEntry.key}: ${reason}`);
1672
- return { ok: false, kind: "config", message: reason };
1673
- }
1674
- const accepts = await resolveX402Accepts(
1675
- request,
1676
- routeEntry,
1677
- deps.x402Accepts,
1678
- deps.payeeAddress,
1679
- body
1680
- );
1681
- const verifyResult = await verifyX402Payment({
1682
- server: deps.x402Server,
1683
- request,
1684
- price,
1685
- accepts
1686
- });
1687
- if (!verifyResult?.valid) return { ok: false, kind: "invalid" };
1688
- const wallet = normalizeWalletAddress(verifyResult.payer);
1689
- const matchedNetwork = getRequirementNetwork(verifyResult.requirements, deps.network);
1690
- const matchedRecipient = getRequirementRecipient(verifyResult.requirements);
1691
- const payment = {
1692
- protocol: "x402",
1693
- status: "verified",
1694
- payer: wallet,
1695
- amount: price,
1696
- network: matchedNetwork,
1697
- ...matchedRecipient ? { recipient: matchedRecipient } : {}
1698
- };
1699
- return {
1700
- ok: true,
1701
- wallet,
1702
- payment,
1703
- token: {
1704
- payload: verifyResult.payload,
1705
- requirements: verifyResult.requirements
1706
- }
1707
- };
1708
- },
1709
- async settle(args) {
1710
- const { response, payment, token, deps } = args;
1711
- const x402Token = token;
1712
- try {
1713
- const settle = await settleX402Payment(
1714
- deps.x402Server,
1715
- x402Token.payload,
1716
- x402Token.requirements
1717
- );
1718
- if (!settle.result?.success) {
1719
- const reason = settle.result?.errorReason || "x402 settlement returned success=false";
1720
- const error = new Error(reason);
1721
- error.errorReason = reason;
1722
- throw error;
1723
- }
1724
- response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
1725
- response.headers.set("Cache-Control", "private");
1726
- const transaction = String(settle.result?.transaction ?? "");
1727
- const settledPayment = {
1728
- ...payment,
1729
- status: "settled",
1730
- ...transaction ? { transaction } : {}
2041
+ verify: (args) => verifyX402(args),
2042
+ settle: (args) => settleX402(args),
2043
+ buildChallenge: (args) => buildX402ChallengeContribution(args)
2044
+ };
2045
+ async function verifyX402(args) {
2046
+ const { request, body, price, routeEntry, deps, report } = args;
2047
+ if (!deps.x402Server) {
2048
+ 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";
2049
+ report("error", reason);
2050
+ return { ok: false, kind: "config", message: reason };
2051
+ }
2052
+ const accepts = await resolveX402Accepts(
2053
+ request,
2054
+ routeEntry,
2055
+ deps.x402Accepts,
2056
+ deps.payeeAddress,
2057
+ body
2058
+ );
2059
+ const verifyResult = await verifyX402Payment({
2060
+ server: deps.x402Server,
2061
+ request,
2062
+ price,
2063
+ accepts,
2064
+ report
2065
+ });
2066
+ if (!verifyResult?.valid) {
2067
+ const failure = verifyResult?.failure;
2068
+ if (failure) {
2069
+ return {
2070
+ ok: false,
2071
+ kind: "invalid",
2072
+ failure: {
2073
+ reason: failure.reason,
2074
+ message: formatVerifyFailureMessage(failure)
2075
+ }
1731
2076
  };
1732
- return { ok: true, response, settledPayment };
1733
- } catch (err) {
1734
- const errObj = err;
1735
- console.error("Settlement failed", {
1736
- message: err instanceof Error ? err.message : String(err),
1737
- route: args.routeEntry.key,
1738
- network: payment.network,
1739
- errorReason: errObj.errorReason,
1740
- facilitatorStatus: errObj.response?.status,
1741
- facilitatorBody: errObj.response?.data ?? errObj.response?.body
1742
- });
1743
- return { ok: false, error: err, failMessage: "Settlement failed" };
1744
2077
  }
1745
- },
1746
- async buildChallenge(args) {
1747
- const { request, routeEntry, body, price, extensions, deps } = args;
1748
- if (!deps.x402Server) return {};
1749
- const accepts = await resolveX402Accepts(
1750
- request,
1751
- routeEntry,
1752
- deps.x402Accepts,
1753
- deps.payeeAddress,
1754
- body
1755
- );
1756
- const { encoded } = await buildX402Challenge({
1757
- server: deps.x402Server,
1758
- routeEntry,
1759
- request,
1760
- price,
1761
- accepts,
1762
- facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
1763
- extensions
1764
- });
1765
- return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
2078
+ return { ok: false, kind: "invalid" };
2079
+ }
2080
+ const wallet = normalizeWalletAddress(verifyResult.payer);
2081
+ const { network, payTo } = verifyResult.requirements;
2082
+ const payment = {
2083
+ protocol: "x402",
2084
+ status: "verified",
2085
+ payer: wallet,
2086
+ amount: price,
2087
+ network,
2088
+ ...payTo ? { recipient: payTo } : {}
2089
+ };
2090
+ return {
2091
+ ok: true,
2092
+ wallet,
2093
+ payment,
2094
+ token: {
2095
+ payload: verifyResult.payload,
2096
+ requirements: verifyResult.requirements
2097
+ }
2098
+ };
2099
+ }
2100
+ async function settleX402(args) {
2101
+ const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
2102
+ const { payload, requirements } = token;
2103
+ const override = routeEntry.dynamicPrice ? { amount: billedAmount } : void 0;
2104
+ try {
2105
+ const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2106
+ if (!settle.result?.success) {
2107
+ throw Object.assign(
2108
+ new Error(settle.result?.errorReason ?? "x402 settlement returned success=false"),
2109
+ { errorReason: settle.result?.errorReason }
2110
+ );
2111
+ }
2112
+ response.headers.set(HEADERS.X402_PAYMENT_RESPONSE, settle.encoded);
2113
+ response.headers.set("Cache-Control", "private");
2114
+ const transaction = String(settle.result.transaction ?? "");
2115
+ const settledPayment = {
2116
+ ...payment,
2117
+ status: "settled",
2118
+ amount: billedAmount,
2119
+ ...transaction ? { transaction } : {}
2120
+ };
2121
+ return { ok: true, response, settledPayment };
2122
+ } catch (err) {
2123
+ reportSettleFailure(report, err, payment.network);
2124
+ return { ok: false, error: err, failMessage: "Settlement failed" };
1766
2125
  }
1767
- };
1768
- function getRequirementNetwork(requirements, fallback) {
1769
- const network = requirements?.network;
1770
- return typeof network === "string" ? network : fallback;
1771
2126
  }
1772
- function getRequirementRecipient(requirements) {
1773
- const payTo = requirements?.payTo;
1774
- return typeof payTo === "string" ? payTo : void 0;
2127
+ async function buildX402ChallengeContribution(args) {
2128
+ const { request, routeEntry, body, price, extensions, deps, report } = args;
2129
+ if (!deps.x402Server) return {};
2130
+ const accepts = await resolveX402Accepts(
2131
+ request,
2132
+ routeEntry,
2133
+ deps.x402Accepts,
2134
+ deps.payeeAddress,
2135
+ body
2136
+ );
2137
+ const { encoded } = await buildX402Challenge({
2138
+ server: deps.x402Server,
2139
+ routeEntry,
2140
+ request,
2141
+ price,
2142
+ accepts,
2143
+ facilitatorsByNetwork: deps.x402FacilitatorsByNetwork,
2144
+ extensions,
2145
+ report
2146
+ });
2147
+ return { headers: { [HEADERS.X402_PAYMENT_REQUIRED]: encoded } };
2148
+ }
2149
+ function reportSettleFailure(report, err, network) {
2150
+ const facilitator = err ?? {};
2151
+ report("error", "Settlement failed", {
2152
+ error: err instanceof Error ? err.message : String(err),
2153
+ network,
2154
+ errorReason: facilitator.errorReason,
2155
+ facilitatorStatus: facilitator.response?.status,
2156
+ facilitatorBody: facilitator.response?.data ?? facilitator.response?.body
2157
+ });
1775
2158
  }
1776
2159
 
1777
2160
  // src/protocols/detect.ts
@@ -1805,23 +2188,75 @@ function getAllowedStrategies(allowed) {
1805
2188
  return allowed.map((name) => STRATEGIES[name]);
1806
2189
  }
1807
2190
 
1808
- // src/pipeline/challenge.ts
2191
+ // src/pipeline/flows/build402.ts
1809
2192
  import { NextResponse as NextResponse4 } from "next/server";
1810
- async function build402(ctx, pricing, body) {
1811
- let challengePrice;
2193
+
2194
+ // src/pipeline/challenge-extensions.ts
2195
+ async function buildChallengeExtensions(ctx) {
2196
+ const { routeEntry } = ctx;
2197
+ let extensions;
1812
2198
  try {
1813
- challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
2199
+ const { z } = await import("zod");
2200
+ const { declareDiscoveryExtension } = await import("@x402/extensions/bazaar");
2201
+ const toJSON = (schema) => z.toJSONSchema(schema, {
2202
+ target: "draft-2020-12",
2203
+ unrepresentable: "any"
2204
+ });
2205
+ const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
2206
+ const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
2207
+ if (inputSchema) {
2208
+ const config = {
2209
+ method: routeEntry.method,
2210
+ bodyType: routeEntry.bodySchema ? "json" : void 0,
2211
+ inputSchema
2212
+ };
2213
+ if (routeEntry.inputExample !== void 0) {
2214
+ config.input = routeEntry.inputExample;
2215
+ }
2216
+ if (outputSchema && routeEntry.outputExample !== void 0) {
2217
+ config.output = { schema: outputSchema, example: routeEntry.outputExample };
2218
+ }
2219
+ extensions = declareDiscoveryExtension(config);
2220
+ }
1814
2221
  } catch (err) {
1815
- const message = errorMessage(err, "Price calculation failed");
1816
- const errorResponse = NextResponse4.json(
1817
- { success: false, error: message },
1818
- { status: errorStatus(err, 500) }
1819
- );
1820
- firePluginResponse(ctx, errorResponse);
1821
- return errorResponse;
1822
- }
1823
- const extensions = await buildChallengeExtensions(ctx);
1824
- const response = new NextResponse4(null, {
2222
+ firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
2223
+ level: "warn",
2224
+ message: `Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`,
2225
+ route: routeEntry.key
2226
+ });
2227
+ }
2228
+ if (routeEntry.siwxEnabled) {
2229
+ try {
2230
+ const siwxExtension = await buildSIWXExtension();
2231
+ if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
2232
+ extensions = {
2233
+ ...extensions ?? {},
2234
+ ...siwxExtension
2235
+ };
2236
+ }
2237
+ } catch {
2238
+ }
2239
+ }
2240
+ return extensions;
2241
+ }
2242
+
2243
+ // src/pipeline/flows/build402.ts
2244
+ async function build402(ctx, pricing, body, failure) {
2245
+ let challengePrice;
2246
+ try {
2247
+ challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
2248
+ } catch (err) {
2249
+ const message = errorMessage(err, "Price calculation failed");
2250
+ const errorResponse = NextResponse4.json(
2251
+ { success: false, error: message },
2252
+ { status: errorStatus(err, 500) }
2253
+ );
2254
+ firePluginResponse(ctx, errorResponse);
2255
+ return errorResponse;
2256
+ }
2257
+ const extensions = await buildChallengeExtensions(ctx);
2258
+ const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
2259
+ const response = new NextResponse4(responseBody, {
1825
2260
  status: 402,
1826
2261
  headers: {
1827
2262
  "Content-Type": "application/json",
@@ -1836,7 +2271,8 @@ async function build402(ctx, pricing, body) {
1836
2271
  body,
1837
2272
  price: challengePrice,
1838
2273
  extensions,
1839
- deps: ctx.deps
2274
+ deps: ctx.deps,
2275
+ report: ctx.report
1840
2276
  });
1841
2277
  if (contribution.headers) {
1842
2278
  for (const [name, value] of Object.entries(contribution.headers)) {
@@ -1845,11 +2281,7 @@ async function build402(ctx, pricing, body) {
1845
2281
  }
1846
2282
  } catch (err) {
1847
2283
  const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
1848
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
1849
- level: "critical",
1850
- message,
1851
- route: ctx.routeEntry.key
1852
- });
2284
+ ctx.report("critical", message);
1853
2285
  if (strategy.protocol === "x402") {
1854
2286
  const errorResponse = NextResponse4.json(
1855
2287
  { success: false, error: message },
@@ -1863,97 +2295,313 @@ async function build402(ctx, pricing, body) {
1863
2295
  firePluginResponse(ctx, response);
1864
2296
  return response;
1865
2297
  }
1866
- async function buildChallengeExtensions(ctx) {
1867
- const { routeEntry } = ctx;
1868
- let extensions;
2298
+
2299
+ // src/pipeline/flows/dynamic/dynamic-body-and-price.ts
2300
+ async function resolveDynamicBodyAndPrice(args) {
2301
+ const { ctx, pricing, skipBody } = args;
2302
+ if (skipBody) {
2303
+ return {
2304
+ ok: true,
2305
+ parsedBody: void 0,
2306
+ price: surrogatePriceForSkippedBody(ctx.routeEntry)
2307
+ };
2308
+ }
2309
+ const body = await parseBody(ctx);
2310
+ if (!body.ok) return { ok: false, response: body.response };
2311
+ const validateErr = await runValidate(ctx, body.data);
2312
+ if (validateErr) {
2313
+ return { ok: false, response: validateErr };
2314
+ }
2315
+ if (!pricing) {
2316
+ return { ok: false, response: fail(ctx, 500, "Pricing not configured", body.data) };
2317
+ }
1869
2318
  try {
1870
- const { z } = await import("zod");
1871
- const { declareDiscoveryExtension } = await import("@x402/extensions/bazaar");
1872
- const toJSON = (schema) => z.toJSONSchema(schema, {
1873
- target: "draft-2020-12",
1874
- unrepresentable: "any"
1875
- });
1876
- const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
1877
- const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
1878
- if (inputSchema) {
1879
- const config = {
1880
- method: routeEntry.method,
1881
- bodyType: routeEntry.bodySchema ? "json" : void 0,
1882
- inputSchema
1883
- };
1884
- if (routeEntry.inputExample !== void 0) {
1885
- config.input = routeEntry.inputExample;
1886
- }
1887
- if (outputSchema && routeEntry.outputExample !== void 0) {
1888
- config.output = { schema: outputSchema, example: routeEntry.outputExample };
1889
- }
1890
- extensions = declareDiscoveryExtension(config);
1891
- }
2319
+ const price = await pricing.quote(body.data);
2320
+ return { ok: true, parsedBody: body.data, price };
1892
2321
  } catch (err) {
1893
- firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
1894
- level: "warn",
1895
- message: `Bazaar schema generation failed: ${err instanceof Error ? err.message : String(err)}`,
1896
- route: routeEntry.key
1897
- });
2322
+ return {
2323
+ ok: false,
2324
+ response: fail(
2325
+ ctx,
2326
+ errorStatus(err, 500),
2327
+ errorMessage(err, "Price calculation failed"),
2328
+ body.data
2329
+ )
2330
+ };
1898
2331
  }
1899
- if (routeEntry.siwxEnabled) {
1900
- try {
1901
- const siwxExtension = await buildSIWXExtension();
1902
- if (siwxExtension && typeof siwxExtension === "object" && !Array.isArray(siwxExtension)) {
1903
- extensions = {
1904
- ...extensions ?? {},
1905
- ...siwxExtension
1906
- };
1907
- }
1908
- } catch {
2332
+ }
2333
+ function surrogatePriceForSkippedBody(routeEntry) {
2334
+ return routeEntry.maxPrice ?? routeEntry.minPrice ?? "0";
2335
+ }
2336
+
2337
+ // src/pipeline/flows/dynamic/dynamic-channel-mgmt.ts
2338
+ import { NextResponse as NextResponse5 } from "next/server";
2339
+ async function runDynamicChannelMgmtFlow(args) {
2340
+ const { ctx, strategy, account, pricing, skipBody } = args;
2341
+ const { request, routeEntry, deps, report } = ctx;
2342
+ const bodyAndPrice = await resolveDynamicBodyAndPrice({ ctx, pricing, skipBody });
2343
+ if (!bodyAndPrice.ok) return bodyAndPrice.response;
2344
+ const { parsedBody, price } = bodyAndPrice;
2345
+ const verifyOutcome = await strategy.verify({
2346
+ request,
2347
+ body: parsedBody,
2348
+ price,
2349
+ routeEntry,
2350
+ deps,
2351
+ report
2352
+ });
2353
+ if (verifyOutcome.ok === false) {
2354
+ if (verifyOutcome.kind === "config") {
2355
+ return fail(ctx, 500, verifyOutcome.message, parsedBody);
1909
2356
  }
2357
+ return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
1910
2358
  }
1911
- return extensions;
2359
+ ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2360
+ firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
2361
+ protocol: strategy.protocol,
2362
+ payer: verifyOutcome.wallet,
2363
+ amount: price,
2364
+ network: verifyOutcome.payment.network
2365
+ });
2366
+ const synthetic = new NextResponse5(null, { status: 200 });
2367
+ const settleScope = {
2368
+ wallet: verifyOutcome.wallet,
2369
+ account,
2370
+ body: parsedBody,
2371
+ payment: verifyOutcome.payment,
2372
+ response: synthetic,
2373
+ rawResult: void 0
2374
+ };
2375
+ const beforeErr = await runBeforeSettle(ctx, settleScope);
2376
+ if (beforeErr) return beforeErr;
2377
+ return settleAndFinalizeRequest({
2378
+ ctx,
2379
+ strategy,
2380
+ verifyOutcome,
2381
+ scope: settleScope,
2382
+ rawResult: void 0,
2383
+ body: parsedBody,
2384
+ billedAmount: "0",
2385
+ onSettleError: async (error, failMessage) => {
2386
+ await runSettlementError(ctx, settleScope, error, "settle");
2387
+ report("critical", `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`);
2388
+ }
2389
+ });
1912
2390
  }
1913
2391
 
1914
- // src/pipeline/flows/paid.ts
1915
- async function runPaidFlow(ctx) {
1916
- const { request, routeEntry, deps } = ctx;
1917
- let account = void 0;
1918
- if (routeEntry.apiKeyResolver) {
1919
- const apiKeyResult = await verifyApiKey(request, routeEntry.apiKeyResolver);
1920
- if (!apiKeyResult.valid) return fail(ctx, 401, "Invalid or missing API key");
1921
- account = apiKeyResult.account;
1922
- firePluginHook(deps.plugin, "onAuthVerified", ctx.pluginCtx, {
1923
- authMode: "apiKey",
1924
- wallet: null,
1925
- route: routeEntry.key,
1926
- account
2392
+ // src/pipeline/flows/dynamic/dynamic-invoke.ts
2393
+ import { NextResponse as NextResponse6 } from "next/server";
2394
+
2395
+ // src/pricing/atomic.ts
2396
+ var USDC_DECIMALS = 6;
2397
+ function decimalToAtomic(amount) {
2398
+ const m = /^(\d+)(?:\.(\d+))?$/.exec(amount.trim());
2399
+ if (!m) {
2400
+ throw Object.assign(new Error(`'${amount}' is not a valid decimal-dollar string`), {
2401
+ status: 400
1927
2402
  });
1928
2403
  }
1929
- const alertFn = (level, message, meta) => {
1930
- firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
1931
- level,
1932
- message,
1933
- route: routeEntry.key,
1934
- meta
1935
- });
2404
+ const whole = m[1];
2405
+ const fraction = (m[2] ?? "").slice(0, USDC_DECIMALS).padEnd(USDC_DECIMALS, "0");
2406
+ return BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0");
2407
+ }
2408
+ function atomicToDecimal(atomic) {
2409
+ const whole = atomic / 10n ** BigInt(USDC_DECIMALS);
2410
+ const fraction = atomic % 10n ** BigInt(USDC_DECIMALS);
2411
+ if (fraction === 0n) return whole.toString();
2412
+ const fractionStr = fraction.toString().padStart(USDC_DECIMALS, "0").replace(/0+$/, "");
2413
+ return `${whole}.${fractionStr}`;
2414
+ }
2415
+
2416
+ // src/pricing/charge-context.ts
2417
+ function createChargeContext(args) {
2418
+ const { tickCost, maxPrice, route } = args;
2419
+ const tickAtomic = decimalToAtomic(tickCost);
2420
+ if (tickAtomic <= 0n) {
2421
+ throw new Error(`route '${route}': tickCost '${tickCost}' must be a positive decimal string`);
2422
+ }
2423
+ const capAtomic = maxPrice !== void 0 ? decimalToAtomic(maxPrice) : null;
2424
+ let ticks = 0;
2425
+ let atomic = 0n;
2426
+ let channelCharge = null;
2427
+ const charge = async () => {
2428
+ const nextAtomic = atomic + tickAtomic;
2429
+ if (capAtomic !== null && nextAtomic > capAtomic) {
2430
+ throw Object.assign(
2431
+ new Error(
2432
+ `route '${route}': charge() running total ($${atomicToDecimal(nextAtomic)}) exceeds maxPrice ($${atomicToDecimal(capAtomic)})`
2433
+ ),
2434
+ { status: 400, code: "CHARGE_OVER_CAP" }
2435
+ );
2436
+ }
2437
+ ticks += 1;
2438
+ atomic = nextAtomic;
2439
+ if (channelCharge) await channelCharge();
1936
2440
  };
2441
+ return {
2442
+ charge,
2443
+ bindChannelCharge: (fn) => {
2444
+ channelCharge = fn;
2445
+ },
2446
+ tickCount: () => ticks,
2447
+ atomicTotal: () => atomic
2448
+ };
2449
+ }
2450
+
2451
+ // src/pipeline/flows/dynamic/dynamic-invoke.ts
2452
+ async function invokeDynamic(ctx, wallet, account, body, payment) {
2453
+ const streaming = ctx.routeEntry.streaming === true;
2454
+ const chargeContext = streaming ? createChargeContext({
2455
+ tickCost: ctx.routeEntry.tickCost,
2456
+ maxPrice: ctx.routeEntry.maxPrice,
2457
+ route: ctx.routeEntry.key
2458
+ }) : null;
2459
+ const baseHandlerCtx = {
2460
+ body,
2461
+ query: parseQuery(ctx.request, ctx.routeEntry),
2462
+ request: ctx.request,
2463
+ requestId: ctx.meta.requestId,
2464
+ route: ctx.routeEntry.key,
2465
+ wallet,
2466
+ payment,
2467
+ account,
2468
+ alert(level, message, alertMeta) {
2469
+ firePluginHook(ctx.deps.plugin, "onAlert", ctx.pluginCtx, {
2470
+ level,
2471
+ message,
2472
+ route: ctx.routeEntry.key,
2473
+ meta: alertMeta
2474
+ });
2475
+ },
2476
+ setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
2477
+ };
2478
+ const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
2479
+ let returned;
2480
+ try {
2481
+ returned = ctx.handler(handlerCtx);
2482
+ } catch (error) {
2483
+ return errorResult2(error, chargeContext);
2484
+ }
2485
+ if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2486
+ if (!chargeContext) {
2487
+ return errorResult2(
2488
+ new HttpError(
2489
+ "route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
2490
+ 500
2491
+ ),
2492
+ null
2493
+ );
2494
+ }
2495
+ return {
2496
+ kind: "stream",
2497
+ source: returned,
2498
+ chargeContext
2499
+ };
2500
+ }
2501
+ let rawResult;
2502
+ try {
2503
+ rawResult = await returned;
2504
+ } catch (error) {
2505
+ return errorResult2(error, chargeContext);
2506
+ }
2507
+ const response = rawResult instanceof Response ? rawResult : NextResponse6.json(rawResult);
2508
+ return { kind: "request", response, rawResult };
2509
+ }
2510
+ function errorResult2(error, chargeContext) {
2511
+ const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
2512
+ const message = error instanceof Error ? error.message : "Internal error";
2513
+ void chargeContext;
2514
+ return {
2515
+ kind: "request",
2516
+ response: NextResponse6.json({ success: false, error: message }, { status }),
2517
+ rawResult: void 0,
2518
+ handlerError: error
2519
+ };
2520
+ }
2521
+ function isAsyncIterable2(value) {
2522
+ return value != null && typeof value === "object" && Symbol.asyncIterator in value;
2523
+ }
2524
+ function isThenable2(value) {
2525
+ return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
2526
+ }
2527
+
2528
+ // src/pipeline/flows/dynamic/dynamic-preflight.ts
2529
+ function resolveDynamicPreflight(strategy, request, routeEntry) {
2530
+ const outcome = strategy.preflight?.(request, routeEntry) ?? null;
2531
+ return {
2532
+ skipBody: outcome?.skipBody ?? false,
2533
+ skipHandler: outcome?.skipHandler ?? false
2534
+ };
2535
+ }
2536
+
2537
+ // src/pipeline/flows/dynamic/dynamic-request.ts
2538
+ async function runDynamicRequestFlow(args) {
2539
+ const { ctx, strategy, verifyOutcome, account, body, result } = args;
2540
+ const { deps, routeEntry } = ctx;
2541
+ const settleScope = {
2542
+ wallet: verifyOutcome.wallet,
2543
+ account,
2544
+ body,
2545
+ payment: verifyOutcome.payment,
2546
+ response: result.response,
2547
+ rawResult: result.rawResult,
2548
+ handlerError: result.handlerError
2549
+ };
2550
+ if (result.response.status >= 400) {
2551
+ return finalize(ctx, result.response, result.rawResult, body);
2552
+ }
2553
+ const beforeErr = await runBeforeSettle(ctx, settleScope);
2554
+ if (beforeErr) return beforeErr;
2555
+ const billedAmount = routeEntry.tickCost;
2556
+ return settleAndFinalizeRequest({
2557
+ ctx,
2558
+ strategy,
2559
+ verifyOutcome,
2560
+ scope: settleScope,
2561
+ rawResult: result.rawResult,
2562
+ body,
2563
+ billedAmount,
2564
+ onSettleError: async (error, failMessage) => {
2565
+ await runSettlementError(ctx, settleScope, error, "settle");
2566
+ firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2567
+ level: "critical",
2568
+ message: `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
2569
+ route: routeEntry.key
2570
+ });
2571
+ }
2572
+ });
2573
+ }
2574
+
2575
+ // src/pipeline/flows/dynamic/dynamic-stream.ts
2576
+ async function runDynamicStreamFlow(args) {
2577
+ const { ctx, strategy, verifyOutcome, account, body, result } = args;
2578
+ return settleAndFinalizeStream({
2579
+ ctx,
2580
+ strategy,
2581
+ verifyOutcome,
2582
+ source: result.source,
2583
+ account,
2584
+ body,
2585
+ bindChannelCharge: result.chargeContext.bindChannelCharge
2586
+ });
2587
+ }
2588
+
2589
+ // src/pipeline/flows/dynamic/dynamic-paid.ts
2590
+ async function runDynamicPaidFlow(ctx) {
2591
+ const { request, routeEntry, deps, report } = ctx;
2592
+ const apiKeyGate = await runApiKeyGate(ctx);
2593
+ if (!apiKeyGate.ok) return apiKeyGate.response;
2594
+ const { account } = apiKeyGate;
1937
2595
  const pricing = selectPricing(routeEntry.pricing, {
1938
- alert: alertFn,
2596
+ alert: report,
1939
2597
  maxPrice: routeEntry.maxPrice,
1940
2598
  minPrice: routeEntry.minPrice,
1941
2599
  route: routeEntry.key
1942
2600
  });
1943
2601
  const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
1944
- let earlyBody = void 0;
1945
- if (shouldParseBodyEarly(incomingStrategy, routeEntry, pricing)) {
1946
- const earlyClone = request.clone();
1947
- const earlyResult = await parseBody(earlyClone, routeEntry);
1948
- if (earlyResult.ok) {
1949
- earlyBody = earlyResult.data;
1950
- const validateErr2 = await runValidate(ctx, earlyBody);
1951
- if (validateErr2) return validateErr2;
1952
- } else {
1953
- firePluginResponse(ctx, earlyResult.response);
1954
- return earlyResult.response;
1955
- }
1956
- }
2602
+ const earlyResolution = await resolveEarlyBody({ ctx, pricing, incomingStrategy });
2603
+ if (!earlyResolution.ok) return earlyResolution.response;
2604
+ const { earlyBody } = earlyResolution;
1957
2605
  const siwxFastPath = await trySiwxFastPath(ctx, account);
1958
2606
  if (siwxFastPath) return siwxFastPath;
1959
2607
  if (!incomingStrategy) {
@@ -1961,39 +2609,32 @@ async function runPaidFlow(ctx) {
1961
2609
  if (initError) return fail(ctx, 500, initError);
1962
2610
  return build402(ctx, pricing, earlyBody);
1963
2611
  }
1964
- const body = await parseBody(request, routeEntry);
1965
- if (!body.ok) {
1966
- firePluginResponse(ctx, body.response);
1967
- return body.response;
1968
- }
1969
- const validateErr = await runValidate(ctx, body.data);
1970
- if (validateErr) return validateErr;
1971
- if (!pricing) {
1972
- return fail(ctx, 500, "Pricing not configured", body.data);
1973
- }
1974
- let price;
1975
- try {
1976
- price = await pricing.quote(body.data);
1977
- } catch (err) {
1978
- return fail(
2612
+ const { skipBody, skipHandler } = resolveDynamicPreflight(incomingStrategy, request, routeEntry);
2613
+ if (skipHandler) {
2614
+ return runDynamicChannelMgmtFlow({
1979
2615
  ctx,
1980
- errorStatus(err, 500),
1981
- errorMessage(err, "Price calculation failed"),
1982
- body.data
1983
- );
2616
+ strategy: incomingStrategy,
2617
+ account,
2618
+ pricing,
2619
+ skipBody
2620
+ });
1984
2621
  }
2622
+ const bodyAndPrice = await resolveDynamicBodyAndPrice({ ctx, pricing, skipBody });
2623
+ if (!bodyAndPrice.ok) return bodyAndPrice.response;
2624
+ const { parsedBody, price } = bodyAndPrice;
1985
2625
  const verifyOutcome = await incomingStrategy.verify({
1986
2626
  request,
1987
- body: body.data,
2627
+ body: parsedBody,
1988
2628
  price,
1989
2629
  routeEntry,
1990
- deps
2630
+ deps,
2631
+ report
1991
2632
  });
1992
2633
  if (verifyOutcome.ok === false) {
1993
2634
  if (verifyOutcome.kind === "config") {
1994
- return fail(ctx, 500, verifyOutcome.message, body.data);
2635
+ return fail(ctx, 500, verifyOutcome.message, parsedBody);
1995
2636
  }
1996
- return build402(ctx, pricing, body.data);
2637
+ return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
1997
2638
  }
1998
2639
  ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
1999
2640
  firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
@@ -2002,11 +2643,71 @@ async function runPaidFlow(ctx) {
2002
2643
  amount: price,
2003
2644
  network: verifyOutcome.payment.network
2004
2645
  });
2005
- const result = await invoke(ctx, verifyOutcome.wallet, account, body.data, verifyOutcome.payment);
2646
+ const result = await invokeDynamic(
2647
+ ctx,
2648
+ verifyOutcome.wallet,
2649
+ account,
2650
+ parsedBody,
2651
+ verifyOutcome.payment
2652
+ );
2653
+ switch (result.kind) {
2654
+ case "stream":
2655
+ return runDynamicStreamFlow({
2656
+ ctx,
2657
+ strategy: incomingStrategy,
2658
+ verifyOutcome,
2659
+ account,
2660
+ body: parsedBody,
2661
+ result
2662
+ });
2663
+ case "request":
2664
+ return runDynamicRequestFlow({
2665
+ ctx,
2666
+ strategy: incomingStrategy,
2667
+ verifyOutcome,
2668
+ account,
2669
+ body: parsedBody,
2670
+ result
2671
+ });
2672
+ }
2673
+ }
2674
+
2675
+ // src/pipeline/flows/static/static-body-and-price.ts
2676
+ async function resolveStaticBodyAndPrice(args) {
2677
+ const { ctx, pricing } = args;
2678
+ const body = await parseBody(ctx);
2679
+ if (!body.ok) return { ok: false, response: body.response };
2680
+ const validateErr = await runValidate(ctx, body.data);
2681
+ if (validateErr) {
2682
+ return { ok: false, response: validateErr };
2683
+ }
2684
+ if (!pricing) {
2685
+ return { ok: false, response: fail(ctx, 500, "Pricing not configured", body.data) };
2686
+ }
2687
+ try {
2688
+ const price = await pricing.quote(body.data);
2689
+ return { ok: true, parsedBody: body.data, price };
2690
+ } catch (err) {
2691
+ return {
2692
+ ok: false,
2693
+ response: fail(
2694
+ ctx,
2695
+ errorStatus(err, 500),
2696
+ errorMessage(err, "Price calculation failed"),
2697
+ body.data
2698
+ )
2699
+ };
2700
+ }
2701
+ }
2702
+
2703
+ // src/pipeline/flows/static/static-request.ts
2704
+ async function runStaticRequestFlow(args) {
2705
+ const { ctx, strategy, verifyOutcome, account, body, price, result } = args;
2706
+ const { deps, routeEntry } = ctx;
2006
2707
  const settleScope = {
2007
2708
  wallet: verifyOutcome.wallet,
2008
2709
  account,
2009
- body: body.data,
2710
+ body,
2010
2711
  payment: verifyOutcome.payment,
2011
2712
  response: result.response,
2012
2713
  rawResult: result.rawResult,
@@ -2016,44 +2717,198 @@ async function runPaidFlow(ctx) {
2016
2717
  if (result.response.status >= 400) {
2017
2718
  const settledScope = settleScope;
2018
2719
  await runSettledHandlerError(ctx, settledScope);
2019
- return finalize(ctx, result.response, result.rawResult, body.data);
2720
+ return finalize(ctx, result.response, result.rawResult, body);
2020
2721
  }
2021
- return settleAndFinalize({
2722
+ return settleAndFinalizeRequest({
2022
2723
  ctx,
2023
- strategy: incomingStrategy,
2724
+ strategy,
2024
2725
  verifyOutcome,
2025
2726
  scope: settleScope,
2026
2727
  rawResult: result.rawResult,
2027
- body: body.data
2728
+ body,
2729
+ billedAmount: price
2028
2730
  });
2029
2731
  }
2030
2732
  if (result.response.status >= 400) {
2031
- return finalize(ctx, result.response, result.rawResult, body.data);
2733
+ return finalize(ctx, result.response, result.rawResult, body);
2032
2734
  }
2033
2735
  const beforeErr = await runBeforeSettle(ctx, settleScope);
2034
2736
  if (beforeErr) return beforeErr;
2035
- return settleAndFinalize({
2737
+ return settleAndFinalizeRequest({
2036
2738
  ctx,
2037
- strategy: incomingStrategy,
2739
+ strategy,
2038
2740
  verifyOutcome,
2039
2741
  scope: settleScope,
2040
2742
  rawResult: result.rawResult,
2041
- body: body.data,
2743
+ body,
2744
+ billedAmount: price,
2042
2745
  onSettleError: async (error, failMessage) => {
2043
2746
  await runSettlementError(ctx, settleScope, error, "settle");
2044
2747
  firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2045
2748
  level: "critical",
2046
- message: `${incomingStrategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
2749
+ message: `${strategy.protocol} ${failMessage}: ${errorMessage(error, "unknown")}`,
2047
2750
  route: routeEntry.key
2048
2751
  });
2049
2752
  }
2050
2753
  });
2051
2754
  }
2052
2755
 
2756
+ // src/pipeline/flows/static/static-paid.ts
2757
+ async function runStaticPaidFlow(ctx) {
2758
+ const { request, routeEntry, deps, report } = ctx;
2759
+ const apiKeyGate = await runApiKeyGate(ctx);
2760
+ if (!apiKeyGate.ok) return apiKeyGate.response;
2761
+ const { account } = apiKeyGate;
2762
+ const pricing = selectPricing(routeEntry.pricing, {
2763
+ alert: report,
2764
+ maxPrice: routeEntry.maxPrice,
2765
+ minPrice: routeEntry.minPrice,
2766
+ route: routeEntry.key
2767
+ });
2768
+ const incomingStrategy = selectIncomingStrategy(request, routeEntry.protocols);
2769
+ const earlyResolution = await resolveEarlyBody({ ctx, pricing, incomingStrategy });
2770
+ if (!earlyResolution.ok) return earlyResolution.response;
2771
+ const { earlyBody } = earlyResolution;
2772
+ const siwxFastPath = await trySiwxFastPath(ctx, account);
2773
+ if (siwxFastPath) return siwxFastPath;
2774
+ if (!incomingStrategy) {
2775
+ const initError = protocolInitError(routeEntry, deps);
2776
+ if (initError) return fail(ctx, 500, initError);
2777
+ return build402(ctx, pricing, earlyBody);
2778
+ }
2779
+ const bodyAndPrice = await resolveStaticBodyAndPrice({ ctx, pricing });
2780
+ if (!bodyAndPrice.ok) return bodyAndPrice.response;
2781
+ const { parsedBody, price } = bodyAndPrice;
2782
+ const verifyOutcome = await incomingStrategy.verify({
2783
+ request,
2784
+ body: parsedBody,
2785
+ price,
2786
+ routeEntry,
2787
+ deps,
2788
+ report
2789
+ });
2790
+ if (verifyOutcome.ok === false) {
2791
+ if (verifyOutcome.kind === "config") {
2792
+ return fail(ctx, 500, verifyOutcome.message, parsedBody);
2793
+ }
2794
+ return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
2795
+ }
2796
+ ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2797
+ firePluginHook(deps.plugin, "onPaymentVerified", ctx.pluginCtx, {
2798
+ protocol: incomingStrategy.protocol,
2799
+ payer: verifyOutcome.wallet,
2800
+ amount: price,
2801
+ network: verifyOutcome.payment.network
2802
+ });
2803
+ const result = await invokePaidStatic(
2804
+ ctx,
2805
+ verifyOutcome.wallet,
2806
+ account,
2807
+ parsedBody,
2808
+ verifyOutcome.payment
2809
+ );
2810
+ return runStaticRequestFlow({
2811
+ ctx,
2812
+ strategy: incomingStrategy,
2813
+ verifyOutcome,
2814
+ account,
2815
+ body: parsedBody,
2816
+ price,
2817
+ result
2818
+ });
2819
+ }
2820
+
2821
+ // src/pipeline/flows/paid.ts
2822
+ async function runPaidFlow(ctx) {
2823
+ const dynamicPrice = ctx.routeEntry.dynamicPrice ?? false;
2824
+ switch (dynamicPrice) {
2825
+ case true:
2826
+ return runDynamicPaidFlow(ctx);
2827
+ case false:
2828
+ return runStaticPaidFlow(ctx);
2829
+ }
2830
+ }
2831
+
2053
2832
  // src/pipeline/flows/siwx-only.ts
2054
- import { NextResponse as NextResponse5 } from "next/server";
2833
+ import { NextResponse as NextResponse7 } from "next/server";
2834
+
2835
+ // src/kv-store/client.ts
2836
+ function restKvStore(url, token) {
2837
+ const base = url.replace(/\/+$/, "");
2838
+ const authHeader = { Authorization: `Bearer ${token}` };
2839
+ const jsonHeaders = { ...authHeader, "Content-Type": "application/json" };
2840
+ async function exec(command) {
2841
+ const res = await fetch(base, {
2842
+ method: "POST",
2843
+ headers: jsonHeaders,
2844
+ body: JSON.stringify(command)
2845
+ });
2846
+ if (!res.ok) {
2847
+ throw new Error(`[kv-store] ${command[0]} ${command[1] ?? ""}: ${res.status}`);
2848
+ }
2849
+ const body = await res.json();
2850
+ if (body.error) throw new Error(`[kv-store] ${command[0]}: ${body.error}`);
2851
+ return body.result ?? null;
2852
+ }
2853
+ async function get(key) {
2854
+ const res = await fetch(`${base}/get/${encodeURIComponent(key)}`, { headers: authHeader });
2855
+ if (!res.ok) throw new Error(`[kv-store] GET ${key}: ${res.status}`);
2856
+ const { result } = await res.json();
2857
+ return result ?? null;
2858
+ }
2859
+ async function set(key, value) {
2860
+ await exec(["SET", key, JSON.stringify(value)]);
2861
+ }
2862
+ async function del(key) {
2863
+ await exec(["DEL", key]);
2864
+ }
2865
+ async function setNxEx(key, value, ttlSeconds) {
2866
+ const result = await exec(["SET", key, JSON.stringify(value), "EX", ttlSeconds, "NX"]);
2867
+ return result === "OK";
2868
+ }
2869
+ async function sadd(key, member) {
2870
+ await exec(["SADD", key, member]);
2871
+ }
2872
+ async function sismember(key, member) {
2873
+ const result = await exec(["SISMEMBER", key, member]);
2874
+ return result === 1;
2875
+ }
2876
+ async function update(key, fn) {
2877
+ const current = await get(key);
2878
+ const change = fn(current);
2879
+ if (change.op === "set") await set(key, change.value);
2880
+ if (change.op === "delete") await del(key);
2881
+ return change.result;
2882
+ }
2883
+ return { get, set, del, setNxEx, sadd, sismember, update };
2884
+ }
2885
+ function isRestConfig(input) {
2886
+ return typeof input === "object" && input !== null && typeof input.url === "string" && typeof input.token === "string" && typeof input.get !== "function";
2887
+ }
2888
+ function resolveKvStore(input, env = process.env) {
2889
+ if (input) {
2890
+ if (isRestConfig(input)) return restKvStore(input.url, input.token);
2891
+ return input;
2892
+ }
2893
+ const url = env.KV_REST_API_URL;
2894
+ const token = env.KV_REST_API_TOKEN;
2895
+ if (url && token) return restKvStore(url, token);
2896
+ return void 0;
2897
+ }
2898
+ function withPrefix(kv, prefix) {
2899
+ const k = (key) => `${prefix}${key}`;
2900
+ return {
2901
+ get: (key) => kv.get(k(key)),
2902
+ set: (key, value) => kv.set(k(key), value),
2903
+ del: (key) => kv.del(k(key)),
2904
+ setNxEx: (key, value, ttl) => kv.setNxEx(k(key), value, ttl),
2905
+ sadd: (key, member) => kv.sadd(k(key), member),
2906
+ sismember: (key, member) => kv.sismember(k(key), member),
2907
+ update: (key, fn) => kv.update(k(key), fn)
2908
+ };
2909
+ }
2055
2910
 
2056
- // src/auth/nonce.ts
2911
+ // src/kv-store/nonce.ts
2057
2912
  var SIWX_CHALLENGE_EXPIRY_MS = 5 * 60 * 1e3;
2058
2913
  var MemoryNonceStore = class {
2059
2914
  seen = /* @__PURE__ */ new Map();
@@ -2070,48 +2925,59 @@ var MemoryNonceStore = class {
2070
2925
  }
2071
2926
  }
2072
2927
  };
2073
- function detectRedisClientType(client) {
2074
- if (!client || typeof client !== "object") {
2075
- throw new Error(
2076
- "createRedisNonceStore requires a Redis client. Supported: @upstash/redis, ioredis. Pass your Redis client instance as the first argument."
2077
- );
2078
- }
2079
- if ("options" in client && "status" in client) {
2080
- return "ioredis";
2081
- }
2082
- const constructor = client.constructor?.name;
2083
- if (constructor === "Redis" && "url" in client) {
2084
- return "upstash";
2928
+ function createKvNonceStore(kv, options) {
2929
+ const prefix = options?.prefix ?? "siwx:nonce:";
2930
+ const ttlSeconds = Math.ceil((options?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
2931
+ return {
2932
+ async check(nonce) {
2933
+ return kv.setNxEx(`${prefix}${nonce}`, 1, ttlSeconds);
2934
+ }
2935
+ };
2936
+ }
2937
+
2938
+ // src/kv-store/entitlement.ts
2939
+ var MemoryEntitlementStore = class {
2940
+ routeToWallets = /* @__PURE__ */ new Map();
2941
+ async has(route, wallet) {
2942
+ const wallets = this.routeToWallets.get(route);
2943
+ if (!wallets) return false;
2944
+ return wallets.has(normalizeWalletAddress(wallet));
2085
2945
  }
2086
- if (typeof client.set === "function") {
2087
- return "upstash";
2946
+ async grant(route, wallet) {
2947
+ const normalized = normalizeWalletAddress(wallet);
2948
+ let wallets = this.routeToWallets.get(route);
2949
+ if (!wallets) {
2950
+ wallets = /* @__PURE__ */ new Set();
2951
+ this.routeToWallets.set(route, wallets);
2952
+ }
2953
+ wallets.add(normalized);
2088
2954
  }
2089
- throw new Error(
2090
- "Unrecognized Redis client. Supported: @upstash/redis, ioredis. If using a different client, implement NonceStore interface directly."
2091
- );
2092
- }
2093
- function createRedisNonceStore(client, opts) {
2094
- const prefix = opts?.prefix ?? "siwx:nonce:";
2095
- const ttlSeconds = Math.ceil((opts?.ttlMs ?? SIWX_CHALLENGE_EXPIRY_MS) / 1e3);
2096
- const clientType = detectRedisClientType(client);
2955
+ };
2956
+ function createKvEntitlementStore(kv, options) {
2957
+ const prefix = options?.prefix ?? "siwx:ent:";
2097
2958
  return {
2098
- async check(nonce) {
2099
- const key = `${prefix}${nonce}`;
2100
- if (clientType === "upstash") {
2101
- const redis = client;
2102
- const result = await redis.set(key, "1", { ex: ttlSeconds, nx: true });
2103
- return result !== null;
2104
- }
2105
- if (clientType === "ioredis") {
2106
- const redis = client;
2107
- const result = await redis.set(key, "1", "EX", ttlSeconds, "NX");
2108
- return result === "OK";
2109
- }
2110
- throw new Error("Unknown Redis client type");
2959
+ async has(route, wallet) {
2960
+ return kv.sismember(`${prefix}${route}`, normalizeWalletAddress(wallet));
2961
+ },
2962
+ async grant(route, wallet) {
2963
+ await kv.sadd(`${prefix}${route}`, normalizeWalletAddress(wallet));
2111
2964
  }
2112
2965
  };
2113
2966
  }
2114
2967
 
2968
+ // src/kv-store/mpp.ts
2969
+ async function createKvMppStore(kv, options) {
2970
+ const prefix = options?.prefix ?? "mpp:";
2971
+ const namespaced = withPrefix(kv, prefix);
2972
+ const { Store: StoreNs } = await import("mppx");
2973
+ return StoreNs.upstash({
2974
+ get: (key) => namespaced.get(key),
2975
+ set: (key, value) => namespaced.set(key, value),
2976
+ del: (key) => namespaced.del(key),
2977
+ update: (key, fn) => namespaced.update(key, fn)
2978
+ });
2979
+ }
2980
+
2115
2981
  // src/protocols/mpp/siwx-mode.ts
2116
2982
  import { Credential as Credential2 } from "mppx";
2117
2983
  async function verifyMppSiwx(request, mppx) {
@@ -2130,12 +2996,11 @@ async function runSiwxOnlyFlow(ctx) {
2130
2996
  const { request, routeEntry, deps } = ctx;
2131
2997
  if (routeEntry.validateFn && routeEntry.bodySchema && !request.headers.get(HEADERS.SIWX)) {
2132
2998
  const earlyClone = request.clone();
2133
- const earlyBody = await parseBody(earlyClone, routeEntry);
2999
+ const earlyBody = await parseBody(ctx, earlyClone);
2134
3000
  if (earlyBody.ok) {
2135
3001
  const validateErr = await runValidate(ctx, earlyBody.data);
2136
3002
  if (validateErr) return validateErr;
2137
3003
  } else {
2138
- firePluginResponse(ctx, earlyBody.response);
2139
3004
  return earlyBody.response;
2140
3005
  }
2141
3006
  }
@@ -2147,11 +3012,7 @@ async function runSiwxOnlyFlow(ctx) {
2147
3012
  mppSiwxResult = await verifyMppSiwx(request, deps.mppx);
2148
3013
  } catch (err) {
2149
3014
  const message = err instanceof Error ? err.message : String(err);
2150
- firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2151
- level: "critical",
2152
- message: `MPP SIWX verification failed: ${message}`,
2153
- route: routeEntry.key
2154
- });
3015
+ ctx.report("critical", `MPP SIWX verification failed: ${message}`);
2155
3016
  return fail(ctx, 500, `MPP SIWX verification failed: ${message}`);
2156
3017
  }
2157
3018
  if (mppSiwxResult.valid) {
@@ -2173,7 +3034,7 @@ async function runSiwxOnlyFlow(ctx) {
2173
3034
  }
2174
3035
  const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
2175
3036
  if (!siwx.valid) {
2176
- const response = NextResponse5.json(
3037
+ const response = NextResponse7.json(
2177
3038
  { error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
2178
3039
  { status: 402 }
2179
3040
  );
@@ -2223,7 +3084,6 @@ async function buildSiwxChallenge(ctx) {
2223
3084
  extensions: {
2224
3085
  "sign-in-with-x": {
2225
3086
  info: siwxInfo,
2226
- // Required by MCP tools at the top level for chain detection.
2227
3087
  supportedChains,
2228
3088
  ...siwxSchema ? { schema: siwxSchema } : {}
2229
3089
  }
@@ -2234,13 +3094,12 @@ async function buildSiwxChallenge(ctx) {
2234
3094
  const { encodePaymentRequiredHeader } = await import("@x402/core/http");
2235
3095
  encoded = encodePaymentRequiredHeader(paymentRequired);
2236
3096
  } catch (err) {
2237
- firePluginHook(deps.plugin, "onAlert", ctx.pluginCtx, {
2238
- level: "warn",
2239
- message: `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`,
2240
- route: routeEntry.key
2241
- });
3097
+ ctx.report(
3098
+ "warn",
3099
+ `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`
3100
+ );
2242
3101
  }
2243
- const response = new NextResponse5(JSON.stringify(paymentRequired), {
3102
+ const response = new NextResponse7(JSON.stringify(paymentRequired), {
2244
3103
  status: 402,
2245
3104
  headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
2246
3105
  });
@@ -2346,6 +3205,12 @@ var RouteBuilder = class {
2346
3205
  /** @internal */
2347
3206
  _minPrice;
2348
3207
  /** @internal */
3208
+ _dynamicPrice = false;
3209
+ /** @internal */
3210
+ _tickCost;
3211
+ /** @internal */
3212
+ _unitType;
3213
+ /** @internal */
2349
3214
  _payTo;
2350
3215
  /** @internal */
2351
3216
  _bodySchema;
@@ -2390,7 +3255,8 @@ var RouteBuilder = class {
2390
3255
  next._protocols = [...this._protocols];
2391
3256
  return next;
2392
3257
  }
2393
- paid(pricing, options) {
3258
+ paid(pricingOrOptions, options) {
3259
+ const { pricing, resolvedOptions } = resolvePaidArgs(this._key, pricingOrOptions, options);
2394
3260
  if (this._authMode === "unprotected") {
2395
3261
  throw new Error(
2396
3262
  `route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
@@ -2404,16 +3270,24 @@ var RouteBuilder = class {
2404
3270
  const next = this.fork();
2405
3271
  next._authMode = "paid";
2406
3272
  next._pricing = pricing;
2407
- if (options?.protocols) {
2408
- next._protocols = [...options.protocols];
3273
+ if (resolvedOptions?.protocols) {
3274
+ next._protocols = [...resolvedOptions.protocols];
2409
3275
  } else if (next._protocols.length === 0) {
2410
3276
  next._protocols = ["x402"];
2411
3277
  }
2412
- if (options?.maxPrice) next._maxPrice = options.maxPrice;
2413
- if (options?.minPrice) next._minPrice = options.minPrice;
2414
- if (options?.payTo) next._payTo = options.payTo;
2415
- if (options?.mpp) next._mppInfo = options.mpp;
3278
+ if (resolvedOptions?.maxPrice) next._maxPrice = resolvedOptions.maxPrice;
3279
+ if (resolvedOptions?.minPrice) next._minPrice = resolvedOptions.minPrice;
3280
+ if (resolvedOptions?.payTo) next._payTo = resolvedOptions.payTo;
3281
+ if (resolvedOptions?.mpp) next._mppInfo = resolvedOptions.mpp;
3282
+ if (resolvedOptions?.dynamic) next._dynamicPrice = true;
3283
+ if (resolvedOptions?.tickCost) next._tickCost = resolvedOptions.tickCost;
3284
+ if (resolvedOptions?.unitType) next._unitType = resolvedOptions.unitType;
2416
3285
  if (typeof pricing === "object" && "tiers" in pricing) {
3286
+ if (next._dynamicPrice) {
3287
+ throw new Error(
3288
+ `route '${this._key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
3289
+ );
3290
+ }
2417
3291
  for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
2418
3292
  if (!tierKey) {
2419
3293
  throw new Error(`route '${this._key}': tier key cannot be empty`);
@@ -2426,16 +3300,40 @@ var RouteBuilder = class {
2426
3300
  }
2427
3301
  }
2428
3302
  }
2429
- if (options?.maxPrice !== void 0) {
2430
- const parsed = parseFloat(options.maxPrice);
3303
+ if (resolvedOptions?.maxPrice !== void 0) {
3304
+ const parsed = parseFloat(resolvedOptions.maxPrice);
2431
3305
  if (isNaN(parsed) || parsed <= 0) {
2432
3306
  throw new Error(
2433
- `route '${this._key}': maxPrice '${options.maxPrice}' must be a positive decimal string`
3307
+ `route '${this._key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
2434
3308
  );
2435
3309
  }
2436
3310
  }
3311
+ if (resolvedOptions?.tickCost !== void 0) {
3312
+ const parsed = parseFloat(resolvedOptions.tickCost);
3313
+ if (isNaN(parsed) || parsed <= 0) {
3314
+ throw new Error(
3315
+ `route '${this._key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
3316
+ );
3317
+ }
3318
+ }
3319
+ if (next._dynamicPrice && !next._maxPrice) {
3320
+ throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires maxPrice`);
3321
+ }
3322
+ if (next._dynamicPrice && !next._tickCost) {
3323
+ throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires tickCost`);
3324
+ }
2437
3325
  return next;
2438
3326
  }
3327
+ /**
3328
+ * Require Sign-In-with-X wallet identity on this route — clients prove
3329
+ * control of a wallet via a signed challenge. Combine with `.paid()` to gate
3330
+ * a paid route on a verified wallet identity.
3331
+ *
3332
+ * @example
3333
+ * ```ts
3334
+ * router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
3335
+ * ```
3336
+ */
2439
3337
  siwx() {
2440
3338
  if (this._authMode === "unprotected") {
2441
3339
  throw new Error(
@@ -2458,6 +3356,19 @@ var RouteBuilder = class {
2458
3356
  next._protocols = [];
2459
3357
  return next;
2460
3358
  }
3359
+ /**
3360
+ * Require an `X-API-Key` header (or `Authorization: Bearer <key>`); the
3361
+ * resolver returns the account record, or `null` for 401. Composes with
3362
+ * `.paid()` — key is checked first, payment second.
3363
+ *
3364
+ * @example
3365
+ * ```ts
3366
+ * router
3367
+ * .route('admin/users')
3368
+ * .apiKey(async (key) => db.admin.findByKey(key))
3369
+ * .handler(async ({ account }) => db.user.list(account.orgId));
3370
+ * ```
3371
+ */
2461
3372
  apiKey(resolver) {
2462
3373
  if (this._siwxEnabled) {
2463
3374
  throw new Error(
@@ -2469,6 +3380,15 @@ var RouteBuilder = class {
2469
3380
  next._apiKeyResolver = resolver;
2470
3381
  return next;
2471
3382
  }
3383
+ /**
3384
+ * Mark the route as public — no auth, no payment, no SIWX. The handler
3385
+ * receives `null` for `wallet`, `payment`, and `account`.
3386
+ *
3387
+ * @example
3388
+ * ```ts
3389
+ * router.route('health').unprotected().handler(async () => ({ status: 'ok' }));
3390
+ * ```
3391
+ */
2472
3392
  unprotected() {
2473
3393
  if (this._authMode && this._authMode !== "unprotected") {
2474
3394
  throw new Error(
@@ -2485,60 +3405,82 @@ var RouteBuilder = class {
2485
3405
  next._protocols = [];
2486
3406
  return next;
2487
3407
  }
2488
- // -------------------------------------------------------------------------
2489
- // Provider monitoring
2490
- // -------------------------------------------------------------------------
3408
+ /**
3409
+ * Tag the route with an upstream provider for discovery and provider-side
3410
+ * monitoring. The provider name and config surface in `well-known` and
3411
+ * OpenAPI output.
3412
+ *
3413
+ * @example
3414
+ * ```ts
3415
+ * router
3416
+ * .route('search')
3417
+ * .paid('0.01')
3418
+ * .provider('exa', { quotaPerMonth: 1000 })
3419
+ * .handler(handler);
3420
+ * ```
3421
+ */
2491
3422
  provider(name, config) {
2492
3423
  const next = this.fork();
2493
3424
  next._providerName = name;
2494
3425
  next._providerConfig = config ?? {};
2495
3426
  return next;
2496
3427
  }
2497
- body(schema, example) {
3428
+ /**
3429
+ * Declare the request body's Zod schema. Parsed body is typed as `ctx.body`
3430
+ * in the handler. Use `.inputExample()` to attach a discovery example.
3431
+ *
3432
+ * @example
3433
+ * ```ts
3434
+ * .body(z.object({ query: z.string() }))
3435
+ * .handler(async ({ body }) => search(body.query));
3436
+ * ```
3437
+ */
3438
+ body(schema) {
2498
3439
  const next = this.fork();
2499
3440
  next._bodySchema = schema;
2500
- if (example !== void 0) {
2501
- next._inputExample = example;
2502
- next._hasInputExample = true;
2503
- }
2504
3441
  return next;
2505
3442
  }
2506
- query(schema, example) {
3443
+ /**
3444
+ * Declare a query-string Zod schema and switch the route to `GET`. Parsed
3445
+ * query is typed as `ctx.query` in the handler. Use `.inputExample()` to
3446
+ * attach a discovery example.
3447
+ *
3448
+ * @example
3449
+ * ```ts
3450
+ * .query(z.object({ id: z.string() }))
3451
+ * .handler(async ({ query }) => getById(query.id));
3452
+ * ```
3453
+ */
3454
+ query(schema) {
2507
3455
  const next = this.fork();
2508
3456
  next._querySchema = schema;
2509
- if (example !== void 0) {
2510
- next._inputExample = example;
2511
- next._hasInputExample = true;
2512
- }
2513
3457
  next._method = "GET";
2514
3458
  return next;
2515
3459
  }
2516
- output(schema, example) {
3460
+ /**
3461
+ * Declare the response output's Zod schema for OpenAPI generation. The
3462
+ * runtime does not validate handler return values — use Zod's `.parse()`
3463
+ * inside the handler if strict output validation is required. Use
3464
+ * `.outputExample()` to attach a discovery example.
3465
+ *
3466
+ * @example
3467
+ * ```ts
3468
+ * .output(z.object({ result: z.string() }))
3469
+ * .handler(async () => ({ result: 'ok' }));
3470
+ * ```
3471
+ */
3472
+ output(schema) {
2517
3473
  const next = this.fork();
2518
3474
  next._outputSchema = schema;
2519
- if (example !== void 0) {
2520
- next._outputExample = example;
2521
- next._hasOutputExample = true;
2522
- }
2523
3475
  return next;
2524
3476
  }
2525
3477
  /**
2526
- * Provide a conforming example of the request input (body or query params).
2527
- *
2528
- * Optional. When provided, the example is validated against the request schema
2529
- * at route registration and embedded in the bazaar discovery extension so
2530
- * indexers can advertise a working sample call.
2531
- *
2532
- * For the common case, pass the example directly to `.body(schema, example)` or
2533
- * `.query(schema, example)` instead.
3478
+ * Attach an example of the request body or query for discovery output,
3479
+ * validated against the registered schema at registration.
2534
3480
  *
2535
3481
  * @example
2536
3482
  * ```ts
2537
- * router.route('search')
2538
- * .paid('0.01')
2539
- * .body(z.object({ q: z.string() }))
2540
- * .inputExample({ q: 'hello world' })
2541
- * .handler(async ({ body }) => { ... });
3483
+ * .body(searchSchema).inputExample({ query: 'cats' });
2542
3484
  * ```
2543
3485
  */
2544
3486
  inputExample(example) {
@@ -2548,32 +3490,12 @@ var RouteBuilder = class {
2548
3490
  return next;
2549
3491
  }
2550
3492
  /**
2551
- * Provide a conforming example of the response output.
2552
- *
2553
- * Optional. When provided, the example is validated against the output schema
2554
- * at route registration and embedded in the bazaar discovery extension so
2555
- * indexers can advertise the response shape.
2556
- *
2557
- * For the common case, pass the example directly to `.output(schema, example)` instead.
2558
- *
2559
- * Accepts any JSON value (objects, arrays, or primitives) — top-level array
2560
- * or primitive responses (e.g. `z.array(...)`) are supported alongside the
2561
- * common object case.
3493
+ * Attach an example response for discovery output, validated against the
3494
+ * registered output schema at registration.
2562
3495
  *
2563
3496
  * @example
2564
3497
  * ```ts
2565
- * router.route('search')
2566
- * .paid('0.01')
2567
- * .output(z.object({ results: z.array(z.string()) }))
2568
- * .outputExample({ results: ['a', 'b'] })
2569
- * .handler(async () => { ... });
2570
- *
2571
- * // Top-level array response
2572
- * router.route('chains')
2573
- * .paid('0.01')
2574
- * .output(z.array(z.object({ name: z.string() })))
2575
- * .outputExample([{ name: 'Ethereum' }])
2576
- * .handler(async () => { ... });
3498
+ * .output(resultSchema).outputExample({ result: 'ok' });
2577
3499
  * ```
2578
3500
  */
2579
3501
  outputExample(example) {
@@ -2582,43 +3504,60 @@ var RouteBuilder = class {
2582
3504
  next._hasOutputExample = true;
2583
3505
  return next;
2584
3506
  }
3507
+ /**
3508
+ * Set a human-readable summary of the route. Surfaces in OpenAPI,
3509
+ * `well-known`, and `llms.txt` discovery output.
3510
+ *
3511
+ * @example
3512
+ * ```ts
3513
+ * .description('Search indexed web pages by full-text query');
3514
+ * ```
3515
+ */
2585
3516
  description(text) {
2586
3517
  const next = this.fork();
2587
3518
  next._description = text;
2588
3519
  return next;
2589
3520
  }
3521
+ /**
3522
+ * Override the URL path advertised in discovery output. Defaults to the
3523
+ * registry key passed to `.route()`.
3524
+ *
3525
+ * @example
3526
+ * ```ts
3527
+ * router.route('search').path('/v2/search').handler(handler);
3528
+ * ```
3529
+ */
2590
3530
  path(p) {
2591
3531
  const next = this.fork();
2592
3532
  next._path = p;
2593
3533
  return next;
2594
3534
  }
3535
+ /**
3536
+ * Override the HTTP method advertised in discovery. Defaults to `POST`, or
3537
+ * `GET` when `.query()` has been called.
3538
+ *
3539
+ * @example
3540
+ * ```ts
3541
+ * router.route('items/delete').method('DELETE').handler(handler);
3542
+ * ```
3543
+ */
2595
3544
  method(m) {
2596
3545
  const next = this.fork();
2597
3546
  next._method = m;
2598
3547
  return next;
2599
3548
  }
2600
- // -------------------------------------------------------------------------
2601
- // Pre-payment validation
2602
- // -------------------------------------------------------------------------
2603
3549
  /**
2604
- * Add pre-payment validation that runs after body parsing but before the 402
2605
- * challenge is shown. Use this for async business logic like "is this resource
2606
- * available?" or "has this user hit their rate limit?".
2607
- *
2608
- * Requires `.body()` — call `.body()` before `.validate()` for type inference.
3550
+ * Run validation against the parsed body before the 402 challenge. Throw
3551
+ * `Object.assign(new Error('...'), { status })` to reject with a custom
3552
+ * status code; defaults to 400. Requires `.body()` to be called first.
2609
3553
  *
2610
3554
  * @example
2611
- * ```typescript
2612
- * router
2613
- * .route('domain/register')
2614
- * .paid(calculatePrice)
2615
- * .body(RegisterSchema) // .body() first for type inference
2616
- * .validate(async (body) => {
2617
- * if (await isDomainTaken(body.domain)) {
2618
- * throw Object.assign(new Error('Domain taken'), { status: 409 });
2619
- * }
2620
- * })
2621
- * .handler(async ({ body }) => { ... });
3555
+ * ```ts
3556
+ * .body(RegisterSchema).validate(async (body) => {
3557
+ * if (await isTaken(body.name)) {
3558
+ * throw Object.assign(new Error('taken'), { status: 409 });
3559
+ * }
3560
+ * });
2622
3561
  * ```
2623
3562
  */
2624
3563
  validate(fn) {
@@ -2626,27 +3565,64 @@ var RouteBuilder = class {
2626
3565
  next._validateFn = fn;
2627
3566
  return next;
2628
3567
  }
2629
- // -------------------------------------------------------------------------
2630
- // Settlement lifecycle
2631
- // -------------------------------------------------------------------------
2632
3568
  /**
2633
- * Add route-specific settlement hooks.
3569
+ * Hook into the settlement lifecycle. `beforeSettle` runs after the handler
3570
+ * succeeds but before on-chain settlement and can cancel the charge;
3571
+ * `afterSettle` runs after settlement completes (success or failure).
2634
3572
  *
2635
- * `beforeSettle` runs after a successful handler response but before
2636
- * router-controlled settlement/broadcast, so it can still prevent the charge
2637
- * for x402 and MPP transaction-payload flows. `afterSettle` runs after
2638
- * settlement and is intended for durable ledgers or app-owned refund queues.
3573
+ * @example
3574
+ * ```ts
3575
+ * .settlement({
3576
+ * beforeSettle: ({ result }) => (result.refund ? 'skip' : 'continue'),
3577
+ * afterSettle: ({ tx }) => analytics.track('settled', { tx }),
3578
+ * });
3579
+ * ```
2639
3580
  */
2640
3581
  settlement(lifecycle) {
2641
3582
  const next = this.fork();
2642
3583
  next._settlement = lifecycle;
2643
3584
  return next;
2644
3585
  }
2645
- // -------------------------------------------------------------------------
2646
- // Terminal method
2647
- // -------------------------------------------------------------------------
3586
+ /**
3587
+ * Register the request handler and return the Next.js route function. The
3588
+ * handler receives a typed context and may return a value (serialized to
3589
+ * JSON), a raw `Response`, or throw an `HttpError` for a non-2xx status.
3590
+ *
3591
+ * @example
3592
+ * ```ts
3593
+ * export const POST = router
3594
+ * .route('search')
3595
+ * .paid('0.01')
3596
+ * .body(schema)
3597
+ * .handler(async ({ body, wallet }) => searchService(body, wallet));
3598
+ * ```
3599
+ */
2648
3600
  handler(fn) {
2649
- const handlerFn = fn;
3601
+ return this.register(fn, false);
3602
+ }
3603
+ /**
3604
+ * Register a streaming handler (`async function*`) and return the Next.js
3605
+ * route function. Each `charge()` call bills one tick (`tickCost` USDC) up
3606
+ * to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
3607
+ *
3608
+ * @example
3609
+ * ```ts
3610
+ * export const POST = router
3611
+ * .route('llm/stream')
3612
+ * .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
3613
+ * .body(schema)
3614
+ * .stream(async function* ({ body, charge }) {
3615
+ * for await (const token of streamLLM(body.prompt)) {
3616
+ * await charge();
3617
+ * yield token;
3618
+ * }
3619
+ * });
3620
+ * ```
3621
+ */
3622
+ stream(fn) {
3623
+ return this.register(fn, true);
3624
+ }
3625
+ register(handlerFn, streaming) {
2650
3626
  if (!this._authMode) {
2651
3627
  throw new Error(
2652
3628
  `route '${this._key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
@@ -2660,6 +3636,26 @@ var RouteBuilder = class {
2660
3636
  if (this._settlement && !this._pricing) {
2661
3637
  throw new Error(`route '${this._key}': .settlement() requires a paid route`);
2662
3638
  }
3639
+ if (this._dynamicPrice && this._protocols.includes("x402")) {
3640
+ const hasUpto = this._deps.x402Accepts.some((accept) => accept.scheme === "upto");
3641
+ if (!hasUpto) {
3642
+ throw new Error(
3643
+ `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.`
3644
+ );
3645
+ }
3646
+ }
3647
+ if (this._dynamicPrice && this._protocols.includes("mpp")) {
3648
+ if (!this._deps.mppSessionConfig) {
3649
+ throw new Error(
3650
+ `route '${this._key}': .paid({ dynamic: true }) on an MPP route requires session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
3651
+ );
3652
+ }
3653
+ }
3654
+ if (streaming && !this._dynamicPrice) {
3655
+ throw new Error(
3656
+ `route '${this._key}': .stream() requires .paid({ dynamic: true }) \u2014 static/free routes can't meter per-chunk billing.`
3657
+ );
3658
+ }
2663
3659
  validateExamples(
2664
3660
  this._key,
2665
3661
  this._bodySchema,
@@ -2675,6 +3671,8 @@ var RouteBuilder = class {
2675
3671
  authMode: this._authMode,
2676
3672
  siwxEnabled: this._siwxEnabled,
2677
3673
  pricing: this._pricing,
3674
+ dynamicPrice: this._dynamicPrice ? true : void 0,
3675
+ streaming: streaming ? true : void 0,
2678
3676
  protocols: this._protocols,
2679
3677
  bodySchema: this._bodySchema,
2680
3678
  querySchema: this._querySchema,
@@ -2692,81 +3690,28 @@ var RouteBuilder = class {
2692
3690
  providerConfig: this._providerConfig,
2693
3691
  validateFn: this._validateFn,
2694
3692
  settlement: this._settlement,
2695
- mppInfo: this._mppInfo
3693
+ mppInfo: this._mppInfo,
3694
+ tickCost: this._tickCost,
3695
+ unitType: this._unitType
2696
3696
  };
2697
3697
  this._registry.register(entry);
2698
- return createRequestHandler(
2699
- entry,
2700
- handlerFn,
2701
- this._deps
2702
- );
3698
+ return createRequestHandler(entry, handlerFn, this._deps);
2703
3699
  }
2704
3700
  };
2705
-
2706
- // src/auth/entitlement.ts
2707
- var MemoryEntitlementStore = class {
2708
- routeToWallets = /* @__PURE__ */ new Map();
2709
- async has(route, wallet) {
2710
- const wallets = this.routeToWallets.get(route);
2711
- if (!wallets) return false;
2712
- return wallets.has(normalizeWalletAddress(wallet));
2713
- }
2714
- async grant(route, wallet) {
2715
- const normalized = normalizeWalletAddress(wallet);
2716
- let wallets = this.routeToWallets.get(route);
2717
- if (!wallets) {
2718
- wallets = /* @__PURE__ */ new Set();
2719
- this.routeToWallets.set(route, wallets);
3701
+ function resolvePaidArgs(routeKey, pricingOrOptions, options) {
3702
+ const isHandlerDynamicShape = typeof pricingOrOptions === "object" && pricingOrOptions !== null && typeof pricingOrOptions !== "function" && !("tiers" in pricingOrOptions) && "dynamic" in pricingOrOptions && pricingOrOptions.dynamic;
3703
+ if (isHandlerDynamicShape) {
3704
+ const opts = pricingOrOptions;
3705
+ if (!opts.maxPrice) {
3706
+ throw new Error(`route '${routeKey}': .paid({ dynamic: true }) requires maxPrice`);
2720
3707
  }
2721
- wallets.add(normalized);
3708
+ return { pricing: opts.maxPrice, resolvedOptions: opts };
2722
3709
  }
2723
- };
2724
- function detectRedisClientType2(client) {
2725
- if (!client || typeof client !== "object") {
2726
- throw new Error(
2727
- "createRedisEntitlementStore requires a Redis client. Supported: @upstash/redis, ioredis."
2728
- );
2729
- }
2730
- if ("options" in client && "status" in client) return "ioredis";
2731
- const constructor = client.constructor?.name;
2732
- if (constructor === "Redis" && "url" in client) return "upstash";
2733
- if (typeof client.sadd === "function" && typeof client.sismember === "function") {
2734
- return "upstash";
2735
- }
2736
- throw new Error("Unrecognized Redis client for entitlement store.");
2737
- }
2738
- function createRedisEntitlementStore(client, options) {
2739
- const clientType = detectRedisClientType2(client);
2740
- const prefix = options?.prefix ?? "siwx:entitlement:";
2741
- return {
2742
- async has(route, wallet) {
2743
- const key = `${prefix}${route}`;
2744
- const normalized = normalizeWalletAddress(wallet);
2745
- if (clientType === "upstash") {
2746
- const redis2 = client;
2747
- const result2 = await redis2.sismember(key, normalized);
2748
- return result2 === 1 || result2 === true;
2749
- }
2750
- const redis = client;
2751
- const result = await redis.sismember(key, normalized);
2752
- return result === 1;
2753
- },
2754
- async grant(route, wallet) {
2755
- const key = `${prefix}${route}`;
2756
- const normalized = normalizeWalletAddress(wallet);
2757
- if (clientType === "upstash") {
2758
- const redis2 = client;
2759
- await redis2.sadd(key, normalized);
2760
- return;
2761
- }
2762
- const redis = client;
2763
- await redis.sadd(key, normalized);
2764
- }
2765
- };
3710
+ return { pricing: pricingOrOptions, resolvedOptions: options };
2766
3711
  }
2767
3712
 
2768
3713
  // src/discovery/well-known.ts
2769
- import { NextResponse as NextResponse6 } from "next/server";
3714
+ import { NextResponse as NextResponse8 } from "next/server";
2770
3715
 
2771
3716
  // src/discovery/utils/guidance.ts
2772
3717
  async function resolveGuidance(discovery) {
@@ -2810,7 +3755,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
2810
3755
  if (instructions) {
2811
3756
  body.instructions = instructions;
2812
3757
  }
2813
- return NextResponse6.json(body, {
3758
+ return NextResponse8.json(body, {
2814
3759
  headers: {
2815
3760
  "Access-Control-Allow-Origin": "*",
2816
3761
  "Access-Control-Allow-Methods": "GET",
@@ -2828,13 +3773,13 @@ function toDiscoveryResource(method, url, mode) {
2828
3773
 
2829
3774
  // src/discovery/openapi.ts
2830
3775
  init_constants();
2831
- import { NextResponse as NextResponse7 } from "next/server";
3776
+ import { NextResponse as NextResponse9 } from "next/server";
2832
3777
  function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2833
3778
  const normalizedBase = baseUrl.replace(/\/+$/, "");
2834
3779
  let cached = null;
2835
3780
  let validated = false;
2836
3781
  return async (_request) => {
2837
- if (cached) return NextResponse7.json(cached);
3782
+ if (cached) return NextResponse9.json(cached);
2838
3783
  if (!validated && pricesKeys) {
2839
3784
  registry.validate(pricesKeys);
2840
3785
  validated = true;
@@ -2897,7 +3842,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2897
3842
  paths
2898
3843
  };
2899
3844
  cached = createDocument(openApiDocument);
2900
- return NextResponse7.json(cached);
3845
+ return NextResponse9.json(cached);
2901
3846
  };
2902
3847
  }
2903
3848
  function deriveTag(routeKey) {
@@ -2959,97 +3904,336 @@ function buildOperation(routeKey, entry, tag) {
2959
3904
  };
2960
3905
  }
2961
3906
  return {
2962
- operation,
2963
- requiresSiwxScheme,
2964
- requiresApiKeyScheme
3907
+ operation,
3908
+ requiresSiwxScheme,
3909
+ requiresApiKeyScheme
3910
+ };
3911
+ }
3912
+ function toProtocolObject(protocol, mppInfo) {
3913
+ if (protocol === "mpp") {
3914
+ return {
3915
+ mpp: {
3916
+ method: mppInfo?.method ?? "tempo",
3917
+ intent: mppInfo?.intent ?? "charge",
3918
+ currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
3919
+ }
3920
+ };
3921
+ }
3922
+ return { [protocol]: {} };
3923
+ }
3924
+ function buildPricingInfo(entry) {
3925
+ if (!entry.pricing) return void 0;
3926
+ if (typeof entry.pricing === "string") {
3927
+ return {
3928
+ price: { mode: "fixed", currency: "USD", amount: entry.pricing }
3929
+ };
3930
+ }
3931
+ if (typeof entry.pricing === "function") {
3932
+ return {
3933
+ price: {
3934
+ mode: "dynamic",
3935
+ currency: "USD",
3936
+ min: entry.minPrice ?? "0",
3937
+ max: entry.maxPrice ?? "0"
3938
+ }
3939
+ };
3940
+ }
3941
+ if ("tiers" in entry.pricing) {
3942
+ const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
3943
+ const min = Math.min(...tierPrices);
3944
+ const max = Math.max(...tierPrices);
3945
+ if (Number.isFinite(min) && Number.isFinite(max)) {
3946
+ if (min === max) {
3947
+ return {
3948
+ price: { mode: "fixed", currency: "USD", amount: String(min) }
3949
+ };
3950
+ }
3951
+ return {
3952
+ price: { mode: "dynamic", currency: "USD", min: String(min), max: String(max) }
3953
+ };
3954
+ }
3955
+ return {
3956
+ price: {
3957
+ mode: "dynamic",
3958
+ currency: "USD",
3959
+ min: "0",
3960
+ max: entry.maxPrice ?? "0"
3961
+ }
3962
+ };
3963
+ }
3964
+ return void 0;
3965
+ }
3966
+
3967
+ // src/discovery/llms-txt.ts
3968
+ import { NextResponse as NextResponse10 } from "next/server";
3969
+ function createLlmsTxtHandler(discovery) {
3970
+ return async (_request) => {
3971
+ const guidance = await resolveGuidance(discovery) ?? "";
3972
+ return new NextResponse10(guidance, {
3973
+ headers: {
3974
+ "Content-Type": "text/plain; charset=utf-8",
3975
+ "Access-Control-Allow-Origin": "*"
3976
+ }
3977
+ });
3978
+ };
3979
+ }
3980
+
3981
+ // src/index.ts
3982
+ init_accepts();
3983
+ init_constants();
3984
+
3985
+ // src/config/error.ts
3986
+ var RouterConfigError = class extends Error {
3987
+ issues;
3988
+ constructor(issues) {
3989
+ super(formatRouterConfigIssues(issues));
3990
+ this.name = "RouterConfigError";
3991
+ this.issues = issues;
3992
+ }
3993
+ };
3994
+ function formatRouterConfigIssues(issues) {
3995
+ return issues.map((issue) => issue.message).join("\n");
3996
+ }
3997
+
3998
+ // src/config/validators/x402.ts
3999
+ init_accepts();
4000
+
4001
+ // src/config/validators/shared.ts
4002
+ init_evm();
4003
+ init_solana();
4004
+ init_accepts();
4005
+ function isEvmAddress(value) {
4006
+ return /^0x[a-fA-F0-9]{40}$/.test(value);
4007
+ }
4008
+ function isEvmPrivateKey(value) {
4009
+ return /^0x[a-fA-F0-9]{64}$/.test(value);
4010
+ }
4011
+ function isSupportedX402Network(network) {
4012
+ return isEvmNetwork(network) || isSolanaNetwork(network);
4013
+ }
4014
+ function findPlaceholderPayee(values) {
4015
+ return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
4016
+ }
4017
+ function usesDefaultEvmFacilitator(config) {
4018
+ return getConfiguredX402Networks(config).some(
4019
+ (network) => typeof network === "string" && isEvmNetwork(network)
4020
+ ) && config.x402?.facilitators?.evm === void 0;
4021
+ }
4022
+
4023
+ // src/config/validators/x402.ts
4024
+ var CHECKS = [
4025
+ checkAcceptNetworkPresent,
4026
+ checkAcceptNetworkSupported,
4027
+ checkNonExactRequiresAsset,
4028
+ checkDecimalsAreValid,
4029
+ checkPayee,
4030
+ checkPlaceholderPayeeAddress,
4031
+ checkCdpKeys
4032
+ ];
4033
+ function validateX402Config(config, env, options) {
4034
+ const accepts = getConfiguredX402Accepts(config);
4035
+ if (accepts.length === 0) {
4036
+ return [
4037
+ {
4038
+ code: "missing_x402_accepts",
4039
+ protocol: "x402",
4040
+ message: "x402 requires at least one accept configuration."
4041
+ }
4042
+ ];
4043
+ }
4044
+ const args = { config, accepts, env, options };
4045
+ return CHECKS.map((check) => check(args)).filter(
4046
+ (issue) => issue !== null
4047
+ );
4048
+ }
4049
+ function checkAcceptNetworkPresent({ accepts }) {
4050
+ return accepts.some((accept) => !accept.network) ? { code: "missing_x402_network", protocol: "x402", message: "x402 accepts require a network." } : null;
4051
+ }
4052
+ function checkAcceptNetworkSupported({ accepts }) {
4053
+ const unsupported = accepts.find(
4054
+ (accept) => accept.network && !isSupportedX402Network(accept.network)
4055
+ );
4056
+ if (!unsupported) return null;
4057
+ return {
4058
+ code: "unsupported_x402_network",
4059
+ protocol: "x402",
4060
+ message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
4061
+ };
4062
+ }
4063
+ function checkNonExactRequiresAsset({ accepts }) {
4064
+ return accepts.some((accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset) ? {
4065
+ code: "missing_x402_asset",
4066
+ protocol: "x402",
4067
+ message: "non-exact x402 accepts require an asset."
4068
+ } : null;
4069
+ }
4070
+ function checkDecimalsAreValid({ accepts }) {
4071
+ const invalid = accepts.find(
4072
+ (accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
4073
+ );
4074
+ if (!invalid) return null;
4075
+ return {
4076
+ code: "invalid_x402_decimals",
4077
+ protocol: "x402",
4078
+ message: "x402 accept decimals must be a non-negative integer."
4079
+ };
4080
+ }
4081
+ function checkPayee({ config, accepts }) {
4082
+ if (config.payeeAddress) return null;
4083
+ return accepts.some((accept) => !accept.payTo) ? {
4084
+ code: "missing_x402_payee",
4085
+ protocol: "x402",
4086
+ message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
4087
+ } : null;
4088
+ }
4089
+ function checkPlaceholderPayeeAddress({
4090
+ config,
4091
+ accepts
4092
+ }) {
4093
+ const placeholder = findPlaceholderPayee([
4094
+ config.payeeAddress,
4095
+ ...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
4096
+ ]);
4097
+ if (!placeholder) return null;
4098
+ return {
4099
+ code: "placeholder_payee",
4100
+ protocol: "x402",
4101
+ message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
4102
+ };
4103
+ }
4104
+ function checkCdpKeys({ config, env, options }) {
4105
+ if (options.requireCdpKeys === false) return null;
4106
+ if (!usesDefaultEvmFacilitator(config)) return null;
4107
+ const missing = [
4108
+ env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
4109
+ env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
4110
+ ].filter(Boolean);
4111
+ if (missing.length === 0) return null;
4112
+ return {
4113
+ code: "missing_cdp_keys",
4114
+ protocol: "x402",
4115
+ message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
4116
+ };
4117
+ }
4118
+
4119
+ // src/config/validators/mpp.ts
4120
+ import { privateKeyToAccount } from "viem/accounts";
4121
+ var CHECKS2 = [
4122
+ checkSecretKey,
4123
+ checkCurrency,
4124
+ checkRecipient,
4125
+ checkPlaceholderRecipient,
4126
+ checkRpcUrl,
4127
+ checkFeePayerKey,
4128
+ checkOperatorKey,
4129
+ checkOperatorMatchesFeePayer
4130
+ ];
4131
+ function validateMppConfig(config, env) {
4132
+ const mpp = config.mpp;
4133
+ if (!mpp) {
4134
+ return [
4135
+ {
4136
+ code: "missing_mpp_config",
4137
+ protocol: "mpp",
4138
+ message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
4139
+ }
4140
+ ];
4141
+ }
4142
+ const args = { config, mpp, env };
4143
+ return CHECKS2.map((check) => check(args)).filter(
4144
+ (issue) => issue !== null
4145
+ );
4146
+ }
4147
+ function checkSecretKey({ mpp }) {
4148
+ if (mpp.secretKey) return null;
4149
+ return {
4150
+ code: "missing_mpp_secret_key",
4151
+ protocol: "mpp",
4152
+ message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
2965
4153
  };
2966
4154
  }
2967
- function toProtocolObject(protocol, mppInfo) {
2968
- if (protocol === "mpp") {
4155
+ function checkCurrency({ mpp }) {
4156
+ if (!mpp.currency) {
2969
4157
  return {
2970
- mpp: {
2971
- method: mppInfo?.method ?? "tempo",
2972
- intent: mppInfo?.intent ?? "charge",
2973
- currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
2974
- }
4158
+ code: "missing_mpp_currency",
4159
+ protocol: "mpp",
4160
+ message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
2975
4161
  };
2976
4162
  }
2977
- return { [protocol]: {} };
2978
- }
2979
- function buildPricingInfo(entry) {
2980
- if (!entry.pricing) return void 0;
2981
- if (typeof entry.pricing === "string") {
4163
+ if (!isEvmAddress(mpp.currency)) {
2982
4164
  return {
2983
- price: { mode: "fixed", currency: "USD", amount: entry.pricing }
4165
+ code: "invalid_mpp_currency",
4166
+ protocol: "mpp",
4167
+ message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
2984
4168
  };
2985
4169
  }
2986
- if (typeof entry.pricing === "function") {
4170
+ return null;
4171
+ }
4172
+ function checkRecipient({ config, mpp }) {
4173
+ const recipient = mpp.recipient ?? config.payeeAddress;
4174
+ if (!recipient) {
2987
4175
  return {
2988
- price: {
2989
- mode: "dynamic",
2990
- currency: "USD",
2991
- min: entry.minPrice ?? "0",
2992
- max: entry.maxPrice ?? "0"
2993
- }
4176
+ code: "missing_mpp_recipient",
4177
+ protocol: "mpp",
4178
+ message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
2994
4179
  };
2995
4180
  }
2996
- if ("tiers" in entry.pricing) {
2997
- const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
2998
- const min = Math.min(...tierPrices);
2999
- const max = Math.max(...tierPrices);
3000
- if (Number.isFinite(min) && Number.isFinite(max)) {
3001
- if (min === max) {
3002
- return {
3003
- price: { mode: "fixed", currency: "USD", amount: String(min) }
3004
- };
3005
- }
3006
- return {
3007
- price: { mode: "dynamic", currency: "USD", min: String(min), max: String(max) }
3008
- };
3009
- }
4181
+ if (!isEvmAddress(recipient)) {
3010
4182
  return {
3011
- price: {
3012
- mode: "dynamic",
3013
- currency: "USD",
3014
- min: "0",
3015
- max: entry.maxPrice ?? "0"
3016
- }
4183
+ code: "invalid_mpp_recipient",
4184
+ protocol: "mpp",
4185
+ message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
3017
4186
  };
3018
4187
  }
3019
- return void 0;
4188
+ return null;
3020
4189
  }
3021
-
3022
- // src/discovery/llms-txt.ts
3023
- import { NextResponse as NextResponse8 } from "next/server";
3024
- function createLlmsTxtHandler(discovery) {
3025
- return async (_request) => {
3026
- const guidance = await resolveGuidance(discovery) ?? "";
3027
- return new NextResponse8(guidance, {
3028
- headers: {
3029
- "Content-Type": "text/plain; charset=utf-8",
3030
- "Access-Control-Allow-Origin": "*"
3031
- }
3032
- });
4190
+ function checkPlaceholderRecipient({ config, mpp }) {
4191
+ const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
4192
+ if (!placeholder) return null;
4193
+ return {
4194
+ code: "placeholder_payee",
4195
+ protocol: "mpp",
4196
+ message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
4197
+ };
4198
+ }
4199
+ function checkRpcUrl({ mpp, env }) {
4200
+ if (mpp.rpcUrl ?? env.TEMPO_RPC_URL) return null;
4201
+ return {
4202
+ code: "missing_mpp_rpc_url",
4203
+ protocol: "mpp",
4204
+ message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
4205
+ };
4206
+ }
4207
+ function checkFeePayerKey({ mpp }) {
4208
+ if (!mpp.feePayerKey || isEvmPrivateKey(mpp.feePayerKey)) return null;
4209
+ return {
4210
+ code: "invalid_mpp_fee_payer_key",
4211
+ protocol: "mpp",
4212
+ message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
4213
+ };
4214
+ }
4215
+ function checkOperatorKey({ mpp }) {
4216
+ if (!mpp.operatorKey || isEvmPrivateKey(mpp.operatorKey)) return null;
4217
+ return {
4218
+ code: "invalid_mpp_operator_key",
4219
+ protocol: "mpp",
4220
+ message: "MPP operatorKey must be a 0x-prefixed 32-byte EVM private key."
4221
+ };
4222
+ }
4223
+ function checkOperatorMatchesFeePayer({ mpp }) {
4224
+ if (!mpp.operatorKey || !mpp.feePayerKey) return null;
4225
+ if (!isEvmPrivateKey(mpp.operatorKey) || !isEvmPrivateKey(mpp.feePayerKey)) return null;
4226
+ const opAddr = privateKeyToAccount(mpp.operatorKey).address.toLowerCase();
4227
+ const fpAddr = privateKeyToAccount(mpp.feePayerKey).address.toLowerCase();
4228
+ if (opAddr !== fpAddr) return null;
4229
+ return {
4230
+ code: "mpp_operator_equals_fee_payer",
4231
+ protocol: "mpp",
4232
+ 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).`
3033
4233
  };
3034
4234
  }
3035
4235
 
3036
- // src/index.ts
3037
- init_accepts();
3038
- init_constants();
3039
-
3040
- // src/config.ts
3041
- init_constants();
3042
- init_evm();
3043
- init_solana();
3044
- init_accepts();
3045
- var RouterConfigError = class extends Error {
3046
- issues;
3047
- constructor(issues) {
3048
- super(formatRouterConfigIssues(issues));
3049
- this.name = "RouterConfigError";
3050
- this.issues = issues;
3051
- }
3052
- };
4236
+ // src/config/validate.ts
3053
4237
  function validateRouterConfig(config, options = {}) {
3054
4238
  const issues = getRouterConfigIssues(config, options);
3055
4239
  if (issues.length > 0) throw new RouterConfigError(issues);
@@ -3078,9 +4262,9 @@ function getRouterConfigIssues(config, options = {}) {
3078
4262
  }
3079
4263
  return issues;
3080
4264
  }
3081
- function formatRouterConfigIssues(issues) {
3082
- return issues.map((issue) => issue.message).join("\n");
3083
- }
4265
+
4266
+ // src/config/env.ts
4267
+ init_constants();
3084
4268
  function mppFromEnv(env, options = {}) {
3085
4269
  const secretKey = env.MPP_SECRET_KEY;
3086
4270
  const currency = env.MPP_CURRENCY;
@@ -3111,8 +4295,7 @@ function mppFromEnv(env, options = {}) {
3111
4295
  currency,
3112
4296
  rpcUrl,
3113
4297
  ...options.recipient ? { recipient: options.recipient } : {},
3114
- ...feePayerKey ? { feePayerKey } : {},
3115
- ...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
4298
+ ...feePayerKey ? { feePayerKey } : {}
3116
4299
  };
3117
4300
  }
3118
4301
  function x402AcceptsFromEnv(env, options = {}) {
@@ -3142,189 +4325,145 @@ function x402AcceptsFromEnv(env, options = {}) {
3142
4325
  function paidOptionsForProtocols(protocols) {
3143
4326
  return { protocols: [...protocols] };
3144
4327
  }
3145
- function validateX402Config(config, env, options) {
3146
- const issues = [];
3147
- const accepts = getConfiguredX402Accepts(config);
3148
- if (accepts.length === 0) {
3149
- issues.push({
3150
- code: "missing_x402_accepts",
3151
- protocol: "x402",
3152
- message: "x402 requires at least one accept configuration."
3153
- });
3154
- return issues;
3155
- }
3156
- const acceptWithoutNetwork = accepts.find((accept) => !accept.network);
3157
- if (acceptWithoutNetwork) {
3158
- issues.push({
3159
- code: "missing_x402_network",
3160
- protocol: "x402",
3161
- message: "x402 accepts require a network."
3162
- });
3163
- }
3164
- const unsupported = accepts.find(
3165
- (accept) => accept.network && !isSupportedX402Network(accept.network)
3166
- );
3167
- if (unsupported) {
3168
- issues.push({
3169
- code: "unsupported_x402_network",
3170
- protocol: "x402",
3171
- message: `unsupported x402 network '${unsupported.network}'. Use eip155:* or solana:*.`
3172
- });
3173
- }
3174
- const missingAsset = accepts.find(
3175
- (accept) => (accept.scheme ?? "exact") !== "exact" && !accept.asset
3176
- );
3177
- if (missingAsset) {
3178
- issues.push({
3179
- code: "missing_x402_asset",
3180
- protocol: "x402",
3181
- message: "non-exact x402 accepts require an asset."
3182
- });
3183
- }
3184
- const invalidDecimals = accepts.find(
3185
- (accept) => accept.decimals !== void 0 && (!Number.isInteger(accept.decimals) || accept.decimals < 0)
3186
- );
3187
- if (invalidDecimals) {
3188
- issues.push({
3189
- code: "invalid_x402_decimals",
3190
- protocol: "x402",
3191
- message: "x402 accept decimals must be a non-negative integer."
3192
- });
3193
- }
3194
- if (accepts.some((accept) => !accept.payTo) && !config.payeeAddress) {
3195
- issues.push({
3196
- code: "missing_x402_payee",
3197
- protocol: "x402",
3198
- message: "x402 requires payeeAddress in router config or payTo on every x402 accept."
3199
- });
3200
- }
3201
- const placeholder = findPlaceholderPayee([
3202
- config.payeeAddress,
3203
- ...accepts.map((accept) => typeof accept.payTo === "string" ? accept.payTo : void 0)
3204
- ]);
3205
- if (placeholder) {
3206
- issues.push({
3207
- code: "placeholder_payee",
3208
- protocol: "x402",
3209
- message: `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
3210
- });
4328
+
4329
+ // src/init/x402.ts
4330
+ async function initX402(config, configError) {
4331
+ if (configError) return { initError: configError };
4332
+ try {
4333
+ const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4334
+ const result = await createX402Server2(config);
4335
+ await result.initPromise;
4336
+ return {
4337
+ server: result.server,
4338
+ facilitatorsByNetwork: result.facilitatorsByNetwork
4339
+ };
4340
+ } catch (err) {
4341
+ return { initError: err instanceof Error ? err.message : String(err) };
3211
4342
  }
3212
- if (options.requireCdpKeys !== false && usesDefaultEvmFacilitator(config)) {
3213
- const missing = [
3214
- env.CDP_API_KEY_ID ? null : "CDP_API_KEY_ID",
3215
- env.CDP_API_KEY_SECRET ? null : "CDP_API_KEY_SECRET"
3216
- ].filter(Boolean);
3217
- if (missing.length > 0) {
3218
- issues.push({
3219
- code: "missing_cdp_keys",
3220
- protocol: "x402",
3221
- message: `default EVM x402 facilitator requires ${missing.join(" and ")}.`
3222
- });
4343
+ }
4344
+
4345
+ // src/mppx-init.ts
4346
+ function getMppxRequestContext(args) {
4347
+ const {
4348
+ Mppx,
4349
+ tempo,
4350
+ mppConfig,
4351
+ payeeAddress,
4352
+ getClient,
4353
+ feePayerAccount,
4354
+ resolvedStore,
4355
+ sessionEnabled,
4356
+ sharedSessionParams,
4357
+ realm
4358
+ } = args;
4359
+ const instance = Mppx.create({
4360
+ methods: [
4361
+ tempo.charge({
4362
+ currency: mppConfig.currency,
4363
+ recipient: mppConfig.recipient ?? payeeAddress,
4364
+ getClient,
4365
+ ...feePayerAccount ? { feePayer: feePayerAccount } : {},
4366
+ ...resolvedStore ? { store: resolvedStore } : {}
4367
+ }),
4368
+ ...sessionEnabled ? [
4369
+ tempo.session({
4370
+ ...sharedSessionParams,
4371
+ sse: false
4372
+ })
4373
+ ] : []
4374
+ ],
4375
+ secretKey: mppConfig.secretKey,
4376
+ realm
4377
+ });
4378
+ return instance;
4379
+ }
4380
+ function getMppxStreamingContext(args) {
4381
+ if (!args.sessionEnabled) return null;
4382
+ const { Mppx, tempo, mppConfig, sharedSessionParams, realm } = args;
4383
+ const instance = Mppx.create({
4384
+ methods: [
4385
+ tempo.session({
4386
+ ...sharedSessionParams,
4387
+ sse: true
4388
+ })
4389
+ ],
4390
+ secretKey: mppConfig.secretKey,
4391
+ realm
4392
+ });
4393
+ return instance;
4394
+ }
4395
+
4396
+ // src/init/mpp.ts
4397
+ async function initMpp(config, resolvedBaseUrl, kvStore, configError) {
4398
+ if (configError) return { initError: configError };
4399
+ if (!config.mpp) return {};
4400
+ try {
4401
+ const { Mppx, tempo } = await import("mppx/server");
4402
+ const { createClient, http } = await import("viem");
4403
+ const { tempo: tempoChain } = await import("viem/chains");
4404
+ const { privateKeyToAccount: privateKeyToAccount2 } = await import("viem/accounts");
4405
+ const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
4406
+ const tempoClient = createClient({ chain: tempoChain, transport: http(rpcUrl) });
4407
+ const getClient = async () => tempoClient;
4408
+ const operatorAccount = config.mpp.operatorKey ? privateKeyToAccount2(config.mpp.operatorKey) : void 0;
4409
+ const feePayerAccount = config.mpp.feePayerKey ? privateKeyToAccount2(config.mpp.feePayerKey) : void 0;
4410
+ if (config.mpp.session && operatorAccount) {
4411
+ assertOperatorMatchesRecipient(config, operatorAccount.address);
3223
4412
  }
4413
+ const resolvedStore = kvStore ? await createKvMppStore(kvStore) : void 0;
4414
+ const realm = new URL(resolvedBaseUrl).host;
4415
+ const mppConfig = config.mpp;
4416
+ const sessionEnabled = !!(mppConfig.session && operatorAccount);
4417
+ const sharedSessionParams = {
4418
+ currency: mppConfig.currency,
4419
+ decimals: 6,
4420
+ recipient: mppConfig.recipient ?? config.payeeAddress,
4421
+ getClient,
4422
+ ...operatorAccount ? { account: operatorAccount } : {},
4423
+ ...feePayerAccount ? { feePayer: feePayerAccount } : {},
4424
+ ...resolvedStore ? { store: resolvedStore } : {}
4425
+ };
4426
+ const mppxArgs = {
4427
+ Mppx,
4428
+ tempo,
4429
+ mppConfig,
4430
+ payeeAddress: config.payeeAddress ?? "",
4431
+ getClient,
4432
+ feePayerAccount,
4433
+ resolvedStore,
4434
+ sessionEnabled,
4435
+ sharedSessionParams,
4436
+ realm
4437
+ };
4438
+ const primary = getMppxRequestContext(mppxArgs);
4439
+ const streaming = getMppxStreamingContext(mppxArgs);
4440
+ const mppx = {
4441
+ charge: primary.charge,
4442
+ ...primary.session ? { sessionRequest: primary.session } : {},
4443
+ ...streaming?.session ? { sessionStream: streaming.session } : {}
4444
+ };
4445
+ return { mppx, tempoClient };
4446
+ } catch (err) {
4447
+ return { initError: err instanceof Error ? err.message : String(err) };
3224
4448
  }
3225
- return issues;
3226
4449
  }
3227
- function validateMppConfig(config, env) {
3228
- const issues = [];
3229
- const mpp = config.mpp;
3230
- if (!mpp) {
3231
- return [
3232
- {
3233
- code: "missing_mpp_config",
3234
- protocol: "mpp",
3235
- message: 'protocols includes "mpp" but mpp config is missing. Add mpp: { secretKey, currency, recipient } to your router config.'
3236
- }
3237
- ];
3238
- }
3239
- if (!mpp.secretKey) {
3240
- issues.push({
3241
- code: "missing_mpp_secret_key",
3242
- protocol: "mpp",
3243
- message: "MPP requires secretKey. Set MPP_SECRET_KEY or pass mpp.secretKey."
3244
- });
3245
- }
3246
- if (!mpp.currency) {
3247
- issues.push({
3248
- code: "missing_mpp_currency",
3249
- protocol: "mpp",
3250
- message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
3251
- });
3252
- } else if (!isEvmAddress(mpp.currency)) {
3253
- issues.push({
3254
- code: "invalid_mpp_currency",
3255
- protocol: "mpp",
3256
- message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
3257
- });
3258
- }
3259
- const mppRecipient = mpp.recipient ?? config.payeeAddress;
3260
- if (!mppRecipient) {
3261
- issues.push({
3262
- code: "missing_mpp_recipient",
3263
- protocol: "mpp",
3264
- message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
3265
- });
3266
- } else if (!isEvmAddress(mppRecipient)) {
3267
- issues.push({
3268
- code: "invalid_mpp_recipient",
3269
- protocol: "mpp",
3270
- message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
3271
- });
3272
- }
3273
- const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
3274
- if (placeholder) {
3275
- issues.push({
3276
- code: "placeholder_payee",
3277
- protocol: "mpp",
3278
- message: `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
3279
- });
3280
- }
3281
- if (!(mpp.rpcUrl ?? env.TEMPO_RPC_URL)) {
3282
- issues.push({
3283
- code: "missing_mpp_rpc_url",
3284
- protocol: "mpp",
3285
- message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
3286
- });
3287
- }
3288
- if (mpp.feePayerKey && !isEvmPrivateKey(mpp.feePayerKey)) {
3289
- issues.push({
3290
- code: "invalid_mpp_fee_payer_key",
3291
- protocol: "mpp",
3292
- message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
3293
- });
3294
- }
3295
- if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
3296
- issues.push({
3297
- code: "missing_mpp_default_store_env",
3298
- protocol: "mpp",
3299
- message: "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
3300
- });
4450
+ function assertOperatorMatchesRecipient(config, operatorAddress) {
4451
+ const recipient = (config.mpp?.recipient ?? config.payeeAddress)?.toLowerCase();
4452
+ const opAddr = operatorAddress.toLowerCase();
4453
+ if (recipient && opAddr !== recipient) {
4454
+ throw new Error(
4455
+ `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}.`
4456
+ );
3301
4457
  }
3302
- return issues;
3303
- }
3304
- function usesDefaultEvmFacilitator(config) {
3305
- return getConfiguredX402Networks(config).some(
3306
- (network) => typeof network === "string" && isEvmNetwork(network)
3307
- ) && config.x402?.facilitators?.evm === void 0;
3308
- }
3309
- function isSupportedX402Network(network) {
3310
- return isEvmNetwork(network) || isSolanaNetwork(network);
3311
- }
3312
- function isEvmAddress(value) {
3313
- return /^0x[a-fA-F0-9]{40}$/.test(value);
3314
- }
3315
- function isEvmPrivateKey(value) {
3316
- return /^0x[a-fA-F0-9]{64}$/.test(value);
3317
- }
3318
- function findPlaceholderPayee(values) {
3319
- return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
3320
4458
  }
3321
4459
 
3322
4460
  // src/index.ts
3323
4461
  init_constants();
3324
4462
  function createRouter(config) {
3325
4463
  const registry = new RouteRegistry();
3326
- const nonceStore = config.siwx?.nonceStore ?? new MemoryNonceStore();
3327
- const entitlementStore = config.siwx?.entitlementStore ?? new MemoryEntitlementStore();
4464
+ const kvStore = resolveKvStore(config.kvStore);
4465
+ const nonceStore = kvStore ? createKvNonceStore(kvStore) : new MemoryNonceStore();
4466
+ const entitlementStore = kvStore ? createKvEntitlementStore(kvStore) : new MemoryEntitlementStore();
3328
4467
  const network = config.network ?? BASE_NETWORK;
3329
4468
  const x402Accepts = getConfiguredX402Accepts(config);
3330
4469
  const configIssues = getRouterConfigIssues(config, {
@@ -3370,69 +4509,20 @@ function createRouter(config) {
3370
4509
  x402FacilitatorsByNetwork: void 0,
3371
4510
  x402Accepts,
3372
4511
  mppx: null,
3373
- tempoClient: null
4512
+ tempoClient: null,
4513
+ mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
3374
4514
  };
3375
4515
  deps.initPromise = (async () => {
3376
- if (x402ConfigError) {
3377
- deps.x402InitError = x402ConfigError;
3378
- } else {
3379
- try {
3380
- const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_server(), server_exports));
3381
- const result = await createX402Server2(config);
3382
- deps.x402Server = result.server;
3383
- deps.x402FacilitatorsByNetwork = result.facilitatorsByNetwork;
3384
- await result.initPromise;
3385
- } catch (err) {
3386
- deps.x402Server = null;
3387
- deps.x402InitError = err instanceof Error ? err.message : String(err);
3388
- }
3389
- }
3390
- if (mppConfigError) {
3391
- deps.mppInitError = mppConfigError;
3392
- } else if (config.mpp) {
3393
- try {
3394
- const { Mppx, tempo } = await import("mppx/server");
3395
- const rpcUrl = config.mpp.rpcUrl ?? process.env.TEMPO_RPC_URL;
3396
- const { createClient, http } = await import("viem");
3397
- const { tempo: tempoChain } = await import("viem/chains");
3398
- deps.tempoClient = createClient({ chain: tempoChain, transport: http(rpcUrl) });
3399
- const getClient = async () => deps.tempoClient;
3400
- let feePayerAccount;
3401
- if (config.mpp.feePayerKey) {
3402
- const { privateKeyToAccount } = await import("viem/accounts");
3403
- feePayerAccount = privateKeyToAccount(config.mpp.feePayerKey);
3404
- }
3405
- let resolvedStore = config.mpp.store;
3406
- if (!resolvedStore && config.mpp.useDefaultStore) {
3407
- const kvUrl = process.env.KV_REST_API_URL;
3408
- const kvToken = process.env.KV_REST_API_TOKEN;
3409
- if (!kvUrl || !kvToken) {
3410
- throw new Error(
3411
- "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
3412
- );
3413
- }
3414
- const { Store } = await import("mppx");
3415
- const { createUpstashRest: createUpstashRest2 } = await Promise.resolve().then(() => (init_upstash_rest(), upstash_rest_exports));
3416
- resolvedStore = Store.upstash(createUpstashRest2(kvUrl, kvToken));
3417
- }
3418
- deps.mppx = Mppx.create({
3419
- methods: [
3420
- tempo.charge({
3421
- currency: config.mpp.currency,
3422
- recipient: config.mpp.recipient ?? config.payeeAddress,
3423
- getClient,
3424
- ...feePayerAccount ? { feePayer: feePayerAccount } : {},
3425
- ...resolvedStore ? { store: resolvedStore } : {}
3426
- })
3427
- ],
3428
- secretKey: config.mpp.secretKey,
3429
- realm: new URL(resolvedBaseUrl).host
3430
- });
3431
- } catch (err) {
3432
- deps.mppx = null;
3433
- deps.mppInitError = err instanceof Error ? err.message : String(err);
3434
- console.error(`[router] MPP initialization failed: ${deps.mppInitError}`);
3435
- }
4516
+ const x402Result = await initX402(config, x402ConfigError);
4517
+ deps.x402Server = x402Result.server ?? null;
4518
+ deps.x402FacilitatorsByNetwork = x402Result.facilitatorsByNetwork;
4519
+ if (x402Result.initError) deps.x402InitError = x402Result.initError;
4520
+ const mppResult = await initMpp(config, resolvedBaseUrl, kvStore, mppConfigError);
4521
+ deps.mppx = mppResult.mppx ?? null;
4522
+ deps.tempoClient = mppResult.tempoClient ?? null;
4523
+ if (mppResult.initError) {
4524
+ deps.mppInitError = mppResult.initError;
4525
+ console.error(`[router] MPP initialization failed: ${mppResult.initError}`);
3436
4526
  }
3437
4527
  })();
3438
4528
  const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
@@ -3513,13 +4603,15 @@ export {
3513
4603
  TEMPO_USDC_CURRENCY,
3514
4604
  ZERO_EVM_ADDRESS,
3515
4605
  consolePlugin,
3516
- createRedisEntitlementStore,
3517
- createRedisNonceStore,
4606
+ createKvEntitlementStore,
4607
+ createKvMppStore,
4608
+ createKvNonceStore,
3518
4609
  createRouter,
3519
4610
  formatRouterConfigIssues,
3520
4611
  getRouterConfigIssues,
3521
4612
  mppFromEnv,
3522
4613
  paidOptionsForProtocols,
3523
4614
  validateRouterConfig,
4615
+ withPrefix,
3524
4616
  x402AcceptsFromEnv
3525
4617
  };