@agentcash/router 1.7.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -67,6 +67,9 @@ function getConfiguredX402Accepts(config) {
67
67
  function getConfiguredX402Networks(config) {
68
68
  return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
69
69
  }
70
+ function selectRouteAccepts(accepts, routeEntry) {
71
+ return routeEntry.billing === "upto" ? accepts.filter((accept) => accept.scheme === "upto") : accepts.filter((accept) => (accept.scheme ?? "exact") !== "upto");
72
+ }
70
73
  async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, body) {
71
74
  return Promise.all(
72
75
  accepts.map(async (accept) => ({
@@ -362,9 +365,18 @@ function withScopedKinds(client, kinds) {
362
365
  return {
363
366
  verify: client.verify.bind(client),
364
367
  settle: client.settle.bind(client),
365
- getSupported: async () => ({ ...await client.getSupported(), kinds })
368
+ getSupported: async () => {
369
+ const live = await client.getSupported();
370
+ return { ...live, kinds: mergeKindExtras(kinds, live.kinds) };
371
+ }
366
372
  };
367
373
  }
374
+ function mergeKindExtras(scoped, live) {
375
+ return scoped.map((kind) => {
376
+ const match = live.find((l) => l.scheme === kind.scheme && l.network === kind.network);
377
+ return match?.extra ? { ...kind, extra: { ...kind.extra, ...match.extra } } : kind;
378
+ });
379
+ }
368
380
  function buildSupportedKinds(group) {
369
381
  return group.networks.flatMap((network) => {
370
382
  if (group.family === "solana") {
@@ -592,7 +604,8 @@ function preflight(routeEntry, handler, deps, request) {
592
604
  request,
593
605
  meta,
594
606
  pluginCtx,
595
- report: createReporter(deps.plugin, pluginCtx, routeEntry.key)
607
+ report: createReporter(deps.plugin, pluginCtx, routeEntry.key),
608
+ query: void 0
596
609
  };
597
610
  }
598
611
  function buildMeta(request, routeEntry) {
@@ -615,13 +628,19 @@ function buildMeta(request, routeEntry) {
615
628
  var import_server = require("next/server");
616
629
 
617
630
  // src/pipeline/body.ts
631
+ var MalformedJsonError = class extends Error {
632
+ constructor() {
633
+ super("Invalid JSON");
634
+ this.name = "MalformedJsonError";
635
+ }
636
+ };
618
637
  async function bufferBody(request) {
619
638
  const text = await request.text();
620
- if (!text) return void 0;
639
+ if (!text.trim()) return void 0;
621
640
  try {
622
641
  return JSON.parse(text);
623
642
  } catch {
624
- return void 0;
643
+ throw new MalformedJsonError();
625
644
  }
626
645
  }
627
646
  function validateBody(parsed, schema) {
@@ -702,7 +721,18 @@ function computeQuotaLevel(remaining, warn, critical) {
702
721
  // src/pipeline/steps/parse-body.ts
703
722
  async function parseBody(ctx, request = ctx.request) {
704
723
  if (!ctx.routeEntry.bodySchema) return { ok: true, data: void 0 };
705
- const raw = await bufferBody(request);
724
+ let raw;
725
+ try {
726
+ raw = await bufferBody(request);
727
+ } catch (err) {
728
+ if (!(err instanceof MalformedJsonError)) throw err;
729
+ const response2 = import_server.NextResponse.json(
730
+ { success: false, error: "Invalid JSON", issues: [] },
731
+ { status: 400 }
732
+ );
733
+ firePluginResponse(ctx, response2);
734
+ return { ok: false, response: response2 };
735
+ }
706
736
  const result = validateBody(raw, ctx.routeEntry.bodySchema);
707
737
  if (result.success) return { ok: true, data: result.data };
708
738
  const response = import_server.NextResponse.json(
@@ -713,6 +743,22 @@ async function parseBody(ctx, request = ctx.request) {
713
743
  return { ok: false, response };
714
744
  }
715
745
 
746
+ // src/pipeline/steps/parse-query.ts
747
+ var import_server2 = require("next/server");
748
+ function validateQuery(ctx) {
749
+ const { querySchema } = ctx.routeEntry;
750
+ if (!querySchema) return { ok: true, data: void 0 };
751
+ const params = Object.fromEntries(ctx.request.nextUrl.searchParams.entries());
752
+ const result = validateBody(params, querySchema);
753
+ if (result.success) return { ok: true, data: result.data };
754
+ const response = import_server2.NextResponse.json(
755
+ { success: false, error: result.error, issues: result.issues },
756
+ { status: 400 }
757
+ );
758
+ firePluginResponse(ctx, response);
759
+ return { ok: false, response };
760
+ }
761
+
716
762
  // src/pipeline/steps/errors.ts
717
763
  function errorStatus(error, fallback) {
718
764
  const status = error?.status;
@@ -727,9 +773,9 @@ function handlerFailureError(response) {
727
773
  }
728
774
 
729
775
  // src/pipeline/steps/fail.ts
730
- var import_server2 = require("next/server");
776
+ var import_server3 = require("next/server");
731
777
  function fail(ctx, status, message, requestBody) {
732
- const response = import_server2.NextResponse.json({ success: false, error: message }, { status });
778
+ const response = import_server3.NextResponse.json({ success: false, error: message }, { status });
733
779
  firePluginResponse(ctx, response, requestBody);
734
780
  return response;
735
781
  }
@@ -746,7 +792,7 @@ async function runValidate(ctx, body) {
746
792
  }
747
793
 
748
794
  // src/pipeline/flows/static/static-invoke.ts
749
- var import_server3 = require("next/server");
795
+ var import_server4 = require("next/server");
750
796
 
751
797
  // src/types.ts
752
798
  var HttpError = class extends Error {
@@ -757,14 +803,6 @@ var HttpError = class extends Error {
757
803
  }
758
804
  };
759
805
 
760
- // src/pipeline/steps/parse-query.ts
761
- function parseQuery(request, routeEntry) {
762
- if (!routeEntry.querySchema) return void 0;
763
- const params = Object.fromEntries(request.nextUrl.searchParams.entries());
764
- const result = routeEntry.querySchema.safeParse(params);
765
- return result.success ? result.data : params;
766
- }
767
-
768
806
  // src/pipeline/flows/static/static-invoke.ts
769
807
  function invokePaidStatic(ctx, wallet, account, body, payment) {
770
808
  return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
@@ -775,7 +813,7 @@ function invokeUnauthed(ctx, wallet, account, body) {
775
813
  function buildHandlerCtx(ctx, wallet, account, body, payment) {
776
814
  return {
777
815
  body,
778
- query: parseQuery(ctx.request, ctx.routeEntry),
816
+ query: ctx.query,
779
817
  request: ctx.request,
780
818
  requestId: ctx.meta.requestId,
781
819
  route: ctx.routeEntry.key,
@@ -795,10 +833,7 @@ async function runHandler(ctx, handlerCtx) {
795
833
  }
796
834
  if (isAsyncIterable(returned) && !isThenable(returned)) {
797
835
  return errorResult(
798
- new HttpError(
799
- `route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
800
- 500
801
- )
836
+ new HttpError(`route '${ctx.routeEntry.key}': streaming handlers require .metered()`, 500)
802
837
  );
803
838
  }
804
839
  let rawResult;
@@ -807,14 +842,14 @@ async function runHandler(ctx, handlerCtx) {
807
842
  } catch (error) {
808
843
  return errorResult(error);
809
844
  }
810
- const response = rawResult instanceof Response ? rawResult : import_server3.NextResponse.json(rawResult);
845
+ const response = rawResult instanceof Response ? rawResult : import_server4.NextResponse.json(rawResult);
811
846
  return { response, rawResult };
812
847
  }
813
848
  function errorResult(error) {
814
849
  const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
815
850
  const message = error instanceof Error ? error.message : "Internal error";
816
851
  return {
817
- response: import_server3.NextResponse.json({ success: false, error: message }, { status }),
852
+ response: import_server4.NextResponse.json({ success: false, error: message }, { status }),
818
853
  rawResult: void 0,
819
854
  handlerError: error
820
855
  };
@@ -1227,10 +1262,12 @@ var DynamicPricing = class {
1227
1262
  }
1228
1263
  needsBody = true;
1229
1264
  async quote(body) {
1265
+ let priced;
1230
1266
  try {
1231
1267
  const raw = await this.opts.fn(body);
1232
- return this.cap(raw, body);
1268
+ priced = this.cap(raw, body);
1233
1269
  } catch (err) {
1270
+ if (err instanceof HttpError) throw err;
1234
1271
  this.alert("error", `Pricing function failed: ${msg(err)}`, {
1235
1272
  error: err instanceof Error ? err.stack : String(err),
1236
1273
  body
@@ -1241,6 +1278,13 @@ var DynamicPricing = class {
1241
1278
  }
1242
1279
  throw err;
1243
1280
  }
1281
+ if (!isPositiveDecimal(priced)) {
1282
+ throw new HttpError(
1283
+ `route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
1284
+ 500
1285
+ );
1286
+ }
1287
+ return priced;
1244
1288
  }
1245
1289
  challengeQuote(body) {
1246
1290
  if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
@@ -1315,14 +1359,14 @@ var TieredPricing = class {
1315
1359
  `Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
1316
1360
  );
1317
1361
  }
1318
- challengeQuote(body) {
1362
+ async challengeQuote(body) {
1319
1363
  if (body !== void 0) {
1320
1364
  try {
1321
- return this.quote(body);
1365
+ return await this.quote(body);
1322
1366
  } catch {
1323
1367
  }
1324
1368
  }
1325
- return Promise.resolve(this.maxTierPrice());
1369
+ return this.maxTierPrice();
1326
1370
  }
1327
1371
  describe() {
1328
1372
  return {
@@ -1753,7 +1797,7 @@ var mppStrategy = {
1753
1797
  async verify(args) {
1754
1798
  const info = readMppCredential(args.request);
1755
1799
  if (!info) return { ok: false, kind: "invalid" };
1756
- if (args.routeEntry.dynamicPrice) {
1800
+ if (args.routeEntry.billing === "metered") {
1757
1801
  if (!info.sessionAction) return { ok: false, kind: "invalid" };
1758
1802
  return verifySessionMode(args, info);
1759
1803
  }
@@ -1804,7 +1848,7 @@ var mppStrategy = {
1804
1848
  async buildChallenge(args) {
1805
1849
  if (!args.deps.mppx) return {};
1806
1850
  const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
1807
- if (args.routeEntry.dynamicPrice && sessionsConfigured) {
1851
+ if (args.routeEntry.billing === "metered" && sessionsConfigured) {
1808
1852
  const tickCost = args.routeEntry.tickCost;
1809
1853
  const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
1810
1854
  const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
@@ -2056,12 +2100,12 @@ async function settleX402Payment(server, payload, requirements, amountOverride)
2056
2100
  };
2057
2101
  }
2058
2102
  function tagBareDecimalAsDollars(amount) {
2059
- if (/^\d+\.\d+$/.test(amount)) return `$${amount}`;
2103
+ if (/^\d+(?:\.\d+)?$/.test(amount)) return `$${amount}`;
2060
2104
  return amount;
2061
2105
  }
2062
2106
 
2063
2107
  // src/protocols/x402/verify.ts
2064
- var import_types2 = require("@x402/core/types");
2108
+ var import_types3 = require("@x402/core/types");
2065
2109
  async function verifyX402Payment(opts) {
2066
2110
  const { server, request, price, accepts, report } = opts;
2067
2111
  const payload = await readPaymentPayload(request);
@@ -2080,7 +2124,7 @@ async function verifyX402Payment(opts) {
2080
2124
  try {
2081
2125
  verify = await server.verifyPayment(payload, matching);
2082
2126
  } catch (err) {
2083
- if (err instanceof import_types2.VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
2127
+ if (err instanceof import_types3.VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
2084
2128
  return invalidPaymentVerification({
2085
2129
  reason: err.invalidReason ?? "verify_error",
2086
2130
  ...err.invalidMessage ? { message: err.invalidMessage } : {},
@@ -2181,7 +2225,7 @@ async function verifyX402(args) {
2181
2225
  const accepts = await resolveX402Accepts(
2182
2226
  request,
2183
2227
  routeEntry,
2184
- deps.x402Accepts,
2228
+ selectRouteAccepts(deps.x402Accepts, routeEntry),
2185
2229
  deps.payeeAddress,
2186
2230
  body
2187
2231
  );
@@ -2229,7 +2273,7 @@ async function verifyX402(args) {
2229
2273
  async function settleX402(args) {
2230
2274
  const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
2231
2275
  const { payload, requirements } = token;
2232
- const override = routeEntry.dynamicPrice ? { amount: billedAmount } : void 0;
2276
+ const override = routeEntry.billing === "exact" ? void 0 : { amount: billedAmount };
2233
2277
  try {
2234
2278
  const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2235
2279
  if (!settle.result?.success) {
@@ -2259,7 +2303,7 @@ async function buildX402ChallengeContribution(args) {
2259
2303
  const accepts = await resolveX402Accepts(
2260
2304
  request,
2261
2305
  routeEntry,
2262
- deps.x402Accepts,
2306
+ selectRouteAccepts(deps.x402Accepts, routeEntry),
2263
2307
  deps.payeeAddress,
2264
2308
  body
2265
2309
  );
@@ -2303,8 +2347,8 @@ function getAllowedStrategies(allowed) {
2303
2347
  return allowed.map((name) => STRATEGIES[name]);
2304
2348
  }
2305
2349
 
2306
- // src/pipeline/flows/build402.ts
2307
- var import_server4 = require("next/server");
2350
+ // src/pipeline/flows/challenge-response.ts
2351
+ var import_server5 = require("next/server");
2308
2352
 
2309
2353
  // src/pipeline/challenge-extensions.ts
2310
2354
  init_evm();
@@ -2320,20 +2364,17 @@ async function buildChallengeExtensions(ctx) {
2320
2364
  });
2321
2365
  const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
2322
2366
  const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
2323
- if (inputSchema) {
2324
- const config = {
2325
- method: routeEntry.method,
2326
- bodyType: routeEntry.bodySchema ? "json" : void 0,
2327
- inputSchema
2328
- };
2329
- if (routeEntry.inputExample !== void 0) {
2330
- config.input = routeEntry.inputExample;
2331
- }
2332
- if (outputSchema && routeEntry.outputExample !== void 0) {
2333
- config.output = { schema: outputSchema, example: routeEntry.outputExample };
2334
- }
2335
- extensions = declareDiscoveryExtension(config);
2367
+ const isBodyMethod = routeEntry.method === "POST" || routeEntry.method === "PUT" || routeEntry.method === "PATCH";
2368
+ const config = { method: routeEntry.method };
2369
+ if (isBodyMethod) config.bodyType = "json";
2370
+ if (inputSchema) config.inputSchema = inputSchema;
2371
+ if (routeEntry.inputExample !== void 0) {
2372
+ config.input = routeEntry.inputExample;
2373
+ }
2374
+ if (outputSchema && routeEntry.outputExample !== void 0) {
2375
+ config.output = { schema: outputSchema, example: routeEntry.outputExample };
2336
2376
  }
2377
+ extensions = declareDiscoveryExtension(config);
2337
2378
  } catch (err) {
2338
2379
  ctx.report(
2339
2380
  "warn",
@@ -2352,9 +2393,7 @@ async function buildChallengeExtensions(ctx) {
2352
2393
  } catch {
2353
2394
  }
2354
2395
  }
2355
- const hasEvmUpto = ctx.deps.x402Accepts.some(
2356
- (accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
2357
- );
2396
+ const hasEvmUpto = ctx.routeEntry.billing === "upto" && ctx.deps.x402Accepts.some((accept) => accept.scheme === "upto" && isEvmNetwork(accept.network));
2358
2397
  if (hasEvmUpto) {
2359
2398
  try {
2360
2399
  const { declareEip2612GasSponsoringExtension } = await import("@x402/extensions");
@@ -2372,14 +2411,14 @@ async function buildChallengeExtensions(ctx) {
2372
2411
  return extensions;
2373
2412
  }
2374
2413
 
2375
- // src/pipeline/flows/build402.ts
2376
- async function build402(ctx, pricing, body, failure) {
2414
+ // src/pipeline/flows/challenge-response.ts
2415
+ async function buildChallengeResponse(ctx, pricing, body, failure) {
2377
2416
  let challengePrice;
2378
2417
  try {
2379
2418
  challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
2380
2419
  } catch (err) {
2381
2420
  const message = errorMessage(err, "Price calculation failed");
2382
- const errorResponse = import_server4.NextResponse.json(
2421
+ const errorResponse = import_server5.NextResponse.json(
2383
2422
  { success: false, error: message },
2384
2423
  { status: errorStatus(err, 500) }
2385
2424
  );
@@ -2388,7 +2427,7 @@ async function build402(ctx, pricing, body, failure) {
2388
2427
  }
2389
2428
  const extensions = await buildChallengeExtensions(ctx);
2390
2429
  const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
2391
- const response = new import_server4.NextResponse(responseBody, {
2430
+ const response = new import_server5.NextResponse(responseBody, {
2392
2431
  status: 402,
2393
2432
  headers: {
2394
2433
  "Content-Type": "application/json",
@@ -2415,7 +2454,7 @@ async function build402(ctx, pricing, body, failure) {
2415
2454
  const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
2416
2455
  ctx.report("critical", message);
2417
2456
  if (strategy.protocol === "x402") {
2418
- const errorResponse = import_server4.NextResponse.json(
2457
+ const errorResponse = import_server5.NextResponse.json(
2419
2458
  { success: false, error: message },
2420
2459
  { status: 500 }
2421
2460
  );
@@ -2467,7 +2506,7 @@ function surrogatePriceForSkippedBody(routeEntry) {
2467
2506
  }
2468
2507
 
2469
2508
  // src/pipeline/flows/dynamic/dynamic-channel-mgmt.ts
2470
- var import_server5 = require("next/server");
2509
+ var import_server6 = require("next/server");
2471
2510
  async function runDynamicChannelMgmtFlow(args) {
2472
2511
  const { ctx, strategy, account, pricing, skipBody } = args;
2473
2512
  const { request, routeEntry, deps, report } = ctx;
@@ -2486,7 +2525,7 @@ async function runDynamicChannelMgmtFlow(args) {
2486
2525
  if (verifyOutcome.kind === "config") {
2487
2526
  return fail(ctx, 500, verifyOutcome.message, parsedBody);
2488
2527
  }
2489
- return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
2528
+ return buildChallengeResponse(ctx, pricing, parsedBody, verifyOutcome.failure);
2490
2529
  }
2491
2530
  ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2492
2531
  firePaymentVerified(ctx, {
@@ -2495,7 +2534,7 @@ async function runDynamicChannelMgmtFlow(args) {
2495
2534
  amount: price,
2496
2535
  network: verifyOutcome.payment.network
2497
2536
  });
2498
- const synthetic = new import_server5.NextResponse(null, { status: 200 });
2537
+ const synthetic = new import_server6.NextResponse(null, { status: 200 });
2499
2538
  const settleScope = {
2500
2539
  wallet: verifyOutcome.wallet,
2501
2540
  account,
@@ -2521,10 +2560,7 @@ async function runDynamicChannelMgmtFlow(args) {
2521
2560
  });
2522
2561
  }
2523
2562
 
2524
- // src/pipeline/flows/dynamic/dynamic-invoke.ts
2525
- var import_server6 = require("next/server");
2526
-
2527
- // src/pricing/charge-context.ts
2563
+ // src/pricing/metered-charge.ts
2528
2564
  function createChargeContext(args) {
2529
2565
  const { tickCost, maxPrice, route } = args;
2530
2566
  const tickAtomic = decimalToAtomic(tickCost);
@@ -2559,17 +2595,12 @@ function createChargeContext(args) {
2559
2595
  };
2560
2596
  }
2561
2597
 
2562
- // src/pipeline/flows/dynamic/dynamic-invoke.ts
2563
- async function invokeDynamic(ctx, wallet, account, body, payment) {
2564
- const streaming = ctx.routeEntry.streaming === true;
2565
- const chargeContext = streaming ? createChargeContext({
2566
- tickCost: ctx.routeEntry.tickCost,
2567
- maxPrice: ctx.routeEntry.maxPrice,
2568
- route: ctx.routeEntry.key
2569
- }) : null;
2570
- const baseHandlerCtx = {
2598
+ // src/pipeline/flows/dynamic/dynamic-invoke/shared.ts
2599
+ var import_server7 = require("next/server");
2600
+ function buildBaseHandlerCtx(ctx, wallet, account, body, payment) {
2601
+ return {
2571
2602
  body,
2572
- query: parseQuery(ctx.request, ctx.routeEntry),
2603
+ query: ctx.query,
2573
2604
  request: ctx.request,
2574
2605
  requestId: ctx.meta.requestId,
2575
2606
  route: ctx.routeEntry.key,
@@ -2579,12 +2610,41 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
2579
2610
  alert: ctx.report,
2580
2611
  setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
2581
2612
  };
2613
+ }
2614
+ function toResponse(rawResult) {
2615
+ return rawResult instanceof Response ? rawResult : import_server7.NextResponse.json(rawResult);
2616
+ }
2617
+ function errorResult2(error) {
2618
+ const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
2619
+ const message = error instanceof Error ? error.message : "Internal error";
2620
+ return {
2621
+ kind: "request",
2622
+ response: import_server7.NextResponse.json({ success: false, error: message }, { status }),
2623
+ rawResult: void 0,
2624
+ handlerError: error
2625
+ };
2626
+ }
2627
+ function isAsyncIterable2(value) {
2628
+ return value != null && typeof value === "object" && Symbol.asyncIterator in value;
2629
+ }
2630
+ function isThenable2(value) {
2631
+ return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
2632
+ }
2633
+
2634
+ // src/pipeline/flows/dynamic/dynamic-invoke/metered-invoke.ts
2635
+ async function invokeMetered(ctx, wallet, account, body, payment) {
2636
+ const chargeContext = ctx.routeEntry.streaming ? createChargeContext({
2637
+ tickCost: ctx.routeEntry.tickCost,
2638
+ maxPrice: ctx.routeEntry.maxPrice,
2639
+ route: ctx.routeEntry.key
2640
+ }) : null;
2641
+ const baseHandlerCtx = buildBaseHandlerCtx(ctx, wallet, account, body, payment);
2582
2642
  const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
2583
2643
  let returned;
2584
2644
  try {
2585
2645
  returned = ctx.handler(handlerCtx);
2586
2646
  } catch (error) {
2587
- return errorResult2(error, chargeContext);
2647
+ return errorResult2(error);
2588
2648
  }
2589
2649
  if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2590
2650
  if (!chargeContext) {
@@ -2592,41 +2652,80 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
2592
2652
  new HttpError(
2593
2653
  "route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
2594
2654
  500
2595
- ),
2596
- null
2655
+ )
2597
2656
  );
2598
2657
  }
2599
- return {
2600
- kind: "stream",
2601
- source: returned,
2602
- chargeContext
2603
- };
2658
+ return { kind: "stream", source: returned, chargeContext };
2604
2659
  }
2605
2660
  let rawResult;
2606
2661
  try {
2607
2662
  rawResult = await returned;
2608
2663
  } catch (error) {
2609
- return errorResult2(error, chargeContext);
2664
+ return errorResult2(error);
2610
2665
  }
2611
- const response = rawResult instanceof Response ? rawResult : import_server6.NextResponse.json(rawResult);
2612
- return { kind: "request", response, rawResult };
2666
+ return { kind: "request", response: toResponse(rawResult), rawResult };
2613
2667
  }
2614
- function errorResult2(error, chargeContext) {
2615
- const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
2616
- const message = error instanceof Error ? error.message : "Internal error";
2617
- void chargeContext;
2668
+
2669
+ // src/pricing/upto-charge.ts
2670
+ function createUptoChargeContext(args) {
2671
+ const { maxPrice, route } = args;
2672
+ const capAtomic = decimalToAtomic(maxPrice);
2673
+ if (capAtomic <= 0n) {
2674
+ throw new Error(`route '${route}': maxPrice '${maxPrice}' must be a positive decimal string`);
2675
+ }
2676
+ let calls = 0;
2677
+ let atomic = 0n;
2678
+ const charge = async (amount) => {
2679
+ const nextAtomic = atomic + decimalToAtomic(amount);
2680
+ if (nextAtomic > capAtomic) {
2681
+ throw Object.assign(
2682
+ new Error(
2683
+ `route '${route}': charge() running total ($${atomicToDecimal(nextAtomic)}) exceeds maxPrice ($${atomicToDecimal(capAtomic)})`
2684
+ ),
2685
+ { status: 400, code: "CHARGE_OVER_CAP" }
2686
+ );
2687
+ }
2688
+ calls += 1;
2689
+ atomic = nextAtomic;
2690
+ };
2618
2691
  return {
2619
- kind: "request",
2620
- response: import_server6.NextResponse.json({ success: false, error: message }, { status }),
2621
- rawResult: void 0,
2622
- handlerError: error
2692
+ charge,
2693
+ callCount: () => calls,
2694
+ atomicTotal: () => atomic
2623
2695
  };
2624
2696
  }
2625
- function isAsyncIterable2(value) {
2626
- return value != null && typeof value === "object" && Symbol.asyncIterator in value;
2627
- }
2628
- function isThenable2(value) {
2629
- return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
2697
+
2698
+ // src/pipeline/flows/dynamic/dynamic-invoke/upto-invoke.ts
2699
+ async function invokeUpto(ctx, wallet, account, body, payment) {
2700
+ const uptoCtx = createUptoChargeContext({
2701
+ maxPrice: ctx.routeEntry.maxPrice,
2702
+ route: ctx.routeEntry.key
2703
+ });
2704
+ const handlerCtx = {
2705
+ ...buildBaseHandlerCtx(ctx, wallet, account, body, payment),
2706
+ charge: uptoCtx.charge
2707
+ };
2708
+ let returned;
2709
+ try {
2710
+ returned = ctx.handler(handlerCtx);
2711
+ } catch (error) {
2712
+ return errorResult2(error);
2713
+ }
2714
+ if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2715
+ return errorResult2(
2716
+ new HttpError(
2717
+ "streaming is not supported on .upTo() routes \u2014 return a value from .handler() instead",
2718
+ 500
2719
+ )
2720
+ );
2721
+ }
2722
+ let rawResult;
2723
+ try {
2724
+ rawResult = await returned;
2725
+ } catch (error) {
2726
+ return errorResult2(error);
2727
+ }
2728
+ return { kind: "request", response: toResponse(rawResult), rawResult, uptoContext: uptoCtx };
2630
2729
  }
2631
2730
 
2632
2731
  // src/pipeline/flows/dynamic/dynamic-preflight.ts
@@ -2656,7 +2755,7 @@ async function runDynamicRequestFlow(args) {
2656
2755
  }
2657
2756
  const beforeErr = await runBeforeSettle(ctx, settleScope);
2658
2757
  if (beforeErr) return beforeErr;
2659
- const billedAmount = routeEntry.tickCost;
2758
+ const billedAmount = computeBilledAmount(routeEntry, result);
2660
2759
  return settleAndFinalizeRequest({
2661
2760
  ctx,
2662
2761
  strategy,
@@ -2674,6 +2773,19 @@ async function runDynamicRequestFlow(args) {
2674
2773
  }
2675
2774
  });
2676
2775
  }
2776
+ function computeBilledAmount(routeEntry, result) {
2777
+ if (routeEntry.billing === "upto") {
2778
+ const total = result.uptoContext?.atomicTotal() ?? 0n;
2779
+ if (total <= 0n) {
2780
+ throw new HttpError(
2781
+ `route '${routeEntry.key}': handler did not call charge(amount) \u2014 upto routes must accumulate a non-zero billed amount`,
2782
+ 500
2783
+ );
2784
+ }
2785
+ return atomicToDecimal(total);
2786
+ }
2787
+ return routeEntry.tickCost;
2788
+ }
2677
2789
 
2678
2790
  // src/pipeline/flows/dynamic/dynamic-stream.ts
2679
2791
  async function runDynamicStreamFlow(args) {
@@ -2710,7 +2822,7 @@ async function runDynamicPaidFlow(ctx) {
2710
2822
  if (!incomingStrategy) {
2711
2823
  const initError = protocolInitError(routeEntry, deps);
2712
2824
  if (initError) return fail(ctx, 500, initError);
2713
- return build402(ctx, pricing, earlyBody);
2825
+ return buildChallengeResponse(ctx, pricing, earlyBody);
2714
2826
  }
2715
2827
  const { skipBody, skipHandler } = resolveDynamicPreflight(incomingStrategy, request, routeEntry);
2716
2828
  if (skipHandler) {
@@ -2737,7 +2849,7 @@ async function runDynamicPaidFlow(ctx) {
2737
2849
  if (verifyOutcome.kind === "config") {
2738
2850
  return fail(ctx, 500, verifyOutcome.message, parsedBody);
2739
2851
  }
2740
- return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
2852
+ return buildChallengeResponse(ctx, pricing, parsedBody, verifyOutcome.failure);
2741
2853
  }
2742
2854
  ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2743
2855
  firePaymentVerified(ctx, {
@@ -2746,13 +2858,7 @@ async function runDynamicPaidFlow(ctx) {
2746
2858
  amount: price,
2747
2859
  network: verifyOutcome.payment.network
2748
2860
  });
2749
- const result = await invokeDynamic(
2750
- ctx,
2751
- verifyOutcome.wallet,
2752
- account,
2753
- parsedBody,
2754
- verifyOutcome.payment
2755
- );
2861
+ const result = await invokeDynamic(ctx, verifyOutcome, account, parsedBody);
2756
2862
  switch (result.kind) {
2757
2863
  case "stream":
2758
2864
  return runDynamicStreamFlow({
@@ -2774,6 +2880,18 @@ async function runDynamicPaidFlow(ctx) {
2774
2880
  });
2775
2881
  }
2776
2882
  }
2883
+ async function invokeDynamic(ctx, verifyOutcome, account, parsedBody) {
2884
+ switch (ctx.routeEntry.billing) {
2885
+ case "upto":
2886
+ return invokeUpto(ctx, verifyOutcome.wallet, account, parsedBody, verifyOutcome.payment);
2887
+ case "metered":
2888
+ return invokeMetered(ctx, verifyOutcome.wallet, account, parsedBody, verifyOutcome.payment);
2889
+ case "exact":
2890
+ throw new Error(
2891
+ `route '${ctx.routeEntry.key}': exact billing must not reach the dynamic paid flow`
2892
+ );
2893
+ }
2894
+ }
2777
2895
 
2778
2896
  // src/pipeline/flows/static/static-body-and-price.ts
2779
2897
  async function resolveStaticBodyAndPrice(args) {
@@ -2875,7 +2993,7 @@ async function runStaticPaidFlow(ctx) {
2875
2993
  if (!incomingStrategy) {
2876
2994
  const initError = protocolInitError(routeEntry, deps);
2877
2995
  if (initError) return fail(ctx, 500, initError);
2878
- return build402(ctx, pricing, earlyBody);
2996
+ return buildChallengeResponse(ctx, pricing, earlyBody);
2879
2997
  }
2880
2998
  const bodyAndPrice = await resolveStaticBodyAndPrice({ ctx, pricing });
2881
2999
  if (!bodyAndPrice.ok) return bodyAndPrice.response;
@@ -2892,7 +3010,7 @@ async function runStaticPaidFlow(ctx) {
2892
3010
  if (verifyOutcome.kind === "config") {
2893
3011
  return fail(ctx, 500, verifyOutcome.message, parsedBody);
2894
3012
  }
2895
- return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
3013
+ return buildChallengeResponse(ctx, pricing, parsedBody, verifyOutcome.failure);
2896
3014
  }
2897
3015
  ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2898
3016
  firePaymentVerified(ctx, {
@@ -2921,17 +3039,12 @@ async function runStaticPaidFlow(ctx) {
2921
3039
 
2922
3040
  // src/pipeline/flows/paid.ts
2923
3041
  async function runPaidFlow(ctx) {
2924
- const dynamicPrice = ctx.routeEntry.dynamicPrice ?? false;
2925
- switch (dynamicPrice) {
2926
- case true:
2927
- return runDynamicPaidFlow(ctx);
2928
- case false:
2929
- return runStaticPaidFlow(ctx);
2930
- }
3042
+ const handlerCharged = ctx.routeEntry.billing !== "exact";
3043
+ return handlerCharged ? runDynamicPaidFlow(ctx) : runStaticPaidFlow(ctx);
2931
3044
  }
2932
3045
 
2933
3046
  // src/pipeline/flows/siwx-only.ts
2934
- var import_server7 = require("next/server");
3047
+ var import_server8 = require("next/server");
2935
3048
 
2936
3049
  // src/kv-store/client.ts
2937
3050
  var BIGINT_SUFFIX = "#__bigint";
@@ -3165,7 +3278,7 @@ async function runSiwxOnlyFlow(ctx) {
3165
3278
  }
3166
3279
  const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
3167
3280
  if (!siwx.valid) {
3168
- const response = import_server7.NextResponse.json(
3281
+ const response = import_server8.NextResponse.json(
3169
3282
  { error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
3170
3283
  { status: 402 }
3171
3284
  );
@@ -3226,7 +3339,7 @@ async function buildSiwxChallenge(ctx) {
3226
3339
  `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`
3227
3340
  );
3228
3341
  }
3229
- const response = new import_server7.NextResponse(JSON.stringify(paymentRequired), {
3342
+ const response = new import_server8.NextResponse(JSON.stringify(paymentRequired), {
3230
3343
  status: 402,
3231
3344
  headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
3232
3345
  });
@@ -3272,6 +3385,9 @@ function createRequestHandler(routeEntry, handler, deps) {
3272
3385
  return async (request) => {
3273
3386
  await deps.initPromise;
3274
3387
  const ctx = preflight(routeEntry, handler, deps, request);
3388
+ const query = validateQuery(ctx);
3389
+ if (!query.ok) return query.response;
3390
+ ctx.query = query.data;
3275
3391
  if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
3276
3392
  if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
3277
3393
  if (routeEntry.pricing) return runPaidFlow(ctx);
@@ -3325,7 +3441,7 @@ var RouteBuilder = class _RouteBuilder {
3325
3441
  protocols: defaults?.protocols ? [...defaults.protocols] : ["x402"],
3326
3442
  maxPrice: void 0,
3327
3443
  minPrice: void 0,
3328
- dynamicPrice: false,
3444
+ billing: "exact",
3329
3445
  tickCost: void 0,
3330
3446
  unitType: void 0,
3331
3447
  payTo: void 0,
@@ -3356,39 +3472,84 @@ var RouteBuilder = class _RouteBuilder {
3356
3472
  next.#s = { ...this.#s, protocols: [...this.#s.protocols] };
3357
3473
  return next;
3358
3474
  }
3359
- paid(pricingOrOptions, options) {
3360
- const { pricing, resolvedOptions } = resolvePaidArgs(this.#s.key, pricingOrOptions, options);
3475
+ paid(arg, options) {
3476
+ return this.applyPaid(normalizePaidArg(this.#s.key, arg, options), "paid");
3477
+ }
3478
+ /**
3479
+ * x402-only handler-computed billing. The handler receives `charge(amount)`
3480
+ * and the request settles once for the accumulated total, capped at
3481
+ * `maxPrice`. Requires an `'upto'` accept on at least one configured network.
3482
+ * Pass a bare string as sugar for `{ maxPrice }`.
3483
+ *
3484
+ * @example
3485
+ * ```ts
3486
+ * router.route('llm')
3487
+ * .upTo('0.05')
3488
+ * .body(schema)
3489
+ * .handler(async ({ body, charge }) => { await charge('0.001'); ... });
3490
+ * ```
3491
+ */
3492
+ upTo(arg) {
3493
+ return this.applyPaid(normalizeUpToArg(this.#s.key, arg), "upTo");
3494
+ }
3495
+ /**
3496
+ * MPP-only per-tick billing. `.handler()` bills exactly `tickCost`;
3497
+ * `.stream()` calls `charge()` (no-arg) per yield, settling per tick up to
3498
+ * `maxPrice`. Requires `RouterConfig.mpp.session`.
3499
+ *
3500
+ * @example
3501
+ * ```ts
3502
+ * router.route('llm/stream')
3503
+ * .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
3504
+ * .stream(async function* ({ charge }) { await charge(); yield 'hi'; });
3505
+ * ```
3506
+ */
3507
+ metered(options) {
3508
+ return this.applyPaid(normalizeMeteredArg(this.#s.key, options), "metered");
3509
+ }
3510
+ applyPaid(normalized, method) {
3511
+ const { pricing, resolvedOptions, billing, tickCost, unitType, maxPrice } = normalized;
3361
3512
  if (this.#s.authMode === "unprotected") {
3362
3513
  throw new Error(
3363
- `route '${this.#s.key}': Cannot combine .unprotected() and .paid() on the same route.`
3514
+ `route '${this.#s.key}': Cannot combine .unprotected() and .${method}() on the same route.`
3364
3515
  );
3365
3516
  }
3366
3517
  if (this.#s.pricing !== void 0) {
3367
3518
  throw new Error(
3368
- `route '${this.#s.key}': Cannot call .paid() more than once on the same route.`
3519
+ `route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
3369
3520
  );
3370
3521
  }
3371
3522
  const next = this.fork();
3372
3523
  next.#s.authMode = "paid";
3373
3524
  next.#s.pricing = pricing;
3374
- if (resolvedOptions?.protocols) {
3525
+ if (billing === "upto") {
3526
+ if (resolvedOptions.protocols?.some((p) => p !== "x402")) {
3527
+ throw new Error(
3528
+ `route '${this.#s.key}': .upTo() is x402-only \u2014 remove the conflicting protocols override.`
3529
+ );
3530
+ }
3531
+ next.#s.protocols = ["x402"];
3532
+ } else if (billing === "metered") {
3533
+ if (resolvedOptions.protocols?.some((p) => p !== "mpp")) {
3534
+ throw new Error(
3535
+ `route '${this.#s.key}': .metered() is MPP-only \u2014 remove the conflicting protocols override.`
3536
+ );
3537
+ }
3538
+ next.#s.protocols = ["mpp"];
3539
+ } else if (resolvedOptions.protocols) {
3375
3540
  next.#s.protocols = [...resolvedOptions.protocols];
3376
3541
  } else if (next.#s.protocols.length === 0) {
3377
3542
  next.#s.protocols = ["x402"];
3378
3543
  }
3379
- if (resolvedOptions?.maxPrice) next.#s.maxPrice = resolvedOptions.maxPrice;
3380
- if (resolvedOptions?.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
3381
- if (resolvedOptions?.payTo) next.#s.payTo = resolvedOptions.payTo;
3382
- if (resolvedOptions?.mpp) next.#s.mppInfo = resolvedOptions.mpp;
3383
- if (resolvedOptions?.dynamic) next.#s.dynamicPrice = true;
3384
- if (resolvedOptions?.tickCost) next.#s.tickCost = resolvedOptions.tickCost;
3385
- if (resolvedOptions?.unitType) next.#s.unitType = resolvedOptions.unitType;
3544
+ if (resolvedOptions.maxPrice) next.#s.maxPrice = resolvedOptions.maxPrice;
3545
+ if (maxPrice) next.#s.maxPrice = maxPrice;
3546
+ if (resolvedOptions.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
3547
+ if (resolvedOptions.payTo) next.#s.payTo = resolvedOptions.payTo;
3548
+ if (resolvedOptions.mpp) next.#s.mppInfo = resolvedOptions.mpp;
3549
+ next.#s.billing = billing;
3550
+ if (tickCost) next.#s.tickCost = tickCost;
3551
+ if (unitType) next.#s.unitType = unitType;
3386
3552
  if (typeof pricing === "object" && "tiers" in pricing) {
3387
- if (next.#s.dynamicPrice) {
3388
- throw new Error(
3389
- `route '${this.#s.key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
3390
- );
3391
- }
3392
3553
  for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
3393
3554
  if (!tierKey) {
3394
3555
  throw new Error(`route '${this.#s.key}': tier key cannot be empty`);
@@ -3400,21 +3561,25 @@ var RouteBuilder = class _RouteBuilder {
3400
3561
  }
3401
3562
  }
3402
3563
  }
3403
- if (resolvedOptions?.maxPrice !== void 0 && !isPositiveDecimal(resolvedOptions.maxPrice)) {
3564
+ if (billing === "exact" && typeof pricing === "string" && !isPositiveDecimal(pricing)) {
3404
3565
  throw new Error(
3405
- `route '${this.#s.key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
3566
+ `route '${this.#s.key}': price '${pricing}' must be a positive decimal string`
3406
3567
  );
3407
3568
  }
3408
- if (resolvedOptions?.tickCost !== void 0 && !isPositiveDecimal(resolvedOptions.tickCost)) {
3569
+ if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
3409
3570
  throw new Error(
3410
- `route '${this.#s.key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
3571
+ `route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
3411
3572
  );
3412
3573
  }
3413
- if (next.#s.dynamicPrice && !next.#s.maxPrice) {
3414
- throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires maxPrice`);
3574
+ if (next.#s.minPrice !== void 0 && !isPositiveDecimal(next.#s.minPrice)) {
3575
+ throw new Error(
3576
+ `route '${this.#s.key}': minPrice '${next.#s.minPrice}' must be a positive decimal string`
3577
+ );
3415
3578
  }
3416
- if (next.#s.dynamicPrice && !next.#s.tickCost) {
3417
- throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires tickCost`);
3579
+ if (next.#s.tickCost !== void 0 && !isPositiveDecimal(next.#s.tickCost)) {
3580
+ throw new Error(
3581
+ `route '${this.#s.key}': tickCost '${next.#s.tickCost}' must be a positive decimal string`
3582
+ );
3418
3583
  }
3419
3584
  return next;
3420
3585
  }
@@ -3697,13 +3862,13 @@ var RouteBuilder = class _RouteBuilder {
3697
3862
  /**
3698
3863
  * Register a streaming handler (`async function*`) and return the Next.js
3699
3864
  * route function. Each `charge()` call bills one tick (`tickCost` USDC) up
3700
- * to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
3865
+ * to `maxPrice`; requires `.metered({ ... })` and MPP session mode.
3701
3866
  *
3702
3867
  * @example
3703
3868
  * ```ts
3704
3869
  * export const POST = router
3705
3870
  * .route('llm/stream')
3706
- * .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
3871
+ * .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
3707
3872
  * .body(schema)
3708
3873
  * .stream(async function* ({ body, charge }) {
3709
3874
  * for await (const token of streamLLM(body.prompt)) {
@@ -3719,7 +3884,7 @@ var RouteBuilder = class _RouteBuilder {
3719
3884
  register(handlerFn, streaming) {
3720
3885
  if (!this.#s.authMode) {
3721
3886
  throw new Error(
3722
- `route '${this.#s.key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
3887
+ `route '${this.#s.key}': Select an auth mode: .paid(pricing), .upTo(maxPrice), .metered(options), .siwx(), .apiKey(resolver), or .unprotected()`
3723
3888
  );
3724
3889
  }
3725
3890
  if (this.#s.validateFn && !this.#s.bodySchema) {
@@ -3730,24 +3895,34 @@ var RouteBuilder = class _RouteBuilder {
3730
3895
  if (this.#s.settlement && !this.#s.pricing) {
3731
3896
  throw new Error(`route '${this.#s.key}': .settlement() requires a paid route`);
3732
3897
  }
3733
- if (this.#s.dynamicPrice && this.#s.protocols.includes("x402")) {
3898
+ if (this.#s.billing === "upto") {
3734
3899
  const hasUpto = this.#s.deps.x402Accepts.some((accept) => accept.scheme === "upto");
3735
3900
  if (!hasUpto) {
3736
3901
  throw new Error(
3737
- `route '${this.#s.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.`
3902
+ `route '${this.#s.key}': .upTo() requires an 'upto' accept on at least one configured network. Add { scheme: 'upto', network, asset } to RouterConfig.x402.accepts.`
3738
3903
  );
3739
3904
  }
3740
3905
  }
3741
- if (this.#s.dynamicPrice && this.#s.protocols.includes("mpp")) {
3906
+ if (this.#s.pricing !== void 0 && this.#s.billing === "exact" && this.#s.protocols.includes("x402")) {
3907
+ const hasExact = this.#s.deps.x402Accepts.some(
3908
+ (accept) => (accept.scheme ?? "exact") !== "upto"
3909
+ );
3910
+ if (!hasExact) {
3911
+ throw new Error(
3912
+ `route '${this.#s.key}': .paid() needs a non-'upto' x402 accept \u2014 an 'upto'-only accept list cannot serve a fixed-price route. Add { scheme: 'exact', network } to RouterConfig.x402.accepts, or use .upTo() for handler-computed billing.`
3913
+ );
3914
+ }
3915
+ }
3916
+ if (this.#s.billing === "metered") {
3742
3917
  if (!this.#s.deps.mppSessionConfig) {
3743
3918
  throw new Error(
3744
- `route '${this.#s.key}': .paid({ dynamic: true }) on an MPP route requires session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
3919
+ `route '${this.#s.key}': .metered() requires MPP session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
3745
3920
  );
3746
3921
  }
3747
3922
  }
3748
- if (streaming && !this.#s.dynamicPrice) {
3923
+ if (streaming && this.#s.billing !== "metered") {
3749
3924
  throw new Error(
3750
- `route '${this.#s.key}': .stream() requires .paid({ dynamic: true }) \u2014 static/free routes can't meter per-chunk billing.`
3925
+ `route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
3751
3926
  );
3752
3927
  }
3753
3928
  validateExamples(
@@ -3765,7 +3940,7 @@ var RouteBuilder = class _RouteBuilder {
3765
3940
  authMode: this.#s.authMode,
3766
3941
  siwxEnabled: this.#s.siwxEnabled,
3767
3942
  pricing: this.#s.pricing,
3768
- dynamicPrice: this.#s.dynamicPrice ? true : void 0,
3943
+ billing: this.#s.billing,
3769
3944
  streaming: streaming ? true : void 0,
3770
3945
  protocols: this.#s.protocols,
3771
3946
  bodySchema: this.#s.bodySchema,
@@ -3792,20 +3967,63 @@ var RouteBuilder = class _RouteBuilder {
3792
3967
  return createRequestHandler(entry, handlerFn, this.#s.deps);
3793
3968
  }
3794
3969
  };
3795
- function resolvePaidArgs(routeKey, pricingOrOptions, options) {
3796
- const isHandlerDynamicShape = typeof pricingOrOptions === "object" && pricingOrOptions !== null && typeof pricingOrOptions !== "function" && !("tiers" in pricingOrOptions) && "dynamic" in pricingOrOptions && pricingOrOptions.dynamic;
3797
- if (isHandlerDynamicShape) {
3798
- const opts = pricingOrOptions;
3799
- if (!opts.maxPrice) {
3800
- throw new Error(`route '${routeKey}': .paid({ dynamic: true }) requires maxPrice`);
3801
- }
3802
- return { pricing: opts.maxPrice, resolvedOptions: opts };
3970
+ function normalizePaidArg(routeKey, arg, options) {
3971
+ if (typeof arg === "string") {
3972
+ return { pricing: arg, resolvedOptions: options ?? {}, billing: "exact" };
3803
3973
  }
3804
- return { pricing: pricingOrOptions, resolvedOptions: options };
3974
+ if (typeof arg === "function") {
3975
+ return {
3976
+ pricing: arg,
3977
+ resolvedOptions: options ?? {},
3978
+ billing: "exact"
3979
+ };
3980
+ }
3981
+ if ("tiers" in arg && "field" in arg) {
3982
+ return {
3983
+ pricing: { field: arg.field, tiers: arg.tiers, default: arg.default },
3984
+ resolvedOptions: arg,
3985
+ billing: "exact"
3986
+ };
3987
+ }
3988
+ if ("price" in arg && typeof arg.price === "string") {
3989
+ return { pricing: arg.price, resolvedOptions: arg, billing: "exact" };
3990
+ }
3991
+ throw new Error(
3992
+ `route '${routeKey}': .paid() requires one of: a price string, a (body) => string function, { price }, or { field, tiers }. For handler-computed billing use .upTo(); for per-tick billing use .metered().`
3993
+ );
3994
+ }
3995
+ function normalizeUpToArg(routeKey, arg) {
3996
+ const options = typeof arg === "string" ? { maxPrice: arg } : arg;
3997
+ if (!options.maxPrice) {
3998
+ throw new Error(`route '${routeKey}': .upTo() requires maxPrice`);
3999
+ }
4000
+ return {
4001
+ pricing: options.maxPrice,
4002
+ resolvedOptions: options,
4003
+ billing: "upto",
4004
+ unitType: options.unitType,
4005
+ maxPrice: options.maxPrice
4006
+ };
4007
+ }
4008
+ function normalizeMeteredArg(routeKey, options) {
4009
+ if (!options.maxPrice) {
4010
+ throw new Error(`route '${routeKey}': .metered() requires maxPrice`);
4011
+ }
4012
+ if (!options.tickCost) {
4013
+ throw new Error(`route '${routeKey}': .metered() requires tickCost`);
4014
+ }
4015
+ return {
4016
+ pricing: options.maxPrice,
4017
+ resolvedOptions: options,
4018
+ billing: "metered",
4019
+ tickCost: options.tickCost,
4020
+ unitType: options.unitType,
4021
+ maxPrice: options.maxPrice
4022
+ };
3805
4023
  }
3806
4024
 
3807
4025
  // src/discovery/well-known.ts
3808
- var import_server8 = require("next/server");
4026
+ var import_server9 = require("next/server");
3809
4027
 
3810
4028
  // src/discovery/utils/guidance.ts
3811
4029
  async function resolveGuidance(discovery) {
@@ -3849,7 +4067,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
3849
4067
  if (instructions) {
3850
4068
  body.instructions = instructions;
3851
4069
  }
3852
- return import_server8.NextResponse.json(body, {
4070
+ return import_server9.NextResponse.json(body, {
3853
4071
  headers: {
3854
4072
  "Access-Control-Allow-Origin": "*",
3855
4073
  "Access-Control-Allow-Methods": "GET",
@@ -3866,14 +4084,14 @@ function toDiscoveryResource(method, url, mode) {
3866
4084
  }
3867
4085
 
3868
4086
  // src/discovery/openapi.ts
3869
- var import_server9 = require("next/server");
4087
+ var import_server10 = require("next/server");
3870
4088
  init_constants();
3871
4089
  function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
3872
4090
  const normalizedBase = baseUrl.replace(/\/+$/, "");
3873
4091
  let cached = null;
3874
4092
  let validated = false;
3875
4093
  return async (_request) => {
3876
- if (cached) return import_server9.NextResponse.json(cached);
4094
+ if (cached) return import_server10.NextResponse.json(cached);
3877
4095
  if (!validated && pricesKeys) {
3878
4096
  registry.validate(pricesKeys);
3879
4097
  validated = true;
@@ -3936,7 +4154,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
3936
4154
  paths
3937
4155
  };
3938
4156
  cached = createDocument(openApiDocument);
3939
- return import_server9.NextResponse.json(cached);
4157
+ return import_server10.NextResponse.json(cached);
3940
4158
  };
3941
4159
  }
3942
4160
  function deriveTag(routeKey) {
@@ -4072,11 +4290,11 @@ function tierExtrema(prices) {
4072
4290
  }
4073
4291
 
4074
4292
  // src/discovery/llms-txt.ts
4075
- var import_server10 = require("next/server");
4293
+ var import_server11 = require("next/server");
4076
4294
  function createLlmsTxtHandler(discovery) {
4077
4295
  return async (_request) => {
4078
4296
  const guidance = await resolveGuidance(discovery) ?? "";
4079
- return new import_server10.NextResponse(guidance, {
4297
+ return new import_server11.NextResponse(guidance, {
4080
4298
  headers: {
4081
4299
  "Content-Type": "text/plain; charset=utf-8",
4082
4300
  "Access-Control-Allow-Origin": "*"
@@ -4126,10 +4344,14 @@ var isPlaceholderEvm = (v) => ZERO_EVM_ADDRESS_RE.test(v);
4126
4344
  var isSolanaAddress = (v) => SOLANA_ADDRESS_RE.test(v);
4127
4345
  var isX402Network = (v) => v.startsWith("eip155:") || v.startsWith("solana:");
4128
4346
  var canonicalizeEvm = (addr) => addr.toLowerCase();
4347
+ function evmAddressFromKey(key) {
4348
+ if (!key || !isEvmPrivateKey(key)) return null;
4349
+ return (0, import_accounts.privateKeyToAccount)(key).address.toLowerCase();
4350
+ }
4129
4351
  function operatorAddressesCollide(opKey, fpKey) {
4130
- if (!opKey || !fpKey || !isEvmPrivateKey(opKey) || !isEvmPrivateKey(fpKey)) return null;
4131
- const op = (0, import_accounts.privateKeyToAccount)(opKey).address.toLowerCase();
4132
- const fp = (0, import_accounts.privateKeyToAccount)(fpKey).address.toLowerCase();
4352
+ const op = evmAddressFromKey(opKey);
4353
+ const fp = evmAddressFromKey(fpKey);
4354
+ if (!op || !fp) return null;
4133
4355
  return op === fp ? op : null;
4134
4356
  }
4135
4357
  function trimAll(raw) {
@@ -4291,7 +4513,7 @@ function getConfiguredX402Accepts2(config) {
4291
4513
  }
4292
4514
  ];
4293
4515
  }
4294
- function validateX402Config(config, env, options) {
4516
+ function validateX402Config(config, env) {
4295
4517
  const accepts = getConfiguredX402Accepts2(config);
4296
4518
  const issues = [];
4297
4519
  const push = (code, message) => issues.push({ code, protocol: "x402", message });
@@ -4333,23 +4555,21 @@ function validateX402Config(config, env, options) {
4333
4555
  `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
4334
4556
  );
4335
4557
  }
4336
- if (options.requireCdpKeys !== false) {
4337
- const hasEvm = accepts.some(
4338
- (a) => typeof a.network === "string" && a.network.startsWith("eip155:")
4339
- );
4340
- if (hasEvm) {
4341
- const missing = ["CDP_API_KEY_ID", "CDP_API_KEY_SECRET"].filter((k) => !env[k]);
4342
- if (missing.length > 0) {
4343
- push(
4344
- "missing_cdp_keys",
4345
- `x402 EVM facilitator (Coinbase) requires ${missing.join(" and ")}.`
4346
- );
4347
- }
4558
+ const hasEvm = accepts.some(
4559
+ (a) => typeof a.network === "string" && a.network.startsWith("eip155:")
4560
+ );
4561
+ if (hasEvm) {
4562
+ const missing = ["CDP_API_KEY_ID", "CDP_API_KEY_SECRET"].filter((k) => !env[k]);
4563
+ if (missing.length > 0) {
4564
+ push(
4565
+ "missing_cdp_keys",
4566
+ `x402 EVM facilitator (Coinbase) requires ${missing.join(" and ")}. Create an API key at https://portal.cdp.coinbase.com and set it via env.`
4567
+ );
4348
4568
  }
4349
4569
  }
4350
4570
  return issues;
4351
4571
  }
4352
- function validateMppConfig(config) {
4572
+ function validateMppConfig(config, env) {
4353
4573
  const m = config.mpp;
4354
4574
  if (!m) {
4355
4575
  return [
@@ -4397,7 +4617,7 @@ function validateMppConfig(config) {
4397
4617
  `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
4398
4618
  );
4399
4619
  }
4400
- if (!m.rpcUrl) {
4620
+ if (!m.rpcUrl && !env.TEMPO_RPC_URL) {
4401
4621
  push(
4402
4622
  "missing_mpp_rpc_url",
4403
4623
  "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
@@ -4422,6 +4642,15 @@ function validateMppConfig(config) {
4422
4642
  `MPP operatorKey and feePayerKey resolve to the same address (${collision}). 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).`
4423
4643
  );
4424
4644
  }
4645
+ if (m.session && recipient && isEvmAddress(recipient)) {
4646
+ const operatorAddress = evmAddressFromKey(m.operatorKey);
4647
+ if (operatorAddress && operatorAddress !== recipient.toLowerCase()) {
4648
+ push(
4649
+ "mpp_operator_recipient_mismatch",
4650
+ `MPP session operatorKey resolves to ${operatorAddress}, which must equal the recipient/payee ${recipient.toLowerCase()}. mppx's channel-close handler asserts sender === payee. Set mpp.operatorKey to the recipient\u2019s private key, or set mpp.recipient/payeeAddress to the operator address.`
4651
+ );
4652
+ }
4653
+ }
4425
4654
  return issues;
4426
4655
  }
4427
4656
  function translateZodIssues(error) {
@@ -4550,8 +4779,8 @@ function getRouterConfigIssues(config, options = {}) {
4550
4779
  message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
4551
4780
  });
4552
4781
  }
4553
- if (protocols.includes("x402")) issues.push(...validateX402Config(config, env, options));
4554
- if (protocols.includes("mpp")) issues.push(...validateMppConfig(config));
4782
+ if (protocols.includes("x402")) issues.push(...validateX402Config(config, env));
4783
+ if (protocols.includes("mpp")) issues.push(...validateMppConfig(config, env));
4555
4784
  return issues;
4556
4785
  }
4557
4786
 
@@ -4636,9 +4865,6 @@ async function initMpp(config, resolvedBaseUrl, kvStore, configError) {
4636
4865
  const getClient = async () => tempoClient;
4637
4866
  const operatorAccount = config.mpp.operatorKey ? privateKeyToAccount2(config.mpp.operatorKey) : void 0;
4638
4867
  const feePayerAccount = config.mpp.feePayerKey ? privateKeyToAccount2(config.mpp.feePayerKey) : void 0;
4639
- if (config.mpp.session && operatorAccount) {
4640
- assertOperatorMatchesRecipient(config, operatorAccount.address);
4641
- }
4642
4868
  const resolvedStore = kvStore ? await createKvMppStore(kvStore) : void 0;
4643
4869
  const realm = new URL(resolvedBaseUrl).host;
4644
4870
  const mppConfig = config.mpp;
@@ -4676,15 +4902,6 @@ async function initMpp(config, resolvedBaseUrl, kvStore, configError) {
4676
4902
  return { initError: err instanceof Error ? err.message : String(err) };
4677
4903
  }
4678
4904
  }
4679
- function assertOperatorMatchesRecipient(config, operatorAddress) {
4680
- const recipient = (config.mpp?.recipient ?? config.payeeAddress)?.toLowerCase();
4681
- const opAddr = operatorAddress.toLowerCase();
4682
- if (recipient && opAddr !== recipient) {
4683
- throw new Error(
4684
- `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}.`
4685
- );
4686
- }
4687
- }
4688
4905
 
4689
4906
  // src/index.ts
4690
4907
  init_constants();
@@ -4695,10 +4912,7 @@ function createRouter(config) {
4695
4912
  const entitlementStore = kvStore ? createKvEntitlementStore(kvStore) : new MemoryEntitlementStore();
4696
4913
  const network = config.network ?? BASE_MAINNET_NETWORK;
4697
4914
  const x402Accepts = getConfiguredX402Accepts(config);
4698
- const configIssues = getRouterConfigIssues(config, {
4699
- env: process.env,
4700
- requireCdpKeys: process.env.NODE_ENV === "production"
4701
- });
4915
+ const configIssues = getRouterConfigIssues(config, { env: process.env });
4702
4916
  const baseUrlIssue = configIssues.find((issue) => issue.code === "missing_base_url");
4703
4917
  if (baseUrlIssue) throw new RouterConfigError([baseUrlIssue]);
4704
4918
  const emptyProtocolsIssue = configIssues.find((issue) => issue.code === "empty_protocols");
@@ -4711,10 +4925,7 @@ function createRouter(config) {
4711
4925
  const x402ConfigError = x402ConfigIssues.length > 0 ? formatRouterConfigIssues(x402ConfigIssues) : void 0;
4712
4926
  const mppConfigError = mppConfigIssues.length > 0 ? formatRouterConfigIssues(mppConfigIssues) : void 0;
4713
4927
  if (protocolConfigIssues.length > 0) {
4714
- for (const issue of protocolConfigIssues) console.error(`[router] ${issue.message}`);
4715
- if (process.env.NODE_ENV === "production") {
4716
- throw new RouterConfigError(protocolConfigIssues);
4717
- }
4928
+ throw new RouterConfigError(protocolConfigIssues);
4718
4929
  }
4719
4930
  const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
4720
4931
  if (config.plugin?.init) {
@@ -4740,7 +4951,7 @@ function createRouter(config) {
4740
4951
  x402Accepts,
4741
4952
  mppx: null,
4742
4953
  tempoClient: null,
4743
- mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
4954
+ mppSessionConfig: config.mpp?.session && config.mpp.operatorKey ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
4744
4955
  };
4745
4956
  deps.initPromise = (async () => {
4746
4957
  const x402Result = await initX402(config, kvStore, x402ConfigError);