@agentcash/router 1.7.0 → 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) => ({
@@ -242,12 +245,148 @@ var init_facilitators = __esm({
242
245
  }
243
246
  });
244
247
 
248
+ // src/kv-store/facilitator-supported.ts
249
+ function withCachedSupported(inner, options = {}) {
250
+ const { kv, cacheKey, ttlSeconds = FACILITATOR_SUPPORTED_TTL_SECONDS, fallback } = options;
251
+ const kvKey = kv && cacheKey ? `${FACILITATOR_SUPPORTED_KV_PREFIX}${cacheKey}` : void 0;
252
+ let inflight;
253
+ return {
254
+ verify: inner.verify.bind(inner),
255
+ settle: inner.settle.bind(inner),
256
+ getSupported: () => {
257
+ if (inflight) return inflight;
258
+ const attempt = fetchSupported(inner, kv, kvKey, ttlSeconds, fallback);
259
+ inflight = attempt;
260
+ attempt.catch(() => {
261
+ if (inflight === attempt) inflight = void 0;
262
+ });
263
+ return attempt;
264
+ }
265
+ };
266
+ }
267
+ async function fetchSupported(inner, kv, kvKey, ttlSeconds, fallback) {
268
+ if (kv && kvKey) {
269
+ const cached = await readKvCache(kv, kvKey);
270
+ if (cached) return cached;
271
+ }
272
+ const fresh = await tryFetchLive(inner, fallback);
273
+ if (fresh === null) return fallback();
274
+ if (kv && kvKey) await writeKvCache(kv, kvKey, fresh, ttlSeconds);
275
+ return fresh;
276
+ }
277
+ async function tryFetchLive(inner, fallback) {
278
+ try {
279
+ return await inner.getSupported();
280
+ } catch (err) {
281
+ if (!fallback) throw err;
282
+ console.warn(
283
+ `[x402] facilitator /supported failed, using hardcoded baseline: ${err instanceof Error ? err.message : String(err)}`
284
+ );
285
+ return null;
286
+ }
287
+ }
288
+ async function readKvCache(kv, key) {
289
+ try {
290
+ const cached = await kv.get(key);
291
+ return isSupportedResponse(cached) ? cached : void 0;
292
+ } catch {
293
+ return void 0;
294
+ }
295
+ }
296
+ async function writeKvCache(kv, key, value, ttlSeconds) {
297
+ try {
298
+ await kv.setNxEx(key, value, ttlSeconds);
299
+ } catch {
300
+ }
301
+ }
302
+ function isSupportedResponse(value) {
303
+ return typeof value === "object" && value !== null && Array.isArray(value.kinds);
304
+ }
305
+ var FACILITATOR_SUPPORTED_TTL_SECONDS, FACILITATOR_SUPPORTED_KV_PREFIX;
306
+ var init_facilitator_supported = __esm({
307
+ "src/kv-store/facilitator-supported.ts"() {
308
+ "use strict";
309
+ FACILITATOR_SUPPORTED_TTL_SECONDS = 60 * 60;
310
+ FACILITATOR_SUPPORTED_KV_PREFIX = "x402:facilitator-supported:";
311
+ }
312
+ });
313
+
314
+ // src/protocols/x402/facilitator-clients.ts
315
+ function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient, kvStore) {
316
+ return getResolvedX402FacilitatorGroups(facilitatorsByNetwork).map((group) => {
317
+ const inner = new HTTPFacilitatorClient(group.config);
318
+ const kinds = buildSupportedKinds(group);
319
+ const baseline = () => ({
320
+ kinds,
321
+ extensions: [],
322
+ signers: {}
323
+ });
324
+ if (group.family === "solana") {
325
+ return hardcodedSupportedClient(inner, baseline);
326
+ }
327
+ const cached = withCachedSupported(inner, {
328
+ kv: kvStore,
329
+ cacheKey: group.config.url,
330
+ fallback: baseline
331
+ });
332
+ return withScopedKinds(cached, kinds);
333
+ });
334
+ }
335
+ function hardcodedSupportedClient(inner, build) {
336
+ return {
337
+ verify: inner.verify.bind(inner),
338
+ settle: inner.settle.bind(inner),
339
+ getSupported: async () => build()
340
+ };
341
+ }
342
+ function withScopedKinds(client, kinds) {
343
+ return {
344
+ verify: client.verify.bind(client),
345
+ settle: client.settle.bind(client),
346
+ getSupported: async () => {
347
+ const live = await client.getSupported();
348
+ return { ...live, kinds: mergeKindExtras(kinds, live.kinds) };
349
+ }
350
+ };
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
+ }
358
+ function buildSupportedKinds(group) {
359
+ return group.networks.flatMap((network) => {
360
+ if (group.family === "solana") {
361
+ return [
362
+ {
363
+ x402Version: 2,
364
+ scheme: "exact",
365
+ network,
366
+ extra: { features: { xSettlementAccountSupported: true } }
367
+ }
368
+ ];
369
+ }
370
+ return [
371
+ { x402Version: 2, scheme: "exact", network },
372
+ { x402Version: 2, scheme: "upto", network }
373
+ ];
374
+ });
375
+ }
376
+ var init_facilitator_clients = __esm({
377
+ "src/protocols/x402/facilitator-clients.ts"() {
378
+ "use strict";
379
+ init_facilitator_supported();
380
+ init_facilitators();
381
+ }
382
+ });
383
+
245
384
  // src/init/x402-server.ts
246
385
  var x402_server_exports = {};
247
386
  __export(x402_server_exports, {
248
387
  createX402Server: () => createX402Server
249
388
  });
250
- async function createX402Server(config) {
389
+ async function createX402Server(config, kvStore) {
251
390
  const { x402ResourceServer, HTTPFacilitatorClient } = await import("@x402/core/server");
252
391
  const { registerExactEvmScheme } = await import("@x402/evm/exact/server");
253
392
  const { bazaarResourceServerExtension } = await import("@x402/extensions/bazaar");
@@ -261,7 +400,11 @@ async function createX402Server(config) {
261
400
  );
262
401
  const evmNetworks = filterEvmNetworks(configuredNetworks);
263
402
  const svmNetworks = filterSolanaNetworks(configuredNetworks);
264
- const facilitatorClients = createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient);
403
+ const facilitatorClients = createFacilitatorClients(
404
+ facilitatorsByNetwork,
405
+ HTTPFacilitatorClient,
406
+ kvStore
407
+ );
265
408
  const server = new x402ResourceServer(
266
409
  facilitatorClients.length === 1 ? facilitatorClients[0] : facilitatorClients
267
410
  );
@@ -285,48 +428,13 @@ async function createX402Server(config) {
285
428
  facilitatorsByNetwork
286
429
  };
287
430
  }
288
- function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
289
- const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
290
- return groups.map((group) => {
291
- const inner = new HTTPFacilitatorClient(group.config);
292
- const kinds = buildSupportedKinds(group);
293
- return hardcodedSupportedClient(inner, kinds);
294
- });
295
- }
296
- function hardcodedSupportedClient(inner, kinds) {
297
- return {
298
- verify: inner.verify.bind(inner),
299
- settle: inner.settle.bind(inner),
300
- getSupported: async () => ({ kinds, extensions: [], signers: {} })
301
- };
302
- }
303
- function buildSupportedKinds(group) {
304
- return group.networks.flatMap((network) => {
305
- const exactKind = {
306
- x402Version: 2,
307
- scheme: "exact",
308
- network,
309
- ...group.family === "solana" ? {
310
- extra: {
311
- features: {
312
- xSettlementAccountSupported: true
313
- }
314
- }
315
- } : {}
316
- };
317
- const uptoKind = { x402Version: 2, scheme: "upto", network };
318
- if (group.family === "evm") {
319
- return [exactKind, uptoKind];
320
- }
321
- return [exactKind, uptoKind];
322
- });
323
- }
324
431
  var init_x402_server = __esm({
325
432
  "src/init/x402-server.ts"() {
326
433
  "use strict";
327
434
  init_evm();
328
435
  init_solana();
329
436
  init_facilitators();
437
+ init_facilitator_clients();
330
438
  init_accepts();
331
439
  }
332
440
  });
@@ -658,10 +766,7 @@ async function runHandler(ctx, handlerCtx) {
658
766
  }
659
767
  if (isAsyncIterable(returned) && !isThenable(returned)) {
660
768
  return errorResult(
661
- new HttpError(
662
- `route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
663
- 500
664
- )
769
+ new HttpError(`route '${ctx.routeEntry.key}': streaming handlers require .metered()`, 500)
665
770
  );
666
771
  }
667
772
  let rawResult;
@@ -1028,6 +1133,61 @@ async function runApiKeyOnlyFlow(ctx) {
1028
1133
  return runHandlerOnly(ctx, null, result.account);
1029
1134
  }
1030
1135
 
1136
+ // src/pricing/format.ts
1137
+ var USDC_DECIMALS = 6;
1138
+ var DECIMAL_RE = /^(\d+)(?:\.(\d+))?$/;
1139
+ function badDecimal(amount) {
1140
+ return Object.assign(new Error(`'${amount}' is not a valid decimal-dollar string`), {
1141
+ status: 400
1142
+ });
1143
+ }
1144
+ function decimalToAtomic(amount, decimals = USDC_DECIMALS) {
1145
+ const match = DECIMAL_RE.exec(amount.trim());
1146
+ if (!match) throw badDecimal(amount);
1147
+ const whole = match[1];
1148
+ const fraction = match[2] ?? "";
1149
+ if (fraction.length > decimals) {
1150
+ throw Object.assign(new Error(`Amount '${amount}' exceeds ${decimals} decimal places`), {
1151
+ status: 400
1152
+ });
1153
+ }
1154
+ const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
1155
+ return BigInt(normalized || "0");
1156
+ }
1157
+ function atomicToDecimal(atomic, decimals = USDC_DECIMALS) {
1158
+ const divisor = 10n ** BigInt(decimals);
1159
+ const whole = atomic / divisor;
1160
+ const fraction = atomic % divisor;
1161
+ if (fraction === 0n) return whole.toString();
1162
+ const fractionStr = fraction.toString().padStart(decimals, "0").replace(/0+$/, "");
1163
+ return `${whole}.${fractionStr}`;
1164
+ }
1165
+ function compareDecimals(a, b) {
1166
+ const av = decimalToAtomic(a);
1167
+ const bv = decimalToAtomic(b);
1168
+ if (av < bv) return -1;
1169
+ if (av > bv) return 1;
1170
+ return 0;
1171
+ }
1172
+ function isPositiveDecimal(value) {
1173
+ try {
1174
+ return decimalToAtomic(value) > 0n;
1175
+ } catch {
1176
+ return false;
1177
+ }
1178
+ }
1179
+ function multiplyDecimal(decimal, factor) {
1180
+ if (!Number.isFinite(factor) || factor <= 0) return decimal;
1181
+ const [whole, fraction = ""] = decimal.split(".");
1182
+ const scaled = (BigInt(whole + fraction) * BigInt(factor)).toString();
1183
+ const decimals = fraction.length;
1184
+ if (decimals === 0) return scaled;
1185
+ const padded = scaled.padStart(decimals + 1, "0");
1186
+ const intPart = padded.slice(0, padded.length - decimals);
1187
+ const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
1188
+ return fracPart ? `${intPart}.${fracPart}` : intPart;
1189
+ }
1190
+
1031
1191
  // src/pricing/dynamic.ts
1032
1192
  var DynamicPricing = class {
1033
1193
  constructor(opts) {
@@ -1039,6 +1199,7 @@ var DynamicPricing = class {
1039
1199
  const raw = await this.opts.fn(body);
1040
1200
  return this.cap(raw, body);
1041
1201
  } catch (err) {
1202
+ if (err instanceof HttpError) throw err;
1042
1203
  this.alert("error", `Pricing function failed: ${msg(err)}`, {
1043
1204
  error: err instanceof Error ? err.stack : String(err),
1044
1205
  body
@@ -1063,9 +1224,13 @@ var DynamicPricing = class {
1063
1224
  }
1064
1225
  cap(raw, body) {
1065
1226
  if (!this.opts.maxPrice) return raw;
1066
- const n = parseFloat(raw);
1067
- const max = parseFloat(this.opts.maxPrice);
1068
- if (!Number.isFinite(n) || n > max) {
1227
+ let overCap;
1228
+ try {
1229
+ overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
1230
+ } catch {
1231
+ overCap = true;
1232
+ }
1233
+ if (overCap) {
1069
1234
  this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
1070
1235
  calculated: raw,
1071
1236
  maxPrice: this.opts.maxPrice,
@@ -1142,7 +1307,7 @@ var TieredPricing = class {
1142
1307
  maxTierPrice() {
1143
1308
  let max = "0";
1144
1309
  for (const tier of Object.values(this.opts.tiers)) {
1145
- if (parseFloat(tier.price) > parseFloat(max)) max = tier.price;
1310
+ if (compareDecimals(tier.price, max) > 0) max = tier.price;
1146
1311
  }
1147
1312
  return max;
1148
1313
  }
@@ -1557,7 +1722,7 @@ var mppStrategy = {
1557
1722
  async verify(args) {
1558
1723
  const info = readMppCredential(args.request);
1559
1724
  if (!info) return { ok: false, kind: "invalid" };
1560
- if (args.routeEntry.dynamicPrice) {
1725
+ if (args.routeEntry.billing === "metered") {
1561
1726
  if (!info.sessionAction) return { ok: false, kind: "invalid" };
1562
1727
  return verifySessionMode(args, info);
1563
1728
  }
@@ -1608,7 +1773,7 @@ var mppStrategy = {
1608
1773
  async buildChallenge(args) {
1609
1774
  if (!args.deps.mppx) return {};
1610
1775
  const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
1611
- if (args.routeEntry.dynamicPrice && sessionsConfigured) {
1776
+ if (args.routeEntry.billing === "metered" && sessionsConfigured) {
1612
1777
  const tickCost = args.routeEntry.tickCost;
1613
1778
  const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
1614
1779
  const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
@@ -1620,17 +1785,6 @@ var mppStrategy = {
1620
1785
  return buildChargeChallenge(args);
1621
1786
  }
1622
1787
  };
1623
- function multiplyDecimal(decimal, factor) {
1624
- if (!Number.isFinite(factor) || factor <= 0) return decimal;
1625
- const [whole, fraction = ""] = decimal.split(".");
1626
- const scaled = (BigInt(whole + fraction) * BigInt(factor)).toString();
1627
- const decimals = fraction.length;
1628
- if (decimals === 0) return scaled;
1629
- const padded = scaled.padStart(decimals + 1, "0");
1630
- const intPart = padded.slice(0, padded.length - decimals);
1631
- const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
1632
- return fracPart ? `${intPart}.${fracPart}` : intPart;
1633
- }
1634
1788
  async function buildChargeChallenge(args) {
1635
1789
  if (!args.deps.mppx) return {};
1636
1790
  try {
@@ -1723,26 +1877,13 @@ function buildCustomRequirement(price, accept) {
1723
1877
  return {
1724
1878
  scheme: accept.scheme,
1725
1879
  network: accept.network,
1726
- amount: decimalToAtomicUnits(price, accept.decimals ?? 6),
1880
+ amount: decimalToAtomic(price, accept.decimals ?? 6).toString(),
1727
1881
  asset: accept.asset,
1728
1882
  payTo: accept.payTo,
1729
1883
  maxTimeoutSeconds: accept.maxTimeoutSeconds ?? 300,
1730
1884
  extra: accept.extra ?? {}
1731
1885
  };
1732
1886
  }
1733
- function decimalToAtomicUnits(amount, decimals) {
1734
- const match = /^(?<whole>\d+)(?:\.(?<fraction>\d+))?$/.exec(amount);
1735
- if (!match?.groups) {
1736
- throw new Error(`Invalid decimal amount '${amount}'`);
1737
- }
1738
- const whole = match.groups.whole;
1739
- const fraction = match.groups.fraction ?? "";
1740
- if (fraction.length > decimals) {
1741
- throw new Error(`Amount '${amount}' exceeds ${decimals} decimal places`);
1742
- }
1743
- const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
1744
- return normalized === "" ? "0" : normalized;
1745
- }
1746
1887
 
1747
1888
  // src/protocols/x402/challenge.ts
1748
1889
  async function buildX402Challenge(opts) {
@@ -2009,7 +2150,7 @@ async function verifyX402(args) {
2009
2150
  const accepts = await resolveX402Accepts(
2010
2151
  request,
2011
2152
  routeEntry,
2012
- deps.x402Accepts,
2153
+ selectRouteAccepts(deps.x402Accepts, routeEntry),
2013
2154
  deps.payeeAddress,
2014
2155
  body
2015
2156
  );
@@ -2057,7 +2198,7 @@ async function verifyX402(args) {
2057
2198
  async function settleX402(args) {
2058
2199
  const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
2059
2200
  const { payload, requirements } = token;
2060
- const override = routeEntry.dynamicPrice ? { amount: billedAmount } : void 0;
2201
+ const override = routeEntry.billing === "exact" ? void 0 : { amount: billedAmount };
2061
2202
  try {
2062
2203
  const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2063
2204
  if (!settle.result?.success) {
@@ -2087,7 +2228,7 @@ async function buildX402ChallengeContribution(args) {
2087
2228
  const accepts = await resolveX402Accepts(
2088
2229
  request,
2089
2230
  routeEntry,
2090
- deps.x402Accepts,
2231
+ selectRouteAccepts(deps.x402Accepts, routeEntry),
2091
2232
  deps.payeeAddress,
2092
2233
  body
2093
2234
  );
@@ -2105,13 +2246,14 @@ async function buildX402ChallengeContribution(args) {
2105
2246
  }
2106
2247
  function reportSettleFailure(report, err, network) {
2107
2248
  const facilitator = err ?? {};
2108
- report("error", "Settlement failed", {
2249
+ const meta = {
2109
2250
  error: err instanceof Error ? err.message : String(err),
2110
2251
  network,
2111
2252
  errorReason: facilitator.errorReason,
2112
2253
  facilitatorStatus: facilitator.response?.status,
2113
2254
  facilitatorBody: facilitator.response?.data ?? facilitator.response?.body
2114
- });
2255
+ };
2256
+ report("error", "Settlement failed", meta);
2115
2257
  }
2116
2258
 
2117
2259
  // src/protocols/index.ts
@@ -2134,6 +2276,7 @@ function getAllowedStrategies(allowed) {
2134
2276
  import { NextResponse as NextResponse4 } from "next/server";
2135
2277
 
2136
2278
  // src/pipeline/challenge-extensions.ts
2279
+ init_evm();
2137
2280
  async function buildChallengeExtensions(ctx) {
2138
2281
  const { routeEntry } = ctx;
2139
2282
  let extensions;
@@ -2178,6 +2321,21 @@ async function buildChallengeExtensions(ctx) {
2178
2321
  } catch {
2179
2322
  }
2180
2323
  }
2324
+ const hasEvmUpto = ctx.routeEntry.billing === "upto" && ctx.deps.x402Accepts.some((accept) => accept.scheme === "upto" && isEvmNetwork(accept.network));
2325
+ if (hasEvmUpto) {
2326
+ try {
2327
+ const { declareEip2612GasSponsoringExtension } = await import("@x402/extensions");
2328
+ extensions = {
2329
+ ...extensions ?? {},
2330
+ ...declareEip2612GasSponsoringExtension()
2331
+ };
2332
+ } catch (err) {
2333
+ ctx.report(
2334
+ "warn",
2335
+ `EIP-2612 gas-sponsoring declaration failed: ${err instanceof Error ? err.message : String(err)}`
2336
+ );
2337
+ }
2338
+ }
2181
2339
  return extensions;
2182
2340
  }
2183
2341
 
@@ -2330,31 +2488,7 @@ async function runDynamicChannelMgmtFlow(args) {
2330
2488
  });
2331
2489
  }
2332
2490
 
2333
- // src/pipeline/flows/dynamic/dynamic-invoke.ts
2334
- import { NextResponse as NextResponse6 } from "next/server";
2335
-
2336
- // src/pricing/atomic.ts
2337
- var USDC_DECIMALS = 6;
2338
- function decimalToAtomic(amount) {
2339
- const m = /^(\d+)(?:\.(\d+))?$/.exec(amount.trim());
2340
- if (!m) {
2341
- throw Object.assign(new Error(`'${amount}' is not a valid decimal-dollar string`), {
2342
- status: 400
2343
- });
2344
- }
2345
- const whole = m[1];
2346
- const fraction = (m[2] ?? "").slice(0, USDC_DECIMALS).padEnd(USDC_DECIMALS, "0");
2347
- return BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0");
2348
- }
2349
- function atomicToDecimal(atomic) {
2350
- const whole = atomic / 10n ** BigInt(USDC_DECIMALS);
2351
- const fraction = atomic % 10n ** BigInt(USDC_DECIMALS);
2352
- if (fraction === 0n) return whole.toString();
2353
- const fractionStr = fraction.toString().padStart(USDC_DECIMALS, "0").replace(/0+$/, "");
2354
- return `${whole}.${fractionStr}`;
2355
- }
2356
-
2357
- // src/pricing/charge-context.ts
2491
+ // src/pricing/metered-charge.ts
2358
2492
  function createChargeContext(args) {
2359
2493
  const { tickCost, maxPrice, route } = args;
2360
2494
  const tickAtomic = decimalToAtomic(tickCost);
@@ -2389,15 +2523,10 @@ function createChargeContext(args) {
2389
2523
  };
2390
2524
  }
2391
2525
 
2392
- // src/pipeline/flows/dynamic/dynamic-invoke.ts
2393
- async function invokeDynamic(ctx, wallet, account, body, payment) {
2394
- const streaming = ctx.routeEntry.streaming === true;
2395
- const chargeContext = streaming ? createChargeContext({
2396
- tickCost: ctx.routeEntry.tickCost,
2397
- maxPrice: ctx.routeEntry.maxPrice,
2398
- route: ctx.routeEntry.key
2399
- }) : null;
2400
- 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 {
2401
2530
  body,
2402
2531
  query: parseQuery(ctx.request, ctx.routeEntry),
2403
2532
  request: ctx.request,
@@ -2409,12 +2538,41 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
2409
2538
  alert: ctx.report,
2410
2539
  setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
2411
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);
2412
2570
  const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
2413
2571
  let returned;
2414
2572
  try {
2415
2573
  returned = ctx.handler(handlerCtx);
2416
2574
  } catch (error) {
2417
- return errorResult2(error, chargeContext);
2575
+ return errorResult2(error);
2418
2576
  }
2419
2577
  if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2420
2578
  if (!chargeContext) {
@@ -2422,41 +2580,80 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
2422
2580
  new HttpError(
2423
2581
  "route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
2424
2582
  500
2425
- ),
2426
- null
2583
+ )
2427
2584
  );
2428
2585
  }
2429
- return {
2430
- kind: "stream",
2431
- source: returned,
2432
- chargeContext
2433
- };
2586
+ return { kind: "stream", source: returned, chargeContext };
2434
2587
  }
2435
2588
  let rawResult;
2436
2589
  try {
2437
2590
  rawResult = await returned;
2438
2591
  } catch (error) {
2439
- return errorResult2(error, chargeContext);
2592
+ return errorResult2(error);
2440
2593
  }
2441
- const response = rawResult instanceof Response ? rawResult : NextResponse6.json(rawResult);
2442
- return { kind: "request", response, rawResult };
2594
+ return { kind: "request", response: toResponse(rawResult), rawResult };
2443
2595
  }
2444
- function errorResult2(error, chargeContext) {
2445
- const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
2446
- const message = error instanceof Error ? error.message : "Internal error";
2447
- 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
+ };
2448
2619
  return {
2449
- kind: "request",
2450
- response: NextResponse6.json({ success: false, error: message }, { status }),
2451
- rawResult: void 0,
2452
- handlerError: error
2620
+ charge,
2621
+ callCount: () => calls,
2622
+ atomicTotal: () => atomic
2453
2623
  };
2454
2624
  }
2455
- function isAsyncIterable2(value) {
2456
- return value != null && typeof value === "object" && Symbol.asyncIterator in value;
2457
- }
2458
- function isThenable2(value) {
2459
- 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 };
2460
2657
  }
2461
2658
 
2462
2659
  // src/pipeline/flows/dynamic/dynamic-preflight.ts
@@ -2486,7 +2683,7 @@ async function runDynamicRequestFlow(args) {
2486
2683
  }
2487
2684
  const beforeErr = await runBeforeSettle(ctx, settleScope);
2488
2685
  if (beforeErr) return beforeErr;
2489
- const billedAmount = routeEntry.tickCost;
2686
+ const billedAmount = computeBilledAmount(routeEntry, result);
2490
2687
  return settleAndFinalizeRequest({
2491
2688
  ctx,
2492
2689
  strategy,
@@ -2504,6 +2701,19 @@ async function runDynamicRequestFlow(args) {
2504
2701
  }
2505
2702
  });
2506
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
+ }
2507
2717
 
2508
2718
  // src/pipeline/flows/dynamic/dynamic-stream.ts
2509
2719
  async function runDynamicStreamFlow(args) {
@@ -2576,13 +2786,7 @@ async function runDynamicPaidFlow(ctx) {
2576
2786
  amount: price,
2577
2787
  network: verifyOutcome.payment.network
2578
2788
  });
2579
- const result = await invokeDynamic(
2580
- ctx,
2581
- verifyOutcome.wallet,
2582
- account,
2583
- parsedBody,
2584
- verifyOutcome.payment
2585
- );
2789
+ const result = await invokeDynamic(ctx, verifyOutcome, account, parsedBody);
2586
2790
  switch (result.kind) {
2587
2791
  case "stream":
2588
2792
  return runDynamicStreamFlow({
@@ -2604,6 +2808,18 @@ async function runDynamicPaidFlow(ctx) {
2604
2808
  });
2605
2809
  }
2606
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
+ }
2607
2823
 
2608
2824
  // src/pipeline/flows/static/static-body-and-price.ts
2609
2825
  async function resolveStaticBodyAndPrice(args) {
@@ -2751,19 +2967,27 @@ async function runStaticPaidFlow(ctx) {
2751
2967
 
2752
2968
  // src/pipeline/flows/paid.ts
2753
2969
  async function runPaidFlow(ctx) {
2754
- const dynamicPrice = ctx.routeEntry.dynamicPrice ?? false;
2755
- switch (dynamicPrice) {
2756
- case true:
2757
- return runDynamicPaidFlow(ctx);
2758
- case false:
2759
- return runStaticPaidFlow(ctx);
2760
- }
2970
+ const handlerCharged = ctx.routeEntry.billing !== "exact";
2971
+ return handlerCharged ? runDynamicPaidFlow(ctx) : runStaticPaidFlow(ctx);
2761
2972
  }
2762
2973
 
2763
2974
  // src/pipeline/flows/siwx-only.ts
2764
2975
  import { NextResponse as NextResponse7 } from "next/server";
2765
2976
 
2766
2977
  // src/kv-store/client.ts
2978
+ var BIGINT_SUFFIX = "#__bigint";
2979
+ function stringifyValue(value) {
2980
+ return JSON.stringify(
2981
+ value,
2982
+ (_key, v) => typeof v === "bigint" ? `${v.toString()}${BIGINT_SUFFIX}` : v
2983
+ );
2984
+ }
2985
+ function parseValue(raw) {
2986
+ return JSON.parse(
2987
+ raw,
2988
+ (_key, v) => typeof v === "string" && v.endsWith(BIGINT_SUFFIX) ? BigInt(v.slice(0, -BIGINT_SUFFIX.length)) : v
2989
+ );
2990
+ }
2767
2991
  function restKvStore(url, token) {
2768
2992
  const base = url.replace(/\/+$/, "");
2769
2993
  const authHeader = { Authorization: `Bearer ${token}` };
@@ -2785,16 +3009,22 @@ function restKvStore(url, token) {
2785
3009
  const res = await fetch(`${base}/get/${encodeURIComponent(key)}`, { headers: authHeader });
2786
3010
  if (!res.ok) throw new Error(`[kv-store] GET ${key}: ${res.status}`);
2787
3011
  const { result } = await res.json();
2788
- return result ?? null;
3012
+ if (result == null) return null;
3013
+ if (typeof result !== "string") return result;
3014
+ try {
3015
+ return parseValue(result);
3016
+ } catch {
3017
+ return result;
3018
+ }
2789
3019
  }
2790
3020
  async function set(key, value) {
2791
- await exec(["SET", key, JSON.stringify(value)]);
3021
+ await exec(["SET", key, stringifyValue(value)]);
2792
3022
  }
2793
3023
  async function del(key) {
2794
3024
  await exec(["DEL", key]);
2795
3025
  }
2796
3026
  async function setNxEx(key, value, ttlSeconds) {
2797
- const result = await exec(["SET", key, JSON.stringify(value), "EX", ttlSeconds, "NX"]);
3027
+ const result = await exec(["SET", key, stringifyValue(value), "EX", ttlSeconds, "NX"]);
2798
3028
  return result === "OK";
2799
3029
  }
2800
3030
  async function sadd(key, member) {
@@ -3123,142 +3353,148 @@ ${issues}`
3123
3353
  }
3124
3354
 
3125
3355
  // src/builder.ts
3126
- var RouteBuilder = class {
3127
- /** @internal */
3128
- _key;
3129
- /** @internal */
3130
- _registry;
3131
- /** @internal */
3132
- _deps;
3133
- /** @internal */
3134
- _authMode = null;
3135
- /** @internal */
3136
- _pricing;
3137
- /** @internal */
3138
- _siwxEnabled = false;
3139
- /** @internal */
3140
- _protocols = ["x402"];
3141
- /** @internal */
3142
- _maxPrice;
3143
- /** @internal */
3144
- _minPrice;
3145
- /** @internal */
3146
- _dynamicPrice = false;
3147
- /** @internal */
3148
- _tickCost;
3149
- /** @internal */
3150
- _unitType;
3151
- /** @internal */
3152
- _payTo;
3153
- /** @internal */
3154
- _bodySchema;
3155
- /** @internal */
3156
- _querySchema;
3157
- /** @internal */
3158
- _outputSchema;
3159
- /** @internal */
3160
- _inputExample = void 0;
3161
- /** @internal */
3162
- _hasInputExample = false;
3163
- /** @internal */
3164
- _outputExample = void 0;
3165
- /** @internal */
3166
- _hasOutputExample = false;
3167
- /** @internal */
3168
- _description;
3169
- /** @internal */
3170
- _path;
3171
- /** @internal */
3172
- _method = "POST";
3173
- /** @internal */
3174
- _apiKeyResolver;
3175
- /** @internal */
3176
- _providerName;
3177
- /** @internal */
3178
- _providerConfig;
3179
- /** @internal */
3180
- _validateFn;
3181
- /** @internal */
3182
- _settlement;
3183
- /** @internal */
3184
- _mppInfo;
3185
- constructor(key, registry, deps) {
3186
- this._key = key;
3187
- this._registry = registry;
3188
- this._deps = deps;
3356
+ var RouteBuilder = class _RouteBuilder {
3357
+ #s;
3358
+ constructor(key, registry, deps, defaults) {
3359
+ this.#s = {
3360
+ key,
3361
+ registry,
3362
+ deps,
3363
+ authMode: null,
3364
+ pricing: void 0,
3365
+ siwxEnabled: false,
3366
+ protocols: defaults?.protocols ? [...defaults.protocols] : ["x402"],
3367
+ maxPrice: void 0,
3368
+ minPrice: void 0,
3369
+ billing: "exact",
3370
+ tickCost: void 0,
3371
+ unitType: void 0,
3372
+ payTo: void 0,
3373
+ bodySchema: void 0,
3374
+ querySchema: void 0,
3375
+ outputSchema: void 0,
3376
+ inputExample: void 0,
3377
+ hasInputExample: false,
3378
+ outputExample: void 0,
3379
+ hasOutputExample: false,
3380
+ description: void 0,
3381
+ path: void 0,
3382
+ method: "POST",
3383
+ apiKeyResolver: void 0,
3384
+ providerName: void 0,
3385
+ providerConfig: void 0,
3386
+ validateFn: void 0,
3387
+ settlement: void 0,
3388
+ mppInfo: void 0
3389
+ };
3189
3390
  }
3190
3391
  fork() {
3191
- const next = Object.create(Object.getPrototypeOf(this));
3192
- Object.assign(next, this);
3193
- next._protocols = [...this._protocols];
3392
+ const next = new _RouteBuilder(
3393
+ this.#s.key,
3394
+ this.#s.registry,
3395
+ this.#s.deps
3396
+ );
3397
+ next.#s = { ...this.#s, protocols: [...this.#s.protocols] };
3194
3398
  return next;
3195
3399
  }
3196
- paid(pricingOrOptions, options) {
3197
- const { pricing, resolvedOptions } = resolvePaidArgs(this._key, pricingOrOptions, options);
3198
- if (this._authMode === "unprotected") {
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;
3437
+ if (this.#s.authMode === "unprotected") {
3199
3438
  throw new Error(
3200
- `route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
3439
+ `route '${this.#s.key}': Cannot combine .unprotected() and .${method}() on the same route.`
3201
3440
  );
3202
3441
  }
3203
- if (this._pricing !== void 0) {
3442
+ if (this.#s.pricing !== void 0) {
3204
3443
  throw new Error(
3205
- `route '${this._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.`
3206
3445
  );
3207
3446
  }
3208
3447
  const next = this.fork();
3209
- next._authMode = "paid";
3210
- next._pricing = pricing;
3211
- if (resolvedOptions?.protocols) {
3212
- next._protocols = [...resolvedOptions.protocols];
3213
- } else if (next._protocols.length === 0) {
3214
- next._protocols = ["x402"];
3215
- }
3216
- if (resolvedOptions?.maxPrice) next._maxPrice = resolvedOptions.maxPrice;
3217
- if (resolvedOptions?.minPrice) next._minPrice = resolvedOptions.minPrice;
3218
- if (resolvedOptions?.payTo) next._payTo = resolvedOptions.payTo;
3219
- if (resolvedOptions?.mpp) next._mppInfo = resolvedOptions.mpp;
3220
- if (resolvedOptions?.dynamic) next._dynamicPrice = true;
3221
- if (resolvedOptions?.tickCost) next._tickCost = resolvedOptions.tickCost;
3222
- if (resolvedOptions?.unitType) next._unitType = resolvedOptions.unitType;
3223
- if (typeof pricing === "object" && "tiers" in pricing) {
3224
- if (next._dynamicPrice) {
3448
+ next.#s.authMode = "paid";
3449
+ next.#s.pricing = pricing;
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")) {
3225
3459
  throw new Error(
3226
- `route '${this._key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
3460
+ `route '${this.#s.key}': .metered() is MPP-only \u2014 remove the conflicting protocols override.`
3227
3461
  );
3228
3462
  }
3463
+ next.#s.protocols = ["mpp"];
3464
+ } else if (resolvedOptions.protocols) {
3465
+ next.#s.protocols = [...resolvedOptions.protocols];
3466
+ } else if (next.#s.protocols.length === 0) {
3467
+ next.#s.protocols = ["x402"];
3468
+ }
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;
3477
+ if (typeof pricing === "object" && "tiers" in pricing) {
3229
3478
  for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
3230
3479
  if (!tierKey) {
3231
- throw new Error(`route '${this._key}': tier key cannot be empty`);
3480
+ throw new Error(`route '${this.#s.key}': tier key cannot be empty`);
3232
3481
  }
3233
- const tierPrice = parseFloat(tierConfig.price);
3234
- if (isNaN(tierPrice) || tierPrice <= 0) {
3482
+ if (!isPositiveDecimal(tierConfig.price)) {
3235
3483
  throw new Error(
3236
- `route '${this._key}': tier '${tierKey}' price '${tierConfig.price}' must be a positive decimal string`
3484
+ `route '${this.#s.key}': tier '${tierKey}' price '${tierConfig.price}' must be a positive decimal string`
3237
3485
  );
3238
3486
  }
3239
3487
  }
3240
3488
  }
3241
- if (resolvedOptions?.maxPrice !== void 0) {
3242
- const parsed = parseFloat(resolvedOptions.maxPrice);
3243
- if (isNaN(parsed) || parsed <= 0) {
3244
- throw new Error(
3245
- `route '${this._key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
3246
- );
3247
- }
3248
- }
3249
- if (resolvedOptions?.tickCost !== void 0) {
3250
- const parsed = parseFloat(resolvedOptions.tickCost);
3251
- if (isNaN(parsed) || parsed <= 0) {
3252
- throw new Error(
3253
- `route '${this._key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
3254
- );
3255
- }
3256
- }
3257
- if (next._dynamicPrice && !next._maxPrice) {
3258
- throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires maxPrice`);
3489
+ if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
3490
+ throw new Error(
3491
+ `route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
3492
+ );
3259
3493
  }
3260
- if (next._dynamicPrice && !next._tickCost) {
3261
- throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires tickCost`);
3494
+ if (next.#s.tickCost !== void 0 && !isPositiveDecimal(next.#s.tickCost)) {
3495
+ throw new Error(
3496
+ `route '${this.#s.key}': tickCost '${next.#s.tickCost}' must be a positive decimal string`
3497
+ );
3262
3498
  }
3263
3499
  return next;
3264
3500
  }
@@ -3273,25 +3509,25 @@ var RouteBuilder = class {
3273
3509
  * ```
3274
3510
  */
3275
3511
  siwx() {
3276
- if (this._authMode === "unprotected") {
3512
+ if (this.#s.authMode === "unprotected") {
3277
3513
  throw new Error(
3278
- `route '${this._key}': Cannot combine .unprotected() and .siwx() on the same route.`
3514
+ `route '${this.#s.key}': Cannot combine .unprotected() and .siwx() on the same route.`
3279
3515
  );
3280
3516
  }
3281
- if (this._apiKeyResolver) {
3517
+ if (this.#s.apiKeyResolver) {
3282
3518
  throw new Error(
3283
- `route '${this._key}': Combining .siwx() and .apiKey() is not supported on the same route.`
3519
+ `route '${this.#s.key}': Combining .siwx() and .apiKey() is not supported on the same route.`
3284
3520
  );
3285
3521
  }
3286
3522
  const next = this.fork();
3287
- next._siwxEnabled = true;
3288
- if (next._authMode === "paid" || next._pricing) {
3289
- next._authMode = "paid";
3290
- if (next._protocols.length === 0) next._protocols = ["x402"];
3523
+ next.#s.siwxEnabled = true;
3524
+ if (next.#s.authMode === "paid" || next.#s.pricing) {
3525
+ next.#s.authMode = "paid";
3526
+ if (next.#s.protocols.length === 0) next.#s.protocols = ["x402"];
3291
3527
  return next;
3292
3528
  }
3293
- next._authMode = "siwx";
3294
- next._protocols = [];
3529
+ next.#s.authMode = "siwx";
3530
+ next.#s.protocols = [];
3295
3531
  return next;
3296
3532
  }
3297
3533
  /**
@@ -3308,14 +3544,14 @@ var RouteBuilder = class {
3308
3544
  * ```
3309
3545
  */
3310
3546
  apiKey(resolver) {
3311
- if (this._siwxEnabled) {
3547
+ if (this.#s.siwxEnabled) {
3312
3548
  throw new Error(
3313
- `route '${this._key}': Combining .apiKey() and .siwx() is not supported on the same route.`
3549
+ `route '${this.#s.key}': Combining .apiKey() and .siwx() is not supported on the same route.`
3314
3550
  );
3315
3551
  }
3316
3552
  const next = this.fork();
3317
- next._authMode = "apiKey";
3318
- next._apiKeyResolver = resolver;
3553
+ next.#s.authMode = "apiKey";
3554
+ next.#s.apiKeyResolver = resolver;
3319
3555
  return next;
3320
3556
  }
3321
3557
  /**
@@ -3328,19 +3564,19 @@ var RouteBuilder = class {
3328
3564
  * ```
3329
3565
  */
3330
3566
  unprotected() {
3331
- if (this._authMode && this._authMode !== "unprotected") {
3567
+ if (this.#s.authMode && this.#s.authMode !== "unprotected") {
3332
3568
  throw new Error(
3333
- `route '${this._key}': Cannot combine .unprotected() and .${this._authMode}() on the same route.`
3569
+ `route '${this.#s.key}': Cannot combine .unprotected() and .${this.#s.authMode}() on the same route.`
3334
3570
  );
3335
3571
  }
3336
- if (this._pricing) {
3572
+ if (this.#s.pricing) {
3337
3573
  throw new Error(
3338
- `route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
3574
+ `route '${this.#s.key}': Cannot combine .unprotected() and .paid() on the same route.`
3339
3575
  );
3340
3576
  }
3341
3577
  const next = this.fork();
3342
- next._authMode = "unprotected";
3343
- next._protocols = [];
3578
+ next.#s.authMode = "unprotected";
3579
+ next.#s.protocols = [];
3344
3580
  return next;
3345
3581
  }
3346
3582
  /**
@@ -3359,8 +3595,8 @@ var RouteBuilder = class {
3359
3595
  */
3360
3596
  provider(name, config) {
3361
3597
  const next = this.fork();
3362
- next._providerName = name;
3363
- next._providerConfig = config ?? {};
3598
+ next.#s.providerName = name;
3599
+ next.#s.providerConfig = config ?? {};
3364
3600
  return next;
3365
3601
  }
3366
3602
  /**
@@ -3375,7 +3611,7 @@ var RouteBuilder = class {
3375
3611
  */
3376
3612
  body(schema) {
3377
3613
  const next = this.fork();
3378
- next._bodySchema = schema;
3614
+ next.#s.bodySchema = schema;
3379
3615
  return next;
3380
3616
  }
3381
3617
  /**
@@ -3391,8 +3627,8 @@ var RouteBuilder = class {
3391
3627
  */
3392
3628
  query(schema) {
3393
3629
  const next = this.fork();
3394
- next._querySchema = schema;
3395
- next._method = "GET";
3630
+ next.#s.querySchema = schema;
3631
+ next.#s.method = "GET";
3396
3632
  return next;
3397
3633
  }
3398
3634
  /**
@@ -3409,7 +3645,7 @@ var RouteBuilder = class {
3409
3645
  */
3410
3646
  output(schema) {
3411
3647
  const next = this.fork();
3412
- next._outputSchema = schema;
3648
+ next.#s.outputSchema = schema;
3413
3649
  return next;
3414
3650
  }
3415
3651
  /**
@@ -3423,8 +3659,8 @@ var RouteBuilder = class {
3423
3659
  */
3424
3660
  inputExample(example) {
3425
3661
  const next = this.fork();
3426
- next._inputExample = example;
3427
- next._hasInputExample = true;
3662
+ next.#s.inputExample = example;
3663
+ next.#s.hasInputExample = true;
3428
3664
  return next;
3429
3665
  }
3430
3666
  /**
@@ -3438,8 +3674,8 @@ var RouteBuilder = class {
3438
3674
  */
3439
3675
  outputExample(example) {
3440
3676
  const next = this.fork();
3441
- next._outputExample = example;
3442
- next._hasOutputExample = true;
3677
+ next.#s.outputExample = example;
3678
+ next.#s.hasOutputExample = true;
3443
3679
  return next;
3444
3680
  }
3445
3681
  /**
@@ -3453,7 +3689,7 @@ var RouteBuilder = class {
3453
3689
  */
3454
3690
  description(text) {
3455
3691
  const next = this.fork();
3456
- next._description = text;
3692
+ next.#s.description = text;
3457
3693
  return next;
3458
3694
  }
3459
3695
  /**
@@ -3467,7 +3703,7 @@ var RouteBuilder = class {
3467
3703
  */
3468
3704
  path(p) {
3469
3705
  const next = this.fork();
3470
- next._path = p;
3706
+ next.#s.path = p;
3471
3707
  return next;
3472
3708
  }
3473
3709
  /**
@@ -3481,7 +3717,7 @@ var RouteBuilder = class {
3481
3717
  */
3482
3718
  method(m) {
3483
3719
  const next = this.fork();
3484
- next._method = m;
3720
+ next.#s.method = m;
3485
3721
  return next;
3486
3722
  }
3487
3723
  /**
@@ -3500,7 +3736,7 @@ var RouteBuilder = class {
3500
3736
  */
3501
3737
  validate(fn) {
3502
3738
  const next = this.fork();
3503
- next._validateFn = fn;
3739
+ next.#s.validateFn = fn;
3504
3740
  return next;
3505
3741
  }
3506
3742
  /**
@@ -3518,7 +3754,7 @@ var RouteBuilder = class {
3518
3754
  */
3519
3755
  settlement(lifecycle) {
3520
3756
  const next = this.fork();
3521
- next._settlement = lifecycle;
3757
+ next.#s.settlement = lifecycle;
3522
3758
  return next;
3523
3759
  }
3524
3760
  /**
@@ -3541,13 +3777,13 @@ var RouteBuilder = class {
3541
3777
  /**
3542
3778
  * Register a streaming handler (`async function*`) and return the Next.js
3543
3779
  * route function. Each `charge()` call bills one tick (`tickCost` USDC) up
3544
- * to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
3780
+ * to `maxPrice`; requires `.metered({ ... })` and MPP session mode.
3545
3781
  *
3546
3782
  * @example
3547
3783
  * ```ts
3548
3784
  * export const POST = router
3549
3785
  * .route('llm/stream')
3550
- * .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
3786
+ * .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
3551
3787
  * .body(schema)
3552
3788
  * .stream(async function* ({ body, charge }) {
3553
3789
  * for await (const token of streamLLM(body.prompt)) {
@@ -3561,91 +3797,144 @@ var RouteBuilder = class {
3561
3797
  return this.register(fn, true);
3562
3798
  }
3563
3799
  register(handlerFn, streaming) {
3564
- if (!this._authMode) {
3800
+ if (!this.#s.authMode) {
3565
3801
  throw new Error(
3566
- `route '${this._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()`
3567
3803
  );
3568
3804
  }
3569
- if (this._validateFn && !this._bodySchema) {
3805
+ if (this.#s.validateFn && !this.#s.bodySchema) {
3570
3806
  throw new Error(
3571
- `route '${this._key}': .validate() requires .body() \u2014 validation runs on parsed body`
3807
+ `route '${this.#s.key}': .validate() requires .body() \u2014 validation runs on parsed body`
3572
3808
  );
3573
3809
  }
3574
- if (this._settlement && !this._pricing) {
3575
- throw new Error(`route '${this._key}': .settlement() requires a paid route`);
3810
+ if (this.#s.settlement && !this.#s.pricing) {
3811
+ throw new Error(`route '${this.#s.key}': .settlement() requires a paid route`);
3576
3812
  }
3577
- if (this._dynamicPrice && this._protocols.includes("x402")) {
3578
- const hasUpto = this._deps.x402Accepts.some((accept) => accept.scheme === "upto");
3813
+ if (this.#s.billing === "upto") {
3814
+ const hasUpto = this.#s.deps.x402Accepts.some((accept) => accept.scheme === "upto");
3579
3815
  if (!hasUpto) {
3580
3816
  throw new Error(
3581
- `route '${this._key}': .paid({ dynamic: true }) on an x402 route requires an 'upto' accept on at least one configured network. Add { scheme: 'upto', network, asset } to RouterConfig.x402.accepts.`
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.`
3582
3828
  );
3583
3829
  }
3584
3830
  }
3585
- if (this._dynamicPrice && this._protocols.includes("mpp")) {
3586
- if (!this._deps.mppSessionConfig) {
3831
+ if (this.#s.billing === "metered") {
3832
+ if (!this.#s.deps.mppSessionConfig) {
3587
3833
  throw new Error(
3588
- `route '${this._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.`
3589
3835
  );
3590
3836
  }
3591
3837
  }
3592
- if (streaming && !this._dynamicPrice) {
3838
+ if (streaming && this.#s.billing !== "metered") {
3593
3839
  throw new Error(
3594
- `route '${this._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.`
3595
3841
  );
3596
3842
  }
3597
3843
  validateExamples(
3598
- this._key,
3599
- this._bodySchema,
3600
- this._querySchema,
3601
- this._outputSchema,
3602
- this._inputExample,
3603
- this._hasInputExample,
3604
- this._outputExample,
3605
- this._hasOutputExample
3844
+ this.#s.key,
3845
+ this.#s.bodySchema,
3846
+ this.#s.querySchema,
3847
+ this.#s.outputSchema,
3848
+ this.#s.inputExample,
3849
+ this.#s.hasInputExample,
3850
+ this.#s.outputExample,
3851
+ this.#s.hasOutputExample
3606
3852
  );
3607
3853
  const entry = {
3608
- key: this._key,
3609
- authMode: this._authMode,
3610
- siwxEnabled: this._siwxEnabled,
3611
- pricing: this._pricing,
3612
- dynamicPrice: this._dynamicPrice ? true : void 0,
3854
+ key: this.#s.key,
3855
+ authMode: this.#s.authMode,
3856
+ siwxEnabled: this.#s.siwxEnabled,
3857
+ pricing: this.#s.pricing,
3858
+ billing: this.#s.billing,
3613
3859
  streaming: streaming ? true : void 0,
3614
- protocols: this._protocols,
3615
- bodySchema: this._bodySchema,
3616
- querySchema: this._querySchema,
3617
- outputSchema: this._outputSchema,
3618
- inputExample: this._hasInputExample ? this._inputExample : void 0,
3619
- outputExample: this._hasOutputExample ? this._outputExample : void 0,
3620
- description: this._description,
3621
- path: this._path,
3622
- method: this._method,
3623
- maxPrice: this._maxPrice,
3624
- minPrice: this._minPrice,
3625
- payTo: this._payTo,
3626
- apiKeyResolver: this._apiKeyResolver,
3627
- providerName: this._providerName,
3628
- providerConfig: this._providerConfig,
3629
- validateFn: this._validateFn,
3630
- settlement: this._settlement,
3631
- mppInfo: this._mppInfo,
3632
- tickCost: this._tickCost,
3633
- unitType: this._unitType
3860
+ protocols: this.#s.protocols,
3861
+ bodySchema: this.#s.bodySchema,
3862
+ querySchema: this.#s.querySchema,
3863
+ outputSchema: this.#s.outputSchema,
3864
+ inputExample: this.#s.hasInputExample ? this.#s.inputExample : void 0,
3865
+ outputExample: this.#s.hasOutputExample ? this.#s.outputExample : void 0,
3866
+ description: this.#s.description,
3867
+ path: this.#s.path,
3868
+ method: this.#s.method,
3869
+ maxPrice: this.#s.maxPrice,
3870
+ minPrice: this.#s.minPrice,
3871
+ payTo: this.#s.payTo,
3872
+ apiKeyResolver: this.#s.apiKeyResolver,
3873
+ providerName: this.#s.providerName,
3874
+ providerConfig: this.#s.providerConfig,
3875
+ validateFn: this.#s.validateFn,
3876
+ settlement: this.#s.settlement,
3877
+ mppInfo: this.#s.mppInfo,
3878
+ tickCost: this.#s.tickCost,
3879
+ unitType: this.#s.unitType
3634
3880
  };
3635
- this._registry.register(entry);
3636
- return createRequestHandler(entry, handlerFn, this._deps);
3881
+ this.#s.registry.register(entry);
3882
+ return createRequestHandler(entry, handlerFn, this.#s.deps);
3637
3883
  }
3638
3884
  };
3639
- function resolvePaidArgs(routeKey, pricingOrOptions, options) {
3640
- const isHandlerDynamicShape = typeof pricingOrOptions === "object" && pricingOrOptions !== null && typeof pricingOrOptions !== "function" && !("tiers" in pricingOrOptions) && "dynamic" in pricingOrOptions && pricingOrOptions.dynamic;
3641
- if (isHandlerDynamicShape) {
3642
- const opts = pricingOrOptions;
3643
- if (!opts.maxPrice) {
3644
- throw new Error(`route '${routeKey}': .paid({ dynamic: true }) requires maxPrice`);
3645
- }
3646
- 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
+ };
3895
+ }
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`);
3647
3929
  }
3648
- return { pricing: pricingOrOptions, resolvedOptions: options };
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
+ };
3649
3938
  }
3650
3939
 
3651
3940
  // src/discovery/well-known.ts
@@ -3877,17 +4166,16 @@ function buildPricingInfo(entry) {
3877
4166
  };
3878
4167
  }
3879
4168
  if ("tiers" in entry.pricing) {
3880
- const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
3881
- const min = Math.min(...tierPrices);
3882
- const max = Math.max(...tierPrices);
3883
- if (Number.isFinite(min) && Number.isFinite(max)) {
3884
- if (min === max) {
4169
+ const tierPrices = Object.values(entry.pricing.tiers).map((tier) => tier.price);
4170
+ const extrema = tierExtrema(tierPrices);
4171
+ if (extrema) {
4172
+ if (extrema.min === extrema.max) {
3885
4173
  return {
3886
- price: { mode: "fixed", currency: "USD", amount: String(min) }
4174
+ price: { mode: "fixed", currency: "USD", amount: extrema.min }
3887
4175
  };
3888
4176
  }
3889
4177
  return {
3890
- price: { mode: "dynamic", currency: "USD", min: String(min), max: String(max) }
4178
+ price: { mode: "dynamic", currency: "USD", min: extrema.min, max: extrema.max }
3891
4179
  };
3892
4180
  }
3893
4181
  return {
@@ -3901,6 +4189,20 @@ function buildPricingInfo(entry) {
3901
4189
  }
3902
4190
  return void 0;
3903
4191
  }
4192
+ function tierExtrema(prices) {
4193
+ if (prices.length === 0) return null;
4194
+ let min = prices[0];
4195
+ let max = prices[0];
4196
+ try {
4197
+ for (const price of prices.slice(1)) {
4198
+ if (compareDecimals(price, min) < 0) min = price;
4199
+ if (compareDecimals(price, max) > 0) max = price;
4200
+ }
4201
+ } catch {
4202
+ return null;
4203
+ }
4204
+ return { min, max };
4205
+ }
3904
4206
 
3905
4207
  // src/discovery/llms-txt.ts
3906
4208
  import { NextResponse as NextResponse10 } from "next/server";
@@ -4387,11 +4689,11 @@ function getRouterConfigIssues(config, options = {}) {
4387
4689
  }
4388
4690
 
4389
4691
  // src/init/x402.ts
4390
- async function initX402(config, configError) {
4692
+ async function initX402(config, kvStore, configError) {
4391
4693
  if (configError) return { initError: configError };
4392
4694
  try {
4393
4695
  const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_x402_server(), x402_server_exports));
4394
- const result = await createX402Server2(config);
4696
+ const result = await createX402Server2(config, kvStore);
4395
4697
  await result.initPromise;
4396
4698
  return {
4397
4699
  server: result.server,
@@ -4571,10 +4873,10 @@ function createRouter(config) {
4571
4873
  x402Accepts,
4572
4874
  mppx: null,
4573
4875
  tempoClient: null,
4574
- 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
4575
4877
  };
4576
4878
  deps.initPromise = (async () => {
4577
- const x402Result = await initX402(config, x402ConfigError);
4879
+ const x402Result = await initX402(config, kvStore, x402ConfigError);
4578
4880
  deps.x402Server = x402Result.server ?? null;
4579
4881
  deps.x402FacilitatorsByNetwork = x402Result.facilitatorsByNetwork;
4580
4882
  if (x402Result.initError) deps.x402InitError = x402Result.initError;
@@ -4603,11 +4905,10 @@ function createRouter(config) {
4603
4905
  `[router] strictRoutes=true forbids key/path divergence for route '${definition.path}'. Remove custom \`key\` or make it equal to \`path\`.`
4604
4906
  );
4605
4907
  }
4606
- let builder = new RouteBuilder(key, registry, deps);
4908
+ let builder = new RouteBuilder(key, registry, deps, {
4909
+ protocols: config.protocols
4910
+ });
4607
4911
  builder = builder.path(normalizedPath);
4608
- if (config.protocols) {
4609
- builder._protocols = [...config.protocols];
4610
- }
4611
4912
  if (definition.method) {
4612
4913
  builder = builder.method(definition.method);
4613
4914
  }