@agentcash/router 1.2.2 → 1.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
@@ -307,6 +307,47 @@ var init_server = __esm({
307
307
  }
308
308
  });
309
309
 
310
+ // src/upstash-rest.ts
311
+ var upstash_rest_exports = {};
312
+ __export(upstash_rest_exports, {
313
+ createUpstashRest: () => createUpstashRest
314
+ });
315
+ function createUpstashRest(url, token) {
316
+ const base = url.replace(/\/+$/, "");
317
+ const headers = { Authorization: `Bearer ${token}` };
318
+ return {
319
+ async get(key) {
320
+ const res = await fetch(`${base}/get/${key}`, { headers });
321
+ if (!res.ok) throw new Error(`[upstash-rest] GET ${key}: ${res.status}`);
322
+ const { result } = await res.json();
323
+ return result ?? null;
324
+ },
325
+ async set(key, value) {
326
+ const res = await fetch(`${base}`, {
327
+ method: "POST",
328
+ headers: { ...headers, "Content-Type": "application/json" },
329
+ body: JSON.stringify(["SET", key, JSON.stringify(value)])
330
+ });
331
+ if (!res.ok) throw new Error(`[upstash-rest] SET ${key}: ${res.status}`);
332
+ return await res.json();
333
+ },
334
+ async del(key) {
335
+ const res = await fetch(`${base}`, {
336
+ method: "POST",
337
+ headers: { ...headers, "Content-Type": "application/json" },
338
+ body: JSON.stringify(["DEL", key])
339
+ });
340
+ if (!res.ok) throw new Error(`[upstash-rest] DEL ${key}: ${res.status}`);
341
+ return await res.json();
342
+ }
343
+ };
344
+ }
345
+ var init_upstash_rest = __esm({
346
+ "src/upstash-rest.ts"() {
347
+ "use strict";
348
+ }
349
+ });
350
+
310
351
  // src/index.ts
311
352
  var index_exports = {};
312
353
  __export(index_exports, {
@@ -2229,6 +2270,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2229
2270
  title: discovery.title,
2230
2271
  description: discovery.description,
2231
2272
  version: discovery.version,
2273
+ ...guidance !== void 0 && { "x-guidance": guidance },
2232
2274
  guidance,
2233
2275
  ...discovery.contact && { contact: discovery.contact }
2234
2276
  },
@@ -2473,8 +2515,21 @@ function createRouter(config) {
2473
2515
  const getClient = async () => deps.tempoClient;
2474
2516
  let feePayerAccount;
2475
2517
  if (config.mpp.feePayerKey) {
2476
- const { Account } = await import("viem/tempo");
2477
- feePayerAccount = Account.fromSecp256k1(config.mpp.feePayerKey);
2518
+ const { privateKeyToAccount } = await import("viem/accounts");
2519
+ feePayerAccount = privateKeyToAccount(config.mpp.feePayerKey);
2520
+ }
2521
+ let resolvedStore = config.mpp.store;
2522
+ if (!resolvedStore && config.mpp.useDefaultStore) {
2523
+ const kvUrl = process.env.KV_REST_API_URL;
2524
+ const kvToken = process.env.KV_REST_API_TOKEN;
2525
+ if (!kvUrl || !kvToken) {
2526
+ throw new Error(
2527
+ "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
2528
+ );
2529
+ }
2530
+ const { Store } = await import("mppx");
2531
+ const { createUpstashRest: createUpstashRest2 } = await Promise.resolve().then(() => (init_upstash_rest(), upstash_rest_exports));
2532
+ resolvedStore = Store.upstash(createUpstashRest2(kvUrl, kvToken));
2478
2533
  }
2479
2534
  deps.mppx = Mppx.create({
2480
2535
  methods: [
@@ -2482,7 +2537,8 @@ function createRouter(config) {
2482
2537
  currency: config.mpp.currency,
2483
2538
  recipient: config.mpp.recipient ?? config.payeeAddress,
2484
2539
  getClient,
2485
- ...feePayerAccount ? { feePayer: feePayerAccount } : {}
2540
+ ...feePayerAccount ? { feePayer: feePayerAccount } : {},
2541
+ ...resolvedStore ? { store: resolvedStore } : {}
2486
2542
  })
2487
2543
  ],
2488
2544
  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';
@@ -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';
@@ -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
@@ -285,6 +285,47 @@ var init_server = __esm({
285
285
  }
286
286
  });
287
287
 
288
+ // src/upstash-rest.ts
289
+ var upstash_rest_exports = {};
290
+ __export(upstash_rest_exports, {
291
+ createUpstashRest: () => createUpstashRest
292
+ });
293
+ function createUpstashRest(url, token) {
294
+ const base = url.replace(/\/+$/, "");
295
+ const headers = { Authorization: `Bearer ${token}` };
296
+ return {
297
+ async get(key) {
298
+ const res = await fetch(`${base}/get/${key}`, { headers });
299
+ if (!res.ok) throw new Error(`[upstash-rest] GET ${key}: ${res.status}`);
300
+ const { result } = await res.json();
301
+ return result ?? null;
302
+ },
303
+ async set(key, value) {
304
+ const res = await fetch(`${base}`, {
305
+ method: "POST",
306
+ headers: { ...headers, "Content-Type": "application/json" },
307
+ body: JSON.stringify(["SET", key, JSON.stringify(value)])
308
+ });
309
+ if (!res.ok) throw new Error(`[upstash-rest] SET ${key}: ${res.status}`);
310
+ return await res.json();
311
+ },
312
+ async del(key) {
313
+ const res = await fetch(`${base}`, {
314
+ method: "POST",
315
+ headers: { ...headers, "Content-Type": "application/json" },
316
+ body: JSON.stringify(["DEL", key])
317
+ });
318
+ if (!res.ok) throw new Error(`[upstash-rest] DEL ${key}: ${res.status}`);
319
+ return await res.json();
320
+ }
321
+ };
322
+ }
323
+ var init_upstash_rest = __esm({
324
+ "src/upstash-rest.ts"() {
325
+ "use strict";
326
+ }
327
+ });
328
+
288
329
  // src/registry.ts
289
330
  var RouteRegistry = class {
290
331
  routes = /* @__PURE__ */ new Map();
@@ -2190,6 +2231,7 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2190
2231
  title: discovery.title,
2191
2232
  description: discovery.description,
2192
2233
  version: discovery.version,
2234
+ ...guidance !== void 0 && { "x-guidance": guidance },
2193
2235
  guidance,
2194
2236
  ...discovery.contact && { contact: discovery.contact }
2195
2237
  },
@@ -2434,8 +2476,21 @@ function createRouter(config) {
2434
2476
  const getClient = async () => deps.tempoClient;
2435
2477
  let feePayerAccount;
2436
2478
  if (config.mpp.feePayerKey) {
2437
- const { Account } = await import("viem/tempo");
2438
- feePayerAccount = Account.fromSecp256k1(config.mpp.feePayerKey);
2479
+ const { privateKeyToAccount } = await import("viem/accounts");
2480
+ feePayerAccount = privateKeyToAccount(config.mpp.feePayerKey);
2481
+ }
2482
+ let resolvedStore = config.mpp.store;
2483
+ if (!resolvedStore && config.mpp.useDefaultStore) {
2484
+ const kvUrl = process.env.KV_REST_API_URL;
2485
+ const kvToken = process.env.KV_REST_API_TOKEN;
2486
+ if (!kvUrl || !kvToken) {
2487
+ throw new Error(
2488
+ "mpp.useDefaultStore requires KV_REST_API_URL and KV_REST_API_TOKEN environment variables. These are automatically set by Vercel KV."
2489
+ );
2490
+ }
2491
+ const { Store } = await import("mppx");
2492
+ const { createUpstashRest: createUpstashRest2 } = await Promise.resolve().then(() => (init_upstash_rest(), upstash_rest_exports));
2493
+ resolvedStore = Store.upstash(createUpstashRest2(kvUrl, kvToken));
2439
2494
  }
2440
2495
  deps.mppx = Mppx.create({
2441
2496
  methods: [
@@ -2443,7 +2498,8 @@ function createRouter(config) {
2443
2498
  currency: config.mpp.currency,
2444
2499
  recipient: config.mpp.recipient ?? config.payeeAddress,
2445
2500
  getClient,
2446
- ...feePayerAccount ? { feePayer: feePayerAccount } : {}
2501
+ ...feePayerAccount ? { feePayer: feePayerAccount } : {},
2502
+ ...resolvedStore ? { store: resolvedStore } : {}
2447
2503
  })
2448
2504
  ],
2449
2505
  secretKey: config.mpp.secretKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Unified route builder for Next.js App Router APIs with x402, MPP, SIWX, and API key auth",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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.10",
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.10",
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"