@agentcash/router 0.4.0 → 0.4.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/.claude/CLAUDE.md CHANGED
@@ -46,6 +46,16 @@ The router uses the default facilitator from `@coinbase/x402`, which requires CD
46
46
 
47
47
  **Critical for Next.js apps with env validation (T3 stack, `@t3-oss/env-nextjs`):** These variables must be explicitly declared in your env schema. Next.js does not automatically expose all env vars to `process.env` — undeclared vars are invisible at runtime.
48
48
 
49
+ ### MPP (Tempo) Environment Variables
50
+
51
+ MPP payment verification requires an **authenticated** Tempo RPC endpoint. The public `https://rpc.tempo.xyz/` returns `401 Unauthorized`.
52
+
53
+ - `TEMPO_RPC_URL` — Authenticated Tempo RPC URL (e.g. `https://user:pass@rpc.mainnet.tempo.xyz`)
54
+
55
+ Alternatively, pass `rpcUrl` in the `mpp` config object to `createRouter()`. Without either, MPP on-chain verification fails with "unauthorized: authentication required".
56
+
57
+ ### CDP Environment Variables
58
+
49
59
  Without these keys, the default facilitator cannot authenticate with CDP:
50
60
  - x402 server `initialize()` fails with "Failed to fetch supported kinds from facilitator: TypeError: fetch failed" or "Facilitator getSupported failed (401): Unauthorized"
51
61
  - All payment routes return empty 402 responses (no `PAYMENT-REQUIRED` header, no body)
@@ -84,3 +94,29 @@ pnpm test # vitest
84
94
  pnpm typecheck # tsc --noEmit
85
95
  pnpm check # format + lint + typecheck + build + test
86
96
  ```
97
+
98
+ ## Development Record
99
+
100
+ The `.claude/` directory contains design docs, decision records, and bug analyses that document the reasoning behind the router's architecture. See `.claude/INDEX.md` for a table of contents.
101
+
102
+ **Convention:** Every doc has a `Status` header. When you resolve work described in a doc, update its Status to `Resolved in vX.Y.Z` and update INDEX.md.
103
+
104
+ ## Releasing
105
+
106
+ This repo uses [changesets](https://github.com/changesets/changesets) for versioning and npm publishing.
107
+
108
+ ### When doing work that should be released:
109
+
110
+ 1. **Create a changeset** — Run `pnpm changeset` and describe the changes (patch/minor/major)
111
+ 2. **Include the changeset file** in your PR (committed to `.changeset/`)
112
+ 3. **Merge PR to main**
113
+
114
+ ### What happens automatically:
115
+
116
+ 1. When PRs with changesets merge to `main`, the `changesets/action` creates a **"chore: version packages"** PR that bumps `package.json` version and updates `CHANGELOG.md`
117
+ 2. When that version PR is merged, the action **publishes to npm** automatically
118
+
119
+ ### Troubleshooting
120
+
121
+ - **Publish fails**: Check `NPM_TOKEN` secret is set and has write access to `@agentcash` scope
122
+ - **No version PR created**: Ensure your PR included a `.changeset/*.md` file
@@ -31,7 +31,7 @@ Every paid API route in a Merit Systems service shared the same ~80-150 lines of
31
31
 
32
32
  7. **Self-registering routes + validated barrel (Approach B).** Routes self-register via `.handler()` at import time. A barrel file imports all route modules. Discovery endpoints (`.wellKnown()`, `.openapi()`) validate that the barrel is complete by comparing registered routes against the `prices` map. For routes in separate handler files (e.g., Next.js `route.ts` files), use **discovery stubs** — lightweight registrations that provide metadata for discovery without the real handler. Guard stubs with `registry.has()` to avoid unnecessary overwrites.
33
33
 
34
- 8. **Both x402 and MPP ship from day one.** Dual-protocol support is not an afterthought. Routes declare `protocols: ['x402', 'mpp']` and the orchestration layer routes to the correct handler based on the request header. MPP uses low-level `mpay` primitives (`Challenge`, `Credential`, `tempo.charge`) — not the high-level `Mpay.create()` wrapper — because the router owns orchestration.
34
+ 8. **Both x402 and MPP ship from day one.** Dual-protocol support is not an afterthought. Routes declare `protocols: ['x402', 'mpp']` and the orchestration layer routes to the correct handler based on the request header. MPP uses low-level `mppx` primitives (`Challenge`, `Credential`, `tempo.charge`) — not the high-level `Mppx.create()` wrapper — because the router owns orchestration.
35
35
 
36
36
  9. **`zod-openapi` for OpenAPI 3.1.** Zod schemas are the single source of truth for request/response types. OpenAPI docs are auto-generated from them. No manual spec maintenance.
37
37
 
@@ -101,7 +101,7 @@ Response out
101
101
  | `src/registry.ts` | `RouteRegistry` with barrel validation |
102
102
  | `src/protocols/detect.ts` | Header-based protocol detection |
103
103
  | `src/protocols/x402.ts` | x402 challenge/verify/settle wrappers |
104
- | `src/protocols/mpp.ts` | MPP challenge/verify/receipt wrappers (uses mpay low-level primitives) |
104
+ | `src/protocols/mpp.ts` | MPP challenge/verify/receipt wrappers (uses mppx low-level primitives) |
105
105
  | `src/server.ts` | x402 server initialization with retry |
106
106
  | `src/auth/siwx.ts` | SIWX verification |
107
107
  | `src/auth/api-key.ts` | API key verification |
@@ -159,7 +159,7 @@ TEMPO_RPC_URL=https://user:pass@rpc.mainnet.tempo.xyz # Authenticated Tempo RPC
159
159
 
160
160
  **Tempo RPC requires authentication.** The default `rpc.tempo.xyz` returns 401. Get credentials from the Tempo team. The `rpcUrl` can be set in config or via `TEMPO_RPC_URL` env var (config takes precedence).
161
161
 
162
- **Peer dependencies for MPP:** `mpay` is an optional peer dep. When installed, it brings `viem` as a transitive dependency. Both are required for MPP support.
162
+ **Peer dependencies for MPP:** `mppx` is an optional peer dep. It has `viem` as a peer dependency. Both are required for MPP support.
163
163
 
164
164
  ## Creating Routes
165
165
 
@@ -176,7 +176,7 @@ export const router = createRouter({
176
176
  protocols: ['x402', 'mpp'], // protocols for auto-priced routes (default: ['x402'])
177
177
  plugin: myPlugin, // observability
178
178
  prices: { 'search': '0.02' }, // central pricing map
179
- mpp: { // MPP support (requires mpay peer dep)
179
+ mpp: { // MPP support (requires mppx peer dep)
180
180
  secretKey: process.env.MPP_SECRET_KEY!,
181
181
  currency: '0x20c0000000000000000000000000000000000000', // PathUSD on Tempo
182
182
  recipient: process.env.X402_PAYEE_ADDRESS!,
@@ -417,7 +417,7 @@ The type system (generic parameters `HasAuth`, `NeedsBody`, `HasBody`) prevents
417
417
 
418
418
  ## MPP Internals (Critical Pitfalls)
419
419
 
420
- The router uses mpay's **low-level primitives**, not the high-level `Mpay.create()` API. This matters because mpay's internals have subtle conventions:
420
+ The router uses mppx's **low-level primitives**, not the high-level `Mppx.create()` API. This matters because mppx's internals have subtle conventions:
421
421
 
422
422
  1. **NextRequest vs Request.** `Credential.fromRequest()` breaks with Next.js `NextRequest` due to subtle header handling differences. The router converts via `toStandardRequest()` — creating a new standard `Request` with the same URL, method, headers, and body.
423
423
 
@@ -425,11 +425,11 @@ The router uses mpay's **low-level primitives**, not the high-level `Mpay.create
425
425
 
426
426
  3. **`tempo.charge().verify()` returns a receipt, not `{ valid, payer }`.** On success it returns `{ method, status, reference, timestamp }`. On failure it throws. Check `receipt.status === 'success'`, not `receipt.valid`.
427
427
 
428
- 4. **`getClient` must be synchronous.** mpay's `Client.getResolver()` checks `if (getClient) return getClient` it does NOT await. An async `getClient` will silently fall through to the default RPC URL.
428
+ 4. **`tempo.charge()` constructor takes operational config only.** In mppx, `currency` and `recipient` are NOT passed to `tempo.charge()`. They are passed in the `request` object to `Challenge.fromIntent()` and `verify()`. Only `getClient`, `decimals`, `feePayer`, `testnet`, etc. go in the constructor.
429
429
 
430
430
  5. **Tempo RPC requires authentication.** The default `rpc.tempo.xyz` returns 401. Always provide `rpcUrl` or set `TEMPO_RPC_URL` env var with authenticated credentials.
431
431
 
432
- 6. **viem is loaded eagerly in `ensureMpay()`.** Since `getClient` must be synchronous, viem's `createClient` and `http` are loaded once when mpay initializes, not per-call. viem is a transitive dep of mpay and is always available when mpay is installed.
432
+ 6. **viem is a peer dep of mppx.** The router uses viem directly for `createClient` and `http` in `buildGetClient()`. viem is always available when mppx is installed as a peer dep.
433
433
 
434
434
  ## Registration-Time Validation
435
435
 
@@ -501,7 +501,7 @@ Barrel validation catches mismatches: keys in `prices` but not registered → er
501
501
  | MPP verify returns `status: 'success'` but route returns 402 | Code checking `.valid` instead of `.status` | Verify returns a receipt `{ status, reference }`, not `{ valid, payer }` |
502
502
  | MPP using wrong RPC URL after rebuild | Next.js webpack cache or stale pnpm link | Delete `.next/`, run `pnpm install` in the app to pick up new router dist |
503
503
  | `route 'X' in prices map but not registered` | Discovery endpoint hit before route module loaded | Add barrel import to discovery route files |
504
- | `mpay package is required` | mpay not installed | `pnpm add mpay` — it's an optional peer dep |
504
+ | `mppx package is required` | mppx not installed | `pnpm add mppx` — it's an optional peer dep |
505
505
 
506
506
  ## Maintaining This Skill
507
507
 
package/README.md CHANGED
@@ -15,7 +15,7 @@ Peer dependencies:
15
15
  ```bash
16
16
  pnpm add next zod @x402/core @x402/evm @x402/extensions @coinbase/x402 zod-openapi
17
17
  # Optional: for MPP support
18
- pnpm add mpay
18
+ pnpm add mppx
19
19
  ```
20
20
 
21
21
  ## Environment Setup
package/dist/index.cjs CHANGED
@@ -354,8 +354,8 @@ async function settleX402Payment(server, payload, requirements) {
354
354
  }
355
355
 
356
356
  // src/protocols/mpp.ts
357
- var import_mpay = require("mpay");
358
- var import_server2 = require("mpay/server");
357
+ var import_mppx = require("mppx");
358
+ var import_server2 = require("mppx/server");
359
359
  var import_viem = require("viem");
360
360
  var import_chains = require("viem/chains");
361
361
  function buildGetClient(rpcUrl) {
@@ -371,10 +371,7 @@ function toStandardRequest(request) {
371
371
  }
372
372
  return new Request(request.url, {
373
373
  method: request.method,
374
- headers: request.headers,
375
- body: request.body,
376
- // @ts-expect-error - Request.duplex is required for streaming bodies but not in types yet
377
- duplex: "half"
374
+ headers: request.headers
378
375
  });
379
376
  }
380
377
  var DEFAULT_DECIMALS = 6;
@@ -382,11 +379,8 @@ async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
382
379
  const standardRequest = toStandardRequest(request);
383
380
  const currency = mppConfig.currency;
384
381
  const recipient = mppConfig.recipient ?? "";
385
- const methodIntent = import_server2.tempo.charge({
386
- currency,
387
- recipient
388
- });
389
- const challenge = import_mpay.Challenge.fromIntent(methodIntent, {
382
+ const methodIntent = import_server2.tempo.charge();
383
+ const challenge = import_mppx.Challenge.fromIntent(methodIntent, {
390
384
  secretKey: mppConfig.secretKey,
391
385
  realm: new URL(standardRequest.url).origin,
392
386
  request: {
@@ -396,7 +390,7 @@ async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
396
390
  decimals: DEFAULT_DECIMALS
397
391
  }
398
392
  });
399
- return import_mpay.Challenge.serialize(challenge);
393
+ return import_mppx.Challenge.serialize(challenge);
400
394
  }
401
395
  async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
402
396
  const standardRequest = toStandardRequest(request);
@@ -408,19 +402,17 @@ async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
408
402
  console.error("[MPP] No Authorization header found");
409
403
  return null;
410
404
  }
411
- const credential = import_mpay.Credential.fromRequest(standardRequest);
405
+ const credential = import_mppx.Credential.fromRequest(standardRequest);
412
406
  if (!credential?.challenge) {
413
407
  console.error("[MPP] Invalid credential structure");
414
408
  return null;
415
409
  }
416
- const isValid = import_mpay.Challenge.verify(credential.challenge, { secretKey: mppConfig.secretKey });
410
+ const isValid = import_mppx.Challenge.verify(credential.challenge, { secretKey: mppConfig.secretKey });
417
411
  if (!isValid) {
418
412
  console.error("[MPP] Challenge HMAC verification failed");
419
413
  return { valid: false, payer: null };
420
414
  }
421
415
  const methodIntent = import_server2.tempo.charge({
422
- currency,
423
- recipient,
424
416
  ...buildGetClient(mppConfig.rpcUrl)
425
417
  });
426
418
  const paymentRequest = {
@@ -429,10 +421,9 @@ async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
429
421
  recipient,
430
422
  decimals: DEFAULT_DECIMALS
431
423
  };
432
- const resolvedRequest = methodIntent.request ? await methodIntent.request({ credential, request: paymentRequest }) : paymentRequest;
433
424
  const receipt = await methodIntent.verify({
434
425
  credential,
435
- request: resolvedRequest
426
+ request: paymentRequest
436
427
  });
437
428
  if (!receipt || receipt.status !== "success") {
438
429
  console.error("[MPP] Tempo verification failed:", receipt);
@@ -454,13 +445,13 @@ async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
454
445
  }
455
446
  }
456
447
  function buildMPPReceipt(reference) {
457
- const receipt = import_mpay.Receipt.from({
448
+ const receipt = import_mppx.Receipt.from({
458
449
  method: "tempo",
459
450
  status: "success",
460
451
  reference,
461
452
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
462
453
  });
463
- return import_mpay.Receipt.serialize(receipt);
454
+ return import_mppx.Receipt.serialize(receipt);
464
455
  }
465
456
 
466
457
  // src/auth/siwx.ts
package/dist/index.js CHANGED
@@ -320,8 +320,8 @@ async function settleX402Payment(server, payload, requirements) {
320
320
  }
321
321
 
322
322
  // src/protocols/mpp.ts
323
- import { Challenge, Credential, Receipt } from "mpay";
324
- import { tempo } from "mpay/server";
323
+ import { Challenge, Credential, Receipt } from "mppx";
324
+ import { tempo } from "mppx/server";
325
325
  import { createClient, http } from "viem";
326
326
  import { tempo as tempoChain } from "viem/chains";
327
327
  function buildGetClient(rpcUrl) {
@@ -337,10 +337,7 @@ function toStandardRequest(request) {
337
337
  }
338
338
  return new Request(request.url, {
339
339
  method: request.method,
340
- headers: request.headers,
341
- body: request.body,
342
- // @ts-expect-error - Request.duplex is required for streaming bodies but not in types yet
343
- duplex: "half"
340
+ headers: request.headers
344
341
  });
345
342
  }
346
343
  var DEFAULT_DECIMALS = 6;
@@ -348,10 +345,7 @@ async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
348
345
  const standardRequest = toStandardRequest(request);
349
346
  const currency = mppConfig.currency;
350
347
  const recipient = mppConfig.recipient ?? "";
351
- const methodIntent = tempo.charge({
352
- currency,
353
- recipient
354
- });
348
+ const methodIntent = tempo.charge();
355
349
  const challenge = Challenge.fromIntent(methodIntent, {
356
350
  secretKey: mppConfig.secretKey,
357
351
  realm: new URL(standardRequest.url).origin,
@@ -385,8 +379,6 @@ async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
385
379
  return { valid: false, payer: null };
386
380
  }
387
381
  const methodIntent = tempo.charge({
388
- currency,
389
- recipient,
390
382
  ...buildGetClient(mppConfig.rpcUrl)
391
383
  });
392
384
  const paymentRequest = {
@@ -395,10 +387,9 @@ async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
395
387
  recipient,
396
388
  decimals: DEFAULT_DECIMALS
397
389
  };
398
- const resolvedRequest = methodIntent.request ? await methodIntent.request({ credential, request: paymentRequest }) : paymentRequest;
399
390
  const receipt = await methodIntent.verify({
400
391
  credential,
401
- request: resolvedRequest
392
+ request: paymentRequest
402
393
  });
403
394
  if (!receipt || receipt.status !== "success") {
404
395
  console.error("[MPP] Tempo verification failed:", receipt);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "0.4.0",
3
+ "version": "0.4.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": {
@@ -23,28 +23,23 @@
23
23
  ".claude/CLAUDE.md",
24
24
  ".claude/skills"
25
25
  ],
26
- "scripts": {
27
- "build": "tsup",
28
- "typecheck": "tsc --noEmit",
29
- "lint": "eslint src/",
30
- "lint:fix": "eslint src/ --fix",
31
- "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
32
- "format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
33
- "test": "vitest run",
34
- "test:watch": "vitest",
35
- "check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm build && pnpm test"
36
- },
37
26
  "peerDependencies": {
38
27
  "@coinbase/x402": "^2.1.0",
39
28
  "@x402/core": "^2.3.0",
40
29
  "@x402/evm": "^2.3.0",
41
30
  "@x402/extensions": "^2.3.0",
42
- "mpay": "^0.2.4",
31
+ "mppx": "^0.1.1",
43
32
  "next": ">=15.0.0",
44
33
  "zod": "^4.0.0",
45
34
  "zod-openapi": "^5.0.0"
46
35
  },
36
+ "peerDependenciesMeta": {
37
+ "mppx": {
38
+ "optional": true
39
+ }
40
+ },
47
41
  "devDependencies": {
42
+ "@changesets/cli": "^2.29.8",
48
43
  "@coinbase/x402": "^2.1.0",
49
44
  "@eslint/js": "^10.0.1",
50
45
  "@types/node": "^22.0.0",
@@ -52,7 +47,7 @@
52
47
  "@x402/evm": "^2.3.0",
53
48
  "@x402/extensions": "^2.3.0",
54
49
  "eslint": "^10.0.0",
55
- "mpay": "^0.2.4",
50
+ "mppx": "^0.1.1",
56
51
  "next": "^15.0.0",
57
52
  "prettier": "^3.8.1",
58
53
  "react": "^19.0.0",
@@ -64,10 +59,20 @@
64
59
  "zod": "^4.0.0",
65
60
  "zod-openapi": "^5.0.0"
66
61
  },
67
- "packageManager": "pnpm@10.28.0",
68
62
  "license": "MIT",
69
63
  "repository": {
70
64
  "type": "git",
71
65
  "url": "git+https://github.com/merit-systems/agentcash-router.git"
66
+ },
67
+ "scripts": {
68
+ "build": "tsup",
69
+ "typecheck": "tsc --noEmit",
70
+ "lint": "eslint src/",
71
+ "lint:fix": "eslint src/ --fix",
72
+ "format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
73
+ "format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
74
+ "test": "vitest run",
75
+ "test:watch": "vitest",
76
+ "check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm build && pnpm test"
72
77
  }
73
- }
78
+ }