@agentcash/router 1.2.3 → 1.2.5

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.
@@ -104,6 +104,7 @@ Response out
104
104
  | `src/server.ts` | x402 server initialization with retry |
105
105
  | `src/auth/siwx.ts` | SIWX verification |
106
106
  | `src/auth/api-key.ts` | API key verification |
107
+ | `src/upstash-rest.ts` | Minimal fetch-only Upstash REST client for `useDefaultStore` |
107
108
  | `src/auth/nonce.ts` | `NonceStore` interface + `MemoryNonceStore` |
108
109
  | `src/discovery/well-known.ts` | `.well-known/x402` generation |
109
110
  | `src/discovery/openapi.ts` | OpenAPI 3.1 spec generation |
@@ -180,6 +181,7 @@ export const router = createRouter({
180
181
  currency: '0x20c0000000000000000000000000000000000000', // PathUSD on Tempo
181
182
  recipient: process.env.X402_PAYEE_ADDRESS!,
182
183
  rpcUrl: process.env.TEMPO_RPC_URL, // falls back to TEMPO_RPC_URL env var
184
+ useDefaultStore: true, // auto-configures Upstash from KV_REST_API_URL + KV_REST_API_TOKEN
183
185
  },
184
186
  siwx: { nonceStore }, // custom nonce store
185
187
  });
@@ -420,6 +422,48 @@ The type system (generic parameters `HasAuth`, `NeedsBody`, `HasBody`) prevents
420
422
  - `.siwx()` is mutually exclusive with `.paid()`
421
423
  - `.apiKey()` CAN compose with `.paid()`
422
424
 
425
+ ## MPP Persistent Store
426
+
427
+ mppx uses a key-value store for transaction hash replay protection. Without a persistent store, `Store.memory()` is used — which is wiped on every cold start. This is unsafe on Vercel or any multi-instance deployment.
428
+
429
+ ### Vercel (zero config)
430
+
431
+ Set `useDefaultStore: true` to auto-configure an Upstash-backed store from Vercel KV environment variables (`KV_REST_API_URL` + `KV_REST_API_TOKEN`). Uses raw `fetch` — no extra npm dependencies.
432
+
433
+ ```typescript
434
+ createRouter({
435
+ mpp: {
436
+ secretKey: process.env.MPP_SECRET_KEY!,
437
+ currency: USDC,
438
+ useDefaultStore: true, // reads KV_REST_API_URL + KV_REST_API_TOKEN automatically
439
+ }
440
+ })
441
+ ```
442
+
443
+ ### Cloudflare / custom
444
+
445
+ Pass any `Store.Store` implementation directly via `mpp.store`:
446
+
447
+ ```typescript
448
+ import { Store } from 'mppx'
449
+
450
+ createRouter({
451
+ mpp: {
452
+ secretKey: process.env.MPP_SECRET_KEY!,
453
+ currency: USDC,
454
+ store: Store.cloudflare(env.MY_KV_NAMESPACE),
455
+ }
456
+ })
457
+ ```
458
+
459
+ Available adapters from `mppx`: `Store.upstash(redis)`, `Store.cloudflare(kv)`, `Store.redis(client)`, `Store.memory()`, `Store.from(custom)`.
460
+
461
+ ### Resolution order
462
+
463
+ 1. Explicit `store` wins if provided
464
+ 2. `useDefaultStore: true` creates an Upstash store from env vars
465
+ 3. Neither → mppx defaults to `Store.memory()`
466
+
423
467
  ## MPP Internals
424
468
 
425
469
  The router uses `mppx`'s high-level `Mppx.create()` API, which encapsulates the entire challenge-credential-receipt lifecycle.
@@ -509,6 +553,7 @@ Barrel validation catches mismatches: keys in `prices` but not registered → er
509
553
  | MPP 401 `unauthorized: authentication required` | Using default unauthenticated Tempo RPC | Set `TEMPO_RPC_URL` env var or `mpp.rpcUrl` config with authenticated URL |
510
554
  | `route 'X' in prices map but not registered` | Discovery endpoint hit before route module loaded | Add barrel import to discovery route files |
511
555
  | `mppx package is required` | mppx not installed | `pnpm add mppx` — it's an optional peer dep |
556
+ | `useDefaultStore requires KV_REST_API_URL` | Vercel KV env vars not set | Add Vercel KV integration or set `KV_REST_API_URL` + `KV_REST_API_TOKEN` manually |
512
557
 
513
558
  ## Maintaining This Skill
514
559
 
package/dist/index.cjs CHANGED
@@ -189,10 +189,10 @@ var init_x402_facilitators = __esm({
189
189
  });
190
190
 
191
191
  // src/x402-config.ts
192
- async function resolvePayToValue(payTo, request, fallback) {
192
+ async function resolvePayToValue(payTo, request, fallback, body) {
193
193
  if (!payTo) return fallback;
194
194
  if (typeof payTo === "string") return payTo;
195
- return payTo(request);
195
+ return payTo(request, body);
196
196
  }
197
197
  function getConfiguredX402Accepts(config) {
198
198
  if (config.x402?.accepts?.length) {
@@ -209,12 +209,17 @@ function getConfiguredX402Accepts(config) {
209
209
  function getConfiguredX402Networks(config) {
210
210
  return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
211
211
  }
212
- async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo) {
212
+ async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, body) {
213
213
  return Promise.all(
214
214
  accepts.map(async (accept) => ({
215
215
  network: accept.network,
216
216
  scheme: accept.scheme ?? "exact",
217
- payTo: await resolvePayToValue(accept.payTo ?? routeEntry.payTo, request, fallbackPayTo),
217
+ payTo: await resolvePayToValue(
218
+ routeEntry.payTo ?? accept.payTo,
219
+ request,
220
+ fallbackPayTo,
221
+ body
222
+ ),
218
223
  ...accept.asset ? { asset: accept.asset } : {},
219
224
  ...accept.decimals !== void 0 ? { decimals: accept.decimals } : {},
220
225
  ...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
@@ -307,6 +312,47 @@ var init_server = __esm({
307
312
  }
308
313
  });
309
314
 
315
+ // src/upstash-rest.ts
316
+ var upstash_rest_exports = {};
317
+ __export(upstash_rest_exports, {
318
+ createUpstashRest: () => createUpstashRest
319
+ });
320
+ function createUpstashRest(url, token) {
321
+ const base = url.replace(/\/+$/, "");
322
+ const headers = { Authorization: `Bearer ${token}` };
323
+ return {
324
+ async get(key) {
325
+ const res = await fetch(`${base}/get/${key}`, { headers });
326
+ if (!res.ok) throw new Error(`[upstash-rest] GET ${key}: ${res.status}`);
327
+ const { result } = await res.json();
328
+ return result ?? null;
329
+ },
330
+ async set(key, value) {
331
+ const res = await fetch(`${base}`, {
332
+ method: "POST",
333
+ headers: { ...headers, "Content-Type": "application/json" },
334
+ body: JSON.stringify(["SET", key, JSON.stringify(value)])
335
+ });
336
+ if (!res.ok) throw new Error(`[upstash-rest] SET ${key}: ${res.status}`);
337
+ return await res.json();
338
+ },
339
+ async del(key) {
340
+ const res = await fetch(`${base}`, {
341
+ method: "POST",
342
+ headers: { ...headers, "Content-Type": "application/json" },
343
+ body: JSON.stringify(["DEL", key])
344
+ });
345
+ if (!res.ok) throw new Error(`[upstash-rest] DEL ${key}: ${res.status}`);
346
+ return await res.json();
347
+ }
348
+ };
349
+ }
350
+ var init_upstash_rest = __esm({
351
+ "src/upstash-rest.ts"() {
352
+ "use strict";
353
+ }
354
+ });
355
+
310
356
  // src/index.ts
311
357
  var index_exports = {};
312
358
  __export(index_exports, {
@@ -1302,7 +1348,8 @@ function createRequestHandler(routeEntry, handler, deps) {
1302
1348
  request,
1303
1349
  routeEntry,
1304
1350
  deps.x402Accepts,
1305
- deps.payeeAddress
1351
+ deps.payeeAddress,
1352
+ body.data
1306
1353
  );
1307
1354
  const verify = await verifyX402Payment({
1308
1355
  server: deps.x402Server,
@@ -1733,7 +1780,8 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
1733
1780
  request,
1734
1781
  routeEntry,
1735
1782
  deps.x402Accepts,
1736
- deps.payeeAddress
1783
+ deps.payeeAddress,
1784
+ bodyData
1737
1785
  );
1738
1786
  const { encoded } = await buildX402Challenge({
1739
1787
  server: deps.x402Server,
@@ -2474,8 +2522,21 @@ function createRouter(config) {
2474
2522
  const getClient = async () => deps.tempoClient;
2475
2523
  let feePayerAccount;
2476
2524
  if (config.mpp.feePayerKey) {
2477
- const { Account } = await import("viem/tempo");
2478
- feePayerAccount = Account.fromSecp256k1(config.mpp.feePayerKey);
2525
+ const { privateKeyToAccount } = await import("viem/accounts");
2526
+ feePayerAccount = privateKeyToAccount(config.mpp.feePayerKey);
2527
+ }
2528
+ let resolvedStore = config.mpp.store;
2529
+ if (!resolvedStore && config.mpp.useDefaultStore) {
2530
+ const kvUrl = process.env.KV_REST_API_URL;
2531
+ const kvToken = process.env.KV_REST_API_TOKEN;
2532
+ if (!kvUrl || !kvToken) {
2533
+ throw new Error(
2534
+ "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
2535
+ );
2536
+ }
2537
+ const { Store } = await import("mppx");
2538
+ const { createUpstashRest: createUpstashRest2 } = await Promise.resolve().then(() => (init_upstash_rest(), upstash_rest_exports));
2539
+ resolvedStore = Store.upstash(createUpstashRest2(kvUrl, kvToken));
2479
2540
  }
2480
2541
  deps.mppx = Mppx.create({
2481
2542
  methods: [
@@ -2483,7 +2544,8 @@ function createRouter(config) {
2483
2544
  currency: config.mpp.currency,
2484
2545
  recipient: config.mpp.recipient ?? config.payeeAddress,
2485
2546
  getClient,
2486
- ...feePayerAccount ? { feePayer: feePayerAccount } : {}
2547
+ ...feePayerAccount ? { feePayer: feePayerAccount } : {},
2548
+ ...resolvedStore ? { store: resolvedStore } : {}
2487
2549
  })
2488
2550
  ],
2489
2551
  secretKey: config.mpp.secretKey,
package/dist/index.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { FacilitatorConfig } from '@x402/core/http';
2
2
  import { NextRequest, NextResponse } from 'next/server';
3
3
  import { ZodType } from 'zod';
4
+ import { Store } from 'mppx';
4
5
  import { PaymentRequirements, PaymentRequired, SettleResponse, Network } from '@x402/core/types';
5
6
  import * as viem from 'viem';
6
7
  export { S as SIWX_ERROR_MESSAGES, a as SiwxErrorCode } from './siwx-BMlja_nt.cjs';
@@ -216,7 +217,7 @@ type PricingConfig<TBody = unknown> = string | ((body: TBody) => string | Promis
216
217
  tiers: Record<string, TierConfig>;
217
218
  default?: string;
218
219
  };
219
- type PayToConfig = string | ((request: Request) => string | Promise<string>);
220
+ type PayToConfig = string | ((request: Request, body?: unknown) => string | Promise<string>);
220
221
  interface X402AcceptBase {
221
222
  network: string;
222
223
  asset?: string;
@@ -354,6 +355,36 @@ interface RouterConfig {
354
355
  * Must be a hex-encoded private key (e.g. `0xabc123...`).
355
356
  */
356
357
  feePayerKey?: string;
358
+ /**
359
+ * Persistent store for transaction hash replay protection.
360
+ *
361
+ * Without this, mppx defaults to `Store.memory()` which is wiped on every cold start —
362
+ * unsafe on Vercel or any multi-instance deployment. Pass `Store.upstash(redis)` or
363
+ * `Store.cloudflare(kv)` for a shared persistent store.
364
+ *
365
+ * @example
366
+ * import { Store } from 'mppx'
367
+ * store: Store.upstash({ get, set, del })
368
+ * store: Store.cloudflare(env.MY_KV_NAMESPACE)
369
+ */
370
+ store?: Store.Store;
371
+ /**
372
+ * When `true`, auto-configures an Upstash-backed persistent store from Vercel KV
373
+ * environment variables (`KV_REST_API_URL` + `KV_REST_API_TOKEN`).
374
+ *
375
+ * Uses raw `fetch` against the Upstash REST API — no extra npm dependencies.
376
+ * Ignored when `store` is explicitly provided.
377
+ *
378
+ * @example
379
+ * createRouter({
380
+ * mpp: {
381
+ * secretKey: process.env.MPP_SECRET_KEY!,
382
+ * currency: USDC,
383
+ * useDefaultStore: true,
384
+ * }
385
+ * })
386
+ */
387
+ useDefaultStore?: boolean;
357
388
  };
358
389
  /**
359
390
  * Payment protocols to accept on auto-priced routes (those using the `prices` config).
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { FacilitatorConfig } from '@x402/core/http';
2
2
  import { NextRequest, NextResponse } from 'next/server';
3
3
  import { ZodType } from 'zod';
4
+ import { Store } from 'mppx';
4
5
  import { PaymentRequirements, PaymentRequired, SettleResponse, Network } from '@x402/core/types';
5
6
  import * as viem from 'viem';
6
7
  export { S as SIWX_ERROR_MESSAGES, a as SiwxErrorCode } from './siwx-BMlja_nt.js';
@@ -216,7 +217,7 @@ type PricingConfig<TBody = unknown> = string | ((body: TBody) => string | Promis
216
217
  tiers: Record<string, TierConfig>;
217
218
  default?: string;
218
219
  };
219
- type PayToConfig = string | ((request: Request) => string | Promise<string>);
220
+ type PayToConfig = string | ((request: Request, body?: unknown) => string | Promise<string>);
220
221
  interface X402AcceptBase {
221
222
  network: string;
222
223
  asset?: string;
@@ -354,6 +355,36 @@ interface RouterConfig {
354
355
  * Must be a hex-encoded private key (e.g. `0xabc123...`).
355
356
  */
356
357
  feePayerKey?: string;
358
+ /**
359
+ * Persistent store for transaction hash replay protection.
360
+ *
361
+ * Without this, mppx defaults to `Store.memory()` which is wiped on every cold start —
362
+ * unsafe on Vercel or any multi-instance deployment. Pass `Store.upstash(redis)` or
363
+ * `Store.cloudflare(kv)` for a shared persistent store.
364
+ *
365
+ * @example
366
+ * import { Store } from 'mppx'
367
+ * store: Store.upstash({ get, set, del })
368
+ * store: Store.cloudflare(env.MY_KV_NAMESPACE)
369
+ */
370
+ store?: Store.Store;
371
+ /**
372
+ * When `true`, auto-configures an Upstash-backed persistent store from Vercel KV
373
+ * environment variables (`KV_REST_API_URL` + `KV_REST_API_TOKEN`).
374
+ *
375
+ * Uses raw `fetch` against the Upstash REST API — no extra npm dependencies.
376
+ * Ignored when `store` is explicitly provided.
377
+ *
378
+ * @example
379
+ * createRouter({
380
+ * mpp: {
381
+ * secretKey: process.env.MPP_SECRET_KEY!,
382
+ * currency: USDC,
383
+ * useDefaultStore: true,
384
+ * }
385
+ * })
386
+ */
387
+ useDefaultStore?: boolean;
357
388
  };
358
389
  /**
359
390
  * Payment protocols to accept on auto-priced routes (those using the `prices` config).
package/dist/index.js CHANGED
@@ -167,10 +167,10 @@ var init_x402_facilitators = __esm({
167
167
  });
168
168
 
169
169
  // src/x402-config.ts
170
- async function resolvePayToValue(payTo, request, fallback) {
170
+ async function resolvePayToValue(payTo, request, fallback, body) {
171
171
  if (!payTo) return fallback;
172
172
  if (typeof payTo === "string") return payTo;
173
- return payTo(request);
173
+ return payTo(request, body);
174
174
  }
175
175
  function getConfiguredX402Accepts(config) {
176
176
  if (config.x402?.accepts?.length) {
@@ -187,12 +187,17 @@ function getConfiguredX402Accepts(config) {
187
187
  function getConfiguredX402Networks(config) {
188
188
  return [...new Set(getConfiguredX402Accepts(config).map((accept) => accept.network))];
189
189
  }
190
- async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo) {
190
+ async function resolveX402Accepts(request, routeEntry, accepts, fallbackPayTo, body) {
191
191
  return Promise.all(
192
192
  accepts.map(async (accept) => ({
193
193
  network: accept.network,
194
194
  scheme: accept.scheme ?? "exact",
195
- payTo: await resolvePayToValue(accept.payTo ?? routeEntry.payTo, request, fallbackPayTo),
195
+ payTo: await resolvePayToValue(
196
+ routeEntry.payTo ?? accept.payTo,
197
+ request,
198
+ fallbackPayTo,
199
+ body
200
+ ),
196
201
  ...accept.asset ? { asset: accept.asset } : {},
197
202
  ...accept.decimals !== void 0 ? { decimals: accept.decimals } : {},
198
203
  ...accept.maxTimeoutSeconds !== void 0 ? { maxTimeoutSeconds: accept.maxTimeoutSeconds } : {},
@@ -285,6 +290,47 @@ var init_server = __esm({
285
290
  }
286
291
  });
287
292
 
293
+ // src/upstash-rest.ts
294
+ var upstash_rest_exports = {};
295
+ __export(upstash_rest_exports, {
296
+ createUpstashRest: () => createUpstashRest
297
+ });
298
+ function createUpstashRest(url, token) {
299
+ const base = url.replace(/\/+$/, "");
300
+ const headers = { Authorization: `Bearer ${token}` };
301
+ return {
302
+ async get(key) {
303
+ const res = await fetch(`${base}/get/${key}`, { headers });
304
+ if (!res.ok) throw new Error(`[upstash-rest] GET ${key}: ${res.status}`);
305
+ const { result } = await res.json();
306
+ return result ?? null;
307
+ },
308
+ async set(key, value) {
309
+ const res = await fetch(`${base}`, {
310
+ method: "POST",
311
+ headers: { ...headers, "Content-Type": "application/json" },
312
+ body: JSON.stringify(["SET", key, JSON.stringify(value)])
313
+ });
314
+ if (!res.ok) throw new Error(`[upstash-rest] SET ${key}: ${res.status}`);
315
+ return await res.json();
316
+ },
317
+ async del(key) {
318
+ const res = await fetch(`${base}`, {
319
+ method: "POST",
320
+ headers: { ...headers, "Content-Type": "application/json" },
321
+ body: JSON.stringify(["DEL", key])
322
+ });
323
+ if (!res.ok) throw new Error(`[upstash-rest] DEL ${key}: ${res.status}`);
324
+ return await res.json();
325
+ }
326
+ };
327
+ }
328
+ var init_upstash_rest = __esm({
329
+ "src/upstash-rest.ts"() {
330
+ "use strict";
331
+ }
332
+ });
333
+
288
334
  // src/registry.ts
289
335
  var RouteRegistry = class {
290
336
  routes = /* @__PURE__ */ new Map();
@@ -1263,7 +1309,8 @@ function createRequestHandler(routeEntry, handler, deps) {
1263
1309
  request,
1264
1310
  routeEntry,
1265
1311
  deps.x402Accepts,
1266
- deps.payeeAddress
1312
+ deps.payeeAddress,
1313
+ body.data
1267
1314
  );
1268
1315
  const verify = await verifyX402Payment({
1269
1316
  server: deps.x402Server,
@@ -1694,7 +1741,8 @@ async function build402(request, routeEntry, deps, meta, pluginCtx, bodyData) {
1694
1741
  request,
1695
1742
  routeEntry,
1696
1743
  deps.x402Accepts,
1697
- deps.payeeAddress
1744
+ deps.payeeAddress,
1745
+ bodyData
1698
1746
  );
1699
1747
  const { encoded } = await buildX402Challenge({
1700
1748
  server: deps.x402Server,
@@ -2435,8 +2483,21 @@ function createRouter(config) {
2435
2483
  const getClient = async () => deps.tempoClient;
2436
2484
  let feePayerAccount;
2437
2485
  if (config.mpp.feePayerKey) {
2438
- const { Account } = await import("viem/tempo");
2439
- feePayerAccount = Account.fromSecp256k1(config.mpp.feePayerKey);
2486
+ const { privateKeyToAccount } = await import("viem/accounts");
2487
+ feePayerAccount = privateKeyToAccount(config.mpp.feePayerKey);
2488
+ }
2489
+ let resolvedStore = config.mpp.store;
2490
+ if (!resolvedStore && config.mpp.useDefaultStore) {
2491
+ const kvUrl = process.env.KV_REST_API_URL;
2492
+ const kvToken = process.env.KV_REST_API_TOKEN;
2493
+ if (!kvUrl || !kvToken) {
2494
+ throw new Error(
2495
+ "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
2496
+ );
2497
+ }
2498
+ const { Store } = await import("mppx");
2499
+ const { createUpstashRest: createUpstashRest2 } = await Promise.resolve().then(() => (init_upstash_rest(), upstash_rest_exports));
2500
+ resolvedStore = Store.upstash(createUpstashRest2(kvUrl, kvToken));
2440
2501
  }
2441
2502
  deps.mppx = Mppx.create({
2442
2503
  methods: [
@@ -2444,7 +2505,8 @@ function createRouter(config) {
2444
2505
  currency: config.mpp.currency,
2445
2506
  recipient: config.mpp.recipient ?? config.payeeAddress,
2446
2507
  getClient,
2447
- ...feePayerAccount ? { feePayer: feePayerAccount } : {}
2508
+ ...feePayerAccount ? { feePayer: feePayerAccount } : {},
2509
+ ...resolvedStore ? { store: resolvedStore } : {}
2448
2510
  })
2449
2511
  ],
2450
2512
  secretKey: config.mpp.secretKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
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": {
@@ -29,7 +29,7 @@
29
29
  "@x402/evm": "^2.3.0",
30
30
  "@x402/extensions": "^2.3.0",
31
31
  "@x402/svm": "2.3.0",
32
- "mppx": "^0.4.2",
32
+ "mppx": "^0.4.11",
33
33
  "next": ">=15.0.0",
34
34
  "zod": "^4.0.0",
35
35
  "zod-openapi": "^5.0.0"
@@ -61,14 +61,14 @@
61
61
  "@x402/extensions": "^2.3.0",
62
62
  "@x402/svm": "2.3.0",
63
63
  "eslint": "^10.0.0",
64
- "mppx": "^0.4.8",
64
+ "mppx": "^0.4.11",
65
65
  "next": "^15.0.0",
66
66
  "prettier": "^3.8.1",
67
67
  "react": "^19.0.0",
68
68
  "tsup": "^8.0.0",
69
69
  "typescript": "^5.8.0",
70
70
  "typescript-eslint": "^8.55.0",
71
- "viem": "^2.47.2",
71
+ "viem": "^2.47.6",
72
72
  "vitest": "^3.0.0",
73
73
  "zod": "^4.0.0",
74
74
  "zod-openapi": "^5.0.0"