@agentcash/router 1.5.1 → 1.6.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/.claude/CLAUDE.md +116 -2
- package/README.md +34 -5
- package/dist/index.cjs +3319 -2009
- package/dist/index.d.cts +399 -274
- package/dist/index.d.ts +399 -274
- package/dist/index.js +3316 -2008
- package/package.json +11 -11
- package/dist/client/index.cjs +0 -94
- package/dist/client/index.d.cts +0 -86
- package/dist/client/index.d.ts +0 -86
- package/dist/client/index.js +0 -56
- package/dist/siwx-BMlja_nt.d.cts +0 -9
- package/dist/siwx-BMlja_nt.d.ts +0 -9
package/.claude/CLAUDE.md
CHANGED
|
@@ -24,7 +24,8 @@ Protocol-agnostic route framework for Next.js App Router APIs with x402 payment,
|
|
|
24
24
|
- `src/pricing.ts` — Price resolution (static, tiered, dynamic)
|
|
25
25
|
- `src/plugin.ts` — Plugin hook system
|
|
26
26
|
- `src/server.ts` — x402 server initialization
|
|
27
|
-
- `src/auth/` — Auth modules (siwx.ts, api-key.ts
|
|
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)
|
|
28
29
|
- `src/protocols/` — Protocol handlers (x402.ts, detect.ts). MPP is handled via `mppx` high-level API (`Mppx.create` in index.ts)
|
|
29
30
|
- `src/discovery/` — Auto-generated endpoints (well-known.ts, openapi.ts)
|
|
30
31
|
|
|
@@ -35,10 +36,91 @@ Four auth modes, mutually exclusive (except `.apiKey()` composes with `.paid()`)
|
|
|
35
36
|
### `.paid(pricing)` — Payment required
|
|
36
37
|
```typescript
|
|
37
38
|
.paid('0.01') // Static price
|
|
38
|
-
.paid((body) => calcPrice(body)) // Dynamic pricing
|
|
39
|
+
.paid((body) => calcPrice(body)) // Dynamic pricing (body-driven, pre-handler)
|
|
39
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
|
+
});
|
|
40
95
|
```
|
|
41
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
|
+
|
|
42
124
|
### `.siwx()` — Wallet identity required (no payment)
|
|
43
125
|
```typescript
|
|
44
126
|
.siwx().handler(async ({ wallet }) => { /* wallet is verified */ })
|
|
@@ -161,6 +243,38 @@ MPP payment verification requires an **authenticated** Tempo RPC endpoint. The p
|
|
|
161
243
|
|
|
162
244
|
Alternatively, pass `rpcUrl` in the `mpp` config object to `createRouter()`. Without either, MPP on-chain verification fails with "unauthorized: authentication required".
|
|
163
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
|
+
|
|
164
278
|
### CDP Environment Variables
|
|
165
279
|
|
|
166
280
|
Without these keys, the default facilitator cannot authenticate with CDP:
|
package/README.md
CHANGED
|
@@ -75,7 +75,6 @@ const config = {
|
|
|
75
75
|
x402: { accepts },
|
|
76
76
|
mpp: mppFromEnv(process.env, {
|
|
77
77
|
recipient: payeeAddress,
|
|
78
|
-
useDefaultStore: true,
|
|
79
78
|
}),
|
|
80
79
|
discovery: {
|
|
81
80
|
title: 'My API',
|
|
@@ -99,8 +98,38 @@ and `TEMPO_RPC_URL`. `MPP_CURRENCY` must be the Tempo currency address; for
|
|
|
99
98
|
Tempo USDC use `TEMPO_USDC_CURRENCY`. Optional `MPP_FEE_PAYER_KEY` is included
|
|
100
99
|
when present and validated as a 32-byte EVM private key. `mppFromEnv()` only
|
|
101
100
|
builds config; call `validateRouterConfig(config)` before `createRouter(config)`
|
|
102
|
-
to fail fast on
|
|
103
|
-
|
|
101
|
+
to fail fast on misconfiguration.
|
|
102
|
+
|
|
103
|
+
### Persistent KV store
|
|
104
|
+
|
|
105
|
+
The router uses a single KV cache for three things: SIWX nonce replay
|
|
106
|
+
protection, SIWX entitlement records, and MPP tx-hash replay protection. Each
|
|
107
|
+
consumer gets its own key prefix (`siwx:nonce:`, `siwx:ent:`, `mpp:`) so one
|
|
108
|
+
Redis/Upstash instance serves all three.
|
|
109
|
+
|
|
110
|
+
`createRouter` reads `KV_REST_API_URL` + `KV_REST_API_TOKEN` from `process.env`
|
|
111
|
+
automatically (Vercel KV and Upstash both set these). If both are present, it
|
|
112
|
+
builds an Upstash-compatible REST client and wires it into every store. If
|
|
113
|
+
either is missing, all three stores fall back to in-memory — fine for local
|
|
114
|
+
dev, unsafe in serverless production.
|
|
115
|
+
|
|
116
|
+
To use REST credentials under a different env name, pass them as
|
|
117
|
+
`{ url, token }`:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { createRouter } from '@agentcash/router';
|
|
121
|
+
|
|
122
|
+
createRouter({
|
|
123
|
+
kvStore: {
|
|
124
|
+
url: process.env.UPSTASH_REDIS_REST_URL!,
|
|
125
|
+
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
|
|
126
|
+
},
|
|
127
|
+
// ...
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For a different backend entirely (Cloudflare KV, ioredis, etc.), implement the
|
|
132
|
+
`KvStore` interface and pass your instance as `kvStore`.
|
|
104
133
|
|
|
105
134
|
## Quick Start
|
|
106
135
|
|
|
@@ -196,8 +225,8 @@ Creates a `ServiceRouter` instance.
|
|
|
196
225
|
| `network` | `string` | `'eip155:8453'` | Blockchain network |
|
|
197
226
|
| `plugin` | `RouterPlugin` | `undefined` | Observability plugin |
|
|
198
227
|
| `prices` | `Record<string, string>` | `undefined` | Central pricing map (auto-applied) |
|
|
199
|
-
| `
|
|
200
|
-
| `mpp` | `{ secretKey, currency, recipient?, rpcUrl?, feePayerKey?,
|
|
228
|
+
| `kvStore` | `KvStore \| { url, token }` | auto from `KV_REST_API_URL` + `KV_REST_API_TOKEN`, else memory | Single KV cache for SIWX nonce, SIWX entitlement, and MPP tx-hash replay |
|
|
229
|
+
| `mpp` | `{ secretKey, currency, recipient?, rpcUrl?, feePayerKey?, session? }` | `undefined` | MPP config |
|
|
201
230
|
| `protocols` | `('x402' \| 'mpp')[]` | `['x402']` | Default protocols for paid routes |
|
|
202
231
|
| `strictRoutes` | `boolean` | `false` | Enforce `route({ path })` and prevent key/path divergence |
|
|
203
232
|
|