@agentcash/router 1.5.2 → 1.7.0
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/AGENTS.md +85 -0
- package/README.md +138 -526
- package/dist/index.cjs +2380 -1244
- package/dist/index.d.cts +451 -317
- package/dist/index.d.ts +451 -317
- package/dist/index.js +2372 -1227
- package/package.json +16 -24
- package/.claude/CLAUDE.md +0 -229
- package/.claude/skills/router-guide/SKILL.md +0 -585
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentcash/router",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
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": {
|
|
@@ -20,16 +20,16 @@
|
|
|
20
20
|
"types": "./dist/index.d.ts",
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
|
-
".
|
|
24
|
-
".
|
|
23
|
+
"AGENTS.md",
|
|
24
|
+
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"peerDependencies": {
|
|
27
27
|
"@coinbase/x402": "^2.1.0",
|
|
28
|
-
"@x402/core": "^2.
|
|
29
|
-
"@x402/evm": "^2.
|
|
30
|
-
"@x402/extensions": "^2.
|
|
31
|
-
"@x402/svm": "^2.
|
|
32
|
-
"mppx": "^0.6.
|
|
28
|
+
"@x402/core": "^2.11.0",
|
|
29
|
+
"@x402/evm": "^2.11.0",
|
|
30
|
+
"@x402/extensions": "^2.11.0",
|
|
31
|
+
"@x402/svm": "^2.11.0",
|
|
32
|
+
"mppx": "^0.6.16",
|
|
33
33
|
"next": ">=15.0.0",
|
|
34
34
|
"zod": "^4.0.0",
|
|
35
35
|
"zod-openapi": "^5.0.0"
|
|
@@ -42,26 +42,17 @@
|
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@changesets/cli": "^2.29.8",
|
|
44
44
|
"@coinbase/x402": "^2.1.0",
|
|
45
|
-
"@crossmint/lobster-cli": "^0.1.6",
|
|
46
45
|
"@eslint/js": "^10.0.1",
|
|
47
|
-
"@faremeter/facilitator": "0.17.1",
|
|
48
|
-
"@faremeter/fetch": "^0.17.1",
|
|
49
|
-
"@faremeter/info": "^0.17.1",
|
|
50
|
-
"@faremeter/payment-solana": "^0.17.1",
|
|
51
|
-
"@faremeter/types": "^0.17.1",
|
|
52
|
-
"@faremeter/wallet-solana": "^0.17.1",
|
|
53
|
-
"@faremeter/x-solana-settlement": "^0.4.0",
|
|
54
46
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
55
47
|
"@solana/kit": "^5.1.0",
|
|
56
|
-
"@solana/spl-token": "^0.4.14",
|
|
57
|
-
"@solana/web3.js": "^1.98.4",
|
|
58
48
|
"@types/node": "^22.0.0",
|
|
59
|
-
"@x402/core": "^2.
|
|
60
|
-
"@x402/evm": "^2.
|
|
61
|
-
"@x402/extensions": "^2.
|
|
62
|
-
"@x402/svm": "^2.
|
|
49
|
+
"@x402/core": "^2.11.0",
|
|
50
|
+
"@x402/evm": "^2.11.0",
|
|
51
|
+
"@x402/extensions": "^2.11.0",
|
|
52
|
+
"@x402/svm": "^2.11.0",
|
|
63
53
|
"eslint": "^10.0.0",
|
|
64
|
-
"
|
|
54
|
+
"knip": "^6.13.1",
|
|
55
|
+
"mppx": "^0.6.16",
|
|
65
56
|
"next": "^15.0.0",
|
|
66
57
|
"prettier": "^3.8.1",
|
|
67
58
|
"react": "^19.0.0",
|
|
@@ -87,6 +78,7 @@
|
|
|
87
78
|
"format:check": "prettier --check 'src/**/*.ts' 'tests/**/*.ts' '*.json' '*.mjs'",
|
|
88
79
|
"test": "vitest run",
|
|
89
80
|
"test:watch": "vitest",
|
|
90
|
-
"
|
|
81
|
+
"knip": "knip",
|
|
82
|
+
"check": "pnpm format:check && pnpm lint && pnpm typecheck && pnpm knip && pnpm build && pnpm test"
|
|
91
83
|
}
|
|
92
84
|
}
|
package/.claude/CLAUDE.md
DELETED
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
# CLAUDE.md — @agentcash/router
|
|
2
|
-
|
|
3
|
-
Protocol-agnostic route framework for Next.js App Router APIs with x402 payment, MPP payment, SIWX identity auth, and API key auth. Used by stablestudio, enrichx402, x402email, agentfacilitator, agentupload.
|
|
4
|
-
|
|
5
|
-
## Guiding Principles
|
|
6
|
-
|
|
7
|
-
1. **Route definition is 3-6 lines.** Everything else is derived.
|
|
8
|
-
2. **Single source of truth.** Route registry drives discovery, OpenAPI, pricing, Bazaar schemas.
|
|
9
|
-
3. **Auth and pricing are orthogonal and composable.** `.paid()`, `.siwx()`, `.apiKey()`, `.unprotected()`.
|
|
10
|
-
4. **Observability is pluggable via RouterPlugin.** Zero boilerplate.
|
|
11
|
-
5. **The package owns the x402 server lifecycle.** Init, verify, settle.
|
|
12
|
-
6. **Convention over configuration.** Sane defaults for Base, USDC, exact scheme.
|
|
13
|
-
7. **Compose, don't reimplement.** Zero payment/auth protocol logic — delegates to `@x402/*`, `@coinbase/x402`, and `mppx`.
|
|
14
|
-
|
|
15
|
-
## Architecture
|
|
16
|
-
|
|
17
|
-
**Orchestrate pipeline:** auth check -> body parse -> price resolve -> payment verify -> handler invoke -> settle -> finalize
|
|
18
|
-
|
|
19
|
-
- `src/orchestrate.ts` — Full request lifecycle orchestration
|
|
20
|
-
- `src/builder.ts` — Fluent RouteBuilder API
|
|
21
|
-
- `src/handler.ts` — Safe handler invocation with error mapping
|
|
22
|
-
- `src/types.ts` — Core types (RouteEntry, HandlerContext, HttpError)
|
|
23
|
-
- `src/registry.ts` — Route registry (Map-backed, silent overwrite on duplicate keys)
|
|
24
|
-
- `src/pricing.ts` — Price resolution (static, tiered, dynamic)
|
|
25
|
-
- `src/plugin.ts` — Plugin hook system
|
|
26
|
-
- `src/server.ts` — x402 server initialization
|
|
27
|
-
- `src/auth/` — Auth modules (siwx.ts, api-key.ts, nonce.ts)
|
|
28
|
-
- `src/protocols/` — Protocol handlers (x402.ts, detect.ts). MPP is handled via `mppx` high-level API (`Mppx.create` in index.ts)
|
|
29
|
-
- `src/discovery/` — Auto-generated endpoints (well-known.ts, openapi.ts)
|
|
30
|
-
|
|
31
|
-
## Auth Modes
|
|
32
|
-
|
|
33
|
-
Four auth modes, mutually exclusive (except `.apiKey()` composes with `.paid()`):
|
|
34
|
-
|
|
35
|
-
### `.paid(pricing)` — Payment required
|
|
36
|
-
```typescript
|
|
37
|
-
.paid('0.01') // Static price
|
|
38
|
-
.paid((body) => calcPrice(body)) // Dynamic pricing
|
|
39
|
-
.paid({ field: 'tier', tiers: { basic: { price: '0.01' } } }) // Tiered
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### `.siwx()` — Wallet identity required (no payment)
|
|
43
|
-
```typescript
|
|
44
|
-
.siwx().handler(async ({ wallet }) => { /* wallet is verified */ })
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### `.apiKey(resolver)` — API key / Bearer token auth
|
|
48
|
-
For admin routes, cron jobs, internal services. Checks `X-API-Key` header OR `Authorization: Bearer <token>`.
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
// Admin route with API key
|
|
52
|
-
export const GET = router
|
|
53
|
-
.route('admin/users')
|
|
54
|
-
.apiKey(async (key) => {
|
|
55
|
-
const admin = await db.admin.findByKey(key);
|
|
56
|
-
return admin ?? null; // null = 401, truthy = ctx.account
|
|
57
|
-
})
|
|
58
|
-
.handler(async ({ account }) => {
|
|
59
|
-
// account is whatever resolver returned
|
|
60
|
-
return db.user.findMany();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// Cron job with static secret
|
|
64
|
-
export const POST = router
|
|
65
|
-
.route('cron/cleanup')
|
|
66
|
-
.apiKey((key) => key === process.env.CRON_SECRET ? { cron: true } : null)
|
|
67
|
-
.handler(async () => { /* ... */ });
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
**Headers accepted:** `X-API-Key: <key>` or `Authorization: Bearer <key>`
|
|
71
|
-
|
|
72
|
-
**Composing with payment:** `.apiKey()` can layer on `.paid()` — auth runs first, payment second:
|
|
73
|
-
```typescript
|
|
74
|
-
.apiKey(resolver).paid('0.01') // Must pass API key AND pay
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### `.unprotected()` — No auth
|
|
78
|
-
```typescript
|
|
79
|
-
.unprotected().handler(async () => { /* public endpoint */ })
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Pre-Payment Validation
|
|
83
|
-
|
|
84
|
-
### `.validate(fn)` — Async business validation before 402 challenge
|
|
85
|
-
|
|
86
|
-
For checks that need DB lookups or external APIs before showing a price. Runs after body parsing, before the 402 challenge. Requires `.body()`.
|
|
87
|
-
|
|
88
|
-
```typescript
|
|
89
|
-
// Domain registration with availability check
|
|
90
|
-
router
|
|
91
|
-
.route('domain/register')
|
|
92
|
-
.paid(calculatePrice, { maxPrice: '10.00' })
|
|
93
|
-
.body(RegisterSchema) // .body() before .validate() for type inference
|
|
94
|
-
.validate(async (body) => {
|
|
95
|
-
if (await isDomainTaken(body.domain)) {
|
|
96
|
-
throw Object.assign(new Error('Domain already taken'), { status: 409 });
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
.handler(async ({ body, wallet }) => {
|
|
100
|
-
return registerDomain(body.domain, wallet);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// Rate limiting before payment
|
|
104
|
-
router
|
|
105
|
-
.route('api/expensive')
|
|
106
|
-
.paid('1.00')
|
|
107
|
-
.body(RequestSchema)
|
|
108
|
-
.validate(async (body) => {
|
|
109
|
-
const usage = await getUserUsage(body.userId);
|
|
110
|
-
if (usage >= DAILY_LIMIT) {
|
|
111
|
-
throw Object.assign(new Error('Daily limit reached'), { status: 429 });
|
|
112
|
-
}
|
|
113
|
-
})
|
|
114
|
-
.handler(async ({ body }) => { ... });
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
**Pipeline order:** `body parse → validate → 402 challenge → payment → handler`
|
|
118
|
-
|
|
119
|
-
**Error handling:** Respects `.status` on thrown errors (default: 400). Use `Object.assign(new Error('msg'), { status: 409 })` for custom codes.
|
|
120
|
-
|
|
121
|
-
**Works with all auth modes:** paid, siwx, apiKey, unprotected.
|
|
122
|
-
|
|
123
|
-
## Critical Rules
|
|
124
|
-
|
|
125
|
-
- **Error handling:** Respect `.status` on any thrown error, not just `HttpError`. The `Object.assign(new Error(), { status })` pattern is universal in Node.js.
|
|
126
|
-
- **SIWX challenge:** Must return a proper x402v2 challenge with `PAYMENT-REQUIRED` header and JSON body containing `extensions['sign-in-with-x']` with `domain`, `uri`, `version`, `chainId`, `type`, `nonce`, `issuedAt`.
|
|
127
|
-
- **Discovery:** `authMode !== 'unprotected'` determines well-known visibility, not the protocol list. SIWX routes return 402 challenges and must be discoverable.
|
|
128
|
-
- **OpenAPI:** Merge paths for multi-method endpoints (GET + DELETE on same path). Never overwrite.
|
|
129
|
-
- **Duplicate route keys:** Registry silently overwrites (last-write-wins) with a dev-only `console.warn`. This is intentional — Next.js module loading order is non-deterministic during `next build`, so discovery stubs and real handlers may register the same key in either order. Prior art: ElysiaJS uses the identical pattern. See stablestudio `.claude/13_route-registry-dedup.md` for full research.
|
|
130
|
-
- **Dynamic pricing (v0.3.1+):** Early body parsing with `request.clone()` enables accurate dynamic pricing. `maxPrice` is optional and acts as a safety net (cap + fallback). Body is parsed before 402 challenge generation when pricing function exists. See `.claude/15_router-dynamic-pricing-solution.md` for full design and derisking.
|
|
131
|
-
|
|
132
|
-
## Environment Variables
|
|
133
|
-
|
|
134
|
-
### Base URL
|
|
135
|
-
|
|
136
|
-
`baseUrl` is **required** in `RouterConfig`. No auto-detection, no fallbacks. The realm is load-bearing for payment matching (MPP memo indexing, 402 challenge realm), so it must be explicitly set.
|
|
137
|
-
|
|
138
|
-
```typescript
|
|
139
|
-
createRouter({
|
|
140
|
-
baseUrl: process.env.BASE_URL!,
|
|
141
|
-
// ...
|
|
142
|
-
})
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
If `baseUrl` is missing, `createRouter` throws immediately — in dev and prod. This ensures devs discover the issue on first `pnpm dev` rather than deploying with a wrong realm.
|
|
146
|
-
|
|
147
|
-
### CDP API Keys
|
|
148
|
-
|
|
149
|
-
The router uses the default facilitator from `@coinbase/x402`, which requires CDP API keys in `process.env`:
|
|
150
|
-
|
|
151
|
-
- `CDP_API_KEY_ID` — Coinbase Developer Platform API key ID
|
|
152
|
-
- `CDP_API_KEY_SECRET` — CDP API key secret
|
|
153
|
-
|
|
154
|
-
**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.
|
|
155
|
-
|
|
156
|
-
### MPP (Tempo) Environment Variables
|
|
157
|
-
|
|
158
|
-
MPP payment verification requires an **authenticated** Tempo RPC endpoint. The public `https://rpc.tempo.xyz/` returns `401 Unauthorized`.
|
|
159
|
-
|
|
160
|
-
- `TEMPO_RPC_URL` — Authenticated Tempo RPC URL (e.g. `https://user:pass@rpc.mainnet.tempo.xyz`)
|
|
161
|
-
|
|
162
|
-
Alternatively, pass `rpcUrl` in the `mpp` config object to `createRouter()`. Without either, MPP on-chain verification fails with "unauthorized: authentication required".
|
|
163
|
-
|
|
164
|
-
### CDP Environment Variables
|
|
165
|
-
|
|
166
|
-
Without these keys, the default facilitator cannot authenticate with CDP:
|
|
167
|
-
- x402 server `initialize()` fails with "Failed to fetch supported kinds from facilitator: TypeError: fetch failed" or "Facilitator getSupported failed (401): Unauthorized"
|
|
168
|
-
- All payment routes return empty 402 responses (no `PAYMENT-REQUIRED` header, no body)
|
|
169
|
-
|
|
170
|
-
**Example env schema (T3/`@t3-oss/env-nextjs`):**
|
|
171
|
-
|
|
172
|
-
```typescript
|
|
173
|
-
import { createEnv } from "@t3-oss/env-nextjs";
|
|
174
|
-
import { z } from "zod";
|
|
175
|
-
|
|
176
|
-
export const env = createEnv({
|
|
177
|
-
server: {
|
|
178
|
-
CDP_API_KEY_ID: z.string(),
|
|
179
|
-
CDP_API_KEY_SECRET: z.string(),
|
|
180
|
-
// ... other vars
|
|
181
|
-
},
|
|
182
|
-
runtimeEnv: {
|
|
183
|
-
CDP_API_KEY_ID: process.env.CDP_API_KEY_ID,
|
|
184
|
-
CDP_API_KEY_SECRET: process.env.CDP_API_KEY_SECRET,
|
|
185
|
-
// ... other vars
|
|
186
|
-
},
|
|
187
|
-
});
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
**Alternative:** Pass a custom facilitator config to `createRouter()` if you want to use a different facilitator URL or auth mechanism. But for most apps, the default CDP facilitator is correct.
|
|
191
|
-
|
|
192
|
-
## Version Stability
|
|
193
|
-
|
|
194
|
-
The public API is **not stable**. Downstream consumers should pin exact versions (`"@agentcash/router": "0.2.0"`, not `"^0.2.0"`). Breaking changes will happen as we build out multi-protocol support and discover patterns across services. Semver will be respected once we hit 1.0.
|
|
195
|
-
|
|
196
|
-
## Build & Test
|
|
197
|
-
|
|
198
|
-
```bash
|
|
199
|
-
pnpm build # tsup
|
|
200
|
-
pnpm test # vitest
|
|
201
|
-
pnpm typecheck # tsc --noEmit
|
|
202
|
-
pnpm check # format + lint + typecheck + build + test
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
## Development Record
|
|
206
|
-
|
|
207
|
-
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.
|
|
208
|
-
|
|
209
|
-
**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.
|
|
210
|
-
|
|
211
|
-
## Releasing
|
|
212
|
-
|
|
213
|
-
This repo uses [changesets](https://github.com/changesets/changesets) for versioning and npm publishing.
|
|
214
|
-
|
|
215
|
-
### When doing work that should be released:
|
|
216
|
-
|
|
217
|
-
1. **Create a changeset** — Run `pnpm changeset` and describe the changes (patch/minor/major)
|
|
218
|
-
2. **Include the changeset file** in your PR (committed to `.changeset/`)
|
|
219
|
-
3. **Merge PR to main**
|
|
220
|
-
|
|
221
|
-
### What happens automatically:
|
|
222
|
-
|
|
223
|
-
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`
|
|
224
|
-
2. When that version PR is merged, the action **publishes to npm** automatically
|
|
225
|
-
|
|
226
|
-
### Troubleshooting
|
|
227
|
-
|
|
228
|
-
- **Publish fails**: Check `NPM_TOKEN` secret is set and has write access to `@agentcash` scope
|
|
229
|
-
- **No version PR created**: Ensure your PR included a `.changeset/*.md` file
|