@agentcash/router 1.5.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +85 -0
- package/README.md +138 -526
- package/dist/index.cjs +2380 -1244
- package/dist/index.d.cts +451 -317
- package/dist/index.d.ts +451 -317
- package/dist/index.js +2372 -1227
- package/package.json +16 -24
- package/.claude/CLAUDE.md +0 -229
- package/.claude/skills/router-guide/SKILL.md +0 -585
package/dist/index.d.ts
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 {
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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
|
|
394
|
+
/** Tempo RPC URL for on-chain verification. Falls back to `TEMPO_RPC_URL`. */
|
|
438
395
|
rpcUrl?: string;
|
|
439
|
-
/**
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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:
|
|
455
|
+
charge: MppxMiddleware<{
|
|
537
456
|
amount: string;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
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
|
-
} :
|
|
485
|
+
} : RequestHandlerFn<TBody, TQuery> : {
|
|
574
486
|
__missing: 'Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()';
|
|
575
487
|
};
|
|
576
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
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
|
-
*
|
|
635
|
-
*
|
|
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('
|
|
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
|
-
* .
|
|
642
|
-
* .
|
|
643
|
-
* .handler(async ({ body }) => { ... });
|
|
637
|
+
* .provider('exa', { quotaPerMonth: 1000 })
|
|
638
|
+
* .handler(handler);
|
|
644
639
|
* ```
|
|
645
640
|
*/
|
|
646
|
-
|
|
641
|
+
provider(name: string, config?: ProviderConfig): this;
|
|
647
642
|
/**
|
|
648
|
-
*
|
|
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
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
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
|
-
*
|
|
663
|
-
*
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
671
|
-
*
|
|
672
|
-
|
|
673
|
-
|
|
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
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* ```
|
|
689
|
-
*
|
|
690
|
-
* .
|
|
691
|
-
* .
|
|
692
|
-
*
|
|
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
|
-
|
|
756
|
+
settlement(lifecycle: SettlementLifecycle<TBody>): RouteBuilder<TBody, TQuery, TOutput, HasAuth, NeedsBody, HasBody, IsDynamic>;
|
|
702
757
|
/**
|
|
703
|
-
*
|
|
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
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
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
|
-
|
|
715
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
754
|
-
*
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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 {
|
|
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 };
|