@agentcash/router 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -420,7 +420,7 @@ import { NextResponse as NextResponse2 } from "next/server";
420
420
 
421
421
  // src/auth/normalize-wallet.ts
422
422
  function normalizeWalletAddress(address) {
423
- return address.startsWith("0x") ? address.toLowerCase() : address;
423
+ return /^0x/i.test(address) ? address.toLowerCase() : address;
424
424
  }
425
425
 
426
426
  // src/plugin.ts
@@ -585,12 +585,13 @@ var HttpError = class extends Error {
585
585
  };
586
586
 
587
587
  // src/handler.ts
588
- async function safeCallHandler(handler, ctx) {
588
+ async function safeCallHandler(handler, ctx, options = {}) {
589
589
  try {
590
590
  const result = await handler(ctx);
591
591
  if (result instanceof Response) return result;
592
592
  return NextResponse.json(result);
593
593
  } catch (error) {
594
+ options.onError?.(error);
594
595
  const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
595
596
  const message = error instanceof Error ? error.message : "Internal error";
596
597
  return NextResponse.json({ success: false, error: message }, { status });
@@ -713,6 +714,9 @@ async function verifyX402Payment(opts) {
713
714
  throw err;
714
715
  }
715
716
  if (!verify.isValid) return invalidPaymentVerification();
717
+ if (typeof verify.payer !== "string" || verify.payer.length === 0) {
718
+ throw new Error("x402 verification succeeded without a payer address");
719
+ }
716
720
  return {
717
721
  valid: true,
718
722
  payer: verify.payer,
@@ -1007,6 +1011,17 @@ function getRequirementRecipient(requirements) {
1007
1011
  const payTo = requirements?.payTo;
1008
1012
  return typeof payTo === "string" ? payTo : void 0;
1009
1013
  }
1014
+ function errorStatus(error, fallback) {
1015
+ const status = error?.status;
1016
+ return typeof status === "number" ? status : fallback;
1017
+ }
1018
+ function errorMessage(error, fallback) {
1019
+ return error instanceof Error ? error.message : fallback;
1020
+ }
1021
+ function handlerFailureError(response) {
1022
+ const message = response.statusText || `Handler returned HTTP ${response.status}`;
1023
+ return Object.assign(new Error(message), { status: response.status });
1024
+ }
1010
1025
  function siwxSignatureType(network) {
1011
1026
  return network.startsWith("solana:") ? "ed25519" : "eip191";
1012
1027
  }
@@ -1046,11 +1061,20 @@ function createRequestHandler(routeEntry, handler, deps) {
1046
1061
  setVerifiedWallet: (addr) => pluginCtx.setVerifiedWallet(addr)
1047
1062
  };
1048
1063
  let rawResult;
1049
- const response = await safeCallHandler(async (c) => {
1050
- rawResult = await handler(c);
1051
- return rawResult;
1052
- }, ctx);
1053
- return { response, rawResult };
1064
+ let handlerError;
1065
+ const response = await safeCallHandler(
1066
+ async (c) => {
1067
+ rawResult = await handler(c);
1068
+ return rawResult;
1069
+ },
1070
+ ctx,
1071
+ {
1072
+ onError(error) {
1073
+ handlerError = error;
1074
+ }
1075
+ }
1076
+ );
1077
+ return { response, rawResult, handlerError };
1054
1078
  }
1055
1079
  function finalize(response, rawResult, meta, pluginCtx, requestBody) {
1056
1080
  fireProviderQuota(routeEntry, response, rawResult, deps, pluginCtx);
@@ -1061,6 +1085,87 @@ function createRequestHandler(routeEntry, handler, deps) {
1061
1085
  firePluginResponse(deps, pluginCtx, meta, response, requestBody);
1062
1086
  return response;
1063
1087
  }
1088
+ function settlementContext(scope) {
1089
+ return {
1090
+ route: routeEntry.key,
1091
+ request: scope.request,
1092
+ body: scope.parsedBody,
1093
+ wallet: scope.wallet,
1094
+ account: scope.account,
1095
+ payment: scope.payment,
1096
+ response: scope.response,
1097
+ result: scope.rawResult
1098
+ };
1099
+ }
1100
+ async function runBeforeSettle(scope) {
1101
+ const hook = routeEntry.settlement?.beforeSettle;
1102
+ if (!hook) return null;
1103
+ try {
1104
+ await hook(settlementContext(scope));
1105
+ return null;
1106
+ } catch (error) {
1107
+ return fail(
1108
+ errorStatus(error, 500),
1109
+ errorMessage(error, "Pre-settlement validation failed"),
1110
+ scope.meta,
1111
+ scope.pluginCtx,
1112
+ scope.parsedBody
1113
+ );
1114
+ }
1115
+ }
1116
+ async function runSettlementError(scope, error, phase) {
1117
+ const hook = routeEntry.settlement?.onSettlementError;
1118
+ if (!hook) return;
1119
+ try {
1120
+ await hook({
1121
+ ...settlementContext(scope),
1122
+ error,
1123
+ phase
1124
+ });
1125
+ } catch (hookError) {
1126
+ const message = errorMessage(hookError, "Settlement error hook failed");
1127
+ console.error(`[router] ${routeEntry.key}: onSettlementError failed: ${message}`);
1128
+ firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
1129
+ level: "error",
1130
+ message: `Settlement error hook failed: ${message}`,
1131
+ route: routeEntry.key
1132
+ });
1133
+ }
1134
+ }
1135
+ async function runAfterSettle(scope) {
1136
+ const hook = routeEntry.settlement?.afterSettle;
1137
+ if (!hook) return;
1138
+ try {
1139
+ await hook(settlementContext(scope));
1140
+ } catch (error) {
1141
+ const message = errorMessage(error, "Post-settlement hook failed");
1142
+ console.error(`[router] ${routeEntry.key}: afterSettle failed: ${message}`);
1143
+ firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
1144
+ level: "error",
1145
+ message: `Post-settlement hook failed: ${message}`,
1146
+ route: routeEntry.key
1147
+ });
1148
+ await runSettlementError(scope, error, "afterSettle");
1149
+ }
1150
+ }
1151
+ async function runSettledHandlerError(scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
1152
+ const hook = routeEntry.settlement?.onSettledHandlerError;
1153
+ if (!hook) return;
1154
+ try {
1155
+ await hook({
1156
+ ...settlementContext(scope),
1157
+ error
1158
+ });
1159
+ } catch (hookError) {
1160
+ const message = errorMessage(hookError, "Settled handler error hook failed");
1161
+ console.error(`[router] ${routeEntry.key}: onSettledHandlerError failed: ${message}`);
1162
+ firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
1163
+ level: "error",
1164
+ message: `Settled handler error hook failed: ${message}`,
1165
+ route: routeEntry.key
1166
+ });
1167
+ }
1168
+ }
1064
1169
  return async (request) => {
1065
1170
  await deps.initPromise;
1066
1171
  const meta = buildMeta(request, routeEntry);
@@ -1316,18 +1421,9 @@ function createRequestHandler(routeEntry, handler, deps) {
1316
1421
  return fail(status, message, meta, pluginCtx, body.data);
1317
1422
  }
1318
1423
  }
1319
- let price;
1320
- try {
1321
- price = await resolvePrice(routeEntry.pricing, body.data);
1322
- } catch (err) {
1323
- return fail(
1324
- err.status ?? 500,
1325
- err instanceof Error ? err.message : "Price resolution failed",
1326
- meta,
1327
- pluginCtx,
1328
- body.data
1329
- );
1330
- }
1424
+ const priceResult = await resolveDynamicPrice(body.data, routeEntry, deps, pluginCtx, meta);
1425
+ if ("error" in priceResult) return priceResult.error;
1426
+ const price = priceResult.price;
1331
1427
  if (!routeEntry.protocols.includes(protocol)) {
1332
1428
  const accepted = routeEntry.protocols.join(", ") || "none";
1333
1429
  console.warn(
@@ -1381,7 +1477,7 @@ function createRequestHandler(routeEntry, handler, deps) {
1381
1477
  amount: price,
1382
1478
  network: matchedNetwork
1383
1479
  });
1384
- const { response, rawResult } = await invoke(
1480
+ const { response, rawResult, handlerError } = await invoke(
1385
1481
  request,
1386
1482
  meta,
1387
1483
  pluginCtx,
@@ -1390,7 +1486,21 @@ function createRequestHandler(routeEntry, handler, deps) {
1390
1486
  body.data,
1391
1487
  payment
1392
1488
  );
1489
+ const settleScope = {
1490
+ request,
1491
+ meta,
1492
+ pluginCtx,
1493
+ wallet,
1494
+ account,
1495
+ parsedBody: body.data,
1496
+ payment,
1497
+ response,
1498
+ rawResult,
1499
+ handlerError
1500
+ };
1393
1501
  if (response.status < 400) {
1502
+ const validationFailure = await runBeforeSettle(settleScope);
1503
+ if (validationFailure) return validationFailure;
1394
1504
  try {
1395
1505
  const settle = await settleX402Payment(
1396
1506
  deps.x402Server,
@@ -1416,13 +1526,21 @@ function createRequestHandler(routeEntry, handler, deps) {
1416
1526
  }
1417
1527
  response.headers.set("PAYMENT-RESPONSE", settle.encoded);
1418
1528
  response.headers.set("Cache-Control", "private");
1529
+ const transaction = String(settle.result?.transaction ?? "");
1530
+ const settledPayment = {
1531
+ ...payment,
1532
+ status: "settled",
1533
+ ...transaction ? { transaction } : {}
1534
+ };
1419
1535
  firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
1420
1536
  protocol: "x402",
1421
- payer: verify.payer,
1422
- transaction: String(settle.result?.transaction ?? ""),
1537
+ payer: wallet,
1538
+ transaction,
1423
1539
  network: matchedNetwork
1424
1540
  });
1541
+ await runAfterSettle({ ...settleScope, payment: settledPayment });
1425
1542
  } catch (err) {
1543
+ await runSettlementError(settleScope, err, "settle");
1426
1544
  const errObj = err;
1427
1545
  console.error("Settlement failed", {
1428
1546
  message: err instanceof Error ? err.message : String(err),
@@ -1481,27 +1599,44 @@ function createRequestHandler(routeEntry, handler, deps) {
1481
1599
  amount: price,
1482
1600
  network: "tempo:4217"
1483
1601
  });
1484
- const { response: response2, rawResult: rawResult2 } = await invoke(
1602
+ const mppRecipient2 = deps.mppRecipient ?? deps.payeeAddress;
1603
+ const payment2 = {
1604
+ protocol: "mpp",
1605
+ status: "verified",
1606
+ payer: wallet,
1607
+ amount: price,
1608
+ network: "tempo:4217",
1609
+ ...mppRecipient2 ? { recipient: mppRecipient2 } : {}
1610
+ };
1611
+ const { response: response2, rawResult: rawResult2, handlerError: handlerError2 } = await invoke(
1485
1612
  request,
1486
1613
  meta,
1487
1614
  pluginCtx,
1488
1615
  wallet,
1489
1616
  account,
1490
1617
  body.data,
1491
- {
1492
- protocol: "mpp",
1493
- status: "verified",
1494
- payer: wallet,
1495
- amount: price,
1496
- network: "tempo:4217",
1497
- recipient: deps.payeeAddress
1498
- }
1618
+ payment2
1499
1619
  );
1620
+ const settleScope2 = {
1621
+ request,
1622
+ meta,
1623
+ pluginCtx,
1624
+ wallet,
1625
+ account,
1626
+ parsedBody: body.data,
1627
+ payment: payment2,
1628
+ response: response2,
1629
+ rawResult: rawResult2,
1630
+ handlerError: handlerError2
1631
+ };
1500
1632
  if (response2.status < 400) {
1633
+ const validationFailure = await runBeforeSettle(settleScope2);
1634
+ if (validationFailure) return validationFailure;
1501
1635
  let mppResult2;
1502
1636
  try {
1503
1637
  mppResult2 = await deps.mppx.charge({ amount: price })(request);
1504
1638
  } catch (err) {
1639
+ await runSettlementError(settleScope2, err, "settle");
1505
1640
  const message = err instanceof Error ? err.message : String(err);
1506
1641
  console.error(
1507
1642
  `[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`
@@ -1530,6 +1665,13 @@ function createRequestHandler(routeEntry, handler, deps) {
1530
1665
  } catch {
1531
1666
  }
1532
1667
  const detail = rejectReason || "transaction reverted on-chain after handler execution";
1668
+ const settlementError = Object.assign(new Error(detail), {
1669
+ status: 402,
1670
+ detail,
1671
+ mppResult: mppResult2,
1672
+ challenge: mppResult2.challenge
1673
+ });
1674
+ await runSettlementError(settleScope2, settlementError, "settle");
1533
1675
  console.error(
1534
1676
  `[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`
1535
1677
  );
@@ -1567,6 +1709,17 @@ function createRequestHandler(routeEntry, handler, deps) {
1567
1709
  transaction: txHash2,
1568
1710
  network: "tempo:4217"
1569
1711
  });
1712
+ const settledPayment = {
1713
+ ...payment2,
1714
+ status: "settled",
1715
+ ...txHash2 ? { transaction: txHash2 } : {},
1716
+ ...receiptHeader2 ? { receipt: receiptHeader2 } : {}
1717
+ };
1718
+ await runAfterSettle({
1719
+ ...settleScope2,
1720
+ payment: settledPayment,
1721
+ response: receiptResponse
1722
+ });
1570
1723
  finalize(receiptResponse, rawResult2, meta, pluginCtx, body.data);
1571
1724
  return receiptResponse;
1572
1725
  }
@@ -1622,24 +1775,38 @@ function createRequestHandler(routeEntry, handler, deps) {
1622
1775
  amount: price,
1623
1776
  network: "tempo:4217"
1624
1777
  });
1625
- const { response, rawResult } = await invoke(
1778
+ const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
1779
+ const payment = {
1780
+ protocol: "mpp",
1781
+ status: "settled",
1782
+ payer: wallet,
1783
+ amount: price,
1784
+ network: "tempo:4217",
1785
+ ...mppRecipient ? { recipient: mppRecipient } : {},
1786
+ ...txHash ? { transaction: txHash } : {},
1787
+ ...receiptHeader ? { receipt: receiptHeader } : {}
1788
+ };
1789
+ const { response, rawResult, handlerError } = await invoke(
1626
1790
  request,
1627
1791
  meta,
1628
1792
  pluginCtx,
1629
1793
  wallet,
1630
1794
  account,
1631
1795
  body.data,
1632
- {
1633
- protocol: "mpp",
1634
- status: "settled",
1635
- payer: wallet,
1636
- amount: price,
1637
- network: "tempo:4217",
1638
- recipient: deps.payeeAddress,
1639
- ...txHash ? { transaction: txHash } : {},
1640
- ...receiptHeader ? { receipt: receiptHeader } : {}
1641
- }
1796
+ payment
1642
1797
  );
1798
+ const settleScope = {
1799
+ request,
1800
+ meta,
1801
+ pluginCtx,
1802
+ wallet,
1803
+ account,
1804
+ parsedBody: body.data,
1805
+ payment,
1806
+ response,
1807
+ rawResult,
1808
+ handlerError
1809
+ };
1643
1810
  if (response.status < 400) {
1644
1811
  if (routeEntry.siwxEnabled) {
1645
1812
  try {
@@ -1660,9 +1827,11 @@ function createRequestHandler(routeEntry, handler, deps) {
1660
1827
  transaction: txHash,
1661
1828
  network: "tempo:4217"
1662
1829
  });
1830
+ await runAfterSettle({ ...settleScope, response: receiptResponse });
1663
1831
  finalize(receiptResponse, rawResult, meta, pluginCtx, body.data);
1664
1832
  return receiptResponse;
1665
1833
  }
1834
+ await runSettledHandlerError(settleScope);
1666
1835
  finalize(response, rawResult, meta, pluginCtx, body.data);
1667
1836
  return response;
1668
1837
  }
@@ -1735,9 +1904,10 @@ async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta)
1735
1904
  });
1736
1905
  return { price: routeEntry.maxPrice };
1737
1906
  } else {
1907
+ const message = errorMessage(err, "Price calculation failed");
1738
1908
  const errorResponse = NextResponse2.json(
1739
- { success: false, error: "Price calculation failed" },
1740
- { status: 500 }
1909
+ { success: false, error: message },
1910
+ { status: errorStatus(err, 500) }
1741
1911
  );
1742
1912
  firePluginResponse(deps, pluginCtx, meta, errorResponse);
1743
1913
  return { error: errorResponse };
@@ -1910,22 +2080,13 @@ function fireProviderQuota(routeEntry, response, handlerResult, deps, pluginCtx)
1910
2080
 
1911
2081
  // src/validate-examples.ts
1912
2082
  function validateExamples(key, bodySchema, querySchema, outputSchema, inputExample, hasInputExample, outputExample, hasOutputExample) {
1913
- if (bodySchema && !hasInputExample) {
1914
- throw new Error(
1915
- `route '${key}': .body() requires a matching .inputExample() \u2014 the bazaar discovery extension needs a conforming sample body to advertise.`
1916
- );
1917
- }
1918
- if (querySchema && !hasInputExample) {
1919
- throw new Error(
1920
- `route '${key}': .query() requires a matching .inputExample() \u2014 the bazaar discovery extension needs a conforming sample query to advertise.`
1921
- );
2083
+ const inputSchema = bodySchema ?? querySchema;
2084
+ if (hasInputExample && !inputSchema) {
2085
+ throw new Error(`route '${key}': .inputExample() requires .body() or .query()`);
1922
2086
  }
1923
- if (outputSchema && !hasOutputExample) {
1924
- throw new Error(
1925
- `route '${key}': .output() requires a matching .outputExample() \u2014 the bazaar discovery extension needs a conforming sample response to advertise.`
1926
- );
2087
+ if (hasOutputExample && !outputSchema) {
2088
+ throw new Error(`route '${key}': .outputExample() requires .output()`);
1927
2089
  }
1928
- const inputSchema = bodySchema ?? querySchema;
1929
2090
  if (inputSchema && hasInputExample) {
1930
2091
  const result = inputSchema.safeParse(inputExample);
1931
2092
  if (!result.success) {
@@ -1999,6 +2160,8 @@ var RouteBuilder = class {
1999
2160
  /** @internal */
2000
2161
  _validateFn;
2001
2162
  /** @internal */
2163
+ _settlement;
2164
+ /** @internal */
2002
2165
  _mppInfo;
2003
2166
  constructor(key, registry, deps) {
2004
2167
  this._key = key;
@@ -2012,11 +2175,21 @@ var RouteBuilder = class {
2012
2175
  return next;
2013
2176
  }
2014
2177
  paid(pricing, options) {
2178
+ if (this._authMode === "unprotected") {
2179
+ throw new Error(
2180
+ `route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
2181
+ );
2182
+ }
2183
+ if (this._pricing !== void 0) {
2184
+ throw new Error(
2185
+ `route '${this._key}': Cannot call .paid() more than once on the same route.`
2186
+ );
2187
+ }
2015
2188
  const next = this.fork();
2016
2189
  next._authMode = "paid";
2017
2190
  next._pricing = pricing;
2018
2191
  if (options?.protocols) {
2019
- next._protocols = options.protocols;
2192
+ next._protocols = [...options.protocols];
2020
2193
  } else if (next._protocols.length === 0) {
2021
2194
  next._protocols = ["x402"];
2022
2195
  }
@@ -2081,6 +2254,16 @@ var RouteBuilder = class {
2081
2254
  return next;
2082
2255
  }
2083
2256
  unprotected() {
2257
+ if (this._authMode && this._authMode !== "unprotected") {
2258
+ throw new Error(
2259
+ `route '${this._key}': Cannot combine .unprotected() and .${this._authMode}() on the same route.`
2260
+ );
2261
+ }
2262
+ if (this._pricing) {
2263
+ throw new Error(
2264
+ `route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
2265
+ );
2266
+ }
2084
2267
  const next = this.fork();
2085
2268
  next._authMode = "unprotected";
2086
2269
  next._protocols = [];
@@ -2095,32 +2278,43 @@ var RouteBuilder = class {
2095
2278
  next._providerConfig = config ?? {};
2096
2279
  return next;
2097
2280
  }
2098
- // -------------------------------------------------------------------------
2099
- // Schema methods
2100
- // -------------------------------------------------------------------------
2101
- body(schema) {
2281
+ body(schema, example) {
2102
2282
  const next = this.fork();
2103
2283
  next._bodySchema = schema;
2284
+ if (example !== void 0) {
2285
+ next._inputExample = example;
2286
+ next._hasInputExample = true;
2287
+ }
2104
2288
  return next;
2105
2289
  }
2106
- query(schema) {
2290
+ query(schema, example) {
2107
2291
  const next = this.fork();
2108
2292
  next._querySchema = schema;
2293
+ if (example !== void 0) {
2294
+ next._inputExample = example;
2295
+ next._hasInputExample = true;
2296
+ }
2109
2297
  next._method = "GET";
2110
2298
  return next;
2111
2299
  }
2112
- output(schema) {
2300
+ output(schema, example) {
2113
2301
  const next = this.fork();
2114
2302
  next._outputSchema = schema;
2303
+ if (example !== void 0) {
2304
+ next._outputExample = example;
2305
+ next._hasOutputExample = true;
2306
+ }
2115
2307
  return next;
2116
2308
  }
2117
2309
  /**
2118
2310
  * Provide a conforming example of the request input (body or query params).
2119
2311
  *
2120
- * **Required** whenever `.body()` or `.query()` is set enforced at compile time via
2121
- * `.handler()` overloads, and at route-registration time via Zod validation of the
2122
- * example against the schema. The example is embedded in the bazaar discovery extension
2123
- * so indexers can advertise a working sample call.
2312
+ * Optional. When provided, the example is validated against the request schema
2313
+ * at route registration and embedded in the bazaar discovery extension so
2314
+ * indexers can advertise a working sample call.
2315
+ *
2316
+ * For the common case, pass the example directly to `.body(schema, example)` or
2317
+ * `.query(schema, example)` instead.
2124
2318
  *
2125
2319
  * @example
2126
2320
  * ```ts
@@ -2140,10 +2334,11 @@ var RouteBuilder = class {
2140
2334
  /**
2141
2335
  * Provide a conforming example of the response output.
2142
2336
  *
2143
- * **Required** whenever `.output()` is set enforced at compile time via `.handler()`
2144
- * overloads, and at route-registration time via Zod validation of the example against
2145
- * the schema. The example is embedded in the bazaar discovery extension so indexers
2146
- * can advertise the response shape.
2337
+ * Optional. When provided, the example is validated against the output schema
2338
+ * at route registration and embedded in the bazaar discovery extension so
2339
+ * indexers can advertise the response shape.
2340
+ *
2341
+ * For the common case, pass the example directly to `.output(schema, example)` instead.
2147
2342
  *
2148
2343
  * Accepts any JSON value (objects, arrays, or primitives) — top-level array
2149
2344
  * or primitive responses (e.g. `z.array(...)`) are supported alongside the
@@ -2216,15 +2411,39 @@ var RouteBuilder = class {
2216
2411
  return next;
2217
2412
  }
2218
2413
  // -------------------------------------------------------------------------
2414
+ // Settlement lifecycle
2415
+ // -------------------------------------------------------------------------
2416
+ /**
2417
+ * Add route-specific settlement hooks.
2418
+ *
2419
+ * `beforeSettle` runs after a successful handler response but before
2420
+ * router-controlled settlement/broadcast, so it can still prevent the charge
2421
+ * for x402 and MPP transaction-payload flows. `afterSettle` runs after
2422
+ * settlement and is intended for durable ledgers or app-owned refund queues.
2423
+ */
2424
+ settlement(lifecycle) {
2425
+ const next = this.fork();
2426
+ next._settlement = lifecycle;
2427
+ return next;
2428
+ }
2429
+ // -------------------------------------------------------------------------
2219
2430
  // Terminal method
2220
2431
  // -------------------------------------------------------------------------
2221
2432
  handler(fn) {
2222
2433
  const handlerFn = fn;
2434
+ if (!this._authMode) {
2435
+ throw new Error(
2436
+ `route '${this._key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
2437
+ );
2438
+ }
2223
2439
  if (this._validateFn && !this._bodySchema) {
2224
2440
  throw new Error(
2225
2441
  `route '${this._key}': .validate() requires .body() \u2014 validation runs on parsed body`
2226
2442
  );
2227
2443
  }
2444
+ if (this._settlement && !this._pricing) {
2445
+ throw new Error(`route '${this._key}': .settlement() requires a paid route`);
2446
+ }
2228
2447
  validateExamples(
2229
2448
  this._key,
2230
2449
  this._bodySchema,
@@ -2256,6 +2475,7 @@ var RouteBuilder = class {
2256
2475
  providerName: this._providerName,
2257
2476
  providerConfig: this._providerConfig,
2258
2477
  validateFn: this._validateFn,
2478
+ settlement: this._settlement,
2259
2479
  mppInfo: this._mppInfo
2260
2480
  };
2261
2481
  this._registry.register(entry);
@@ -2391,6 +2611,7 @@ function toDiscoveryResource(method, url, mode) {
2391
2611
  }
2392
2612
 
2393
2613
  // src/discovery/openapi.ts
2614
+ init_constants();
2394
2615
  import { NextResponse as NextResponse4 } from "next/server";
2395
2616
  function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2396
2617
  const normalizedBase = baseUrl.replace(/\/+$/, "");
@@ -2533,7 +2754,7 @@ function toProtocolObject(protocol, mppInfo) {
2533
2754
  mpp: {
2534
2755
  method: mppInfo?.method ?? "tempo",
2535
2756
  intent: mppInfo?.intent ?? "charge",
2536
- currency: mppInfo?.currency ?? "0x20c0000000000000000000000000000000000001"
2757
+ currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
2537
2758
  }
2538
2759
  };
2539
2760
  }
@@ -2648,6 +2869,8 @@ function mppFromEnv(env, options = {}) {
2648
2869
  const secretKey = env.MPP_SECRET_KEY;
2649
2870
  const currency = env.MPP_CURRENCY;
2650
2871
  const rpcUrl = env.TEMPO_RPC_URL;
2872
+ const feePayerKey = options.feePayerKey ?? env.MPP_FEE_PAYER_KEY;
2873
+ const feePayerKeySource = options.feePayerKey !== void 0 ? "feePayerKey" : "MPP_FEE_PAYER_KEY";
2651
2874
  const hasAnyMppEnv = Boolean(secretKey || currency || rpcUrl || options.require);
2652
2875
  if (!hasAnyMppEnv) return void 0;
2653
2876
  const missing = [
@@ -2658,12 +2881,21 @@ function mppFromEnv(env, options = {}) {
2658
2881
  if (missing.length > 0) {
2659
2882
  throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
2660
2883
  }
2884
+ if (!isEvmAddress(currency)) {
2885
+ throw new Error("MPP_CURRENCY must be a 0x-prefixed 20-byte Tempo currency address");
2886
+ }
2887
+ if (options.recipient && !isEvmAddress(options.recipient)) {
2888
+ throw new Error("MPP recipient must be a 0x-prefixed EVM address");
2889
+ }
2890
+ if (feePayerKey && !isEvmPrivateKey(feePayerKey)) {
2891
+ throw new Error(`${feePayerKeySource} must be a 0x-prefixed 32-byte EVM private key`);
2892
+ }
2661
2893
  return {
2662
2894
  secretKey,
2663
2895
  currency,
2664
2896
  rpcUrl,
2665
2897
  ...options.recipient ? { recipient: options.recipient } : {},
2666
- ...options.feePayerKey ? { feePayerKey: options.feePayerKey } : {},
2898
+ ...feePayerKey ? { feePayerKey } : {},
2667
2899
  ...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
2668
2900
  };
2669
2901
  }
@@ -2801,13 +3033,26 @@ function validateMppConfig(config, env) {
2801
3033
  protocol: "mpp",
2802
3034
  message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
2803
3035
  });
3036
+ } else if (!isEvmAddress(mpp.currency)) {
3037
+ issues.push({
3038
+ code: "invalid_mpp_currency",
3039
+ protocol: "mpp",
3040
+ message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
3041
+ });
2804
3042
  }
2805
- if (!mpp.recipient && !config.payeeAddress) {
3043
+ const mppRecipient = mpp.recipient ?? config.payeeAddress;
3044
+ if (!mppRecipient) {
2806
3045
  issues.push({
2807
3046
  code: "missing_mpp_recipient",
2808
3047
  protocol: "mpp",
2809
3048
  message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
2810
3049
  });
3050
+ } else if (!isEvmAddress(mppRecipient)) {
3051
+ issues.push({
3052
+ code: "invalid_mpp_recipient",
3053
+ protocol: "mpp",
3054
+ message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
3055
+ });
2811
3056
  }
2812
3057
  const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
2813
3058
  if (placeholder) {
@@ -2824,6 +3069,13 @@ function validateMppConfig(config, env) {
2824
3069
  message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
2825
3070
  });
2826
3071
  }
3072
+ if (mpp.feePayerKey && !isEvmPrivateKey(mpp.feePayerKey)) {
3073
+ issues.push({
3074
+ code: "invalid_mpp_fee_payer_key",
3075
+ protocol: "mpp",
3076
+ message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
3077
+ });
3078
+ }
2827
3079
  if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
2828
3080
  issues.push({
2829
3081
  code: "missing_mpp_default_store_env",
@@ -2841,8 +3093,14 @@ function usesDefaultEvmFacilitator(config) {
2841
3093
  function isSupportedX402Network(network) {
2842
3094
  return isEvmNetwork(network) || isSolanaNetwork(network);
2843
3095
  }
3096
+ function isEvmAddress(value) {
3097
+ return /^0x[a-fA-F0-9]{40}$/.test(value);
3098
+ }
3099
+ function isEvmPrivateKey(value) {
3100
+ return /^0x[a-fA-F0-9]{64}$/.test(value);
3101
+ }
2844
3102
  function findPlaceholderPayee(values) {
2845
- return values.find((value) => value?.toLowerCase() === ZERO_EVM_ADDRESS) ?? null;
3103
+ return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
2846
3104
  }
2847
3105
 
2848
3106
  // src/index.ts
@@ -2891,6 +3149,7 @@ function createRouter(config) {
2891
3149
  nonceStore,
2892
3150
  entitlementStore,
2893
3151
  payeeAddress: config.payeeAddress ?? "",
3152
+ mppRecipient: config.mpp?.recipient ?? config.payeeAddress,
2894
3153
  network,
2895
3154
  x402FacilitatorsByNetwork: void 0,
2896
3155
  x402Accepts,