@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.cjs CHANGED
@@ -470,7 +470,7 @@ var import_server2 = require("next/server");
470
470
 
471
471
  // src/auth/normalize-wallet.ts
472
472
  function normalizeWalletAddress(address) {
473
- return address.startsWith("0x") ? address.toLowerCase() : address;
473
+ return /^0x/i.test(address) ? address.toLowerCase() : address;
474
474
  }
475
475
 
476
476
  // src/plugin.ts
@@ -635,12 +635,13 @@ var HttpError = class extends Error {
635
635
  };
636
636
 
637
637
  // src/handler.ts
638
- async function safeCallHandler(handler, ctx) {
638
+ async function safeCallHandler(handler, ctx, options = {}) {
639
639
  try {
640
640
  const result = await handler(ctx);
641
641
  if (result instanceof Response) return result;
642
642
  return import_server.NextResponse.json(result);
643
643
  } catch (error) {
644
+ options.onError?.(error);
644
645
  const status = error instanceof HttpError ? error.status : typeof error.status === "number" ? error.status : 500;
645
646
  const message = error instanceof Error ? error.message : "Internal error";
646
647
  return import_server.NextResponse.json({ success: false, error: message }, { status });
@@ -763,6 +764,9 @@ async function verifyX402Payment(opts) {
763
764
  throw err;
764
765
  }
765
766
  if (!verify.isValid) return invalidPaymentVerification();
767
+ if (typeof verify.payer !== "string" || verify.payer.length === 0) {
768
+ throw new Error("x402 verification succeeded without a payer address");
769
+ }
766
770
  return {
767
771
  valid: true,
768
772
  payer: verify.payer,
@@ -1057,6 +1061,17 @@ function getRequirementRecipient(requirements) {
1057
1061
  const payTo = requirements?.payTo;
1058
1062
  return typeof payTo === "string" ? payTo : void 0;
1059
1063
  }
1064
+ function errorStatus(error, fallback) {
1065
+ const status = error?.status;
1066
+ return typeof status === "number" ? status : fallback;
1067
+ }
1068
+ function errorMessage(error, fallback) {
1069
+ return error instanceof Error ? error.message : fallback;
1070
+ }
1071
+ function handlerFailureError(response) {
1072
+ const message = response.statusText || `Handler returned HTTP ${response.status}`;
1073
+ return Object.assign(new Error(message), { status: response.status });
1074
+ }
1060
1075
  function siwxSignatureType(network) {
1061
1076
  return network.startsWith("solana:") ? "ed25519" : "eip191";
1062
1077
  }
@@ -1096,11 +1111,20 @@ function createRequestHandler(routeEntry, handler, deps) {
1096
1111
  setVerifiedWallet: (addr) => pluginCtx.setVerifiedWallet(addr)
1097
1112
  };
1098
1113
  let rawResult;
1099
- const response = await safeCallHandler(async (c) => {
1100
- rawResult = await handler(c);
1101
- return rawResult;
1102
- }, ctx);
1103
- return { response, rawResult };
1114
+ let handlerError;
1115
+ const response = await safeCallHandler(
1116
+ async (c) => {
1117
+ rawResult = await handler(c);
1118
+ return rawResult;
1119
+ },
1120
+ ctx,
1121
+ {
1122
+ onError(error) {
1123
+ handlerError = error;
1124
+ }
1125
+ }
1126
+ );
1127
+ return { response, rawResult, handlerError };
1104
1128
  }
1105
1129
  function finalize(response, rawResult, meta, pluginCtx, requestBody) {
1106
1130
  fireProviderQuota(routeEntry, response, rawResult, deps, pluginCtx);
@@ -1111,6 +1135,87 @@ function createRequestHandler(routeEntry, handler, deps) {
1111
1135
  firePluginResponse(deps, pluginCtx, meta, response, requestBody);
1112
1136
  return response;
1113
1137
  }
1138
+ function settlementContext(scope) {
1139
+ return {
1140
+ route: routeEntry.key,
1141
+ request: scope.request,
1142
+ body: scope.parsedBody,
1143
+ wallet: scope.wallet,
1144
+ account: scope.account,
1145
+ payment: scope.payment,
1146
+ response: scope.response,
1147
+ result: scope.rawResult
1148
+ };
1149
+ }
1150
+ async function runBeforeSettle(scope) {
1151
+ const hook = routeEntry.settlement?.beforeSettle;
1152
+ if (!hook) return null;
1153
+ try {
1154
+ await hook(settlementContext(scope));
1155
+ return null;
1156
+ } catch (error) {
1157
+ return fail(
1158
+ errorStatus(error, 500),
1159
+ errorMessage(error, "Pre-settlement validation failed"),
1160
+ scope.meta,
1161
+ scope.pluginCtx,
1162
+ scope.parsedBody
1163
+ );
1164
+ }
1165
+ }
1166
+ async function runSettlementError(scope, error, phase) {
1167
+ const hook = routeEntry.settlement?.onSettlementError;
1168
+ if (!hook) return;
1169
+ try {
1170
+ await hook({
1171
+ ...settlementContext(scope),
1172
+ error,
1173
+ phase
1174
+ });
1175
+ } catch (hookError) {
1176
+ const message = errorMessage(hookError, "Settlement error hook failed");
1177
+ console.error(`[router] ${routeEntry.key}: onSettlementError failed: ${message}`);
1178
+ firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
1179
+ level: "error",
1180
+ message: `Settlement error hook failed: ${message}`,
1181
+ route: routeEntry.key
1182
+ });
1183
+ }
1184
+ }
1185
+ async function runAfterSettle(scope) {
1186
+ const hook = routeEntry.settlement?.afterSettle;
1187
+ if (!hook) return;
1188
+ try {
1189
+ await hook(settlementContext(scope));
1190
+ } catch (error) {
1191
+ const message = errorMessage(error, "Post-settlement hook failed");
1192
+ console.error(`[router] ${routeEntry.key}: afterSettle failed: ${message}`);
1193
+ firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
1194
+ level: "error",
1195
+ message: `Post-settlement hook failed: ${message}`,
1196
+ route: routeEntry.key
1197
+ });
1198
+ await runSettlementError(scope, error, "afterSettle");
1199
+ }
1200
+ }
1201
+ async function runSettledHandlerError(scope, error = scope.handlerError ?? handlerFailureError(scope.response)) {
1202
+ const hook = routeEntry.settlement?.onSettledHandlerError;
1203
+ if (!hook) return;
1204
+ try {
1205
+ await hook({
1206
+ ...settlementContext(scope),
1207
+ error
1208
+ });
1209
+ } catch (hookError) {
1210
+ const message = errorMessage(hookError, "Settled handler error hook failed");
1211
+ console.error(`[router] ${routeEntry.key}: onSettledHandlerError failed: ${message}`);
1212
+ firePluginHook(deps.plugin, "onAlert", scope.pluginCtx, {
1213
+ level: "error",
1214
+ message: `Settled handler error hook failed: ${message}`,
1215
+ route: routeEntry.key
1216
+ });
1217
+ }
1218
+ }
1114
1219
  return async (request) => {
1115
1220
  await deps.initPromise;
1116
1221
  const meta = buildMeta(request, routeEntry);
@@ -1366,18 +1471,9 @@ function createRequestHandler(routeEntry, handler, deps) {
1366
1471
  return fail(status, message, meta, pluginCtx, body.data);
1367
1472
  }
1368
1473
  }
1369
- let price;
1370
- try {
1371
- price = await resolvePrice(routeEntry.pricing, body.data);
1372
- } catch (err) {
1373
- return fail(
1374
- err.status ?? 500,
1375
- err instanceof Error ? err.message : "Price resolution failed",
1376
- meta,
1377
- pluginCtx,
1378
- body.data
1379
- );
1380
- }
1474
+ const priceResult = await resolveDynamicPrice(body.data, routeEntry, deps, pluginCtx, meta);
1475
+ if ("error" in priceResult) return priceResult.error;
1476
+ const price = priceResult.price;
1381
1477
  if (!routeEntry.protocols.includes(protocol)) {
1382
1478
  const accepted = routeEntry.protocols.join(", ") || "none";
1383
1479
  console.warn(
@@ -1431,7 +1527,7 @@ function createRequestHandler(routeEntry, handler, deps) {
1431
1527
  amount: price,
1432
1528
  network: matchedNetwork
1433
1529
  });
1434
- const { response, rawResult } = await invoke(
1530
+ const { response, rawResult, handlerError } = await invoke(
1435
1531
  request,
1436
1532
  meta,
1437
1533
  pluginCtx,
@@ -1440,7 +1536,21 @@ function createRequestHandler(routeEntry, handler, deps) {
1440
1536
  body.data,
1441
1537
  payment
1442
1538
  );
1539
+ const settleScope = {
1540
+ request,
1541
+ meta,
1542
+ pluginCtx,
1543
+ wallet,
1544
+ account,
1545
+ parsedBody: body.data,
1546
+ payment,
1547
+ response,
1548
+ rawResult,
1549
+ handlerError
1550
+ };
1443
1551
  if (response.status < 400) {
1552
+ const validationFailure = await runBeforeSettle(settleScope);
1553
+ if (validationFailure) return validationFailure;
1444
1554
  try {
1445
1555
  const settle = await settleX402Payment(
1446
1556
  deps.x402Server,
@@ -1466,13 +1576,21 @@ function createRequestHandler(routeEntry, handler, deps) {
1466
1576
  }
1467
1577
  response.headers.set("PAYMENT-RESPONSE", settle.encoded);
1468
1578
  response.headers.set("Cache-Control", "private");
1579
+ const transaction = String(settle.result?.transaction ?? "");
1580
+ const settledPayment = {
1581
+ ...payment,
1582
+ status: "settled",
1583
+ ...transaction ? { transaction } : {}
1584
+ };
1469
1585
  firePluginHook(deps.plugin, "onPaymentSettled", pluginCtx, {
1470
1586
  protocol: "x402",
1471
- payer: verify.payer,
1472
- transaction: String(settle.result?.transaction ?? ""),
1587
+ payer: wallet,
1588
+ transaction,
1473
1589
  network: matchedNetwork
1474
1590
  });
1591
+ await runAfterSettle({ ...settleScope, payment: settledPayment });
1475
1592
  } catch (err) {
1593
+ await runSettlementError(settleScope, err, "settle");
1476
1594
  const errObj = err;
1477
1595
  console.error("Settlement failed", {
1478
1596
  message: err instanceof Error ? err.message : String(err),
@@ -1531,27 +1649,44 @@ function createRequestHandler(routeEntry, handler, deps) {
1531
1649
  amount: price,
1532
1650
  network: "tempo:4217"
1533
1651
  });
1534
- const { response: response2, rawResult: rawResult2 } = await invoke(
1652
+ const mppRecipient2 = deps.mppRecipient ?? deps.payeeAddress;
1653
+ const payment2 = {
1654
+ protocol: "mpp",
1655
+ status: "verified",
1656
+ payer: wallet,
1657
+ amount: price,
1658
+ network: "tempo:4217",
1659
+ ...mppRecipient2 ? { recipient: mppRecipient2 } : {}
1660
+ };
1661
+ const { response: response2, rawResult: rawResult2, handlerError: handlerError2 } = await invoke(
1535
1662
  request,
1536
1663
  meta,
1537
1664
  pluginCtx,
1538
1665
  wallet,
1539
1666
  account,
1540
1667
  body.data,
1541
- {
1542
- protocol: "mpp",
1543
- status: "verified",
1544
- payer: wallet,
1545
- amount: price,
1546
- network: "tempo:4217",
1547
- recipient: deps.payeeAddress
1548
- }
1668
+ payment2
1549
1669
  );
1670
+ const settleScope2 = {
1671
+ request,
1672
+ meta,
1673
+ pluginCtx,
1674
+ wallet,
1675
+ account,
1676
+ parsedBody: body.data,
1677
+ payment: payment2,
1678
+ response: response2,
1679
+ rawResult: rawResult2,
1680
+ handlerError: handlerError2
1681
+ };
1550
1682
  if (response2.status < 400) {
1683
+ const validationFailure = await runBeforeSettle(settleScope2);
1684
+ if (validationFailure) return validationFailure;
1551
1685
  let mppResult2;
1552
1686
  try {
1553
1687
  mppResult2 = await deps.mppx.charge({ amount: price })(request);
1554
1688
  } catch (err) {
1689
+ await runSettlementError(settleScope2, err, "settle");
1555
1690
  const message = err instanceof Error ? err.message : String(err);
1556
1691
  console.error(
1557
1692
  `[router] ${routeEntry.key}: MPP broadcast failed after handler: ${message}`
@@ -1580,6 +1715,13 @@ function createRequestHandler(routeEntry, handler, deps) {
1580
1715
  } catch {
1581
1716
  }
1582
1717
  const detail = rejectReason || "transaction reverted on-chain after handler execution";
1718
+ const settlementError = Object.assign(new Error(detail), {
1719
+ status: 402,
1720
+ detail,
1721
+ mppResult: mppResult2,
1722
+ challenge: mppResult2.challenge
1723
+ });
1724
+ await runSettlementError(settleScope2, settlementError, "settle");
1583
1725
  console.error(
1584
1726
  `[router] ${routeEntry.key}: MPP payment failed after handler \u2014 ${detail}`
1585
1727
  );
@@ -1617,6 +1759,17 @@ function createRequestHandler(routeEntry, handler, deps) {
1617
1759
  transaction: txHash2,
1618
1760
  network: "tempo:4217"
1619
1761
  });
1762
+ const settledPayment = {
1763
+ ...payment2,
1764
+ status: "settled",
1765
+ ...txHash2 ? { transaction: txHash2 } : {},
1766
+ ...receiptHeader2 ? { receipt: receiptHeader2 } : {}
1767
+ };
1768
+ await runAfterSettle({
1769
+ ...settleScope2,
1770
+ payment: settledPayment,
1771
+ response: receiptResponse
1772
+ });
1620
1773
  finalize(receiptResponse, rawResult2, meta, pluginCtx, body.data);
1621
1774
  return receiptResponse;
1622
1775
  }
@@ -1672,24 +1825,38 @@ function createRequestHandler(routeEntry, handler, deps) {
1672
1825
  amount: price,
1673
1826
  network: "tempo:4217"
1674
1827
  });
1675
- const { response, rawResult } = await invoke(
1828
+ const mppRecipient = deps.mppRecipient ?? deps.payeeAddress;
1829
+ const payment = {
1830
+ protocol: "mpp",
1831
+ status: "settled",
1832
+ payer: wallet,
1833
+ amount: price,
1834
+ network: "tempo:4217",
1835
+ ...mppRecipient ? { recipient: mppRecipient } : {},
1836
+ ...txHash ? { transaction: txHash } : {},
1837
+ ...receiptHeader ? { receipt: receiptHeader } : {}
1838
+ };
1839
+ const { response, rawResult, handlerError } = await invoke(
1676
1840
  request,
1677
1841
  meta,
1678
1842
  pluginCtx,
1679
1843
  wallet,
1680
1844
  account,
1681
1845
  body.data,
1682
- {
1683
- protocol: "mpp",
1684
- status: "settled",
1685
- payer: wallet,
1686
- amount: price,
1687
- network: "tempo:4217",
1688
- recipient: deps.payeeAddress,
1689
- ...txHash ? { transaction: txHash } : {},
1690
- ...receiptHeader ? { receipt: receiptHeader } : {}
1691
- }
1846
+ payment
1692
1847
  );
1848
+ const settleScope = {
1849
+ request,
1850
+ meta,
1851
+ pluginCtx,
1852
+ wallet,
1853
+ account,
1854
+ parsedBody: body.data,
1855
+ payment,
1856
+ response,
1857
+ rawResult,
1858
+ handlerError
1859
+ };
1693
1860
  if (response.status < 400) {
1694
1861
  if (routeEntry.siwxEnabled) {
1695
1862
  try {
@@ -1710,9 +1877,11 @@ function createRequestHandler(routeEntry, handler, deps) {
1710
1877
  transaction: txHash,
1711
1878
  network: "tempo:4217"
1712
1879
  });
1880
+ await runAfterSettle({ ...settleScope, response: receiptResponse });
1713
1881
  finalize(receiptResponse, rawResult, meta, pluginCtx, body.data);
1714
1882
  return receiptResponse;
1715
1883
  }
1884
+ await runSettledHandlerError(settleScope);
1716
1885
  finalize(response, rawResult, meta, pluginCtx, body.data);
1717
1886
  return response;
1718
1887
  }
@@ -1785,9 +1954,10 @@ async function resolveDynamicPrice(bodyData, routeEntry, deps, pluginCtx, meta)
1785
1954
  });
1786
1955
  return { price: routeEntry.maxPrice };
1787
1956
  } else {
1957
+ const message = errorMessage(err, "Price calculation failed");
1788
1958
  const errorResponse = import_server2.NextResponse.json(
1789
- { success: false, error: "Price calculation failed" },
1790
- { status: 500 }
1959
+ { success: false, error: message },
1960
+ { status: errorStatus(err, 500) }
1791
1961
  );
1792
1962
  firePluginResponse(deps, pluginCtx, meta, errorResponse);
1793
1963
  return { error: errorResponse };
@@ -1960,22 +2130,13 @@ function fireProviderQuota(routeEntry, response, handlerResult, deps, pluginCtx)
1960
2130
 
1961
2131
  // src/validate-examples.ts
1962
2132
  function validateExamples(key, bodySchema, querySchema, outputSchema, inputExample, hasInputExample, outputExample, hasOutputExample) {
1963
- if (bodySchema && !hasInputExample) {
1964
- throw new Error(
1965
- `route '${key}': .body() requires a matching .inputExample() \u2014 the bazaar discovery extension needs a conforming sample body to advertise.`
1966
- );
1967
- }
1968
- if (querySchema && !hasInputExample) {
1969
- throw new Error(
1970
- `route '${key}': .query() requires a matching .inputExample() \u2014 the bazaar discovery extension needs a conforming sample query to advertise.`
1971
- );
2133
+ const inputSchema = bodySchema ?? querySchema;
2134
+ if (hasInputExample && !inputSchema) {
2135
+ throw new Error(`route '${key}': .inputExample() requires .body() or .query()`);
1972
2136
  }
1973
- if (outputSchema && !hasOutputExample) {
1974
- throw new Error(
1975
- `route '${key}': .output() requires a matching .outputExample() \u2014 the bazaar discovery extension needs a conforming sample response to advertise.`
1976
- );
2137
+ if (hasOutputExample && !outputSchema) {
2138
+ throw new Error(`route '${key}': .outputExample() requires .output()`);
1977
2139
  }
1978
- const inputSchema = bodySchema ?? querySchema;
1979
2140
  if (inputSchema && hasInputExample) {
1980
2141
  const result = inputSchema.safeParse(inputExample);
1981
2142
  if (!result.success) {
@@ -2049,6 +2210,8 @@ var RouteBuilder = class {
2049
2210
  /** @internal */
2050
2211
  _validateFn;
2051
2212
  /** @internal */
2213
+ _settlement;
2214
+ /** @internal */
2052
2215
  _mppInfo;
2053
2216
  constructor(key, registry, deps) {
2054
2217
  this._key = key;
@@ -2062,11 +2225,21 @@ var RouteBuilder = class {
2062
2225
  return next;
2063
2226
  }
2064
2227
  paid(pricing, options) {
2228
+ if (this._authMode === "unprotected") {
2229
+ throw new Error(
2230
+ `route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
2231
+ );
2232
+ }
2233
+ if (this._pricing !== void 0) {
2234
+ throw new Error(
2235
+ `route '${this._key}': Cannot call .paid() more than once on the same route.`
2236
+ );
2237
+ }
2065
2238
  const next = this.fork();
2066
2239
  next._authMode = "paid";
2067
2240
  next._pricing = pricing;
2068
2241
  if (options?.protocols) {
2069
- next._protocols = options.protocols;
2242
+ next._protocols = [...options.protocols];
2070
2243
  } else if (next._protocols.length === 0) {
2071
2244
  next._protocols = ["x402"];
2072
2245
  }
@@ -2131,6 +2304,16 @@ var RouteBuilder = class {
2131
2304
  return next;
2132
2305
  }
2133
2306
  unprotected() {
2307
+ if (this._authMode && this._authMode !== "unprotected") {
2308
+ throw new Error(
2309
+ `route '${this._key}': Cannot combine .unprotected() and .${this._authMode}() on the same route.`
2310
+ );
2311
+ }
2312
+ if (this._pricing) {
2313
+ throw new Error(
2314
+ `route '${this._key}': Cannot combine .unprotected() and .paid() on the same route.`
2315
+ );
2316
+ }
2134
2317
  const next = this.fork();
2135
2318
  next._authMode = "unprotected";
2136
2319
  next._protocols = [];
@@ -2145,32 +2328,43 @@ var RouteBuilder = class {
2145
2328
  next._providerConfig = config ?? {};
2146
2329
  return next;
2147
2330
  }
2148
- // -------------------------------------------------------------------------
2149
- // Schema methods
2150
- // -------------------------------------------------------------------------
2151
- body(schema) {
2331
+ body(schema, example) {
2152
2332
  const next = this.fork();
2153
2333
  next._bodySchema = schema;
2334
+ if (example !== void 0) {
2335
+ next._inputExample = example;
2336
+ next._hasInputExample = true;
2337
+ }
2154
2338
  return next;
2155
2339
  }
2156
- query(schema) {
2340
+ query(schema, example) {
2157
2341
  const next = this.fork();
2158
2342
  next._querySchema = schema;
2343
+ if (example !== void 0) {
2344
+ next._inputExample = example;
2345
+ next._hasInputExample = true;
2346
+ }
2159
2347
  next._method = "GET";
2160
2348
  return next;
2161
2349
  }
2162
- output(schema) {
2350
+ output(schema, example) {
2163
2351
  const next = this.fork();
2164
2352
  next._outputSchema = schema;
2353
+ if (example !== void 0) {
2354
+ next._outputExample = example;
2355
+ next._hasOutputExample = true;
2356
+ }
2165
2357
  return next;
2166
2358
  }
2167
2359
  /**
2168
2360
  * Provide a conforming example of the request input (body or query params).
2169
2361
  *
2170
- * **Required** whenever `.body()` or `.query()` is set enforced at compile time via
2171
- * `.handler()` overloads, and at route-registration time via Zod validation of the
2172
- * example against the schema. The example is embedded in the bazaar discovery extension
2173
- * so indexers can advertise a working sample call.
2362
+ * Optional. When provided, the example is validated against the request schema
2363
+ * at route registration and embedded in the bazaar discovery extension so
2364
+ * indexers can advertise a working sample call.
2365
+ *
2366
+ * For the common case, pass the example directly to `.body(schema, example)` or
2367
+ * `.query(schema, example)` instead.
2174
2368
  *
2175
2369
  * @example
2176
2370
  * ```ts
@@ -2190,10 +2384,11 @@ var RouteBuilder = class {
2190
2384
  /**
2191
2385
  * Provide a conforming example of the response output.
2192
2386
  *
2193
- * **Required** whenever `.output()` is set enforced at compile time via `.handler()`
2194
- * overloads, and at route-registration time via Zod validation of the example against
2195
- * the schema. The example is embedded in the bazaar discovery extension so indexers
2196
- * can advertise the response shape.
2387
+ * Optional. When provided, the example is validated against the output schema
2388
+ * at route registration and embedded in the bazaar discovery extension so
2389
+ * indexers can advertise the response shape.
2390
+ *
2391
+ * For the common case, pass the example directly to `.output(schema, example)` instead.
2197
2392
  *
2198
2393
  * Accepts any JSON value (objects, arrays, or primitives) — top-level array
2199
2394
  * or primitive responses (e.g. `z.array(...)`) are supported alongside the
@@ -2266,15 +2461,39 @@ var RouteBuilder = class {
2266
2461
  return next;
2267
2462
  }
2268
2463
  // -------------------------------------------------------------------------
2464
+ // Settlement lifecycle
2465
+ // -------------------------------------------------------------------------
2466
+ /**
2467
+ * Add route-specific settlement hooks.
2468
+ *
2469
+ * `beforeSettle` runs after a successful handler response but before
2470
+ * router-controlled settlement/broadcast, so it can still prevent the charge
2471
+ * for x402 and MPP transaction-payload flows. `afterSettle` runs after
2472
+ * settlement and is intended for durable ledgers or app-owned refund queues.
2473
+ */
2474
+ settlement(lifecycle) {
2475
+ const next = this.fork();
2476
+ next._settlement = lifecycle;
2477
+ return next;
2478
+ }
2479
+ // -------------------------------------------------------------------------
2269
2480
  // Terminal method
2270
2481
  // -------------------------------------------------------------------------
2271
2482
  handler(fn) {
2272
2483
  const handlerFn = fn;
2484
+ if (!this._authMode) {
2485
+ throw new Error(
2486
+ `route '${this._key}': Select an auth mode: .paid(pricing), .siwx(), .apiKey(resolver), or .unprotected()`
2487
+ );
2488
+ }
2273
2489
  if (this._validateFn && !this._bodySchema) {
2274
2490
  throw new Error(
2275
2491
  `route '${this._key}': .validate() requires .body() \u2014 validation runs on parsed body`
2276
2492
  );
2277
2493
  }
2494
+ if (this._settlement && !this._pricing) {
2495
+ throw new Error(`route '${this._key}': .settlement() requires a paid route`);
2496
+ }
2278
2497
  validateExamples(
2279
2498
  this._key,
2280
2499
  this._bodySchema,
@@ -2306,6 +2525,7 @@ var RouteBuilder = class {
2306
2525
  providerName: this._providerName,
2307
2526
  providerConfig: this._providerConfig,
2308
2527
  validateFn: this._validateFn,
2528
+ settlement: this._settlement,
2309
2529
  mppInfo: this._mppInfo
2310
2530
  };
2311
2531
  this._registry.register(entry);
@@ -2442,6 +2662,7 @@ function toDiscoveryResource(method, url, mode) {
2442
2662
 
2443
2663
  // src/discovery/openapi.ts
2444
2664
  var import_server4 = require("next/server");
2665
+ init_constants();
2445
2666
  function createOpenAPIHandler(registry, baseUrl, pricesKeys, discovery) {
2446
2667
  const normalizedBase = baseUrl.replace(/\/+$/, "");
2447
2668
  let cached = null;
@@ -2583,7 +2804,7 @@ function toProtocolObject(protocol, mppInfo) {
2583
2804
  mpp: {
2584
2805
  method: mppInfo?.method ?? "tempo",
2585
2806
  intent: mppInfo?.intent ?? "charge",
2586
- currency: mppInfo?.currency ?? "0x20c0000000000000000000000000000000000001"
2807
+ currency: mppInfo?.currency ?? TEMPO_USDC_CURRENCY
2587
2808
  }
2588
2809
  };
2589
2810
  }
@@ -2698,6 +2919,8 @@ function mppFromEnv(env, options = {}) {
2698
2919
  const secretKey = env.MPP_SECRET_KEY;
2699
2920
  const currency = env.MPP_CURRENCY;
2700
2921
  const rpcUrl = env.TEMPO_RPC_URL;
2922
+ const feePayerKey = options.feePayerKey ?? env.MPP_FEE_PAYER_KEY;
2923
+ const feePayerKeySource = options.feePayerKey !== void 0 ? "feePayerKey" : "MPP_FEE_PAYER_KEY";
2701
2924
  const hasAnyMppEnv = Boolean(secretKey || currency || rpcUrl || options.require);
2702
2925
  if (!hasAnyMppEnv) return void 0;
2703
2926
  const missing = [
@@ -2708,12 +2931,21 @@ function mppFromEnv(env, options = {}) {
2708
2931
  if (missing.length > 0) {
2709
2932
  throw new Error(`MPP env is incomplete. Missing: ${missing.join(", ")}`);
2710
2933
  }
2934
+ if (!isEvmAddress(currency)) {
2935
+ throw new Error("MPP_CURRENCY must be a 0x-prefixed 20-byte Tempo currency address");
2936
+ }
2937
+ if (options.recipient && !isEvmAddress(options.recipient)) {
2938
+ throw new Error("MPP recipient must be a 0x-prefixed EVM address");
2939
+ }
2940
+ if (feePayerKey && !isEvmPrivateKey(feePayerKey)) {
2941
+ throw new Error(`${feePayerKeySource} must be a 0x-prefixed 32-byte EVM private key`);
2942
+ }
2711
2943
  return {
2712
2944
  secretKey,
2713
2945
  currency,
2714
2946
  rpcUrl,
2715
2947
  ...options.recipient ? { recipient: options.recipient } : {},
2716
- ...options.feePayerKey ? { feePayerKey: options.feePayerKey } : {},
2948
+ ...feePayerKey ? { feePayerKey } : {},
2717
2949
  ...options.useDefaultStore !== void 0 ? { useDefaultStore: options.useDefaultStore } : {}
2718
2950
  };
2719
2951
  }
@@ -2851,13 +3083,26 @@ function validateMppConfig(config, env) {
2851
3083
  protocol: "mpp",
2852
3084
  message: "MPP requires currency. Set MPP_CURRENCY or pass mpp.currency."
2853
3085
  });
3086
+ } else if (!isEvmAddress(mpp.currency)) {
3087
+ issues.push({
3088
+ code: "invalid_mpp_currency",
3089
+ protocol: "mpp",
3090
+ message: "MPP currency must be a 0x-prefixed 20-byte Tempo currency address. Use TEMPO_USDC_CURRENCY for Tempo USDC."
3091
+ });
2854
3092
  }
2855
- if (!mpp.recipient && !config.payeeAddress) {
3093
+ const mppRecipient = mpp.recipient ?? config.payeeAddress;
3094
+ if (!mppRecipient) {
2856
3095
  issues.push({
2857
3096
  code: "missing_mpp_recipient",
2858
3097
  protocol: "mpp",
2859
3098
  message: "MPP requires a recipient address. Set mpp.recipient or payeeAddress in your router config."
2860
3099
  });
3100
+ } else if (!isEvmAddress(mppRecipient)) {
3101
+ issues.push({
3102
+ code: "invalid_mpp_recipient",
3103
+ protocol: "mpp",
3104
+ message: "MPP recipient must be a 0x-prefixed EVM address. Solana recipients require x402."
3105
+ });
2861
3106
  }
2862
3107
  const placeholder = findPlaceholderPayee([mpp.recipient, config.payeeAddress]);
2863
3108
  if (placeholder) {
@@ -2874,6 +3119,13 @@ function validateMppConfig(config, env) {
2874
3119
  message: "MPP requires an authenticated Tempo RPC URL. Set TEMPO_RPC_URL env var or pass rpcUrl in the mpp config object."
2875
3120
  });
2876
3121
  }
3122
+ if (mpp.feePayerKey && !isEvmPrivateKey(mpp.feePayerKey)) {
3123
+ issues.push({
3124
+ code: "invalid_mpp_fee_payer_key",
3125
+ protocol: "mpp",
3126
+ message: "MPP feePayerKey must be a 0x-prefixed 32-byte EVM private key."
3127
+ });
3128
+ }
2877
3129
  if (mpp.useDefaultStore && !mpp.store && (!env.KV_REST_API_URL || !env.KV_REST_API_TOKEN)) {
2878
3130
  issues.push({
2879
3131
  code: "missing_mpp_default_store_env",
@@ -2891,8 +3143,14 @@ function usesDefaultEvmFacilitator(config) {
2891
3143
  function isSupportedX402Network(network) {
2892
3144
  return isEvmNetwork(network) || isSolanaNetwork(network);
2893
3145
  }
3146
+ function isEvmAddress(value) {
3147
+ return /^0x[a-fA-F0-9]{40}$/.test(value);
3148
+ }
3149
+ function isEvmPrivateKey(value) {
3150
+ return /^0x[a-fA-F0-9]{64}$/.test(value);
3151
+ }
2894
3152
  function findPlaceholderPayee(values) {
2895
- return values.find((value) => value?.toLowerCase() === ZERO_EVM_ADDRESS) ?? null;
3153
+ return values.find((value) => value !== void 0 && /^0x0{40}$/i.test(value)) ?? null;
2896
3154
  }
2897
3155
 
2898
3156
  // src/index.ts
@@ -2941,6 +3199,7 @@ function createRouter(config) {
2941
3199
  nonceStore,
2942
3200
  entitlementStore,
2943
3201
  payeeAddress: config.payeeAddress ?? "",
3202
+ mppRecipient: config.mpp?.recipient ?? config.payeeAddress,
2944
3203
  network,
2945
3204
  x402FacilitatorsByNetwork: void 0,
2946
3205
  x402Accepts,