@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/dist/index.d.cts CHANGED
@@ -1,78 +1,39 @@
1
1
  import { FacilitatorConfig } from '@x402/core/http';
2
2
  import { NextRequest, NextResponse } from 'next/server';
3
3
  import { ZodType } from 'zod';
4
- import { Store } from 'mppx';
5
- import { PaymentRequirements, PaymentRequired, SettleResponse, Network } from '@x402/core/types';
4
+ import { PaymentRequirements, PaymentRequired, VerifyResponse, SettleResponse, Network } from '@x402/core/types';
6
5
  import * as viem from 'viem';
6
+ import { Transport } from 'mppx/server';
7
7
 
8
- interface EntitlementStore {
9
- has(route: string, wallet: string): Promise<boolean>;
10
- grant(route: string, wallet: string): Promise<void>;
11
- }
12
- /**
13
- * In-memory SIWX entitlement store.
14
- *
15
- * Suitable for development and tests. Not durable across server restarts.
16
- */
17
- declare class MemoryEntitlementStore implements EntitlementStore {
18
- private readonly routeToWallets;
19
- has(route: string, wallet: string): Promise<boolean>;
20
- grant(route: string, wallet: string): Promise<void>;
21
- }
22
- interface RedisEntitlementStoreOptions {
23
- /** Key prefix. Default: 'siwx:entitlement:' */
24
- prefix?: string;
25
- }
26
- /**
27
- * Redis-backed entitlement store for paid+SIWX acceleration.
28
- */
29
- declare function createRedisEntitlementStore(client: unknown, options?: RedisEntitlementStoreOptions): EntitlementStore;
8
+ interface KvStore {
9
+ get(key: string): Promise<unknown>;
10
+ set(key: string, value: unknown): Promise<void>;
11
+ del(key: string): Promise<void>;
12
+ setNxEx(key: string, value: unknown, ttlSeconds: number): Promise<boolean>;
13
+ sadd(key: string, member: string): Promise<void>;
14
+ sismember(key: string, member: string): Promise<boolean>;
15
+ update<R>(key: string, fn: (current: unknown) => KvChange<R>): Promise<R>;
16
+ }
17
+ type KvChange<R> = {
18
+ op: 'noop';
19
+ result: R;
20
+ } | {
21
+ op: 'set';
22
+ value: unknown;
23
+ result: R;
24
+ } | {
25
+ op: 'delete';
26
+ result: R;
27
+ };
30
28
 
31
- /**
32
- * SIWX challenge expiry in milliseconds.
33
- * Currently not configurable per-route — this is a known limitation.
34
- * Future versions may add `siwx: { expiryMs }` to RouterConfig.
35
- */
36
- declare const SIWX_CHALLENGE_EXPIRY_MS: number;
37
29
  interface NonceStore {
38
30
  check(nonce: string): Promise<boolean>;
39
31
  }
40
- /**
41
- * In-memory nonce store for development and testing.
42
- * NOT suitable for production serverless environments (Vercel, etc.)
43
- * where each function invocation gets fresh memory.
44
- *
45
- * For production, use `createRedisNonceStore()` with Upstash or ioredis.
46
- */
47
- declare class MemoryNonceStore implements NonceStore {
48
- private seen;
49
- check(nonce: string): Promise<boolean>;
50
- private evict;
51
- }
52
- interface RedisNonceStoreOptions {
53
- /** Key prefix for nonce storage. Default: 'siwx:nonce:' */
54
- prefix?: string;
55
- /** TTL in milliseconds. Default: SIWX_CHALLENGE_EXPIRY_MS (5 minutes) */
56
- ttlMs?: number;
32
+
33
+ interface EntitlementStore {
34
+ has(route: string, wallet: string): Promise<boolean>;
35
+ grant(route: string, wallet: string): Promise<void>;
57
36
  }
58
- /**
59
- * Create a Redis-backed nonce store for production SIWX replay protection.
60
- * Auto-detects client type (Upstash or ioredis) and uses appropriate API.
61
- *
62
- * @example
63
- * ```ts
64
- * // Upstash (Vercel)
65
- * import { Redis } from '@upstash/redis';
66
- * const redis = new Redis({ url: process.env.UPSTASH_URL, token: process.env.UPSTASH_TOKEN });
67
- * const nonceStore = createRedisNonceStore(redis);
68
- *
69
- * // ioredis
70
- * import Redis from 'ioredis';
71
- * const redis = new Redis(process.env.REDIS_URL);
72
- * const nonceStore = createRedisNonceStore(redis);
73
- * ```
74
- */
75
- declare function createRedisNonceStore(client: unknown, opts?: RedisNonceStoreOptions): NonceStore;
76
37
 
77
38
  interface RequestMeta {
78
39
  requestId: string;
@@ -148,7 +109,6 @@ interface RouterPlugin {
148
109
  onAlert?(ctx: PluginContext, alert: AlertEvent): void;
149
110
  onProviderQuota?(ctx: PluginContext, event: ProviderQuotaEvent): void;
150
111
  }
151
- declare function consolePlugin(): RouterPlugin;
152
112
 
153
113
  declare class HttpError extends Error {
154
114
  readonly status: number;
@@ -167,14 +127,19 @@ type JsonValue = JsonPrimitive | JsonObject | JsonValue[];
167
127
  type JsonObject = {
168
128
  [key: string]: JsonValue;
169
129
  };
170
-
171
130
  interface X402Server {
172
131
  initialize(): Promise<void>;
173
132
  buildPaymentRequirementsFromOptions(options: Array<{
174
133
  scheme: string;
175
134
  network: string;
176
- price: string;
135
+ price: string | {
136
+ asset: string;
137
+ amount: string;
138
+ extra?: Record<string, unknown>;
139
+ };
177
140
  payTo: string;
141
+ maxTimeoutSeconds?: number;
142
+ extra?: Record<string, unknown>;
178
143
  }>, context: {
179
144
  request: Request;
180
145
  }): Promise<PaymentRequirements[]>;
@@ -184,32 +149,20 @@ interface X402Server {
184
149
  description?: string;
185
150
  }, error?: string, extensions?: Record<string, unknown>): Promise<PaymentRequired>;
186
151
  findMatchingRequirements(requirements: PaymentRequirements[], payload: unknown): PaymentRequirements;
187
- verifyPayment(payload: unknown, requirements: PaymentRequirements): Promise<{
188
- isValid: boolean;
189
- payer?: string;
190
- }>;
191
- settlePayment(payload: unknown, requirements: PaymentRequirements): Promise<SettleResponse>;
152
+ verifyPayment(payload: unknown, requirements: PaymentRequirements): Promise<VerifyResponse>;
153
+ settlePayment(payload: unknown, requirements: PaymentRequirements, declaredExtensions?: Record<string, unknown>, transportContext?: unknown, settlementOverrides?: {
154
+ amount?: string;
155
+ }): Promise<SettleResponse>;
192
156
  }
193
157
  type ProtocolType = 'x402' | 'mpp';
194
158
  type AuthMode = 'paid' | 'siwx' | 'apiKey' | 'unprotected';
195
159
  type RouteMethod = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH';
196
160
  interface RouteDefinition<K extends string = string> {
197
- /**
198
- * Public API path segment (without `/api/` prefix).
199
- * Example: `flightaware/airports/id/flights/arrivals`
200
- */
161
+ /** Public API path segment without the `/api/` prefix (e.g. `flightaware/airports/id/flights/arrivals`). */
201
162
  path: K;
202
- /**
203
- * Internal route ID for pricing maps / analytics. Defaults to `path`.
204
- *
205
- * In `strictRoutes` mode, custom keys are disallowed to prevent discovery
206
- * drift between internal IDs and advertised paths.
207
- */
163
+ /** Internal route ID for pricing maps / analytics. Defaults to `path`. Disallowed under `strictRoutes` to prevent discovery drift. */
208
164
  key?: string;
209
- /**
210
- * Optional explicit method. If omitted, defaults to builder behavior
211
- * (`POST`, or `GET` when `.query()` is used).
212
- */
165
+ /** Explicit HTTP method. Defaults to `POST`, or `GET` when `.query()` is used. */
213
166
  method?: RouteMethod;
214
167
  }
215
168
  interface TierConfig {
@@ -223,26 +176,31 @@ type PricingConfig<TBody = unknown> = string | ((body: TBody) => string | Promis
223
176
  };
224
177
  type PayToConfig = string | ((request: Request, body?: unknown) => string | Promise<string>);
225
178
  interface X402AcceptBase {
179
+ /** Chain identifier (e.g. `base`, `base-sepolia`, `solana-mainnet`). */
226
180
  network: string;
181
+ /** Token contract address (EVM) or mint (Solana). Defaults to USDC for the network. */
227
182
  asset?: string;
183
+ /** Token decimals. Defaults to USDC's 6. */
228
184
  decimals?: number;
185
+ /** Max payment-proof age the facilitator will accept, in seconds. */
229
186
  maxTimeoutSeconds?: number;
187
+ /** Extra fields passed through to the x402 PaymentRequirements `extra` block. */
230
188
  extra?: Record<string, unknown>;
231
189
  }
232
190
  interface X402AcceptConfig extends X402AcceptBase {
191
+ /** `'exact'` for fixed-price one-shot payments; `'upto'` for settle-≤-cap (required for `.paid({ dynamic: true })` on x402). @default 'exact' */
233
192
  scheme?: string;
193
+ /** Per-accept payee override. Function form receives the request and parsed body for dynamic recipient routing. Falls back to `RouterConfig.payeeAddress`. */
234
194
  payTo?: PayToConfig;
235
195
  }
236
- interface X402ResolvedAccept extends X402AcceptBase {
237
- scheme: string;
238
- payTo: string;
239
- }
240
196
  interface X402RouterFacilitatorConfig extends FacilitatorConfig {
197
+ /** Async header builder invoked per facilitator call. Use for short-lived auth tokens (e.g. CDP signed headers). */
241
198
  createAcceptsHeaders?: () => Promise<Record<string, string>>;
242
199
  }
200
+ /** A facilitator URL or a full `FacilitatorConfig` (URL + auth header builders). */
243
201
  type X402FacilitatorTarget = string | X402RouterFacilitatorConfig;
244
202
  interface X402FacilitatorsConfig {
245
- evm?: X402FacilitatorTarget;
203
+ /** Facilitator for Solana. Defaults to {@link DEFAULT_SOLANA_FACILITATOR_URL}. The EVM facilitator is hardcoded to Coinbase (CDP) — set `CDP_API_KEY_ID` / `CDP_API_KEY_SECRET`. */
246
204
  solana?: X402FacilitatorTarget;
247
205
  }
248
206
  interface MppProtocolInfo {
@@ -258,6 +216,12 @@ interface PaidOptions {
258
216
  payTo?: PayToConfig;
259
217
  /** Override MPP protocol metadata in x-payment-info discovery. */
260
218
  mpp?: MppProtocolInfo;
219
+ /** Handler-driven dynamic pricing: handler calls `charge()` per tick, total billed is `tickCost × calls` capped at `maxPrice`. Requires `maxPrice`. Incompatible with tiered pricing. On x402 needs an `upto` accept; on MPP needs `RouterConfig.mpp.session`. */
220
+ dynamic?: boolean;
221
+ /** Per-tick cost (positive decimal-dollar string). Required for `.paid({ dynamic: true })`. Also the voucher-headroom granularity for MPP sessions. */
222
+ tickCost?: string;
223
+ /** Cosmetic unit label for 402 challenges / UIs (e.g. `'token'`, `'byte'`). Does not affect billing. */
224
+ unitType?: string;
261
225
  }
262
226
  type PaymentStatus = 'verified' | 'settled';
263
227
  interface HandlerPaymentContext {
@@ -293,26 +257,16 @@ interface SettledHandlerErrorContext<TBody = unknown> extends SettlementSettledC
293
257
  error: unknown;
294
258
  }
295
259
  interface SettlementLifecycle<TBody = unknown> {
296
- /**
297
- * Runs after the handler returns a successful response, before router-controlled
298
- * settlement/broadcast. Throw with `.status` to return a specific error and
299
- * skip settlement when the protocol flow has not already settled.
300
- */
260
+ /** Runs after a successful handler response, before router-controlled settlement/broadcast. Throw with `.status` to fail the request and skip settlement (when not already settled). */
301
261
  beforeSettle?: (ctx: SettlementLifecycleContext<TBody>) => void | Promise<void>;
302
- /**
303
- * Runs after successful settlement. Use for durable ledgers and audit rows.
304
- * Errors are alerted and do not change the already-settled response.
305
- */
262
+ /** Runs after successful settlement; for durable ledgers and audit rows. Errors are alerted but don't change the already-settled response. */
306
263
  afterSettle?: (ctx: SettlementSettledContext<TBody>) => void | Promise<void>;
307
- /**
308
- * Runs when the router has already observed a settled payment, then the
309
- * handler returns an error response. Use for app-owned refund or
310
- * compensation queues.
311
- */
264
+ /** Runs when payment was settled but the handler then returned an error response. Use for app-owned refund / compensation queues. */
312
265
  onSettledHandlerError?: (ctx: SettledHandlerErrorContext<TBody>) => void | Promise<void>;
313
266
  /** Runs when router-controlled settlement fails after the handler succeeded. */
314
267
  onSettlementError?: (ctx: SettlementErrorContext<TBody>) => void | Promise<void>;
315
268
  }
269
+ type ChargeFn = () => Promise<void>;
316
270
  interface HandlerContext<TBody = undefined, TQuery = undefined> {
317
271
  body: TBody;
318
272
  query: TQuery;
@@ -325,6 +279,10 @@ interface HandlerContext<TBody = undefined, TQuery = undefined> {
325
279
  alert: AlertFn;
326
280
  setVerifiedWallet: (addr: string) => void;
327
281
  }
282
+ /** Handler context for streaming `.paid({ dynamic: true })` handlers (async generators). Call `charge()` once per billable unit. */
283
+ interface StreamingHandlerContext<TBody = undefined, TQuery = undefined> extends HandlerContext<TBody, TQuery> {
284
+ charge: ChargeFn;
285
+ }
328
286
  type OveragePolicy = 'same-rate' | 'increased-rate' | 'hard-stop';
329
287
  type QuotaLevel = 'healthy' | 'warn' | 'critical';
330
288
  interface QuotaInfo {
@@ -358,28 +316,17 @@ interface RouteEntry {
358
316
  */
359
317
  siwxEnabled?: boolean;
360
318
  pricing?: PricingConfig;
319
+ /** When true the route is dynamic-priced; bills `tickCost` per request (request-mode) or per `charge()` call (streaming). */
320
+ dynamicPrice?: boolean;
321
+ /** True iff handler is an async generator. Streaming handlers settle per-tick over SSE; non-streaming dynamic handlers bill exactly `tickCost` per request. Set by the builder at `.handler(fn)` time. */
322
+ streaming?: boolean;
361
323
  protocols: ProtocolType[];
362
324
  bodySchema?: ZodType;
363
325
  querySchema?: ZodType;
364
326
  outputSchema?: ZodType;
365
- /**
366
- * Optional conforming example for the request input (body for body routes, query params for query routes).
367
- * When present, it must satisfy the corresponding schema and is validated at route registration.
368
- *
369
- * Emitted in the bazaar discovery extension so indexers can advertise a working sample call.
370
- */
327
+ /** Optional conforming example for the request input (body or query). Validated against the schema at registration. Emitted in the bazaar discovery extension. */
371
328
  inputExample?: JsonObject;
372
- /**
373
- * Optional conforming example for the response output. When present, it must
374
- * satisfy `outputSchema` and is validated at route registration.
375
- *
376
- * Accepts any JSON value (object, array, or primitive) to support top-level array or
377
- * primitive response schemas.
378
- *
379
- * Emitted in the bazaar discovery extension. Without it the `output` block is
380
- * dropped from the declaration entirely (the output schema alone cannot be
381
- * exposed in bazaar without an example).
382
- */
329
+ /** Optional conforming example for the response output (any JSON value). Validated against `outputSchema` at registration. Without it, the bazaar `output` block is omitted (schema alone can't be exposed). */
383
330
  outputExample?: JsonValue;
384
331
  description?: string;
385
332
  path?: string;
@@ -393,6 +340,10 @@ interface RouteEntry {
393
340
  validateFn?: (body: unknown) => void | Promise<void>;
394
341
  settlement?: SettlementLifecycle;
395
342
  mppInfo?: MppProtocolInfo;
343
+ /** Per-tick cost (decimal-dollar). Required when `dynamicPrice` is true. */
344
+ tickCost?: string;
345
+ /** Cosmetic unit label for 402 challenges and client UIs. */
346
+ unitType?: string;
396
347
  }
397
348
  interface DiscoveryConfig {
398
349
  title: string;
@@ -410,93 +361,53 @@ interface DiscoveryConfig {
410
361
  serverUrl?: string;
411
362
  }
412
363
  interface RouterConfig {
364
+ /** Default payee for paid routes — populates `payTo` on the auto-generated x402 `exact` accept and acts as the MPP `recipient` fallback. Override per-protocol via `x402.accepts[i].payTo` / `mpp.recipient`, or per-route via `.paid({ payTo })`. */
413
365
  payeeAddress?: string;
414
- /**
415
- * Origin URL (e.g. `https://myapp.com`).
416
- * Used for 402 challenge realm, discovery URLs, OpenAPI servers, and MPP memo indexing.
417
- *
418
- * **Required.** No auto-detection — the realm is load-bearing for payment matching,
419
- * so it must be explicitly set by the consuming app.
420
- */
366
+ /** Origin URL (required). Used as 402 realm, discovery base, OpenAPI server, and MPP memo prefix — must match the public domain or payment matching breaks. */
421
367
  baseUrl: string;
368
+ /** Default chain for the auto-generated x402 `exact` accept (e.g. `base`, `base-sepolia`). Ignored when `x402.accepts` is set. @default 'base' */
422
369
  network?: string;
370
+ /** x402 protocol settings. Omit to default to a single `exact`/USDC accept on `network` paid to `payeeAddress`, verified via the Coinbase default facilitator (requires `CDP_API_KEY_ID`/`CDP_API_KEY_SECRET`). */
423
371
  x402?: {
372
+ /** Explicit accepts list (scheme + network + asset). Overrides the auto-generated default. Add an `upto` accept here to enable `.paid({ dynamic: true })` on x402. */
424
373
  accepts?: X402AcceptConfig[];
374
+ /** Per-chain facilitator overrides (`evm`/`solana`). Defaults to the Coinbase facilitator on EVM; set `solana` to accept Solana payments. */
425
375
  facilitators?: X402FacilitatorsConfig;
426
376
  };
377
+ /** Observability hook receiving request/auth/payment/settlement events. Implement `RouterPlugin` for structured logs/analytics. */
427
378
  plugin?: RouterPlugin;
428
- siwx?: {
429
- nonceStore?: NonceStore;
430
- entitlementStore?: EntitlementStore;
379
+ /** Single KV cache for SIWX nonce, SIWX entitlement, and MPP tx-hash replay (prefixed `siwx:nonce:`, `siwx:ent:`, `mpp:`). Pass `{ url, token }` for an Upstash-compatible REST endpoint (Upstash, Vercel KV), or a custom `KvStore` implementation. Omitted: auto-bootstraps from `KV_REST_API_URL` + `KV_REST_API_TOKEN`; falls back to in-memory when missing (unsafe in serverless). */
380
+ kvStore?: KvStore | {
381
+ url: string;
382
+ token: string;
431
383
  };
384
+ /** Centralized price map keyed by route ID. `.route(key)` auto-applies `.paid(prices[key])` when `key` is listed; per-route `.paid()` still works for keys not in the map. */
432
385
  prices?: Record<string, string>;
386
+ /** MPP (Tempo) payment-channel config. Required when `protocols` includes `'mpp'`. */
433
387
  mpp?: {
388
+ /** HMAC key for signing/verifying MPP challenge nonces. Persist across deploys — rotating invalidates outstanding 402 challenges. Falls back to `MPP_SECRET_KEY`. */
434
389
  secretKey: string;
390
+ /** Tempo currency contract address (0x-prefixed). Use `TEMPO_USDC_ADDRESS` for USDC on Tempo. */
435
391
  currency: string;
392
+ /** MPP payee address (EVM). Overrides `payeeAddress` for MPP only. Required when `payeeAddress` is unset. MUST equal `operatorKey`'s derived address when `session` is enabled. */
436
393
  recipient?: string;
437
- /** Tempo RPC URL for on-chain verification. Falls back to TEMPO_RPC_URL env var. */
394
+ /** Tempo RPC URL for on-chain verification. Falls back to `TEMPO_RPC_URL`. */
438
395
  rpcUrl?: string;
439
- /**
440
- * Private key of the account that sponsors transaction fees.
441
- * When set, clients don't need gas tokensthe server pays fees on their behalf.
442
- * Must be a hex-encoded private key (e.g. `0xabc123...`).
443
- */
396
+ /** Hex private key. Signs channel close/settle; required for `session`. Address MUST equal `recipient`/payee — mppx asserts sender===payee on settle. Validated at init. */
397
+ operatorKey?: string;
398
+ /** Hex private key. Sponsors gas for client channel open/topUp. MUST resolve to a different address than `operatorKey` Tempo rejects sender===feePayer. Validated at init. Omit to make clients pay their own gas. */
444
399
  feePayerKey?: string;
445
- /**
446
- * Persistent store for transaction hash replay protection.
447
- *
448
- * Without this, mppx defaults to `Store.memory()` which is wiped on every cold start —
449
- * unsafe on Vercel or any multi-instance deployment. Pass `Store.upstash(redis)` or
450
- * `Store.cloudflare(kv)` for a shared persistent store.
451
- *
452
- * @example
453
- * import { Store } from 'mppx'
454
- * store: Store.upstash({ get, set, del })
455
- * store: Store.cloudflare(env.MY_KV_NAMESPACE)
456
- */
457
- store?: Store.Store;
458
- /**
459
- * When `true`, auto-configures an Upstash-backed persistent store from Vercel KV
460
- * environment variables (`KV_REST_API_URL` + `KV_REST_API_TOKEN`).
461
- *
462
- * Uses raw `fetch` against the Upstash REST API — no extra npm dependencies.
463
- * Ignored when `store` is explicitly provided.
464
- *
465
- * @example
466
- * createRouter({
467
- * mpp: {
468
- * secretKey: process.env.MPP_SECRET_KEY!,
469
- * currency: TEMPO_USDC_CURRENCY,
470
- * useDefaultStore: true,
471
- * }
472
- * })
473
- */
474
- useDefaultStore?: boolean;
400
+ /** Enables MPP payment-channel sessions for `.paid({ dynamic: true })` routes (registers both request and SSE session middleware). Also requires `mpp.operatorKey`. */
401
+ session?: {
402
+ /** Suggested deposit on the 402 challenge = `tickCost × depositMultiplier` USDC. Route `maxPrice` overrides. @default 10 */
403
+ depositMultiplier?: number;
404
+ };
475
405
  };
476
- /**
477
- * Payment protocols to accept on paid routes unless a route overrides them.
478
- *
479
- * @default ['x402']
480
- *
481
- * @example
482
- * // Accept both x402 and MPP payments
483
- * createRouter({
484
- * protocols: ['x402', 'mpp'],
485
- * mpp: { secretKey, currency: TEMPO_USDC_CURRENCY, recipient },
486
- * prices: { 'exa/search': '0.01' }
487
- * })
488
- */
406
+ /** Payment protocols to accept on paid routes unless overridden per route. @default ['x402'] */
489
407
  protocols?: ProtocolType[];
490
- /**
491
- * Enforce explicit, path-first route definitions.
492
- *
493
- * When enabled:
494
- * - `.route('key')` is rejected; use `.route({ path })`.
495
- * - custom `key` differing from `path` is rejected.
496
- *
497
- * This prevents discovery/openapi drift caused by shorthand internal keys.
498
- */
408
+ /** When true, `.route('key')` is rejected (use `.route({ path })`) and custom `key !== path` is rejected. Prevents discovery/openapi drift. */
499
409
  strictRoutes?: boolean;
410
+ /** Static metadata for auto-generated discovery surfaces — `/.well-known/x402`, OpenAPI (`/api/openapi`), and `/llms.txt`. */
500
411
  discovery: DiscoveryConfig;
501
412
  }
502
413
 
@@ -519,6 +430,14 @@ interface ResolvedX402Facilitator {
519
430
  config: X402RouterFacilitatorConfig;
520
431
  }
521
432
 
433
+ type MppxMiddlewareResponse<T extends Transport.AnyTransport> = {
434
+ status: 402;
435
+ challenge: Transport.ChallengeOutputOf<T>;
436
+ } | {
437
+ status: 200;
438
+ withReceipt: Transport.WithReceipt<T>;
439
+ };
440
+ type MppxMiddleware<TOptions, T extends Transport.AnyTransport> = (options: TOptions) => (input: Request) => Promise<MppxMiddlewareResponse<T>>;
522
441
  interface RouterDeps {
523
442
  x402Server: X402Server | null;
524
443
  initPromise: Promise<void>;
@@ -533,15 +452,22 @@ interface RouterDeps {
533
452
  x402FacilitatorsByNetwork?: Record<string, ResolvedX402Facilitator>;
534
453
  x402Accepts: X402AcceptConfig[];
535
454
  mppx?: {
536
- charge: (options: {
455
+ charge: MppxMiddleware<{
537
456
  amount: string;
538
- }) => (input: Request) => Promise<{
539
- status: 402;
540
- challenge: Response;
541
- } | {
542
- status: 200;
543
- withReceipt: (response: Response) => Response;
544
- }>;
457
+ }, Transport.Http>;
458
+ sessionRequest?: MppxMiddleware<{
459
+ amount: string;
460
+ unitType?: string;
461
+ suggestedDeposit?: string;
462
+ }, Transport.Http>;
463
+ sessionStream?: MppxMiddleware<{
464
+ amount: string;
465
+ unitType?: string;
466
+ suggestedDeposit?: string;
467
+ }, Transport.Sse>;
468
+ } | null;
469
+ mppSessionConfig?: {
470
+ depositMultiplier: number;
545
471
  } | null;
546
472
  tempoClient?: viem.Client | null;
547
473
  }
@@ -551,29 +477,22 @@ type OrchestrateDeps = RouterDeps;
551
477
 
552
478
  type True = true;
553
479
  type False = false;
554
- /**
555
- * Active request-input type at a builder position. Resolves to `TBody` when
556
- * `.body()` has been called, `TQuery` when `.query()` has been called, and
557
- * `never` when neither — making `.inputExample()` unusable before a schema
558
- * is set (the literal won't assign to `never`).
559
- */
560
480
  type InputTypeFor<TBody, TQuery> = [TBody] extends [undefined] ? [TQuery] extends [undefined] ? never : TQuery : TBody;
561
- /**
562
- * The handler argument type. Narrows to the real handler signature when the
563
- * builder state is valid, and to a descriptive error object when it isn't —
564
- * the mismatch surfaces as a TS type error at the `.handler(...)` call site
565
- * with the `__missing` string as the contextual hint.
566
- *
567
- * Encoded as a conditional argument rather than overload `this:` constraints
568
- * because TypeScript doesn't reliably gate overload selection on `this` for
569
- * generic classes (structurally identical instance types collapse).
570
- */
481
+ type RequestHandlerFn<TBody, TQuery> = (ctx: HandlerContext<TBody, TQuery>) => Promise<unknown>;
482
+ type StreamingHandlerFn<TBody, TQuery> = (ctx: StreamingHandlerContext<TBody, TQuery>) => AsyncIterable<unknown>;
571
483
  type HandlerArg<TBody, TQuery, HasAuth extends boolean, NeedsBody extends boolean, HasBody extends boolean> = HasAuth extends true ? [NeedsBody, HasBody] extends [true, false] ? {
572
484
  __missing: 'Call .body(schema) — dynamic/tiered pricing requires a body schema to resolve the price against';
573
- } : (ctx: HandlerContext<TBody, TQuery>) => Promise<unknown> : {
485
+ } : RequestHandlerFn<TBody, TQuery> : {
574
486
  __missing: 'Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()';
575
487
  };
576
- declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = undefined, HasAuth extends boolean = false, NeedsBody extends boolean = false, HasBody extends boolean = false> {
488
+ type StreamArg<TBody, TQuery, HasAuth extends boolean, NeedsBody extends boolean, HasBody extends boolean, IsDynamic extends boolean> = HasAuth extends true ? IsDynamic extends true ? [NeedsBody, HasBody] extends [true, false] ? {
489
+ __missing: 'Call .body(schema) — dynamic pricing requires a body schema to resolve the price against';
490
+ } : StreamingHandlerFn<TBody, TQuery> : {
491
+ __missing: 'Streaming handlers require .paid({ dynamic: true, tickCost, unitType, maxPrice }) — static/free routes cannot meter per-chunk billing';
492
+ } : {
493
+ __missing: 'Select an auth mode: .paid({ dynamic: true, ... }) — streaming requires handler-driven dynamic pricing';
494
+ };
495
+ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = undefined, HasAuth extends boolean = false, NeedsBody extends boolean = false, HasBody extends boolean = false, IsDynamic extends boolean = false> {
577
496
  /** @internal */ readonly _key: string;
578
497
  /** @internal */ readonly _registry: RouteRegistry;
579
498
  /** @internal */ readonly _deps: OrchestrateDeps;
@@ -583,6 +502,9 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
583
502
  /** @internal */ _protocols: ProtocolType[];
584
503
  /** @internal */ _maxPrice: string | undefined;
585
504
  /** @internal */ _minPrice: string | undefined;
505
+ /** @internal */ _dynamicPrice: boolean;
506
+ /** @internal */ _tickCost: string | undefined;
507
+ /** @internal */ _unitType: string | undefined;
586
508
  /** @internal */ _payTo: PayToConfig | undefined;
587
509
  /** @internal */ _bodySchema: ZodType | undefined;
588
510
  /** @internal */ _querySchema: ZodType | undefined;
@@ -602,10 +524,63 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
602
524
  /** @internal */ _mppInfo: MppProtocolInfo | undefined;
603
525
  constructor(key: string, registry: RouteRegistry, deps: OrchestrateDeps);
604
526
  private fork;
605
- paid(pricing: string, options?: PaidOptions): RouteBuilder<TBody, TQuery, TOutput, True, False, HasBody>;
527
+ /**
528
+ * Charge a fixed price per request, denominated in USDC as a decimal string.
529
+ *
530
+ * @example
531
+ * ```ts
532
+ * router.route('search').paid('0.01').handler(handler);
533
+ * ```
534
+ */
535
+ paid(pricing: string, options?: PaidOptions): RouteBuilder<TBody, TQuery, TOutput, True, False, HasBody, IsDynamic>;
536
+ /**
537
+ * Configure handler-driven dynamic pricing — each tick costs `tickCost` USDC,
538
+ * capped at `maxPrice`. Pair with `.handler()` for one-tick-per-request
539
+ * billing, or with `.stream()` for per-yield metering.
540
+ *
541
+ * @example
542
+ * ```ts
543
+ * router
544
+ * .route('llm/stream')
545
+ * .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
546
+ * .stream(async function* ({ charge }) { await charge(); yield 'hi'; });
547
+ * ```
548
+ */
549
+ paid(options: PaidOptions & {
550
+ dynamic: true;
551
+ maxPrice: string;
552
+ }): RouteBuilder<TBody, TQuery, TOutput, True, False, HasBody, True>;
553
+ /**
554
+ * Compute the price from the parsed body before issuing the 402 challenge.
555
+ * Throw an `HttpError` from the pricing function to reject the request before
556
+ * payment is requested.
557
+ *
558
+ * @example
559
+ * ```ts
560
+ * router
561
+ * .route('llm')
562
+ * .paid((body) => `${body.tokens * 0.0001}`, { maxPrice: '5.00' })
563
+ * .body(schema)
564
+ * .handler(handler);
565
+ * ```
566
+ */
606
567
  paid<TBodyIn>(pricing: (body: TBodyIn) => string | Promise<string>, options?: PaidOptions & {
607
568
  maxPrice?: string;
608
- }): RouteBuilder<TBody, TQuery, TOutput, True, True, HasBody>;
569
+ }): RouteBuilder<TBody, TQuery, TOutput, True, True, HasBody, IsDynamic>;
570
+ /**
571
+ * Select a price tier from `body[field]`, optionally falling back to the
572
+ * `default` tier when the value is missing. The 402 challenge advertises the
573
+ * highest tier price.
574
+ *
575
+ * @example
576
+ * ```ts
577
+ * router
578
+ * .route('upload')
579
+ * .paid({ field: 'size', tiers: { sm: { price: '0.01' }, lg: { price: '0.10' } } })
580
+ * .body(schema)
581
+ * .handler(handler);
582
+ * ```
583
+ */
609
584
  paid(pricing: {
610
585
  field: string;
611
586
  tiers: Record<string, {
@@ -613,149 +588,287 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
613
588
  label?: string;
614
589
  }>;
615
590
  default?: string;
616
- }, options?: PaidOptions): RouteBuilder<TBody, TQuery, TOutput, True, True, HasBody>;
617
- siwx(): RouteBuilder<TBody, TQuery, TOutput, True, False, HasBody>;
618
- apiKey(resolver: (key: string) => unknown | Promise<unknown>): RouteBuilder<TBody, TQuery, TOutput, True, NeedsBody, HasBody>;
619
- unprotected(): RouteBuilder<TBody, TQuery, TOutput, True, False, HasBody>;
620
- provider(name: string, config?: ProviderConfig): this;
621
- body<T>(schema: ZodType<T>): RouteBuilder<T, TQuery, TOutput, HasAuth, NeedsBody, True>;
622
- body<T>(schema: ZodType<T>, example: T & JsonObject): RouteBuilder<T, TQuery, TOutput, HasAuth, NeedsBody, True>;
623
- query<T>(schema: ZodType<T>): RouteBuilder<TBody, T, TOutput, HasAuth, NeedsBody, HasBody>;
624
- query<T>(schema: ZodType<T>, example: T & JsonObject): RouteBuilder<TBody, T, TOutput, HasAuth, NeedsBody, HasBody>;
625
- output<T>(schema: ZodType<T>): RouteBuilder<TBody, TQuery, T, HasAuth, NeedsBody, HasBody>;
626
- output<T>(schema: ZodType<T>, example: T & JsonValue): RouteBuilder<TBody, TQuery, T, HasAuth, NeedsBody, HasBody>;
591
+ }, options?: PaidOptions): RouteBuilder<TBody, TQuery, TOutput, True, True, HasBody, IsDynamic>;
627
592
  /**
628
- * Provide a conforming example of the request input (body or query params).
593
+ * Require Sign-In-with-X wallet identity on this route clients prove
594
+ * control of a wallet via a signed challenge. Combine with `.paid()` to gate
595
+ * a paid route on a verified wallet identity.
629
596
  *
630
- * Optional. When provided, the example is validated against the request schema
631
- * at route registration and embedded in the bazaar discovery extension so
632
- * indexers can advertise a working sample call.
597
+ * @example
598
+ * ```ts
599
+ * router.route('profile').siwx().handler(async ({ wallet }) => getProfile(wallet));
600
+ * ```
601
+ */
602
+ siwx(): RouteBuilder<TBody, TQuery, TOutput, True, False, HasBody, IsDynamic>;
603
+ /**
604
+ * Require an `X-API-Key` header (or `Authorization: Bearer <key>`); the
605
+ * resolver returns the account record, or `null` for 401. Composes with
606
+ * `.paid()` — key is checked first, payment second.
633
607
  *
634
- * For the common case, pass the example directly to `.body(schema, example)` or
635
- * `.query(schema, example)` instead.
608
+ * @example
609
+ * ```ts
610
+ * router
611
+ * .route('admin/users')
612
+ * .apiKey(async (key) => db.admin.findByKey(key))
613
+ * .handler(async ({ account }) => db.user.list(account.orgId));
614
+ * ```
615
+ */
616
+ apiKey(resolver: (key: string) => unknown | Promise<unknown>): RouteBuilder<TBody, TQuery, TOutput, True, NeedsBody, HasBody, IsDynamic>;
617
+ /**
618
+ * Mark the route as public — no auth, no payment, no SIWX. The handler
619
+ * receives `null` for `wallet`, `payment`, and `account`.
636
620
  *
637
621
  * @example
638
622
  * ```ts
639
- * router.route('search')
623
+ * router.route('health').unprotected().handler(async () => ({ status: 'ok' }));
624
+ * ```
625
+ */
626
+ unprotected(): RouteBuilder<TBody, TQuery, TOutput, True, False, HasBody, IsDynamic>;
627
+ /**
628
+ * Tag the route with an upstream provider for discovery and provider-side
629
+ * monitoring. The provider name and config surface in `well-known` and
630
+ * OpenAPI output.
631
+ *
632
+ * @example
633
+ * ```ts
634
+ * router
635
+ * .route('search')
640
636
  * .paid('0.01')
641
- * .body(z.object({ q: z.string() }))
642
- * .inputExample({ q: 'hello world' })
643
- * .handler(async ({ body }) => { ... });
637
+ * .provider('exa', { quotaPerMonth: 1000 })
638
+ * .handler(handler);
644
639
  * ```
645
640
  */
646
- inputExample(example: InputTypeFor<TBody, TQuery> & JsonObject): RouteBuilder<TBody, TQuery, TOutput, HasAuth, NeedsBody, HasBody>;
641
+ provider(name: string, config?: ProviderConfig): this;
647
642
  /**
648
- * Provide a conforming example of the response output.
643
+ * Declare the request body's Zod schema. Parsed body is typed as `ctx.body`
644
+ * in the handler. Use `.inputExample()` to attach a discovery example.
649
645
  *
650
- * Optional. When provided, the example is validated against the output schema
651
- * at route registration and embedded in the bazaar discovery extension so
652
- * indexers can advertise the response shape.
646
+ * @example
647
+ * ```ts
648
+ * .body(z.object({ query: z.string() }))
649
+ * .handler(async ({ body }) => search(body.query));
650
+ * ```
651
+ */
652
+ body<T>(schema: ZodType<T>): RouteBuilder<T, TQuery, TOutput, HasAuth, NeedsBody, True, IsDynamic>;
653
+ /**
654
+ * Declare a query-string Zod schema and switch the route to `GET`. Parsed
655
+ * query is typed as `ctx.query` in the handler. Use `.inputExample()` to
656
+ * attach a discovery example.
653
657
  *
654
- * For the common case, pass the example directly to `.output(schema, example)` instead.
658
+ * @example
659
+ * ```ts
660
+ * .query(z.object({ id: z.string() }))
661
+ * .handler(async ({ query }) => getById(query.id));
662
+ * ```
663
+ */
664
+ query<T>(schema: ZodType<T>): RouteBuilder<TBody, T, TOutput, HasAuth, NeedsBody, HasBody, IsDynamic>;
665
+ /**
666
+ * Declare the response output's Zod schema for OpenAPI generation. The
667
+ * runtime does not validate handler return values — use Zod's `.parse()`
668
+ * inside the handler if strict output validation is required. Use
669
+ * `.outputExample()` to attach a discovery example.
655
670
  *
656
- * Accepts any JSON value (objects, arrays, or primitives) — top-level array
657
- * or primitive responses (e.g. `z.array(...)`) are supported alongside the
658
- * common object case.
671
+ * @example
672
+ * ```ts
673
+ * .output(z.object({ result: z.string() }))
674
+ * .handler(async () => ({ result: 'ok' }));
675
+ * ```
676
+ */
677
+ output<T>(schema: ZodType<T>): RouteBuilder<TBody, TQuery, T, HasAuth, NeedsBody, HasBody, IsDynamic>;
678
+ /**
679
+ * Attach an example of the request body or query for discovery output,
680
+ * validated against the registered schema at registration.
659
681
  *
660
682
  * @example
661
683
  * ```ts
662
- * router.route('search')
663
- * .paid('0.01')
664
- * .output(z.object({ results: z.array(z.string()) }))
665
- * .outputExample({ results: ['a', 'b'] })
666
- * .handler(async () => { ... });
684
+ * .body(searchSchema).inputExample({ query: 'cats' });
685
+ * ```
686
+ */
687
+ inputExample(example: InputTypeFor<TBody, TQuery> & JsonObject): RouteBuilder<TBody, TQuery, TOutput, HasAuth, NeedsBody, HasBody, IsDynamic>;
688
+ /**
689
+ * Attach an example response for discovery output, validated against the
690
+ * registered output schema at registration.
667
691
  *
668
- * // Top-level array response
669
- * router.route('chains')
670
- * .paid('0.01')
671
- * .output(z.array(z.object({ name: z.string() })))
672
- * .outputExample([{ name: 'Ethereum' }])
673
- * .handler(async () => { ... });
692
+ * @example
693
+ * ```ts
694
+ * .output(resultSchema).outputExample({ result: 'ok' });
695
+ * ```
696
+ */
697
+ outputExample(example: TOutput & JsonValue): RouteBuilder<TBody, TQuery, TOutput, HasAuth, NeedsBody, HasBody, IsDynamic>;
698
+ /**
699
+ * Set a human-readable summary of the route. Surfaces in OpenAPI,
700
+ * `well-known`, and `llms.txt` discovery output.
701
+ *
702
+ * @example
703
+ * ```ts
704
+ * .description('Search indexed web pages by full-text query');
674
705
  * ```
675
706
  */
676
- outputExample(example: TOutput & JsonValue): RouteBuilder<TBody, TQuery, TOutput, HasAuth, NeedsBody, HasBody>;
677
707
  description(text: string): this;
708
+ /**
709
+ * Override the URL path advertised in discovery output. Defaults to the
710
+ * registry key passed to `.route()`.
711
+ *
712
+ * @example
713
+ * ```ts
714
+ * router.route('search').path('/v2/search').handler(handler);
715
+ * ```
716
+ */
678
717
  path(p: string): this;
718
+ /**
719
+ * Override the HTTP method advertised in discovery. Defaults to `POST`, or
720
+ * `GET` when `.query()` has been called.
721
+ *
722
+ * @example
723
+ * ```ts
724
+ * router.route('items/delete').method('DELETE').handler(handler);
725
+ * ```
726
+ */
679
727
  method(m: 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH'): this;
680
728
  /**
681
- * Add pre-payment validation that runs after body parsing but before the 402
682
- * challenge is shown. Use this for async business logic like "is this resource
683
- * available?" or "has this user hit their rate limit?".
729
+ * Run validation against the parsed body before the 402 challenge. Throw
730
+ * `Object.assign(new Error('...'), { status })` to reject with a custom
731
+ * status code; defaults to 400. Requires `.body()` to be called first.
684
732
  *
685
- * Requires `.body()` — call `.body()` before `.validate()` for type inference.
733
+ * @example
734
+ * ```ts
735
+ * .body(RegisterSchema).validate(async (body) => {
736
+ * if (await isTaken(body.name)) {
737
+ * throw Object.assign(new Error('taken'), { status: 409 });
738
+ * }
739
+ * });
740
+ * ```
741
+ */
742
+ validate(fn: (body: TBody) => void | Promise<void>): RouteBuilder<TBody, TQuery, TOutput, HasAuth, NeedsBody, HasBody, IsDynamic>;
743
+ /**
744
+ * Hook into the settlement lifecycle. `beforeSettle` runs after the handler
745
+ * succeeds but before on-chain settlement and can cancel the charge;
746
+ * `afterSettle` runs after settlement completes (success or failure).
686
747
  *
687
748
  * @example
688
- * ```typescript
689
- * router
690
- * .route('domain/register')
691
- * .paid(calculatePrice)
692
- * .body(RegisterSchema) // .body() first for type inference
693
- * .validate(async (body) => {
694
- * if (await isDomainTaken(body.domain)) {
695
- * throw Object.assign(new Error('Domain taken'), { status: 409 });
696
- * }
697
- * })
698
- * .handler(async ({ body }) => { ... });
749
+ * ```ts
750
+ * .settlement({
751
+ * beforeSettle: ({ result }) => (result.refund ? 'skip' : 'continue'),
752
+ * afterSettle: ({ tx }) => analytics.track('settled', { tx }),
753
+ * });
699
754
  * ```
700
755
  */
701
- validate(fn: (body: TBody) => void | Promise<void>): RouteBuilder<TBody, TQuery, TOutput, HasAuth, NeedsBody, HasBody>;
756
+ settlement(lifecycle: SettlementLifecycle<TBody>): RouteBuilder<TBody, TQuery, TOutput, HasAuth, NeedsBody, HasBody, IsDynamic>;
702
757
  /**
703
- * Add route-specific settlement hooks.
758
+ * Register the request handler and return the Next.js route function. The
759
+ * handler receives a typed context and may return a value (serialized to
760
+ * JSON), a raw `Response`, or throw an `HttpError` for a non-2xx status.
704
761
  *
705
- * `beforeSettle` runs after a successful handler response but before
706
- * router-controlled settlement/broadcast, so it can still prevent the charge
707
- * for x402 and MPP transaction-payload flows. `afterSettle` runs after
708
- * settlement and is intended for durable ledgers or app-owned refund queues.
762
+ * @example
763
+ * ```ts
764
+ * export const POST = router
765
+ * .route('search')
766
+ * .paid('0.01')
767
+ * .body(schema)
768
+ * .handler(async ({ body, wallet }) => searchService(body, wallet));
769
+ * ```
709
770
  */
710
- settlement(lifecycle: SettlementLifecycle<TBody>): RouteBuilder<TBody, TQuery, TOutput, HasAuth, NeedsBody, HasBody>;
711
771
  handler(fn: HandlerArg<TBody, TQuery, HasAuth, NeedsBody, HasBody>): (request: NextRequest) => Promise<Response>;
772
+ /**
773
+ * Register a streaming handler (`async function*`) and return the Next.js
774
+ * route function. Each `charge()` call bills one tick (`tickCost` USDC) up
775
+ * to `maxPrice`; requires `.paid({ dynamic: true, ... })` and MPP session mode.
776
+ *
777
+ * @example
778
+ * ```ts
779
+ * export const POST = router
780
+ * .route('llm/stream')
781
+ * .paid({ dynamic: true, tickCost: '0.0001', unitType: 'token', maxPrice: '0.05' })
782
+ * .body(schema)
783
+ * .stream(async function* ({ body, charge }) {
784
+ * for await (const token of streamLLM(body.prompt)) {
785
+ * await charge();
786
+ * yield token;
787
+ * }
788
+ * });
789
+ * ```
790
+ */
791
+ stream(fn: StreamArg<TBody, TQuery, HasAuth, NeedsBody, HasBody, IsDynamic>): (request: NextRequest) => Promise<Response>;
792
+ private register;
712
793
  }
713
794
 
714
- declare const BASE_NETWORK = "eip155:8453";
715
- declare const SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
716
- declare const TEMPO_USDC_CURRENCY = "0x20c000000000000000000000b9537d11c60e8b50";
717
- declare const ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
718
-
719
- type RouterEnv = Record<string, string | undefined>;
720
- type RouterConfigIssueCode = 'missing_base_url' | 'empty_protocols' | 'missing_x402_accepts' | 'missing_x402_network' | 'unsupported_x402_network' | 'missing_x402_asset' | 'invalid_x402_decimals' | 'missing_x402_payee' | 'missing_cdp_keys' | 'placeholder_payee' | 'missing_mpp_config' | 'missing_mpp_secret_key' | 'missing_mpp_currency' | 'invalid_mpp_currency' | 'missing_mpp_recipient' | 'invalid_mpp_recipient' | 'missing_mpp_rpc_url' | 'invalid_mpp_fee_payer_key' | 'missing_mpp_default_store_env';
795
+ type RouterConfigIssueCode = 'missing_base_url' | 'invalid_base_url' | 'empty_protocols' | 'missing_x402_accepts' | 'missing_x402_network' | 'unsupported_x402_network' | 'missing_x402_asset' | 'invalid_x402_decimals' | 'missing_x402_payee' | 'invalid_x402_payee' | 'invalid_solana_payee' | 'invalid_solana_facilitator_url' | 'missing_cdp_keys' | 'placeholder_payee' | 'missing_mpp_config' | 'missing_mpp_secret_key' | 'missing_mpp_currency' | 'invalid_mpp_currency' | 'missing_mpp_recipient' | 'invalid_mpp_recipient' | 'missing_mpp_rpc_url' | 'invalid_mpp_rpc_url' | 'invalid_mpp_fee_payer_key' | 'invalid_mpp_operator_key' | 'mpp_operator_equals_fee_payer' | 'missing_discovery_title' | 'missing_discovery_description' | 'missing_discovery_guidance' | 'invalid_server_url' | 'kv_url_without_token' | 'kv_token_without_url' | 'invalid_kv_url' | 'missing_kv_in_production';
796
+ type RouterConfigIssueSeverity = 'error' | 'warning';
721
797
  interface RouterConfigIssue {
722
798
  code: RouterConfigIssueCode;
723
799
  message: string;
724
800
  protocol?: ProtocolType;
801
+ /** @default 'error' — warnings are surfaced via `console.warn` and do not throw. */
802
+ severity?: RouterConfigIssueSeverity;
803
+ }
804
+ /** Options for {@link createRouterFromEnv} / {@link routerConfigFromEnv}. */
805
+ interface CreateRouterFromEnvOptions<TPrices extends Record<string, string> = Record<never, string>> {
806
+ /** Defaults to `process.env`. Pass an explicit object in tests. */
807
+ env?: Record<string, string | undefined>;
808
+ /** Discovery title. Shown in `.well-known/agentcash`, OpenAPI, and `/llms.txt`. */
809
+ title: string;
810
+ /** Discovery description. */
811
+ description: string;
812
+ /** Long-form usage guidance for agent consumers. Served at `/llms.txt`. Pass an empty string to opt out. */
813
+ guidance: string;
814
+ /** Discovery version. @default '1.0.0' */
815
+ version?: string;
816
+ /** Optional contact metadata published in discovery. */
817
+ contact?: DiscoveryConfig['contact'];
818
+ /** Optional ownership proofs published in `.well-known/agentcash`. */
819
+ ownershipProofs?: string[];
820
+ /** Per-route HTTP method hint visibility. */
821
+ methodHints?: DiscoveryConfig['methodHints'];
822
+ /** Override the OpenAPI `servers[].url`. Defaults to `BASE_URL`. */
823
+ serverUrl?: string;
824
+ /** Centralized price map keyed by route id. `route(key)` auto-applies `.paid(prices[key])` for matching keys. */
825
+ prices?: TPrices;
826
+ /** Observability plugin. */
827
+ plugin?: RouterPlugin;
828
+ /** Custom KV store. When omitted, the router auto-bootstraps from `KV_REST_API_URL` + `KV_REST_API_TOKEN`. */
829
+ kvStore?: KvStore;
830
+ /** Override x402 facilitators. The Solana facilitator defaults to `SOLANA_FACILITATOR_URL` env or `DEFAULT_SOLANA_FACILITATOR_URL`. */
831
+ x402Facilitators?: X402FacilitatorsConfig;
832
+ /** Explicit protocol list. Default: `['x402']`, with `'mpp'` added when `MPP_SECRET_KEY` is set. */
833
+ protocols?: readonly ProtocolType[];
834
+ /** Require `route({ path })` form for every route. @default false */
835
+ strictRoutes?: boolean;
725
836
  }
726
- interface RouterConfigValidationOptions {
727
- env?: RouterEnv;
728
- requireCdpKeys?: boolean;
729
- }
837
+
730
838
  declare class RouterConfigError extends Error {
731
839
  readonly issues: RouterConfigIssue[];
732
840
  constructor(issues: RouterConfigIssue[]);
733
841
  }
734
- declare function validateRouterConfig(config: RouterConfig, options?: RouterConfigValidationOptions): void;
735
- declare function getRouterConfigIssues(config: RouterConfig, options?: RouterConfigValidationOptions): RouterConfigIssue[];
736
- declare function formatRouterConfigIssues(issues: readonly RouterConfigIssue[]): string;
737
- declare function mppFromEnv(env: RouterEnv, options?: {
738
- recipient?: string;
739
- require?: boolean;
740
- useDefaultStore?: boolean;
741
- feePayerKey?: string;
742
- }): RouterConfig['mpp'] | undefined;
743
- declare function x402AcceptsFromEnv(env: RouterEnv, options?: {
744
- payeeAddress?: string;
745
- payeeEnv?: string;
746
- network?: string;
747
- solanaPayeeAddress?: string;
748
- solanaPayeeEnv?: string;
749
- }): X402AcceptConfig[];
750
- declare function paidOptionsForProtocols(protocols: readonly ProtocolType[]): PaidOptions;
751
842
 
752
843
  /**
753
- * SIWX verification error codes.
754
- * Enables clients to auto-retry transient failures (e.g., expired challenge).
844
+ * Build a {@link RouterConfig} from environment variables.
845
+ *
846
+ * `envShape` in this file is the single source of truth for env vars; README
847
+ * and `.env.example` are drift-tested against `ENV_KEYS`. Validates every
848
+ * required value up front and throws a single {@link RouterConfigError}
849
+ * containing all issues at once. Soft warnings (e.g. half-configured KV) are
850
+ * emitted via `console.warn`.
755
851
  */
756
- type SiwxErrorCode = 'siwx_missing_header' | 'siwx_malformed' | 'siwx_expired' | 'siwx_nonce_used' | 'siwx_invalid_signature';
757
- /** Human-readable error messages for each SIWX error code. */
758
- declare const SIWX_ERROR_MESSAGES: Record<SiwxErrorCode, string>;
852
+ declare function routerConfigFromEnv<const TPrices extends Record<string, string> = Record<never, string>>(options: CreateRouterFromEnvOptions<TPrices>): RouterConfig & {
853
+ prices?: TPrices;
854
+ };
855
+
856
+ /** Base mainnet CAIP-2 network ID (`eip155:8453`). */
857
+ declare const BASE_MAINNET_NETWORK = "eip155:8453";
858
+ /** Solana mainnet CAIP-2 network ID. */
859
+ declare const SOLANA_MAINNET_NETWORK = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
860
+ /** USDC contract address on Tempo (the `mpp.currency` value for Tempo USDC). */
861
+ declare const TEMPO_USDC_ADDRESS = "0x20c000000000000000000000b9537d11c60e8b50";
862
+ /** USDC has 6 decimals on Tempo. */
863
+ declare const TEMPO_USDC_DECIMALS = 6;
864
+ /** USDC contract on Base mainnet. */
865
+ declare const BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
866
+ /** USDC has 6 decimals on Base. */
867
+ declare const BASE_USDC_DECIMALS = 6;
868
+ /** All-zeros EVM address. Used as a placeholder/sentinel; not a valid payee. */
869
+ declare const ZERO_EVM_ADDRESS = "0x0000000000000000000000000000000000000000";
870
+ /** Public Solana x402 facilitator. Override per-deployment via `SOLANA_FACILITATOR_URL`. */
871
+ declare const DEFAULT_SOLANA_FACILITATOR_URL = "https://facilitator.corbits.dev";
759
872
 
760
873
  interface MonitorEntry {
761
874
  provider: string;
@@ -776,5 +889,26 @@ interface ServiceRouter<TPriceKeys extends string = never> {
776
889
  declare function createRouter<const P extends Record<string, string> = Record<never, string>>(config: RouterConfig & {
777
890
  prices?: P;
778
891
  }): ServiceRouter<Extract<keyof P, string>>;
892
+ /**
893
+ * Build a {@link ServiceRouter} from environment variables.
894
+ *
895
+ * Validates every required env var up front and throws a single
896
+ * {@link RouterConfigError} containing all problems at once. Most consumers
897
+ * should use this entry point. Use {@link createRouter} when you need to
898
+ * construct a {@link RouterConfig} programmatically.
899
+ *
900
+ * The env vars this function reads are the canonical schema in
901
+ * `src/config/schema.ts` (`ENV_SPEC`).
902
+ *
903
+ * @example
904
+ * ```ts
905
+ * export const router = createRouterFromEnv({
906
+ * title: 'My API',
907
+ * description: 'Pay-per-call search.',
908
+ * guidance: 'POST /search with { q: string }. Returns top 10 results.',
909
+ * });
910
+ * ```
911
+ */
912
+ declare function createRouterFromEnv<const P extends Record<string, string> = Record<never, string>>(options: CreateRouterFromEnvOptions<P>): ServiceRouter<Extract<keyof P, string>>;
779
913
 
780
- export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, BASE_NETWORK, type DiscoveryConfig, type EntitlementStore, type ErrorEvent, type HandlerContext, type HandlerPaymentContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type MppProtocolInfo, type NonceStore, type OveragePolicy, type PaidOptions, type PayToConfig, type PaymentEvent, type PaymentStatus, type PluginContext, type PricingConfig, type ProtocolType, type ProviderConfig, type ProviderQuotaEvent, type QuotaInfo, type QuotaLevel, type RedisEntitlementStoreOptions, type RedisNonceStoreOptions, type RequestMeta, type ResponseMeta, RouteBuilder, type RouteEntry, RouteRegistry, type RouterConfig, RouterConfigError, type RouterConfigIssue, type RouterConfigIssueCode, type RouterConfigValidationOptions, type RouterEnv, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, SIWX_ERROR_MESSAGES, SOLANA_MAINNET_NETWORK, type ServiceRouter, type SettledHandlerErrorContext, type SettlementErrorContext, type SettlementEvent, type SettlementLifecycle, type SettlementLifecycleContext, type SettlementSettledContext, type SiwxErrorCode, TEMPO_USDC_CURRENCY, type TierConfig, type X402AcceptConfig, type X402FacilitatorTarget, type X402FacilitatorsConfig, type X402ResolvedAccept, type X402RouterFacilitatorConfig, type X402Server, ZERO_EVM_ADDRESS, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter, formatRouterConfigIssues, getRouterConfigIssues, mppFromEnv, paidOptionsForProtocols, validateRouterConfig, x402AcceptsFromEnv };
914
+ export { BASE_MAINNET_NETWORK, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, type CreateRouterFromEnvOptions, DEFAULT_SOLANA_FACILITATOR_URL, type DiscoveryConfig, type HandlerContext, HttpError, type KvStore, type PaidOptions, type ProtocolType, type RouterConfig, RouterConfigError, type RouterConfigIssue, type RouterConfigIssueCode, type RouterConfigIssueSeverity, type RouterPlugin, SOLANA_MAINNET_NETWORK, type ServiceRouter, type SettlementErrorContext, type SettlementLifecycleContext, type SettlementSettledContext, TEMPO_USDC_ADDRESS, TEMPO_USDC_DECIMALS, type X402FacilitatorsConfig, ZERO_EVM_ADDRESS, createRouter, createRouterFromEnv, routerConfigFromEnv };