@agentcash/router 1.4.1 → 1.5.1

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/README.md CHANGED
@@ -46,7 +46,61 @@ export const env = createEnv({
46
46
  });
47
47
  ```
48
48
 
49
- Without these keys, x402 routes will fail to initialize (empty 402 responses, no payment header).
49
+ Without these keys, x402 routes that use the default EVM facilitator will fail to initialize.
50
+ `createRouter(config)` always validates the base URL and protocol list, throws protocol config
51
+ errors in production, and keeps protocol-specific init errors as request-time JSON 500s in
52
+ development. Call `validateRouterConfig(config)` before `createRouter(config)` when you want
53
+ missing facilitator, MPP, or store configuration to throw immediately in every environment.
54
+
55
+ ### Recommended strict setup
56
+
57
+ ```typescript
58
+ import {
59
+ createRouter,
60
+ mppFromEnv,
61
+ validateRouterConfig,
62
+ x402AcceptsFromEnv,
63
+ type ProtocolType,
64
+ } from '@agentcash/router';
65
+
66
+ const payeeAddress = process.env.X402_WALLET_ADDRESS ?? process.env.X402_PAYEE_ADDRESS;
67
+ const accepts = x402AcceptsFromEnv(process.env, { payeeAddress });
68
+ const protocols: ProtocolType[] = process.env.MPP_SECRET_KEY ? ['x402', 'mpp'] : ['x402'];
69
+
70
+ const config = {
71
+ payeeAddress: payeeAddress!,
72
+ baseUrl: process.env.NEXT_PUBLIC_BASE_URL!,
73
+ strictRoutes: true,
74
+ protocols,
75
+ x402: { accepts },
76
+ mpp: mppFromEnv(process.env, {
77
+ recipient: payeeAddress,
78
+ useDefaultStore: true,
79
+ }),
80
+ discovery: {
81
+ title: 'My API',
82
+ version: '1.0.0',
83
+ },
84
+ };
85
+
86
+ validateRouterConfig(config);
87
+ export const router = createRouter(config);
88
+ ```
89
+
90
+ `x402AcceptsFromEnv()` always adds Base (`BASE_NETWORK`) and also adds Solana
91
+ mainnet (`SOLANA_MAINNET_NETWORK`) when `SOLANA_PAYEE_ADDRESS` is set. Solana
92
+ addresses are case-sensitive and are preserved as-is. It reads
93
+ `X402_WALLET_ADDRESS` by default; older services that still use
94
+ `X402_PAYEE_ADDRESS` can pass `{ payeeEnv: 'X402_PAYEE_ADDRESS' }`.
95
+
96
+ `mppFromEnv()` returns `undefined` when no MPP env vars are present. If any MPP
97
+ env var is present, the full trio is required: `MPP_SECRET_KEY`, `MPP_CURRENCY`,
98
+ and `TEMPO_RPC_URL`. `MPP_CURRENCY` must be the Tempo currency address; for
99
+ Tempo USDC use `TEMPO_USDC_CURRENCY`. Optional `MPP_FEE_PAYER_KEY` is included
100
+ when present and validated as a 32-byte EVM private key. `mppFromEnv()` only
101
+ builds config; call `validateRouterConfig(config)` before `createRouter(config)`
102
+ to fail fast on `mpp.useDefaultStore` store env (`KV_REST_API_URL` and
103
+ `KV_REST_API_TOKEN`) when you use the default store.
50
104
 
51
105
  ## Quick Start
52
106
 
@@ -57,7 +111,7 @@ Without these keys, x402 routes will fail to initialize (empty 402 responses, no
57
111
  import { createRouter } from '@agentcash/router';
58
112
 
59
113
  export const router = createRouter({
60
- payeeAddress: process.env.X402_PAYEE_ADDRESS!,
114
+ payeeAddress: process.env.X402_WALLET_ADDRESS!,
61
115
  baseUrl: process.env.NEXT_PUBLIC_BASE_URL!,
62
116
  strictRoutes: true, // recommended
63
117
  discovery: {
@@ -143,9 +197,40 @@ Creates a `ServiceRouter` instance.
143
197
  | `plugin` | `RouterPlugin` | `undefined` | Observability plugin |
144
198
  | `prices` | `Record<string, string>` | `undefined` | Central pricing map (auto-applied) |
145
199
  | `siwx.nonceStore` | `NonceStore` | `MemoryNonceStore` | Custom nonce store |
146
- | `mpp` | `{ secretKey, currency, recipient? }` | `undefined` | MPP config |
200
+ | `mpp` | `{ secretKey, currency, recipient?, rpcUrl?, feePayerKey?, useDefaultStore? }` | `undefined` | MPP config |
201
+ | `protocols` | `('x402' \| 'mpp')[]` | `['x402']` | Default protocols for paid routes |
147
202
  | `strictRoutes` | `boolean` | `false` | Enforce `route({ path })` and prevent key/path divergence |
148
203
 
204
+ ### Config validation helpers
205
+
206
+ ```typescript
207
+ import {
208
+ BASE_NETWORK,
209
+ SOLANA_MAINNET_NETWORK,
210
+ TEMPO_USDC_CURRENCY,
211
+ getRouterConfigIssues,
212
+ mppFromEnv,
213
+ paidOptionsForProtocols,
214
+ validateRouterConfig,
215
+ x402AcceptsFromEnv,
216
+ } from '@agentcash/router';
217
+ ```
218
+
219
+ - `validateRouterConfig(config)` throws `RouterConfigError` with structured
220
+ issues. Use it when you want invalid env/config to fail at startup in every
221
+ environment; `createRouter(config)` still performs its own validation and
222
+ keeps deferred protocol init errors in development for request-time feedback.
223
+ - `getRouterConfigIssues(config)` returns the same structured issues without
224
+ throwing.
225
+ - `x402AcceptsFromEnv(env)` builds Base and optional Solana x402 accepts from
226
+ `X402_WALLET_ADDRESS` and `SOLANA_PAYEE_ADDRESS`.
227
+ - `mppFromEnv(env)` builds MPP config only when MPP env is present, and rejects
228
+ partial MPP env.
229
+ - `paidOptionsForProtocols(protocols)` copies a protocol array into a
230
+ route-level `PaidOptions` object.
231
+ - Manual `.paid(price)` routes inherit `createRouter({ protocols })` unless
232
+ the route passes its own `options.protocols`.
233
+
149
234
  ### Path-First Routing
150
235
 
151
236
  Use path-first route definitions to keep runtime, OpenAPI, and discovery aligned:
@@ -170,11 +255,13 @@ The fluent builder ensures compile-time safety:
170
255
  - `.siwx()` - SIWX wallet auth
171
256
  - `.apiKey(resolver)` - API key auth (composable with `.paid()`)
172
257
  - `.unprotected()` - No auth
173
- - `.body(zodSchema)` - Request body validation
174
- - `.query(zodSchema)` - Query parameter validation
175
- - `.output(zodSchema)` - Response schema (for OpenAPI)
258
+ - `.body(zodSchema, example?)` - Request body validation with optional discovery example
259
+ - `.query(zodSchema, example?)` - Query parameter validation with optional discovery example
260
+ - `.output(zodSchema, example?)` - Response schema with optional discovery example
261
+ - `.inputExample(sample)` / `.outputExample(sample)` - Optional examples when a separate call reads better
176
262
  - `.description(text)` - Route description (for OpenAPI)
177
263
  - `.provider(name, config?)` - Provider monitoring (see [Provider Monitoring](#provider-monitoring))
264
+ - `.settlement({ beforeSettle, afterSettle, onSettledHandlerError, onSettlementError })` - Payment lifecycle hooks
178
265
  - `.handler(fn)` - Terminal method, returns Next.js handler
179
266
 
180
267
  ### Pricing Modes
@@ -259,12 +346,51 @@ interface HandlerContext<TBody, TQuery> {
259
346
  query: TQuery; // Parsed + validated
260
347
  request: NextRequest; // Raw request
261
348
  wallet: string | null; // Verified wallet address
349
+ payment: HandlerPaymentContext | null; // Payment metadata for this request
262
350
  account: unknown; // From .apiKey() resolver
263
351
  alert: AlertFn; // Fire observability alerts
264
352
  setVerifiedWallet: (addr: string) => void;
265
353
  }
266
354
  ```
267
355
 
356
+ `payment` is `null` for unprotected, API-key-only, and SIWX-only requests. For
357
+ paid requests it includes `protocol`, `status`, `payer`, `amount`, `network`,
358
+ and best-effort recipient/transaction/receipt metadata when the protocol
359
+ provides it. x402 handlers currently see `status: 'verified'` because settlement
360
+ happens after a successful handler response.
361
+
362
+ ### Settlement Lifecycle
363
+
364
+ For paid routes, use `.settlement()` when final checks belong after handler work
365
+ but before router-controlled settlement:
366
+
367
+ ```typescript
368
+ router.route('render')
369
+ .paid('0.10')
370
+ .body(schema, { prompt: 'city at dusk' })
371
+ .settlement({
372
+ beforeSettle: async ({ result }) => {
373
+ if (!isUsableResult(result)) {
374
+ throw Object.assign(new Error('Render failed validation'), { status: 502 });
375
+ }
376
+ },
377
+ afterSettle: async ({ payment, result }) => {
378
+ await ledger.record({ tx: payment.transaction, result });
379
+ },
380
+ onSettledHandlerError: async ({ payment, error }) => {
381
+ await compensationQueue.enqueue({ receipt: payment.receipt, error });
382
+ },
383
+ })
384
+ .handler(async ({ body }) => render(body));
385
+ ```
386
+
387
+ `beforeSettle` can still prevent the charge for x402 and MPP
388
+ transaction-payload flows. `afterSettle` is for durable ledgers, analytics, and
389
+ post-settlement bookkeeping. `onSettledHandlerError` covers already-settled
390
+ MPP requests whose handler returns an error response, which is the right place
391
+ to enqueue app-owned refund or compensation work. The router cannot generically
392
+ refund protocol payments because it does not hold merchant signing keys.
393
+
268
394
  ### RouterPlugin
269
395
 
270
396
  Pluggable observability. All hooks are optional and fire-and-forget.
@@ -283,8 +409,13 @@ const myPlugin: RouterPlugin = {
283
409
  };
284
410
 
285
411
  export const router = createRouter({
286
- payeeAddress: process.env.X402_PAYEE_ADDRESS!,
412
+ payeeAddress: process.env.X402_WALLET_ADDRESS!,
413
+ baseUrl: process.env.NEXT_PUBLIC_BASE_URL!,
287
414
  plugin: myPlugin,
415
+ discovery: {
416
+ title: 'My API',
417
+ version: '1.0.0',
418
+ },
288
419
  });
289
420
  ```
290
421
 
@@ -294,8 +425,13 @@ Built-in `consolePlugin()` logs lifecycle events:
294
425
  import { createRouter, consolePlugin } from '@agentcash/router';
295
426
 
296
427
  export const router = createRouter({
297
- payeeAddress: process.env.X402_PAYEE_ADDRESS!,
428
+ payeeAddress: process.env.X402_WALLET_ADDRESS!,
429
+ baseUrl: process.env.NEXT_PUBLIC_BASE_URL!,
298
430
  plugin: consolePlugin(),
431
+ discovery: {
432
+ title: 'My API',
433
+ version: '1.0.0',
434
+ },
299
435
  });
300
436
  ```
301
437
 
@@ -305,7 +441,12 @@ For services with many static-priced routes:
305
441
 
306
442
  ```typescript
307
443
  const router = createRouter({
308
- payeeAddress: process.env.X402_PAYEE_ADDRESS!,
444
+ payeeAddress: process.env.X402_WALLET_ADDRESS!,
445
+ baseUrl: process.env.NEXT_PUBLIC_BASE_URL!,
446
+ discovery: {
447
+ title: 'My API',
448
+ version: '1.0.0',
449
+ },
309
450
  prices: {
310
451
  'search': '0.02',
311
452
  'lookup': '0.05',