@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.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) => ({
@@ -264,12 +267,148 @@ var init_facilitators = __esm({
264
267
  }
265
268
  });
266
269
 
270
+ // src/kv-store/facilitator-supported.ts
271
+ function withCachedSupported(inner, options = {}) {
272
+ const { kv, cacheKey, ttlSeconds = FACILITATOR_SUPPORTED_TTL_SECONDS, fallback } = options;
273
+ const kvKey = kv && cacheKey ? `${FACILITATOR_SUPPORTED_KV_PREFIX}${cacheKey}` : void 0;
274
+ let inflight;
275
+ return {
276
+ verify: inner.verify.bind(inner),
277
+ settle: inner.settle.bind(inner),
278
+ getSupported: () => {
279
+ if (inflight) return inflight;
280
+ const attempt = fetchSupported(inner, kv, kvKey, ttlSeconds, fallback);
281
+ inflight = attempt;
282
+ attempt.catch(() => {
283
+ if (inflight === attempt) inflight = void 0;
284
+ });
285
+ return attempt;
286
+ }
287
+ };
288
+ }
289
+ async function fetchSupported(inner, kv, kvKey, ttlSeconds, fallback) {
290
+ if (kv && kvKey) {
291
+ const cached = await readKvCache(kv, kvKey);
292
+ if (cached) return cached;
293
+ }
294
+ const fresh = await tryFetchLive(inner, fallback);
295
+ if (fresh === null) return fallback();
296
+ if (kv && kvKey) await writeKvCache(kv, kvKey, fresh, ttlSeconds);
297
+ return fresh;
298
+ }
299
+ async function tryFetchLive(inner, fallback) {
300
+ try {
301
+ return await inner.getSupported();
302
+ } catch (err) {
303
+ if (!fallback) throw err;
304
+ console.warn(
305
+ `[x402] facilitator /supported failed, using hardcoded baseline: ${err instanceof Error ? err.message : String(err)}`
306
+ );
307
+ return null;
308
+ }
309
+ }
310
+ async function readKvCache(kv, key) {
311
+ try {
312
+ const cached = await kv.get(key);
313
+ return isSupportedResponse(cached) ? cached : void 0;
314
+ } catch {
315
+ return void 0;
316
+ }
317
+ }
318
+ async function writeKvCache(kv, key, value, ttlSeconds) {
319
+ try {
320
+ await kv.setNxEx(key, value, ttlSeconds);
321
+ } catch {
322
+ }
323
+ }
324
+ function isSupportedResponse(value) {
325
+ return typeof value === "object" && value !== null && Array.isArray(value.kinds);
326
+ }
327
+ var FACILITATOR_SUPPORTED_TTL_SECONDS, FACILITATOR_SUPPORTED_KV_PREFIX;
328
+ var init_facilitator_supported = __esm({
329
+ "src/kv-store/facilitator-supported.ts"() {
330
+ "use strict";
331
+ FACILITATOR_SUPPORTED_TTL_SECONDS = 60 * 60;
332
+ FACILITATOR_SUPPORTED_KV_PREFIX = "x402:facilitator-supported:";
333
+ }
334
+ });
335
+
336
+ // src/protocols/x402/facilitator-clients.ts
337
+ function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient, kvStore) {
338
+ return getResolvedX402FacilitatorGroups(facilitatorsByNetwork).map((group) => {
339
+ const inner = new HTTPFacilitatorClient(group.config);
340
+ const kinds = buildSupportedKinds(group);
341
+ const baseline = () => ({
342
+ kinds,
343
+ extensions: [],
344
+ signers: {}
345
+ });
346
+ if (group.family === "solana") {
347
+ return hardcodedSupportedClient(inner, baseline);
348
+ }
349
+ const cached = withCachedSupported(inner, {
350
+ kv: kvStore,
351
+ cacheKey: group.config.url,
352
+ fallback: baseline
353
+ });
354
+ return withScopedKinds(cached, kinds);
355
+ });
356
+ }
357
+ function hardcodedSupportedClient(inner, build) {
358
+ return {
359
+ verify: inner.verify.bind(inner),
360
+ settle: inner.settle.bind(inner),
361
+ getSupported: async () => build()
362
+ };
363
+ }
364
+ function withScopedKinds(client, kinds) {
365
+ return {
366
+ verify: client.verify.bind(client),
367
+ settle: client.settle.bind(client),
368
+ getSupported: async () => {
369
+ const live = await client.getSupported();
370
+ return { ...live, kinds: mergeKindExtras(kinds, live.kinds) };
371
+ }
372
+ };
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
+ }
380
+ function buildSupportedKinds(group) {
381
+ return group.networks.flatMap((network) => {
382
+ if (group.family === "solana") {
383
+ return [
384
+ {
385
+ x402Version: 2,
386
+ scheme: "exact",
387
+ network,
388
+ extra: { features: { xSettlementAccountSupported: true } }
389
+ }
390
+ ];
391
+ }
392
+ return [
393
+ { x402Version: 2, scheme: "exact", network },
394
+ { x402Version: 2, scheme: "upto", network }
395
+ ];
396
+ });
397
+ }
398
+ var init_facilitator_clients = __esm({
399
+ "src/protocols/x402/facilitator-clients.ts"() {
400
+ "use strict";
401
+ init_facilitator_supported();
402
+ init_facilitators();
403
+ }
404
+ });
405
+
267
406
  // src/init/x402-server.ts
268
407
  var x402_server_exports = {};
269
408
  __export(x402_server_exports, {
270
409
  createX402Server: () => createX402Server
271
410
  });
272
- async function createX402Server(config) {
411
+ async function createX402Server(config, kvStore) {
273
412
  const { x402ResourceServer, HTTPFacilitatorClient } = await import("@x402/core/server");
274
413
  const { registerExactEvmScheme } = await import("@x402/evm/exact/server");
275
414
  const { bazaarResourceServerExtension } = await import("@x402/extensions/bazaar");
@@ -283,7 +422,11 @@ async function createX402Server(config) {
283
422
  );
284
423
  const evmNetworks = filterEvmNetworks(configuredNetworks);
285
424
  const svmNetworks = filterSolanaNetworks(configuredNetworks);
286
- const facilitatorClients = createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient);
425
+ const facilitatorClients = createFacilitatorClients(
426
+ facilitatorsByNetwork,
427
+ HTTPFacilitatorClient,
428
+ kvStore
429
+ );
287
430
  const server = new x402ResourceServer(
288
431
  facilitatorClients.length === 1 ? facilitatorClients[0] : facilitatorClients
289
432
  );
@@ -307,48 +450,13 @@ async function createX402Server(config) {
307
450
  facilitatorsByNetwork
308
451
  };
309
452
  }
310
- function createFacilitatorClients(facilitatorsByNetwork, HTTPFacilitatorClient) {
311
- const groups = getResolvedX402FacilitatorGroups(facilitatorsByNetwork);
312
- return groups.map((group) => {
313
- const inner = new HTTPFacilitatorClient(group.config);
314
- const kinds = buildSupportedKinds(group);
315
- return hardcodedSupportedClient(inner, kinds);
316
- });
317
- }
318
- function hardcodedSupportedClient(inner, kinds) {
319
- return {
320
- verify: inner.verify.bind(inner),
321
- settle: inner.settle.bind(inner),
322
- getSupported: async () => ({ kinds, extensions: [], signers: {} })
323
- };
324
- }
325
- function buildSupportedKinds(group) {
326
- return group.networks.flatMap((network) => {
327
- const exactKind = {
328
- x402Version: 2,
329
- scheme: "exact",
330
- network,
331
- ...group.family === "solana" ? {
332
- extra: {
333
- features: {
334
- xSettlementAccountSupported: true
335
- }
336
- }
337
- } : {}
338
- };
339
- const uptoKind = { x402Version: 2, scheme: "upto", network };
340
- if (group.family === "evm") {
341
- return [exactKind, uptoKind];
342
- }
343
- return [exactKind, uptoKind];
344
- });
345
- }
346
453
  var init_x402_server = __esm({
347
454
  "src/init/x402-server.ts"() {
348
455
  "use strict";
349
456
  init_evm();
350
457
  init_solana();
351
458
  init_facilitators();
459
+ init_facilitator_clients();
352
460
  init_accepts();
353
461
  }
354
462
  });
@@ -699,10 +807,7 @@ async function runHandler(ctx, handlerCtx) {
699
807
  }
700
808
  if (isAsyncIterable(returned) && !isThenable(returned)) {
701
809
  return errorResult(
702
- new HttpError(
703
- `route '${ctx.routeEntry.key}': streaming handlers require .paid({ dynamic: true })`,
704
- 500
705
- )
810
+ new HttpError(`route '${ctx.routeEntry.key}': streaming handlers require .metered()`, 500)
706
811
  );
707
812
  }
708
813
  let rawResult;
@@ -1069,6 +1174,61 @@ async function runApiKeyOnlyFlow(ctx) {
1069
1174
  return runHandlerOnly(ctx, null, result.account);
1070
1175
  }
1071
1176
 
1177
+ // src/pricing/format.ts
1178
+ var USDC_DECIMALS = 6;
1179
+ var DECIMAL_RE = /^(\d+)(?:\.(\d+))?$/;
1180
+ function badDecimal(amount) {
1181
+ return Object.assign(new Error(`'${amount}' is not a valid decimal-dollar string`), {
1182
+ status: 400
1183
+ });
1184
+ }
1185
+ function decimalToAtomic(amount, decimals = USDC_DECIMALS) {
1186
+ const match = DECIMAL_RE.exec(amount.trim());
1187
+ if (!match) throw badDecimal(amount);
1188
+ const whole = match[1];
1189
+ const fraction = match[2] ?? "";
1190
+ if (fraction.length > decimals) {
1191
+ throw Object.assign(new Error(`Amount '${amount}' exceeds ${decimals} decimal places`), {
1192
+ status: 400
1193
+ });
1194
+ }
1195
+ const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
1196
+ return BigInt(normalized || "0");
1197
+ }
1198
+ function atomicToDecimal(atomic, decimals = USDC_DECIMALS) {
1199
+ const divisor = 10n ** BigInt(decimals);
1200
+ const whole = atomic / divisor;
1201
+ const fraction = atomic % divisor;
1202
+ if (fraction === 0n) return whole.toString();
1203
+ const fractionStr = fraction.toString().padStart(decimals, "0").replace(/0+$/, "");
1204
+ return `${whole}.${fractionStr}`;
1205
+ }
1206
+ function compareDecimals(a, b) {
1207
+ const av = decimalToAtomic(a);
1208
+ const bv = decimalToAtomic(b);
1209
+ if (av < bv) return -1;
1210
+ if (av > bv) return 1;
1211
+ return 0;
1212
+ }
1213
+ function isPositiveDecimal(value) {
1214
+ try {
1215
+ return decimalToAtomic(value) > 0n;
1216
+ } catch {
1217
+ return false;
1218
+ }
1219
+ }
1220
+ function multiplyDecimal(decimal, factor) {
1221
+ if (!Number.isFinite(factor) || factor <= 0) return decimal;
1222
+ const [whole, fraction = ""] = decimal.split(".");
1223
+ const scaled = (BigInt(whole + fraction) * BigInt(factor)).toString();
1224
+ const decimals = fraction.length;
1225
+ if (decimals === 0) return scaled;
1226
+ const padded = scaled.padStart(decimals + 1, "0");
1227
+ const intPart = padded.slice(0, padded.length - decimals);
1228
+ const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
1229
+ return fracPart ? `${intPart}.${fracPart}` : intPart;
1230
+ }
1231
+
1072
1232
  // src/pricing/dynamic.ts
1073
1233
  var DynamicPricing = class {
1074
1234
  constructor(opts) {
@@ -1080,6 +1240,7 @@ var DynamicPricing = class {
1080
1240
  const raw = await this.opts.fn(body);
1081
1241
  return this.cap(raw, body);
1082
1242
  } catch (err) {
1243
+ if (err instanceof HttpError) throw err;
1083
1244
  this.alert("error", `Pricing function failed: ${msg(err)}`, {
1084
1245
  error: err instanceof Error ? err.stack : String(err),
1085
1246
  body
@@ -1104,9 +1265,13 @@ var DynamicPricing = class {
1104
1265
  }
1105
1266
  cap(raw, body) {
1106
1267
  if (!this.opts.maxPrice) return raw;
1107
- const n = parseFloat(raw);
1108
- const max = parseFloat(this.opts.maxPrice);
1109
- if (!Number.isFinite(n) || n > max) {
1268
+ let overCap;
1269
+ try {
1270
+ overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
1271
+ } catch {
1272
+ overCap = true;
1273
+ }
1274
+ if (overCap) {
1110
1275
  this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
1111
1276
  calculated: raw,
1112
1277
  maxPrice: this.opts.maxPrice,
@@ -1183,7 +1348,7 @@ var TieredPricing = class {
1183
1348
  maxTierPrice() {
1184
1349
  let max = "0";
1185
1350
  for (const tier of Object.values(this.opts.tiers)) {
1186
- if (parseFloat(tier.price) > parseFloat(max)) max = tier.price;
1351
+ if (compareDecimals(tier.price, max) > 0) max = tier.price;
1187
1352
  }
1188
1353
  return max;
1189
1354
  }
@@ -1598,7 +1763,7 @@ var mppStrategy = {
1598
1763
  async verify(args) {
1599
1764
  const info = readMppCredential(args.request);
1600
1765
  if (!info) return { ok: false, kind: "invalid" };
1601
- if (args.routeEntry.dynamicPrice) {
1766
+ if (args.routeEntry.billing === "metered") {
1602
1767
  if (!info.sessionAction) return { ok: false, kind: "invalid" };
1603
1768
  return verifySessionMode(args, info);
1604
1769
  }
@@ -1649,7 +1814,7 @@ var mppStrategy = {
1649
1814
  async buildChallenge(args) {
1650
1815
  if (!args.deps.mppx) return {};
1651
1816
  const sessionsConfigured = args.deps.mppSessionConfig && (args.deps.mppx.sessionRequest || args.deps.mppx.sessionStream);
1652
- if (args.routeEntry.dynamicPrice && sessionsConfigured) {
1817
+ if (args.routeEntry.billing === "metered" && sessionsConfigured) {
1653
1818
  const tickCost = args.routeEntry.tickCost;
1654
1819
  const computedDeposit = tickCost !== void 0 ? multiplyDecimal(tickCost, args.deps.mppSessionConfig.depositMultiplier) : void 0;
1655
1820
  const suggestedDeposit = args.routeEntry.maxPrice ?? computedDeposit ?? args.price;
@@ -1661,17 +1826,6 @@ var mppStrategy = {
1661
1826
  return buildChargeChallenge(args);
1662
1827
  }
1663
1828
  };
1664
- function multiplyDecimal(decimal, factor) {
1665
- if (!Number.isFinite(factor) || factor <= 0) return decimal;
1666
- const [whole, fraction = ""] = decimal.split(".");
1667
- const scaled = (BigInt(whole + fraction) * BigInt(factor)).toString();
1668
- const decimals = fraction.length;
1669
- if (decimals === 0) return scaled;
1670
- const padded = scaled.padStart(decimals + 1, "0");
1671
- const intPart = padded.slice(0, padded.length - decimals);
1672
- const fracPart = padded.slice(padded.length - decimals).replace(/0+$/, "");
1673
- return fracPart ? `${intPart}.${fracPart}` : intPart;
1674
- }
1675
1829
  async function buildChargeChallenge(args) {
1676
1830
  if (!args.deps.mppx) return {};
1677
1831
  try {
@@ -1764,26 +1918,13 @@ function buildCustomRequirement(price, accept) {
1764
1918
  return {
1765
1919
  scheme: accept.scheme,
1766
1920
  network: accept.network,
1767
- amount: decimalToAtomicUnits(price, accept.decimals ?? 6),
1921
+ amount: decimalToAtomic(price, accept.decimals ?? 6).toString(),
1768
1922
  asset: accept.asset,
1769
1923
  payTo: accept.payTo,
1770
1924
  maxTimeoutSeconds: accept.maxTimeoutSeconds ?? 300,
1771
1925
  extra: accept.extra ?? {}
1772
1926
  };
1773
1927
  }
1774
- function decimalToAtomicUnits(amount, decimals) {
1775
- const match = /^(?<whole>\d+)(?:\.(?<fraction>\d+))?$/.exec(amount);
1776
- if (!match?.groups) {
1777
- throw new Error(`Invalid decimal amount '${amount}'`);
1778
- }
1779
- const whole = match.groups.whole;
1780
- const fraction = match.groups.fraction ?? "";
1781
- if (fraction.length > decimals) {
1782
- throw new Error(`Amount '${amount}' exceeds ${decimals} decimal places`);
1783
- }
1784
- const normalized = `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
1785
- return normalized === "" ? "0" : normalized;
1786
- }
1787
1928
 
1788
1929
  // src/protocols/x402/challenge.ts
1789
1930
  async function buildX402Challenge(opts) {
@@ -1930,7 +2071,7 @@ function tagBareDecimalAsDollars(amount) {
1930
2071
  }
1931
2072
 
1932
2073
  // src/protocols/x402/verify.ts
1933
- var import_types2 = require("@x402/core/types");
2074
+ var import_types3 = require("@x402/core/types");
1934
2075
  async function verifyX402Payment(opts) {
1935
2076
  const { server, request, price, accepts, report } = opts;
1936
2077
  const payload = await readPaymentPayload(request);
@@ -1949,7 +2090,7 @@ async function verifyX402Payment(opts) {
1949
2090
  try {
1950
2091
  verify = await server.verifyPayment(payload, matching);
1951
2092
  } catch (err) {
1952
- 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) {
1953
2094
  return invalidPaymentVerification({
1954
2095
  reason: err.invalidReason ?? "verify_error",
1955
2096
  ...err.invalidMessage ? { message: err.invalidMessage } : {},
@@ -2050,7 +2191,7 @@ async function verifyX402(args) {
2050
2191
  const accepts = await resolveX402Accepts(
2051
2192
  request,
2052
2193
  routeEntry,
2053
- deps.x402Accepts,
2194
+ selectRouteAccepts(deps.x402Accepts, routeEntry),
2054
2195
  deps.payeeAddress,
2055
2196
  body
2056
2197
  );
@@ -2098,7 +2239,7 @@ async function verifyX402(args) {
2098
2239
  async function settleX402(args) {
2099
2240
  const { response, payment, token, deps, routeEntry, billedAmount, report } = args;
2100
2241
  const { payload, requirements } = token;
2101
- const override = routeEntry.dynamicPrice ? { amount: billedAmount } : void 0;
2242
+ const override = routeEntry.billing === "exact" ? void 0 : { amount: billedAmount };
2102
2243
  try {
2103
2244
  const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2104
2245
  if (!settle.result?.success) {
@@ -2128,7 +2269,7 @@ async function buildX402ChallengeContribution(args) {
2128
2269
  const accepts = await resolveX402Accepts(
2129
2270
  request,
2130
2271
  routeEntry,
2131
- deps.x402Accepts,
2272
+ selectRouteAccepts(deps.x402Accepts, routeEntry),
2132
2273
  deps.payeeAddress,
2133
2274
  body
2134
2275
  );
@@ -2146,13 +2287,14 @@ async function buildX402ChallengeContribution(args) {
2146
2287
  }
2147
2288
  function reportSettleFailure(report, err, network) {
2148
2289
  const facilitator = err ?? {};
2149
- report("error", "Settlement failed", {
2290
+ const meta = {
2150
2291
  error: err instanceof Error ? err.message : String(err),
2151
2292
  network,
2152
2293
  errorReason: facilitator.errorReason,
2153
2294
  facilitatorStatus: facilitator.response?.status,
2154
2295
  facilitatorBody: facilitator.response?.data ?? facilitator.response?.body
2155
- });
2296
+ };
2297
+ report("error", "Settlement failed", meta);
2156
2298
  }
2157
2299
 
2158
2300
  // src/protocols/index.ts
@@ -2175,6 +2317,7 @@ function getAllowedStrategies(allowed) {
2175
2317
  var import_server4 = require("next/server");
2176
2318
 
2177
2319
  // src/pipeline/challenge-extensions.ts
2320
+ init_evm();
2178
2321
  async function buildChallengeExtensions(ctx) {
2179
2322
  const { routeEntry } = ctx;
2180
2323
  let extensions;
@@ -2219,6 +2362,21 @@ async function buildChallengeExtensions(ctx) {
2219
2362
  } catch {
2220
2363
  }
2221
2364
  }
2365
+ const hasEvmUpto = ctx.routeEntry.billing === "upto" && ctx.deps.x402Accepts.some((accept) => accept.scheme === "upto" && isEvmNetwork(accept.network));
2366
+ if (hasEvmUpto) {
2367
+ try {
2368
+ const { declareEip2612GasSponsoringExtension } = await import("@x402/extensions");
2369
+ extensions = {
2370
+ ...extensions ?? {},
2371
+ ...declareEip2612GasSponsoringExtension()
2372
+ };
2373
+ } catch (err) {
2374
+ ctx.report(
2375
+ "warn",
2376
+ `EIP-2612 gas-sponsoring declaration failed: ${err instanceof Error ? err.message : String(err)}`
2377
+ );
2378
+ }
2379
+ }
2222
2380
  return extensions;
2223
2381
  }
2224
2382
 
@@ -2371,31 +2529,7 @@ async function runDynamicChannelMgmtFlow(args) {
2371
2529
  });
2372
2530
  }
2373
2531
 
2374
- // src/pipeline/flows/dynamic/dynamic-invoke.ts
2375
- var import_server6 = require("next/server");
2376
-
2377
- // src/pricing/atomic.ts
2378
- var USDC_DECIMALS = 6;
2379
- function decimalToAtomic(amount) {
2380
- const m = /^(\d+)(?:\.(\d+))?$/.exec(amount.trim());
2381
- if (!m) {
2382
- throw Object.assign(new Error(`'${amount}' is not a valid decimal-dollar string`), {
2383
- status: 400
2384
- });
2385
- }
2386
- const whole = m[1];
2387
- const fraction = (m[2] ?? "").slice(0, USDC_DECIMALS).padEnd(USDC_DECIMALS, "0");
2388
- return BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0");
2389
- }
2390
- function atomicToDecimal(atomic) {
2391
- const whole = atomic / 10n ** BigInt(USDC_DECIMALS);
2392
- const fraction = atomic % 10n ** BigInt(USDC_DECIMALS);
2393
- if (fraction === 0n) return whole.toString();
2394
- const fractionStr = fraction.toString().padStart(USDC_DECIMALS, "0").replace(/0+$/, "");
2395
- return `${whole}.${fractionStr}`;
2396
- }
2397
-
2398
- // src/pricing/charge-context.ts
2532
+ // src/pricing/metered-charge.ts
2399
2533
  function createChargeContext(args) {
2400
2534
  const { tickCost, maxPrice, route } = args;
2401
2535
  const tickAtomic = decimalToAtomic(tickCost);
@@ -2430,15 +2564,10 @@ function createChargeContext(args) {
2430
2564
  };
2431
2565
  }
2432
2566
 
2433
- // src/pipeline/flows/dynamic/dynamic-invoke.ts
2434
- async function invokeDynamic(ctx, wallet, account, body, payment) {
2435
- const streaming = ctx.routeEntry.streaming === true;
2436
- const chargeContext = streaming ? createChargeContext({
2437
- tickCost: ctx.routeEntry.tickCost,
2438
- maxPrice: ctx.routeEntry.maxPrice,
2439
- route: ctx.routeEntry.key
2440
- }) : null;
2441
- 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 {
2442
2571
  body,
2443
2572
  query: parseQuery(ctx.request, ctx.routeEntry),
2444
2573
  request: ctx.request,
@@ -2450,12 +2579,41 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
2450
2579
  alert: ctx.report,
2451
2580
  setVerifiedWallet: (addr) => ctx.pluginCtx.setVerifiedWallet(addr)
2452
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);
2453
2611
  const handlerCtx = chargeContext !== null ? { ...baseHandlerCtx, charge: chargeContext.charge } : baseHandlerCtx;
2454
2612
  let returned;
2455
2613
  try {
2456
2614
  returned = ctx.handler(handlerCtx);
2457
2615
  } catch (error) {
2458
- return errorResult2(error, chargeContext);
2616
+ return errorResult2(error);
2459
2617
  }
2460
2618
  if (isAsyncIterable2(returned) && !isThenable2(returned)) {
2461
2619
  if (!chargeContext) {
@@ -2463,41 +2621,80 @@ async function invokeDynamic(ctx, wallet, account, body, payment) {
2463
2621
  new HttpError(
2464
2622
  "route returned an async iterable from a non-streaming handler \u2014 use .stream(async function*(...)) instead of .handler() to opt into streaming",
2465
2623
  500
2466
- ),
2467
- null
2624
+ )
2468
2625
  );
2469
2626
  }
2470
- return {
2471
- kind: "stream",
2472
- source: returned,
2473
- chargeContext
2474
- };
2627
+ return { kind: "stream", source: returned, chargeContext };
2475
2628
  }
2476
2629
  let rawResult;
2477
2630
  try {
2478
2631
  rawResult = await returned;
2479
2632
  } catch (error) {
2480
- return errorResult2(error, chargeContext);
2633
+ return errorResult2(error);
2481
2634
  }
2482
- const response = rawResult instanceof Response ? rawResult : import_server6.NextResponse.json(rawResult);
2483
- return { kind: "request", response, rawResult };
2635
+ return { kind: "request", response: toResponse(rawResult), rawResult };
2484
2636
  }
2485
- function errorResult2(error, chargeContext) {
2486
- const status = error instanceof HttpError ? error.status : typeof error?.status === "number" ? error.status : 500;
2487
- const message = error instanceof Error ? error.message : "Internal error";
2488
- 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
+ };
2489
2660
  return {
2490
- kind: "request",
2491
- response: import_server6.NextResponse.json({ success: false, error: message }, { status }),
2492
- rawResult: void 0,
2493
- handlerError: error
2661
+ charge,
2662
+ callCount: () => calls,
2663
+ atomicTotal: () => atomic
2494
2664
  };
2495
2665
  }
2496
- function isAsyncIterable2(value) {
2497
- return value != null && typeof value === "object" && Symbol.asyncIterator in value;
2498
- }
2499
- function isThenable2(value) {
2500
- 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 };
2501
2698
  }
2502
2699
 
2503
2700
  // src/pipeline/flows/dynamic/dynamic-preflight.ts
@@ -2527,7 +2724,7 @@ async function runDynamicRequestFlow(args) {
2527
2724
  }
2528
2725
  const beforeErr = await runBeforeSettle(ctx, settleScope);
2529
2726
  if (beforeErr) return beforeErr;
2530
- const billedAmount = routeEntry.tickCost;
2727
+ const billedAmount = computeBilledAmount(routeEntry, result);
2531
2728
  return settleAndFinalizeRequest({
2532
2729
  ctx,
2533
2730
  strategy,
@@ -2545,6 +2742,19 @@ async function runDynamicRequestFlow(args) {
2545
2742
  }
2546
2743
  });
2547
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
+ }
2548
2758
 
2549
2759
  // src/pipeline/flows/dynamic/dynamic-stream.ts
2550
2760
  async function runDynamicStreamFlow(args) {
@@ -2617,13 +2827,7 @@ async function runDynamicPaidFlow(ctx) {
2617
2827
  amount: price,
2618
2828
  network: verifyOutcome.payment.network
2619
2829
  });
2620
- const result = await invokeDynamic(
2621
- ctx,
2622
- verifyOutcome.wallet,
2623
- account,
2624
- parsedBody,
2625
- verifyOutcome.payment
2626
- );
2830
+ const result = await invokeDynamic(ctx, verifyOutcome, account, parsedBody);
2627
2831
  switch (result.kind) {
2628
2832
  case "stream":
2629
2833
  return runDynamicStreamFlow({
@@ -2645,6 +2849,18 @@ async function runDynamicPaidFlow(ctx) {
2645
2849
  });
2646
2850
  }
2647
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
+ }
2648
2864
 
2649
2865
  // src/pipeline/flows/static/static-body-and-price.ts
2650
2866
  async function resolveStaticBodyAndPrice(args) {
@@ -2792,19 +3008,27 @@ async function runStaticPaidFlow(ctx) {
2792
3008
 
2793
3009
  // src/pipeline/flows/paid.ts
2794
3010
  async function runPaidFlow(ctx) {
2795
- const dynamicPrice = ctx.routeEntry.dynamicPrice ?? false;
2796
- switch (dynamicPrice) {
2797
- case true:
2798
- return runDynamicPaidFlow(ctx);
2799
- case false:
2800
- return runStaticPaidFlow(ctx);
2801
- }
3011
+ const handlerCharged = ctx.routeEntry.billing !== "exact";
3012
+ return handlerCharged ? runDynamicPaidFlow(ctx) : runStaticPaidFlow(ctx);
2802
3013
  }
2803
3014
 
2804
3015
  // src/pipeline/flows/siwx-only.ts
2805
3016
  var import_server7 = require("next/server");
2806
3017
 
2807
3018
  // src/kv-store/client.ts
3019
+ var BIGINT_SUFFIX = "#__bigint";
3020
+ function stringifyValue(value) {
3021
+ return JSON.stringify(
3022
+ value,
3023
+ (_key, v) => typeof v === "bigint" ? `${v.toString()}${BIGINT_SUFFIX}` : v
3024
+ );
3025
+ }
3026
+ function parseValue(raw) {
3027
+ return JSON.parse(
3028
+ raw,
3029
+ (_key, v) => typeof v === "string" && v.endsWith(BIGINT_SUFFIX) ? BigInt(v.slice(0, -BIGINT_SUFFIX.length)) : v
3030
+ );
3031
+ }
2808
3032
  function restKvStore(url, token) {
2809
3033
  const base = url.replace(/\/+$/, "");
2810
3034
  const authHeader = { Authorization: `Bearer ${token}` };
@@ -2826,16 +3050,22 @@ function restKvStore(url, token) {
2826
3050
  const res = await fetch(`${base}/get/${encodeURIComponent(key)}`, { headers: authHeader });
2827
3051
  if (!res.ok) throw new Error(`[kv-store] GET ${key}: ${res.status}`);
2828
3052
  const { result } = await res.json();
2829
- return result ?? null;
3053
+ if (result == null) return null;
3054
+ if (typeof result !== "string") return result;
3055
+ try {
3056
+ return parseValue(result);
3057
+ } catch {
3058
+ return result;
3059
+ }
2830
3060
  }
2831
3061
  async function set(key, value) {
2832
- await exec(["SET", key, JSON.stringify(value)]);
3062
+ await exec(["SET", key, stringifyValue(value)]);
2833
3063
  }
2834
3064
  async function del(key) {
2835
3065
  await exec(["DEL", key]);
2836
3066
  }
2837
3067
  async function setNxEx(key, value, ttlSeconds) {
2838
- const result = await exec(["SET", key, JSON.stringify(value), "EX", ttlSeconds, "NX"]);
3068
+ const result = await exec(["SET", key, stringifyValue(value), "EX", ttlSeconds, "NX"]);
2839
3069
  return result === "OK";
2840
3070
  }
2841
3071
  async function sadd(key, member) {
@@ -3164,142 +3394,148 @@ ${issues}`
3164
3394
  }
3165
3395
 
3166
3396
  // src/builder.ts
3167
- var RouteBuilder = class {
3168
- /** @internal */
3169
- _key;
3170
- /** @internal */
3171
- _registry;
3172
- /** @internal */
3173
- _deps;
3174
- /** @internal */
3175
- _authMode = null;
3176
- /** @internal */
3177
- _pricing;
3178
- /** @internal */
3179
- _siwxEnabled = false;
3180
- /** @internal */
3181
- _protocols = ["x402"];
3182
- /** @internal */
3183
- _maxPrice;
3184
- /** @internal */
3185
- _minPrice;
3186
- /** @internal */
3187
- _dynamicPrice = false;
3188
- /** @internal */
3189
- _tickCost;
3190
- /** @internal */
3191
- _unitType;
3192
- /** @internal */
3193
- _payTo;
3194
- /** @internal */
3195
- _bodySchema;
3196
- /** @internal */
3197
- _querySchema;
3198
- /** @internal */
3199
- _outputSchema;
3200
- /** @internal */
3201
- _inputExample = void 0;
3202
- /** @internal */
3203
- _hasInputExample = false;
3204
- /** @internal */
3205
- _outputExample = void 0;
3206
- /** @internal */
3207
- _hasOutputExample = false;
3208
- /** @internal */
3209
- _description;
3210
- /** @internal */
3211
- _path;
3212
- /** @internal */
3213
- _method = "POST";
3214
- /** @internal */
3215
- _apiKeyResolver;
3216
- /** @internal */
3217
- _providerName;
3218
- /** @internal */
3219
- _providerConfig;
3220
- /** @internal */
3221
- _validateFn;
3222
- /** @internal */
3223
- _settlement;
3224
- /** @internal */
3225
- _mppInfo;
3226
- constructor(key, registry, deps) {
3227
- this._key = key;
3228
- this._registry = registry;
3229
- this._deps = deps;
3397
+ var RouteBuilder = class _RouteBuilder {
3398
+ #s;
3399
+ constructor(key, registry, deps, defaults) {
3400
+ this.#s = {
3401
+ key,
3402
+ registry,
3403
+ deps,
3404
+ authMode: null,
3405
+ pricing: void 0,
3406
+ siwxEnabled: false,
3407
+ protocols: defaults?.protocols ? [...defaults.protocols] : ["x402"],
3408
+ maxPrice: void 0,
3409
+ minPrice: void 0,
3410
+ billing: "exact",
3411
+ tickCost: void 0,
3412
+ unitType: void 0,
3413
+ payTo: void 0,
3414
+ bodySchema: void 0,
3415
+ querySchema: void 0,
3416
+ outputSchema: void 0,
3417
+ inputExample: void 0,
3418
+ hasInputExample: false,
3419
+ outputExample: void 0,
3420
+ hasOutputExample: false,
3421
+ description: void 0,
3422
+ path: void 0,
3423
+ method: "POST",
3424
+ apiKeyResolver: void 0,
3425
+ providerName: void 0,
3426
+ providerConfig: void 0,
3427
+ validateFn: void 0,
3428
+ settlement: void 0,
3429
+ mppInfo: void 0
3430
+ };
3230
3431
  }
3231
3432
  fork() {
3232
- const next = Object.create(Object.getPrototypeOf(this));
3233
- Object.assign(next, this);
3234
- next._protocols = [...this._protocols];
3433
+ const next = new _RouteBuilder(
3434
+ this.#s.key,
3435
+ this.#s.registry,
3436
+ this.#s.deps
3437
+ );
3438
+ next.#s = { ...this.#s, protocols: [...this.#s.protocols] };
3235
3439
  return next;
3236
3440
  }
3237
- paid(pricingOrOptions, options) {
3238
- const { pricing, resolvedOptions } = resolvePaidArgs(this._key, pricingOrOptions, options);
3239
- if (this._authMode === "unprotected") {
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;
3478
+ if (this.#s.authMode === "unprotected") {
3240
3479
  throw new Error(
3241
- `route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
3480
+ `route '${this.#s.key}': Cannot combine .unprotected() and .${method}() on the same route.`
3242
3481
  );
3243
3482
  }
3244
- if (this._pricing !== void 0) {
3483
+ if (this.#s.pricing !== void 0) {
3245
3484
  throw new Error(
3246
- `route '${this._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.`
3247
3486
  );
3248
3487
  }
3249
3488
  const next = this.fork();
3250
- next._authMode = "paid";
3251
- next._pricing = pricing;
3252
- if (resolvedOptions?.protocols) {
3253
- next._protocols = [...resolvedOptions.protocols];
3254
- } else if (next._protocols.length === 0) {
3255
- next._protocols = ["x402"];
3256
- }
3257
- if (resolvedOptions?.maxPrice) next._maxPrice = resolvedOptions.maxPrice;
3258
- if (resolvedOptions?.minPrice) next._minPrice = resolvedOptions.minPrice;
3259
- if (resolvedOptions?.payTo) next._payTo = resolvedOptions.payTo;
3260
- if (resolvedOptions?.mpp) next._mppInfo = resolvedOptions.mpp;
3261
- if (resolvedOptions?.dynamic) next._dynamicPrice = true;
3262
- if (resolvedOptions?.tickCost) next._tickCost = resolvedOptions.tickCost;
3263
- if (resolvedOptions?.unitType) next._unitType = resolvedOptions.unitType;
3264
- if (typeof pricing === "object" && "tiers" in pricing) {
3265
- if (next._dynamicPrice) {
3489
+ next.#s.authMode = "paid";
3490
+ next.#s.pricing = pricing;
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")) {
3266
3500
  throw new Error(
3267
- `route '${this._key}': .paid({ dynamic: true }) is incompatible with tiered pricing`
3501
+ `route '${this.#s.key}': .metered() is MPP-only \u2014 remove the conflicting protocols override.`
3268
3502
  );
3269
3503
  }
3504
+ next.#s.protocols = ["mpp"];
3505
+ } else if (resolvedOptions.protocols) {
3506
+ next.#s.protocols = [...resolvedOptions.protocols];
3507
+ } else if (next.#s.protocols.length === 0) {
3508
+ next.#s.protocols = ["x402"];
3509
+ }
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;
3518
+ if (typeof pricing === "object" && "tiers" in pricing) {
3270
3519
  for (const [tierKey, tierConfig] of Object.entries(pricing.tiers)) {
3271
3520
  if (!tierKey) {
3272
- throw new Error(`route '${this._key}': tier key cannot be empty`);
3521
+ throw new Error(`route '${this.#s.key}': tier key cannot be empty`);
3273
3522
  }
3274
- const tierPrice = parseFloat(tierConfig.price);
3275
- if (isNaN(tierPrice) || tierPrice <= 0) {
3523
+ if (!isPositiveDecimal(tierConfig.price)) {
3276
3524
  throw new Error(
3277
- `route '${this._key}': tier '${tierKey}' price '${tierConfig.price}' must be a positive decimal string`
3525
+ `route '${this.#s.key}': tier '${tierKey}' price '${tierConfig.price}' must be a positive decimal string`
3278
3526
  );
3279
3527
  }
3280
3528
  }
3281
3529
  }
3282
- if (resolvedOptions?.maxPrice !== void 0) {
3283
- const parsed = parseFloat(resolvedOptions.maxPrice);
3284
- if (isNaN(parsed) || parsed <= 0) {
3285
- throw new Error(
3286
- `route '${this._key}': maxPrice '${resolvedOptions.maxPrice}' must be a positive decimal string`
3287
- );
3288
- }
3289
- }
3290
- if (resolvedOptions?.tickCost !== void 0) {
3291
- const parsed = parseFloat(resolvedOptions.tickCost);
3292
- if (isNaN(parsed) || parsed <= 0) {
3293
- throw new Error(
3294
- `route '${this._key}': tickCost '${resolvedOptions.tickCost}' must be a positive decimal string`
3295
- );
3296
- }
3297
- }
3298
- if (next._dynamicPrice && !next._maxPrice) {
3299
- throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires maxPrice`);
3530
+ if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
3531
+ throw new Error(
3532
+ `route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
3533
+ );
3300
3534
  }
3301
- if (next._dynamicPrice && !next._tickCost) {
3302
- throw new Error(`route '${this._key}': .paid({ dynamic: true }) requires tickCost`);
3535
+ if (next.#s.tickCost !== void 0 && !isPositiveDecimal(next.#s.tickCost)) {
3536
+ throw new Error(
3537
+ `route '${this.#s.key}': tickCost '${next.#s.tickCost}' must be a positive decimal string`
3538
+ );
3303
3539
  }
3304
3540
  return next;
3305
3541
  }
@@ -3314,25 +3550,25 @@ var RouteBuilder = class {
3314
3550
  * ```
3315
3551
  */
3316
3552
  siwx() {
3317
- if (this._authMode === "unprotected") {
3553
+ if (this.#s.authMode === "unprotected") {
3318
3554
  throw new Error(
3319
- `route '${this._key}': Cannot combine .unprotected() and .siwx() on the same route.`
3555
+ `route '${this.#s.key}': Cannot combine .unprotected() and .siwx() on the same route.`
3320
3556
  );
3321
3557
  }
3322
- if (this._apiKeyResolver) {
3558
+ if (this.#s.apiKeyResolver) {
3323
3559
  throw new Error(
3324
- `route '${this._key}': Combining .siwx() and .apiKey() is not supported on the same route.`
3560
+ `route '${this.#s.key}': Combining .siwx() and .apiKey() is not supported on the same route.`
3325
3561
  );
3326
3562
  }
3327
3563
  const next = this.fork();
3328
- next._siwxEnabled = true;
3329
- if (next._authMode === "paid" || next._pricing) {
3330
- next._authMode = "paid";
3331
- if (next._protocols.length === 0) next._protocols = ["x402"];
3564
+ next.#s.siwxEnabled = true;
3565
+ if (next.#s.authMode === "paid" || next.#s.pricing) {
3566
+ next.#s.authMode = "paid";
3567
+ if (next.#s.protocols.length === 0) next.#s.protocols = ["x402"];
3332
3568
  return next;
3333
3569
  }
3334
- next._authMode = "siwx";
3335
- next._protocols = [];
3570
+ next.#s.authMode = "siwx";
3571
+ next.#s.protocols = [];
3336
3572
  return next;
3337
3573
  }
3338
3574
  /**
@@ -3349,14 +3585,14 @@ var RouteBuilder = class {
3349
3585
  * ```
3350
3586
  */
3351
3587
  apiKey(resolver) {
3352
- if (this._siwxEnabled) {
3588
+ if (this.#s.siwxEnabled) {
3353
3589
  throw new Error(
3354
- `route '${this._key}': Combining .apiKey() and .siwx() is not supported on the same route.`
3590
+ `route '${this.#s.key}': Combining .apiKey() and .siwx() is not supported on the same route.`
3355
3591
  );
3356
3592
  }
3357
3593
  const next = this.fork();
3358
- next._authMode = "apiKey";
3359
- next._apiKeyResolver = resolver;
3594
+ next.#s.authMode = "apiKey";
3595
+ next.#s.apiKeyResolver = resolver;
3360
3596
  return next;
3361
3597
  }
3362
3598
  /**
@@ -3369,19 +3605,19 @@ var RouteBuilder = class {
3369
3605
  * ```
3370
3606
  */
3371
3607
  unprotected() {
3372
- if (this._authMode && this._authMode !== "unprotected") {
3608
+ if (this.#s.authMode && this.#s.authMode !== "unprotected") {
3373
3609
  throw new Error(
3374
- `route '${this._key}': Cannot combine .unprotected() and .${this._authMode}() on the same route.`
3610
+ `route '${this.#s.key}': Cannot combine .unprotected() and .${this.#s.authMode}() on the same route.`
3375
3611
  );
3376
3612
  }
3377
- if (this._pricing) {
3613
+ if (this.#s.pricing) {
3378
3614
  throw new Error(
3379
- `route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
3615
+ `route '${this.#s.key}': Cannot combine .unprotected() and .paid() on the same route.`
3380
3616
  );
3381
3617
  }
3382
3618
  const next = this.fork();
3383
- next._authMode = "unprotected";
3384
- next._protocols = [];
3619
+ next.#s.authMode = "unprotected";
3620
+ next.#s.protocols = [];
3385
3621
  return next;
3386
3622
  }
3387
3623
  /**
@@ -3400,8 +3636,8 @@ var RouteBuilder = class {
3400
3636
  */
3401
3637
  provider(name, config) {
3402
3638
  const next = this.fork();
3403
- next._providerName = name;
3404
- next._providerConfig = config ?? {};
3639
+ next.#s.providerName = name;
3640
+ next.#s.providerConfig = config ?? {};
3405
3641
  return next;
3406
3642
  }
3407
3643
  /**
@@ -3416,7 +3652,7 @@ var RouteBuilder = class {
3416
3652
  */
3417
3653
  body(schema) {
3418
3654
  const next = this.fork();
3419
- next._bodySchema = schema;
3655
+ next.#s.bodySchema = schema;
3420
3656
  return next;
3421
3657
  }
3422
3658
  /**
@@ -3432,8 +3668,8 @@ var RouteBuilder = class {
3432
3668
  */
3433
3669
  query(schema) {
3434
3670
  const next = this.fork();
3435
- next._querySchema = schema;
3436
- next._method = "GET";
3671
+ next.#s.querySchema = schema;
3672
+ next.#s.method = "GET";
3437
3673
  return next;
3438
3674
  }
3439
3675
  /**
@@ -3450,7 +3686,7 @@ var RouteBuilder = class {
3450
3686
  */
3451
3687
  output(schema) {
3452
3688
  const next = this.fork();
3453
- next._outputSchema = schema;
3689
+ next.#s.outputSchema = schema;
3454
3690
  return next;
3455
3691
  }
3456
3692
  /**
@@ -3464,8 +3700,8 @@ var RouteBuilder = class {
3464
3700
  */
3465
3701
  inputExample(example) {
3466
3702
  const next = this.fork();
3467
- next._inputExample = example;
3468
- next._hasInputExample = true;
3703
+ next.#s.inputExample = example;
3704
+ next.#s.hasInputExample = true;
3469
3705
  return next;
3470
3706
  }
3471
3707
  /**
@@ -3479,8 +3715,8 @@ var RouteBuilder = class {
3479
3715
  */
3480
3716
  outputExample(example) {
3481
3717
  const next = this.fork();
3482
- next._outputExample = example;
3483
- next._hasOutputExample = true;
3718
+ next.#s.outputExample = example;
3719
+ next.#s.hasOutputExample = true;
3484
3720
  return next;
3485
3721
  }
3486
3722
  /**
@@ -3494,7 +3730,7 @@ var RouteBuilder = class {
3494
3730
  */
3495
3731
  description(text) {
3496
3732
  const next = this.fork();
3497
- next._description = text;
3733
+ next.#s.description = text;
3498
3734
  return next;
3499
3735
  }
3500
3736
  /**
@@ -3508,7 +3744,7 @@ var RouteBuilder = class {
3508
3744
  */
3509
3745
  path(p) {
3510
3746
  const next = this.fork();
3511
- next._path = p;
3747
+ next.#s.path = p;
3512
3748
  return next;
3513
3749
  }
3514
3750
  /**
@@ -3522,7 +3758,7 @@ var RouteBuilder = class {
3522
3758
  */
3523
3759
  method(m) {
3524
3760
  const next = this.fork();
3525
- next._method = m;
3761
+ next.#s.method = m;
3526
3762
  return next;
3527
3763
  }
3528
3764
  /**
@@ -3541,7 +3777,7 @@ var RouteBuilder = class {
3541
3777
  */
3542
3778
  validate(fn) {
3543
3779
  const next = this.fork();
3544
- next._validateFn = fn;
3780
+ next.#s.validateFn = fn;
3545
3781
  return next;
3546
3782
  }
3547
3783
  /**
@@ -3559,7 +3795,7 @@ var RouteBuilder = class {
3559
3795
  */
3560
3796
  settlement(lifecycle) {
3561
3797
  const next = this.fork();
3562
- next._settlement = lifecycle;
3798
+ next.#s.settlement = lifecycle;
3563
3799
  return next;
3564
3800
  }
3565
3801
  /**
@@ -3582,13 +3818,13 @@ var RouteBuilder = class {
3582
3818
  /**
3583
3819
  * Register a streaming handler (`async function*`) and return the Next.js
3584
3820
  * route function. Each `charge()` call bills one tick (`tickCost` USDC) up
3585
- * to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
3821
+ * to `maxPrice`; requires `.metered({ ... })` and MPP session mode.
3586
3822
  *
3587
3823
  * @example
3588
3824
  * ```ts
3589
3825
  * export const POST = router
3590
3826
  * .route('llm/stream')
3591
- * .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
3827
+ * .metered({ tickCost: '0.0001', maxPrice: '0.05', unitType: 'token' })
3592
3828
  * .body(schema)
3593
3829
  * .stream(async function* ({ body, charge }) {
3594
3830
  * for await (const token of streamLLM(body.prompt)) {
@@ -3602,91 +3838,144 @@ var RouteBuilder = class {
3602
3838
  return this.register(fn, true);
3603
3839
  }
3604
3840
  register(handlerFn, streaming) {
3605
- if (!this._authMode) {
3841
+ if (!this.#s.authMode) {
3606
3842
  throw new Error(
3607
- `route '${this._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()`
3608
3844
  );
3609
3845
  }
3610
- if (this._validateFn && !this._bodySchema) {
3846
+ if (this.#s.validateFn && !this.#s.bodySchema) {
3611
3847
  throw new Error(
3612
- `route '${this._key}': .validate() requires .body() \u2014 validation runs on parsed body`
3848
+ `route '${this.#s.key}': .validate() requires .body() \u2014 validation runs on parsed body`
3613
3849
  );
3614
3850
  }
3615
- if (this._settlement && !this._pricing) {
3616
- throw new Error(`route '${this._key}': .settlement() requires a paid route`);
3851
+ if (this.#s.settlement && !this.#s.pricing) {
3852
+ throw new Error(`route '${this.#s.key}': .settlement() requires a paid route`);
3617
3853
  }
3618
- if (this._dynamicPrice && this._protocols.includes("x402")) {
3619
- const hasUpto = this._deps.x402Accepts.some((accept) => accept.scheme === "upto");
3854
+ if (this.#s.billing === "upto") {
3855
+ const hasUpto = this.#s.deps.x402Accepts.some((accept) => accept.scheme === "upto");
3620
3856
  if (!hasUpto) {
3621
3857
  throw new Error(
3622
- `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.`
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.`
3623
3869
  );
3624
3870
  }
3625
3871
  }
3626
- if (this._dynamicPrice && this._protocols.includes("mpp")) {
3627
- if (!this._deps.mppSessionConfig) {
3872
+ if (this.#s.billing === "metered") {
3873
+ if (!this.#s.deps.mppSessionConfig) {
3628
3874
  throw new Error(
3629
- `route '${this._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.`
3630
3876
  );
3631
3877
  }
3632
3878
  }
3633
- if (streaming && !this._dynamicPrice) {
3879
+ if (streaming && this.#s.billing !== "metered") {
3634
3880
  throw new Error(
3635
- `route '${this._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.`
3636
3882
  );
3637
3883
  }
3638
3884
  validateExamples(
3639
- this._key,
3640
- this._bodySchema,
3641
- this._querySchema,
3642
- this._outputSchema,
3643
- this._inputExample,
3644
- this._hasInputExample,
3645
- this._outputExample,
3646
- this._hasOutputExample
3885
+ this.#s.key,
3886
+ this.#s.bodySchema,
3887
+ this.#s.querySchema,
3888
+ this.#s.outputSchema,
3889
+ this.#s.inputExample,
3890
+ this.#s.hasInputExample,
3891
+ this.#s.outputExample,
3892
+ this.#s.hasOutputExample
3647
3893
  );
3648
3894
  const entry = {
3649
- key: this._key,
3650
- authMode: this._authMode,
3651
- siwxEnabled: this._siwxEnabled,
3652
- pricing: this._pricing,
3653
- dynamicPrice: this._dynamicPrice ? true : void 0,
3895
+ key: this.#s.key,
3896
+ authMode: this.#s.authMode,
3897
+ siwxEnabled: this.#s.siwxEnabled,
3898
+ pricing: this.#s.pricing,
3899
+ billing: this.#s.billing,
3654
3900
  streaming: streaming ? true : void 0,
3655
- protocols: this._protocols,
3656
- bodySchema: this._bodySchema,
3657
- querySchema: this._querySchema,
3658
- outputSchema: this._outputSchema,
3659
- inputExample: this._hasInputExample ? this._inputExample : void 0,
3660
- outputExample: this._hasOutputExample ? this._outputExample : void 0,
3661
- description: this._description,
3662
- path: this._path,
3663
- method: this._method,
3664
- maxPrice: this._maxPrice,
3665
- minPrice: this._minPrice,
3666
- payTo: this._payTo,
3667
- apiKeyResolver: this._apiKeyResolver,
3668
- providerName: this._providerName,
3669
- providerConfig: this._providerConfig,
3670
- validateFn: this._validateFn,
3671
- settlement: this._settlement,
3672
- mppInfo: this._mppInfo,
3673
- tickCost: this._tickCost,
3674
- unitType: this._unitType
3901
+ protocols: this.#s.protocols,
3902
+ bodySchema: this.#s.bodySchema,
3903
+ querySchema: this.#s.querySchema,
3904
+ outputSchema: this.#s.outputSchema,
3905
+ inputExample: this.#s.hasInputExample ? this.#s.inputExample : void 0,
3906
+ outputExample: this.#s.hasOutputExample ? this.#s.outputExample : void 0,
3907
+ description: this.#s.description,
3908
+ path: this.#s.path,
3909
+ method: this.#s.method,
3910
+ maxPrice: this.#s.maxPrice,
3911
+ minPrice: this.#s.minPrice,
3912
+ payTo: this.#s.payTo,
3913
+ apiKeyResolver: this.#s.apiKeyResolver,
3914
+ providerName: this.#s.providerName,
3915
+ providerConfig: this.#s.providerConfig,
3916
+ validateFn: this.#s.validateFn,
3917
+ settlement: this.#s.settlement,
3918
+ mppInfo: this.#s.mppInfo,
3919
+ tickCost: this.#s.tickCost,
3920
+ unitType: this.#s.unitType
3675
3921
  };
3676
- this._registry.register(entry);
3677
- return createRequestHandler(entry, handlerFn, this._deps);
3922
+ this.#s.registry.register(entry);
3923
+ return createRequestHandler(entry, handlerFn, this.#s.deps);
3678
3924
  }
3679
3925
  };
3680
- function resolvePaidArgs(routeKey, pricingOrOptions, options) {
3681
- const isHandlerDynamicShape = typeof pricingOrOptions === "object" && pricingOrOptions !== null && typeof pricingOrOptions !== "function" && !("tiers" in pricingOrOptions) && "dynamic" in pricingOrOptions && pricingOrOptions.dynamic;
3682
- if (isHandlerDynamicShape) {
3683
- const opts = pricingOrOptions;
3684
- if (!opts.maxPrice) {
3685
- throw new Error(`route '${routeKey}': .paid({ dynamic: true }) requires maxPrice`);
3686
- }
3687
- 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
+ };
3936
+ }
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`);
3688
3970
  }
3689
- return { pricing: pricingOrOptions, resolvedOptions: options };
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
+ };
3690
3979
  }
3691
3980
 
3692
3981
  // src/discovery/well-known.ts
@@ -3918,17 +4207,16 @@ function buildPricingInfo(entry) {
3918
4207
  };
3919
4208
  }
3920
4209
  if ("tiers" in entry.pricing) {
3921
- const tierPrices = Object.values(entry.pricing.tiers).map((tier) => parseFloat(tier.price));
3922
- const min = Math.min(...tierPrices);
3923
- const max = Math.max(...tierPrices);
3924
- if (Number.isFinite(min) && Number.isFinite(max)) {
3925
- if (min === max) {
4210
+ const tierPrices = Object.values(entry.pricing.tiers).map((tier) => tier.price);
4211
+ const extrema = tierExtrema(tierPrices);
4212
+ if (extrema) {
4213
+ if (extrema.min === extrema.max) {
3926
4214
  return {
3927
- price: { mode: "fixed", currency: "USD", amount: String(min) }
4215
+ price: { mode: "fixed", currency: "USD", amount: extrema.min }
3928
4216
  };
3929
4217
  }
3930
4218
  return {
3931
- price: { mode: "dynamic", currency: "USD", min: String(min), max: String(max) }
4219
+ price: { mode: "dynamic", currency: "USD", min: extrema.min, max: extrema.max }
3932
4220
  };
3933
4221
  }
3934
4222
  return {
@@ -3942,6 +4230,20 @@ function buildPricingInfo(entry) {
3942
4230
  }
3943
4231
  return void 0;
3944
4232
  }
4233
+ function tierExtrema(prices) {
4234
+ if (prices.length === 0) return null;
4235
+ let min = prices[0];
4236
+ let max = prices[0];
4237
+ try {
4238
+ for (const price of prices.slice(1)) {
4239
+ if (compareDecimals(price, min) < 0) min = price;
4240
+ if (compareDecimals(price, max) > 0) max = price;
4241
+ }
4242
+ } catch {
4243
+ return null;
4244
+ }
4245
+ return { min, max };
4246
+ }
3945
4247
 
3946
4248
  // src/discovery/llms-txt.ts
3947
4249
  var import_server10 = require("next/server");
@@ -4428,11 +4730,11 @@ function getRouterConfigIssues(config, options = {}) {
4428
4730
  }
4429
4731
 
4430
4732
  // src/init/x402.ts
4431
- async function initX402(config, configError) {
4733
+ async function initX402(config, kvStore, configError) {
4432
4734
  if (configError) return { initError: configError };
4433
4735
  try {
4434
4736
  const { createX402Server: createX402Server2 } = await Promise.resolve().then(() => (init_x402_server(), x402_server_exports));
4435
- const result = await createX402Server2(config);
4737
+ const result = await createX402Server2(config, kvStore);
4436
4738
  await result.initPromise;
4437
4739
  return {
4438
4740
  server: result.server,
@@ -4612,10 +4914,10 @@ function createRouter(config) {
4612
4914
  x402Accepts,
4613
4915
  mppx: null,
4614
4916
  tempoClient: null,
4615
- 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
4616
4918
  };
4617
4919
  deps.initPromise = (async () => {
4618
- const x402Result = await initX402(config, x402ConfigError);
4920
+ const x402Result = await initX402(config, kvStore, x402ConfigError);
4619
4921
  deps.x402Server = x402Result.server ?? null;
4620
4922
  deps.x402FacilitatorsByNetwork = x402Result.facilitatorsByNetwork;
4621
4923
  if (x402Result.initError) deps.x402InitError = x402Result.initError;
@@ -4644,11 +4946,10 @@ function createRouter(config) {
4644
4946
  `[router] strictRoutes=true forbids key/path divergence for route '${definition.path}'. Remove custom \`key\` or make it equal to \`path\`.`
4645
4947
  );
4646
4948
  }
4647
- let builder = new RouteBuilder(key, registry, deps);
4949
+ let builder = new RouteBuilder(key, registry, deps, {
4950
+ protocols: config.protocols
4951
+ });
4648
4952
  builder = builder.path(normalizedPath);
4649
- if (config.protocols) {
4650
- builder._protocols = [...config.protocols];
4651
- }
4652
4953
  if (definition.method) {
4653
4954
  builder = builder.method(definition.method);
4654
4955
  }