@agentcash/router 1.9.3 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -119,6 +119,7 @@ export const POST = router.route({ path: 'search' })
119
119
  // app/api/inbox/status/route.ts
120
120
  export const GET = router.route({ path: 'inbox/status' })
121
121
  .siwx()
122
+ .method('GET')
122
123
  .handler(async ({ wallet }) => getStatus(wallet));
123
124
  ```
124
125
 
@@ -163,6 +164,20 @@ router.route({ path: 'gated' })
163
164
  .handler(fn);
164
165
  ```
165
166
 
167
+ ### `.siwx()` on paid routes. Pay once, identity-gated replays
168
+
169
+ `.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.
170
+
171
+ ```typescript
172
+ router.route({ path: 'inbox' })
173
+ .paid('0.01').siwx() // first call pays $0.01, later calls present a SIWX sig instead
174
+ .handler(async ({ wallet }) => getInbox(wallet));
175
+ ```
176
+
177
+ `.metered()` is mutually exclusive with `.siwx()` — per-tick MPP billing has no entitlement model — and the builder throws at registration if you combine them.
178
+
179
+ > **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.
180
+
166
181
  ## Pricing
167
182
 
168
183
  `.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,199 @@ function getAllowedStrategies(allowed) {
2347
2177
  return allowed.map((name) => STRATEGIES[name]);
2348
2178
  }
2349
2179
 
2180
+ // src/protocols/detect.ts
2181
+ function detectProtocol(request) {
2182
+ if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
2183
+ return "x402";
2184
+ }
2185
+ const auth = request.headers.get(HEADERS.AUTHORIZATION);
2186
+ if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
2187
+ return "mpp";
2188
+ }
2189
+ if (request.headers.get(HEADERS.SIWX)) {
2190
+ return "siwx";
2191
+ }
2192
+ return null;
2193
+ }
2194
+
2195
+ // src/pipeline/flows/api-key-only.ts
2196
+ async function runApiKeyOnlyFlow(ctx) {
2197
+ if (!ctx.routeEntry.apiKeyResolver) {
2198
+ return fail(ctx, 401, "API key resolver not configured");
2199
+ }
2200
+ const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
2201
+ if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
2202
+ fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
2203
+ return runHandlerOnly(ctx, null, result.account);
2204
+ }
2205
+
2206
+ // src/pricing/dynamic.ts
2207
+ var DynamicPricing = class {
2208
+ constructor(opts) {
2209
+ this.opts = opts;
2210
+ if (!isPositiveDecimal(opts.maxPrice)) {
2211
+ throw new Error(
2212
+ `route '${opts.route ?? "unknown"}': dynamic pricing requires a positive maxPrice, got '${opts.maxPrice}'`
2213
+ );
2214
+ }
2215
+ }
2216
+ needsBody = true;
2217
+ async quote(body) {
2218
+ let priced;
2219
+ try {
2220
+ const raw = await this.opts.fn(body);
2221
+ priced = this.cap(raw, body);
2222
+ } catch (err) {
2223
+ if (err instanceof HttpError) throw err;
2224
+ this.alert("error", `Pricing function failed: ${msg(err)}`, {
2225
+ error: err instanceof Error ? err.stack : String(err),
2226
+ body
2227
+ });
2228
+ this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
2229
+ return this.opts.maxPrice;
2230
+ }
2231
+ if (!isPositiveDecimal(priced)) {
2232
+ throw new HttpError(
2233
+ `route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
2234
+ 500
2235
+ );
2236
+ }
2237
+ return priced;
2238
+ }
2239
+ challengeQuote(body) {
2240
+ if (body === void 0) return Promise.resolve(this.opts.maxPrice);
2241
+ return this.quote(body);
2242
+ }
2243
+ describe() {
2244
+ return {
2245
+ mode: "dynamic",
2246
+ min: this.opts.minPrice ?? "0",
2247
+ max: this.opts.maxPrice
2248
+ };
2249
+ }
2250
+ cap(raw, body) {
2251
+ let overCap;
2252
+ try {
2253
+ overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
2254
+ } catch {
2255
+ overCap = true;
2256
+ }
2257
+ if (overCap) {
2258
+ this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
2259
+ calculated: raw,
2260
+ maxPrice: this.opts.maxPrice,
2261
+ body
2262
+ });
2263
+ return this.opts.maxPrice;
2264
+ }
2265
+ return raw;
2266
+ }
2267
+ alert(level, message, meta) {
2268
+ this.opts.alert?.(level, message, meta);
2269
+ }
2270
+ };
2271
+ function msg(err) {
2272
+ return err instanceof Error ? err.message : String(err);
2273
+ }
2274
+
2275
+ // src/pricing/fixed.ts
2276
+ var FixedPricing = class {
2277
+ constructor(price) {
2278
+ this.price = price;
2279
+ }
2280
+ needsBody = false;
2281
+ quote() {
2282
+ return Promise.resolve(this.price);
2283
+ }
2284
+ challengeQuote() {
2285
+ return Promise.resolve(this.price);
2286
+ }
2287
+ describe() {
2288
+ return { mode: "fixed", amount: this.price };
2289
+ }
2290
+ };
2291
+
2292
+ // src/pricing/tiered.ts
2293
+ var TieredPricing = class {
2294
+ constructor(opts) {
2295
+ this.opts = opts;
2296
+ }
2297
+ needsBody = true;
2298
+ async quote(body) {
2299
+ const { field, tiers, default: defaultTier } = this.opts;
2300
+ const tierKey = body != null ? String(body[field] ?? "") : "";
2301
+ if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
2302
+ if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
2303
+ if (!tierKey) {
2304
+ throw httpError(400, `Missing required field '${field}' for tier pricing`);
2305
+ }
2306
+ throw httpError(
2307
+ 400,
2308
+ `Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
2309
+ );
2310
+ }
2311
+ async challengeQuote(body) {
2312
+ if (body !== void 0) {
2313
+ try {
2314
+ return await this.quote(body);
2315
+ } catch {
2316
+ }
2317
+ }
2318
+ return this.maxTierPrice();
2319
+ }
2320
+ describe() {
2321
+ return {
2322
+ mode: "tiered",
2323
+ tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
2324
+ key,
2325
+ price: tier.price,
2326
+ ...tier.label !== void 0 ? { label: tier.label } : {}
2327
+ })),
2328
+ ...this.opts.default !== void 0 ? { default: this.opts.default } : {}
2329
+ };
2330
+ }
2331
+ maxTierPrice() {
2332
+ let max = "0";
2333
+ for (const tier of Object.values(this.opts.tiers)) {
2334
+ if (compareDecimals(tier.price, max) > 0) max = tier.price;
2335
+ }
2336
+ return max;
2337
+ }
2338
+ };
2339
+ function httpError(status, message) {
2340
+ return Object.assign(new Error(message), { status });
2341
+ }
2342
+
2343
+ // src/pricing/index.ts
2344
+ function selectPricing(raw, deps = {}) {
2345
+ if (raw == null) return null;
2346
+ if (typeof raw === "string") {
2347
+ return new FixedPricing(raw);
2348
+ }
2349
+ if (typeof raw === "function") {
2350
+ if (!deps.maxPrice) {
2351
+ throw new Error(`route '${deps.route ?? "unknown"}': dynamic pricing requires maxPrice`);
2352
+ }
2353
+ return new DynamicPricing({
2354
+ fn: raw,
2355
+ maxPrice: deps.maxPrice,
2356
+ minPrice: deps.minPrice,
2357
+ route: deps.route,
2358
+ alert: deps.alert
2359
+ });
2360
+ }
2361
+ if (typeof raw === "object" && "tiers" in raw) {
2362
+ return new TieredPricing({
2363
+ field: raw.field,
2364
+ tiers: raw.tiers,
2365
+ default: raw.default,
2366
+ maxPrice: deps.maxPrice,
2367
+ minPrice: deps.minPrice
2368
+ });
2369
+ }
2370
+ throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
2371
+ }
2372
+
2350
2373
  // src/pipeline/flows/challenge-response.ts
2351
2374
  var import_server5 = require("next/server");
2352
2375
 
@@ -3211,21 +3234,6 @@ async function createKvMppStore(kv, options) {
3211
3234
  });
3212
3235
  }
3213
3236
 
3214
- // src/protocols/detect.ts
3215
- function detectProtocol(request) {
3216
- if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
3217
- return "x402";
3218
- }
3219
- const auth = request.headers.get(HEADERS.AUTHORIZATION);
3220
- if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
3221
- return "mpp";
3222
- }
3223
- if (request.headers.get(HEADERS.SIWX)) {
3224
- return "siwx";
3225
- }
3226
- return null;
3227
- }
3228
-
3229
3237
  // src/protocols/mpp/siwx-mode.ts
3230
3238
  var import_mppx3 = require("mppx");
3231
3239
  async function verifyMppSiwx(request, mppx) {
@@ -3248,8 +3256,6 @@ async function runSiwxOnlyFlow(ctx) {
3248
3256
  if (earlyBody.ok) {
3249
3257
  const validateErr = await runValidate(ctx, earlyBody.data);
3250
3258
  if (validateErr) return validateErr;
3251
- } else {
3252
- return earlyBody.response;
3253
3259
  }
3254
3260
  }
3255
3261
  const siwxHeader = request.headers.get(HEADERS.SIWX);
@@ -3381,13 +3387,24 @@ async function runUnprotectedFlow(ctx) {
3381
3387
  }
3382
3388
 
3383
3389
  // src/pipeline/orchestrate.ts
3390
+ function shouldSkipQueryValidation(routeEntry, request) {
3391
+ if (routeEntry.pricing || routeEntry.authMode === "paid") {
3392
+ return selectIncomingStrategy(request, routeEntry.protocols) === null;
3393
+ }
3394
+ if (routeEntry.authMode === "siwx") {
3395
+ return !request.headers.get(HEADERS.SIWX) && detectProtocol(request) !== "mpp";
3396
+ }
3397
+ return false;
3398
+ }
3384
3399
  function createRequestHandler(routeEntry, handler, deps) {
3385
3400
  return async (request) => {
3386
3401
  await deps.initPromise;
3387
3402
  const ctx = preflight(routeEntry, handler, deps, request);
3388
- const query = validateQuery(ctx);
3389
- if (!query.ok) return query.response;
3390
- ctx.query = query.data;
3403
+ if (!shouldSkipQueryValidation(routeEntry, request)) {
3404
+ const query = validateQuery(ctx);
3405
+ if (!query.ok) return query.response;
3406
+ ctx.query = query.data;
3407
+ }
3391
3408
  if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
3392
3409
  if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
3393
3410
  if (routeEntry.pricing) return runPaidFlow(ctx);
@@ -3519,6 +3536,11 @@ var RouteBuilder = class _RouteBuilder {
3519
3536
  `route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
3520
3537
  );
3521
3538
  }
3539
+ if (this.#s.siwxEnabled && billing === "metered") {
3540
+ throw new Error(
3541
+ `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.`
3542
+ );
3543
+ }
3522
3544
  const next = this.fork();
3523
3545
  next.#s.authMode = "paid";
3524
3546
  next.#s.pricing = pricing;
@@ -3566,6 +3588,11 @@ var RouteBuilder = class _RouteBuilder {
3566
3588
  `route '${this.#s.key}': price '${pricing}' must be a positive decimal string`
3567
3589
  );
3568
3590
  }
3591
+ if (typeof pricing === "function" && next.#s.maxPrice === void 0) {
3592
+ throw new Error(
3593
+ `route '${this.#s.key}': dynamic pricing requires maxPrice \u2014 without it, bare probes would advertise a $0 challenge`
3594
+ );
3595
+ }
3569
3596
  if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
3570
3597
  throw new Error(
3571
3598
  `route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
@@ -3585,12 +3612,16 @@ var RouteBuilder = class _RouteBuilder {
3585
3612
  }
3586
3613
  /**
3587
3614
  * 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.
3615
+ * control of a wallet via a signed challenge. Composes with `.paid()` and
3616
+ * `.upTo()` for pay-once-then-replay: the first request settles normally,
3617
+ * subsequent requests with a valid SIWX signature for the same wallet skip
3618
+ * payment (on `.upTo()`, `charge(amount)` becomes a no-op on the replay).
3619
+ * Mutually exclusive with `.metered()`.
3590
3620
  *
3591
3621
  * @example
3592
3622
  * ```ts
3593
3623
  * router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
3624
+ * router.route('inbox').paid('0.01').siwx().handler(async ({ wallet }) => getInbox(wallet));
3594
3625
  * ```
3595
3626
  */
3596
3627
  siwx() {
@@ -3604,6 +3635,11 @@ var RouteBuilder = class _RouteBuilder {
3604
3635
  `route '${this.#s.key}': Combining .siwx() and .apiKey() is not supported on the same route.`
3605
3636
  );
3606
3637
  }
3638
+ if (this.#s.billing === "metered") {
3639
+ throw new Error(
3640
+ `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.`
3641
+ );
3642
+ }
3607
3643
  const next = this.fork();
3608
3644
  next.#s.siwxEnabled = true;
3609
3645
  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,199 @@ function getAllowedStrategies(allowed) {
2306
2136
  return allowed.map((name) => STRATEGIES[name]);
2307
2137
  }
2308
2138
 
2139
+ // src/protocols/detect.ts
2140
+ function detectProtocol(request) {
2141
+ if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
2142
+ return "x402";
2143
+ }
2144
+ const auth = request.headers.get(HEADERS.AUTHORIZATION);
2145
+ if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
2146
+ return "mpp";
2147
+ }
2148
+ if (request.headers.get(HEADERS.SIWX)) {
2149
+ return "siwx";
2150
+ }
2151
+ return null;
2152
+ }
2153
+
2154
+ // src/pipeline/flows/api-key-only.ts
2155
+ async function runApiKeyOnlyFlow(ctx) {
2156
+ if (!ctx.routeEntry.apiKeyResolver) {
2157
+ return fail(ctx, 401, "API key resolver not configured");
2158
+ }
2159
+ const result = await verifyApiKey(ctx.request, ctx.routeEntry.apiKeyResolver);
2160
+ if (!result.valid) return fail(ctx, 401, "Invalid or missing API key");
2161
+ fireAuthVerified(ctx, { authMode: "apiKey", wallet: null, account: result.account });
2162
+ return runHandlerOnly(ctx, null, result.account);
2163
+ }
2164
+
2165
+ // src/pricing/dynamic.ts
2166
+ var DynamicPricing = class {
2167
+ constructor(opts) {
2168
+ this.opts = opts;
2169
+ if (!isPositiveDecimal(opts.maxPrice)) {
2170
+ throw new Error(
2171
+ `route '${opts.route ?? "unknown"}': dynamic pricing requires a positive maxPrice, got '${opts.maxPrice}'`
2172
+ );
2173
+ }
2174
+ }
2175
+ needsBody = true;
2176
+ async quote(body) {
2177
+ let priced;
2178
+ try {
2179
+ const raw = await this.opts.fn(body);
2180
+ priced = this.cap(raw, body);
2181
+ } catch (err) {
2182
+ if (err instanceof HttpError) throw err;
2183
+ this.alert("error", `Pricing function failed: ${msg(err)}`, {
2184
+ error: err instanceof Error ? err.stack : String(err),
2185
+ body
2186
+ });
2187
+ this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
2188
+ return this.opts.maxPrice;
2189
+ }
2190
+ if (!isPositiveDecimal(priced)) {
2191
+ throw new HttpError(
2192
+ `route '${this.opts.route ?? "unknown"}': dynamic pricing returned an invalid amount '${priced}'`,
2193
+ 500
2194
+ );
2195
+ }
2196
+ return priced;
2197
+ }
2198
+ challengeQuote(body) {
2199
+ if (body === void 0) return Promise.resolve(this.opts.maxPrice);
2200
+ return this.quote(body);
2201
+ }
2202
+ describe() {
2203
+ return {
2204
+ mode: "dynamic",
2205
+ min: this.opts.minPrice ?? "0",
2206
+ max: this.opts.maxPrice
2207
+ };
2208
+ }
2209
+ cap(raw, body) {
2210
+ let overCap;
2211
+ try {
2212
+ overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
2213
+ } catch {
2214
+ overCap = true;
2215
+ }
2216
+ if (overCap) {
2217
+ this.alert("warn", `Price ${raw} exceeds maxPrice ${this.opts.maxPrice}, capping`, {
2218
+ calculated: raw,
2219
+ maxPrice: this.opts.maxPrice,
2220
+ body
2221
+ });
2222
+ return this.opts.maxPrice;
2223
+ }
2224
+ return raw;
2225
+ }
2226
+ alert(level, message, meta) {
2227
+ this.opts.alert?.(level, message, meta);
2228
+ }
2229
+ };
2230
+ function msg(err) {
2231
+ return err instanceof Error ? err.message : String(err);
2232
+ }
2233
+
2234
+ // src/pricing/fixed.ts
2235
+ var FixedPricing = class {
2236
+ constructor(price) {
2237
+ this.price = price;
2238
+ }
2239
+ needsBody = false;
2240
+ quote() {
2241
+ return Promise.resolve(this.price);
2242
+ }
2243
+ challengeQuote() {
2244
+ return Promise.resolve(this.price);
2245
+ }
2246
+ describe() {
2247
+ return { mode: "fixed", amount: this.price };
2248
+ }
2249
+ };
2250
+
2251
+ // src/pricing/tiered.ts
2252
+ var TieredPricing = class {
2253
+ constructor(opts) {
2254
+ this.opts = opts;
2255
+ }
2256
+ needsBody = true;
2257
+ async quote(body) {
2258
+ const { field, tiers, default: defaultTier } = this.opts;
2259
+ const tierKey = body != null ? String(body[field] ?? "") : "";
2260
+ if (tierKey && tiers[tierKey]) return tiers[tierKey].price;
2261
+ if (defaultTier && tiers[defaultTier]) return tiers[defaultTier].price;
2262
+ if (!tierKey) {
2263
+ throw httpError(400, `Missing required field '${field}' for tier pricing`);
2264
+ }
2265
+ throw httpError(
2266
+ 400,
2267
+ `Unknown tier '${tierKey}' for field '${field}'. Valid tiers: ${Object.keys(tiers).join(", ")}`
2268
+ );
2269
+ }
2270
+ async challengeQuote(body) {
2271
+ if (body !== void 0) {
2272
+ try {
2273
+ return await this.quote(body);
2274
+ } catch {
2275
+ }
2276
+ }
2277
+ return this.maxTierPrice();
2278
+ }
2279
+ describe() {
2280
+ return {
2281
+ mode: "tiered",
2282
+ tiers: Object.entries(this.opts.tiers).map(([key, tier]) => ({
2283
+ key,
2284
+ price: tier.price,
2285
+ ...tier.label !== void 0 ? { label: tier.label } : {}
2286
+ })),
2287
+ ...this.opts.default !== void 0 ? { default: this.opts.default } : {}
2288
+ };
2289
+ }
2290
+ maxTierPrice() {
2291
+ let max = "0";
2292
+ for (const tier of Object.values(this.opts.tiers)) {
2293
+ if (compareDecimals(tier.price, max) > 0) max = tier.price;
2294
+ }
2295
+ return max;
2296
+ }
2297
+ };
2298
+ function httpError(status, message) {
2299
+ return Object.assign(new Error(message), { status });
2300
+ }
2301
+
2302
+ // src/pricing/index.ts
2303
+ function selectPricing(raw, deps = {}) {
2304
+ if (raw == null) return null;
2305
+ if (typeof raw === "string") {
2306
+ return new FixedPricing(raw);
2307
+ }
2308
+ if (typeof raw === "function") {
2309
+ if (!deps.maxPrice) {
2310
+ throw new Error(`route '${deps.route ?? "unknown"}': dynamic pricing requires maxPrice`);
2311
+ }
2312
+ return new DynamicPricing({
2313
+ fn: raw,
2314
+ maxPrice: deps.maxPrice,
2315
+ minPrice: deps.minPrice,
2316
+ route: deps.route,
2317
+ alert: deps.alert
2318
+ });
2319
+ }
2320
+ if (typeof raw === "object" && "tiers" in raw) {
2321
+ return new TieredPricing({
2322
+ field: raw.field,
2323
+ tiers: raw.tiers,
2324
+ default: raw.default,
2325
+ maxPrice: deps.maxPrice,
2326
+ minPrice: deps.minPrice
2327
+ });
2328
+ }
2329
+ throw new Error(`Unknown pricing config: ${JSON.stringify(raw)}`);
2330
+ }
2331
+
2309
2332
  // src/pipeline/flows/challenge-response.ts
2310
2333
  import { NextResponse as NextResponse5 } from "next/server";
2311
2334
 
@@ -3170,21 +3193,6 @@ async function createKvMppStore(kv, options) {
3170
3193
  });
3171
3194
  }
3172
3195
 
3173
- // src/protocols/detect.ts
3174
- function detectProtocol(request) {
3175
- if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
3176
- return "x402";
3177
- }
3178
- const auth = request.headers.get(HEADERS.AUTHORIZATION);
3179
- if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
3180
- return "mpp";
3181
- }
3182
- if (request.headers.get(HEADERS.SIWX)) {
3183
- return "siwx";
3184
- }
3185
- return null;
3186
- }
3187
-
3188
3196
  // src/protocols/mpp/siwx-mode.ts
3189
3197
  import { Credential as Credential2 } from "mppx";
3190
3198
  async function verifyMppSiwx(request, mppx) {
@@ -3207,8 +3215,6 @@ async function runSiwxOnlyFlow(ctx) {
3207
3215
  if (earlyBody.ok) {
3208
3216
  const validateErr = await runValidate(ctx, earlyBody.data);
3209
3217
  if (validateErr) return validateErr;
3210
- } else {
3211
- return earlyBody.response;
3212
3218
  }
3213
3219
  }
3214
3220
  const siwxHeader = request.headers.get(HEADERS.SIWX);
@@ -3340,13 +3346,24 @@ async function runUnprotectedFlow(ctx) {
3340
3346
  }
3341
3347
 
3342
3348
  // src/pipeline/orchestrate.ts
3349
+ function shouldSkipQueryValidation(routeEntry, request) {
3350
+ if (routeEntry.pricing || routeEntry.authMode === "paid") {
3351
+ return selectIncomingStrategy(request, routeEntry.protocols) === null;
3352
+ }
3353
+ if (routeEntry.authMode === "siwx") {
3354
+ return !request.headers.get(HEADERS.SIWX) && detectProtocol(request) !== "mpp";
3355
+ }
3356
+ return false;
3357
+ }
3343
3358
  function createRequestHandler(routeEntry, handler, deps) {
3344
3359
  return async (request) => {
3345
3360
  await deps.initPromise;
3346
3361
  const ctx = preflight(routeEntry, handler, deps, request);
3347
- const query = validateQuery(ctx);
3348
- if (!query.ok) return query.response;
3349
- ctx.query = query.data;
3362
+ if (!shouldSkipQueryValidation(routeEntry, request)) {
3363
+ const query = validateQuery(ctx);
3364
+ if (!query.ok) return query.response;
3365
+ ctx.query = query.data;
3366
+ }
3350
3367
  if (routeEntry.authMode === "unprotected") return runUnprotectedFlow(ctx);
3351
3368
  if (routeEntry.authMode === "siwx") return runSiwxOnlyFlow(ctx);
3352
3369
  if (routeEntry.pricing) return runPaidFlow(ctx);
@@ -3478,6 +3495,11 @@ var RouteBuilder = class _RouteBuilder {
3478
3495
  `route '${this.#s.key}': Cannot combine .paid(), .upTo(), and .metered() \u2014 pick one pricing mode.`
3479
3496
  );
3480
3497
  }
3498
+ if (this.#s.siwxEnabled && billing === "metered") {
3499
+ throw new Error(
3500
+ `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.`
3501
+ );
3502
+ }
3481
3503
  const next = this.fork();
3482
3504
  next.#s.authMode = "paid";
3483
3505
  next.#s.pricing = pricing;
@@ -3525,6 +3547,11 @@ var RouteBuilder = class _RouteBuilder {
3525
3547
  `route '${this.#s.key}': price '${pricing}' must be a positive decimal string`
3526
3548
  );
3527
3549
  }
3550
+ if (typeof pricing === "function" && next.#s.maxPrice === void 0) {
3551
+ throw new Error(
3552
+ `route '${this.#s.key}': dynamic pricing requires maxPrice \u2014 without it, bare probes would advertise a $0 challenge`
3553
+ );
3554
+ }
3528
3555
  if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
3529
3556
  throw new Error(
3530
3557
  `route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
@@ -3544,12 +3571,16 @@ var RouteBuilder = class _RouteBuilder {
3544
3571
  }
3545
3572
  /**
3546
3573
  * 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.
3574
+ * control of a wallet via a signed challenge. Composes with `.paid()` and
3575
+ * `.upTo()` for pay-once-then-replay: the first request settles normally,
3576
+ * subsequent requests with a valid SIWX signature for the same wallet skip
3577
+ * payment (on `.upTo()`, `charge(amount)` becomes a no-op on the replay).
3578
+ * Mutually exclusive with `.metered()`.
3549
3579
  *
3550
3580
  * @example
3551
3581
  * ```ts
3552
3582
  * router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
3583
+ * router.route('inbox').paid('0.01').siwx().handler(async ({ wallet }) => getInbox(wallet));
3553
3584
  * ```
3554
3585
  */
3555
3586
  siwx() {
@@ -3563,6 +3594,11 @@ var RouteBuilder = class _RouteBuilder {
3563
3594
  `route '${this.#s.key}': Combining .siwx() and .apiKey() is not supported on the same route.`
3564
3595
  );
3565
3596
  }
3597
+ if (this.#s.billing === "metered") {
3598
+ throw new Error(
3599
+ `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.`
3600
+ );
3601
+ }
3566
3602
  const next = this.fork();
3567
3603
  next.#s.siwxEnabled = true;
3568
3604
  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.10.0",
4
4
  "description": "Unified route builder for Next.js App Router APIs with x402, MPP, SIWX, and API key auth",
5
5
  "type": "module",
6
6
  "exports": {