@agentcash/router 0.7.0 → 1.0.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/README.md CHANGED
@@ -58,6 +58,8 @@ import { createRouter } from '@agentcash/router';
58
58
 
59
59
  export const router = createRouter({
60
60
  payeeAddress: process.env.X402_PAYEE_ADDRESS!,
61
+ baseUrl: process.env.NEXT_PUBLIC_BASE_URL!,
62
+ strictRoutes: true, // recommended
61
63
  });
62
64
  ```
63
65
 
@@ -70,7 +72,7 @@ export const router = createRouter({
70
72
  import { router } from '@/lib/routes';
71
73
  import { searchSchema, searchResponseSchema } from '@/lib/schemas';
72
74
 
73
- export const POST = router.route('search')
75
+ export const POST = router.route({ path: 'search' })
74
76
  .paid('0.01')
75
77
  .body(searchSchema)
76
78
  .output(searchResponseSchema)
@@ -81,7 +83,7 @@ export const POST = router.route('search')
81
83
  **SIWX-authenticated route**
82
84
 
83
85
  ```typescript
84
- export const GET = router.route('inbox/status')
86
+ export const GET = router.route({ path: 'inbox/status' })
85
87
  .siwx()
86
88
  .query(statusQuerySchema)
87
89
  .handler(async ({ query, wallet }) => getStatus(query, wallet));
@@ -90,7 +92,7 @@ export const GET = router.route('inbox/status')
90
92
  **Unprotected route**
91
93
 
92
94
  ```typescript
93
- export const GET = router.route('health')
95
+ export const GET = router.route({ path: 'health' })
94
96
  .unprotected()
95
97
  .handler(async () => ({ status: 'ok' }));
96
98
  ```
@@ -101,7 +103,7 @@ export const GET = router.route('health')
101
103
  // app/.well-known/x402/route.ts
102
104
  import { router } from '@/lib/routes';
103
105
  import '@/lib/routes/barrel'; // ensures all routes are imported
104
- export const GET = router.wellKnown();
106
+ export const GET = router.wellKnown({ methodHints: 'non-default' });
105
107
 
106
108
  // app/openapi.json/route.ts
107
109
  import { router } from '@/lib/routes';
@@ -129,11 +131,29 @@ Creates a `ServiceRouter` instance.
129
131
  | Option | Type | Default | Description |
130
132
  |--------|------|---------|-------------|
131
133
  | `payeeAddress` | `string` | **required** | Wallet address to receive payments |
134
+ | `baseUrl` | `string` | **required** | Service origin used for discovery/OpenAPI/realm |
132
135
  | `network` | `string` | `'eip155:8453'` | Blockchain network |
133
136
  | `plugin` | `RouterPlugin` | `undefined` | Observability plugin |
134
137
  | `prices` | `Record<string, string>` | `undefined` | Central pricing map (auto-applied) |
135
138
  | `siwx.nonceStore` | `NonceStore` | `MemoryNonceStore` | Custom nonce store |
136
139
  | `mpp` | `{ secretKey, currency, recipient? }` | `undefined` | MPP config |
140
+ | `strictRoutes` | `boolean` | `false` | Enforce `route({ path })` and prevent key/path divergence |
141
+
142
+ ### Path-First Routing
143
+
144
+ Use path-first route definitions to keep runtime, OpenAPI, and discovery aligned:
145
+
146
+ ```typescript
147
+ router.route({ path: 'flightaware/airports/id/flights/arrivals', method: 'GET' })
148
+ ```
149
+
150
+ If you need a custom internal key (legacy pricing map), you can pass:
151
+
152
+ ```typescript
153
+ router.route({ path: 'public/path', key: 'legacy/key' })
154
+ ```
155
+
156
+ In `strictRoutes` mode, custom keys are rejected to prevent discovery drift.
137
157
 
138
158
  ### Route Builder
139
159
 
package/dist/index.cjs CHANGED
@@ -1456,7 +1456,15 @@ function createRedisEntitlementStore(client, options) {
1456
1456
 
1457
1457
  // src/discovery/well-known.ts
1458
1458
  var import_server3 = require("next/server");
1459
- function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1459
+
1460
+ // src/discovery/utils/guidance.ts
1461
+ async function resolveGuidance(discovery) {
1462
+ if (typeof discovery.guidance === "function") return discovery.guidance();
1463
+ return discovery.guidance;
1464
+ }
1465
+
1466
+ // src/discovery/well-known.ts
1467
+ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
1460
1468
  const normalizedBase = baseUrl.replace(/\/+$/, "");
1461
1469
  let validated = false;
1462
1470
  return async (_request) => {
@@ -1466,17 +1474,14 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1466
1474
  }
1467
1475
  const x402Set = /* @__PURE__ */ new Set();
1468
1476
  const mppSet = /* @__PURE__ */ new Set();
1477
+ const methodHints = discovery.methodHints ?? "non-default";
1469
1478
  for (const [key, entry] of registry.entries()) {
1470
1479
  const url = `${normalizedBase}/api/${entry.path ?? key}`;
1471
- if (entry.authMode !== "unprotected") x402Set.add(url);
1472
- if (entry.protocols.includes("mpp")) mppSet.add(url);
1473
- }
1474
- let instructions;
1475
- if (typeof options.instructions === "function") {
1476
- instructions = await options.instructions();
1477
- } else if (typeof options.instructions === "string") {
1478
- instructions = options.instructions;
1480
+ const resource = toDiscoveryResource(entry.method, url, methodHints);
1481
+ if (entry.authMode !== "unprotected") x402Set.add(resource);
1482
+ if (entry.protocols.includes("mpp")) mppSet.add(resource);
1479
1483
  }
1484
+ const instructions = await resolveGuidance(discovery);
1480
1485
  const body = {
1481
1486
  version: 1,
1482
1487
  resources: Array.from(x402Set)
@@ -1485,11 +1490,11 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1485
1490
  if (mppResources.length > 0) {
1486
1491
  body.mppResources = mppResources;
1487
1492
  }
1488
- if (options.description) {
1489
- body.description = options.description;
1493
+ if (discovery.description) {
1494
+ body.description = discovery.description;
1490
1495
  }
1491
- if (options.ownershipProofs) {
1492
- body.ownershipProofs = options.ownershipProofs;
1496
+ if (discovery.ownershipProofs) {
1497
+ body.ownershipProofs = discovery.ownershipProofs;
1493
1498
  }
1494
1499
  if (instructions) {
1495
1500
  body.instructions = instructions;
@@ -1503,10 +1508,16 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1503
1508
  });
1504
1509
  };
1505
1510
  }
1511
+ function toDiscoveryResource(method, url, mode) {
1512
+ if (mode === "off") return url;
1513
+ if (mode === "always") return `${method} ${url}`;
1514
+ const isDefaultProbeMethod = method === "GET" || method === "POST";
1515
+ return isDefaultProbeMethod ? url : `${method} ${url}`;
1516
+ }
1506
1517
 
1507
1518
  // src/discovery/openapi.ts
1508
1519
  var import_server4 = require("next/server");
1509
- function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1520
+ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
1510
1521
  const normalizedBase = baseUrl.replace(/\/+$/, "");
1511
1522
  let cached = null;
1512
1523
  let validated = false;
@@ -1547,21 +1558,20 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1547
1558
  };
1548
1559
  }
1549
1560
  const discoveryMetadata = {};
1550
- if (options.ownershipProofs && options.ownershipProofs.length > 0) {
1551
- discoveryMetadata.ownershipProofs = options.ownershipProofs;
1552
- }
1553
- if (options.llmsTxtUrl) {
1554
- discoveryMetadata.llmsTxtUrl = options.llmsTxtUrl;
1561
+ if (discovery.ownershipProofs && discovery.ownershipProofs.length > 0) {
1562
+ discoveryMetadata.ownershipProofs = discovery.ownershipProofs;
1555
1563
  }
1564
+ const guidance = await resolveGuidance(discovery);
1556
1565
  const openApiDocument = {
1557
1566
  openapi: "3.1.0",
1558
1567
  info: {
1559
- title: options.title,
1560
- description: options.description,
1561
- version: options.version,
1562
- ...options.contact && { contact: options.contact }
1568
+ title: discovery.title,
1569
+ description: discovery.description,
1570
+ version: discovery.version,
1571
+ guidance,
1572
+ ...discovery.contact && { contact: discovery.contact }
1563
1573
  },
1564
- servers: [{ url: (options.baseUrl ?? normalizedBase).replace(/\/+$/, "") }],
1574
+ servers: [{ url: (discovery.serverUrl ?? normalizedBase).replace(/\/+$/, "") }],
1565
1575
  tags: Array.from(tagSet).sort().map((name) => ({ name })),
1566
1576
  ...Object.keys(securitySchemes).length > 0 ? {
1567
1577
  components: {
@@ -1681,6 +1691,20 @@ function buildPricingInfo(entry) {
1681
1691
  return void 0;
1682
1692
  }
1683
1693
 
1694
+ // src/discovery/llms-txt.ts
1695
+ var import_server5 = require("next/server");
1696
+ function createLlmsTxtHandler(discovery) {
1697
+ return async (_request) => {
1698
+ const guidance = await resolveGuidance(discovery) ?? "";
1699
+ return new import_server5.NextResponse(guidance, {
1700
+ headers: {
1701
+ "Content-Type": "text/plain; charset=utf-8",
1702
+ "Access-Control-Allow-Origin": "*"
1703
+ }
1704
+ });
1705
+ };
1706
+ }
1707
+
1684
1708
  // src/index.ts
1685
1709
  function createRouter(config) {
1686
1710
  const registry = new RouteRegistry();
@@ -1783,19 +1807,40 @@ function createRouter(config) {
1783
1807
  })();
1784
1808
  const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
1785
1809
  return {
1786
- route(key) {
1787
- const builder = new RouteBuilder(key, registry, deps);
1810
+ route(keyOrDefinition) {
1811
+ const isDefinition = typeof keyOrDefinition !== "string";
1812
+ if (config.strictRoutes && !isDefinition) {
1813
+ throw new Error(
1814
+ "[router] strictRoutes=true requires route({ path }) form. Replace route('my/key') with route({ path: 'my/key' })."
1815
+ );
1816
+ }
1817
+ const definition = isDefinition ? keyOrDefinition : { path: keyOrDefinition, key: keyOrDefinition };
1818
+ const normalizedPath = normalizePath(definition.path);
1819
+ const key = definition.key ?? normalizedPath;
1820
+ if (config.strictRoutes && definition.key && definition.key !== definition.path) {
1821
+ throw new Error(
1822
+ `[router] strictRoutes=true forbids key/path divergence for route '${definition.path}'. Remove custom \`key\` or make it equal to \`path\`.`
1823
+ );
1824
+ }
1825
+ let builder = new RouteBuilder(key, registry, deps);
1826
+ builder = builder.path(normalizedPath);
1827
+ if (definition.method) {
1828
+ builder = builder.method(definition.method);
1829
+ }
1788
1830
  if (config.prices && key in config.prices) {
1789
1831
  const options = config.protocols ? { protocols: config.protocols } : void 0;
1790
1832
  return builder.paid(config.prices[key], options);
1791
1833
  }
1792
1834
  return builder;
1793
1835
  },
1794
- wellKnown(options) {
1795
- return createWellKnownHandler(registry, resolvedBaseUrl, pricesKeys, options);
1836
+ wellKnown() {
1837
+ return createWellKnownHandler(registry, resolvedBaseUrl, pricesKeys, config.discovery);
1796
1838
  },
1797
- openapi(options) {
1798
- return createOpenAPIHandler(registry, resolvedBaseUrl, pricesKeys, options);
1839
+ openapi() {
1840
+ return createOpenAPIHandler(registry, resolvedBaseUrl, pricesKeys, config.discovery);
1841
+ },
1842
+ llmsTxt() {
1843
+ return createLlmsTxtHandler(config.discovery);
1799
1844
  },
1800
1845
  monitors() {
1801
1846
  const result = [];
@@ -1816,6 +1861,12 @@ function createRouter(config) {
1816
1861
  registry
1817
1862
  };
1818
1863
  }
1864
+ function normalizePath(path) {
1865
+ let normalized = path.trim();
1866
+ normalized = normalized.replace(/^\/+/, "");
1867
+ normalized = normalized.replace(/^api\/+/, "");
1868
+ return normalized.replace(/\/+$/, "");
1869
+ }
1819
1870
  // Annotate the CommonJS export names for ESM import in node:
1820
1871
  0 && (module.exports = {
1821
1872
  HttpError,
package/dist/index.d.cts CHANGED
@@ -185,6 +185,26 @@ interface X402Server {
185
185
  }
186
186
  type ProtocolType = 'x402' | 'mpp';
187
187
  type AuthMode = 'paid' | 'siwx' | 'apiKey' | 'unprotected';
188
+ type RouteMethod = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH';
189
+ interface RouteDefinition<K extends string = string> {
190
+ /**
191
+ * Public API path segment (without `/api/` prefix).
192
+ * Example: `flightaware/airports/id/flights/arrivals`
193
+ */
194
+ path: K;
195
+ /**
196
+ * Internal route ID for pricing maps / analytics. Defaults to `path`.
197
+ *
198
+ * In `strictRoutes` mode, custom keys are disallowed to prevent discovery
199
+ * drift between internal IDs and advertised paths.
200
+ */
201
+ key?: string;
202
+ /**
203
+ * Optional explicit method. If omitted, defaults to builder behavior
204
+ * (`POST`, or `GET` when `.query()` is used).
205
+ */
206
+ method?: RouteMethod;
207
+ }
188
208
  interface TierConfig {
189
209
  price: string;
190
210
  label?: string;
@@ -251,7 +271,7 @@ interface RouteEntry {
251
271
  outputSchema?: ZodType;
252
272
  description?: string;
253
273
  path?: string;
254
- method: 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH';
274
+ method: RouteMethod;
255
275
  maxPrice?: string;
256
276
  minPrice?: string;
257
277
  payTo?: string | ((request: Request) => string | Promise<string>);
@@ -260,6 +280,21 @@ interface RouteEntry {
260
280
  providerConfig?: ProviderConfig;
261
281
  validateFn?: (body: unknown) => void | Promise<void>;
262
282
  }
283
+ interface DiscoveryConfig {
284
+ title: string;
285
+ version: string;
286
+ description?: string;
287
+ contact?: {
288
+ name?: string;
289
+ url?: string;
290
+ };
291
+ ownershipProofs?: string[];
292
+ methodHints?: 'off' | 'non-default' | 'always';
293
+ /** Natural language guidance for agents. Served as wellknown `instructions` and `/llms.txt`. */
294
+ guidance?: string | (() => string | Promise<string>);
295
+ /** Override the OpenAPI `servers` URL. Defaults to `RouterConfig.baseUrl`. Use when the public API hostname differs from the payment realm URL. */
296
+ serverUrl?: string;
297
+ }
263
298
  interface RouterConfig {
264
299
  payeeAddress: string;
265
300
  /**
@@ -299,6 +334,17 @@ interface RouterConfig {
299
334
  * })
300
335
  */
301
336
  protocols?: ProtocolType[];
337
+ /**
338
+ * Enforce explicit, path-first route definitions.
339
+ *
340
+ * When enabled:
341
+ * - `.route('key')` is rejected; use `.route({ path })`.
342
+ * - custom `key` differing from `path` is rejected.
343
+ *
344
+ * This prevents discovery/openapi drift caused by shorthand internal keys.
345
+ */
346
+ strictRoutes?: boolean;
347
+ discovery: DiscoveryConfig;
302
348
  }
303
349
 
304
350
  declare class RouteRegistry {
@@ -311,25 +357,6 @@ declare class RouteRegistry {
311
357
  validate(expectedKeys?: string[]): void;
312
358
  }
313
359
 
314
- interface WellKnownOptions {
315
- description?: string;
316
- instructions?: string | (() => string | Promise<string>);
317
- ownershipProofs?: string[];
318
- }
319
-
320
- interface OpenAPIOptions {
321
- title: string;
322
- version: string;
323
- description?: string;
324
- baseUrl?: string;
325
- contact?: {
326
- name?: string;
327
- url?: string;
328
- };
329
- llmsTxtUrl?: string;
330
- ownershipProofs?: string[];
331
- }
332
-
333
360
  interface OrchestrateDeps {
334
361
  x402Server: X402Server | null;
335
362
  initPromise: Promise<void>;
@@ -437,9 +464,10 @@ interface MonitorEntry {
437
464
  critical?: number;
438
465
  }
439
466
  interface ServiceRouter<TPriceKeys extends string = never> {
440
- route<K extends string>(key: K): [K] extends [TPriceKeys] ? RouteBuilder<undefined, undefined, true, false, false> : RouteBuilder<undefined, undefined, false, false, false>;
441
- wellKnown(options?: WellKnownOptions): (request: NextRequest) => Promise<NextResponse>;
442
- openapi(options: OpenAPIOptions): (request: NextRequest) => Promise<NextResponse>;
467
+ route<K extends string>(keyOrDefinition: K | RouteDefinition<K>): [K] extends [TPriceKeys] ? RouteBuilder<undefined, undefined, true, false, false> : RouteBuilder<undefined, undefined, false, false, false>;
468
+ wellKnown(): (request: NextRequest) => Promise<NextResponse>;
469
+ openapi(): (request: NextRequest) => Promise<NextResponse>;
470
+ llmsTxt(): (request: NextRequest) => Promise<NextResponse>;
443
471
  monitors(): MonitorEntry[];
444
472
  registry: RouteRegistry;
445
473
  }
@@ -447,4 +475,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
447
475
  prices?: P;
448
476
  }): ServiceRouter<Extract<keyof P, string>>;
449
477
 
450
- export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type EntitlementStore, type ErrorEvent, type HandlerContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type NonceStore, type OveragePolicy, type PaidOptions, type PaymentEvent, 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, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402Server, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter };
478
+ export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type DiscoveryConfig, type EntitlementStore, type ErrorEvent, type HandlerContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type NonceStore, type OveragePolicy, type PaidOptions, type PaymentEvent, 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, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402Server, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter };
package/dist/index.d.ts CHANGED
@@ -185,6 +185,26 @@ interface X402Server {
185
185
  }
186
186
  type ProtocolType = 'x402' | 'mpp';
187
187
  type AuthMode = 'paid' | 'siwx' | 'apiKey' | 'unprotected';
188
+ type RouteMethod = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH';
189
+ interface RouteDefinition<K extends string = string> {
190
+ /**
191
+ * Public API path segment (without `/api/` prefix).
192
+ * Example: `flightaware/airports/id/flights/arrivals`
193
+ */
194
+ path: K;
195
+ /**
196
+ * Internal route ID for pricing maps / analytics. Defaults to `path`.
197
+ *
198
+ * In `strictRoutes` mode, custom keys are disallowed to prevent discovery
199
+ * drift between internal IDs and advertised paths.
200
+ */
201
+ key?: string;
202
+ /**
203
+ * Optional explicit method. If omitted, defaults to builder behavior
204
+ * (`POST`, or `GET` when `.query()` is used).
205
+ */
206
+ method?: RouteMethod;
207
+ }
188
208
  interface TierConfig {
189
209
  price: string;
190
210
  label?: string;
@@ -251,7 +271,7 @@ interface RouteEntry {
251
271
  outputSchema?: ZodType;
252
272
  description?: string;
253
273
  path?: string;
254
- method: 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH';
274
+ method: RouteMethod;
255
275
  maxPrice?: string;
256
276
  minPrice?: string;
257
277
  payTo?: string | ((request: Request) => string | Promise<string>);
@@ -260,6 +280,21 @@ interface RouteEntry {
260
280
  providerConfig?: ProviderConfig;
261
281
  validateFn?: (body: unknown) => void | Promise<void>;
262
282
  }
283
+ interface DiscoveryConfig {
284
+ title: string;
285
+ version: string;
286
+ description?: string;
287
+ contact?: {
288
+ name?: string;
289
+ url?: string;
290
+ };
291
+ ownershipProofs?: string[];
292
+ methodHints?: 'off' | 'non-default' | 'always';
293
+ /** Natural language guidance for agents. Served as wellknown `instructions` and `/llms.txt`. */
294
+ guidance?: string | (() => string | Promise<string>);
295
+ /** Override the OpenAPI `servers` URL. Defaults to `RouterConfig.baseUrl`. Use when the public API hostname differs from the payment realm URL. */
296
+ serverUrl?: string;
297
+ }
263
298
  interface RouterConfig {
264
299
  payeeAddress: string;
265
300
  /**
@@ -299,6 +334,17 @@ interface RouterConfig {
299
334
  * })
300
335
  */
301
336
  protocols?: ProtocolType[];
337
+ /**
338
+ * Enforce explicit, path-first route definitions.
339
+ *
340
+ * When enabled:
341
+ * - `.route('key')` is rejected; use `.route({ path })`.
342
+ * - custom `key` differing from `path` is rejected.
343
+ *
344
+ * This prevents discovery/openapi drift caused by shorthand internal keys.
345
+ */
346
+ strictRoutes?: boolean;
347
+ discovery: DiscoveryConfig;
302
348
  }
303
349
 
304
350
  declare class RouteRegistry {
@@ -311,25 +357,6 @@ declare class RouteRegistry {
311
357
  validate(expectedKeys?: string[]): void;
312
358
  }
313
359
 
314
- interface WellKnownOptions {
315
- description?: string;
316
- instructions?: string | (() => string | Promise<string>);
317
- ownershipProofs?: string[];
318
- }
319
-
320
- interface OpenAPIOptions {
321
- title: string;
322
- version: string;
323
- description?: string;
324
- baseUrl?: string;
325
- contact?: {
326
- name?: string;
327
- url?: string;
328
- };
329
- llmsTxtUrl?: string;
330
- ownershipProofs?: string[];
331
- }
332
-
333
360
  interface OrchestrateDeps {
334
361
  x402Server: X402Server | null;
335
362
  initPromise: Promise<void>;
@@ -437,9 +464,10 @@ interface MonitorEntry {
437
464
  critical?: number;
438
465
  }
439
466
  interface ServiceRouter<TPriceKeys extends string = never> {
440
- route<K extends string>(key: K): [K] extends [TPriceKeys] ? RouteBuilder<undefined, undefined, true, false, false> : RouteBuilder<undefined, undefined, false, false, false>;
441
- wellKnown(options?: WellKnownOptions): (request: NextRequest) => Promise<NextResponse>;
442
- openapi(options: OpenAPIOptions): (request: NextRequest) => Promise<NextResponse>;
467
+ route<K extends string>(keyOrDefinition: K | RouteDefinition<K>): [K] extends [TPriceKeys] ? RouteBuilder<undefined, undefined, true, false, false> : RouteBuilder<undefined, undefined, false, false, false>;
468
+ wellKnown(): (request: NextRequest) => Promise<NextResponse>;
469
+ openapi(): (request: NextRequest) => Promise<NextResponse>;
470
+ llmsTxt(): (request: NextRequest) => Promise<NextResponse>;
443
471
  monitors(): MonitorEntry[];
444
472
  registry: RouteRegistry;
445
473
  }
@@ -447,4 +475,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
447
475
  prices?: P;
448
476
  }): ServiceRouter<Extract<keyof P, string>>;
449
477
 
450
- export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type EntitlementStore, type ErrorEvent, type HandlerContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type NonceStore, type OveragePolicy, type PaidOptions, type PaymentEvent, 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, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402Server, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter };
478
+ export { type AlertEvent, type AlertFn, type AlertLevel, type AuthEvent, type AuthMode, type DiscoveryConfig, type EntitlementStore, type ErrorEvent, type HandlerContext, HttpError, MemoryEntitlementStore, MemoryNonceStore, type MonitorEntry, type NonceStore, type OveragePolicy, type PaidOptions, type PaymentEvent, 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, type RouterPlugin, SIWX_CHALLENGE_EXPIRY_MS, type ServiceRouter, type SettlementEvent, type TierConfig, type X402Server, consolePlugin, createRedisEntitlementStore, createRedisNonceStore, createRouter };
package/dist/index.js CHANGED
@@ -1417,7 +1417,15 @@ function createRedisEntitlementStore(client, options) {
1417
1417
 
1418
1418
  // src/discovery/well-known.ts
1419
1419
  import { NextResponse as NextResponse3 } from "next/server";
1420
- function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1420
+
1421
+ // src/discovery/utils/guidance.ts
1422
+ async function resolveGuidance(discovery) {
1423
+ if (typeof discovery.guidance === "function") return discovery.guidance();
1424
+ return discovery.guidance;
1425
+ }
1426
+
1427
+ // src/discovery/well-known.ts
1428
+ function createWellKnownHandler(registry, baseUrl, pricesKeys, discovery) {
1421
1429
  const normalizedBase = baseUrl.replace(/\/+$/, "");
1422
1430
  let validated = false;
1423
1431
  return async (_request) => {
@@ -1427,17 +1435,14 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1427
1435
  }
1428
1436
  const x402Set = /* @__PURE__ */ new Set();
1429
1437
  const mppSet = /* @__PURE__ */ new Set();
1438
+ const methodHints = discovery.methodHints ?? "non-default";
1430
1439
  for (const [key, entry] of registry.entries()) {
1431
1440
  const url = `${normalizedBase}/api/${entry.path ?? key}`;
1432
- if (entry.authMode !== "unprotected") x402Set.add(url);
1433
- if (entry.protocols.includes("mpp")) mppSet.add(url);
1434
- }
1435
- let instructions;
1436
- if (typeof options.instructions === "function") {
1437
- instructions = await options.instructions();
1438
- } else if (typeof options.instructions === "string") {
1439
- instructions = options.instructions;
1441
+ const resource = toDiscoveryResource(entry.method, url, methodHints);
1442
+ if (entry.authMode !== "unprotected") x402Set.add(resource);
1443
+ if (entry.protocols.includes("mpp")) mppSet.add(resource);
1440
1444
  }
1445
+ const instructions = await resolveGuidance(discovery);
1441
1446
  const body = {
1442
1447
  version: 1,
1443
1448
  resources: Array.from(x402Set)
@@ -1446,11 +1451,11 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1446
1451
  if (mppResources.length > 0) {
1447
1452
  body.mppResources = mppResources;
1448
1453
  }
1449
- if (options.description) {
1450
- body.description = options.description;
1454
+ if (discovery.description) {
1455
+ body.description = discovery.description;
1451
1456
  }
1452
- if (options.ownershipProofs) {
1453
- body.ownershipProofs = options.ownershipProofs;
1457
+ if (discovery.ownershipProofs) {
1458
+ body.ownershipProofs = discovery.ownershipProofs;
1454
1459
  }
1455
1460
  if (instructions) {
1456
1461
  body.instructions = instructions;
@@ -1464,10 +1469,16 @@ function createWellKnownHandler(registry, baseUrl, pricesKeys, options = {}) {
1464
1469
  });
1465
1470
  };
1466
1471
  }
1472
+ function toDiscoveryResource(method, url, mode) {
1473
+ if (mode === "off") return url;
1474
+ if (mode === "always") return `${method} ${url}`;
1475
+ const isDefaultProbeMethod = method === "GET" || method === "POST";
1476
+ return isDefaultProbeMethod ? url : `${method} ${url}`;
1477
+ }
1467
1478
 
1468
1479
  // src/discovery/openapi.ts
1469
1480
  import { NextResponse as NextResponse4 } from "next/server";
1470
- function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1481
+ function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
1471
1482
  const normalizedBase = baseUrl.replace(/\/+$/, "");
1472
1483
  let cached = null;
1473
1484
  let validated = false;
@@ -1508,21 +1519,20 @@ function createOpenAPIHandler(registry, baseUrl, pricesKeys, options) {
1508
1519
  };
1509
1520
  }
1510
1521
  const discoveryMetadata = {};
1511
- if (options.ownershipProofs && options.ownershipProofs.length > 0) {
1512
- discoveryMetadata.ownershipProofs = options.ownershipProofs;
1513
- }
1514
- if (options.llmsTxtUrl) {
1515
- discoveryMetadata.llmsTxtUrl = options.llmsTxtUrl;
1522
+ if (discovery.ownershipProofs && discovery.ownershipProofs.length > 0) {
1523
+ discoveryMetadata.ownershipProofs = discovery.ownershipProofs;
1516
1524
  }
1525
+ const guidance = await resolveGuidance(discovery);
1517
1526
  const openApiDocument = {
1518
1527
  openapi: "3.1.0",
1519
1528
  info: {
1520
- title: options.title,
1521
- description: options.description,
1522
- version: options.version,
1523
- ...options.contact && { contact: options.contact }
1529
+ title: discovery.title,
1530
+ description: discovery.description,
1531
+ version: discovery.version,
1532
+ guidance,
1533
+ ...discovery.contact && { contact: discovery.contact }
1524
1534
  },
1525
- servers: [{ url: (options.baseUrl ?? normalizedBase).replace(/\/+$/, "") }],
1535
+ servers: [{ url: (discovery.serverUrl ?? normalizedBase).replace(/\/+$/, "") }],
1526
1536
  tags: Array.from(tagSet).sort().map((name) => ({ name })),
1527
1537
  ...Object.keys(securitySchemes).length > 0 ? {
1528
1538
  components: {
@@ -1642,6 +1652,20 @@ function buildPricingInfo(entry) {
1642
1652
  return void 0;
1643
1653
  }
1644
1654
 
1655
+ // src/discovery/llms-txt.ts
1656
+ import { NextResponse as NextResponse5 } from "next/server";
1657
+ function createLlmsTxtHandler(discovery) {
1658
+ return async (_request) => {
1659
+ const guidance = await resolveGuidance(discovery) ?? "";
1660
+ return new NextResponse5(guidance, {
1661
+ headers: {
1662
+ "Content-Type": "text/plain; charset=utf-8",
1663
+ "Access-Control-Allow-Origin": "*"
1664
+ }
1665
+ });
1666
+ };
1667
+ }
1668
+
1645
1669
  // src/index.ts
1646
1670
  function createRouter(config) {
1647
1671
  const registry = new RouteRegistry();
@@ -1744,19 +1768,40 @@ function createRouter(config) {
1744
1768
  })();
1745
1769
  const pricesKeys = config.prices ? Object.keys(config.prices) : void 0;
1746
1770
  return {
1747
- route(key) {
1748
- const builder = new RouteBuilder(key, registry, deps);
1771
+ route(keyOrDefinition) {
1772
+ const isDefinition = typeof keyOrDefinition !== "string";
1773
+ if (config.strictRoutes && !isDefinition) {
1774
+ throw new Error(
1775
+ "[router] strictRoutes=true requires route({ path }) form. Replace route('my/key') with route({ path: 'my/key' })."
1776
+ );
1777
+ }
1778
+ const definition = isDefinition ? keyOrDefinition : { path: keyOrDefinition, key: keyOrDefinition };
1779
+ const normalizedPath = normalizePath(definition.path);
1780
+ const key = definition.key ?? normalizedPath;
1781
+ if (config.strictRoutes && definition.key && definition.key !== definition.path) {
1782
+ throw new Error(
1783
+ `[router] strictRoutes=true forbids key/path divergence for route '${definition.path}'. Remove custom \`key\` or make it equal to \`path\`.`
1784
+ );
1785
+ }
1786
+ let builder = new RouteBuilder(key, registry, deps);
1787
+ builder = builder.path(normalizedPath);
1788
+ if (definition.method) {
1789
+ builder = builder.method(definition.method);
1790
+ }
1749
1791
  if (config.prices && key in config.prices) {
1750
1792
  const options = config.protocols ? { protocols: config.protocols } : void 0;
1751
1793
  return builder.paid(config.prices[key], options);
1752
1794
  }
1753
1795
  return builder;
1754
1796
  },
1755
- wellKnown(options) {
1756
- return createWellKnownHandler(registry, resolvedBaseUrl, pricesKeys, options);
1797
+ wellKnown() {
1798
+ return createWellKnownHandler(registry, resolvedBaseUrl, pricesKeys, config.discovery);
1757
1799
  },
1758
- openapi(options) {
1759
- return createOpenAPIHandler(registry, resolvedBaseUrl, pricesKeys, options);
1800
+ openapi() {
1801
+ return createOpenAPIHandler(registry, resolvedBaseUrl, pricesKeys, config.discovery);
1802
+ },
1803
+ llmsTxt() {
1804
+ return createLlmsTxtHandler(config.discovery);
1760
1805
  },
1761
1806
  monitors() {
1762
1807
  const result = [];
@@ -1777,6 +1822,12 @@ function createRouter(config) {
1777
1822
  registry
1778
1823
  };
1779
1824
  }
1825
+ function normalizePath(path) {
1826
+ let normalized = path.trim();
1827
+ normalized = normalized.replace(/^\/+/, "");
1828
+ normalized = normalized.replace(/^api\/+/, "");
1829
+ return normalized.replace(/\/+$/, "");
1830
+ }
1780
1831
  export {
1781
1832
  HttpError,
1782
1833
  MemoryEntitlementStore,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "0.7.0",
3
+ "version": "1.0.0",
4
4
  "description": "Unified route builder for Next.js App Router APIs with x402, MPP, SIWX, and API key auth",
5
5
  "type": "module",
6
6
  "exports": {
@@ -48,7 +48,7 @@
48
48
  "@x402/evm": "^2.3.0",
49
49
  "@x402/extensions": "^2.3.0",
50
50
  "eslint": "^10.0.0",
51
- "mppx": "^0.3.9",
51
+ "mppx": "^0.3.13",
52
52
  "next": "^15.0.0",
53
53
  "prettier": "^3.8.1",
54
54
  "react": "^19.0.0",