@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/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") {
@@ -754,10 +766,7 @@ async function runHandler(ctx, handlerCtx) {
754
766
  }
755
767
  if (isAsyncIterable(returned) && !isThenable(returned)) {
756
768
  return errorResult(
757
- new HttpError(
758
- `route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
759
- 500
760
- )
769
+ new HttpError(`route '${ctx.routeEntry.key}': streaming handlers require .metered()`, 500)
761
770
  );
762
771
  }
763
772
  let rawResult;
@@ -1190,6 +1199,7 @@ var DynamicPricing = class {
1190
1199
  const raw = await this.opts.fn(body);
1191
1200
  return this.cap(raw, body);
1192
1201
  } catch (err) {
1202
+ if (err instanceof HttpError) throw err;
1193
1203
  this.alert("error", `Pricing function failed: ${msg(err)}`, {
1194
1204
  error: err instanceof Error ? err.stack : String(err),
1195
1205
  body
@@ -1712,7 +1722,7 @@ var mppStrategy = {
1712
1722
  async verify(args) {
1713
1723
  const info = readMppCredential(args.request);
1714
1724
  if (!info) return { ok: false, kind: "invalid" };
1715
- if (args.routeEntry.dynamicPrice) {
1725
+ if (args.routeEntry.billing === "metered") {
1716
1726
  if (!info.sessionAction) return { ok: false, kind: "invalid" };
1717
1727
  return verifySessionMode(args, info);
1718
1728
  }
@@ -1763,7 +1773,7 @@ var mppStrategy = {
1763
1773
  async buildChallenge(args) {
1764
1774
  if (!args.deps.mppx) return {};
1765
1775
  const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
1766
- if (args.routeEntry.dynamicPrice && sessionsConfigured) {
1776
+ if (args.routeEntry.billing === "metered" && sessionsConfigured) {
1767
1777
  const tickCost = args.routeEntry.tickCost;
1768
1778
  const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
1769
1779
  const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
@@ -2140,7 +2150,7 @@ async function verifyX402(args) {
2140
2150
  const accepts = await resolveX402Accepts(
2141
2151
  request,
2142
2152
  routeEntry,
2143
- deps.x402Accepts,
2153
+ selectRouteAccepts(deps.x402Accepts, routeEntry),
2144
2154
  deps.payeeAddress,
2145
2155
  body
2146
2156
  );
@@ -2188,7 +2198,7 @@ async function verifyX402(args) {
2188
2198
  async function settleX402(args) {
2189
2199
  const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
2190
2200
  const { payload, requirements } = token;
2191
- const override = routeEntry.dynamicPrice ? { amount: billedAmount } : void 0;
2201
+ const override = routeEntry.billing === "exact" ? void 0 : { amount: billedAmount };
2192
2202
  try {
2193
2203
  const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2194
2204
  if (!settle.result?.success) {
@@ -2218,7 +2228,7 @@ async function buildX402ChallengeContribution(args) {
2218
2228
  const accepts = await resolveX402Accepts(
2219
2229
  request,
2220
2230
  routeEntry,
2221
- deps.x402Accepts,
2231
+ selectRouteAccepts(deps.x402Accepts, routeEntry),
2222
2232
  deps.payeeAddress,
2223
2233
  body
2224
2234
  );
@@ -2311,9 +2321,7 @@ async function buildChallengeExtensions(ctx) {
2311
2321
  } catch {
2312
2322
  }
2313
2323
  }
2314
- const hasEvmUpto = ctx.deps.x402Accepts.some(
2315
- (accept) => accept.scheme === "upto" && isEvmNetwork(accept.network)
2316
- );
2324
+ const hasEvmUpto = ctx.routeEntry.billing === "upto" && ctx.deps.x402Accepts.some((accept) => accept.scheme === "upto" && isEvmNetwork(accept.network));
2317
2325
  if (hasEvmUpto) {
2318
2326
  try {
2319
2327
  const { declareEip2612GasSponsoringExtension } = await import("@x402/extensions");
@@ -2480,10 +2488,7 @@ async function runDynamicChannelMgmtFlow(args) {
2480
2488
  });
2481
2489
  }
2482
2490
 
2483
- // src/pipeline/flows/dynamic/dynamic-invoke.ts
2484
- import { NextResponse as NextResponse6 } from "next/server";
2485
-
2486
- // src/pricing/charge-context.ts
2491
+ // src/pricing/metered-charge.ts
2487
2492
  function createChargeContext(args) {
2488
2493
  const { tickCost, maxPrice, route } = args;
2489
2494
  const tickAtomic = decimalToAtomic(tickCost);
@@ -2518,15 +2523,10 @@ function createChargeContext(args) {
2518
2523
  };
2519
2524
  }
2520
2525
 
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 = {
2526
+ // src/pipeline/flows/dynamic/dynamic-invoke/shared.ts
2527
+ import { NextResponse as NextResponse6 } from "next/server";
2528
+ function buildBaseHandlerCtx(ctx, wallet, account, body, payment) {
2529
+ return {
2530
2530
  body,
2531
2531
  query: parseQuery(ctx.request, ctx.routeEntry),
2532
2532
  request: ctx.request,
@@ -2538,12 +2538,41 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
2538
2538
  alert: ctx.report,
2539
2539
  setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
2540
2540
  };
2541
+ }
2542
+ function toResponse(rawResult) {
2543
+ return rawResult instanceof Response ? rawResult : NextResponse6.json(rawResult);
2544
+ }
2545
+ function errorResult2(error) {
2546
+ const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
2547
+ const message = error instanceof Error ? error.message : "Internal error";
2548
+ return {
2549
+ kind: "request",
2550
+ response: NextResponse6.json({ success: false, error: message }, { status }),
2551
+ rawResult: void 0,
2552
+ handlerError: error
2553
+ };
2554
+ }
2555
+ function isAsyncIterable2(value) {
2556
+ return value != null && typeof value === "object" && Symbol.asyncIterator in value;
2557
+ }
2558
+ function isThenable2(value) {
2559
+ return value != null && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
2560
+ }
2561
+
2562
+ // src/pipeline/flows/dynamic/dynamic-invoke/metered-invoke.ts
2563
+ async function invokeMetered(ctx, wallet, account, body, payment) {
2564
+ const chargeContext = ctx.routeEntry.streaming ? createChargeContext({
2565
+ tickCost: ctx.routeEntry.tickCost,
2566
+ maxPrice: ctx.routeEntry.maxPrice,
2567
+ route: ctx.routeEntry.key
2568
+ }) : null;
2569
+ const baseHandlerCtx = buildBaseHandlerCtx(ctx, wallet, account, body, payment);
2541
2570
  const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
2542
2571
  let returned;
2543
2572
  try {
2544
2573
  returned = ctx.handler(handlerCtx);
2545
2574
  } catch (error) {
2546
- return errorResult2(error, chargeContext);
2575
+ return errorResult2(error);
2547
2576
  }
2548
2577
  if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2549
2578
  if (!chargeContext) {
@@ -2551,41 +2580,80 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
2551
2580
  new HttpError(
2552
2581
  "route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
2553
2582
  500
2554
- ),
2555
- null
2583
+ )
2556
2584
  );
2557
2585
  }
2558
- return {
2559
- kind: "stream",
2560
- source: returned,
2561
- chargeContext
2562
- };
2586
+ return { kind: "stream", source: returned, chargeContext };
2563
2587
  }
2564
2588
  let rawResult;
2565
2589
  try {
2566
2590
  rawResult = await returned;
2567
2591
  } catch (error) {
2568
- return errorResult2(error, chargeContext);
2592
+ return errorResult2(error);
2569
2593
  }
2570
- const response = rawResult instanceof Response ? rawResult : NextResponse6.json(rawResult);
2571
- return { kind: "request", response, rawResult };
2594
+ return { kind: "request", response: toResponse(rawResult), rawResult };
2572
2595
  }
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;
2596
+
2597
+ // src/pricing/upto-charge.ts
2598
+ function createUptoChargeContext(args) {
2599
+ const { maxPrice, route } = args;
2600
+ const capAtomic = decimalToAtomic(maxPrice);
2601
+ if (capAtomic <= 0n) {
2602
+ throw new Error(`route '${route}': maxPrice '${maxPrice}' must be a positive decimal string`);
2603
+ }
2604
+ let calls = 0;
2605
+ let atomic = 0n;
2606
+ const charge = async (amount) => {
2607
+ const nextAtomic = atomic + decimalToAtomic(amount);
2608
+ if (nextAtomic > capAtomic) {
2609
+ throw Object.assign(
2610
+ new Error(
2611
+ `route '${route}': charge() running total ($${atomicToDecimal(nextAtomic)}) exceeds maxPrice ($${atomicToDecimal(capAtomic)})`
2612
+ ),
2613
+ { status: 400, code: "CHARGE_OVER_CAP" }
2614
+ );
2615
+ }
2616
+ calls += 1;
2617
+ atomic = nextAtomic;
2618
+ };
2577
2619
  return {
2578
- kind: "request",
2579
- response: NextResponse6.json({ success: false, error: message }, { status }),
2580
- rawResult: void 0,
2581
- handlerError: error
2620
+ charge,
2621
+ callCount: () => calls,
2622
+ atomicTotal: () => atomic
2582
2623
  };
2583
2624
  }
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";
2625
+
2626
+ // src/pipeline/flows/dynamic/dynamic-invoke/upto-invoke.ts
2627
+ async function invokeUpto(ctx, wallet, account, body, payment) {
2628
+ const uptoCtx = createUptoChargeContext({
2629
+ maxPrice: ctx.routeEntry.maxPrice,
2630
+ route: ctx.routeEntry.key
2631
+ });
2632
+ const handlerCtx = {
2633
+ ...buildBaseHandlerCtx(ctx, wallet, account, body, payment),
2634
+ charge: uptoCtx.charge
2635
+ };
2636
+ let returned;
2637
+ try {
2638
+ returned = ctx.handler(handlerCtx);
2639
+ } catch (error) {
2640
+ return errorResult2(error);
2641
+ }
2642
+ if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2643
+ return errorResult2(
2644
+ new HttpError(
2645
+ "streaming is not supported on .upTo() routes \u2014 return a value from .handler() instead",
2646
+ 500
2647
+ )
2648
+ );
2649
+ }
2650
+ let rawResult;
2651
+ try {
2652
+ rawResult = await returned;
2653
+ } catch (error) {
2654
+ return errorResult2(error);
2655
+ }
2656
+ return { kind: "request", response: toResponse(rawResult), rawResult, uptoContext: uptoCtx };
2589
2657
  }
2590
2658
 
2591
2659
  // src/pipeline/flows/dynamic/dynamic-preflight.ts
@@ -2615,7 +2683,7 @@ async function runDynamicRequestFlow(args) {
2615
2683
  }
2616
2684
  const beforeErr = await runBeforeSettle(ctx, settleScope);
2617
2685
  if (beforeErr) return beforeErr;
2618
- const billedAmount = routeEntry.tickCost;
2686
+ const billedAmount = computeBilledAmount(routeEntry, result);
2619
2687
  return settleAndFinalizeRequest({
2620
2688
  ctx,
2621
2689
  strategy,
@@ -2633,6 +2701,19 @@ async function runDynamicRequestFlow(args) {
2633
2701
  }
2634
2702
  });
2635
2703
  }
2704
+ function computeBilledAmount(routeEntry, result) {
2705
+ if (routeEntry.billing === "upto") {
2706
+ const total = result.uptoContext?.atomicTotal() ?? 0n;
2707
+ if (total <= 0n) {
2708
+ throw new HttpError(
2709
+ `route '${routeEntry.key}': handler did not call charge(amount) \u2014 upto routes must accumulate a non-zero billed amount`,
2710
+ 500
2711
+ );
2712
+ }
2713
+ return atomicToDecimal(total);
2714
+ }
2715
+ return routeEntry.tickCost;
2716
+ }
2636
2717
 
2637
2718
  // src/pipeline/flows/dynamic/dynamic-stream.ts
2638
2719
  async function runDynamicStreamFlow(args) {
@@ -2705,13 +2786,7 @@ async function runDynamicPaidFlow(ctx) {
2705
2786
  amount: price,
2706
2787
  network: verifyOutcome.payment.network
2707
2788
  });
2708
- const result = await invokeDynamic(
2709
- ctx,
2710
- verifyOutcome.wallet,
2711
- account,
2712
- parsedBody,
2713
- verifyOutcome.payment
2714
- );
2789
+ const result = await invokeDynamic(ctx, verifyOutcome, account, parsedBody);
2715
2790
  switch (result.kind) {
2716
2791
  case "stream":
2717
2792
  return runDynamicStreamFlow({
@@ -2733,6 +2808,18 @@ async function runDynamicPaidFlow(ctx) {
2733
2808
  });
2734
2809
  }
2735
2810
  }
2811
+ async function invokeDynamic(ctx, verifyOutcome, account, parsedBody) {
2812
+ switch (ctx.routeEntry.billing) {
2813
+ case "upto":
2814
+ return invokeUpto(ctx, verifyOutcome.wallet, account, parsedBody, verifyOutcome.payment);
2815
+ case "metered":
2816
+ return invokeMetered(ctx, verifyOutcome.wallet, account, parsedBody, verifyOutcome.payment);
2817
+ case "exact":
2818
+ throw new Error(
2819
+ `route '${ctx.routeEntry.key}': exact billing must not reach the dynamic paid flow`
2820
+ );
2821
+ }
2822
+ }
2736
2823
 
2737
2824
  // src/pipeline/flows/static/static-body-and-price.ts
2738
2825
  async function resolveStaticBodyAndPrice(args) {
@@ -2880,13 +2967,8 @@ async function runStaticPaidFlow(ctx) {
2880
2967
 
2881
2968
  // src/pipeline/flows/paid.ts
2882
2969
  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
- }
2970
+ const handlerCharged = ctx.routeEntry.billing !== "exact";
2971
+ return handlerCharged ? runDynamicPaidFlow(ctx) : runStaticPaidFlow(ctx);
2890
2972
  }
2891
2973
 
2892
2974
  // src/pipeline/flows/siwx-only.ts
@@ -3284,7 +3366,7 @@ var RouteBuilder = class _RouteBuilder {
3284
3366
  protocols: defaults?.protocols ? [...defaults.protocols] : ["x402"],
3285
3367
  maxPrice: void 0,
3286
3368
  minPrice: void 0,
3287
- dynamicPrice: false,
3369
+ billing: "exact",
3288
3370
  tickCost: void 0,
3289
3371
  unitType: void 0,
3290
3372
  payTo: void 0,
@@ -3315,39 +3397,84 @@ var RouteBuilder = class _RouteBuilder {
3315
3397
  next.#s = { ...this.#s, protocols: [...this.#s.protocols] };
3316
3398
  return next;
3317
3399
  }
3318
- paid(pricingOrOptions, options) {
3319
- const { pricing, resolvedOptions } = resolvePaidArgs(this.#s.key, pricingOrOptions, options);
3400
+ paid(arg, options) {
3401
+ return this.applyPaid(normalizePaidArg(this.#s.key, arg, options), "paid");
3402
+ }
3403
+ /**
3404
+ * x402-only handler-computed billing. The handler receives `charge(amount)`
3405
+ * and the request settles once for the accumulated total, capped at
3406
+ * `maxPrice`. Requires an `'upto'` accept on at least one configured network.
3407
+ * Pass a bare string as sugar for `{ maxPrice }`.
3408
+ *
3409
+ * @example
3410
+ * ```ts
3411
+ * router.route('llm')
3412
+ * .upTo('0.05')
3413
+ * .body(schema)
3414
+ * .handler(async ({ body, charge }) => { await charge('0.001'); ... });
3415
+ * ```
3416
+ */
3417
+ upTo(arg) {
3418
+ return this.applyPaid(normalizeUpToArg(this.#s.key, arg), "upTo");
3419
+ }
3420
+ /**
3421
+ * MPP-only per-tick billing. `.handler()` bills exactly `tickCost`;
3422
+ * `.stream()` calls `charge()` (no-arg) per yield, settling per tick up to
3423
+ * `maxPrice`. Requires `RouterConfig.mpp.session`.
3424
+ *
3425
+ * @example
3426
+ * ```ts
3427
+ * router.route('llm/stream')
3428
+ * .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
3429
+ * .stream(async function* ({ charge }) { await charge(); yield 'hi'; });
3430
+ * ```
3431
+ */
3432
+ metered(options) {
3433
+ return this.applyPaid(normalizeMeteredArg(this.#s.key, options), "metered");
3434
+ }
3435
+ applyPaid(normalized, method) {
3436
+ const { pricing, resolvedOptions, billing, tickCost, unitType, maxPrice } = normalized;
3320
3437
  if (this.#s.authMode === "unprotected") {
3321
3438
  throw new Error(
3322
- `route '${this.#s.key}': Cannot combine .unprotected() and .paid() on the same route.`
3439
+ `route '${this.#s.key}': Cannot combine .unprotected() and .${method}() on the same route.`
3323
3440
  );
3324
3441
  }
3325
3442
  if (this.#s.pricing !== void 0) {
3326
3443
  throw new Error(
3327
- `route '${this.#s.key}': Cannot call .paid() more than once on the same route.`
3444
+ `route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
3328
3445
  );
3329
3446
  }
3330
3447
  const next = this.fork();
3331
3448
  next.#s.authMode = "paid";
3332
3449
  next.#s.pricing = pricing;
3333
- if (resolvedOptions?.protocols) {
3450
+ if (billing === "upto") {
3451
+ if (resolvedOptions.protocols?.some((p) => p !== "x402")) {
3452
+ throw new Error(
3453
+ `route '${this.#s.key}': .upTo() is x402-only \u2014 remove the conflicting protocols override.`
3454
+ );
3455
+ }
3456
+ next.#s.protocols = ["x402"];
3457
+ } else if (billing === "metered") {
3458
+ if (resolvedOptions.protocols?.some((p) => p !== "mpp")) {
3459
+ throw new Error(
3460
+ `route '${this.#s.key}': .metered() is MPP-only \u2014 remove the conflicting protocols override.`
3461
+ );
3462
+ }
3463
+ next.#s.protocols = ["mpp"];
3464
+ } else if (resolvedOptions.protocols) {
3334
3465
  next.#s.protocols = [...resolvedOptions.protocols];
3335
3466
  } else if (next.#s.protocols.length === 0) {
3336
3467
  next.#s.protocols = ["x402"];
3337
3468
  }
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;
3469
+ if (resolvedOptions.maxPrice) next.#s.maxPrice = resolvedOptions.maxPrice;
3470
+ if (maxPrice) next.#s.maxPrice = maxPrice;
3471
+ if (resolvedOptions.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
3472
+ if (resolvedOptions.payTo) next.#s.payTo = resolvedOptions.payTo;
3473
+ if (resolvedOptions.mpp) next.#s.mppInfo = resolvedOptions.mpp;
3474
+ next.#s.billing = billing;
3475
+ if (tickCost) next.#s.tickCost = tickCost;
3476
+ if (unitType) next.#s.unitType = unitType;
3345
3477
  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
3478
  for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
3352
3479
  if (!tierKey) {
3353
3480
  throw new Error(`route '${this.#s.key}': tier key cannot be empty`);
@@ -3359,22 +3486,16 @@ var RouteBuilder = class _RouteBuilder {
3359
3486
  }
3360
3487
  }
3361
3488
  }
3362
- if (resolvedOptions?.maxPrice !== void 0 && !isPositiveDecimal(resolvedOptions.maxPrice)) {
3489
+ if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
3363
3490
  throw new Error(
3364
- `route '${this.#s.key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
3491
+ `route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
3365
3492
  );
3366
3493
  }
3367
- if (resolvedOptions?.tickCost !== void 0 && !isPositiveDecimal(resolvedOptions.tickCost)) {
3494
+ if (next.#s.tickCost !== void 0 && !isPositiveDecimal(next.#s.tickCost)) {
3368
3495
  throw new Error(
3369
- `route '${this.#s.key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
3496
+ `route '${this.#s.key}': tickCost '${next.#s.tickCost}' must be a positive decimal string`
3370
3497
  );
3371
3498
  }
3372
- if (next.#s.dynamicPrice && !next.#s.maxPrice) {
3373
- throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires maxPrice`);
3374
- }
3375
- if (next.#s.dynamicPrice && !next.#s.tickCost) {
3376
- throw new Error(`route '${this.#s.key}': .paid({ dynamic: true }) requires tickCost`);
3377
- }
3378
3499
  return next;
3379
3500
  }
3380
3501
  /**
@@ -3656,13 +3777,13 @@ var RouteBuilder = class _RouteBuilder {
3656
3777
  /**
3657
3778
  * Register a streaming handler (`async function*`) and return the Next.js
3658
3779
  * route function. Each `charge()` call bills one tick (`tickCost` USDC) up
3659
- * to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
3780
+ * to `maxPrice`; requires `.metered({ ... })` and MPP session mode.
3660
3781
  *
3661
3782
  * @example
3662
3783
  * ```ts
3663
3784
  * export const POST = router
3664
3785
  * .route('llm/stream')
3665
- * .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
3786
+ * .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
3666
3787
  * .body(schema)
3667
3788
  * .stream(async function* ({ body, charge }) {
3668
3789
  * for await (const token of streamLLM(body.prompt)) {
@@ -3678,7 +3799,7 @@ var RouteBuilder = class _RouteBuilder {
3678
3799
  register(handlerFn, streaming) {
3679
3800
  if (!this.#s.authMode) {
3680
3801
  throw new Error(
3681
- `route '${this.#s.key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
3802
+ `route '${this.#s.key}': Select an auth mode: .paid(pricing), .upTo(maxPrice), .metered(options), .siwx(), .apiKey(resolver), or .unprotected()`
3682
3803
  );
3683
3804
  }
3684
3805
  if (this.#s.validateFn && !this.#s.bodySchema) {
@@ -3689,24 +3810,34 @@ var RouteBuilder = class _RouteBuilder {
3689
3810
  if (this.#s.settlement && !this.#s.pricing) {
3690
3811
  throw new Error(`route '${this.#s.key}': .settlement() requires a paid route`);
3691
3812
  }
3692
- if (this.#s.dynamicPrice && this.#s.protocols.includes("x402")) {
3813
+ if (this.#s.billing === "upto") {
3693
3814
  const hasUpto = this.#s.deps.x402Accepts.some((accept) => accept.scheme === "upto");
3694
3815
  if (!hasUpto) {
3695
3816
  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.`
3817
+ `route '${this.#s.key}': .upTo() requires an 'upto' accept on at least one configured network. Add { scheme: 'upto', network, asset } to RouterConfig.x402.accepts.`
3818
+ );
3819
+ }
3820
+ }
3821
+ if (this.#s.pricing !== void 0 && this.#s.billing === "exact" && this.#s.protocols.includes("x402")) {
3822
+ const hasExact = this.#s.deps.x402Accepts.some(
3823
+ (accept) => (accept.scheme ?? "exact") !== "upto"
3824
+ );
3825
+ if (!hasExact) {
3826
+ throw new Error(
3827
+ `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.`
3697
3828
  );
3698
3829
  }
3699
3830
  }
3700
- if (this.#s.dynamicPrice && this.#s.protocols.includes("mpp")) {
3831
+ if (this.#s.billing === "metered") {
3701
3832
  if (!this.#s.deps.mppSessionConfig) {
3702
3833
  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.`
3834
+ `route '${this.#s.key}': .metered() requires MPP session mode. Set RouterConfig.mpp.session = {} and provide mpp.operatorKey.`
3704
3835
  );
3705
3836
  }
3706
3837
  }
3707
- if (streaming && !this.#s.dynamicPrice) {
3838
+ if (streaming && this.#s.billing !== "metered") {
3708
3839
  throw new Error(
3709
- `route '${this.#s.key}': .stream() requires .paid({ dynamic: true }) \u2014 static/free routes can't meter per-chunk billing.`
3840
+ `route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
3710
3841
  );
3711
3842
  }
3712
3843
  validateExamples(
@@ -3724,7 +3855,7 @@ var RouteBuilder = class _RouteBuilder {
3724
3855
  authMode: this.#s.authMode,
3725
3856
  siwxEnabled: this.#s.siwxEnabled,
3726
3857
  pricing: this.#s.pricing,
3727
- dynamicPrice: this.#s.dynamicPrice ? true : void 0,
3858
+ billing: this.#s.billing,
3728
3859
  streaming: streaming ? true : void 0,
3729
3860
  protocols: this.#s.protocols,
3730
3861
  bodySchema: this.#s.bodySchema,
@@ -3751,16 +3882,59 @@ var RouteBuilder = class _RouteBuilder {
3751
3882
  return createRequestHandler(entry, handlerFn, this.#s.deps);
3752
3883
  }
3753
3884
  };
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 };
3885
+ function normalizePaidArg(routeKey, arg, options) {
3886
+ if (typeof arg === "string") {
3887
+ return { pricing: arg, resolvedOptions: options ?? {}, billing: "exact" };
3888
+ }
3889
+ if (typeof arg === "function") {
3890
+ return {
3891
+ pricing: arg,
3892
+ resolvedOptions: options ?? {},
3893
+ billing: "exact"
3894
+ };
3762
3895
  }
3763
- return { pricing: pricingOrOptions, resolvedOptions: options };
3896
+ if ("tiers" in arg && "field" in arg) {
3897
+ return {
3898
+ pricing: { field: arg.field, tiers: arg.tiers, default: arg.default },
3899
+ resolvedOptions: arg,
3900
+ billing: "exact"
3901
+ };
3902
+ }
3903
+ if ("price" in arg && typeof arg.price === "string") {
3904
+ return { pricing: arg.price, resolvedOptions: arg, billing: "exact" };
3905
+ }
3906
+ throw new Error(
3907
+ `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().`
3908
+ );
3909
+ }
3910
+ function normalizeUpToArg(routeKey, arg) {
3911
+ const options = typeof arg === "string" ? { maxPrice: arg } : arg;
3912
+ if (!options.maxPrice) {
3913
+ throw new Error(`route '${routeKey}': .upTo() requires maxPrice`);
3914
+ }
3915
+ return {
3916
+ pricing: options.maxPrice,
3917
+ resolvedOptions: options,
3918
+ billing: "upto",
3919
+ unitType: options.unitType,
3920
+ maxPrice: options.maxPrice
3921
+ };
3922
+ }
3923
+ function normalizeMeteredArg(routeKey, options) {
3924
+ if (!options.maxPrice) {
3925
+ throw new Error(`route '${routeKey}': .metered() requires maxPrice`);
3926
+ }
3927
+ if (!options.tickCost) {
3928
+ throw new Error(`route '${routeKey}': .metered() requires tickCost`);
3929
+ }
3930
+ return {
3931
+ pricing: options.maxPrice,
3932
+ resolvedOptions: options,
3933
+ billing: "metered",
3934
+ tickCost: options.tickCost,
3935
+ unitType: options.unitType,
3936
+ maxPrice: options.maxPrice
3937
+ };
3764
3938
  }
3765
3939
 
3766
3940
  // src/discovery/well-known.ts
@@ -4699,7 +4873,7 @@ function createRouter(config) {
4699
4873
  x402Accepts,
4700
4874
  mppx: null,
4701
4875
  tempoClient: null,
4702
- mppSessionConfig: config.mpp?.session ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
4876
+ mppSessionConfig: config.mpp?.session && config.mpp.operatorKey ? { depositMultiplier: config.mpp.session.depositMultiplier ?? 10 } : null
4703
4877
  };
4704
4878
  deps.initPromise = (async () => {
4705
4879
  const x402Result = await initX402(config, kvStore, x402ConfigError);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "Unified route builder for Next.js App Router APIs with x402, MPP, SIWX, and API key auth",
5
5
  "type": "module",
6
6
  "exports": {
@@ -19,7 +19,10 @@
19
19
  "module": "./dist/index.js",
20
20
  "types": "./dist/index.d.ts",
21
21
  "files": [
22
- "dist",
22
+ "dist/index.js",
23
+ "dist/index.cjs",
24
+ "dist/index.d.ts",
25
+ "dist/index.d.cts",
23
26
  "AGENTS.md",
24
27
  "README.md"
25
28
  ],