@agentcash/router 0.4.1 → 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)
@@ -90,3 +100,23 @@ pnpm check # format + lint + typecheck + build + test
90
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.
91
101
 
92
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) {
@@ -379,11 +379,8 @@ async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
379
379
  const standardRequest = toStandardRequest(request);
380
380
  const currency = mppConfig.currency;
381
381
  const recipient = mppConfig.recipient ?? "";
382
- const methodIntent = import_server2.tempo.charge({
383
- currency,
384
- recipient
385
- });
386
- const challenge = import_mpay.Challenge.fromIntent(methodIntent, {
382
+ const methodIntent = import_server2.tempo.charge();
383
+ const challenge = import_mppx.Challenge.fromIntent(methodIntent, {
387
384
  secretKey: mppConfig.secretKey,
388
385
  realm: new URL(standardRequest.url).origin,
389
386
  request: {
@@ -393,7 +390,7 @@ async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
393
390
  decimals: DEFAULT_DECIMALS
394
391
  }
395
392
  });
396
- return import_mpay.Challenge.serialize(challenge);
393
+ return import_mppx.Challenge.serialize(challenge);
397
394
  }
398
395
  async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
399
396
  const standardRequest = toStandardRequest(request);
@@ -405,19 +402,17 @@ async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
405
402
  console.error("[MPP] No Authorization header found");
406
403
  return null;
407
404
  }
408
- const credential = import_mpay.Credential.fromRequest(standardRequest);
405
+ const credential = import_mppx.Credential.fromRequest(standardRequest);
409
406
  if (!credential?.challenge) {
410
407
  console.error("[MPP] Invalid credential structure");
411
408
  return null;
412
409
  }
413
- const isValid = import_mpay.Challenge.verify(credential.challenge, { secretKey: mppConfig.secretKey });
410
+ const isValid = import_mppx.Challenge.verify(credential.challenge, { secretKey: mppConfig.secretKey });
414
411
  if (!isValid) {
415
412
  console.error("[MPP] Challenge HMAC verification failed");
416
413
  return { valid: false, payer: null };
417
414
  }
418
415
  const methodIntent = import_server2.tempo.charge({
419
- currency,
420
- recipient,
421
416
  ...buildGetClient(mppConfig.rpcUrl)
422
417
  });
423
418
  const paymentRequest = {
@@ -426,10 +421,9 @@ async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
426
421
  recipient,
427
422
  decimals: DEFAULT_DECIMALS
428
423
  };
429
- const resolvedRequest = methodIntent.request ? await methodIntent.request({ credential, request: paymentRequest }) : paymentRequest;
430
424
  const receipt = await methodIntent.verify({
431
425
  credential,
432
- request: resolvedRequest
426
+ request: paymentRequest
433
427
  });
434
428
  if (!receipt || receipt.status !== "success") {
435
429
  console.error("[MPP] Tempo verification failed:", receipt);
@@ -451,13 +445,13 @@ async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
451
445
  }
452
446
  }
453
447
  function buildMPPReceipt(reference) {
454
- const receipt = import_mpay.Receipt.from({
448
+ const receipt = import_mppx.Receipt.from({
455
449
  method: "tempo",
456
450
  status: "success",
457
451
  reference,
458
452
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
459
453
  });
460
- return import_mpay.Receipt.serialize(receipt);
454
+ return import_mppx.Receipt.serialize(receipt);
461
455
  }
462
456
 
463
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) {
@@ -345,10 +345,7 @@ async function buildMPPChallenge(routeEntry, request, mppConfig, price) {
345
345
  const standardRequest = toStandardRequest(request);
346
346
  const currency = mppConfig.currency;
347
347
  const recipient = mppConfig.recipient ?? "";
348
- const methodIntent = tempo.charge({
349
- currency,
350
- recipient
351
- });
348
+ const methodIntent = tempo.charge();
352
349
  const challenge = Challenge.fromIntent(methodIntent, {
353
350
  secretKey: mppConfig.secretKey,
354
351
  realm: new URL(standardRequest.url).origin,
@@ -382,8 +379,6 @@ async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
382
379
  return { valid: false, payer: null };
383
380
  }
384
381
  const methodIntent = tempo.charge({
385
- currency,
386
- recipient,
387
382
  ...buildGetClient(mppConfig.rpcUrl)
388
383
  });
389
384
  const paymentRequest = {
@@ -392,10 +387,9 @@ async function verifyMPPCredential(request, _routeEntry, mppConfig, price) {
392
387
  recipient,
393
388
  decimals: DEFAULT_DECIMALS
394
389
  };
395
- const resolvedRequest = methodIntent.request ? await methodIntent.request({ credential, request: paymentRequest }) : paymentRequest;
396
390
  const receipt = await methodIntent.verify({
397
391
  credential,
398
- request: resolvedRequest
392
+ request: paymentRequest
399
393
  });
400
394
  if (!receipt || receipt.status !== "success") {
401
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.1",
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
+ }