@agentcash/router 1.6.0 → 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 +133 -550
- package/dist/index.cjs +567 -525
- package/dist/index.d.cts +90 -71
- package/dist/index.d.ts +90 -71
- package/dist/index.js +559 -506
- package/package.json +6 -14
- package/.claude/CLAUDE.md +0 -343
- 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,8 +20,8 @@
|
|
|
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",
|
|
@@ -42,25 +42,16 @@
|
|
|
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
49
|
"@x402/core": "^2.11.0",
|
|
60
50
|
"@x402/evm": "^2.11.0",
|
|
61
51
|
"@x402/extensions": "^2.11.0",
|
|
62
52
|
"@x402/svm": "^2.11.0",
|
|
63
53
|
"eslint": "^10.0.0",
|
|
54
|
+
"knip": "^6.13.1",
|
|
64
55
|
"mppx": "^0.6.16",
|
|
65
56
|
"next": "^15.0.0",
|
|
66
57
|
"prettier": "^3.8.1",
|
|
@@ -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,343 +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)
|
|
28
|
-
- `src/kv-store/` — Single KV cache layer (`client.ts` is the only file that talks to Redis; `nonce.ts`/`entitlement.ts`/`mpp.ts` are namespaced adapters on top)
|
|
29
|
-
- `src/protocols/` — Protocol handlers (x402.ts, detect.ts). MPP is handled via `mppx` high-level API (`Mppx.create` in index.ts)
|
|
30
|
-
- `src/discovery/` — Auto-generated endpoints (well-known.ts, openapi.ts)
|
|
31
|
-
|
|
32
|
-
## Auth Modes
|
|
33
|
-
|
|
34
|
-
Four auth modes, mutually exclusive (except `.apiKey()` composes with `.paid()`):
|
|
35
|
-
|
|
36
|
-
### `.paid(pricing)` — Payment required
|
|
37
|
-
```typescript
|
|
38
|
-
.paid('0.01') // Static price
|
|
39
|
-
.paid((body) => calcPrice(body)) // Dynamic pricing (body-driven, pre-handler)
|
|
40
|
-
.paid({ field: 'tier', tiers: { basic: { price: '0.01' } } }) // Tiered
|
|
41
|
-
|
|
42
|
-
// Handler-driven dynamic pricing — `.handler()` request-mode bills exactly
|
|
43
|
-
// tickCost per request; `.stream()` streaming bills per `charge()` call.
|
|
44
|
-
.paid({ dynamic: true, tickCost: '0.0005', unitType: 'token', maxPrice: '0.10' })
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
#### Handler-driven dynamic (`.paid({ dynamic: true })`)
|
|
48
|
-
|
|
49
|
-
The handler shape determines the billing model and wire transport:
|
|
50
|
-
|
|
51
|
-
**Request-mode** — `async (ctx) => value`. Bills exactly `tickCost` per
|
|
52
|
-
request. No `charge()` on the context — the wire commitment is fixed at
|
|
53
|
-
credential verification (mppx's non-SSE auto-charge for MPP, or x402 `upto`
|
|
54
|
-
settled for `tickCost`). This is the spec-aligned "discrete paid unit" mode
|
|
55
|
-
per `paymentauth.org/draft-tempo-session-00`.
|
|
56
|
-
|
|
57
|
-
```typescript
|
|
58
|
-
router
|
|
59
|
-
.route('llm/summarize')
|
|
60
|
-
.paid({ dynamic: true, tickCost: '0.01', unitType: 'request', maxPrice: '0.01' })
|
|
61
|
-
.body(z.object({ prompt: z.string() }))
|
|
62
|
-
.handler(async ({ body }) => {
|
|
63
|
-
const summary = await callLLM(body.prompt);
|
|
64
|
-
return { summary }; // always bills $0.01
|
|
65
|
-
});
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
For variable-cost-per-request billing, use the streaming shape below —
|
|
69
|
-
splitting work into yields lets `charge()` meter per unit.
|
|
70
|
-
|
|
71
|
-
**Streaming mode** — `.stream(async function* (ctx) { ... })`. Receives a
|
|
72
|
-
`charge()` callback on `ctx`; one call adds one tick. The invariant:
|
|
73
|
-
|
|
74
|
-
> **one `charge()` call === one tick === `tickCost` USDC === one route-defined unit**
|
|
75
|
-
|
|
76
|
-
The route picks `tickCost` to match its billing unit (one token at $0.0005,
|
|
77
|
-
one byte at $0.0000001, one frame at $0.001) and labels it via `unitType`.
|
|
78
|
-
Total billed is `tickCost × call_count`, capped at `maxPrice`. (Internally:
|
|
79
|
-
mppx auto-charges one prepaid tick at credential verify and marks it; the
|
|
80
|
-
first `charge()` consumes the prepaid without an extra debit, then subsequent
|
|
81
|
-
calls bill fresh ticks live.)
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
router
|
|
85
|
-
.route('llm/stream')
|
|
86
|
-
.paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05', protocols: ['mpp'] })
|
|
87
|
-
.body(z.object({ prompt: z.string() }))
|
|
88
|
-
.stream(async function* ({ body, charge }) {
|
|
89
|
-
for await (const token of streamLLM(body.prompt)) {
|
|
90
|
-
await charge(); // one tick = one token; blocks on need-voucher
|
|
91
|
-
yield token;
|
|
92
|
-
}
|
|
93
|
-
yield '[DONE]'; // free trailing event — no charge before it
|
|
94
|
-
});
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
**Type safety**: TypeScript discriminates by which terminal method you call.
|
|
98
|
-
`.handler()` receives a `HandlerContext` with no `charge` — calling it is a
|
|
99
|
-
compile-time error. `.stream()` receives a `StreamingHandlerContext` with
|
|
100
|
-
`charge` and is only callable after `.paid({ dynamic: true, ... })`.
|
|
101
|
-
|
|
102
|
-
`tickCost` is required per-route on `.paid({ dynamic: true })` — the builder
|
|
103
|
-
throws at registration if it's missing. `unitType` is optional (cosmetic
|
|
104
|
-
label, defaults to undefined which mppx surfaces as plain ticks).
|
|
105
|
-
|
|
106
|
-
**Transport selection**: the router picks the wire format from the terminal
|
|
107
|
-
method. `.handler()` request-mode goes through plain HTTP with a
|
|
108
|
-
`Payment-Receipt` header (mppx's `tempo.session({ sse: false })`). `.stream()`
|
|
109
|
-
goes through SSE with inline per-tick voucher events
|
|
110
|
-
(`tempo.session({ sse: true })`). Two mppx instances run side-by-side
|
|
111
|
-
sharing the same store, secretKey, and realm so channel state and challenge
|
|
112
|
-
HMACs are interchangeable.
|
|
113
|
-
|
|
114
|
-
Both terminal methods work on x402 `upto` (settles cumulative atomic amount;
|
|
115
|
-
Permit2Proxy enforces ≤ maxPrice) and MPP. `.stream()` requires MPP — x402
|
|
116
|
-
has no streaming primitive.
|
|
117
|
-
|
|
118
|
-
**`suggestedDeposit` on MPP session 402 challenges**: defaults to
|
|
119
|
-
`tickCost × RouterConfig.mpp.session.depositMultiplier` (default `10`), or
|
|
120
|
-
the route's `maxPrice` when set. Raise the multiplier at deployment level to
|
|
121
|
-
cover more requests per channel, or set `maxPrice` per-route when a single
|
|
122
|
-
request can exceed the default budget.
|
|
123
|
-
|
|
124
|
-
### `.siwx()` — Wallet identity required (no payment)
|
|
125
|
-
```typescript
|
|
126
|
-
.siwx().handler(async ({ wallet }) => { /* wallet is verified */ })
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
### `.apiKey(resolver)` — API key / Bearer token auth
|
|
130
|
-
For admin routes, cron jobs, internal services. Checks `X-API-Key` header OR `Authorization: Bearer <token>`.
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
// Admin route with API key
|
|
134
|
-
export const GET = router
|
|
135
|
-
.route('admin/users')
|
|
136
|
-
.apiKey(async (key) => {
|
|
137
|
-
const admin = await db.admin.findByKey(key);
|
|
138
|
-
return admin ?? null; // null = 401, truthy = ctx.account
|
|
139
|
-
})
|
|
140
|
-
.handler(async ({ account }) => {
|
|
141
|
-
// account is whatever resolver returned
|
|
142
|
-
return db.user.findMany();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Cron job with static secret
|
|
146
|
-
export const POST = router
|
|
147
|
-
.route('cron/cleanup')
|
|
148
|
-
.apiKey((key) => key === process.env.CRON_SECRET ? { cron: true } : null)
|
|
149
|
-
.handler(async () => { /* ... */ });
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
**Headers accepted:** `X-API-Key: <key>` or `Authorization: Bearer <key>`
|
|
153
|
-
|
|
154
|
-
**Composing with payment:** `.apiKey()` can layer on `.paid()` — auth runs first, payment second:
|
|
155
|
-
```typescript
|
|
156
|
-
.apiKey(resolver).paid('0.01') // Must pass API key AND pay
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### `.unprotected()` — No auth
|
|
160
|
-
```typescript
|
|
161
|
-
.unprotected().handler(async () => { /* public endpoint */ })
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
## Pre-Payment Validation
|
|
165
|
-
|
|
166
|
-
### `.validate(fn)` — Async business validation before 402 challenge
|
|
167
|
-
|
|
168
|
-
For checks that need DB lookups or external APIs before showing a price. Runs after body parsing, before the 402 challenge. Requires `.body()`.
|
|
169
|
-
|
|
170
|
-
```typescript
|
|
171
|
-
// Domain registration with availability check
|
|
172
|
-
router
|
|
173
|
-
.route('domain/register')
|
|
174
|
-
.paid(calculatePrice, { maxPrice: '10.00' })
|
|
175
|
-
.body(RegisterSchema) // .body() before .validate() for type inference
|
|
176
|
-
.validate(async (body) => {
|
|
177
|
-
if (await isDomainTaken(body.domain)) {
|
|
178
|
-
throw Object.assign(new Error('Domain already taken'), { status: 409 });
|
|
179
|
-
}
|
|
180
|
-
})
|
|
181
|
-
.handler(async ({ body, wallet }) => {
|
|
182
|
-
return registerDomain(body.domain, wallet);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Rate limiting before payment
|
|
186
|
-
router
|
|
187
|
-
.route('api/expensive')
|
|
188
|
-
.paid('1.00')
|
|
189
|
-
.body(RequestSchema)
|
|
190
|
-
.validate(async (body) => {
|
|
191
|
-
const usage = await getUserUsage(body.userId);
|
|
192
|
-
if (usage >= DAILY_LIMIT) {
|
|
193
|
-
throw Object.assign(new Error('Daily limit reached'), { status: 429 });
|
|
194
|
-
}
|
|
195
|
-
})
|
|
196
|
-
.handler(async ({ body }) => { ... });
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
**Pipeline order:** `body parse → validate → 402 challenge → payment → handler`
|
|
200
|
-
|
|
201
|
-
**Error handling:** Respects `.status` on thrown errors (default: 400). Use `Object.assign(new Error('msg'), { status: 409 })` for custom codes.
|
|
202
|
-
|
|
203
|
-
**Works with all auth modes:** paid, siwx, apiKey, unprotected.
|
|
204
|
-
|
|
205
|
-
## Critical Rules
|
|
206
|
-
|
|
207
|
-
- **Error handling:** Respect `.status` on any thrown error, not just `HttpError`. The `Object.assign(new Error(), { status })` pattern is universal in Node.js.
|
|
208
|
-
- **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`.
|
|
209
|
-
- **Discovery:** `authMode !== 'unprotected'` determines well-known visibility, not the protocol list. SIWX routes return 402 challenges and must be discoverable.
|
|
210
|
-
- **OpenAPI:** Merge paths for multi-method endpoints (GET + DELETE on same path). Never overwrite.
|
|
211
|
-
- **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.
|
|
212
|
-
- **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.
|
|
213
|
-
|
|
214
|
-
## Environment Variables
|
|
215
|
-
|
|
216
|
-
### Base URL
|
|
217
|
-
|
|
218
|
-
`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.
|
|
219
|
-
|
|
220
|
-
```typescript
|
|
221
|
-
createRouter({
|
|
222
|
-
baseUrl: process.env.BASE_URL!,
|
|
223
|
-
// ...
|
|
224
|
-
})
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
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.
|
|
228
|
-
|
|
229
|
-
### CDP API Keys
|
|
230
|
-
|
|
231
|
-
The router uses the default facilitator from `@coinbase/x402`, which requires CDP API keys in `process.env`:
|
|
232
|
-
|
|
233
|
-
- `CDP_API_KEY_ID` — Coinbase Developer Platform API key ID
|
|
234
|
-
- `CDP_API_KEY_SECRET` — CDP API key secret
|
|
235
|
-
|
|
236
|
-
**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.
|
|
237
|
-
|
|
238
|
-
### MPP (Tempo) Environment Variables
|
|
239
|
-
|
|
240
|
-
MPP payment verification requires an **authenticated** Tempo RPC endpoint. The public `https://rpc.tempo.xyz/` returns `401 Unauthorized`.
|
|
241
|
-
|
|
242
|
-
- `TEMPO_RPC_URL` — Authenticated Tempo RPC URL (e.g. `https://user:pass@rpc.mainnet.tempo.xyz`)
|
|
243
|
-
|
|
244
|
-
Alternatively, pass `rpcUrl` in the `mpp` config object to `createRouter()`. Without either, MPP on-chain verification fails with "unauthorized: authentication required".
|
|
245
|
-
|
|
246
|
-
### MPP Server Wallets: `operatorKey` and `feePayerKey`
|
|
247
|
-
|
|
248
|
-
Two distinct roles, two distinct wallets:
|
|
249
|
-
|
|
250
|
-
- **`mpp.operatorKey`** — signs server-side on-chain ops (channel close/settle). Required for sessions. Its derived address **must equal `recipient`/payee** because mppx's close handler asserts `sender === payee` on settle.
|
|
251
|
-
- **`mpp.feePayerKey`** *(optional)* — sponsors gas for client-signed open/topUp txs. Omit to disable sponsorship; clients then pay their own gas.
|
|
252
|
-
|
|
253
|
-
**The two MUST resolve to different addresses when both are set.** Tempo rejects fee-delegated txs where `sender === feePayer` with `-32000 "fee payer cannot resolve to sender"`. This bites the server-signed close/settle path. The router validates the addresses at `createRouter()` time and throws `mpp_operator_equals_fee_payer` if they collide — production `next build` fails fast; dev surfaces a logged error.
|
|
254
|
-
|
|
255
|
-
### KV Store
|
|
256
|
-
|
|
257
|
-
One KV cache backs all three persistent stores (SIWX nonce, SIWX entitlement, MPP tx-hash replay). Each consumer gets its own key prefix (`siwx:nonce:`, `siwx:ent:`, `mpp:`).
|
|
258
|
-
|
|
259
|
-
Resolution order in `createRouter`:
|
|
260
|
-
|
|
261
|
-
1. `kvStore: { url, token }` — build the REST client from those credentials.
|
|
262
|
-
2. `kvStore: <KvStore>` — bring-your-own implementation (escape hatch for Cloudflare KV, ioredis, etc.).
|
|
263
|
-
3. Omitted — auto-read `KV_REST_API_URL` + `KV_REST_API_TOKEN` from `process.env`.
|
|
264
|
-
4. Env missing — fall back to in-memory stores (fine for local dev, unsafe in serverless production).
|
|
265
|
-
|
|
266
|
-
```typescript
|
|
267
|
-
createRouter({
|
|
268
|
-
kvStore: {
|
|
269
|
-
url: process.env.UPSTASH_REDIS_REST_URL!,
|
|
270
|
-
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
|
271
|
-
},
|
|
272
|
-
// ...
|
|
273
|
-
});
|
|
274
|
-
```
|
|
275
|
-
|
|
276
|
-
The only file that talks to Redis is `src/kv-store/client.ts`. It speaks the Upstash REST protocol with plain `fetch` (also what Vercel KV exposes). If you need a different backend, implement the `KvStore` interface and pass it as `kvStore`.
|
|
277
|
-
|
|
278
|
-
### CDP Environment Variables
|
|
279
|
-
|
|
280
|
-
Without these keys, the default facilitator cannot authenticate with CDP:
|
|
281
|
-
- x402 server `initialize()` fails with "Failed to fetch supported kinds from facilitator: TypeError: fetch failed" or "Facilitator getSupported failed (401): Unauthorized"
|
|
282
|
-
- All payment routes return empty 402 responses (no `PAYMENT-REQUIRED` header, no body)
|
|
283
|
-
|
|
284
|
-
**Example env schema (T3/`@t3-oss/env-nextjs`):**
|
|
285
|
-
|
|
286
|
-
```typescript
|
|
287
|
-
import { createEnv } from "@t3-oss/env-nextjs";
|
|
288
|
-
import { z } from "zod";
|
|
289
|
-
|
|
290
|
-
export const env = createEnv({
|
|
291
|
-
server: {
|
|
292
|
-
CDP_API_KEY_ID: z.string(),
|
|
293
|
-
CDP_API_KEY_SECRET: z.string(),
|
|
294
|
-
// ... other vars
|
|
295
|
-
},
|
|
296
|
-
runtimeEnv: {
|
|
297
|
-
CDP_API_KEY_ID: process.env.CDP_API_KEY_ID,
|
|
298
|
-
CDP_API_KEY_SECRET: process.env.CDP_API_KEY_SECRET,
|
|
299
|
-
// ... other vars
|
|
300
|
-
},
|
|
301
|
-
});
|
|
302
|
-
```
|
|
303
|
-
|
|
304
|
-
**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.
|
|
305
|
-
|
|
306
|
-
## Version Stability
|
|
307
|
-
|
|
308
|
-
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.
|
|
309
|
-
|
|
310
|
-
## Build & Test
|
|
311
|
-
|
|
312
|
-
```bash
|
|
313
|
-
pnpm build # tsup
|
|
314
|
-
pnpm test # vitest
|
|
315
|
-
pnpm typecheck # tsc --noEmit
|
|
316
|
-
pnpm check # format + lint + typecheck + build + test
|
|
317
|
-
```
|
|
318
|
-
|
|
319
|
-
## Development Record
|
|
320
|
-
|
|
321
|
-
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.
|
|
322
|
-
|
|
323
|
-
**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.
|
|
324
|
-
|
|
325
|
-
## Releasing
|
|
326
|
-
|
|
327
|
-
This repo uses [changesets](https://github.com/changesets/changesets) for versioning and npm publishing.
|
|
328
|
-
|
|
329
|
-
### When doing work that should be released:
|
|
330
|
-
|
|
331
|
-
1. **Create a changeset** — Run `pnpm changeset` and describe the changes (patch/minor/major)
|
|
332
|
-
2. **Include the changeset file** in your PR (committed to `.changeset/`)
|
|
333
|
-
3. **Merge PR to main**
|
|
334
|
-
|
|
335
|
-
### What happens automatically:
|
|
336
|
-
|
|
337
|
-
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`
|
|
338
|
-
2. When that version PR is merged, the action **publishes to npm** automatically
|
|
339
|
-
|
|
340
|
-
### Troubleshooting
|
|
341
|
-
|
|
342
|
-
- **Publish fails**: Check `NPM_TOKEN` secret is set and has write access to `@agentcash` scope
|
|
343
|
-
- **No version PR created**: Ensure your PR included a `.changeset/*.md` file
|