@agentcash/router 1.10.4 → 1.10.6

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
@@ -150,6 +150,21 @@ The barrel forces every route module to load before the discovery handler walks
150
150
 
151
151
  The `openapi.json` should be hosted at `GET <origin>/openapi.json`.
152
152
 
153
+ ### 4. Unmatched route fallback
154
+
155
+ ```typescript
156
+ // app/api/[[...path]]/route.ts
157
+ import { router } from '@/lib/router';
158
+
159
+ export const GET = router.notFound();
160
+ export const POST = router.notFound();
161
+ export const DELETE = router.notFound();
162
+ export const PUT = router.notFound();
163
+ export const PATCH = router.notFound();
164
+ ```
165
+
166
+ This catches stale agent calls to API paths that no longer exist and returns a JSON 404 telling the client to rediscover the origin.
167
+
153
168
  ## Auth modes
154
169
 
155
170
  | Method | Purpose |
package/dist/index.cjs CHANGED
@@ -1207,7 +1207,7 @@ async function trySiwxFastPath(ctx, account) {
1207
1207
  function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
1208
1208
  if (incomingStrategy) return false;
1209
1209
  if (!routeEntry.bodySchema) return false;
1210
- return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
1210
+ return (pricing?.needsBody ?? false) || !!routeEntry.validateFn || !!routeEntry.checkoutSession;
1211
1211
  }
1212
1212
 
1213
1213
  // src/pipeline/steps/resolve-early-body.ts
@@ -2106,6 +2106,7 @@ function invalidPaymentVerification(failure) {
2106
2106
  }
2107
2107
 
2108
2108
  // src/protocols/x402/strategy.ts
2109
+ var SETTLE_RETRY_DELAYS_MS = [500, 1e3];
2109
2110
  function formatVerifyFailureMessage(failure) {
2110
2111
  if (failure.reason === "permit2_allowance_required") {
2111
2112
  const wallet = failure.payer ?? "<the payer wallet>";
@@ -2196,7 +2197,17 @@ async function settleX402(args) {
2196
2197
  const { payload, requirements } = token;
2197
2198
  const override = routeEntry.billing === "exact" ? void 0 : { amount: billedAmount };
2198
2199
  try {
2199
- const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2200
+ let settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2201
+ for (let attempt = 0; !settle.result?.success && attempt < SETTLE_RETRY_DELAYS_MS.length; attempt++) {
2202
+ report("warn", "Retrying x402 settlement", {
2203
+ attempt: attempt + 1,
2204
+ errorReason: settle.result?.errorReason
2205
+ });
2206
+ await new Promise((resolve) => {
2207
+ setTimeout(resolve, SETTLE_RETRY_DELAYS_MS[attempt]);
2208
+ });
2209
+ settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2210
+ }
2200
2211
  if (!settle.result?.success) {
2201
2212
  throw Object.assign(
2202
2213
  new Error(settle.result?.errorReason ?? "x402 settlement returned success=false"),
@@ -2538,30 +2549,37 @@ async function buildChallengeResponse(ctx, pricing, body, failure) {
2538
2549
  return errorResponse;
2539
2550
  }
2540
2551
  const extensions = await buildChallengeExtensions(ctx);
2541
- const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
2542
- const response = new import_server5.NextResponse(responseBody, {
2543
- status: 402,
2544
- headers: {
2545
- "Content-Type": "application/json",
2546
- "Cache-Control": "no-store"
2547
- }
2548
- });
2549
- for (const strategy of getAllowedStrategies(ctx.routeEntry.protocols)) {
2552
+ let checkoutSession;
2553
+ if (ctx.routeEntry.checkoutSession) {
2550
2554
  try {
2551
- const contribution = await strategy.buildChallenge({
2555
+ checkoutSession = await ctx.routeEntry.checkoutSession({
2552
2556
  request: ctx.request,
2553
- routeEntry: ctx.routeEntry,
2557
+ route: ctx.routeEntry.key,
2554
2558
  body,
2555
- price: challengePrice,
2556
- extensions,
2557
- deps: ctx.deps,
2558
- report: ctx.report
2559
+ price: challengePrice
2559
2560
  });
2560
- if (contribution.headers) {
2561
- for (const [name, value] of Object.entries(contribution.headers)) {
2562
- response.headers.set(name, value);
2563
- }
2564
- }
2561
+ } catch (err) {
2562
+ const message = errorMessage(err, "Checkout session build failed");
2563
+ const responseBody2 = { success: false, error: message };
2564
+ const errorResponse = import_server5.NextResponse.json(responseBody2, { status: errorStatus(err, 500) });
2565
+ firePluginResponse(ctx, errorResponse, body, responseBody2, { message, cause: err });
2566
+ return errorResponse;
2567
+ }
2568
+ }
2569
+ const contributions = [];
2570
+ for (const strategy of getAllowedStrategies(ctx.routeEntry.protocols)) {
2571
+ try {
2572
+ contributions.push(
2573
+ await strategy.buildChallenge({
2574
+ request: ctx.request,
2575
+ routeEntry: ctx.routeEntry,
2576
+ body,
2577
+ price: challengePrice,
2578
+ extensions,
2579
+ deps: ctx.deps,
2580
+ report: ctx.report
2581
+ })
2582
+ );
2565
2583
  } catch (err) {
2566
2584
  const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
2567
2585
  ctx.report("critical", message);
@@ -2573,6 +2591,26 @@ async function buildChallengeResponse(ctx, pricing, body, failure) {
2573
2591
  }
2574
2592
  }
2575
2593
  }
2594
+ const responsePayload = {
2595
+ ...Object.assign({}, ...contributions.map((contribution) => contribution.body ?? {})),
2596
+ ...failure && { error: failure.message ?? null, reason: failure.reason },
2597
+ ...checkoutSession && { checkout_session: checkoutSession }
2598
+ };
2599
+ const responseBody = Object.keys(responsePayload).length ? JSON.stringify(responsePayload) : null;
2600
+ const response = new import_server5.NextResponse(responseBody, {
2601
+ status: 402,
2602
+ headers: {
2603
+ "Content-Type": "application/json",
2604
+ "Cache-Control": "no-store"
2605
+ }
2606
+ });
2607
+ for (const contribution of contributions) {
2608
+ if (contribution.headers) {
2609
+ for (const [name, value] of Object.entries(contribution.headers)) {
2610
+ response.headers.set(name, value);
2611
+ }
2612
+ }
2613
+ }
2576
2614
  firePluginResponse(ctx, response);
2577
2615
  return response;
2578
2616
  }
@@ -3590,7 +3628,9 @@ var RouteBuilder = class _RouteBuilder {
3590
3628
  providerConfig: void 0,
3591
3629
  validateFn: void 0,
3592
3630
  settlement: void 0,
3593
- mppInfo: void 0
3631
+ mppInfo: void 0,
3632
+ hasCheckout: false,
3633
+ checkoutSession: void 0
3594
3634
  };
3595
3635
  }
3596
3636
  fork() {
@@ -3681,6 +3721,8 @@ var RouteBuilder = class _RouteBuilder {
3681
3721
  if (resolvedOptions.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
3682
3722
  if (resolvedOptions.payTo) next.#s.payTo = resolvedOptions.payTo;
3683
3723
  if (resolvedOptions.mpp) next.#s.mppInfo = resolvedOptions.mpp;
3724
+ if (resolvedOptions.checkout || resolvedOptions.checkoutSession) next.#s.hasCheckout = true;
3725
+ if (resolvedOptions.checkoutSession) next.#s.checkoutSession = resolvedOptions.checkoutSession;
3684
3726
  next.#s.billing = billing;
3685
3727
  if (tickCost) next.#s.tickCost = tickCost;
3686
3728
  if (unitType) next.#s.unitType = unitType;
@@ -4114,6 +4156,8 @@ var RouteBuilder = class _RouteBuilder {
4114
4156
  validateFn: this.#s.validateFn,
4115
4157
  settlement: this.#s.settlement,
4116
4158
  mppInfo: this.#s.mppInfo,
4159
+ hasCheckout: this.#s.hasCheckout ? true : void 0,
4160
+ checkoutSession: this.#s.checkoutSession,
4117
4161
  tickCost: this.#s.tickCost,
4118
4162
  unitType: this.#s.unitType
4119
4163
  };
@@ -4320,6 +4364,7 @@ function buildOperation(routeKey, entry, tag) {
4320
4364
  const requiresSiwxScheme = entry.authMode === "siwx" || Boolean(entry.siwxEnabled);
4321
4365
  const requiresApiKeyScheme = Boolean(entry.apiKeyResolver) && entry.authMode !== "siwx";
4322
4366
  const pricingInfo = buildPricingInfo(entry);
4367
+ const hasCheckout = entry.hasCheckout === true;
4323
4368
  const operation = {
4324
4369
  operationId: routeKey.replace(/\//g, "_"),
4325
4370
  summary: entry.description ?? routeKey,
@@ -4345,10 +4390,11 @@ function buildOperation(routeKey, entry, tag) {
4345
4390
  }
4346
4391
  }
4347
4392
  };
4348
- if (paymentRequired && (pricingInfo || protocols)) {
4393
+ if (paymentRequired && (pricingInfo || protocols || hasCheckout)) {
4349
4394
  operation["x-payment-info"] = {
4350
4395
  ...pricingInfo ?? {},
4351
- ...protocols && { protocols }
4396
+ ...protocols && { protocols },
4397
+ ...hasCheckout && { has_checkout: true }
4352
4398
  };
4353
4399
  }
4354
4400
  if (requiresSiwxScheme) {
@@ -4459,6 +4505,33 @@ function createLlmsTxtHandler(discovery) {
4459
4505
  };
4460
4506
  }
4461
4507
 
4508
+ // src/discovery/not-found.ts
4509
+ var import_server12 = require("next/server");
4510
+ function createNotFoundHandler(baseUrl) {
4511
+ const normalizedBase = baseUrl.replace(/\/+$/, "");
4512
+ return async (request) => import_server12.NextResponse.json(
4513
+ {
4514
+ success: false,
4515
+ code: "route_not_found",
4516
+ error: "Route not found. Rediscover this origin and retry with the current discovery document.",
4517
+ requestedUrl: request.url,
4518
+ discovery: {
4519
+ openapi: `${normalizedBase}/openapi.json`,
4520
+ wellKnown: `${normalizedBase}/.well-known/x402`,
4521
+ llmsTxt: `${normalizedBase}/llms.txt`
4522
+ }
4523
+ },
4524
+ {
4525
+ status: 404,
4526
+ headers: {
4527
+ "Access-Control-Allow-Origin": "*",
4528
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, PUT, PATCH, OPTIONS",
4529
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, SIGN-IN-WITH-X"
4530
+ }
4531
+ }
4532
+ );
4533
+ }
4534
+
4462
4535
  // src/index.ts
4463
4536
  init_accepts();
4464
4537
  init_constants();
@@ -5162,6 +5235,9 @@ function createRouter(config) {
5162
5235
  llmsTxt() {
5163
5236
  return createLlmsTxtHandler(config.discovery);
5164
5237
  },
5238
+ notFound() {
5239
+ return createNotFoundHandler(resolvedBaseUrl);
5240
+ },
5165
5241
  monitors() {
5166
5242
  const result = [];
5167
5243
  for (const [, entry] of registry.entries()) {
package/dist/index.d.cts CHANGED
@@ -219,6 +219,15 @@ interface MppProtocolInfo {
219
219
  intent?: string;
220
220
  currency?: string;
221
221
  }
222
+ type CheckoutSessionResponse = Record<string, unknown>;
223
+ interface CheckoutSessionContext<TBody = unknown> {
224
+ request: NextRequest;
225
+ route: string;
226
+ body: TBody | undefined;
227
+ /** Decimal-dollar price quoted for this 402 challenge. */
228
+ price: string;
229
+ }
230
+ type CheckoutSessionFn<TBody = unknown> = (ctx: CheckoutSessionContext<TBody>) => CheckoutSessionResponse | null | undefined | Promise<CheckoutSessionResponse | null | undefined>;
222
231
  interface PaidOptions {
223
232
  protocols?: ProtocolType[];
224
233
  maxPrice?: string;
@@ -227,6 +236,16 @@ interface PaidOptions {
227
236
  payTo?: PayToConfig;
228
237
  /** Override MPP protocol metadata in x-payment-info discovery. */
229
238
  mpp?: MppProtocolInfo;
239
+ /** Signal in discovery that clients should use an explicit checkout flow before payment. */
240
+ checkout?: boolean;
241
+ /**
242
+ * Build dynamic checkout review metadata for the router-owned 402 response body.
243
+ *
244
+ * The returned object is emitted as `{ "checkout_session": ... }` on the unpaid
245
+ * payment challenge response. Payment terms remain authoritative in the x402/MPP
246
+ * challenge headers.
247
+ */
248
+ checkoutSession?: CheckoutSessionFn;
230
249
  }
231
250
  type PaidArg = (PaidOptions & {
232
251
  price: string;
@@ -371,6 +390,8 @@ interface RouteEntry {
371
390
  validateFn?: (body: unknown) => void | Promise<void>;
372
391
  settlement?: SettlementLifecycle;
373
392
  mppInfo?: MppProtocolInfo;
393
+ hasCheckout?: boolean;
394
+ checkoutSession?: CheckoutSessionFn;
374
395
  /** Per-tick cost (decimal-dollar). Required when `metered` is true. */
375
396
  tickCost?: string;
376
397
  /** Cosmetic unit label for 402 challenges and client UIs. */
@@ -554,8 +575,8 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
554
575
  * - `{ price }` — fixed price (object form of the string sugar).
555
576
  * - `{ field, tiers, default? }` — pick a tier from `body[field]`.
556
577
  *
557
- * Common knobs (`protocols`, `maxPrice`, `minPrice`, `payTo`, `mpp`) live
558
- * alongside the pricing shape. For handler-computed billing use `.upTo()`;
578
+ * Common knobs (`protocols`, `maxPrice`, `minPrice`, `payTo`, `mpp`, `checkout`,
579
+ * `checkoutSession`) live alongside the pricing shape. For handler-computed billing use `.upTo()`;
559
580
  * for per-tick billing use `.metered()`.
560
581
  *
561
582
  * @example
@@ -897,6 +918,7 @@ interface ServiceRouter<TPriceKeys extends string = never> {
897
918
  wellKnown(): (request: NextRequest) => Promise<NextResponse>;
898
919
  openapi(): (request: NextRequest) => Promise<NextResponse>;
899
920
  llmsTxt(): (request: NextRequest) => Promise<NextResponse>;
921
+ notFound(): (request: NextRequest) => Promise<NextResponse>;
900
922
  monitors(): MonitorEntry[];
901
923
  registry: RouteRegistry;
902
924
  }
@@ -925,4 +947,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
925
947
  */
926
948
  declare function createRouterFromEnv<const P extends Record<string, string> = Record<never, string>>(options: CreateRouterFromEnvOptions<P>): ServiceRouter<Extract<keyof P, string>>;
927
949
 
928
- export { BASE_MAINNET_NETWORK, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, type CreateRouterFromEnvOptions, DEFAULT_SOLANA_FACILITATOR_URL, DEFAULT_TEMPO_RPC_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 };
950
+ export { BASE_MAINNET_NETWORK, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, type CheckoutSessionContext, type CheckoutSessionFn, type CheckoutSessionResponse, type CreateRouterFromEnvOptions, DEFAULT_SOLANA_FACILITATOR_URL, DEFAULT_TEMPO_RPC_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 };
package/dist/index.d.ts CHANGED
@@ -219,6 +219,15 @@ interface MppProtocolInfo {
219
219
  intent?: string;
220
220
  currency?: string;
221
221
  }
222
+ type CheckoutSessionResponse = Record<string, unknown>;
223
+ interface CheckoutSessionContext<TBody = unknown> {
224
+ request: NextRequest;
225
+ route: string;
226
+ body: TBody | undefined;
227
+ /** Decimal-dollar price quoted for this 402 challenge. */
228
+ price: string;
229
+ }
230
+ type CheckoutSessionFn<TBody = unknown> = (ctx: CheckoutSessionContext<TBody>) => CheckoutSessionResponse | null | undefined | Promise<CheckoutSessionResponse | null | undefined>;
222
231
  interface PaidOptions {
223
232
  protocols?: ProtocolType[];
224
233
  maxPrice?: string;
@@ -227,6 +236,16 @@ interface PaidOptions {
227
236
  payTo?: PayToConfig;
228
237
  /** Override MPP protocol metadata in x-payment-info discovery. */
229
238
  mpp?: MppProtocolInfo;
239
+ /** Signal in discovery that clients should use an explicit checkout flow before payment. */
240
+ checkout?: boolean;
241
+ /**
242
+ * Build dynamic checkout review metadata for the router-owned 402 response body.
243
+ *
244
+ * The returned object is emitted as `{ "checkout_session": ... }` on the unpaid
245
+ * payment challenge response. Payment terms remain authoritative in the x402/MPP
246
+ * challenge headers.
247
+ */
248
+ checkoutSession?: CheckoutSessionFn;
230
249
  }
231
250
  type PaidArg = (PaidOptions & {
232
251
  price: string;
@@ -371,6 +390,8 @@ interface RouteEntry {
371
390
  validateFn?: (body: unknown) => void | Promise<void>;
372
391
  settlement?: SettlementLifecycle;
373
392
  mppInfo?: MppProtocolInfo;
393
+ hasCheckout?: boolean;
394
+ checkoutSession?: CheckoutSessionFn;
374
395
  /** Per-tick cost (decimal-dollar). Required when `metered` is true. */
375
396
  tickCost?: string;
376
397
  /** Cosmetic unit label for 402 challenges and client UIs. */
@@ -554,8 +575,8 @@ declare class RouteBuilder<TBody = undefined, TQuery = undefined, TOutput = unde
554
575
  * - `{ price }` — fixed price (object form of the string sugar).
555
576
  * - `{ field, tiers, default? }` — pick a tier from `body[field]`.
556
577
  *
557
- * Common knobs (`protocols`, `maxPrice`, `minPrice`, `payTo`, `mpp`) live
558
- * alongside the pricing shape. For handler-computed billing use `.upTo()`;
578
+ * Common knobs (`protocols`, `maxPrice`, `minPrice`, `payTo`, `mpp`, `checkout`,
579
+ * `checkoutSession`) live alongside the pricing shape. For handler-computed billing use `.upTo()`;
559
580
  * for per-tick billing use `.metered()`.
560
581
  *
561
582
  * @example
@@ -897,6 +918,7 @@ interface ServiceRouter<TPriceKeys extends string = never> {
897
918
  wellKnown(): (request: NextRequest) => Promise<NextResponse>;
898
919
  openapi(): (request: NextRequest) => Promise<NextResponse>;
899
920
  llmsTxt(): (request: NextRequest) => Promise<NextResponse>;
921
+ notFound(): (request: NextRequest) => Promise<NextResponse>;
900
922
  monitors(): MonitorEntry[];
901
923
  registry: RouteRegistry;
902
924
  }
@@ -925,4 +947,4 @@ declare function createRouter<const P extends Record<string, string> = Record<ne
925
947
  */
926
948
  declare function createRouterFromEnv<const P extends Record<string, string> = Record<never, string>>(options: CreateRouterFromEnvOptions<P>): ServiceRouter<Extract<keyof P, string>>;
927
949
 
928
- export { BASE_MAINNET_NETWORK, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, type CreateRouterFromEnvOptions, DEFAULT_SOLANA_FACILITATOR_URL, DEFAULT_TEMPO_RPC_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 };
950
+ export { BASE_MAINNET_NETWORK, BASE_USDC_ADDRESS, BASE_USDC_DECIMALS, type CheckoutSessionContext, type CheckoutSessionFn, type CheckoutSessionResponse, type CreateRouterFromEnvOptions, DEFAULT_SOLANA_FACILITATOR_URL, DEFAULT_TEMPO_RPC_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 };
package/dist/index.js CHANGED
@@ -1165,7 +1165,7 @@ async function trySiwxFastPath(ctx, account) {
1165
1165
  function shouldParseBodyEarly(incomingStrategy, routeEntry, pricing) {
1166
1166
  if (incomingStrategy) return false;
1167
1167
  if (!routeEntry.bodySchema) return false;
1168
- return (pricing?.needsBody ?? false) || !!routeEntry.validateFn;
1168
+ return (pricing?.needsBody ?? false) || !!routeEntry.validateFn || !!routeEntry.checkoutSession;
1169
1169
  }
1170
1170
 
1171
1171
  // src/pipeline/steps/resolve-early-body.ts
@@ -2064,6 +2064,7 @@ function invalidPaymentVerification(failure) {
2064
2064
  }
2065
2065
 
2066
2066
  // src/protocols/x402/strategy.ts
2067
+ var SETTLE_RETRY_DELAYS_MS = [500, 1e3];
2067
2068
  function formatVerifyFailureMessage(failure) {
2068
2069
  if (failure.reason === "permit2_allowance_required") {
2069
2070
  const wallet = failure.payer ?? "<the payer wallet>";
@@ -2154,7 +2155,17 @@ async function settleX402(args) {
2154
2155
  const { payload, requirements } = token;
2155
2156
  const override = routeEntry.billing === "exact" ? void 0 : { amount: billedAmount };
2156
2157
  try {
2157
- const settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2158
+ let settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2159
+ for (let attempt = 0; !settle.result?.success && attempt < SETTLE_RETRY_DELAYS_MS.length; attempt++) {
2160
+ report("warn", "Retrying x402 settlement", {
2161
+ attempt: attempt + 1,
2162
+ errorReason: settle.result?.errorReason
2163
+ });
2164
+ await new Promise((resolve) => {
2165
+ setTimeout(resolve, SETTLE_RETRY_DELAYS_MS[attempt]);
2166
+ });
2167
+ settle = await settleX402Payment(deps.x402Server, payload, requirements, override);
2168
+ }
2158
2169
  if (!settle.result?.success) {
2159
2170
  throw Object.assign(
2160
2171
  new Error(settle.result?.errorReason ?? "x402 settlement returned success=false"),
@@ -2496,30 +2507,37 @@ async function buildChallengeResponse(ctx, pricing, body, failure) {
2496
2507
  return errorResponse;
2497
2508
  }
2498
2509
  const extensions = await buildChallengeExtensions(ctx);
2499
- const responseBody = failure ? JSON.stringify({ error: failure.message ?? null, reason: failure.reason }) : null;
2500
- const response = new NextResponse5(responseBody, {
2501
- status: 402,
2502
- headers: {
2503
- "Content-Type": "application/json",
2504
- "Cache-Control": "no-store"
2505
- }
2506
- });
2507
- for (const strategy of getAllowedStrategies(ctx.routeEntry.protocols)) {
2510
+ let checkoutSession;
2511
+ if (ctx.routeEntry.checkoutSession) {
2508
2512
  try {
2509
- const contribution = await strategy.buildChallenge({
2513
+ checkoutSession = await ctx.routeEntry.checkoutSession({
2510
2514
  request: ctx.request,
2511
- routeEntry: ctx.routeEntry,
2515
+ route: ctx.routeEntry.key,
2512
2516
  body,
2513
- price: challengePrice,
2514
- extensions,
2515
- deps: ctx.deps,
2516
- report: ctx.report
2517
+ price: challengePrice
2517
2518
  });
2518
- if (contribution.headers) {
2519
- for (const [name, value] of Object.entries(contribution.headers)) {
2520
- response.headers.set(name, value);
2521
- }
2522
- }
2519
+ } catch (err) {
2520
+ const message = errorMessage(err, "Checkout session build failed");
2521
+ const responseBody2 = { success: false, error: message };
2522
+ const errorResponse = NextResponse5.json(responseBody2, { status: errorStatus(err, 500) });
2523
+ firePluginResponse(ctx, errorResponse, body, responseBody2, { message, cause: err });
2524
+ return errorResponse;
2525
+ }
2526
+ }
2527
+ const contributions = [];
2528
+ for (const strategy of getAllowedStrategies(ctx.routeEntry.protocols)) {
2529
+ try {
2530
+ contributions.push(
2531
+ await strategy.buildChallenge({
2532
+ request: ctx.request,
2533
+ routeEntry: ctx.routeEntry,
2534
+ body,
2535
+ price: challengePrice,
2536
+ extensions,
2537
+ deps: ctx.deps,
2538
+ report: ctx.report
2539
+ })
2540
+ );
2523
2541
  } catch (err) {
2524
2542
  const message = `${strategy.protocol} challenge build failed: ${errorMessage(err, String(err))}`;
2525
2543
  ctx.report("critical", message);
@@ -2531,6 +2549,26 @@ async function buildChallengeResponse(ctx, pricing, body, failure) {
2531
2549
  }
2532
2550
  }
2533
2551
  }
2552
+ const responsePayload = {
2553
+ ...Object.assign({}, ...contributions.map((contribution) => contribution.body ?? {})),
2554
+ ...failure && { error: failure.message ?? null, reason: failure.reason },
2555
+ ...checkoutSession && { checkout_session: checkoutSession }
2556
+ };
2557
+ const responseBody = Object.keys(responsePayload).length ? JSON.stringify(responsePayload) : null;
2558
+ const response = new NextResponse5(responseBody, {
2559
+ status: 402,
2560
+ headers: {
2561
+ "Content-Type": "application/json",
2562
+ "Cache-Control": "no-store"
2563
+ }
2564
+ });
2565
+ for (const contribution of contributions) {
2566
+ if (contribution.headers) {
2567
+ for (const [name, value] of Object.entries(contribution.headers)) {
2568
+ response.headers.set(name, value);
2569
+ }
2570
+ }
2571
+ }
2534
2572
  firePluginResponse(ctx, response);
2535
2573
  return response;
2536
2574
  }
@@ -3548,7 +3586,9 @@ var RouteBuilder = class _RouteBuilder {
3548
3586
  providerConfig: void 0,
3549
3587
  validateFn: void 0,
3550
3588
  settlement: void 0,
3551
- mppInfo: void 0
3589
+ mppInfo: void 0,
3590
+ hasCheckout: false,
3591
+ checkoutSession: void 0
3552
3592
  };
3553
3593
  }
3554
3594
  fork() {
@@ -3639,6 +3679,8 @@ var RouteBuilder = class _RouteBuilder {
3639
3679
  if (resolvedOptions.minPrice) next.#s.minPrice = resolvedOptions.minPrice;
3640
3680
  if (resolvedOptions.payTo) next.#s.payTo = resolvedOptions.payTo;
3641
3681
  if (resolvedOptions.mpp) next.#s.mppInfo = resolvedOptions.mpp;
3682
+ if (resolvedOptions.checkout || resolvedOptions.checkoutSession) next.#s.hasCheckout = true;
3683
+ if (resolvedOptions.checkoutSession) next.#s.checkoutSession = resolvedOptions.checkoutSession;
3642
3684
  next.#s.billing = billing;
3643
3685
  if (tickCost) next.#s.tickCost = tickCost;
3644
3686
  if (unitType) next.#s.unitType = unitType;
@@ -4072,6 +4114,8 @@ var RouteBuilder = class _RouteBuilder {
4072
4114
  validateFn: this.#s.validateFn,
4073
4115
  settlement: this.#s.settlement,
4074
4116
  mppInfo: this.#s.mppInfo,
4117
+ hasCheckout: this.#s.hasCheckout ? true : void 0,
4118
+ checkoutSession: this.#s.checkoutSession,
4075
4119
  tickCost: this.#s.tickCost,
4076
4120
  unitType: this.#s.unitType
4077
4121
  };
@@ -4278,6 +4322,7 @@ function buildOperation(routeKey, entry, tag) {
4278
4322
  const requiresSiwxScheme = entry.authMode === "siwx" || Boolean(entry.siwxEnabled);
4279
4323
  const requiresApiKeyScheme = Boolean(entry.apiKeyResolver) && entry.authMode !== "siwx";
4280
4324
  const pricingInfo = buildPricingInfo(entry);
4325
+ const hasCheckout = entry.hasCheckout === true;
4281
4326
  const operation = {
4282
4327
  operationId: routeKey.replace(/\//g, "_"),
4283
4328
  summary: entry.description ?? routeKey,
@@ -4303,10 +4348,11 @@ function buildOperation(routeKey, entry, tag) {
4303
4348
  }
4304
4349
  }
4305
4350
  };
4306
- if (paymentRequired && (pricingInfo || protocols)) {
4351
+ if (paymentRequired && (pricingInfo || protocols || hasCheckout)) {
4307
4352
  operation["x-payment-info"] = {
4308
4353
  ...pricingInfo ?? {},
4309
- ...protocols && { protocols }
4354
+ ...protocols && { protocols },
4355
+ ...hasCheckout && { has_checkout: true }
4310
4356
  };
4311
4357
  }
4312
4358
  if (requiresSiwxScheme) {
@@ -4417,6 +4463,33 @@ function createLlmsTxtHandler(discovery) {
4417
4463
  };
4418
4464
  }
4419
4465
 
4466
+ // src/discovery/not-found.ts
4467
+ import { NextResponse as NextResponse12 } from "next/server";
4468
+ function createNotFoundHandler(baseUrl) {
4469
+ const normalizedBase = baseUrl.replace(/\/+$/, "");
4470
+ return async (request) => NextResponse12.json(
4471
+ {
4472
+ success: false,
4473
+ code: "route_not_found",
4474
+ error: "Route not found. Rediscover this origin and retry with the current discovery document.",
4475
+ requestedUrl: request.url,
4476
+ discovery: {
4477
+ openapi: `${normalizedBase}/openapi.json`,
4478
+ wellKnown: `${normalizedBase}/.well-known/x402`,
4479
+ llmsTxt: `${normalizedBase}/llms.txt`
4480
+ }
4481
+ },
4482
+ {
4483
+ status: 404,
4484
+ headers: {
4485
+ "Access-Control-Allow-Origin": "*",
4486
+ "Access-Control-Allow-Methods": "GET, POST, DELETE, PUT, PATCH, OPTIONS",
4487
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-API-Key, SIGN-IN-WITH-X"
4488
+ }
4489
+ }
4490
+ );
4491
+ }
4492
+
4420
4493
  // src/index.ts
4421
4494
  init_accepts();
4422
4495
  init_constants();
@@ -5120,6 +5193,9 @@ function createRouter(config) {
5120
5193
  llmsTxt() {
5121
5194
  return createLlmsTxtHandler(config.discovery);
5122
5195
  },
5196
+ notFound() {
5197
+ return createNotFoundHandler(resolvedBaseUrl);
5198
+ },
5123
5199
  monitors() {
5124
5200
  const result = [];
5125
5201
  for (const [, entry] of registry.entries()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcash/router",
3
- "version": "1.10.4",
3
+ "version": "1.10.6",
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": {