@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 +36 -0
- package/.claude/skills/router-guide/SKILL.md +8 -8
- package/README.md +1 -1
- package/dist/index.cjs +11 -20
- package/dist/index.js +5 -14
- package/package.json +21 -16
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 `
|
|
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
|
|
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:** `
|
|
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
|
|
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
|
|
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. **`
|
|
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
|
|
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
|
-
| `
|
|
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
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
|
|
358
|
-
var import_server2 = require("
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
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 "
|
|
324
|
-
import { tempo } from "
|
|
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:
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
+
}
|