@agentcash/router 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -141,6 +141,8 @@ export const GET = router.openapi();
141
141
 
142
142
  The barrel forces every route module to load before the discovery handler walks the registry — Next.js otherwise lazy-loads route files on first hit, and unloaded routes don't appear in the spec.
143
143
 
144
+ The `openapi.json` should be hosted at `GET <origin>/openapi.json`.
145
+
144
146
  ## Auth modes
145
147
 
146
148
  | Method | Purpose |
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") {
@@ -795,10 +807,7 @@ async function runHandler(ctx, handlerCtx) {
795
807
  }
796
808
  if (isAsyncIterable(returned) && !isThenable(returned)) {
797
809
  return errorResult(
798
- new HttpError(
799
- `route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
800
- 500
801
- )
810
+ new HttpError(`route '${ctx.routeEntry.key}': streaming handlers require .metered()`, 500)
802
811
  );
803
812
  }
804
813
  let rawResult;
@@ -1231,6 +1240,7 @@ var DynamicPricing = class {
1231
1240
  const raw = await this.opts.fn(body);
1232
1241
  return this.cap(raw, body);
1233
1242
  } catch (err) {
1243
+ if (err instanceof HttpError) throw err;
1234
1244
  this.alert("error", `Pricing function failed: ${msg(err)}`, {
1235
1245
  error: err instanceof Error ? err.stack : String(err),
1236
1246
  body
@@ -1753,7 +1763,7 @@ var mppStrategy = {
1753
1763
  async verify(args) {
1754
1764
  const info = readMppCredential(args.request);
1755
1765
  if (!info) return { ok: false, kind: "invalid" };
1756
- if (args.routeEntry.dynamicPrice) {
1766
+ if (args.routeEntry.billing === "metered") {
1757
1767
  if (!info.sessionAction) return { ok: false, kind: "invalid" };
1758
1768
  return verifySessionMode(args, info);
1759
1769
  }
@@ -1804,7 +1814,7 @@ var mppStrategy = {
1804
1814
  async buildChallenge(args) {
1805
1815
  if (!args.deps.mppx) return {};
1806
1816
  const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
1807
- if (args.routeEntry.dynamicPrice && sessionsConfigured) {
1817
+ if (args.routeEntry.billing === "metered" && sessionsConfigured) {
1808
1818
  const tickCost = args.routeEntry.tickCost;
1809
1819
  const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
1810
1820
  const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
@@ -2061,7 +2071,7 @@ function tagBareDecimalAsDollars(amount) {
2061
2071
  }
2062
2072
 
2063
2073
  // src/protocols/x402/verify.ts
2064
- var import_types2 = require("@x402/core/types");
2074
+ var import_types3 = require("@x402/core/types");
2065
2075
  async function verifyX402Payment(opts) {
2066
2076
  const { server, request, price, accepts, report } = opts;
2067
2077
  const payload = await readPaymentPayload(request);
@@ -2080,7 +2090,7 @@ async function verifyX402Payment(opts) {
2080
2090
  try {
2081
2091
  verify = await server.verifyPayment(payload, matching);
2082
2092
  } catch (err) {
2083
- if (err instanceof import_types2.VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
2093
+ if (err instanceof import_types3.VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
2084
2094
  return invalidPaymentVerification({
2085
2095
  reason: err.invalidReason ?? "verify_error",
2086
2096
  ...err.invalidMessage ? { message: err.invalidMessage } : {},
@@ -2181,7 +2191,7 @@ async function verifyX402(args) {
2181
2191
  const accepts = await resolveX402Accepts(
2182
2192
  request,
2183
2193
  routeEntry,
2184
- deps.x402Accepts,
2194
+ selectRouteAccepts(deps.x402Accepts, routeEntry),
2185
2195
  deps.payeeAddress,
2186
2196
  body
2187
2197
  );
@@ -2229,7 +2239,7 @@ async function verifyX402(args) {
2229
2239
  async function settleX402(args) {
2230
2240
  const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
2231
2241
  const { payload, requirements } = token;
2232
- const override = routeEntry.dynamicPrice ? { amount: billedAmount } : void 0;
2242
+ const override = routeEntry.billing === "exact" ? void 0 : { amount: billedAmount };
2233
2243
  try {
2234
2244
  const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2235
2245
  if (!settle.result?.success) {
@@ -2259,7 +2269,7 @@ async function buildX402ChallengeContribution(args) {
2259
2269
  const accepts = await resolveX402Accepts(
2260
2270
  request,
2261
2271
  routeEntry,
2262
- deps.x402Accepts,
2272
+ selectRouteAccepts(deps.x402Accepts, routeEntry),
2263
2273
  deps.payeeAddress,
2264
2274
  body
2265
2275
  );
@@ -2352,9 +2362,7 @@ async function buildChallengeExtensions(ctx) {
2352
2362
  } catch {
2353
2363
  }
2354
2364
  }
2355
- const hasEvmUpto = ctx.deps.x402Accepts.some(
2356
- (accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
2357
- );
2365
+ const hasEvmUpto = ctx.routeEntry.billing === "upto" && ctx.deps.x402Accepts.some((accept) => accept.scheme === "upto" && isEvmNetwork(accept.network));
2358
2366
  if (hasEvmUpto) {
2359
2367
  try {
2360
2368
  const { declareEip2612GasSponsoringExtension } = await import("@x402/extensions");
@@ -2521,10 +2529,7 @@ async function runDynamicChannelMgmtFlow(args) {
2521
2529
  });
2522
2530
  }
2523
2531
 
2524
- // src/pipeline/flows/dynamic/dynamic-invoke.ts
2525
- var import_server6 = require("next/server");
2526
-
2527
- // src/pricing/charge-context.ts
2532
+ // src/pricing/metered-charge.ts
2528
2533
  function createChargeContext(args) {
2529
2534
  const { tickCost, maxPrice, route } = args;
2530
2535
  const tickAtomic = decimalToAtomic(tickCost);
@@ -2559,15 +2564,10 @@ function createChargeContext(args) {
2559
2564
  };
2560
2565
  }
2561
2566
 
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 = {
2567
+ // src/pipeline/flows/dynamic/dynamic-invoke/shared.ts
2568
+ var import_server6 = require("next/server");
2569
+ function buildBaseHandlerCtx(ctx, wallet, account, body, payment) {
2570
+ return {
2571
2571
  body,
2572
2572
  query: parseQuery(ctx.request, ctx.routeEntry),
2573
2573
  request: ctx.request,
@@ -2579,12 +2579,41 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
2579
2579
  alert: ctx.report,
2580
2580
  setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
2581
2581
  };
2582
+ }
2583
+ function toResponse(rawResult) {
2584
+ return rawResult instanceof Response ? rawResult : import_server6.NextResponse.json(rawResult);
2585
+ }
2586
+ function errorResult2(error) {
2587
+ const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
2588
+ const message = error instanceof Error ? error.message : "Internal error";
2589
+ return {
2590
+ kind: "request",
2591
+ response: import_server6.NextResponse.json({ success: false, error: message }, { status }),
2592
+ rawResult: void 0,
2593
+ handlerError: error
2594
+ };
2595
+ }
2596
+ function isAsyncIterable2(value) {
2597
+ return value != null && typeof value === "object" && Symbol.asyncIterator in value;
2598
+ }
2599
+ function isThenable2(value) {
2600
+ return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
2601
+ }
2602
+
2603
+ // src/pipeline/flows/dynamic/dynamic-invoke/metered-invoke.ts
2604
+ async function invokeMetered(ctx, wallet, account, body, payment) {
2605
+ const chargeContext = ctx.routeEntry.streaming ? createChargeContext({
2606
+ tickCost: ctx.routeEntry.tickCost,
2607
+ maxPrice: ctx.routeEntry.maxPrice,
2608
+ route: ctx.routeEntry.key
2609
+ }) : null;
2610
+ const baseHandlerCtx = buildBaseHandlerCtx(ctx, wallet, account, body, payment);
2582
2611
  const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
2583
2612
  let returned;
2584
2613
  try {
2585
2614
  returned = ctx.handler(handlerCtx);
2586
2615
  } catch (error) {
2587
- return errorResult2(error, chargeContext);
2616
+ return errorResult2(error);
2588
2617
  }
2589
2618
  if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2590
2619
  if (!chargeContext) {
@@ -2592,41 +2621,80 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
2592
2621
  new HttpError(
2593
2622
  "route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
2594
2623
  500
2595
- ),
2596
- null
2624
+ )
2597
2625
  );
2598
2626
  }
2599
- return {
2600
- kind: "stream",
2601
- source: returned,
2602
- chargeContext
2603
- };
2627
+ return { kind: "stream", source: returned, chargeContext };
2604
2628
  }
2605
2629
  let rawResult;
2606
2630
  try {
2607
2631
  rawResult = await returned;
2608
2632
  } catch (error) {
2609
- return errorResult2(error, chargeContext);
2633
+ return errorResult2(error);
2610
2634
  }
2611
- const response = rawResult instanceof Response ? rawResult : import_server6.NextResponse.json(rawResult);
2612
- return { kind: "request", response, rawResult };
2635
+ return { kind: "request", response: toResponse(rawResult), rawResult };
2613
2636
  }
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;
2637
+
2638
+ // src/pricing/upto-charge.ts
2639
+ function createUptoChargeContext(args) {
2640
+ const { maxPrice, route } = args;
2641
+ const capAtomic = decimalToAtomic(maxPrice);
2642
+ if (capAtomic <= 0n) {
2643
+ throw new Error(`route '${route}': maxPrice '${maxPrice}' must be a positive decimal string`);
2644
+ }
2645
+ let calls = 0;
2646
+ let atomic = 0n;
2647
+ const charge = async (amount) => {
2648
+ const nextAtomic = atomic + decimalToAtomic(amount);
2649
+ if (nextAtomic > capAtomic) {
2650
+ throw Object.assign(
2651
+ new Error(
2652
+ `route '${route}': charge() running total ($${atomicToDecimal(nextAtomic)}) exceeds maxPrice ($${atomicToDecimal(capAtomic)})`
2653
+ ),
2654
+ { status: 400, code: "CHARGE_OVER_CAP" }
2655
+ );
2656
+ }
2657
+ calls += 1;
2658
+ atomic = nextAtomic;
2659
+ };
2618
2660
  return {
2619
- kind: "request",
2620
- response: import_server6.NextResponse.json({ success: false, error: message }, { status }),
2621
- rawResult: void 0,
2622
- handlerError: error
2661
+ charge,
2662
+ callCount: () => calls,
2663
+ atomicTotal: () => atomic
2623
2664
  };
2624
2665
  }
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";
2666
+
2667
+ // src/pipeline/flows/dynamic/dynamic-invoke/upto-invoke.ts
2668
+ async function invokeUpto(ctx, wallet, account, body, payment) {
2669
+ const uptoCtx = createUptoChargeContext({
2670
+ maxPrice: ctx.routeEntry.maxPrice,
2671
+ route: ctx.routeEntry.key
2672
+ });
2673
+ const handlerCtx = {
2674
+ ...buildBaseHandlerCtx(ctx, wallet, account, body, payment),
2675
+ charge: uptoCtx.charge
2676
+ };
2677
+ let returned;
2678
+ try {
2679
+ returned = ctx.handler(handlerCtx);
2680
+ } catch (error) {
2681
+ return errorResult2(error);
2682
+ }
2683
+ if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2684
+ return errorResult2(
2685
+ new HttpError(
2686
+ "streaming is not supported on .upTo() routes \u2014 return a value from .handler() instead",
2687
+ 500
2688
+ )
2689
+ );
2690
+ }
2691
+ let rawResult;
2692
+ try {
2693
+ rawResult = await returned;
2694
+ } catch (error) {
2695
+ return errorResult2(error);
2696
+ }
2697
+ return { kind: "request", response: toResponse(rawResult), rawResult, uptoContext: uptoCtx };
2630
2698
  }
2631
2699
 
2632
2700
  // src/pipeline/flows/dynamic/dynamic-preflight.ts
@@ -2656,7 +2724,7 @@ async function runDynamicRequestFlow(args) {
2656
2724
  }
2657
2725
  const beforeErr = await runBeforeSettle(ctx, settleScope);
2658
2726
  if (beforeErr) return beforeErr;
2659
- const billedAmount = routeEntry.tickCost;
2727
+ const billedAmount = computeBilledAmount(routeEntry, result);
2660
2728
  return settleAndFinalizeRequest({
2661
2729
  ctx,
2662
2730
  strategy,
@@ -2674,6 +2742,19 @@ async function runDynamicRequestFlow(args) {
2674
2742
  }
2675
2743
  });
2676
2744
  }
2745
+ function computeBilledAmount(routeEntry, result) {
2746
+ if (routeEntry.billing === "upto") {
2747
+ const total = result.uptoContext?.atomicTotal() ?? 0n;
2748
+ if (total <= 0n) {
2749
+ throw new HttpError(
2750
+ `route '${routeEntry.key}': handler did not call charge(amount) \u2014 upto routes must accumulate a non-zero billed amount`,
2751
+ 500
2752
+ );
2753
+ }
2754
+ return atomicToDecimal(total);
2755
+ }
2756
+ return routeEntry.tickCost;
2757
+ }
2677
2758
 
2678
2759
  // src/pipeline/flows/dynamic/dynamic-stream.ts
2679
2760
  async function runDynamicStreamFlow(args) {
@@ -2746,13 +2827,7 @@ async function runDynamicPaidFlow(ctx) {
2746
2827
  amount: price,
2747
2828
  network: verifyOutcome.payment.network
2748
2829
  });
2749
- const result = await invokeDynamic(
2750
- ctx,
2751
- verifyOutcome.wallet,
2752
- account,
2753
- parsedBody,
2754
- verifyOutcome.payment
2755
- );
2830
+ const result = await invokeDynamic(ctx, verifyOutcome, account, parsedBody);
2756
2831
  switch (result.kind) {
2757
2832
  case "stream":
2758
2833
  return runDynamicStreamFlow({
@@ -2774,6 +2849,18 @@ async function runDynamicPaidFlow(ctx) {
2774
2849
  });
2775
2850
  }
2776
2851
  }
2852
+ async function invokeDynamic(ctx, verifyOutcome, account, parsedBody) {
2853
+ switch (ctx.routeEntry.billing) {
2854
+ case "upto":
2855
+ return invokeUpto(ctx, verifyOutcome.wallet, account, parsedBody, verifyOutcome.payment);
2856
+ case "metered":
2857
+ return invokeMetered(ctx, verifyOutcome.wallet, account, parsedBody, verifyOutcome.payment);
2858
+ case "exact":
2859
+ throw new Error(
2860
+ `route '${ctx.routeEntry.key}': exact billing must not reach the dynamic paid flow`
2861
+ );
2862
+ }
2863
+ }
2777
2864
 
2778
2865
  // src/pipeline/flows/static/static-body-and-price.ts
2779
2866
  async function resolveStaticBodyAndPrice(args) {
@@ -2921,13 +3008,8 @@ async function runStaticPaidFlow(ctx) {
2921
3008
 
2922
3009
  // src/pipeline/flows/paid.ts
2923
3010
  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
- }
3011
+ const handlerCharged = ctx.routeEntry.billing !== "exact";
3012
+ return handlerCharged ? runDynamicPaidFlow(ctx) : runStaticPaidFlow(ctx);
2931
3013
  }
2932
3014
 
2933
3015
  // src/pipeline/flows/siwx-only.ts
@@ -3325,7 +3407,7 @@ var RouteBuilder = class _RouteBuilder {
3325
3407
  protocols: defaults?.protocols ? [...defaults.protocols] : ["x402"],
3326
3408
  maxPrice: void 0,
3327
3409
  minPrice: void 0,
3328
- dynamicPrice: false,
3410
+ billing: "exact",
3329
3411
  tickCost: void 0,
3330
3412
  unitType: void 0,
3331
3413
  payTo: void 0,
@@ -3356,39 +3438,84 @@ var RouteBuilder = class _RouteBuilder {
3356
3438
  next.#s = { ...this.#s, protocols: [...this.#s.protocols] };
3357
3439
  return next;
3358
3440
  }
3359
- paid(pricingOrOptions, options) {
3360
- const { pricing, resolvedOptions } = resolvePaidArgs(this.#s.key, pricingOrOptions, options);
3441
+ paid(arg, options) {
3442
+ return this.applyPaid(normalizePaidArg(this.#s.key, arg, options), "paid");
3443
+ }
3444
+ /**
3445
+ * x402-only handler-computed billing. The handler receives `charge(amount)`
3446
+ * and the request settles once for the accumulated total, capped at
3447
+ * `maxPrice`. Requires an `'upto'` accept on at least one configured network.
3448
+ * Pass a bare string as sugar for `{ maxPrice }`.
3449
+ *
3450
+ * @example
3451
+ * ```ts
3452
+ * router.route('llm')
3453
+ * .upTo('0.05')
3454
+ * .body(schema)
3455
+ * .handler(async ({ body, charge }) => { await charge('0.001'); ... });
3456
+ * ```
3457
+ */
3458
+ upTo(arg) {
3459
+ return this.applyPaid(normalizeUpToArg(this.#s.key, arg), "upTo");
3460
+ }
3461
+ /**
3462
+ * MPP-only per-tick billing. `.handler()` bills exactly `tickCost`;
3463
+ * `.stream()` calls `charge()` (no-arg) per yield, settling per tick up to
3464
+ * `maxPrice`. Requires `RouterConfig.mpp.session`.
3465
+ *
3466
+ * @example
3467
+ * ```ts
3468
+ * router.route('llm/stream')
3469
+ * .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
3470
+ * .stream(async function* ({ charge }) { await charge(); yield 'hi'; });
3471
+ * ```
3472
+ */
3473
+ metered(options) {
3474
+ return this.applyPaid(normalizeMeteredArg(this.#s.key, options), "metered");
3475
+ }
3476
+ applyPaid(normalized, method) {
3477
+ const { pricing, resolvedOptions, billing, tickCost, unitType, maxPrice } = normalized;
3361
3478
  if (this.#s.authMode === "unprotected") {
3362
3479
  throw new Error(
3363
- `route '${this.#s.key}': Cannot combine .unprotected() and .paid() on the same route.`
3480
+ `route '${this.#s.key}': Cannot combine .unprotected() and .${method}() on the same route.`
3364
3481
  );
3365
3482
  }
3366
3483
  if (this.#s.pricing !== void 0) {
3367
3484
  throw new Error(
3368
- `route '${this.#s.key}': Cannot call .paid() more than once on the same route.`
3485
+ `route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
3369
3486
  );
3370
3487
  }
3371
3488
  const next = this.fork();
3372
3489
  next.#s.authMode = "paid";
3373
3490
  next.#s.pricing = pricing;
3374
- if (resolvedOptions?.protocols) {
3491
+ if (billing === "upto") {
3492
+ if (resolvedOptions.protocols?.some((p) => p !== "x402")) {
3493
+ throw new Error(
3494
+ `route '${this.#s.key}': .upTo() is x402-only \u2014 remove the conflicting protocols override.`
3495
+ );
3496
+ }
3497
+ next.#s.protocols = ["x402"];
3498
+ } else if (billing === "metered") {
3499
+ if (resolvedOptions.protocols?.some((p) => p !== "mpp")) {
3500
+ throw new Error(
3501
+ `route '${this.#s.key}': .metered() is MPP-only \u2014 remove the conflicting protocols override.`
3502
+ );
3503
+ }
3504
+ next.#s.protocols = ["mpp"];
3505
+ } else if (resolvedOptions.protocols) {
3375
3506
  next.#s.protocols = [...resolvedOptions.protocols];
3376
3507
  } else if (next.#s.protocols.length === 0) {
3377
3508
  next.#s.protocols = ["x402"];
3378
3509
  }
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;
3510
+ if (resolvedOptions.maxPrice) next.#s.maxPrice = resolvedOptions.maxPrice;
3511
+ if (maxPrice) next.#s.maxPrice = maxPrice;
3512
+ if (resolvedOptions.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
3513
+ if (resolvedOptions.payTo) next.#s.payTo = resolvedOptions.payTo;
3514
+ if (resolvedOptions.mpp) next.#s.mppInfo = resolvedOptions.mpp;
3515
+ next.#s.billing = billing;
3516
+ if (tickCost) next.#s.tickCost = tickCost;
3517
+ if (unitType) next.#s.unitType = unitType;
3386
3518
  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
3519
  for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
3393
3520
  if (!tierKey) {
3394
3521
  throw new Error(`route '${this.#s.key}': tier key cannot be empty`);
@@ -3400,22 +3527,16 @@ var RouteBuilder = class _RouteBuilder {
3400
3527
  }
3401
3528
  }
3402
3529
  }
3403
- if (resolvedOptions?.maxPrice !== void 0 && !isPositiveDecimal(resolvedOptions.maxPrice)) {
3530
+ if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
3404
3531
  throw new Error(
3405
- `route '${this.#s.key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
3532
+ `route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
3406
3533
  );
3407
3534
  }
3408
- if (resolvedOptions?.tickCost !== void 0 && !isPositiveDecimal(resolvedOptions.tickCost)) {
3535
+ if (next.#s.tickCost !== void 0 && !isPositiveDecimal(next.#s.tickCost)) {
3409
3536
  throw new Error(
3410
- `route '${this.#s.key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
3537
+ `route '${this.#s.key}': tickCost '${next.#s.tickCost}' must be a positive decimal string`
3411
3538
  );
3412
3539
  }
3413
- if (next.#s.dynamicPrice && !next.#s.maxPrice) {
3414
- throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires maxPrice`);
3415
- }
3416
- if (next.#s.dynamicPrice && !next.#s.tickCost) {
3417
- throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires tickCost`);
3418
- }
3419
3540
  return next;
3420
3541
  }
3421
3542
  /**
@@ -3697,13 +3818,13 @@ var RouteBuilder = class _RouteBuilder {
3697
3818
  /**
3698
3819
  * Register a streaming handler (`async function*`) and return the Next.js
3699
3820
  * route function. Each `charge()` call bills one tick (`tickCost` USDC) up
3700
- * to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
3821
+ * to `maxPrice`; requires `.metered({ ... })` and MPP session mode.
3701
3822
  *
3702
3823
  * @example
3703
3824
  * ```ts
3704
3825
  * export const POST = router
3705
3826
  * .route('llm/stream')
3706
- * .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
3827
+ * .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
3707
3828
  * .body(schema)
3708
3829
  * .stream(async function* ({ body, charge }) {
3709
3830
  * for await (const token of streamLLM(body.prompt)) {
@@ -3719,7 +3840,7 @@ var RouteBuilder = class _RouteBuilder {
3719
3840
  register(handlerFn, streaming) {
3720
3841
  if (!this.#s.authMode) {
3721
3842
  throw new Error(
3722
- `route '${this.#s.key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
3843
+ `route '${this.#s.key}': Select an auth mode: .paid(pricing), .upTo(maxPrice), .metered(options), .siwx(), .apiKey(resolver), or .unprotected()`
3723
3844
  );
3724
3845
  }
3725
3846
  if (this.#s.validateFn && !this.#s.bodySchema) {
@@ -3730,24 +3851,34 @@ var RouteBuilder = class _RouteBuilder {
3730
3851
  if (this.#s.settlement && !this.#s.pricing) {
3731
3852
  throw new Error(`route '${this.#s.key}': .settlement() requires a paid route`);
3732
3853
  }
3733
- if (this.#s.dynamicPrice && this.#s.protocols.includes("x402")) {
3854
+ if (this.#s.billing === "upto") {
3734
3855
  const hasUpto = this.#s.deps.x402Accepts.some((accept) => accept.scheme === "upto");
3735
3856
  if (!hasUpto) {
3736
3857
  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.`
3858
+ `route '${this.#s.key}': .upTo() requires an 'upto' accept on at least one configured network. Add { scheme: 'upto', network, asset } to RouterConfig.x402.accepts.`
3859
+ );
3860
+ }
3861
+ }
3862
+ if (this.#s.pricing !== void 0 && this.#s.billing === "exact" && this.#s.protocols.includes("x402")) {
3863
+ const hasExact = this.#s.deps.x402Accepts.some(
3864
+ (accept) => (accept.scheme ?? "exact") !== "upto"
3865
+ );
3866
+ if (!hasExact) {
3867
+ throw new Error(
3868
+ `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.`
3738
3869
  );
3739
3870
  }
3740
3871
  }
3741
- if (this.#s.dynamicPrice && this.#s.protocols.includes("mpp")) {
3872
+ if (this.#s.billing === "metered") {
3742
3873
  if (!this.#s.deps.mppSessionConfig) {
3743
3874
  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.`
3875
+ `route '${this.#s.key}': .metered() requires MPP session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
3745
3876
  );
3746
3877
  }
3747
3878
  }
3748
- if (streaming && !this.#s.dynamicPrice) {
3879
+ if (streaming && this.#s.billing !== "metered") {
3749
3880
  throw new Error(
3750
- `route '${this.#s.key}': .stream() requires .paid({ dynamic: true }) \u2014 static/free routes can't meter per-chunk billing.`
3881
+ `route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
3751
3882
  );
3752
3883
  }
3753
3884
  validateExamples(
@@ -3765,7 +3896,7 @@ var RouteBuilder = class _RouteBuilder {
3765
3896
  authMode: this.#s.authMode,
3766
3897
  siwxEnabled: this.#s.siwxEnabled,
3767
3898
  pricing: this.#s.pricing,
3768
- dynamicPrice: this.#s.dynamicPrice ? true : void 0,
3899
+ billing: this.#s.billing,
3769
3900
  streaming: streaming ? true : void 0,
3770
3901
  protocols: this.#s.protocols,
3771
3902
  bodySchema: this.#s.bodySchema,
@@ -3792,16 +3923,59 @@ var RouteBuilder = class _RouteBuilder {
3792
3923
  return createRequestHandler(entry, handlerFn, this.#s.deps);
3793
3924
  }
3794
3925
  };
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 };
3926
+ function normalizePaidArg(routeKey, arg, options) {
3927
+ if (typeof arg === "string") {
3928
+ return { pricing: arg, resolvedOptions: options ?? {}, billing: "exact" };
3929
+ }
3930
+ if (typeof arg === "function") {
3931
+ return {
3932
+ pricing: arg,
3933
+ resolvedOptions: options ?? {},
3934
+ billing: "exact"
3935
+ };
3803
3936
  }
3804
- return { pricing: pricingOrOptions, resolvedOptions: options };
3937
+ if ("tiers" in arg && "field" in arg) {
3938
+ return {
3939
+ pricing: { field: arg.field, tiers: arg.tiers, default: arg.default },
3940
+ resolvedOptions: arg,
3941
+ billing: "exact"
3942
+ };
3943
+ }
3944
+ if ("price" in arg && typeof arg.price === "string") {
3945
+ return { pricing: arg.price, resolvedOptions: arg, billing: "exact" };
3946
+ }
3947
+ throw new Error(
3948
+ `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().`
3949
+ );
3950
+ }
3951
+ function normalizeUpToArg(routeKey, arg) {
3952
+ const options = typeof arg === "string" ? { maxPrice: arg } : arg;
3953
+ if (!options.maxPrice) {
3954
+ throw new Error(`route '${routeKey}': .upTo() requires maxPrice`);
3955
+ }
3956
+ return {
3957
+ pricing: options.maxPrice,
3958
+ resolvedOptions: options,
3959
+ billing: "upto",
3960
+ unitType: options.unitType,
3961
+ maxPrice: options.maxPrice
3962
+ };
3963
+ }
3964
+ function normalizeMeteredArg(routeKey, options) {
3965
+ if (!options.maxPrice) {
3966
+ throw new Error(`route '${routeKey}': .metered() requires maxPrice`);
3967
+ }
3968
+ if (!options.tickCost) {
3969
+ throw new Error(`route '${routeKey}': .metered() requires tickCost`);
3970
+ }
3971
+ return {
3972
+ pricing: options.maxPrice,
3973
+ resolvedOptions: options,
3974
+ billing: "metered",
3975
+ tickCost: options.tickCost,
3976
+ unitType: options.unitType,
3977
+ maxPrice: options.maxPrice
3978
+ };
3805
3979
  }
3806
3980
 
3807
3981
  // src/discovery/well-known.ts
@@ -4740,7 +4914,7 @@ function createRouter(config) {
4740
4914
  x402Accepts,
4741
4915
  mppx: null,
4742
4916
  tempoClient: null,
4743
- mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
4917
+ mppSessionConfig: config.mpp?.session && config.mpp.operatorKey ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
4744
4918
  };
4745
4919
  deps.initPromise = (async () => {
4746
4920
  const x402Result = await initX402(config, kvStore, x402ConfigError);