@agentcash/router 1.9.3 → 1.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -163,6 +163,20 @@ router.route({ path: 'gated' })
163
163
  .handler(fn);
164
164
  ```
165
165
 
166
+ ### `.siwx()` on paid routes. Pay once, identity-gated replays
167
+
168
+ `.paid()` and `.upTo()` compose with `.siwx()` for a pay-once-then-replay-for-free model. The first request settles normally (x402 payment); on success the wallet is recorded in the entitlement KV. Subsequent requests that present a valid SIWX signature for that wallet skip payment and run the handler directly. On `.upTo()` routes, `charge(amount)` becomes a no-op on the SIWX replay path. The handler can continue to call the route unconditionally.
169
+
170
+ ```typescript
171
+ router.route({ path: 'inbox' })
172
+ .paid('0.01').siwx() // first call pays $0.01, later calls present a SIWX sig instead
173
+ .handler(async ({ wallet }) => getInbox(wallet));
174
+ ```
175
+
176
+ `.metered()` is mutually exclusive with `.siwx()` — per-tick MPP billing has no entitlement model — and the builder throws at registration if you combine them.
177
+
178
+ > **Gotcha:** serverless / multi-instance deployments must provide a real `kvStore` (Upstash / Vercel KV). Without one the entitlement is kept in a per-process `Map`, so a wallet that paid on instance A is treated as unpaid on instance B and the user gets charged again.
179
+
166
180
  ## Pricing
167
181
 
168
182
  `.paid()`, `.upTo()`, and `.metered()` are mutually exclusive pricing modes: pick one per route.
package/dist/index.cjs CHANGED
@@ -808,7 +808,11 @@ function invokePaidStatic(ctx, wallet, account, body, payment) {
808
808
  return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
809
809
  }
810
810
  function invokeUnauthed(ctx, wallet, account, body) {
811
- return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, null));
811
+ const base = buildHandlerCtx(ctx, wallet, account, body, null);
812
+ if (ctx.routeEntry.billing !== "upto") return runHandler(ctx, base);
813
+ const uptoCtx = { ...base, charge: async () => {
814
+ } };
815
+ return runHandler(ctx, uptoCtx);
812
816
  }
813
817
  function buildHandlerCtx(ctx, wallet, account, body, payment) {
814
818
  return {
@@ -1137,7 +1141,7 @@ async function resolveEarlyBody(args) {
1137
1141
  }
1138
1142
  const earlyClone = ctx.request.clone();
1139
1143
  const earlyResult = await parseBody(ctx, earlyClone);
1140
- if (!earlyResult.ok) return { ok: false, response: earlyResult.response };
1144
+ if (!earlyResult.ok) return { ok: true, earlyBody: void 0 };
1141
1145
  const validateErr = await runValidate(ctx, earlyResult.data);
1142
1146
  if (validateErr) return { ok: false, response: validateErr };
1143
1147
  return { ok: true, earlyBody: earlyResult.data };
@@ -1189,17 +1193,6 @@ function protocolInitError(routeEntry, deps) {
1189
1193
  return `Payment protocol initialization failed. ${errors.join("; ")}`;
1190
1194
  }
1191
1195
 
1192
- // src/pipeline/flows/api-key-only.ts
1193
- async function runApiKeyOnlyFlow(ctx) {
1194
- if (!ctx.routeEntry.apiKeyResolver) {
1195
- return fail(ctx, 401, "API key resolver not configured");
1196
- }
1197
- const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
1198
- if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
1199
- fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
1200
- return runHandlerOnly(ctx, null, result.account);
1201
- }
1202
-
1203
1196
  // src/pricing/format.ts
1204
1197
  var USDC_DECIMALS = 6;
1205
1198
  var DECIMAL_RE = /^(\d+)(?:\.(\d+))?$/;
@@ -1255,169 +1248,6 @@ function multiplyDecimal(decimal, factor) {
1255
1248
  return fracPart ? `${intPart}.${fracPart}` : intPart;
1256
1249
  }
1257
1250
 
1258
- // src/pricing/dynamic.ts
1259
- var DynamicPricing = class {
1260
- constructor(opts) {
1261
- this.opts = opts;
1262
- }
1263
- needsBody = true;
1264
- async quote(body) {
1265
- let priced;
1266
- try {
1267
- const raw = await this.opts.fn(body);
1268
- priced = this.cap(raw, body);
1269
- } catch (err) {
1270
- if (err instanceof HttpError) throw err;
1271
- this.alert("error", `Pricing function failed: ${msg(err)}`, {
1272
- error: err instanceof Error ? err.stack : String(err),
1273
- body
1274
- });
1275
- if (this.opts.maxPrice) {
1276
- this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
1277
- return this.opts.maxPrice;
1278
- }
1279
- throw err;
1280
- }
1281
- if (!isPositiveDecimal(priced)) {
1282
- throw new HttpError(
1283
- `route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
1284
- 500
1285
- );
1286
- }
1287
- return priced;
1288
- }
1289
- challengeQuote(body) {
1290
- if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
1291
- return this.quote(body);
1292
- }
1293
- describe() {
1294
- return {
1295
- mode: "dynamic",
1296
- min: this.opts.minPrice ?? "0",
1297
- max: this.opts.maxPrice ?? "0"
1298
- };
1299
- }
1300
- cap(raw, body) {
1301
- if (!this.opts.maxPrice) return raw;
1302
- let overCap;
1303
- try {
1304
- overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
1305
- } catch {
1306
- overCap = true;
1307
- }
1308
- if (overCap) {
1309
- this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
1310
- calculated: raw,
1311
- maxPrice: this.opts.maxPrice,
1312
- body
1313
- });
1314
- return this.opts.maxPrice;
1315
- }
1316
- return raw;
1317
- }
1318
- alert(level, message, meta) {
1319
- this.opts.alert?.(level, message, meta);
1320
- }
1321
- };
1322
- function msg(err) {
1323
- return err instanceof Error ? err.message : String(err);
1324
- }
1325
-
1326
- // src/pricing/fixed.ts
1327
- var FixedPricing = class {
1328
- constructor(price) {
1329
- this.price = price;
1330
- }
1331
- needsBody = false;
1332
- quote() {
1333
- return Promise.resolve(this.price);
1334
- }
1335
- challengeQuote() {
1336
- return Promise.resolve(this.price);
1337
- }
1338
- describe() {
1339
- return { mode: "fixed", amount: this.price };
1340
- }
1341
- };
1342
-
1343
- // src/pricing/tiered.ts
1344
- var TieredPricing = class {
1345
- constructor(opts) {
1346
- this.opts = opts;
1347
- }
1348
- needsBody = true;
1349
- async quote(body) {
1350
- const { field, tiers, default: defaultTier } = this.opts;
1351
- const tierKey = body != null ? String(body[field] ?? "") : "";
1352
- if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
1353
- if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
1354
- if (!tierKey) {
1355
- throw httpError(400, `Missing required field '${field}' for tier pricing`);
1356
- }
1357
- throw httpError(
1358
- 400,
1359
- `Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
1360
- );
1361
- }
1362
- async challengeQuote(body) {
1363
- if (body !== void 0) {
1364
- try {
1365
- return await this.quote(body);
1366
- } catch {
1367
- }
1368
- }
1369
- return this.maxTierPrice();
1370
- }
1371
- describe() {
1372
- return {
1373
- mode: "tiered",
1374
- tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
1375
- key,
1376
- price: tier.price,
1377
- ...tier.label !== void 0 ? { label: tier.label } : {}
1378
- })),
1379
- ...this.opts.default !== void 0 ? { default: this.opts.default } : {}
1380
- };
1381
- }
1382
- maxTierPrice() {
1383
- let max = "0";
1384
- for (const tier of Object.values(this.opts.tiers)) {
1385
- if (compareDecimals(tier.price, max) > 0) max = tier.price;
1386
- }
1387
- return max;
1388
- }
1389
- };
1390
- function httpError(status, message) {
1391
- return Object.assign(new Error(message), { status });
1392
- }
1393
-
1394
- // src/pricing/index.ts
1395
- function selectPricing(raw, deps = {}) {
1396
- if (raw == null) return null;
1397
- if (typeof raw === "string") {
1398
- return new FixedPricing(raw);
1399
- }
1400
- if (typeof raw === "function") {
1401
- return new DynamicPricing({
1402
- fn: raw,
1403
- maxPrice: deps.maxPrice,
1404
- minPrice: deps.minPrice,
1405
- route: deps.route,
1406
- alert: deps.alert
1407
- });
1408
- }
1409
- if (typeof raw === "object" && "tiers" in raw) {
1410
- return new TieredPricing({
1411
- field: raw.field,
1412
- tiers: raw.tiers,
1413
- default: raw.default,
1414
- maxPrice: deps.maxPrice,
1415
- minPrice: deps.minPrice
1416
- });
1417
- }
1418
- throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
1419
- }
1420
-
1421
1251
  // src/protocols/mpp/credential.ts
1422
1252
  var import_mppx = require("mppx");
1423
1253
  var import_viem = require("viem");
@@ -2105,7 +1935,7 @@ function tagBareDecimalAsDollars(amount) {
2105
1935
  }
2106
1936
 
2107
1937
  // src/protocols/x402/verify.ts
2108
- var import_types3 = require("@x402/core/types");
1938
+ var import_types2 = require("@x402/core/types");
2109
1939
  async function verifyX402Payment(opts) {
2110
1940
  const { server, request, price, accepts, report } = opts;
2111
1941
  const payload = await readPaymentPayload(request);
@@ -2124,7 +1954,7 @@ async function verifyX402Payment(opts) {
2124
1954
  try {
2125
1955
  verify = await server.verifyPayment(payload, matching);
2126
1956
  } catch (err) {
2127
- if (err instanceof import_types3.VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
1957
+ if (err instanceof import_types2.VerifyError && err.statusCode >= 400 && err.statusCode < 500) {
2128
1958
  return invalidPaymentVerification({
2129
1959
  reason: err.invalidReason ?? "verify_error",
2130
1960
  ...err.invalidMessage ? { message: err.invalidMessage } : {},
@@ -2347,6 +2177,180 @@ function getAllowedStrategies(allowed) {
2347
2177
  return allowed.map((name) => STRATEGIES[name]);
2348
2178
  }
2349
2179
 
2180
+ // src/pipeline/flows/api-key-only.ts
2181
+ async function runApiKeyOnlyFlow(ctx) {
2182
+ if (!ctx.routeEntry.apiKeyResolver) {
2183
+ return fail(ctx, 401, "API key resolver not configured");
2184
+ }
2185
+ const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
2186
+ if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
2187
+ fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
2188
+ return runHandlerOnly(ctx, null, result.account);
2189
+ }
2190
+
2191
+ // src/pricing/dynamic.ts
2192
+ var DynamicPricing = class {
2193
+ constructor(opts) {
2194
+ this.opts = opts;
2195
+ }
2196
+ needsBody = true;
2197
+ async quote(body) {
2198
+ let priced;
2199
+ try {
2200
+ const raw = await this.opts.fn(body);
2201
+ priced = this.cap(raw, body);
2202
+ } catch (err) {
2203
+ if (err instanceof HttpError) throw err;
2204
+ this.alert("error", `Pricing function failed: ${msg(err)}`, {
2205
+ error: err instanceof Error ? err.stack : String(err),
2206
+ body
2207
+ });
2208
+ if (this.opts.maxPrice) {
2209
+ this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
2210
+ return this.opts.maxPrice;
2211
+ }
2212
+ throw err;
2213
+ }
2214
+ if (!isPositiveDecimal(priced)) {
2215
+ throw new HttpError(
2216
+ `route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
2217
+ 500
2218
+ );
2219
+ }
2220
+ return priced;
2221
+ }
2222
+ challengeQuote(body) {
2223
+ if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
2224
+ return this.quote(body);
2225
+ }
2226
+ describe() {
2227
+ return {
2228
+ mode: "dynamic",
2229
+ min: this.opts.minPrice ?? "0",
2230
+ max: this.opts.maxPrice ?? "0"
2231
+ };
2232
+ }
2233
+ cap(raw, body) {
2234
+ if (!this.opts.maxPrice) return raw;
2235
+ let overCap;
2236
+ try {
2237
+ overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
2238
+ } catch {
2239
+ overCap = true;
2240
+ }
2241
+ if (overCap) {
2242
+ this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
2243
+ calculated: raw,
2244
+ maxPrice: this.opts.maxPrice,
2245
+ body
2246
+ });
2247
+ return this.opts.maxPrice;
2248
+ }
2249
+ return raw;
2250
+ }
2251
+ alert(level, message, meta) {
2252
+ this.opts.alert?.(level, message, meta);
2253
+ }
2254
+ };
2255
+ function msg(err) {
2256
+ return err instanceof Error ? err.message : String(err);
2257
+ }
2258
+
2259
+ // src/pricing/fixed.ts
2260
+ var FixedPricing = class {
2261
+ constructor(price) {
2262
+ this.price = price;
2263
+ }
2264
+ needsBody = false;
2265
+ quote() {
2266
+ return Promise.resolve(this.price);
2267
+ }
2268
+ challengeQuote() {
2269
+ return Promise.resolve(this.price);
2270
+ }
2271
+ describe() {
2272
+ return { mode: "fixed", amount: this.price };
2273
+ }
2274
+ };
2275
+
2276
+ // src/pricing/tiered.ts
2277
+ var TieredPricing = class {
2278
+ constructor(opts) {
2279
+ this.opts = opts;
2280
+ }
2281
+ needsBody = true;
2282
+ async quote(body) {
2283
+ const { field, tiers, default: defaultTier } = this.opts;
2284
+ const tierKey = body != null ? String(body[field] ?? "") : "";
2285
+ if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
2286
+ if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
2287
+ if (!tierKey) {
2288
+ throw httpError(400, `Missing required field '${field}' for tier pricing`);
2289
+ }
2290
+ throw httpError(
2291
+ 400,
2292
+ `Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
2293
+ );
2294
+ }
2295
+ async challengeQuote(body) {
2296
+ if (body !== void 0) {
2297
+ try {
2298
+ return await this.quote(body);
2299
+ } catch {
2300
+ }
2301
+ }
2302
+ return this.maxTierPrice();
2303
+ }
2304
+ describe() {
2305
+ return {
2306
+ mode: "tiered",
2307
+ tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
2308
+ key,
2309
+ price: tier.price,
2310
+ ...tier.label !== void 0 ? { label: tier.label } : {}
2311
+ })),
2312
+ ...this.opts.default !== void 0 ? { default: this.opts.default } : {}
2313
+ };
2314
+ }
2315
+ maxTierPrice() {
2316
+ let max = "0";
2317
+ for (const tier of Object.values(this.opts.tiers)) {
2318
+ if (compareDecimals(tier.price, max) > 0) max = tier.price;
2319
+ }
2320
+ return max;
2321
+ }
2322
+ };
2323
+ function httpError(status, message) {
2324
+ return Object.assign(new Error(message), { status });
2325
+ }
2326
+
2327
+ // src/pricing/index.ts
2328
+ function selectPricing(raw, deps = {}) {
2329
+ if (raw == null) return null;
2330
+ if (typeof raw === "string") {
2331
+ return new FixedPricing(raw);
2332
+ }
2333
+ if (typeof raw === "function") {
2334
+ return new DynamicPricing({
2335
+ fn: raw,
2336
+ maxPrice: deps.maxPrice,
2337
+ minPrice: deps.minPrice,
2338
+ route: deps.route,
2339
+ alert: deps.alert
2340
+ });
2341
+ }
2342
+ if (typeof raw === "object" && "tiers" in raw) {
2343
+ return new TieredPricing({
2344
+ field: raw.field,
2345
+ tiers: raw.tiers,
2346
+ default: raw.default,
2347
+ maxPrice: deps.maxPrice,
2348
+ minPrice: deps.minPrice
2349
+ });
2350
+ }
2351
+ throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
2352
+ }
2353
+
2350
2354
  // src/pipeline/flows/challenge-response.ts
2351
2355
  var import_server5 = require("next/server");
2352
2356
 
@@ -3381,13 +3385,20 @@ async function runUnprotectedFlow(ctx) {
3381
3385
  }
3382
3386
 
3383
3387
  // src/pipeline/orchestrate.ts
3388
+ function shouldSkipQueryValidation(routeEntry, request) {
3389
+ const isPaidRoute = !!routeEntry.pricing || routeEntry.authMode === "paid";
3390
+ if (!isPaidRoute) return false;
3391
+ return selectIncomingStrategy(request, routeEntry.protocols) === null;
3392
+ }
3384
3393
  function createRequestHandler(routeEntry, handler, deps) {
3385
3394
  return async (request) => {
3386
3395
  await deps.initPromise;
3387
3396
  const ctx = preflight(routeEntry, handler, deps, request);
3388
- const query = validateQuery(ctx);
3389
- if (!query.ok) return query.response;
3390
- ctx.query = query.data;
3397
+ if (!shouldSkipQueryValidation(routeEntry, request)) {
3398
+ const query = validateQuery(ctx);
3399
+ if (!query.ok) return query.response;
3400
+ ctx.query = query.data;
3401
+ }
3391
3402
  if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
3392
3403
  if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
3393
3404
  if (routeEntry.pricing) return runPaidFlow(ctx);
@@ -3519,6 +3530,11 @@ var RouteBuilder = class _RouteBuilder {
3519
3530
  `route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
3520
3531
  );
3521
3532
  }
3533
+ if (this.#s.siwxEnabled && billing === "metered") {
3534
+ throw new Error(
3535
+ `route '${this.#s.key}': Cannot combine .siwx() and .metered() \u2014 per-tick MPP billing has no entitlement model. Use .paid() or .upTo() with .siwx(), or drop .siwx() for metered routes.`
3536
+ );
3537
+ }
3522
3538
  const next = this.fork();
3523
3539
  next.#s.authMode = "paid";
3524
3540
  next.#s.pricing = pricing;
@@ -3585,12 +3601,16 @@ var RouteBuilder = class _RouteBuilder {
3585
3601
  }
3586
3602
  /**
3587
3603
  * Require Sign-In-with-X wallet identity on this route — clients prove
3588
- * control of a wallet via a signed challenge. Combine with `.paid()` to gate
3589
- * a paid route on a verified wallet identity.
3604
+ * control of a wallet via a signed challenge. Composes with `.paid()` and
3605
+ * `.upTo()` for pay-once-then-replay: the first request settles normally,
3606
+ * subsequent requests with a valid SIWX signature for the same wallet skip
3607
+ * payment (on `.upTo()`, `charge(amount)` becomes a no-op on the replay).
3608
+ * Mutually exclusive with `.metered()`.
3590
3609
  *
3591
3610
  * @example
3592
3611
  * ```ts
3593
3612
  * router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
3613
+ * router.route('inbox').paid('0.01').siwx().handler(async ({ wallet }) => getInbox(wallet));
3594
3614
  * ```
3595
3615
  */
3596
3616
  siwx() {
@@ -3604,6 +3624,11 @@ var RouteBuilder = class _RouteBuilder {
3604
3624
  `route '${this.#s.key}': Combining .siwx() and .apiKey() is not supported on the same route.`
3605
3625
  );
3606
3626
  }
3627
+ if (this.#s.billing === "metered") {
3628
+ throw new Error(
3629
+ `route '${this.#s.key}': Cannot combine .metered() and .siwx() \u2014 per-tick MPP billing has no entitlement model. Use .paid() or .upTo() with .siwx(), or drop .siwx() for metered routes.`
3630
+ );
3631
+ }
3607
3632
  const next = this.fork();
3608
3633
  next.#s.siwxEnabled = true;
3609
3634
  if (next.#s.authMode === "paid" || next.#s.pricing) {
package/dist/index.d.cts CHANGED
@@ -588,12 +588,16 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
588
588
  private applyPaid;
589
589
  /**
590
590
  * Require Sign-In-with-X wallet identity on this route — clients prove
591
- * control of a wallet via a signed challenge. Combine with `.paid()` to gate
592
- * a paid route on a verified wallet identity.
591
+ * control of a wallet via a signed challenge. Composes with `.paid()` and
592
+ * `.upTo()` for pay-once-then-replay: the first request settles normally,
593
+ * subsequent requests with a valid SIWX signature for the same wallet skip
594
+ * payment (on `.upTo()`, `charge(amount)` becomes a no-op on the replay).
595
+ * Mutually exclusive with `.metered()`.
593
596
  *
594
597
  * @example
595
598
  * ```ts
596
599
  * router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
600
+ * router.route('inbox').paid('0.01').siwx().handler(async ({ wallet }) => getInbox(wallet));
597
601
  * ```
598
602
  */
599
603
  siwx(): RouteBuilder<TBody, TQuery, TOutput, True, False, HasBody, Bill>;
package/dist/index.d.ts CHANGED
@@ -588,12 +588,16 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
588
588
  private applyPaid;
589
589
  /**
590
590
  * Require Sign-In-with-X wallet identity on this route — clients prove
591
- * control of a wallet via a signed challenge. Combine with `.paid()` to gate
592
- * a paid route on a verified wallet identity.
591
+ * control of a wallet via a signed challenge. Composes with `.paid()` and
592
+ * `.upTo()` for pay-once-then-replay: the first request settles normally,
593
+ * subsequent requests with a valid SIWX signature for the same wallet skip
594
+ * payment (on `.upTo()`, `charge(amount)` becomes a no-op on the replay).
595
+ * Mutually exclusive with `.metered()`.
593
596
  *
594
597
  * @example
595
598
  * ```ts
596
599
  * router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
600
+ * router.route('inbox').paid('0.01').siwx().handler(async ({ wallet }) => getInbox(wallet));
597
601
  * ```
598
602
  */
599
603
  siwx(): RouteBuilder<TBody, TQuery, TOutput, True, False, HasBody, Bill>;
package/dist/index.js CHANGED
@@ -767,7 +767,11 @@ function invokePaidStatic(ctx, wallet, account, body, payment) {
767
767
  return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, payment));
768
768
  }
769
769
  function invokeUnauthed(ctx, wallet, account, body) {
770
- return runHandler(ctx, buildHandlerCtx(ctx, wallet, account, body, null));
770
+ const base = buildHandlerCtx(ctx, wallet, account, body, null);
771
+ if (ctx.routeEntry.billing !== "upto") return runHandler(ctx, base);
772
+ const uptoCtx = { ...base, charge: async () => {
773
+ } };
774
+ return runHandler(ctx, uptoCtx);
771
775
  }
772
776
  function buildHandlerCtx(ctx, wallet, account, body, payment) {
773
777
  return {
@@ -1096,7 +1100,7 @@ async function resolveEarlyBody(args) {
1096
1100
  }
1097
1101
  const earlyClone = ctx.request.clone();
1098
1102
  const earlyResult = await parseBody(ctx, earlyClone);
1099
- if (!earlyResult.ok) return { ok: false, response: earlyResult.response };
1103
+ if (!earlyResult.ok) return { ok: true, earlyBody: void 0 };
1100
1104
  const validateErr = await runValidate(ctx, earlyResult.data);
1101
1105
  if (validateErr) return { ok: false, response: validateErr };
1102
1106
  return { ok: true, earlyBody: earlyResult.data };
@@ -1148,17 +1152,6 @@ function protocolInitError(routeEntry, deps) {
1148
1152
  return `Payment protocol initialization failed. ${errors.join("; ")}`;
1149
1153
  }
1150
1154
 
1151
- // src/pipeline/flows/api-key-only.ts
1152
- async function runApiKeyOnlyFlow(ctx) {
1153
- if (!ctx.routeEntry.apiKeyResolver) {
1154
- return fail(ctx, 401, "API key resolver not configured");
1155
- }
1156
- const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
1157
- if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
1158
- fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
1159
- return runHandlerOnly(ctx, null, result.account);
1160
- }
1161
-
1162
1155
  // src/pricing/format.ts
1163
1156
  var USDC_DECIMALS = 6;
1164
1157
  var DECIMAL_RE = /^(\d+)(?:\.(\d+))?$/;
@@ -1214,169 +1207,6 @@ function multiplyDecimal(decimal, factor) {
1214
1207
  return fracPart ? `${intPart}.${fracPart}` : intPart;
1215
1208
  }
1216
1209
 
1217
- // src/pricing/dynamic.ts
1218
- var DynamicPricing = class {
1219
- constructor(opts) {
1220
- this.opts = opts;
1221
- }
1222
- needsBody = true;
1223
- async quote(body) {
1224
- let priced;
1225
- try {
1226
- const raw = await this.opts.fn(body);
1227
- priced = this.cap(raw, body);
1228
- } catch (err) {
1229
- if (err instanceof HttpError) throw err;
1230
- this.alert("error", `Pricing function failed: ${msg(err)}`, {
1231
- error: err instanceof Error ? err.stack : String(err),
1232
- body
1233
- });
1234
- if (this.opts.maxPrice) {
1235
- this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
1236
- return this.opts.maxPrice;
1237
- }
1238
- throw err;
1239
- }
1240
- if (!isPositiveDecimal(priced)) {
1241
- throw new HttpError(
1242
- `route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
1243
- 500
1244
- );
1245
- }
1246
- return priced;
1247
- }
1248
- challengeQuote(body) {
1249
- if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
1250
- return this.quote(body);
1251
- }
1252
- describe() {
1253
- return {
1254
- mode: "dynamic",
1255
- min: this.opts.minPrice ?? "0",
1256
- max: this.opts.maxPrice ?? "0"
1257
- };
1258
- }
1259
- cap(raw, body) {
1260
- if (!this.opts.maxPrice) return raw;
1261
- let overCap;
1262
- try {
1263
- overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
1264
- } catch {
1265
- overCap = true;
1266
- }
1267
- if (overCap) {
1268
- this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
1269
- calculated: raw,
1270
- maxPrice: this.opts.maxPrice,
1271
- body
1272
- });
1273
- return this.opts.maxPrice;
1274
- }
1275
- return raw;
1276
- }
1277
- alert(level, message, meta) {
1278
- this.opts.alert?.(level, message, meta);
1279
- }
1280
- };
1281
- function msg(err) {
1282
- return err instanceof Error ? err.message : String(err);
1283
- }
1284
-
1285
- // src/pricing/fixed.ts
1286
- var FixedPricing = class {
1287
- constructor(price) {
1288
- this.price = price;
1289
- }
1290
- needsBody = false;
1291
- quote() {
1292
- return Promise.resolve(this.price);
1293
- }
1294
- challengeQuote() {
1295
- return Promise.resolve(this.price);
1296
- }
1297
- describe() {
1298
- return { mode: "fixed", amount: this.price };
1299
- }
1300
- };
1301
-
1302
- // src/pricing/tiered.ts
1303
- var TieredPricing = class {
1304
- constructor(opts) {
1305
- this.opts = opts;
1306
- }
1307
- needsBody = true;
1308
- async quote(body) {
1309
- const { field, tiers, default: defaultTier } = this.opts;
1310
- const tierKey = body != null ? String(body[field] ?? "") : "";
1311
- if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
1312
- if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
1313
- if (!tierKey) {
1314
- throw httpError(400, `Missing required field '${field}' for tier pricing`);
1315
- }
1316
- throw httpError(
1317
- 400,
1318
- `Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
1319
- );
1320
- }
1321
- async challengeQuote(body) {
1322
- if (body !== void 0) {
1323
- try {
1324
- return await this.quote(body);
1325
- } catch {
1326
- }
1327
- }
1328
- return this.maxTierPrice();
1329
- }
1330
- describe() {
1331
- return {
1332
- mode: "tiered",
1333
- tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
1334
- key,
1335
- price: tier.price,
1336
- ...tier.label !== void 0 ? { label: tier.label } : {}
1337
- })),
1338
- ...this.opts.default !== void 0 ? { default: this.opts.default } : {}
1339
- };
1340
- }
1341
- maxTierPrice() {
1342
- let max = "0";
1343
- for (const tier of Object.values(this.opts.tiers)) {
1344
- if (compareDecimals(tier.price, max) > 0) max = tier.price;
1345
- }
1346
- return max;
1347
- }
1348
- };
1349
- function httpError(status, message) {
1350
- return Object.assign(new Error(message), { status });
1351
- }
1352
-
1353
- // src/pricing/index.ts
1354
- function selectPricing(raw, deps = {}) {
1355
- if (raw == null) return null;
1356
- if (typeof raw === "string") {
1357
- return new FixedPricing(raw);
1358
- }
1359
- if (typeof raw === "function") {
1360
- return new DynamicPricing({
1361
- fn: raw,
1362
- maxPrice: deps.maxPrice,
1363
- minPrice: deps.minPrice,
1364
- route: deps.route,
1365
- alert: deps.alert
1366
- });
1367
- }
1368
- if (typeof raw === "object" && "tiers" in raw) {
1369
- return new TieredPricing({
1370
- field: raw.field,
1371
- tiers: raw.tiers,
1372
- default: raw.default,
1373
- maxPrice: deps.maxPrice,
1374
- minPrice: deps.minPrice
1375
- });
1376
- }
1377
- throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
1378
- }
1379
-
1380
1210
  // src/protocols/mpp/credential.ts
1381
1211
  import { Credential } from "mppx";
1382
1212
  import { getAddress, isAddress } from "viem";
@@ -2306,6 +2136,180 @@ function getAllowedStrategies(allowed) {
2306
2136
  return allowed.map((name) => STRATEGIES[name]);
2307
2137
  }
2308
2138
 
2139
+ // src/pipeline/flows/api-key-only.ts
2140
+ async function runApiKeyOnlyFlow(ctx) {
2141
+ if (!ctx.routeEntry.apiKeyResolver) {
2142
+ return fail(ctx, 401, "API key resolver not configured");
2143
+ }
2144
+ const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
2145
+ if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
2146
+ fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
2147
+ return runHandlerOnly(ctx, null, result.account);
2148
+ }
2149
+
2150
+ // src/pricing/dynamic.ts
2151
+ var DynamicPricing = class {
2152
+ constructor(opts) {
2153
+ this.opts = opts;
2154
+ }
2155
+ needsBody = true;
2156
+ async quote(body) {
2157
+ let priced;
2158
+ try {
2159
+ const raw = await this.opts.fn(body);
2160
+ priced = this.cap(raw, body);
2161
+ } catch (err) {
2162
+ if (err instanceof HttpError) throw err;
2163
+ this.alert("error", `Pricing function failed: ${msg(err)}`, {
2164
+ error: err instanceof Error ? err.stack : String(err),
2165
+ body
2166
+ });
2167
+ if (this.opts.maxPrice) {
2168
+ this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
2169
+ return this.opts.maxPrice;
2170
+ }
2171
+ throw err;
2172
+ }
2173
+ if (!isPositiveDecimal(priced)) {
2174
+ throw new HttpError(
2175
+ `route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
2176
+ 500
2177
+ );
2178
+ }
2179
+ return priced;
2180
+ }
2181
+ challengeQuote(body) {
2182
+ if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
2183
+ return this.quote(body);
2184
+ }
2185
+ describe() {
2186
+ return {
2187
+ mode: "dynamic",
2188
+ min: this.opts.minPrice ?? "0",
2189
+ max: this.opts.maxPrice ?? "0"
2190
+ };
2191
+ }
2192
+ cap(raw, body) {
2193
+ if (!this.opts.maxPrice) return raw;
2194
+ let overCap;
2195
+ try {
2196
+ overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
2197
+ } catch {
2198
+ overCap = true;
2199
+ }
2200
+ if (overCap) {
2201
+ this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
2202
+ calculated: raw,
2203
+ maxPrice: this.opts.maxPrice,
2204
+ body
2205
+ });
2206
+ return this.opts.maxPrice;
2207
+ }
2208
+ return raw;
2209
+ }
2210
+ alert(level, message, meta) {
2211
+ this.opts.alert?.(level, message, meta);
2212
+ }
2213
+ };
2214
+ function msg(err) {
2215
+ return err instanceof Error ? err.message : String(err);
2216
+ }
2217
+
2218
+ // src/pricing/fixed.ts
2219
+ var FixedPricing = class {
2220
+ constructor(price) {
2221
+ this.price = price;
2222
+ }
2223
+ needsBody = false;
2224
+ quote() {
2225
+ return Promise.resolve(this.price);
2226
+ }
2227
+ challengeQuote() {
2228
+ return Promise.resolve(this.price);
2229
+ }
2230
+ describe() {
2231
+ return { mode: "fixed", amount: this.price };
2232
+ }
2233
+ };
2234
+
2235
+ // src/pricing/tiered.ts
2236
+ var TieredPricing = class {
2237
+ constructor(opts) {
2238
+ this.opts = opts;
2239
+ }
2240
+ needsBody = true;
2241
+ async quote(body) {
2242
+ const { field, tiers, default: defaultTier } = this.opts;
2243
+ const tierKey = body != null ? String(body[field] ?? "") : "";
2244
+ if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
2245
+ if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
2246
+ if (!tierKey) {
2247
+ throw httpError(400, `Missing required field '${field}' for tier pricing`);
2248
+ }
2249
+ throw httpError(
2250
+ 400,
2251
+ `Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
2252
+ );
2253
+ }
2254
+ async challengeQuote(body) {
2255
+ if (body !== void 0) {
2256
+ try {
2257
+ return await this.quote(body);
2258
+ } catch {
2259
+ }
2260
+ }
2261
+ return this.maxTierPrice();
2262
+ }
2263
+ describe() {
2264
+ return {
2265
+ mode: "tiered",
2266
+ tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
2267
+ key,
2268
+ price: tier.price,
2269
+ ...tier.label !== void 0 ? { label: tier.label } : {}
2270
+ })),
2271
+ ...this.opts.default !== void 0 ? { default: this.opts.default } : {}
2272
+ };
2273
+ }
2274
+ maxTierPrice() {
2275
+ let max = "0";
2276
+ for (const tier of Object.values(this.opts.tiers)) {
2277
+ if (compareDecimals(tier.price, max) > 0) max = tier.price;
2278
+ }
2279
+ return max;
2280
+ }
2281
+ };
2282
+ function httpError(status, message) {
2283
+ return Object.assign(new Error(message), { status });
2284
+ }
2285
+
2286
+ // src/pricing/index.ts
2287
+ function selectPricing(raw, deps = {}) {
2288
+ if (raw == null) return null;
2289
+ if (typeof raw === "string") {
2290
+ return new FixedPricing(raw);
2291
+ }
2292
+ if (typeof raw === "function") {
2293
+ return new DynamicPricing({
2294
+ fn: raw,
2295
+ maxPrice: deps.maxPrice,
2296
+ minPrice: deps.minPrice,
2297
+ route: deps.route,
2298
+ alert: deps.alert
2299
+ });
2300
+ }
2301
+ if (typeof raw === "object" && "tiers" in raw) {
2302
+ return new TieredPricing({
2303
+ field: raw.field,
2304
+ tiers: raw.tiers,
2305
+ default: raw.default,
2306
+ maxPrice: deps.maxPrice,
2307
+ minPrice: deps.minPrice
2308
+ });
2309
+ }
2310
+ throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
2311
+ }
2312
+
2309
2313
  // src/pipeline/flows/challenge-response.ts
2310
2314
  import { NextResponse as NextResponse5 } from "next/server";
2311
2315
 
@@ -3340,13 +3344,20 @@ async function runUnprotectedFlow(ctx) {
3340
3344
  }
3341
3345
 
3342
3346
  // src/pipeline/orchestrate.ts
3347
+ function shouldSkipQueryValidation(routeEntry, request) {
3348
+ const isPaidRoute = !!routeEntry.pricing || routeEntry.authMode === "paid";
3349
+ if (!isPaidRoute) return false;
3350
+ return selectIncomingStrategy(request, routeEntry.protocols) === null;
3351
+ }
3343
3352
  function createRequestHandler(routeEntry, handler, deps) {
3344
3353
  return async (request) => {
3345
3354
  await deps.initPromise;
3346
3355
  const ctx = preflight(routeEntry, handler, deps, request);
3347
- const query = validateQuery(ctx);
3348
- if (!query.ok) return query.response;
3349
- ctx.query = query.data;
3356
+ if (!shouldSkipQueryValidation(routeEntry, request)) {
3357
+ const query = validateQuery(ctx);
3358
+ if (!query.ok) return query.response;
3359
+ ctx.query = query.data;
3360
+ }
3350
3361
  if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
3351
3362
  if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
3352
3363
  if (routeEntry.pricing) return runPaidFlow(ctx);
@@ -3478,6 +3489,11 @@ var RouteBuilder = class _RouteBuilder {
3478
3489
  `route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
3479
3490
  );
3480
3491
  }
3492
+ if (this.#s.siwxEnabled && billing === "metered") {
3493
+ throw new Error(
3494
+ `route '${this.#s.key}': Cannot combine .siwx() and .metered() \u2014 per-tick MPP billing has no entitlement model. Use .paid() or .upTo() with .siwx(), or drop .siwx() for metered routes.`
3495
+ );
3496
+ }
3481
3497
  const next = this.fork();
3482
3498
  next.#s.authMode = "paid";
3483
3499
  next.#s.pricing = pricing;
@@ -3544,12 +3560,16 @@ var RouteBuilder = class _RouteBuilder {
3544
3560
  }
3545
3561
  /**
3546
3562
  * Require Sign-In-with-X wallet identity on this route — clients prove
3547
- * control of a wallet via a signed challenge. Combine with `.paid()` to gate
3548
- * a paid route on a verified wallet identity.
3563
+ * control of a wallet via a signed challenge. Composes with `.paid()` and
3564
+ * `.upTo()` for pay-once-then-replay: the first request settles normally,
3565
+ * subsequent requests with a valid SIWX signature for the same wallet skip
3566
+ * payment (on `.upTo()`, `charge(amount)` becomes a no-op on the replay).
3567
+ * Mutually exclusive with `.metered()`.
3549
3568
  *
3550
3569
  * @example
3551
3570
  * ```ts
3552
3571
  * router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
3572
+ * router.route('inbox').paid('0.01').siwx().handler(async ({ wallet }) => getInbox(wallet));
3553
3573
  * ```
3554
3574
  */
3555
3575
  siwx() {
@@ -3563,6 +3583,11 @@ var RouteBuilder = class _RouteBuilder {
3563
3583
  `route '${this.#s.key}': Combining .siwx() and .apiKey() is not supported on the same route.`
3564
3584
  );
3565
3585
  }
3586
+ if (this.#s.billing === "metered") {
3587
+ throw new Error(
3588
+ `route '${this.#s.key}': Cannot combine .metered() and .siwx() \u2014 per-tick MPP billing has no entitlement model. Use .paid() or .upTo() with .siwx(), or drop .siwx() for metered routes.`
3589
+ );
3590
+ }
3566
3591
  const next = this.fork();
3567
3592
  next.#s.siwxEnabled = true;
3568
3593
  if (next.#s.authMode === "paid" || next.#s.pricing) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "1.9.3",
3
+ "version": "1.9.4",
4
4
  "description": "Unified route builder for Next.js App Router APIs with x402, MPP, SIWX, and API key auth",
5
5
  "type": "module",
6
6
  "exports": {