@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.js CHANGED
@@ -45,6 +45,9 @@ function getConfiguredX402Accepts(config) {
45
45
  function getConfiguredX402Networks(config) {
46
46
  return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
47
47
  }
48
+ function selectRouteAccepts(accepts, routeEntry) {
49
+ return routeEntry.billing === "upto" ? accepts.filter((accept) => accept.scheme === "upto") : accepts.filter((accept) => (accept.scheme ?? "exact") !== "upto");
50
+ }
48
51
  async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, body) {
49
52
  return Promise.all(
50
53
  accepts.map(async (accept) => ({
@@ -340,9 +343,18 @@ function withScopedKinds(client, kinds) {
340
343
  return {
341
344
  verify: client.verify.bind(client),
342
345
  settle: client.settle.bind(client),
343
- getSupported: async () => ({ ...await client.getSupported(), kinds })
346
+ getSupported: async () => {
347
+ const live = await client.getSupported();
348
+ return { ...live, kinds: mergeKindExtras(kinds, live.kinds) };
349
+ }
344
350
  };
345
351
  }
352
+ function mergeKindExtras(scoped, live) {
353
+ return scoped.map((kind) => {
354
+ const match = live.find((l) => l.scheme === kind.scheme && l.network === kind.network);
355
+ return match?.extra ? { ...kind, extra: { ...kind.extra, ...match.extra } } : kind;
356
+ });
357
+ }
346
358
  function buildSupportedKinds(group) {
347
359
  return group.networks.flatMap((network) => {
348
360
  if (group.family === "solana") {
@@ -551,7 +563,8 @@ function preflight(routeEntry, handler, deps, request) {
551
563
  request,
552
564
  meta,
553
565
  pluginCtx,
554
- report: createReporter(deps.plugin, pluginCtx, routeEntry.key)
566
+ report: createReporter(deps.plugin, pluginCtx, routeEntry.key),
567
+ query: void 0
555
568
  };
556
569
  }
557
570
  function buildMeta(request, routeEntry) {
@@ -574,13 +587,19 @@ function buildMeta(request, routeEntry) {
574
587
  import { NextResponse } from "next/server";
575
588
 
576
589
  // src/pipeline/body.ts
590
+ var MalformedJsonError = class extends Error {
591
+ constructor() {
592
+ super("Invalid JSON");
593
+ this.name = "MalformedJsonError";
594
+ }
595
+ };
577
596
  async function bufferBody(request) {
578
597
  const text = await request.text();
579
- if (!text) return void 0;
598
+ if (!text.trim()) return void 0;
580
599
  try {
581
600
  return JSON.parse(text);
582
601
  } catch {
583
- return void 0;
602
+ throw new MalformedJsonError();
584
603
  }
585
604
  }
586
605
  function validateBody(parsed, schema) {
@@ -661,7 +680,18 @@ function computeQuotaLevel(remaining, warn, critical) {
661
680
  // src/pipeline/steps/parse-body.ts
662
681
  async function parseBody(ctx, request = ctx.request) {
663
682
  if (!ctx.routeEntry.bodySchema) return { ok: true, data: void 0 };
664
- const raw = await bufferBody(request);
683
+ let raw;
684
+ try {
685
+ raw = await bufferBody(request);
686
+ } catch (err) {
687
+ if (!(err instanceof MalformedJsonError)) throw err;
688
+ const response2 = NextResponse.json(
689
+ { success: false, error: "Invalid JSON", issues: [] },
690
+ { status: 400 }
691
+ );
692
+ firePluginResponse(ctx, response2);
693
+ return { ok: false, response: response2 };
694
+ }
665
695
  const result = validateBody(raw, ctx.routeEntry.bodySchema);
666
696
  if (result.success) return { ok: true, data: result.data };
667
697
  const response = NextResponse.json(
@@ -672,6 +702,22 @@ async function parseBody(ctx, request = ctx.request) {
672
702
  return { ok: false, response };
673
703
  }
674
704
 
705
+ // src/pipeline/steps/parse-query.ts
706
+ import { NextResponse as NextResponse2 } from "next/server";
707
+ function validateQuery(ctx) {
708
+ const { querySchema } = ctx.routeEntry;
709
+ if (!querySchema) return { ok: true, data: void 0 };
710
+ const params = Object.fromEntries(ctx.request.nextUrl.searchParams.entries());
711
+ const result = validateBody(params, querySchema);
712
+ if (result.success) return { ok: true, data: result.data };
713
+ const response = NextResponse2.json(
714
+ { success: false, error: result.error, issues: result.issues },
715
+ { status: 400 }
716
+ );
717
+ firePluginResponse(ctx, response);
718
+ return { ok: false, response };
719
+ }
720
+
675
721
  // src/pipeline/steps/errors.ts
676
722
  function errorStatus(error, fallback) {
677
723
  const status = error?.status;
@@ -686,9 +732,9 @@ function handlerFailureError(response) {
686
732
  }
687
733
 
688
734
  // src/pipeline/steps/fail.ts
689
- import { NextResponse as NextResponse2 } from "next/server";
735
+ import { NextResponse as NextResponse3 } from "next/server";
690
736
  function fail(ctx, status, message, requestBody) {
691
- const response = NextResponse2.json({ success: false, error: message }, { status });
737
+ const response = NextResponse3.json({ success: false, error: message }, { status });
692
738
  firePluginResponse(ctx, response, requestBody);
693
739
  return response;
694
740
  }
@@ -705,7 +751,7 @@ async function runValidate(ctx, body) {
705
751
  }
706
752
 
707
753
  // src/pipeline/flows/static/static-invoke.ts
708
- import { NextResponse as NextResponse3 } from "next/server";
754
+ import { NextResponse as NextResponse4 } from "next/server";
709
755
 
710
756
  // src/types.ts
711
757
  var HttpError = class extends Error {
@@ -716,14 +762,6 @@ var HttpError = class extends Error {
716
762
  }
717
763
  };
718
764
 
719
- // src/pipeline/steps/parse-query.ts
720
- function parseQuery(request, routeEntry) {
721
- if (!routeEntry.querySchema) return void 0;
722
- const params = Object.fromEntries(request.nextUrl.searchParams.entries());
723
- const result = routeEntry.querySchema.safeParse(params);
724
- return result.success ? result.data : params;
725
- }
726
-
727
765
  // src/pipeline/flows/static/static-invoke.ts
728
766
  function invokePaidStatic(ctx, wallet, account, body, payment) {
729
767
  return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
@@ -734,7 +772,7 @@ function invokeUnauthed(ctx, wallet, account, body) {
734
772
  function buildHandlerCtx(ctx, wallet, account, body, payment) {
735
773
  return {
736
774
  body,
737
- query: parseQuery(ctx.request, ctx.routeEntry),
775
+ query: ctx.query,
738
776
  request: ctx.request,
739
777
  requestId: ctx.meta.requestId,
740
778
  route: ctx.routeEntry.key,
@@ -754,10 +792,7 @@ async function runHandler(ctx, handlerCtx) {
754
792
  }
755
793
  if (isAsyncIterable(returned) && !isThenable(returned)) {
756
794
  return errorResult(
757
- new HttpError(
758
- `route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
759
- 500
760
- )
795
+ new HttpError(`route '${ctx.routeEntry.key}': streaming handlers require .metered()`, 500)
761
796
  );
762
797
  }
763
798
  let rawResult;
@@ -766,14 +801,14 @@ async function runHandler(ctx, handlerCtx) {
766
801
  } catch (error) {
767
802
  return errorResult(error);
768
803
  }
769
- const response = rawResult instanceof Response ? rawResult : NextResponse3.json(rawResult);
804
+ const response = rawResult instanceof Response ? rawResult : NextResponse4.json(rawResult);
770
805
  return { response, rawResult };
771
806
  }
772
807
  function errorResult(error) {
773
808
  const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
774
809
  const message = error instanceof Error ? error.message : "Internal error";
775
810
  return {
776
- response: NextResponse3.json({ success: false, error: message }, { status }),
811
+ response: NextResponse4.json({ success: false, error: message }, { status }),
777
812
  rawResult: void 0,
778
813
  handlerError: error
779
814
  };
@@ -1186,10 +1221,12 @@ var DynamicPricing = class {
1186
1221
  }
1187
1222
  needsBody = true;
1188
1223
  async quote(body) {
1224
+ let priced;
1189
1225
  try {
1190
1226
  const raw = await this.opts.fn(body);
1191
- return this.cap(raw, body);
1227
+ priced = this.cap(raw, body);
1192
1228
  } catch (err) {
1229
+ if (err instanceof HttpError) throw err;
1193
1230
  this.alert("error", `Pricing function failed: ${msg(err)}`, {
1194
1231
  error: err instanceof Error ? err.stack : String(err),
1195
1232
  body
@@ -1200,6 +1237,13 @@ var DynamicPricing = class {
1200
1237
  }
1201
1238
  throw err;
1202
1239
  }
1240
+ if (!isPositiveDecimal(priced)) {
1241
+ throw new HttpError(
1242
+ `route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
1243
+ 500
1244
+ );
1245
+ }
1246
+ return priced;
1203
1247
  }
1204
1248
  challengeQuote(body) {
1205
1249
  if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
@@ -1274,14 +1318,14 @@ var TieredPricing = class {
1274
1318
  `Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
1275
1319
  );
1276
1320
  }
1277
- challengeQuote(body) {
1321
+ async challengeQuote(body) {
1278
1322
  if (body !== void 0) {
1279
1323
  try {
1280
- return this.quote(body);
1324
+ return await this.quote(body);
1281
1325
  } catch {
1282
1326
  }
1283
1327
  }
1284
- return Promise.resolve(this.maxTierPrice());
1328
+ return this.maxTierPrice();
1285
1329
  }
1286
1330
  describe() {
1287
1331
  return {
@@ -1712,7 +1756,7 @@ var mppStrategy = {
1712
1756
  async verify(args) {
1713
1757
  const info = readMppCredential(args.request);
1714
1758
  if (!info) return { ok: false, kind: "invalid" };
1715
- if (args.routeEntry.dynamicPrice) {
1759
+ if (args.routeEntry.billing === "metered") {
1716
1760
  if (!info.sessionAction) return { ok: false, kind: "invalid" };
1717
1761
  return verifySessionMode(args, info);
1718
1762
  }
@@ -1763,7 +1807,7 @@ var mppStrategy = {
1763
1807
  async buildChallenge(args) {
1764
1808
  if (!args.deps.mppx) return {};
1765
1809
  const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
1766
- if (args.routeEntry.dynamicPrice && sessionsConfigured) {
1810
+ if (args.routeEntry.billing === "metered" && sessionsConfigured) {
1767
1811
  const tickCost = args.routeEntry.tickCost;
1768
1812
  const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
1769
1813
  const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
@@ -2015,7 +2059,7 @@ async function settleX402Payment(server, payload, requirements, amountOverride)
2015
2059
  };
2016
2060
  }
2017
2061
  function tagBareDecimalAsDollars(amount) {
2018
- if (/^\d+\.\d+$/.test(amount)) return `$${amount}`;
2062
+ if (/^\d+(?:\.\d+)?$/.test(amount)) return `$${amount}`;
2019
2063
  return amount;
2020
2064
  }
2021
2065
 
@@ -2140,7 +2184,7 @@ async function verifyX402(args) {
2140
2184
  const accepts = await resolveX402Accepts(
2141
2185
  request,
2142
2186
  routeEntry,
2143
- deps.x402Accepts,
2187
+ selectRouteAccepts(deps.x402Accepts, routeEntry),
2144
2188
  deps.payeeAddress,
2145
2189
  body
2146
2190
  );
@@ -2188,7 +2232,7 @@ async function verifyX402(args) {
2188
2232
  async function settleX402(args) {
2189
2233
  const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
2190
2234
  const { payload, requirements } = token;
2191
- const override = routeEntry.dynamicPrice ? { amount: billedAmount } : void 0;
2235
+ const override = routeEntry.billing === "exact" ? void 0 : { amount: billedAmount };
2192
2236
  try {
2193
2237
  const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2194
2238
  if (!settle.result?.success) {
@@ -2218,7 +2262,7 @@ async function buildX402ChallengeContribution(args) {
2218
2262
  const accepts = await resolveX402Accepts(
2219
2263
  request,
2220
2264
  routeEntry,
2221
- deps.x402Accepts,
2265
+ selectRouteAccepts(deps.x402Accepts, routeEntry),
2222
2266
  deps.payeeAddress,
2223
2267
  body
2224
2268
  );
@@ -2262,8 +2306,8 @@ function getAllowedStrategies(allowed) {
2262
2306
  return allowed.map((name) => STRATEGIES[name]);
2263
2307
  }
2264
2308
 
2265
- // src/pipeline/flows/build402.ts
2266
- import { NextResponse as NextResponse4 } from "next/server";
2309
+ // src/pipeline/flows/challenge-response.ts
2310
+ import { NextResponse as NextResponse5 } from "next/server";
2267
2311
 
2268
2312
  // src/pipeline/challenge-extensions.ts
2269
2313
  init_evm();
@@ -2279,20 +2323,17 @@ async function buildChallengeExtensions(ctx) {
2279
2323
  });
2280
2324
  const inputSchema = routeEntry.bodySchema ? toJSON(routeEntry.bodySchema) : routeEntry.querySchema ? toJSON(routeEntry.querySchema) : void 0;
2281
2325
  const outputSchema = routeEntry.outputSchema ? toJSON(routeEntry.outputSchema) : void 0;
2282
- if (inputSchema) {
2283
- const config = {
2284
- method: routeEntry.method,
2285
- bodyType: routeEntry.bodySchema ? "json" : void 0,
2286
- inputSchema
2287
- };
2288
- if (routeEntry.inputExample !== void 0) {
2289
- config.input = routeEntry.inputExample;
2290
- }
2291
- if (outputSchema && routeEntry.outputExample !== void 0) {
2292
- config.output = { schema: outputSchema, example: routeEntry.outputExample };
2293
- }
2294
- extensions = declareDiscoveryExtension(config);
2326
+ const isBodyMethod = routeEntry.method === "POST" || routeEntry.method === "PUT" || routeEntry.method === "PATCH";
2327
+ const config = { method: routeEntry.method };
2328
+ if (isBodyMethod) config.bodyType = "json";
2329
+ if (inputSchema) config.inputSchema = inputSchema;
2330
+ if (routeEntry.inputExample !== void 0) {
2331
+ config.input = routeEntry.inputExample;
2332
+ }
2333
+ if (outputSchema && routeEntry.outputExample !== void 0) {
2334
+ config.output = { schema: outputSchema, example: routeEntry.outputExample };
2295
2335
  }
2336
+ extensions = declareDiscoveryExtension(config);
2296
2337
  } catch (err) {
2297
2338
  ctx.report(
2298
2339
  "warn",
@@ -2311,9 +2352,7 @@ async function buildChallengeExtensions(ctx) {
2311
2352
  } catch {
2312
2353
  }
2313
2354
  }
2314
- const hasEvmUpto = ctx.deps.x402Accepts.some(
2315
- (accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
2316
- );
2355
+ const hasEvmUpto = ctx.routeEntry.billing === "upto" && ctx.deps.x402Accepts.some((accept) => accept.scheme === "upto" && isEvmNetwork(accept.network));
2317
2356
  if (hasEvmUpto) {
2318
2357
  try {
2319
2358
  const { declareEip2612GasSponsoringExtension } = await import("@x402/extensions");
@@ -2331,14 +2370,14 @@ async function buildChallengeExtensions(ctx) {
2331
2370
  return extensions;
2332
2371
  }
2333
2372
 
2334
- // src/pipeline/flows/build402.ts
2335
- async function build402(ctx, pricing, body, failure) {
2373
+ // src/pipeline/flows/challenge-response.ts
2374
+ async function buildChallengeResponse(ctx, pricing, body, failure) {
2336
2375
  let challengePrice;
2337
2376
  try {
2338
2377
  challengePrice = pricing ? await pricing.challengeQuote(body) : "0";
2339
2378
  } catch (err) {
2340
2379
  const message = errorMessage(err, "Price calculation failed");
2341
- const errorResponse = NextResponse4.json(
2380
+ const errorResponse = NextResponse5.json(
2342
2381
  { success: false, error: message },
2343
2382
  { status: errorStatus(err, 500) }
2344
2383
  );
@@ -2347,7 +2386,7 @@ async function build402(ctx, pricing, body, failure) {
2347
2386
  }
2348
2387
  const extensions = await buildChallengeExtensions(ctx);
2349
2388
  const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
2350
- const response = new NextResponse4(responseBody, {
2389
+ const response = new NextResponse5(responseBody, {
2351
2390
  status: 402,
2352
2391
  headers: {
2353
2392
  "Content-Type": "application/json",
@@ -2374,7 +2413,7 @@ async function build402(ctx, pricing, body, failure) {
2374
2413
  const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
2375
2414
  ctx.report("critical", message);
2376
2415
  if (strategy.protocol === "x402") {
2377
- const errorResponse = NextResponse4.json(
2416
+ const errorResponse = NextResponse5.json(
2378
2417
  { success: false, error: message },
2379
2418
  { status: 500 }
2380
2419
  );
@@ -2426,7 +2465,7 @@ function surrogatePriceForSkippedBody(routeEntry) {
2426
2465
  }
2427
2466
 
2428
2467
  // src/pipeline/flows/dynamic/dynamic-channel-mgmt.ts
2429
- import { NextResponse as NextResponse5 } from "next/server";
2468
+ import { NextResponse as NextResponse6 } from "next/server";
2430
2469
  async function runDynamicChannelMgmtFlow(args) {
2431
2470
  const { ctx, strategy, account, pricing, skipBody } = args;
2432
2471
  const { request, routeEntry, deps, report } = ctx;
@@ -2445,7 +2484,7 @@ async function runDynamicChannelMgmtFlow(args) {
2445
2484
  if (verifyOutcome.kind === "config") {
2446
2485
  return fail(ctx, 500, verifyOutcome.message, parsedBody);
2447
2486
  }
2448
- return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
2487
+ return buildChallengeResponse(ctx, pricing, parsedBody, verifyOutcome.failure);
2449
2488
  }
2450
2489
  ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2451
2490
  firePaymentVerified(ctx, {
@@ -2454,7 +2493,7 @@ async function runDynamicChannelMgmtFlow(args) {
2454
2493
  amount: price,
2455
2494
  network: verifyOutcome.payment.network
2456
2495
  });
2457
- const synthetic = new NextResponse5(null, { status: 200 });
2496
+ const synthetic = new NextResponse6(null, { status: 200 });
2458
2497
  const settleScope = {
2459
2498
  wallet: verifyOutcome.wallet,
2460
2499
  account,
@@ -2480,10 +2519,7 @@ async function runDynamicChannelMgmtFlow(args) {
2480
2519
  });
2481
2520
  }
2482
2521
 
2483
- // src/pipeline/flows/dynamic/dynamic-invoke.ts
2484
- import { NextResponse as NextResponse6 } from "next/server";
2485
-
2486
- // src/pricing/charge-context.ts
2522
+ // src/pricing/metered-charge.ts
2487
2523
  function createChargeContext(args) {
2488
2524
  const { tickCost, maxPrice, route } = args;
2489
2525
  const tickAtomic = decimalToAtomic(tickCost);
@@ -2518,17 +2554,12 @@ function createChargeContext(args) {
2518
2554
  };
2519
2555
  }
2520
2556
 
2521
- // src/pipeline/flows/dynamic/dynamic-invoke.ts
2522
- async function invokeDynamic(ctx, wallet, account, body, payment) {
2523
- const streaming = ctx.routeEntry.streaming === true;
2524
- const chargeContext = streaming ? createChargeContext({
2525
- tickCost: ctx.routeEntry.tickCost,
2526
- maxPrice: ctx.routeEntry.maxPrice,
2527
- route: ctx.routeEntry.key
2528
- }) : null;
2529
- const baseHandlerCtx = {
2557
+ // src/pipeline/flows/dynamic/dynamic-invoke/shared.ts
2558
+ import { NextResponse as NextResponse7 } from "next/server";
2559
+ function buildBaseHandlerCtx(ctx, wallet, account, body, payment) {
2560
+ return {
2530
2561
  body,
2531
- query: parseQuery(ctx.request, ctx.routeEntry),
2562
+ query: ctx.query,
2532
2563
  request: ctx.request,
2533
2564
  requestId: ctx.meta.requestId,
2534
2565
  route: ctx.routeEntry.key,
@@ -2538,12 +2569,41 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
2538
2569
  alert: ctx.report,
2539
2570
  setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
2540
2571
  };
2572
+ }
2573
+ function toResponse(rawResult) {
2574
+ return rawResult instanceof Response ? rawResult : NextResponse7.json(rawResult);
2575
+ }
2576
+ function errorResult2(error) {
2577
+ const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
2578
+ const message = error instanceof Error ? error.message : "Internal error";
2579
+ return {
2580
+ kind: "request",
2581
+ response: NextResponse7.json({ success: false, error: message }, { status }),
2582
+ rawResult: void 0,
2583
+ handlerError: error
2584
+ };
2585
+ }
2586
+ function isAsyncIterable2(value) {
2587
+ return value != null && typeof value === "object" && Symbol.asyncIterator in value;
2588
+ }
2589
+ function isThenable2(value) {
2590
+ return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
2591
+ }
2592
+
2593
+ // src/pipeline/flows/dynamic/dynamic-invoke/metered-invoke.ts
2594
+ async function invokeMetered(ctx, wallet, account, body, payment) {
2595
+ const chargeContext = ctx.routeEntry.streaming ? createChargeContext({
2596
+ tickCost: ctx.routeEntry.tickCost,
2597
+ maxPrice: ctx.routeEntry.maxPrice,
2598
+ route: ctx.routeEntry.key
2599
+ }) : null;
2600
+ const baseHandlerCtx = buildBaseHandlerCtx(ctx, wallet, account, body, payment);
2541
2601
  const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
2542
2602
  let returned;
2543
2603
  try {
2544
2604
  returned = ctx.handler(handlerCtx);
2545
2605
  } catch (error) {
2546
- return errorResult2(error, chargeContext);
2606
+ return errorResult2(error);
2547
2607
  }
2548
2608
  if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2549
2609
  if (!chargeContext) {
@@ -2551,41 +2611,80 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
2551
2611
  new HttpError(
2552
2612
  "route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
2553
2613
  500
2554
- ),
2555
- null
2614
+ )
2556
2615
  );
2557
2616
  }
2558
- return {
2559
- kind: "stream",
2560
- source: returned,
2561
- chargeContext
2562
- };
2617
+ return { kind: "stream", source: returned, chargeContext };
2563
2618
  }
2564
2619
  let rawResult;
2565
2620
  try {
2566
2621
  rawResult = await returned;
2567
2622
  } catch (error) {
2568
- return errorResult2(error, chargeContext);
2623
+ return errorResult2(error);
2569
2624
  }
2570
- const response = rawResult instanceof Response ? rawResult : NextResponse6.json(rawResult);
2571
- return { kind: "request", response, rawResult };
2625
+ return { kind: "request", response: toResponse(rawResult), rawResult };
2572
2626
  }
2573
- function errorResult2(error, chargeContext) {
2574
- const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
2575
- const message = error instanceof Error ? error.message : "Internal error";
2576
- void chargeContext;
2627
+
2628
+ // src/pricing/upto-charge.ts
2629
+ function createUptoChargeContext(args) {
2630
+ const { maxPrice, route } = args;
2631
+ const capAtomic = decimalToAtomic(maxPrice);
2632
+ if (capAtomic <= 0n) {
2633
+ throw new Error(`route '${route}': maxPrice '${maxPrice}' must be a positive decimal string`);
2634
+ }
2635
+ let calls = 0;
2636
+ let atomic = 0n;
2637
+ const charge = async (amount) => {
2638
+ const nextAtomic = atomic + decimalToAtomic(amount);
2639
+ if (nextAtomic > capAtomic) {
2640
+ throw Object.assign(
2641
+ new Error(
2642
+ `route '${route}': charge() running total ($${atomicToDecimal(nextAtomic)}) exceeds maxPrice ($${atomicToDecimal(capAtomic)})`
2643
+ ),
2644
+ { status: 400, code: "CHARGE_OVER_CAP" }
2645
+ );
2646
+ }
2647
+ calls += 1;
2648
+ atomic = nextAtomic;
2649
+ };
2577
2650
  return {
2578
- kind: "request",
2579
- response: NextResponse6.json({ success: false, error: message }, { status }),
2580
- rawResult: void 0,
2581
- handlerError: error
2651
+ charge,
2652
+ callCount: () => calls,
2653
+ atomicTotal: () => atomic
2582
2654
  };
2583
2655
  }
2584
- function isAsyncIterable2(value) {
2585
- return value != null && typeof value === "object" && Symbol.asyncIterator in value;
2586
- }
2587
- function isThenable2(value) {
2588
- return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
2656
+
2657
+ // src/pipeline/flows/dynamic/dynamic-invoke/upto-invoke.ts
2658
+ async function invokeUpto(ctx, wallet, account, body, payment) {
2659
+ const uptoCtx = createUptoChargeContext({
2660
+ maxPrice: ctx.routeEntry.maxPrice,
2661
+ route: ctx.routeEntry.key
2662
+ });
2663
+ const handlerCtx = {
2664
+ ...buildBaseHandlerCtx(ctx, wallet, account, body, payment),
2665
+ charge: uptoCtx.charge
2666
+ };
2667
+ let returned;
2668
+ try {
2669
+ returned = ctx.handler(handlerCtx);
2670
+ } catch (error) {
2671
+ return errorResult2(error);
2672
+ }
2673
+ if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2674
+ return errorResult2(
2675
+ new HttpError(
2676
+ "streaming is not supported on .upTo() routes \u2014 return a value from .handler() instead",
2677
+ 500
2678
+ )
2679
+ );
2680
+ }
2681
+ let rawResult;
2682
+ try {
2683
+ rawResult = await returned;
2684
+ } catch (error) {
2685
+ return errorResult2(error);
2686
+ }
2687
+ return { kind: "request", response: toResponse(rawResult), rawResult, uptoContext: uptoCtx };
2589
2688
  }
2590
2689
 
2591
2690
  // src/pipeline/flows/dynamic/dynamic-preflight.ts
@@ -2615,7 +2714,7 @@ async function runDynamicRequestFlow(args) {
2615
2714
  }
2616
2715
  const beforeErr = await runBeforeSettle(ctx, settleScope);
2617
2716
  if (beforeErr) return beforeErr;
2618
- const billedAmount = routeEntry.tickCost;
2717
+ const billedAmount = computeBilledAmount(routeEntry, result);
2619
2718
  return settleAndFinalizeRequest({
2620
2719
  ctx,
2621
2720
  strategy,
@@ -2633,6 +2732,19 @@ async function runDynamicRequestFlow(args) {
2633
2732
  }
2634
2733
  });
2635
2734
  }
2735
+ function computeBilledAmount(routeEntry, result) {
2736
+ if (routeEntry.billing === "upto") {
2737
+ const total = result.uptoContext?.atomicTotal() ?? 0n;
2738
+ if (total <= 0n) {
2739
+ throw new HttpError(
2740
+ `route '${routeEntry.key}': handler did not call charge(amount) \u2014 upto routes must accumulate a non-zero billed amount`,
2741
+ 500
2742
+ );
2743
+ }
2744
+ return atomicToDecimal(total);
2745
+ }
2746
+ return routeEntry.tickCost;
2747
+ }
2636
2748
 
2637
2749
  // src/pipeline/flows/dynamic/dynamic-stream.ts
2638
2750
  async function runDynamicStreamFlow(args) {
@@ -2669,7 +2781,7 @@ async function runDynamicPaidFlow(ctx) {
2669
2781
  if (!incomingStrategy) {
2670
2782
  const initError = protocolInitError(routeEntry, deps);
2671
2783
  if (initError) return fail(ctx, 500, initError);
2672
- return build402(ctx, pricing, earlyBody);
2784
+ return buildChallengeResponse(ctx, pricing, earlyBody);
2673
2785
  }
2674
2786
  const { skipBody, skipHandler } = resolveDynamicPreflight(incomingStrategy, request, routeEntry);
2675
2787
  if (skipHandler) {
@@ -2696,7 +2808,7 @@ async function runDynamicPaidFlow(ctx) {
2696
2808
  if (verifyOutcome.kind === "config") {
2697
2809
  return fail(ctx, 500, verifyOutcome.message, parsedBody);
2698
2810
  }
2699
- return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
2811
+ return buildChallengeResponse(ctx, pricing, parsedBody, verifyOutcome.failure);
2700
2812
  }
2701
2813
  ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2702
2814
  firePaymentVerified(ctx, {
@@ -2705,13 +2817,7 @@ async function runDynamicPaidFlow(ctx) {
2705
2817
  amount: price,
2706
2818
  network: verifyOutcome.payment.network
2707
2819
  });
2708
- const result = await invokeDynamic(
2709
- ctx,
2710
- verifyOutcome.wallet,
2711
- account,
2712
- parsedBody,
2713
- verifyOutcome.payment
2714
- );
2820
+ const result = await invokeDynamic(ctx, verifyOutcome, account, parsedBody);
2715
2821
  switch (result.kind) {
2716
2822
  case "stream":
2717
2823
  return runDynamicStreamFlow({
@@ -2733,6 +2839,18 @@ async function runDynamicPaidFlow(ctx) {
2733
2839
  });
2734
2840
  }
2735
2841
  }
2842
+ async function invokeDynamic(ctx, verifyOutcome, account, parsedBody) {
2843
+ switch (ctx.routeEntry.billing) {
2844
+ case "upto":
2845
+ return invokeUpto(ctx, verifyOutcome.wallet, account, parsedBody, verifyOutcome.payment);
2846
+ case "metered":
2847
+ return invokeMetered(ctx, verifyOutcome.wallet, account, parsedBody, verifyOutcome.payment);
2848
+ case "exact":
2849
+ throw new Error(
2850
+ `route '${ctx.routeEntry.key}': exact billing must not reach the dynamic paid flow`
2851
+ );
2852
+ }
2853
+ }
2736
2854
 
2737
2855
  // src/pipeline/flows/static/static-body-and-price.ts
2738
2856
  async function resolveStaticBodyAndPrice(args) {
@@ -2834,7 +2952,7 @@ async function runStaticPaidFlow(ctx) {
2834
2952
  if (!incomingStrategy) {
2835
2953
  const initError = protocolInitError(routeEntry, deps);
2836
2954
  if (initError) return fail(ctx, 500, initError);
2837
- return build402(ctx, pricing, earlyBody);
2955
+ return buildChallengeResponse(ctx, pricing, earlyBody);
2838
2956
  }
2839
2957
  const bodyAndPrice = await resolveStaticBodyAndPrice({ ctx, pricing });
2840
2958
  if (!bodyAndPrice.ok) return bodyAndPrice.response;
@@ -2851,7 +2969,7 @@ async function runStaticPaidFlow(ctx) {
2851
2969
  if (verifyOutcome.kind === "config") {
2852
2970
  return fail(ctx, 500, verifyOutcome.message, parsedBody);
2853
2971
  }
2854
- return build402(ctx, pricing, parsedBody, verifyOutcome.failure);
2972
+ return buildChallengeResponse(ctx, pricing, parsedBody, verifyOutcome.failure);
2855
2973
  }
2856
2974
  ctx.pluginCtx.setVerifiedWallet(verifyOutcome.wallet);
2857
2975
  firePaymentVerified(ctx, {
@@ -2880,17 +2998,12 @@ async function runStaticPaidFlow(ctx) {
2880
2998
 
2881
2999
  // src/pipeline/flows/paid.ts
2882
3000
  async function runPaidFlow(ctx) {
2883
- const dynamicPrice = ctx.routeEntry.dynamicPrice ?? false;
2884
- switch (dynamicPrice) {
2885
- case true:
2886
- return runDynamicPaidFlow(ctx);
2887
- case false:
2888
- return runStaticPaidFlow(ctx);
2889
- }
3001
+ const handlerCharged = ctx.routeEntry.billing !== "exact";
3002
+ return handlerCharged ? runDynamicPaidFlow(ctx) : runStaticPaidFlow(ctx);
2890
3003
  }
2891
3004
 
2892
3005
  // src/pipeline/flows/siwx-only.ts
2893
- import { NextResponse as NextResponse7 } from "next/server";
3006
+ import { NextResponse as NextResponse8 } from "next/server";
2894
3007
 
2895
3008
  // src/kv-store/client.ts
2896
3009
  var BIGINT_SUFFIX = "#__bigint";
@@ -3124,7 +3237,7 @@ async function runSiwxOnlyFlow(ctx) {
3124
3237
  }
3125
3238
  const siwx = await verifySIWX(request, routeEntry, deps.nonceStore);
3126
3239
  if (!siwx.valid) {
3127
- const response = NextResponse7.json(
3240
+ const response = NextResponse8.json(
3128
3241
  { error: siwx.code, message: SIWX_ERROR_MESSAGES[siwx.code] },
3129
3242
  { status: 402 }
3130
3243
  );
@@ -3185,7 +3298,7 @@ async function buildSiwxChallenge(ctx) {
3185
3298
  `SIWX challenge header encoding failed: ${err instanceof Error ? err.message : String(err)}`
3186
3299
  );
3187
3300
  }
3188
- const response = new NextResponse7(JSON.stringify(paymentRequired), {
3301
+ const response = new NextResponse8(JSON.stringify(paymentRequired), {
3189
3302
  status: 402,
3190
3303
  headers: { "Content-Type": "application/json", "Cache-Control": "no-store" }
3191
3304
  });
@@ -3231,6 +3344,9 @@ function createRequestHandler(routeEntry, handler, deps) {
3231
3344
  return async (request) => {
3232
3345
  await deps.initPromise;
3233
3346
  const ctx = preflight(routeEntry, handler, deps, request);
3347
+ const query = validateQuery(ctx);
3348
+ if (!query.ok) return query.response;
3349
+ ctx.query = query.data;
3234
3350
  if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
3235
3351
  if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
3236
3352
  if (routeEntry.pricing) return runPaidFlow(ctx);
@@ -3284,7 +3400,7 @@ var RouteBuilder = class _RouteBuilder {
3284
3400
  protocols: defaults?.protocols ? [...defaults.protocols] : ["x402"],
3285
3401
  maxPrice: void 0,
3286
3402
  minPrice: void 0,
3287
- dynamicPrice: false,
3403
+ billing: "exact",
3288
3404
  tickCost: void 0,
3289
3405
  unitType: void 0,
3290
3406
  payTo: void 0,
@@ -3315,39 +3431,84 @@ var RouteBuilder = class _RouteBuilder {
3315
3431
  next.#s = { ...this.#s, protocols: [...this.#s.protocols] };
3316
3432
  return next;
3317
3433
  }
3318
- paid(pricingOrOptions, options) {
3319
- const { pricing, resolvedOptions } = resolvePaidArgs(this.#s.key, pricingOrOptions, options);
3434
+ paid(arg, options) {
3435
+ return this.applyPaid(normalizePaidArg(this.#s.key, arg, options), "paid");
3436
+ }
3437
+ /**
3438
+ * x402-only handler-computed billing. The handler receives `charge(amount)`
3439
+ * and the request settles once for the accumulated total, capped at
3440
+ * `maxPrice`. Requires an `'upto'` accept on at least one configured network.
3441
+ * Pass a bare string as sugar for `{ maxPrice }`.
3442
+ *
3443
+ * @example
3444
+ * ```ts
3445
+ * router.route('llm')
3446
+ * .upTo('0.05')
3447
+ * .body(schema)
3448
+ * .handler(async ({ body, charge }) => { await charge('0.001'); ... });
3449
+ * ```
3450
+ */
3451
+ upTo(arg) {
3452
+ return this.applyPaid(normalizeUpToArg(this.#s.key, arg), "upTo");
3453
+ }
3454
+ /**
3455
+ * MPP-only per-tick billing. `.handler()` bills exactly `tickCost`;
3456
+ * `.stream()` calls `charge()` (no-arg) per yield, settling per tick up to
3457
+ * `maxPrice`. Requires `RouterConfig.mpp.session`.
3458
+ *
3459
+ * @example
3460
+ * ```ts
3461
+ * router.route('llm/stream')
3462
+ * .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
3463
+ * .stream(async function* ({ charge }) { await charge(); yield 'hi'; });
3464
+ * ```
3465
+ */
3466
+ metered(options) {
3467
+ return this.applyPaid(normalizeMeteredArg(this.#s.key, options), "metered");
3468
+ }
3469
+ applyPaid(normalized, method) {
3470
+ const { pricing, resolvedOptions, billing, tickCost, unitType, maxPrice } = normalized;
3320
3471
  if (this.#s.authMode === "unprotected") {
3321
3472
  throw new Error(
3322
- `route '${this.#s.key}': Cannot combine .unprotected() and .paid() on the same route.`
3473
+ `route '${this.#s.key}': Cannot combine .unprotected() and .${method}() on the same route.`
3323
3474
  );
3324
3475
  }
3325
3476
  if (this.#s.pricing !== void 0) {
3326
3477
  throw new Error(
3327
- `route '${this.#s.key}': Cannot call .paid() more than once on the same route.`
3478
+ `route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
3328
3479
  );
3329
3480
  }
3330
3481
  const next = this.fork();
3331
3482
  next.#s.authMode = "paid";
3332
3483
  next.#s.pricing = pricing;
3333
- if (resolvedOptions?.protocols) {
3484
+ if (billing === "upto") {
3485
+ if (resolvedOptions.protocols?.some((p) => p !== "x402")) {
3486
+ throw new Error(
3487
+ `route '${this.#s.key}': .upTo() is x402-only \u2014 remove the conflicting protocols override.`
3488
+ );
3489
+ }
3490
+ next.#s.protocols = ["x402"];
3491
+ } else if (billing === "metered") {
3492
+ if (resolvedOptions.protocols?.some((p) => p !== "mpp")) {
3493
+ throw new Error(
3494
+ `route '${this.#s.key}': .metered() is MPP-only \u2014 remove the conflicting protocols override.`
3495
+ );
3496
+ }
3497
+ next.#s.protocols = ["mpp"];
3498
+ } else if (resolvedOptions.protocols) {
3334
3499
  next.#s.protocols = [...resolvedOptions.protocols];
3335
3500
  } else if (next.#s.protocols.length === 0) {
3336
3501
  next.#s.protocols = ["x402"];
3337
3502
  }
3338
- if (resolvedOptions?.maxPrice) next.#s.maxPrice = resolvedOptions.maxPrice;
3339
- if (resolvedOptions?.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
3340
- if (resolvedOptions?.payTo) next.#s.payTo = resolvedOptions.payTo;
3341
- if (resolvedOptions?.mpp) next.#s.mppInfo = resolvedOptions.mpp;
3342
- if (resolvedOptions?.dynamic) next.#s.dynamicPrice = true;
3343
- if (resolvedOptions?.tickCost) next.#s.tickCost = resolvedOptions.tickCost;
3344
- if (resolvedOptions?.unitType) next.#s.unitType = resolvedOptions.unitType;
3503
+ if (resolvedOptions.maxPrice) next.#s.maxPrice = resolvedOptions.maxPrice;
3504
+ if (maxPrice) next.#s.maxPrice = maxPrice;
3505
+ if (resolvedOptions.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
3506
+ if (resolvedOptions.payTo) next.#s.payTo = resolvedOptions.payTo;
3507
+ if (resolvedOptions.mpp) next.#s.mppInfo = resolvedOptions.mpp;
3508
+ next.#s.billing = billing;
3509
+ if (tickCost) next.#s.tickCost = tickCost;
3510
+ if (unitType) next.#s.unitType = unitType;
3345
3511
  if (typeof pricing === "object" && "tiers" in pricing) {
3346
- if (next.#s.dynamicPrice) {
3347
- throw new Error(
3348
- `route '${this.#s.key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
3349
- );
3350
- }
3351
3512
  for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
3352
3513
  if (!tierKey) {
3353
3514
  throw new Error(`route '${this.#s.key}': tier key cannot be empty`);
@@ -3359,21 +3520,25 @@ var RouteBuilder = class _RouteBuilder {
3359
3520
  }
3360
3521
  }
3361
3522
  }
3362
- if (resolvedOptions?.maxPrice !== void 0 && !isPositiveDecimal(resolvedOptions.maxPrice)) {
3523
+ if (billing === "exact" && typeof pricing === "string" && !isPositiveDecimal(pricing)) {
3363
3524
  throw new Error(
3364
- `route '${this.#s.key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
3525
+ `route '${this.#s.key}': price '${pricing}' must be a positive decimal string`
3365
3526
  );
3366
3527
  }
3367
- if (resolvedOptions?.tickCost !== void 0 && !isPositiveDecimal(resolvedOptions.tickCost)) {
3528
+ if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
3368
3529
  throw new Error(
3369
- `route '${this.#s.key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
3530
+ `route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
3370
3531
  );
3371
3532
  }
3372
- if (next.#s.dynamicPrice && !next.#s.maxPrice) {
3373
- throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires maxPrice`);
3533
+ if (next.#s.minPrice !== void 0 && !isPositiveDecimal(next.#s.minPrice)) {
3534
+ throw new Error(
3535
+ `route '${this.#s.key}': minPrice '${next.#s.minPrice}' must be a positive decimal string`
3536
+ );
3374
3537
  }
3375
- if (next.#s.dynamicPrice && !next.#s.tickCost) {
3376
- throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires tickCost`);
3538
+ if (next.#s.tickCost !== void 0 && !isPositiveDecimal(next.#s.tickCost)) {
3539
+ throw new Error(
3540
+ `route '${this.#s.key}': tickCost '${next.#s.tickCost}' must be a positive decimal string`
3541
+ );
3377
3542
  }
3378
3543
  return next;
3379
3544
  }
@@ -3656,13 +3821,13 @@ var RouteBuilder = class _RouteBuilder {
3656
3821
  /**
3657
3822
  * Register a streaming handler (`async function*`) and return the Next.js
3658
3823
  * route function. Each `charge()` call bills one tick (`tickCost` USDC) up
3659
- * to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
3824
+ * to `maxPrice`; requires `.metered({ ... })` and MPP session mode.
3660
3825
  *
3661
3826
  * @example
3662
3827
  * ```ts
3663
3828
  * export const POST = router
3664
3829
  * .route('llm/stream')
3665
- * .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
3830
+ * .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
3666
3831
  * .body(schema)
3667
3832
  * .stream(async function* ({ body, charge }) {
3668
3833
  * for await (const token of streamLLM(body.prompt)) {
@@ -3678,7 +3843,7 @@ var RouteBuilder = class _RouteBuilder {
3678
3843
  register(handlerFn, streaming) {
3679
3844
  if (!this.#s.authMode) {
3680
3845
  throw new Error(
3681
- `route '${this.#s.key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
3846
+ `route '${this.#s.key}': Select an auth mode: .paid(pricing), .upTo(maxPrice), .metered(options), .siwx(), .apiKey(resolver), or .unprotected()`
3682
3847
  );
3683
3848
  }
3684
3849
  if (this.#s.validateFn && !this.#s.bodySchema) {
@@ -3689,24 +3854,34 @@ var RouteBuilder = class _RouteBuilder {
3689
3854
  if (this.#s.settlement && !this.#s.pricing) {
3690
3855
  throw new Error(`route '${this.#s.key}': .settlement() requires a paid route`);
3691
3856
  }
3692
- if (this.#s.dynamicPrice && this.#s.protocols.includes("x402")) {
3857
+ if (this.#s.billing === "upto") {
3693
3858
  const hasUpto = this.#s.deps.x402Accepts.some((accept) => accept.scheme === "upto");
3694
3859
  if (!hasUpto) {
3695
3860
  throw new Error(
3696
- `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.`
3861
+ `route '${this.#s.key}': .upTo() requires an 'upto' accept on at least one configured network. Add { scheme: 'upto', network, asset } to RouterConfig.x402.accepts.`
3697
3862
  );
3698
3863
  }
3699
3864
  }
3700
- if (this.#s.dynamicPrice && this.#s.protocols.includes("mpp")) {
3865
+ if (this.#s.pricing !== void 0 && this.#s.billing === "exact" && this.#s.protocols.includes("x402")) {
3866
+ const hasExact = this.#s.deps.x402Accepts.some(
3867
+ (accept) => (accept.scheme ?? "exact") !== "upto"
3868
+ );
3869
+ if (!hasExact) {
3870
+ throw new Error(
3871
+ `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.`
3872
+ );
3873
+ }
3874
+ }
3875
+ if (this.#s.billing === "metered") {
3701
3876
  if (!this.#s.deps.mppSessionConfig) {
3702
3877
  throw new Error(
3703
- `route '${this.#s.key}': .paid({ dynamic: true }) on an MPP route requires session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
3878
+ `route '${this.#s.key}': .metered() requires MPP session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
3704
3879
  );
3705
3880
  }
3706
3881
  }
3707
- if (streaming && !this.#s.dynamicPrice) {
3882
+ if (streaming && this.#s.billing !== "metered") {
3708
3883
  throw new Error(
3709
- `route '${this.#s.key}': .stream() requires .paid({ dynamic: true }) \u2014 static/free routes can't meter per-chunk billing.`
3884
+ `route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
3710
3885
  );
3711
3886
  }
3712
3887
  validateExamples(
@@ -3724,7 +3899,7 @@ var RouteBuilder = class _RouteBuilder {
3724
3899
  authMode: this.#s.authMode,
3725
3900
  siwxEnabled: this.#s.siwxEnabled,
3726
3901
  pricing: this.#s.pricing,
3727
- dynamicPrice: this.#s.dynamicPrice ? true : void 0,
3902
+ billing: this.#s.billing,
3728
3903
  streaming: streaming ? true : void 0,
3729
3904
  protocols: this.#s.protocols,
3730
3905
  bodySchema: this.#s.bodySchema,
@@ -3751,20 +3926,63 @@ var RouteBuilder = class _RouteBuilder {
3751
3926
  return createRequestHandler(entry, handlerFn, this.#s.deps);
3752
3927
  }
3753
3928
  };
3754
- function resolvePaidArgs(routeKey, pricingOrOptions, options) {
3755
- const isHandlerDynamicShape = typeof pricingOrOptions === "object" && pricingOrOptions !== null && typeof pricingOrOptions !== "function" && !("tiers" in pricingOrOptions) && "dynamic" in pricingOrOptions && pricingOrOptions.dynamic;
3756
- if (isHandlerDynamicShape) {
3757
- const opts = pricingOrOptions;
3758
- if (!opts.maxPrice) {
3759
- throw new Error(`route '${routeKey}': .paid({ dynamic: true }) requires maxPrice`);
3760
- }
3761
- return { pricing: opts.maxPrice, resolvedOptions: opts };
3929
+ function normalizePaidArg(routeKey, arg, options) {
3930
+ if (typeof arg === "string") {
3931
+ return { pricing: arg, resolvedOptions: options ?? {}, billing: "exact" };
3762
3932
  }
3763
- return { pricing: pricingOrOptions, resolvedOptions: options };
3933
+ if (typeof arg === "function") {
3934
+ return {
3935
+ pricing: arg,
3936
+ resolvedOptions: options ?? {},
3937
+ billing: "exact"
3938
+ };
3939
+ }
3940
+ if ("tiers" in arg && "field" in arg) {
3941
+ return {
3942
+ pricing: { field: arg.field, tiers: arg.tiers, default: arg.default },
3943
+ resolvedOptions: arg,
3944
+ billing: "exact"
3945
+ };
3946
+ }
3947
+ if ("price" in arg && typeof arg.price === "string") {
3948
+ return { pricing: arg.price, resolvedOptions: arg, billing: "exact" };
3949
+ }
3950
+ throw new Error(
3951
+ `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().`
3952
+ );
3953
+ }
3954
+ function normalizeUpToArg(routeKey, arg) {
3955
+ const options = typeof arg === "string" ? { maxPrice: arg } : arg;
3956
+ if (!options.maxPrice) {
3957
+ throw new Error(`route '${routeKey}': .upTo() requires maxPrice`);
3958
+ }
3959
+ return {
3960
+ pricing: options.maxPrice,
3961
+ resolvedOptions: options,
3962
+ billing: "upto",
3963
+ unitType: options.unitType,
3964
+ maxPrice: options.maxPrice
3965
+ };
3966
+ }
3967
+ function normalizeMeteredArg(routeKey, options) {
3968
+ if (!options.maxPrice) {
3969
+ throw new Error(`route '${routeKey}': .metered() requires maxPrice`);
3970
+ }
3971
+ if (!options.tickCost) {
3972
+ throw new Error(`route '${routeKey}': .metered() requires tickCost`);
3973
+ }
3974
+ return {
3975
+ pricing: options.maxPrice,
3976
+ resolvedOptions: options,
3977
+ billing: "metered",
3978
+ tickCost: options.tickCost,
3979
+ unitType: options.unitType,
3980
+ maxPrice: options.maxPrice
3981
+ };
3764
3982
  }
3765
3983
 
3766
3984
  // src/discovery/well-known.ts
3767
- import { NextResponse as NextResponse8 } from "next/server";
3985
+ import { NextResponse as NextResponse9 } from "next/server";
3768
3986
 
3769
3987
  // src/discovery/utils/guidance.ts
3770
3988
  async function resolveGuidance(discovery) {
@@ -3808,7 +4026,7 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
3808
4026
  if (instructions) {
3809
4027
  body.instructions = instructions;
3810
4028
  }
3811
- return NextResponse8.json(body, {
4029
+ return NextResponse9.json(body, {
3812
4030
  headers: {
3813
4031
  "Access-Control-Allow-Origin": "*",
3814
4032
  "Access-Control-Allow-Methods": "GET",
@@ -3826,13 +4044,13 @@ function toDiscoveryResource(method, url, mode) {
3826
4044
 
3827
4045
  // src/discovery/openapi.ts
3828
4046
  init_constants();
3829
- import { NextResponse as NextResponse9 } from "next/server";
4047
+ import { NextResponse as NextResponse10 } from "next/server";
3830
4048
  function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
3831
4049
  const normalizedBase = baseUrl.replace(/\/+$/, "");
3832
4050
  let cached = null;
3833
4051
  let validated = false;
3834
4052
  return async (_request) => {
3835
- if (cached) return NextResponse9.json(cached);
4053
+ if (cached) return NextResponse10.json(cached);
3836
4054
  if (!validated && pricesKeys) {
3837
4055
  registry.validate(pricesKeys);
3838
4056
  validated = true;
@@ -3895,7 +4113,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
3895
4113
  paths
3896
4114
  };
3897
4115
  cached = createDocument(openApiDocument);
3898
- return NextResponse9.json(cached);
4116
+ return NextResponse10.json(cached);
3899
4117
  };
3900
4118
  }
3901
4119
  function deriveTag(routeKey) {
@@ -4031,11 +4249,11 @@ function tierExtrema(prices) {
4031
4249
  }
4032
4250
 
4033
4251
  // src/discovery/llms-txt.ts
4034
- import { NextResponse as NextResponse10 } from "next/server";
4252
+ import { NextResponse as NextResponse11 } from "next/server";
4035
4253
  function createLlmsTxtHandler(discovery) {
4036
4254
  return async (_request) => {
4037
4255
  const guidance = await resolveGuidance(discovery) ?? "";
4038
- return new NextResponse10(guidance, {
4256
+ return new NextResponse11(guidance, {
4039
4257
  headers: {
4040
4258
  "Content-Type": "text/plain; charset=utf-8",
4041
4259
  "Access-Control-Allow-Origin": "*"
@@ -4085,10 +4303,14 @@ var isPlaceholderEvm = (v) => ZERO_EVM_ADDRESS_RE.test(v);
4085
4303
  var isSolanaAddress = (v) => SOLANA_ADDRESS_RE.test(v);
4086
4304
  var isX402Network = (v) => v.startsWith("eip155:") || v.startsWith("solana:");
4087
4305
  var canonicalizeEvm = (addr) => addr.toLowerCase();
4306
+ function evmAddressFromKey(key) {
4307
+ if (!key || !isEvmPrivateKey(key)) return null;
4308
+ return privateKeyToAccount(key).address.toLowerCase();
4309
+ }
4088
4310
  function operatorAddressesCollide(opKey, fpKey) {
4089
- if (!opKey || !fpKey || !isEvmPrivateKey(opKey) || !isEvmPrivateKey(fpKey)) return null;
4090
- const op = privateKeyToAccount(opKey).address.toLowerCase();
4091
- const fp = privateKeyToAccount(fpKey).address.toLowerCase();
4311
+ const op = evmAddressFromKey(opKey);
4312
+ const fp = evmAddressFromKey(fpKey);
4313
+ if (!op || !fp) return null;
4092
4314
  return op === fp ? op : null;
4093
4315
  }
4094
4316
  function trimAll(raw) {
@@ -4250,7 +4472,7 @@ function getConfiguredX402Accepts2(config) {
4250
4472
  }
4251
4473
  ];
4252
4474
  }
4253
- function validateX402Config(config, env, options) {
4475
+ function validateX402Config(config, env) {
4254
4476
  const accepts = getConfiguredX402Accepts2(config);
4255
4477
  const issues = [];
4256
4478
  const push = (code, message) => issues.push({ code, protocol: "x402", message });
@@ -4292,23 +4514,21 @@ function validateX402Config(config, env, options) {
4292
4514
  `x402 payee '${placeholder}' is a placeholder address and cannot receive payments.`
4293
4515
  );
4294
4516
  }
4295
- if (options.requireCdpKeys !== false) {
4296
- const hasEvm = accepts.some(
4297
- (a) => typeof a.network === "string" && a.network.startsWith("eip155:")
4298
- );
4299
- if (hasEvm) {
4300
- const missing = ["CDP_API_KEY_ID", "CDP_API_KEY_SECRET"].filter((k) => !env[k]);
4301
- if (missing.length > 0) {
4302
- push(
4303
- "missing_cdp_keys",
4304
- `x402 EVM facilitator (Coinbase) requires ${missing.join(" and ")}.`
4305
- );
4306
- }
4517
+ const hasEvm = accepts.some(
4518
+ (a) => typeof a.network === "string" && a.network.startsWith("eip155:")
4519
+ );
4520
+ if (hasEvm) {
4521
+ const missing = ["CDP_API_KEY_ID", "CDP_API_KEY_SECRET"].filter((k) => !env[k]);
4522
+ if (missing.length > 0) {
4523
+ push(
4524
+ "missing_cdp_keys",
4525
+ `x402 EVM facilitator (Coinbase) requires ${missing.join(" and ")}. Create an API key at https://portal.cdp.coinbase.com and set it via env.`
4526
+ );
4307
4527
  }
4308
4528
  }
4309
4529
  return issues;
4310
4530
  }
4311
- function validateMppConfig(config) {
4531
+ function validateMppConfig(config, env) {
4312
4532
  const m = config.mpp;
4313
4533
  if (!m) {
4314
4534
  return [
@@ -4356,7 +4576,7 @@ function validateMppConfig(config) {
4356
4576
  `MPP recipient '${placeholder}' is a placeholder address and cannot receive payments.`
4357
4577
  );
4358
4578
  }
4359
- if (!m.rpcUrl) {
4579
+ if (!m.rpcUrl && !env.TEMPO_RPC_URL) {
4360
4580
  push(
4361
4581
  "missing_mpp_rpc_url",
4362
4582
  "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
@@ -4381,6 +4601,15 @@ function validateMppConfig(config) {
4381
4601
  `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).`
4382
4602
  );
4383
4603
  }
4604
+ if (m.session && recipient && isEvmAddress(recipient)) {
4605
+ const operatorAddress = evmAddressFromKey(m.operatorKey);
4606
+ if (operatorAddress && operatorAddress !== recipient.toLowerCase()) {
4607
+ push(
4608
+ "mpp_operator_recipient_mismatch",
4609
+ `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.`
4610
+ );
4611
+ }
4612
+ }
4384
4613
  return issues;
4385
4614
  }
4386
4615
  function translateZodIssues(error) {
@@ -4509,8 +4738,8 @@ function getRouterConfigIssues(config, options = {}) {
4509
4738
  message: "RouterConfig.protocols cannot be empty. Omit the field to use default ['x402'] or specify protocols explicitly."
4510
4739
  });
4511
4740
  }
4512
- if (protocols.includes("x402")) issues.push(...validateX402Config(config, env, options));
4513
- if (protocols.includes("mpp")) issues.push(...validateMppConfig(config));
4741
+ if (protocols.includes("x402")) issues.push(...validateX402Config(config, env));
4742
+ if (protocols.includes("mpp")) issues.push(...validateMppConfig(config, env));
4514
4743
  return issues;
4515
4744
  }
4516
4745
 
@@ -4595,9 +4824,6 @@ async function initMpp(config, resolvedBaseUrl, kvStore, configError) {
4595
4824
  const getClient = async () => tempoClient;
4596
4825
  const operatorAccount = config.mpp.operatorKey ? privateKeyToAccount2(config.mpp.operatorKey) : void 0;
4597
4826
  const feePayerAccount = config.mpp.feePayerKey ? privateKeyToAccount2(config.mpp.feePayerKey) : void 0;
4598
- if (config.mpp.session && operatorAccount) {
4599
- assertOperatorMatchesRecipient(config, operatorAccount.address);
4600
- }
4601
4827
  const resolvedStore = kvStore ? await createKvMppStore(kvStore) : void 0;
4602
4828
  const realm = new URL(resolvedBaseUrl).host;
4603
4829
  const mppConfig = config.mpp;
@@ -4635,15 +4861,6 @@ async function initMpp(config, resolvedBaseUrl, kvStore, configError) {
4635
4861
  return { initError: err instanceof Error ? err.message : String(err) };
4636
4862
  }
4637
4863
  }
4638
- function assertOperatorMatchesRecipient(config, operatorAddress) {
4639
- const recipient = (config.mpp?.recipient ?? config.payeeAddress)?.toLowerCase();
4640
- const opAddr = operatorAddress.toLowerCase();
4641
- if (recipient && opAddr !== recipient) {
4642
- throw new Error(
4643
- `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}.`
4644
- );
4645
- }
4646
- }
4647
4864
 
4648
4865
  // src/index.ts
4649
4866
  init_constants();
@@ -4654,10 +4871,7 @@ function createRouter(config) {
4654
4871
  const entitlementStore = kvStore ? createKvEntitlementStore(kvStore) : new MemoryEntitlementStore();
4655
4872
  const network = config.network ?? BASE_MAINNET_NETWORK;
4656
4873
  const x402Accepts = getConfiguredX402Accepts(config);
4657
- const configIssues = getRouterConfigIssues(config, {
4658
- env: process.env,
4659
- requireCdpKeys: process.env.NODE_ENV === "production"
4660
- });
4874
+ const configIssues = getRouterConfigIssues(config, { env: process.env });
4661
4875
  const baseUrlIssue = configIssues.find((issue) => issue.code === "missing_base_url");
4662
4876
  if (baseUrlIssue) throw new RouterConfigError([baseUrlIssue]);
4663
4877
  const emptyProtocolsIssue = configIssues.find((issue) => issue.code === "empty_protocols");
@@ -4670,10 +4884,7 @@ function createRouter(config) {
4670
4884
  const x402ConfigError = x402ConfigIssues.length > 0 ? formatRouterConfigIssues(x402ConfigIssues) : void 0;
4671
4885
  const mppConfigError = mppConfigIssues.length > 0 ? formatRouterConfigIssues(mppConfigIssues) : void 0;
4672
4886
  if (protocolConfigIssues.length > 0) {
4673
- for (const issue of protocolConfigIssues) console.error(`[router] ${issue.message}`);
4674
- if (process.env.NODE_ENV === "production") {
4675
- throw new RouterConfigError(protocolConfigIssues);
4676
- }
4887
+ throw new RouterConfigError(protocolConfigIssues);
4677
4888
  }
4678
4889
  const resolvedBaseUrl = config.baseUrl.replace(/\/+$/, "");
4679
4890
  if (config.plugin?.init) {
@@ -4699,7 +4910,7 @@ function createRouter(config) {
4699
4910
  x402Accepts,
4700
4911
  mppx: null,
4701
4912
  tempoClient: null,
4702
- mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
4913
+ mppSessionConfig: config.mpp?.session && config.mpp.operatorKey ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
4703
4914
  };
4704
4915
  deps.initPromise = (async () => {
4705
4916
  const x402Result = await initX402(config, kvStore, x402ConfigError);