@agentcash/router 1.10.0 → 1.10.2

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
@@ -306,4 +313,3 @@ const loggingPlugin: RouterPlugin = {
306
313
  },
307
314
  };
308
315
  ```
309
-
package/dist/index.cjs CHANGED
@@ -412,7 +412,7 @@ async function createX402Server(config, kvStore) {
412
412
  const { x402ResourceServer, HTTPFacilitatorClient } = await import("@x402/core/server");
413
413
  const { registerExactEvmScheme } = await import("@x402/evm/exact/server");
414
414
  const { bazaarResourceServerExtension } = await import("@x402/extensions/bazaar");
415
- const { siwxResourceServerExtension } = await import("@x402/extensions/sign-in-with-x");
415
+ const { createSIWxResourceServerExtension, InMemorySIWxStorage } = await import("@x402/extensions/sign-in-with-x");
416
416
  const { facilitator: defaultFacilitator } = await import("@coinbase/x402");
417
417
  const configuredNetworks = getConfiguredX402Networks(config);
418
418
  const facilitatorsByNetwork = getResolvedX402Facilitators(
@@ -442,7 +442,9 @@ async function createX402Server(config, kvStore) {
442
442
  registerExactSvmScheme(server, { networks: svmNetworks });
443
443
  }
444
444
  server.registerExtension(bazaarResourceServerExtension);
445
- server.registerExtension(siwxResourceServerExtension);
445
+ server.registerExtension(
446
+ createSIWxResourceServerExtension({ storage: new InMemorySIWxStorage() })
447
+ );
446
448
  const initPromise = server.initialize();
447
449
  return {
448
450
  server,
@@ -1938,8 +1940,15 @@ function tagBareDecimalAsDollars(amount) {
1938
1940
  var import_types2 = require("@x402/core/types");
1939
1941
  async function verifyX402Payment(opts) {
1940
1942
  const { server, request, price, accepts, report } = opts;
1941
- const payload = await readPaymentPayload(request);
1942
- if (!payload) return null;
1943
+ const payment = await readPaymentPayload(request);
1944
+ if (payment.kind === "none") return null;
1945
+ if (payment.kind === "malformed") {
1946
+ return invalidPaymentVerification({
1947
+ reason: "malformed_payment_header",
1948
+ message: `X-PAYMENT header could not be decoded: ${payment.message}`
1949
+ });
1950
+ }
1951
+ const payload = payment.payload;
1943
1952
  const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
1944
1953
  const matching = findVerifiableRequirements(server, requirements, payload);
1945
1954
  const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
@@ -2000,9 +2009,16 @@ function matchesStableFields(requirement, accepted) {
2000
2009
  }
2001
2010
  async function readPaymentPayload(request) {
2002
2011
  const paymentHeader = request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY);
2003
- if (!paymentHeader) return null;
2012
+ if (!paymentHeader) return { kind: "none" };
2004
2013
  const { decodePaymentSignatureHeader } = await import("@x402/core/http");
2005
- return decodePaymentSignatureHeader(paymentHeader);
2014
+ try {
2015
+ return { kind: "ok", payload: decodePaymentSignatureHeader(paymentHeader) };
2016
+ } catch (err) {
2017
+ return {
2018
+ kind: "malformed",
2019
+ message: err instanceof Error ? err.message : String(err)
2020
+ };
2021
+ }
2006
2022
  }
2007
2023
  function invalidPaymentVerification(failure) {
2008
2024
  return {
@@ -3445,6 +3461,7 @@ ${issues}`
3445
3461
  }
3446
3462
 
3447
3463
  // src/builder.ts
3464
+ var MAX_X402_DESCRIPTION_LENGTH = 400;
3448
3465
  var RouteBuilder = class _RouteBuilder {
3449
3466
  #s;
3450
3467
  constructor(key, registry, deps, defaults) {
@@ -3961,6 +3978,11 @@ var RouteBuilder = class _RouteBuilder {
3961
3978
  `route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
3962
3979
  );
3963
3980
  }
3981
+ if (this.#s.description !== void 0 && this.#s.description.length > MAX_X402_DESCRIPTION_LENGTH && this.#s.pricing !== void 0 && this.#s.protocols.includes("x402")) {
3982
+ throw new Error(
3983
+ `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.`
3984
+ );
3985
+ }
3964
3986
  validateExamples(
3965
3987
  this.#s.key,
3966
3988
  this.#s.bodySchema,
@@ -4416,6 +4438,8 @@ var envShape = {
4416
4438
  params: { code: "invalid_base_url" },
4417
4439
  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."
4418
4440
  }).optional(),
4441
+ VERCEL_PROJECT_PRODUCTION_URL: import_zod.z.string().optional(),
4442
+ VERCEL_URL: import_zod.z.string().optional(),
4419
4443
  EVM_PAYEE_ADDRESS: import_zod.z.string().refine(isEvmAddress, {
4420
4444
  params: { code: "invalid_x402_payee", ...x402 },
4421
4445
  message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
@@ -4460,7 +4484,7 @@ var EnvInputSchema = import_zod.z.object(envShape).passthrough().superRefine((en
4460
4484
  addIssue(
4461
4485
  ctx,
4462
4486
  { code: "missing_base_url" },
4463
- "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.",
4487
+ "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.",
4464
4488
  ["BASE_URL"]
4465
4489
  );
4466
4490
  }
@@ -4541,6 +4565,19 @@ function collectKvWarnings(env, kvStoreOptionProvided) {
4541
4565
  }
4542
4566
  return [];
4543
4567
  }
4568
+ function withHttps(host) {
4569
+ if (!host) return void 0;
4570
+ return host.startsWith("http://") || host.startsWith("https://") ? host : `https://${host}`;
4571
+ }
4572
+ function deriveBaseUrlEnv(env) {
4573
+ if (env.BASE_URL) return env;
4574
+ const vercelBaseUrl = withHttps(env.VERCEL_PROJECT_PRODUCTION_URL) ?? withHttps(env.VERCEL_URL);
4575
+ if (!vercelBaseUrl) return env;
4576
+ return {
4577
+ ...env,
4578
+ BASE_URL: vercelBaseUrl
4579
+ };
4580
+ }
4544
4581
  function getConfiguredX402Accepts2(config) {
4545
4582
  if (config.x402?.accepts?.length) return [...config.x402.accepts];
4546
4583
  return [
@@ -4709,7 +4746,7 @@ function translateZodIssues(error) {
4709
4746
  }
4710
4747
  function routerConfigFromEnv(options) {
4711
4748
  const rawEnv = options.env ?? process.env;
4712
- const env = trimAll(rawEnv);
4749
+ const env = deriveBaseUrlEnv(trimAll(rawEnv));
4713
4750
  const optionIssues = [];
4714
4751
  if (!options.title?.trim()) {
4715
4752
  optionIssues.push({
package/dist/index.js CHANGED
@@ -390,7 +390,7 @@ async function createX402Server(config, kvStore) {
390
390
  const { x402ResourceServer, HTTPFacilitatorClient } = await import("@x402/core/server");
391
391
  const { registerExactEvmScheme } = await import("@x402/evm/exact/server");
392
392
  const { bazaarResourceServerExtension } = await import("@x402/extensions/bazaar");
393
- const { siwxResourceServerExtension } = await import("@x402/extensions/sign-in-with-x");
393
+ const { createSIWxResourceServerExtension, InMemorySIWxStorage } = await import("@x402/extensions/sign-in-with-x");
394
394
  const { facilitator: defaultFacilitator } = await import("@coinbase/x402");
395
395
  const configuredNetworks = getConfiguredX402Networks(config);
396
396
  const facilitatorsByNetwork = getResolvedX402Facilitators(
@@ -420,7 +420,9 @@ async function createX402Server(config, kvStore) {
420
420
  registerExactSvmScheme(server, { networks: svmNetworks });
421
421
  }
422
422
  server.registerExtension(bazaarResourceServerExtension);
423
- server.registerExtension(siwxResourceServerExtension);
423
+ server.registerExtension(
424
+ createSIWxResourceServerExtension({ storage: new InMemorySIWxStorage() })
425
+ );
424
426
  const initPromise = server.initialize();
425
427
  return {
426
428
  server,
@@ -1897,8 +1899,15 @@ function tagBareDecimalAsDollars(amount) {
1897
1899
  import { VerifyError } from "@x402/core/types";
1898
1900
  async function verifyX402Payment(opts) {
1899
1901
  const { server, request, price, accepts, report } = opts;
1900
- const payload = await readPaymentPayload(request);
1901
- if (!payload) return null;
1902
+ const payment = await readPaymentPayload(request);
1903
+ if (payment.kind === "none") return null;
1904
+ if (payment.kind === "malformed") {
1905
+ return invalidPaymentVerification({
1906
+ reason: "malformed_payment_header",
1907
+ message: `X-PAYMENT header could not be decoded: ${payment.message}`
1908
+ });
1909
+ }
1910
+ const payload = payment.payload;
1902
1911
  const requirements = await buildExpectedRequirements(server, request, price, accepts, report);
1903
1912
  const matching = findVerifiableRequirements(server, requirements, payload);
1904
1913
  const accepted = payload.x402Version === 2 ? payload.accepted : void 0;
@@ -1959,9 +1968,16 @@ function matchesStableFields(requirement, accepted) {
1959
1968
  }
1960
1969
  async function readPaymentPayload(request) {
1961
1970
  const paymentHeader = request.headers.get(HEADERS.X402_PAYMENT_SIGNATURE) ?? request.headers.get(HEADERS.X402_PAYMENT_LEGACY);
1962
- if (!paymentHeader) return null;
1971
+ if (!paymentHeader) return { kind: "none" };
1963
1972
  const { decodePaymentSignatureHeader } = await import("@x402/core/http");
1964
- return decodePaymentSignatureHeader(paymentHeader);
1973
+ try {
1974
+ return { kind: "ok", payload: decodePaymentSignatureHeader(paymentHeader) };
1975
+ } catch (err) {
1976
+ return {
1977
+ kind: "malformed",
1978
+ message: err instanceof Error ? err.message : String(err)
1979
+ };
1980
+ }
1965
1981
  }
1966
1982
  function invalidPaymentVerification(failure) {
1967
1983
  return {
@@ -3404,6 +3420,7 @@ ${issues}`
3404
3420
  }
3405
3421
 
3406
3422
  // src/builder.ts
3423
+ var MAX_X402_DESCRIPTION_LENGTH = 400;
3407
3424
  var RouteBuilder = class _RouteBuilder {
3408
3425
  #s;
3409
3426
  constructor(key, registry, deps, defaults) {
@@ -3920,6 +3937,11 @@ var RouteBuilder = class _RouteBuilder {
3920
3937
  `route '${this.#s.key}': .stream() requires .metered() \u2014 static/free/upto routes can't meter per-chunk billing.`
3921
3938
  );
3922
3939
  }
3940
+ if (this.#s.description !== void 0 && this.#s.description.length > MAX_X402_DESCRIPTION_LENGTH && this.#s.pricing !== void 0 && this.#s.protocols.includes("x402")) {
3941
+ throw new Error(
3942
+ `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.`
3943
+ );
3944
+ }
3923
3945
  validateExamples(
3924
3946
  this.#s.key,
3925
3947
  this.#s.bodySchema,
@@ -4375,6 +4397,8 @@ var envShape = {
4375
4397
  params: { code: "invalid_base_url" },
4376
4398
  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."
4377
4399
  }).optional(),
4400
+ VERCEL_PROJECT_PRODUCTION_URL: z.string().optional(),
4401
+ VERCEL_URL: z.string().optional(),
4378
4402
  EVM_PAYEE_ADDRESS: z.string().refine(isEvmAddress, {
4379
4403
  params: { code: "invalid_x402_payee", ...x402 },
4380
4404
  message: "EVM_PAYEE_ADDRESS must be a 0x-prefixed 20-byte EVM address \u2014 the wallet that receives x402 and MPP payments."
@@ -4419,7 +4443,7 @@ var EnvInputSchema = z.object(envShape).passthrough().superRefine((env, ctx) =>
4419
4443
  addIssue(
4420
4444
  ctx,
4421
4445
  { code: "missing_base_url" },
4422
- "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.",
4446
+ "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.",
4423
4447
  ["BASE_URL"]
4424
4448
  );
4425
4449
  }
@@ -4500,6 +4524,19 @@ function collectKvWarnings(env, kvStoreOptionProvided) {
4500
4524
  }
4501
4525
  return [];
4502
4526
  }
4527
+ function withHttps(host) {
4528
+ if (!host) return void 0;
4529
+ return host.startsWith("http://") || host.startsWith("https://") ? host : `https://${host}`;
4530
+ }
4531
+ function deriveBaseUrlEnv(env) {
4532
+ if (env.BASE_URL) return env;
4533
+ const vercelBaseUrl = withHttps(env.VERCEL_PROJECT_PRODUCTION_URL) ?? withHttps(env.VERCEL_URL);
4534
+ if (!vercelBaseUrl) return env;
4535
+ return {
4536
+ ...env,
4537
+ BASE_URL: vercelBaseUrl
4538
+ };
4539
+ }
4503
4540
  function getConfiguredX402Accepts2(config) {
4504
4541
  if (config.x402?.accepts?.length) return [...config.x402.accepts];
4505
4542
  return [
@@ -4668,7 +4705,7 @@ function translateZodIssues(error) {
4668
4705
  }
4669
4706
  function routerConfigFromEnv(options) {
4670
4707
  const rawEnv = options.env ?? process.env;
4671
- const env = trimAll(rawEnv);
4708
+ const env = deriveBaseUrlEnv(trimAll(rawEnv));
4672
4709
  const optionIssues = [];
4673
4710
  if (!options.title?.trim()) {
4674
4711
  optionIssues.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "1.10.0",
3
+ "version": "1.10.2",
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": {
@@ -28,10 +28,10 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "@coinbase/x402": "^2.1.0",
31
- "@x402/core": "^2.11.0",
32
- "@x402/evm": "^2.11.0",
33
- "@x402/extensions": "^2.11.0",
34
- "@x402/svm": "^2.11.0",
31
+ "@x402/core": "^2.13.0",
32
+ "@x402/evm": "^2.13.0",
33
+ "@x402/extensions": "^2.13.0",
34
+ "@x402/svm": "^2.13.0",
35
35
  "mppx": "^0.6.16",
36
36
  "viem": "^2.47.6",
37
37
  "zod-openapi": "^5.0.0"