@agentcash/router 1.9.4 → 1.10.1

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
@@ -18,6 +18,12 @@
18
18
  <a href="#install"><img alt="next.js" src="https://img.shields.io/badge/Next.js-App%20Router-111"></a>
19
19
  </p>
20
20
 
21
+ <p align="center">
22
+ <a href="https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FMerit-Systems%2Fagentcash-router%2Ftree%2Fmain%2Fexamples%2Fvercel-deploy&project-name=agentcash-fortune-demo&repository-name=agentcash-fortune-demo&demo-title=AgentCash%20Router%20Fortune%20Demo&demo-description=Pay-per-call%20fortune%20API%20on%20x402%20and%20MPP&demo-url=https%3A%2F%2Fagentcash.dev&env=EVM_PAYEE_ADDRESS%2CCDP_API_KEY_ID%2CCDP_API_KEY_SECRET&envDescription=Wallet%20that%20receives%20payments%20%2B%20Coinbase%20Developer%20Platform%20API%20keys%20for%20the%20default%20x402%20facilitator.%20Create%20keys%20in%20the%20generous%20CDP%20free%20tier%3A%20https%3A%2F%2Fportal.cdp.coinbase.com%2Fprojects%2Fapi-keys&envLink=https%3A%2F%2Fgithub.com%2FMerit-Systems%2Fagentcash-router%2Fblob%2Fmain%2Fexamples%2Fvercel-deploy%2FREADME.md%23environment-variables">
23
+ <img alt="Deploy with Vercel" src="https://vercel.com/button">
24
+ </a>
25
+ </p>
26
+
21
27
  ---
22
28
 
23
29
  ## Install
@@ -36,7 +42,7 @@ The recommended entry point reads its config from `process.env`. A copy-paste `.
36
42
  | Var | Required | Purpose |
37
43
  |-----|----------|---------|
38
44
  | `EVM_PAYEE_ADDRESS` | yes | EVM address that receives x402 and MPP payments (`0x…`, 20 bytes). Canonicalized to lowercase. The zero address is rejected. |
39
- | `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET` | yes (EVM) | Coinbase Developer Platform credentials for the default EVM facilitator. Create an API key at https://portal.cdp.coinbase.com. T3 / `@t3-oss/env-nextjs` users must declare these in their env schema. |
45
+ | `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET` | yes (EVM) | Coinbase Developer Platform credentials for the default EVM facilitator. Create API keys in the generous CDP free tier at https://portal.cdp.coinbase.com/projects/api-keys. T3 / `@t3-oss/env-nextjs` users must declare these in their env schema. |
40
46
 
41
47
  ### Solana
42
48
 
@@ -59,7 +65,8 @@ The recommended entry point reads its config from `process.env`. A copy-paste `.
59
65
 
60
66
  | Var | Required | Purpose |
61
67
  |-----|----------|---------|
62
- | `BASE_URL` | yes | Origin URL (`https://api.example.com`). Load-bearing: used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Must match the public domain. |
68
+ | `BASE_URL` | yes outside Vercel | Origin URL (`https://api.example.com`). Load-bearing: used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Must match the public domain. On Vercel, `createRouterFromEnv` auto-derives this from `VERCEL_PROJECT_PRODUCTION_URL`, then `VERCEL_URL`. |
69
+ | `VERCEL_PROJECT_PRODUCTION_URL`, `VERCEL_URL` | no | Vercel system env vars used as fallbacks when `BASE_URL` is unset. Do not set these manually; Vercel provides them during builds and runtime. |
63
70
  | `KV_REST_API_URL`, `KV_REST_API_TOKEN` | no | Upstash / Vercel KV. Backs SIWX nonce, SIWX entitlement, and MPP replay. In-memory fallback is unsafe in serverless production. Providing a Kv Store is highly recommended. |
64
71
 
65
72
  ## Quick start
@@ -119,6 +126,7 @@ export const POST = router.route({ path: 'search' })
119
126
  // app/api/inbox/status/route.ts
120
127
  export const GET = router.route({ path: 'inbox/status' })
121
128
  .siwx()
129
+ .method('GET')
122
130
  .handler(async ({ wallet }) => getStatus(wallet));
123
131
  ```
124
132
 
@@ -305,4 +313,3 @@ const loggingPlugin: RouterPlugin = {
305
313
  },
306
314
  };
307
315
  ```
308
-
package/dist/index.cjs CHANGED
@@ -1938,8 +1938,15 @@ function tagBareDecimalAsDollars(amount) {
1938
1938
  var import_types2 = require("@x402/core/types");
1939
1939
  async function verifyX402Payment(opts) {
1940
1940
  const { server, request, price, accepts, report } = opts;
1941
- const payload = await readPaymentPayload(request);
1942
- if (!payload) return null;
1941
+ const payment = await readPaymentPayload(request);
1942
+ if (payment.kind === "none") return null;
1943
+ if (payment.kind === "malformed") {
1944
+ return invalidPaymentVerification({
1945
+ reason: "malformed_payment_header",
1946
+ message: `X-PAYMENT header could not be decoded: ${payment.message}`
1947
+ });
1948
+ }
1949
+ const payload = payment.payload;
1943
1950
  const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
1944
1951
  const matching = findVerifiableRequirements(server, requirements, payload);
1945
1952
  const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
@@ -2000,9 +2007,16 @@ function matchesStableFields(requirement, accepted) {
2000
2007
  }
2001
2008
  async function readPaymentPayload(request) {
2002
2009
  const paymentHeader = request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY);
2003
- if (!paymentHeader) return null;
2010
+ if (!paymentHeader) return { kind: "none" };
2004
2011
  const { decodePaymentSignatureHeader } = await import("@x402/core/http");
2005
- return decodePaymentSignatureHeader(paymentHeader);
2012
+ try {
2013
+ return { kind: "ok", payload: decodePaymentSignatureHeader(paymentHeader) };
2014
+ } catch (err) {
2015
+ return {
2016
+ kind: "malformed",
2017
+ message: err instanceof Error ? err.message : String(err)
2018
+ };
2019
+ }
2006
2020
  }
2007
2021
  function invalidPaymentVerification(failure) {
2008
2022
  return {
@@ -2177,6 +2191,21 @@ function getAllowedStrategies(allowed) {
2177
2191
  return allowed.map((name) => STRATEGIES[name]);
2178
2192
  }
2179
2193
 
2194
+ // src/protocols/detect.ts
2195
+ function detectProtocol(request) {
2196
+ if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
2197
+ return "x402";
2198
+ }
2199
+ const auth = request.headers.get(HEADERS.AUTHORIZATION);
2200
+ if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
2201
+ return "mpp";
2202
+ }
2203
+ if (request.headers.get(HEADERS.SIWX)) {
2204
+ return "siwx";
2205
+ }
2206
+ return null;
2207
+ }
2208
+
2180
2209
  // src/pipeline/flows/api-key-only.ts
2181
2210
  async function runApiKeyOnlyFlow(ctx) {
2182
2211
  if (!ctx.routeEntry.apiKeyResolver) {
@@ -2192,6 +2221,11 @@ async function runApiKeyOnlyFlow(ctx) {
2192
2221
  var DynamicPricing = class {
2193
2222
  constructor(opts) {
2194
2223
  this.opts = opts;
2224
+ if (!isPositiveDecimal(opts.maxPrice)) {
2225
+ throw new Error(
2226
+ `route '${opts.route ?? "unknown"}': dynamic pricing requires a positive maxPrice, got '${opts.maxPrice}'`
2227
+ );
2228
+ }
2195
2229
  }
2196
2230
  needsBody = true;
2197
2231
  async quote(body) {
@@ -2205,11 +2239,8 @@ var DynamicPricing = class {
2205
2239
  error: err instanceof Error ? err.stack : String(err),
2206
2240
  body
2207
2241
  });
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;
2242
+ this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
2243
+ return this.opts.maxPrice;
2213
2244
  }
2214
2245
  if (!isPositiveDecimal(priced)) {
2215
2246
  throw new HttpError(
@@ -2220,18 +2251,17 @@ var DynamicPricing = class {
2220
2251
  return priced;
2221
2252
  }
2222
2253
  challengeQuote(body) {
2223
- if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
2254
+ if (body === void 0) return Promise.resolve(this.opts.maxPrice);
2224
2255
  return this.quote(body);
2225
2256
  }
2226
2257
  describe() {
2227
2258
  return {
2228
2259
  mode: "dynamic",
2229
2260
  min: this.opts.minPrice ?? "0",
2230
- max: this.opts.maxPrice ?? "0"
2261
+ max: this.opts.maxPrice
2231
2262
  };
2232
2263
  }
2233
2264
  cap(raw, body) {
2234
- if (!this.opts.maxPrice) return raw;
2235
2265
  let overCap;
2236
2266
  try {
2237
2267
  overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
@@ -2331,6 +2361,9 @@ function selectPricing(raw, deps = {}) {
2331
2361
  return new FixedPricing(raw);
2332
2362
  }
2333
2363
  if (typeof raw === "function") {
2364
+ if (!deps.maxPrice) {
2365
+ throw new Error(`route '${deps.route ?? "unknown"}': dynamic pricing requires maxPrice`);
2366
+ }
2334
2367
  return new DynamicPricing({
2335
2368
  fn: raw,
2336
2369
  maxPrice: deps.maxPrice,
@@ -3215,21 +3248,6 @@ async function createKvMppStore(kv, options) {
3215
3248
  });
3216
3249
  }
3217
3250
 
3218
- // src/protocols/detect.ts
3219
- function detectProtocol(request) {
3220
- if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
3221
- return "x402";
3222
- }
3223
- const auth = request.headers.get(HEADERS.AUTHORIZATION);
3224
- if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
3225
- return "mpp";
3226
- }
3227
- if (request.headers.get(HEADERS.SIWX)) {
3228
- return "siwx";
3229
- }
3230
- return null;
3231
- }
3232
-
3233
3251
  // src/protocols/mpp/siwx-mode.ts
3234
3252
  var import_mppx3 = require("mppx");
3235
3253
  async function verifyMppSiwx(request, mppx) {
@@ -3252,8 +3270,6 @@ async function runSiwxOnlyFlow(ctx) {
3252
3270
  if (earlyBody.ok) {
3253
3271
  const validateErr = await runValidate(ctx, earlyBody.data);
3254
3272
  if (validateErr) return validateErr;
3255
- } else {
3256
- return earlyBody.response;
3257
3273
  }
3258
3274
  }
3259
3275
  const siwxHeader = request.headers.get(HEADERS.SIWX);
@@ -3386,9 +3402,13 @@ async function runUnprotectedFlow(ctx) {
3386
3402
 
3387
3403
  // src/pipeline/orchestrate.ts
3388
3404
  function shouldSkipQueryValidation(routeEntry, request) {
3389
- const isPaidRoute = !!routeEntry.pricing || routeEntry.authMode === "paid";
3390
- if (!isPaidRoute) return false;
3391
- return selectIncomingStrategy(request, routeEntry.protocols) === null;
3405
+ if (routeEntry.pricing || routeEntry.authMode === "paid") {
3406
+ return selectIncomingStrategy(request, routeEntry.protocols) === null;
3407
+ }
3408
+ if (routeEntry.authMode === "siwx") {
3409
+ return !request.headers.get(HEADERS.SIWX) && detectProtocol(request) !== "mpp";
3410
+ }
3411
+ return false;
3392
3412
  }
3393
3413
  function createRequestHandler(routeEntry, handler, deps) {
3394
3414
  return async (request) => {
@@ -3439,6 +3459,7 @@ ${issues}`
3439
3459
  }
3440
3460
 
3441
3461
  // src/builder.ts
3462
+ var MAX_X402_DESCRIPTION_LENGTH = 400;
3442
3463
  var RouteBuilder = class _RouteBuilder {
3443
3464
  #s;
3444
3465
  constructor(key, registry, deps, defaults) {
@@ -3582,6 +3603,11 @@ var RouteBuilder = class _RouteBuilder {
3582
3603
  `route '${this.#s.key}': price '${pricing}' must be a positive decimal string`
3583
3604
  );
3584
3605
  }
3606
+ if (typeof pricing === "function" && next.#s.maxPrice === void 0) {
3607
+ throw new Error(
3608
+ `route '${this.#s.key}': dynamic pricing requires maxPrice \u2014 without it, bare probes would advertise a $0 challenge`
3609
+ );
3610
+ }
3585
3611
  if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
3586
3612
  throw new Error(
3587
3613
  `route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
@@ -3950,6 +3976,11 @@ var RouteBuilder = class _RouteBuilder {
3950
3976
  `route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
3951
3977
  );
3952
3978
  }
3979
+ if (this.#s.description !== void 0 && this.#s.description.length > MAX_X402_DESCRIPTION_LENGTH && this.#s.pricing !== void 0 && this.#s.protocols.includes("x402")) {
3980
+ throw new Error(
3981
+ `route '${this.#s.key}': .description() is ${this.#s.description.length} chars; must be \u2264 ${MAX_X402_DESCRIPTION_LENGTH} chars \u2014 the CDP x402 facilitator rejects payments whose 402 challenge resource.description exceeds ~500 chars.`
3982
+ );
3983
+ }
3953
3984
  validateExamples(
3954
3985
  this.#s.key,
3955
3986
  this.#s.bodySchema,
@@ -4405,6 +4436,8 @@ var envShape = {
4405
4436
  params: { code: "invalid_base_url" },
4406
4437
  message: "BASE_URL must be a valid URL \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Must match the public domain."
4407
4438
  }).optional(),
4439
+ VERCEL_PROJECT_PRODUCTION_URL: import_zod.z.string().optional(),
4440
+ VERCEL_URL: import_zod.z.string().optional(),
4408
4441
  EVM_PAYEE_ADDRESS: import_zod.z.string().refine(isEvmAddress, {
4409
4442
  params: { code: "invalid_x402_payee", ...x402 },
4410
4443
  message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
@@ -4449,7 +4482,7 @@ var EnvInputSchema = import_zod.z.object(envShape).passthrough().superRefine((en
4449
4482
  addIssue(
4450
4483
  ctx,
4451
4484
  { code: "missing_base_url" },
4452
- "BASE_URL is required \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Set it to your production domain.",
4485
+ "BASE_URL is required \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Set it to your production domain. On Vercel, the router auto-derives it from VERCEL_PROJECT_PRODUCTION_URL or VERCEL_URL.",
4453
4486
  ["BASE_URL"]
4454
4487
  );
4455
4488
  }
@@ -4530,6 +4563,19 @@ function collectKvWarnings(env, kvStoreOptionProvided) {
4530
4563
  }
4531
4564
  return [];
4532
4565
  }
4566
+ function withHttps(host) {
4567
+ if (!host) return void 0;
4568
+ return host.startsWith("http://") || host.startsWith("https://") ? host : `https://${host}`;
4569
+ }
4570
+ function deriveBaseUrlEnv(env) {
4571
+ if (env.BASE_URL) return env;
4572
+ const vercelBaseUrl = withHttps(env.VERCEL_PROJECT_PRODUCTION_URL) ?? withHttps(env.VERCEL_URL);
4573
+ if (!vercelBaseUrl) return env;
4574
+ return {
4575
+ ...env,
4576
+ BASE_URL: vercelBaseUrl
4577
+ };
4578
+ }
4533
4579
  function getConfiguredX402Accepts2(config) {
4534
4580
  if (config.x402?.accepts?.length) return [...config.x402.accepts];
4535
4581
  return [
@@ -4698,7 +4744,7 @@ function translateZodIssues(error) {
4698
4744
  }
4699
4745
  function routerConfigFromEnv(options) {
4700
4746
  const rawEnv = options.env ?? process.env;
4701
- const env = trimAll(rawEnv);
4747
+ const env = deriveBaseUrlEnv(trimAll(rawEnv));
4702
4748
  const optionIssues = [];
4703
4749
  if (!options.title?.trim()) {
4704
4750
  optionIssues.push({
package/dist/index.js CHANGED
@@ -1897,8 +1897,15 @@ function tagBareDecimalAsDollars(amount) {
1897
1897
  import { VerifyError } from "@x402/core/types";
1898
1898
  async function verifyX402Payment(opts) {
1899
1899
  const { server, request, price, accepts, report } = opts;
1900
- const payload = await readPaymentPayload(request);
1901
- if (!payload) return null;
1900
+ const payment = await readPaymentPayload(request);
1901
+ if (payment.kind === "none") return null;
1902
+ if (payment.kind === "malformed") {
1903
+ return invalidPaymentVerification({
1904
+ reason: "malformed_payment_header",
1905
+ message: `X-PAYMENT header could not be decoded: ${payment.message}`
1906
+ });
1907
+ }
1908
+ const payload = payment.payload;
1902
1909
  const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
1903
1910
  const matching = findVerifiableRequirements(server, requirements, payload);
1904
1911
  const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
@@ -1959,9 +1966,16 @@ function matchesStableFields(requirement, accepted) {
1959
1966
  }
1960
1967
  async function readPaymentPayload(request) {
1961
1968
  const paymentHeader = request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY);
1962
- if (!paymentHeader) return null;
1969
+ if (!paymentHeader) return { kind: "none" };
1963
1970
  const { decodePaymentSignatureHeader } = await import("@x402/core/http");
1964
- return decodePaymentSignatureHeader(paymentHeader);
1971
+ try {
1972
+ return { kind: "ok", payload: decodePaymentSignatureHeader(paymentHeader) };
1973
+ } catch (err) {
1974
+ return {
1975
+ kind: "malformed",
1976
+ message: err instanceof Error ? err.message : String(err)
1977
+ };
1978
+ }
1965
1979
  }
1966
1980
  function invalidPaymentVerification(failure) {
1967
1981
  return {
@@ -2136,6 +2150,21 @@ function getAllowedStrategies(allowed) {
2136
2150
  return allowed.map((name) => STRATEGIES[name]);
2137
2151
  }
2138
2152
 
2153
+ // src/protocols/detect.ts
2154
+ function detectProtocol(request) {
2155
+ if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
2156
+ return "x402";
2157
+ }
2158
+ const auth = request.headers.get(HEADERS.AUTHORIZATION);
2159
+ if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
2160
+ return "mpp";
2161
+ }
2162
+ if (request.headers.get(HEADERS.SIWX)) {
2163
+ return "siwx";
2164
+ }
2165
+ return null;
2166
+ }
2167
+
2139
2168
  // src/pipeline/flows/api-key-only.ts
2140
2169
  async function runApiKeyOnlyFlow(ctx) {
2141
2170
  if (!ctx.routeEntry.apiKeyResolver) {
@@ -2151,6 +2180,11 @@ async function runApiKeyOnlyFlow(ctx) {
2151
2180
  var DynamicPricing = class {
2152
2181
  constructor(opts) {
2153
2182
  this.opts = opts;
2183
+ if (!isPositiveDecimal(opts.maxPrice)) {
2184
+ throw new Error(
2185
+ `route '${opts.route ?? "unknown"}': dynamic pricing requires a positive maxPrice, got '${opts.maxPrice}'`
2186
+ );
2187
+ }
2154
2188
  }
2155
2189
  needsBody = true;
2156
2190
  async quote(body) {
@@ -2164,11 +2198,8 @@ var DynamicPricing = class {
2164
2198
  error: err instanceof Error ? err.stack : String(err),
2165
2199
  body
2166
2200
  });
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;
2201
+ this.alert("warn", `Using maxPrice ${this.opts.maxPrice} as fallback after pricing error`);
2202
+ return this.opts.maxPrice;
2172
2203
  }
2173
2204
  if (!isPositiveDecimal(priced)) {
2174
2205
  throw new HttpError(
@@ -2179,18 +2210,17 @@ var DynamicPricing = class {
2179
2210
  return priced;
2180
2211
  }
2181
2212
  challengeQuote(body) {
2182
- if (body === void 0) return Promise.resolve(this.opts.maxPrice ?? "0");
2213
+ if (body === void 0) return Promise.resolve(this.opts.maxPrice);
2183
2214
  return this.quote(body);
2184
2215
  }
2185
2216
  describe() {
2186
2217
  return {
2187
2218
  mode: "dynamic",
2188
2219
  min: this.opts.minPrice ?? "0",
2189
- max: this.opts.maxPrice ?? "0"
2220
+ max: this.opts.maxPrice
2190
2221
  };
2191
2222
  }
2192
2223
  cap(raw, body) {
2193
- if (!this.opts.maxPrice) return raw;
2194
2224
  let overCap;
2195
2225
  try {
2196
2226
  overCap = compareDecimals(raw, this.opts.maxPrice) > 0;
@@ -2290,6 +2320,9 @@ function selectPricing(raw, deps = {}) {
2290
2320
  return new FixedPricing(raw);
2291
2321
  }
2292
2322
  if (typeof raw === "function") {
2323
+ if (!deps.maxPrice) {
2324
+ throw new Error(`route '${deps.route ?? "unknown"}': dynamic pricing requires maxPrice`);
2325
+ }
2293
2326
  return new DynamicPricing({
2294
2327
  fn: raw,
2295
2328
  maxPrice: deps.maxPrice,
@@ -3174,21 +3207,6 @@ async function createKvMppStore(kv, options) {
3174
3207
  });
3175
3208
  }
3176
3209
 
3177
- // src/protocols/detect.ts
3178
- function detectProtocol(request) {
3179
- if (request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY)) {
3180
- return "x402";
3181
- }
3182
- const auth = request.headers.get(HEADERS.AUTHORIZATION);
3183
- if (auth && auth.startsWith(AUTH_SCHEME.MPP_PAYMENT)) {
3184
- return "mpp";
3185
- }
3186
- if (request.headers.get(HEADERS.SIWX)) {
3187
- return "siwx";
3188
- }
3189
- return null;
3190
- }
3191
-
3192
3210
  // src/protocols/mpp/siwx-mode.ts
3193
3211
  import { Credential as Credential2 } from "mppx";
3194
3212
  async function verifyMppSiwx(request, mppx) {
@@ -3211,8 +3229,6 @@ async function runSiwxOnlyFlow(ctx) {
3211
3229
  if (earlyBody.ok) {
3212
3230
  const validateErr = await runValidate(ctx, earlyBody.data);
3213
3231
  if (validateErr) return validateErr;
3214
- } else {
3215
- return earlyBody.response;
3216
3232
  }
3217
3233
  }
3218
3234
  const siwxHeader = request.headers.get(HEADERS.SIWX);
@@ -3345,9 +3361,13 @@ async function runUnprotectedFlow(ctx) {
3345
3361
 
3346
3362
  // src/pipeline/orchestrate.ts
3347
3363
  function shouldSkipQueryValidation(routeEntry, request) {
3348
- const isPaidRoute = !!routeEntry.pricing || routeEntry.authMode === "paid";
3349
- if (!isPaidRoute) return false;
3350
- return selectIncomingStrategy(request, routeEntry.protocols) === null;
3364
+ if (routeEntry.pricing || routeEntry.authMode === "paid") {
3365
+ return selectIncomingStrategy(request, routeEntry.protocols) === null;
3366
+ }
3367
+ if (routeEntry.authMode === "siwx") {
3368
+ return !request.headers.get(HEADERS.SIWX) && detectProtocol(request) !== "mpp";
3369
+ }
3370
+ return false;
3351
3371
  }
3352
3372
  function createRequestHandler(routeEntry, handler, deps) {
3353
3373
  return async (request) => {
@@ -3398,6 +3418,7 @@ ${issues}`
3398
3418
  }
3399
3419
 
3400
3420
  // src/builder.ts
3421
+ var MAX_X402_DESCRIPTION_LENGTH = 400;
3401
3422
  var RouteBuilder = class _RouteBuilder {
3402
3423
  #s;
3403
3424
  constructor(key, registry, deps, defaults) {
@@ -3541,6 +3562,11 @@ var RouteBuilder = class _RouteBuilder {
3541
3562
  `route '${this.#s.key}': price '${pricing}' must be a positive decimal string`
3542
3563
  );
3543
3564
  }
3565
+ if (typeof pricing === "function" && next.#s.maxPrice === void 0) {
3566
+ throw new Error(
3567
+ `route '${this.#s.key}': dynamic pricing requires maxPrice \u2014 without it, bare probes would advertise a $0 challenge`
3568
+ );
3569
+ }
3544
3570
  if (next.#s.maxPrice !== void 0 && !isPositiveDecimal(next.#s.maxPrice)) {
3545
3571
  throw new Error(
3546
3572
  `route '${this.#s.key}': maxPrice '${next.#s.maxPrice}' must be a positive decimal string`
@@ -3909,6 +3935,11 @@ var RouteBuilder = class _RouteBuilder {
3909
3935
  `route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
3910
3936
  );
3911
3937
  }
3938
+ if (this.#s.description !== void 0 && this.#s.description.length > MAX_X402_DESCRIPTION_LENGTH && this.#s.pricing !== void 0 && this.#s.protocols.includes("x402")) {
3939
+ throw new Error(
3940
+ `route '${this.#s.key}': .description() is ${this.#s.description.length} chars; must be \u2264 ${MAX_X402_DESCRIPTION_LENGTH} chars \u2014 the CDP x402 facilitator rejects payments whose 402 challenge resource.description exceeds ~500 chars.`
3941
+ );
3942
+ }
3912
3943
  validateExamples(
3913
3944
  this.#s.key,
3914
3945
  this.#s.bodySchema,
@@ -4364,6 +4395,8 @@ var envShape = {
4364
4395
  params: { code: "invalid_base_url" },
4365
4396
  message: "BASE_URL must be a valid URL \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Must match the public domain."
4366
4397
  }).optional(),
4398
+ VERCEL_PROJECT_PRODUCTION_URL: z.string().optional(),
4399
+ VERCEL_URL: z.string().optional(),
4367
4400
  EVM_PAYEE_ADDRESS: z.string().refine(isEvmAddress, {
4368
4401
  params: { code: "invalid_x402_payee", ...x402 },
4369
4402
  message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
@@ -4408,7 +4441,7 @@ var EnvInputSchema = z.object(envShape).passthrough().superRefine((env, ctx) =>
4408
4441
  addIssue(
4409
4442
  ctx,
4410
4443
  { code: "missing_base_url" },
4411
- "BASE_URL is required \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Set it to your production domain.",
4444
+ "BASE_URL is required \u2014 the public origin used as the 402 realm, OpenAPI server URL, and MPP memo prefix. Set it to your production domain. On Vercel, the router auto-derives it from VERCEL_PROJECT_PRODUCTION_URL or VERCEL_URL.",
4412
4445
  ["BASE_URL"]
4413
4446
  );
4414
4447
  }
@@ -4489,6 +4522,19 @@ function collectKvWarnings(env, kvStoreOptionProvided) {
4489
4522
  }
4490
4523
  return [];
4491
4524
  }
4525
+ function withHttps(host) {
4526
+ if (!host) return void 0;
4527
+ return host.startsWith("http://") || host.startsWith("https://") ? host : `https://${host}`;
4528
+ }
4529
+ function deriveBaseUrlEnv(env) {
4530
+ if (env.BASE_URL) return env;
4531
+ const vercelBaseUrl = withHttps(env.VERCEL_PROJECT_PRODUCTION_URL) ?? withHttps(env.VERCEL_URL);
4532
+ if (!vercelBaseUrl) return env;
4533
+ return {
4534
+ ...env,
4535
+ BASE_URL: vercelBaseUrl
4536
+ };
4537
+ }
4492
4538
  function getConfiguredX402Accepts2(config) {
4493
4539
  if (config.x402?.accepts?.length) return [...config.x402.accepts];
4494
4540
  return [
@@ -4657,7 +4703,7 @@ function translateZodIssues(error) {
4657
4703
  }
4658
4704
  function routerConfigFromEnv(options) {
4659
4705
  const rawEnv = options.env ?? process.env;
4660
- const env = trimAll(rawEnv);
4706
+ const env = deriveBaseUrlEnv(trimAll(rawEnv));
4661
4707
  const optionIssues = [];
4662
4708
  if (!options.title?.trim()) {
4663
4709
  optionIssues.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "1.9.4",
3
+ "version": "1.10.1",
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": {