@absol-labs/agent 0.10.0 → 0.12.0-rc.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.
Files changed (331) hide show
  1. package/README.md +106 -30
  2. package/dist/capability/invocation-capability.d.ts +20 -8
  3. package/dist/capability/invocation-capability.d.ts.map +1 -1
  4. package/dist/capability/invocation-capability.js.map +1 -1
  5. package/dist/capability/probe-capability.d.ts +225 -0
  6. package/dist/capability/probe-capability.d.ts.map +1 -0
  7. package/dist/capability/probe-capability.js +248 -0
  8. package/dist/capability/probe-capability.js.map +1 -0
  9. package/dist/cli/args.d.ts +11 -0
  10. package/dist/cli/args.d.ts.map +1 -1
  11. package/dist/cli/args.js +23 -0
  12. package/dist/cli/args.js.map +1 -1
  13. package/dist/cli/commands/claim.d.ts.map +1 -1
  14. package/dist/cli/commands/claim.js +21 -5
  15. package/dist/cli/commands/claim.js.map +1 -1
  16. package/dist/cli/commands/close.d.ts.map +1 -1
  17. package/dist/cli/commands/close.js +51 -7
  18. package/dist/cli/commands/close.js.map +1 -1
  19. package/dist/cli/commands/invoke.d.ts +2 -0
  20. package/dist/cli/commands/invoke.d.ts.map +1 -1
  21. package/dist/cli/commands/invoke.js +3 -2
  22. package/dist/cli/commands/invoke.js.map +1 -1
  23. package/dist/cli/commands/mcp.d.ts.map +1 -1
  24. package/dist/cli/commands/mcp.js +15 -2
  25. package/dist/cli/commands/mcp.js.map +1 -1
  26. package/dist/cli/commands/open.d.ts.map +1 -1
  27. package/dist/cli/commands/open.js +121 -15
  28. package/dist/cli/commands/open.js.map +1 -1
  29. package/dist/cli/commands/pay.d.ts +19 -0
  30. package/dist/cli/commands/pay.d.ts.map +1 -0
  31. package/dist/cli/commands/pay.js +355 -0
  32. package/dist/cli/commands/pay.js.map +1 -0
  33. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
  34. package/dist/cli/commands/reclaim-unverified.js +15 -6
  35. package/dist/cli/commands/reclaim-unverified.js.map +1 -1
  36. package/dist/cli/commands/reclaim.d.ts +23 -2
  37. package/dist/cli/commands/reclaim.d.ts.map +1 -1
  38. package/dist/cli/commands/reclaim.js +200 -21
  39. package/dist/cli/commands/reclaim.js.map +1 -1
  40. package/dist/cli/commands/refund.d.ts +13 -0
  41. package/dist/cli/commands/refund.d.ts.map +1 -0
  42. package/dist/cli/commands/refund.js +224 -0
  43. package/dist/cli/commands/refund.js.map +1 -0
  44. package/dist/cli/commands/status.d.ts.map +1 -1
  45. package/dist/cli/commands/status.js +10 -1
  46. package/dist/cli/commands/status.js.map +1 -1
  47. package/dist/cli/commands/streams.js +9 -13
  48. package/dist/cli/commands/streams.js.map +1 -1
  49. package/dist/cli/commands/watch.d.ts.map +1 -1
  50. package/dist/cli/commands/watch.js +2 -1
  51. package/dist/cli/commands/watch.js.map +1 -1
  52. package/dist/cli/config.d.ts +44 -26
  53. package/dist/cli/config.d.ts.map +1 -1
  54. package/dist/cli/config.js +88 -38
  55. package/dist/cli/config.js.map +1 -1
  56. package/dist/cli/deps.d.ts +86 -7
  57. package/dist/cli/deps.d.ts.map +1 -1
  58. package/dist/cli/deps.js +102 -16
  59. package/dist/cli/deps.js.map +1 -1
  60. package/dist/cli/errors.d.ts.map +1 -1
  61. package/dist/cli/errors.js +21 -1
  62. package/dist/cli/errors.js.map +1 -1
  63. package/dist/cli/escape-hatch.d.ts +6 -4
  64. package/dist/cli/escape-hatch.d.ts.map +1 -1
  65. package/dist/cli/escape-hatch.js +6 -2
  66. package/dist/cli/escape-hatch.js.map +1 -1
  67. package/dist/cli/listings.d.ts +18 -0
  68. package/dist/cli/listings.d.ts.map +1 -1
  69. package/dist/cli/listings.js +22 -12
  70. package/dist/cli/listings.js.map +1 -1
  71. package/dist/cli/preflight.d.ts +20 -14
  72. package/dist/cli/preflight.d.ts.map +1 -1
  73. package/dist/cli/preflight.js +34 -20
  74. package/dist/cli/preflight.js.map +1 -1
  75. package/dist/cli/privy.d.ts +83 -0
  76. package/dist/cli/privy.d.ts.map +1 -0
  77. package/dist/cli/privy.js +232 -0
  78. package/dist/cli/privy.js.map +1 -0
  79. package/dist/cli/reclaimability.d.ts +82 -0
  80. package/dist/cli/reclaimability.d.ts.map +1 -0
  81. package/dist/cli/reclaimability.js +80 -0
  82. package/dist/cli/reclaimability.js.map +1 -0
  83. package/dist/cli/run.d.ts +2 -1
  84. package/dist/cli/run.d.ts.map +1 -1
  85. package/dist/cli/run.js +40 -11
  86. package/dist/cli/run.js.map +1 -1
  87. package/dist/cli/scan-range.d.ts +2 -2
  88. package/dist/cli/scan-range.js +2 -2
  89. package/dist/cli/spend-policy.d.ts +60 -0
  90. package/dist/cli/spend-policy.d.ts.map +1 -0
  91. package/dist/cli/spend-policy.js +65 -0
  92. package/dist/cli/spend-policy.js.map +1 -0
  93. package/dist/cli/status.d.ts +3 -1
  94. package/dist/cli/status.d.ts.map +1 -1
  95. package/dist/cli/status.js +15 -3
  96. package/dist/cli/status.js.map +1 -1
  97. package/dist/cli/wallet.d.ts +109 -40
  98. package/dist/cli/wallet.d.ts.map +1 -1
  99. package/dist/cli/wallet.js +109 -44
  100. package/dist/cli/wallet.js.map +1 -1
  101. package/dist/discovery/rate-band.d.ts +74 -0
  102. package/dist/discovery/rate-band.d.ts.map +1 -0
  103. package/dist/discovery/rate-band.js +114 -0
  104. package/dist/discovery/rate-band.js.map +1 -0
  105. package/dist/escrow-generation.d.ts +81 -0
  106. package/dist/escrow-generation.d.ts.map +1 -0
  107. package/dist/escrow-generation.js +76 -0
  108. package/dist/escrow-generation.js.map +1 -0
  109. package/dist/frameworks/agentkit.d.ts +8 -4
  110. package/dist/frameworks/agentkit.d.ts.map +1 -1
  111. package/dist/frameworks/agentkit.js +27 -10
  112. package/dist/frameworks/agentkit.js.map +1 -1
  113. package/dist/frameworks/eliza.d.ts +3 -3
  114. package/dist/frameworks/eliza.d.ts.map +1 -1
  115. package/dist/frameworks/eliza.js +15 -6
  116. package/dist/frameworks/eliza.js.map +1 -1
  117. package/dist/frameworks/langchain.d.ts.map +1 -1
  118. package/dist/frameworks/langchain.js +8 -0
  119. package/dist/frameworks/langchain.js.map +1 -1
  120. package/dist/gateway/caller-auth-gateway.d.ts +91 -4
  121. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  122. package/dist/gateway/caller-auth-gateway.js +184 -7
  123. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  124. package/dist/gateway/http-server.d.ts +121 -1
  125. package/dist/gateway/http-server.d.ts.map +1 -1
  126. package/dist/gateway/http-server.js +474 -5
  127. package/dist/gateway/http-server.js.map +1 -1
  128. package/dist/gateway/probe-auth.d.ts +112 -0
  129. package/dist/gateway/probe-auth.d.ts.map +1 -0
  130. package/dist/gateway/probe-auth.js +127 -0
  131. package/dist/gateway/probe-auth.js.map +1 -0
  132. package/dist/gateway/signed-rate-bounds.d.ts +51 -0
  133. package/dist/gateway/signed-rate-bounds.d.ts.map +1 -0
  134. package/dist/gateway/signed-rate-bounds.js +94 -0
  135. package/dist/gateway/signed-rate-bounds.js.map +1 -0
  136. package/dist/gateway/x402-opener-mode.d.ts +215 -0
  137. package/dist/gateway/x402-opener-mode.d.ts.map +1 -0
  138. package/dist/gateway/x402-opener-mode.js +639 -0
  139. package/dist/gateway/x402-opener-mode.js.map +1 -0
  140. package/dist/index.d.ts +11 -5
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +15 -5
  143. package/dist/index.js.map +1 -1
  144. package/dist/mandates/env.d.ts +3 -2
  145. package/dist/mandates/env.d.ts.map +1 -1
  146. package/dist/mandates/env.js +3 -2
  147. package/dist/mandates/env.js.map +1 -1
  148. package/dist/mcp/server.d.ts +100 -0
  149. package/dist/mcp/server.d.ts.map +1 -1
  150. package/dist/mcp/server.js +719 -39
  151. package/dist/mcp/server.js.map +1 -1
  152. package/dist/mcp/service-invoker.d.ts +97 -0
  153. package/dist/mcp/service-invoker.d.ts.map +1 -0
  154. package/dist/mcp/service-invoker.js +167 -0
  155. package/dist/mcp/service-invoker.js.map +1 -0
  156. package/dist/mcp/x402-tools.d.ts +104 -0
  157. package/dist/mcp/x402-tools.d.ts.map +1 -0
  158. package/dist/mcp/x402-tools.js +434 -0
  159. package/dist/mcp/x402-tools.js.map +1 -0
  160. package/dist/network.d.ts +119 -0
  161. package/dist/network.d.ts.map +1 -0
  162. package/dist/network.js +199 -0
  163. package/dist/network.js.map +1 -0
  164. package/dist/sdk/invoke.d.ts +23 -4
  165. package/dist/sdk/invoke.d.ts.map +1 -1
  166. package/dist/sdk/invoke.js +71 -10
  167. package/dist/sdk/invoke.js.map +1 -1
  168. package/dist/wallet/autonomous-wallet-broker.d.ts +3 -0
  169. package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -1
  170. package/dist/wallet/autonomous-wallet-broker.js +27 -23
  171. package/dist/wallet/autonomous-wallet-broker.js.map +1 -1
  172. package/dist/wallet/autonomous-wallet-protocol.d.ts +8 -2
  173. package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -1
  174. package/dist/wallet/autonomous-wallet-protocol.js +9 -2
  175. package/dist/wallet/autonomous-wallet-protocol.js.map +1 -1
  176. package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
  177. package/dist/wallet/autonomous-wallet.js +3 -2
  178. package/dist/wallet/autonomous-wallet.js.map +1 -1
  179. package/dist/wallet/broker-escrows.d.ts +222 -0
  180. package/dist/wallet/broker-escrows.d.ts.map +1 -0
  181. package/dist/wallet/broker-escrows.js +36 -0
  182. package/dist/wallet/broker-escrows.js.map +1 -0
  183. package/dist/wallet/lifecycle.d.ts +21 -5
  184. package/dist/wallet/lifecycle.d.ts.map +1 -1
  185. package/dist/wallet/lifecycle.js +32 -12
  186. package/dist/wallet/lifecycle.js.map +1 -1
  187. package/dist/wallet/privy-broker-server.d.ts.map +1 -1
  188. package/dist/wallet/privy-broker-server.js +31 -26
  189. package/dist/wallet/privy-broker-server.js.map +1 -1
  190. package/dist/wallet/privy-session-broker.d.ts +14 -5
  191. package/dist/wallet/privy-session-broker.d.ts.map +1 -1
  192. package/dist/wallet/privy-session-broker.js +29 -17
  193. package/dist/wallet/privy-session-broker.js.map +1 -1
  194. package/dist/wallet/privy-session-provider.d.ts +3 -1
  195. package/dist/wallet/privy-session-provider.d.ts.map +1 -1
  196. package/dist/wallet/privy-session-provider.js +5 -3
  197. package/dist/wallet/privy-session-provider.js.map +1 -1
  198. package/dist/wallet/provider.d.ts.map +1 -1
  199. package/dist/wallet/provider.js +15 -8
  200. package/dist/wallet/provider.js.map +1 -1
  201. package/dist/x402/facilitator.d.ts +42 -17
  202. package/dist/x402/facilitator.d.ts.map +1 -1
  203. package/dist/x402/facilitator.js +49 -33
  204. package/dist/x402/facilitator.js.map +1 -1
  205. package/dist/x402-buyer/guarded-fetch.d.ts +51 -0
  206. package/dist/x402-buyer/guarded-fetch.d.ts.map +1 -0
  207. package/dist/x402-buyer/guarded-fetch.js +147 -0
  208. package/dist/x402-buyer/guarded-fetch.js.map +1 -0
  209. package/dist/x402-buyer/index.d.ts +10 -0
  210. package/dist/x402-buyer/index.d.ts.map +1 -0
  211. package/dist/x402-buyer/index.js +10 -0
  212. package/dist/x402-buyer/index.js.map +1 -0
  213. package/dist/x402-buyer/pay.d.ts +135 -0
  214. package/dist/x402-buyer/pay.d.ts.map +1 -0
  215. package/dist/x402-buyer/pay.js +336 -0
  216. package/dist/x402-buyer/pay.js.map +1 -0
  217. package/dist/x402-opener/abi.d.ts +684 -0
  218. package/dist/x402-opener/abi.d.ts.map +1 -0
  219. package/dist/x402-opener/abi.js +880 -0
  220. package/dist/x402-opener/abi.js.map +1 -0
  221. package/dist/x402-opener/facilitator-entry.d.ts +3 -0
  222. package/dist/x402-opener/facilitator-entry.d.ts.map +1 -0
  223. package/dist/x402-opener/facilitator-entry.js +37 -0
  224. package/dist/x402-opener/facilitator-entry.js.map +1 -0
  225. package/dist/x402-opener/facilitator-server.d.ts +180 -0
  226. package/dist/x402-opener/facilitator-server.d.ts.map +1 -0
  227. package/dist/x402-opener/facilitator-server.js +696 -0
  228. package/dist/x402-opener/facilitator-server.js.map +1 -0
  229. package/dist/x402-opener/index.d.ts +14 -0
  230. package/dist/x402-opener/index.d.ts.map +1 -0
  231. package/dist/x402-opener/index.js +14 -0
  232. package/dist/x402-opener/index.js.map +1 -0
  233. package/dist/x402-opener/listing-resolver.d.ts +55 -0
  234. package/dist/x402-opener/listing-resolver.d.ts.map +1 -0
  235. package/dist/x402-opener/listing-resolver.js +125 -0
  236. package/dist/x402-opener/listing-resolver.js.map +1 -0
  237. package/dist/x402-opener/refund-relay.d.ts +240 -0
  238. package/dist/x402-opener/refund-relay.d.ts.map +1 -0
  239. package/dist/x402-opener/refund-relay.js +1240 -0
  240. package/dist/x402-opener/refund-relay.js.map +1 -0
  241. package/dist/x402-opener/relayer-queue.d.ts +14 -0
  242. package/dist/x402-opener/relayer-queue.d.ts.map +1 -0
  243. package/dist/x402-opener/relayer-queue.js +18 -0
  244. package/dist/x402-opener/relayer-queue.js.map +1 -0
  245. package/dist/x402-opener/scheme.d.ts +217 -0
  246. package/dist/x402-opener/scheme.d.ts.map +1 -0
  247. package/dist/x402-opener/scheme.js +749 -0
  248. package/dist/x402-opener/scheme.js.map +1 -0
  249. package/dist/x402-opener/terms.d.ts +105 -0
  250. package/dist/x402-opener/terms.d.ts.map +1 -0
  251. package/dist/x402-opener/terms.js +212 -0
  252. package/dist/x402-opener/terms.js.map +1 -0
  253. package/dist/zktls/reclaim.d.ts +2 -2
  254. package/docs/agent-layer.md +13 -8
  255. package/docs/autonomous-privy-wallet.md +188 -0
  256. package/docs/cli.md +342 -53
  257. package/docs/langchain.md +5 -0
  258. package/docs/mcp-hosted.md +4 -3
  259. package/docs/mcp-reference.md +245 -0
  260. package/docs/probe-capability.md +191 -0
  261. package/docs/quickstart.md +54 -5
  262. package/docs/threat-model.md +24 -3
  263. package/docs/x402.md +681 -0
  264. package/package.json +19 -6
  265. package/src/capability/invocation-capability.ts +17 -1
  266. package/src/capability/probe-capability.ts +355 -0
  267. package/src/cli/args.ts +29 -0
  268. package/src/cli/commands/claim.ts +31 -5
  269. package/src/cli/commands/close.ts +56 -7
  270. package/src/cli/commands/invoke.ts +6 -2
  271. package/src/cli/commands/mcp.ts +18 -2
  272. package/src/cli/commands/open.ts +151 -14
  273. package/src/cli/commands/pay.ts +442 -0
  274. package/src/cli/commands/reclaim-unverified.ts +24 -6
  275. package/src/cli/commands/reclaim.ts +274 -19
  276. package/src/cli/commands/refund.ts +277 -0
  277. package/src/cli/commands/status.ts +17 -1
  278. package/src/cli/commands/streams.ts +11 -15
  279. package/src/cli/commands/watch.ts +7 -1
  280. package/src/cli/config.ts +139 -52
  281. package/src/cli/deps.ts +189 -25
  282. package/src/cli/errors.ts +24 -1
  283. package/src/cli/escape-hatch.ts +19 -5
  284. package/src/cli/listings.ts +43 -10
  285. package/src/cli/preflight.ts +36 -21
  286. package/src/cli/privy.ts +398 -0
  287. package/src/cli/reclaimability.ts +154 -0
  288. package/src/cli/run.ts +54 -11
  289. package/src/cli/scan-range.ts +2 -2
  290. package/src/cli/spend-policy.ts +139 -0
  291. package/src/cli/status.ts +14 -1
  292. package/src/cli/wallet.ts +189 -58
  293. package/src/discovery/rate-band.ts +172 -0
  294. package/src/escrow-generation.ts +135 -0
  295. package/src/frameworks/agentkit.ts +45 -11
  296. package/src/frameworks/eliza.ts +27 -5
  297. package/src/frameworks/langchain.ts +14 -0
  298. package/src/gateway/caller-auth-gateway.ts +336 -13
  299. package/src/gateway/http-server.ts +690 -7
  300. package/src/gateway/probe-auth.ts +191 -0
  301. package/src/gateway/signed-rate-bounds.ts +164 -0
  302. package/src/gateway/x402-opener-mode.ts +974 -0
  303. package/src/index.ts +112 -14
  304. package/src/mandates/env.ts +3 -2
  305. package/src/mcp/server.ts +973 -44
  306. package/src/mcp/service-invoker.ts +285 -0
  307. package/src/mcp/x402-tools.ts +621 -0
  308. package/src/network.ts +302 -0
  309. package/src/sdk/invoke.ts +96 -17
  310. package/src/wallet/autonomous-wallet-broker.ts +46 -20
  311. package/src/wallet/autonomous-wallet-protocol.ts +11 -2
  312. package/src/wallet/autonomous-wallet.ts +4 -2
  313. package/src/wallet/broker-escrows.ts +66 -0
  314. package/src/wallet/lifecycle.ts +53 -18
  315. package/src/wallet/privy-broker-server.ts +35 -25
  316. package/src/wallet/privy-session-broker.ts +58 -25
  317. package/src/wallet/privy-session-provider.ts +7 -3
  318. package/src/wallet/provider.ts +20 -15
  319. package/src/x402/facilitator.ts +85 -46
  320. package/src/x402-buyer/guarded-fetch.ts +213 -0
  321. package/src/x402-buyer/index.ts +9 -0
  322. package/src/x402-buyer/pay.ts +517 -0
  323. package/src/x402-opener/abi.ts +881 -0
  324. package/src/x402-opener/facilitator-entry.ts +45 -0
  325. package/src/x402-opener/facilitator-server.ts +1014 -0
  326. package/src/x402-opener/index.ts +13 -0
  327. package/src/x402-opener/listing-resolver.ts +202 -0
  328. package/src/x402-opener/refund-relay.ts +1743 -0
  329. package/src/x402-opener/relayer-queue.ts +18 -0
  330. package/src/x402-opener/scheme.ts +1133 -0
  331. package/src/x402-opener/terms.ts +300 -0
@@ -12,8 +12,31 @@ import {
12
12
  CallerAuthGateway,
13
13
  DEFAULT_MAX_STATE_STALENESS_SECONDS,
14
14
  MAX_CORROBORATING_STREAM_READERS,
15
+ type CallerAuthDecision,
15
16
  type CallerAuthDenialReason,
16
17
  } from "./caller-auth-gateway.js";
18
+ import {
19
+ DEFAULT_MAX_PROBES_PER_MINUTE,
20
+ DEFAULT_ORACLE_SIGNER_CACHE_SECONDS,
21
+ } from "./probe-auth.js";
22
+ import { PROBE_MARKER_HEADER_NAME } from "../capability/probe-capability.js";
23
+ import { networkForCaip2 } from "@absol-labs/sdk";
24
+ import {
25
+ chainIdFromNetwork,
26
+ assertSchemaDeclaration,
27
+ assertValidPrice,
28
+ assertValidTerms,
29
+ isHttpLink,
30
+ type OpenerTerms,
31
+ type X402SchemaDeclaration,
32
+ } from "../x402-opener/terms.js";
33
+ import type {
34
+ GatewayX402OpenerMode,
35
+ X402GatewayResponseReason,
36
+ X402UpstreamResult,
37
+ } from "./x402-opener-mode.js";
38
+ import { loadSignedRateBoundsFile } from "./signed-rate-bounds.js";
39
+ import { defaultNetwork, requireNetworkForChainId } from "../network.js";
17
40
 
18
41
  /**
19
42
  * Reference standalone reverse-proxy gateway server (metrik-agent#63).
@@ -49,6 +72,15 @@ export interface CallerAuthGatewayServerConfig {
49
72
  readonly upstreamUrl: string;
50
73
  /** Extra request-timeout for the upstream fetch, in ms. Default 30000. */
51
74
  readonly upstreamTimeoutMs?: number;
75
+ /**
76
+ * Optional x402 v2 mode (metrik-agent#116): an uncapabilitied request gets a
77
+ * standard x402 `402` whose `payTo` is the listing's escrow opener, and a
78
+ * `PAYMENT-SIGNATURE` is verified, served, and settled into a
79
+ * `StreamEscrowV2` stream. OFF when unset — the server is then byte for byte
80
+ * the capability-only gateway (`test/gateway-http-baseline.test.ts`).
81
+ * Build it with `buildGatewayX402OpenerMode`.
82
+ */
83
+ readonly x402Opener?: GatewayX402OpenerMode;
52
84
  }
53
85
 
54
86
  export interface CallerAuthGatewayServer {
@@ -70,6 +102,7 @@ export function createCallerAuthGatewayServer(
70
102
  config.gateway,
71
103
  upstreamBase,
72
104
  upstreamTimeoutMs,
105
+ config.x402Opener,
73
106
  ).catch((error) => {
74
107
  logError("unhandled request error", error);
75
108
  if (!res.headersSent) {
@@ -105,11 +138,37 @@ async function handleRequest(
105
138
  gateway: CallerAuthGateway,
106
139
  upstreamBase: URL,
107
140
  upstreamTimeoutMs: number,
141
+ x402Opener: GatewayX402OpenerMode | undefined,
108
142
  ): Promise<void> {
109
143
  const requestUrl = new URL(req.url ?? "/", "http://localhost");
110
144
  const method = req.method ?? "GET";
111
145
  const headers = normalizeIncomingHeaders(req.headers);
112
146
 
147
+ if (x402Opener !== undefined) {
148
+ const route = routeX402(headers, gateway);
149
+ if (route === "ambiguous-probe-mode") {
150
+ writeJsonError(res, 400, "ambiguous-probe-mode");
151
+ return;
152
+ }
153
+ if (route === "ambiguous-capability-and-payment") {
154
+ writeJsonError(res, 400, "ambiguous-capability-and-payment");
155
+ return;
156
+ }
157
+ if (route === "x402") {
158
+ await handleX402Request(
159
+ req,
160
+ res,
161
+ x402Opener,
162
+ requestUrl,
163
+ method,
164
+ headers,
165
+ upstreamBase,
166
+ upstreamTimeoutMs,
167
+ );
168
+ return;
169
+ }
170
+ }
171
+
113
172
  const decision = await gateway.authorize({
114
173
  headers,
115
174
  method,
@@ -138,6 +197,16 @@ async function handleRequest(
138
197
  upstreamHeaders.set(key, value);
139
198
  }
140
199
 
200
+ // The probe marker is gateway-asserted, never client-asserted: strip any
201
+ // inbound copy first, so a paying caller cannot mark its own billable request
202
+ // as verification traffic and slip past the seller's metering.
203
+ upstreamHeaders.delete(PROBE_MARKER_HEADER_NAME);
204
+ if (decision.probe !== undefined) {
205
+ // Advisory marker so the seller's OWN metering can exclude verification
206
+ // traffic from billable usage. The gateway itself charges nothing for it.
207
+ upstreamHeaders.set(PROBE_MARKER_HEADER_NAME, "1");
208
+ }
209
+
141
210
  const upstreamUrl = new URL(
142
211
  requestUrl.pathname + requestUrl.search,
143
212
  upstreamBase,
@@ -153,11 +222,10 @@ async function handleRequest(
153
222
  signal: controller.signal,
154
223
  });
155
224
 
156
- res.writeHead(
157
- upstreamResponse.status,
158
- headersToObject(upstreamResponse.headers),
159
- );
160
225
  const buffer = Buffer.from(await upstreamResponse.arrayBuffer());
226
+ const responseHeaders = headersToObject(upstreamResponse.headers);
227
+
228
+ res.writeHead(upstreamResponse.status, responseHeaders);
161
229
  res.end(buffer);
162
230
  } catch (error) {
163
231
  logError("upstream request failed", error);
@@ -167,6 +235,130 @@ async function handleRequest(
167
235
  }
168
236
  }
169
237
 
238
+ /** x402 v2 request header (base64 `PaymentPayload`). */
239
+ const PAYMENT_SIGNATURE_HEADER = "payment-signature";
240
+ /** x402 v1 request header. Never forwarded upstream in x402 mode either. */
241
+ const LEGACY_PAYMENT_HEADER = "x-payment";
242
+
243
+ type X402Route =
244
+ | "x402"
245
+ | "capability-or-probe"
246
+ | "ambiguous-probe-mode"
247
+ | "ambiguous-capability-and-payment";
248
+
249
+ /**
250
+ * Decides which path an x402-mode request takes. A payment is never mixed with
251
+ * a capability or a probe: both-present is a `400`, not a guess.
252
+ */
253
+ function routeX402(
254
+ headers: Record<string, string | undefined>,
255
+ gateway: CallerAuthGateway,
256
+ ): X402Route {
257
+ const present = (name: string) => {
258
+ const value = headers[name];
259
+ return value !== undefined && value.trim().length > 0;
260
+ };
261
+ const hasPayment = present(PAYMENT_SIGNATURE_HEADER);
262
+ const hasCapability = present(gateway.capabilityHeaderName);
263
+ const hasProbe = present(gateway.probeCapabilityHeaderName);
264
+ if (hasProbe && hasPayment) return "ambiguous-probe-mode";
265
+ if (hasCapability && hasPayment) return "ambiguous-capability-and-payment";
266
+ if (hasPayment) return "x402";
267
+ if (!hasCapability && !hasProbe) return "x402";
268
+ return "capability-or-probe";
269
+ }
270
+
271
+ /**
272
+ * The x402 v2 path. The protocol logic (verify → upstream → settle-before-body)
273
+ * lives with the mode in `./x402-opener-mode.ts`, so this module — and the
274
+ * package's root barrel — never loads `@x402/*` for a gateway that does not
275
+ * enable it. This function only supplies the transport.
276
+ */
277
+ async function handleX402Request(
278
+ req: IncomingMessage,
279
+ res: ServerResponse,
280
+ mode: GatewayX402OpenerMode,
281
+ requestUrl: URL,
282
+ method: string,
283
+ headers: Record<string, string | undefined>,
284
+ upstreamBase: URL,
285
+ upstreamTimeoutMs: number,
286
+ ): Promise<void> {
287
+ await mode.handle({
288
+ requestUrl,
289
+ method,
290
+ headers,
291
+ callUpstream: () =>
292
+ callUpstream(
293
+ req,
294
+ method,
295
+ headers,
296
+ requestUrl,
297
+ upstreamBase,
298
+ upstreamTimeoutMs,
299
+ ),
300
+ writeJson: (status, reason) => writeJsonError(res, status, reason),
301
+ write: (status, responseHeaders, body) => {
302
+ if (res.headersSent) {
303
+ res.end();
304
+ return;
305
+ }
306
+ res.writeHead(status, responseHeaders);
307
+ res.end(body);
308
+ },
309
+ logError,
310
+ });
311
+ }
312
+
313
+ /** Buffers one upstream call. Payment headers are never forwarded. */
314
+ async function callUpstream(
315
+ req: IncomingMessage,
316
+ method: string,
317
+ headers: Record<string, string | undefined>,
318
+ requestUrl: URL,
319
+ upstreamBase: URL,
320
+ upstreamTimeoutMs: number,
321
+ ): Promise<X402UpstreamResult> {
322
+ let body: Buffer | undefined;
323
+ if (method !== "GET" && method !== "HEAD") {
324
+ body = await readBody(req);
325
+ }
326
+ const upstreamHeaders = new Headers();
327
+ for (const [key, value] of Object.entries(headers)) {
328
+ if (value === undefined || HOP_BY_HOP_HEADERS.has(key)) continue;
329
+ upstreamHeaders.set(key, value);
330
+ }
331
+ // A signed payment authorization must never reach the upstream: anyone who
332
+ // holds it can submit it to USDC directly, which strands the payer's funds
333
+ // in the opener. The probe marker is gateway-asserted only.
334
+ upstreamHeaders.delete(PAYMENT_SIGNATURE_HEADER);
335
+ upstreamHeaders.delete(LEGACY_PAYMENT_HEADER);
336
+ upstreamHeaders.delete(PROBE_MARKER_HEADER_NAME);
337
+
338
+ const upstreamUrl = new URL(
339
+ requestUrl.pathname + requestUrl.search,
340
+ upstreamBase,
341
+ );
342
+ const controller = new AbortController();
343
+ const timeout = setTimeout(() => controller.abort(), upstreamTimeoutMs);
344
+ try {
345
+ const response = await fetch(upstreamUrl, {
346
+ method,
347
+ headers: upstreamHeaders,
348
+ ...(body === undefined ? {} : { body }),
349
+ signal: controller.signal,
350
+ });
351
+ const buffer = Buffer.from(await response.arrayBuffer());
352
+ return {
353
+ status: response.status,
354
+ headers: headersToObject(response.headers),
355
+ body: buffer,
356
+ };
357
+ } finally {
358
+ clearTimeout(timeout);
359
+ }
360
+ }
361
+
170
362
  function normalizeIncomingHeaders(
171
363
  headers: IncomingMessage["headers"],
172
364
  ): Record<string, string | undefined> {
@@ -210,7 +402,9 @@ function writeJsonError(
210
402
  | CallerAuthDenialReason
211
403
  | "unauthorized"
212
404
  | "internal-error"
213
- | "upstream-unavailable",
405
+ | "upstream-unavailable"
406
+ | "ambiguous-capability-and-payment"
407
+ | X402GatewayResponseReason,
214
408
  ): void {
215
409
  if (res.headersSent) {
216
410
  res.end();
@@ -242,6 +436,50 @@ export interface CallerAuthGatewayEnvConfig {
242
436
  readonly corroboratingRpcUrls: readonly string[];
243
437
  /** Freshness bound for on-chain stream state, in seconds. */
244
438
  readonly maxStateStalenessSeconds: number;
439
+ /** Set only when oracle probe verification is enabled. */
440
+ readonly probe?: CallerAuthGatewayProbeEnvConfig;
441
+ /** Set only when the x402 v2 opener mode is enabled. */
442
+ readonly x402Opener?: CallerAuthGatewayX402OpenerEnvConfig;
443
+ /**
444
+ * Path of the operator-signed descriptor(s) whose rate band the capability
445
+ * path enforces (`METRIK_GATEWAY_SIGNED_DESCRIPTOR_FILE`). Loaded and
446
+ * verified at startup by {@link startCallerAuthGatewayServerFromEnv}.
447
+ */
448
+ readonly signedDescriptorFile?: string;
449
+ }
450
+
451
+ /** Env view of the gateway's x402 v2 opener mode (see {@link parseGatewayX402OpenerEnvConfig}). */
452
+ export interface CallerAuthGatewayX402OpenerEnvConfig {
453
+ readonly facilitatorUrl: string;
454
+ readonly facilitatorToken: string;
455
+ readonly network: `${string}:${string}`;
456
+ readonly factoryAddress: Address;
457
+ readonly asset: Address;
458
+ readonly assetName: string;
459
+ readonly assetVersion: string;
460
+ readonly terms: OpenerTerms;
461
+ readonly price: bigint;
462
+ readonly maxTimeoutSeconds: number;
463
+ /** Per-request facilitator HTTP timeout, ms. */
464
+ readonly facilitatorTimeoutMs: number;
465
+ readonly resourceBaseUrl?: string;
466
+ readonly registryUrl?: string;
467
+ /** Public base URL of the facilitator's refund relay (default: facilitatorUrl). */
468
+ readonly refundBaseUrl?: string;
469
+ readonly docsUrl?: string;
470
+ /** Token decimals for the 402 description (default 6). */
471
+ readonly assetDecimals?: number;
472
+ /** One-sentence service description, prefixed to the fixed disclosure. */
473
+ readonly description?: string;
474
+ /** Optional input/output schema, declared as `extensions.bazaar`. */
475
+ readonly schemaDeclaration?: X402SchemaDeclaration;
476
+ }
477
+
478
+ export interface CallerAuthGatewayProbeEnvConfig {
479
+ /** Free-probe budget per minute. */
480
+ readonly maxProbesPerMinute: number;
481
+ /** Freshness bound for the cached on-chain oracle signer set, in seconds. */
482
+ readonly signerCacheSeconds: number;
245
483
  }
246
484
 
247
485
  /**
@@ -260,8 +498,14 @@ export interface CallerAuthGatewayEnvConfig {
260
498
  * - `METRIK_GATEWAY_MAX_STATE_STALENESS_SECONDS` (optional) - reject stream
261
499
  * state observed more than N seconds behind the wall clock. Default
262
500
  * {@link DEFAULT_MAX_STATE_STALENESS_SECONDS}.
263
- * - `METRIK_GATEWAY_CHAIN_ID` (optional) - default `84532` (Base Sepolia).
501
+ * - `METRIK_GATEWAY_CHAIN_ID` (optional) - default the registry's default
502
+ * network. Must be a network the SDK registry knows.
264
503
  * - `METRIK_GATEWAY_HEADER_NAME` (optional) - default `x-metrik-capability`.
504
+ * - `METRIK_GATEWAY_SIGNED_DESCRIPTOR_FILE` (optional, recommended) - JSON file
505
+ * holding this service's operator-signed descriptor (or an array, one per
506
+ * served serviceRef). Verified at startup; the capability path then refuses
507
+ * any stream whose on-chain `ratePerSecond` is outside the signed
508
+ * `minRatePerSecond`/`maxRatePerSecond` (`stream-rate-out-of-bounds`).
265
509
  * - `PORT` / `METRIK_GATEWAY_PORT` (optional) - default `8787`.
266
510
  * - `METRIK_GATEWAY_HOST` (optional) - default `0.0.0.0`.
267
511
  *
@@ -294,11 +538,15 @@ export function parseCallerAuthGatewayEnvConfig(
294
538
  // eslint-disable-next-line no-new -- validates the URL is well-formed.
295
539
  new URL(upstreamUrl);
296
540
 
297
- const chainIdRaw = env.METRIK_GATEWAY_CHAIN_ID ?? "84532";
541
+ const chainIdRaw =
542
+ env.METRIK_GATEWAY_CHAIN_ID ?? String(defaultNetwork().chain.id);
298
543
  const chainId = Number(chainIdRaw);
299
544
  if (!Number.isInteger(chainId) || chainId <= 0) {
300
545
  throw new Error(`invalid METRIK_GATEWAY_CHAIN_ID: ${chainIdRaw}`);
301
546
  }
547
+ // The stream reader is built from the registry entry for this chain, so an
548
+ // unknown chain is refused at startup rather than at the first request.
549
+ requireNetworkForChainId(chainId, {}, "METRIK_GATEWAY_CHAIN_ID");
302
550
 
303
551
  const portRaw = env.METRIK_GATEWAY_PORT ?? env.PORT ?? "8787";
304
552
  const port = Number(portRaw);
@@ -346,6 +594,15 @@ export function parseCallerAuthGatewayEnvConfig(
346
594
  );
347
595
  }
348
596
 
597
+ const signedDescriptorFile =
598
+ env.METRIK_GATEWAY_SIGNED_DESCRIPTOR_FILE?.trim() || undefined;
599
+
600
+ const probe = parseGatewayProbeEnvConfig(env);
601
+ const x402Opener = parseGatewayX402OpenerEnvConfig(env, {
602
+ chainId,
603
+ serviceRef: serviceRef as Hex,
604
+ });
605
+
349
606
  return {
350
607
  host,
351
608
  port,
@@ -360,9 +617,321 @@ export function parseCallerAuthGatewayEnvConfig(
360
617
  corroboratingRpcUrls,
361
618
  maxStateStalenessSeconds,
362
619
  ...(headerName === undefined ? {} : { headerName }),
620
+ ...(probe === undefined ? {} : { probe }),
621
+ ...(x402Opener === undefined ? {} : { x402Opener }),
622
+ ...(signedDescriptorFile === undefined ? {} : { signedDescriptorFile }),
623
+ };
624
+ }
625
+
626
+ /**
627
+ * The x402 v2 opener mode is OFF unless `METRIK_GATEWAY_X402V2_OPENER` is
628
+ * truthy. When on, an uncapabilitied request is answered with a standard x402
629
+ * v2 `402` whose `payTo` is the opener for this listing's terms, and payments
630
+ * settle into a `StreamEscrowV2` stream through the Metrik opener facilitator.
631
+ * The listing is the gateway's primary `METRIK_GATEWAY_SERVICE_REF`.
632
+ *
633
+ * These names deliberately differ from the removed v1 mode's variables, so a
634
+ * leftover v1 environment can never switch a payment mode on by accident.
635
+ *
636
+ * - `METRIK_GATEWAY_X402V2_OPENER` — `1`/`true`/`yes`/`on` to enable.
637
+ * - `METRIK_GATEWAY_X402V2_FACILITATOR_URL` (required) — the Metrik opener facilitator.
638
+ * - `METRIK_GATEWAY_X402V2_FACILITATOR_TOKEN` (required) — its `/settle` bearer token.
639
+ * - `METRIK_GATEWAY_X402V2_FACTORY_ADDRESS` — the `X402StreamOpenerFactory`; default the
640
+ * network's registered factory (required where the registry has none).
641
+ * - `METRIK_GATEWAY_X402V2_OPERATOR` (required) — must be the signed listing's operator.
642
+ * - `METRIK_GATEWAY_X402V2_RATE_PER_SECOND` (required) — USDC base units per second.
643
+ * - `METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS` (required).
644
+ * - `METRIK_GATEWAY_X402V2_PRICE` (required) — base units per paid request (= deposit),
645
+ * `0 < price <= rate * maxDuration`.
646
+ * - `METRIK_GATEWAY_X402V2_NETWORK` — default `eip155:<METRIK_GATEWAY_CHAIN_ID>`.
647
+ * - `METRIK_GATEWAY_X402V2_ASSET` — default the network's registered USDC.
648
+ * - `METRIK_GATEWAY_X402V2_ASSET_NAME` / `_ASSET_VERSION` — EIP-712 domain, default the
649
+ * registered USDC's own EIP-712 name/version.
650
+ * - `METRIK_GATEWAY_X402V2_MAX_TIMEOUT_SECONDS` — authorization validity, default 300.
651
+ * - `METRIK_GATEWAY_X402V2_FACILITATOR_TIMEOUT_SECONDS` — per-request facilitator
652
+ * timeout, default 120; must exceed the facilitator's receipt wait by 30 s.
653
+ * - `METRIK_GATEWAY_X402V2_RESOURCE_BASE_URL` — public origin for the 402 `resource.url`.
654
+ * - `METRIK_GATEWAY_X402V2_REGISTRY_URL` — registry used to prove the listing.
655
+ * - `METRIK_GATEWAY_X402V2_REFUND_BASE_URL` — public base URL of the facilitator's
656
+ * gasless refund relay, advertised in the 402; default the facilitator URL. Its
657
+ * `/supported` must show the opener marker with `refundRelay: true`.
658
+ * - `METRIK_GATEWAY_X402V2_DOCS_URL` — docs link in the 402; default docs/x402.md on GitHub.
659
+ * - `METRIK_GATEWAY_X402V2_ASSET_DECIMALS` — decimals for the 402 description; default 6.
660
+ * - `METRIK_GATEWAY_X402V2_DESCRIPTION` — one sentence on what the service does,
661
+ * prefixed to the fixed disclosure of what the payment buys (at most 300 chars).
662
+ * - `METRIK_GATEWAY_X402V2_SCHEMA` — optional JSON `{info: {input, output}, schema}`
663
+ * declared in the 402 as `extensions.bazaar` (declared only, never registered).
664
+ */
665
+ export function parseGatewayX402OpenerEnvConfig(
666
+ env: NodeJS.ProcessEnv,
667
+ context: { readonly chainId: number; readonly serviceRef: Hex },
668
+ ): CallerAuthGatewayX402OpenerEnvConfig | undefined {
669
+ const raw = env.METRIK_GATEWAY_X402V2_OPENER?.trim().toLowerCase();
670
+ if (
671
+ raw === undefined ||
672
+ raw.length === 0 ||
673
+ ["0", "false", "no", "off"].includes(raw)
674
+ ) {
675
+ return undefined;
676
+ }
677
+ if (!["1", "true", "yes", "on"].includes(raw)) {
678
+ throw new Error(
679
+ `invalid METRIK_GATEWAY_X402V2_OPENER: ${raw} (expected a boolean)`,
680
+ );
681
+ }
682
+ const facilitatorUrl = requireEnv(
683
+ env,
684
+ "METRIK_GATEWAY_X402V2_FACILITATOR_URL",
685
+ ).trim();
686
+ // eslint-disable-next-line no-new -- validates the URL is well-formed.
687
+ new URL(facilitatorUrl);
688
+ const facilitatorToken = requireEnv(
689
+ env,
690
+ "METRIK_GATEWAY_X402V2_FACILITATOR_TOKEN",
691
+ ).trim();
692
+ const network = (env.METRIK_GATEWAY_X402V2_NETWORK?.trim() ||
693
+ `eip155:${String(context.chainId)}`) as `${string}:${string}`;
694
+ if (chainIdFromNetwork(network) !== context.chainId) {
695
+ throw new Error(
696
+ `METRIK_GATEWAY_X402V2_NETWORK ${network} does not match METRIK_GATEWAY_CHAIN_ID ${context.chainId}`,
697
+ );
698
+ }
699
+ // Defaults come from the registry entry for THIS network only.
700
+ const registered = networkForCaip2(network);
701
+ const factoryRaw = env.METRIK_GATEWAY_X402V2_FACTORY_ADDRESS?.trim();
702
+ let factoryAddress: Address;
703
+ if (factoryRaw === undefined || factoryRaw.length === 0) {
704
+ if (registered?.openerFactory == null) {
705
+ throw new Error(
706
+ `METRIK_GATEWAY_X402V2_FACTORY_ADDRESS is required on ${network}: the network registry has no opener factory for it`,
707
+ );
708
+ }
709
+ factoryAddress = registered.openerFactory.address;
710
+ } else if (isAddress(factoryRaw)) {
711
+ factoryAddress = factoryRaw;
712
+ } else {
713
+ throw new Error(
714
+ "METRIK_GATEWAY_X402V2_FACTORY_ADDRESS must be an EVM address",
715
+ );
716
+ }
717
+ const operator = requireEnv(env, "METRIK_GATEWAY_X402V2_OPERATOR").trim();
718
+ if (!isAddress(operator)) {
719
+ throw new Error("METRIK_GATEWAY_X402V2_OPERATOR must be an EVM address");
720
+ }
721
+ const assetRaw = env.METRIK_GATEWAY_X402V2_ASSET?.trim();
722
+ let asset: Address;
723
+ if (assetRaw === undefined || assetRaw.length === 0) {
724
+ if (registered === undefined) {
725
+ throw new Error(
726
+ `METRIK_GATEWAY_X402V2_ASSET is required on ${network}: the network registry does not know it`,
727
+ );
728
+ }
729
+ asset = registered.usdc.address;
730
+ } else if (isAddress(assetRaw)) {
731
+ asset = assetRaw;
732
+ } else {
733
+ throw new Error("METRIK_GATEWAY_X402V2_ASSET must be an EVM address");
734
+ }
735
+ const terms: OpenerTerms = {
736
+ operator,
737
+ serviceRef: context.serviceRef,
738
+ ratePerSecond: requireUintEnv(env, "METRIK_GATEWAY_X402V2_RATE_PER_SECOND"),
739
+ maxDuration: requireUintEnv(
740
+ env,
741
+ "METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS",
742
+ ),
743
+ };
744
+ const price = requireUintEnv(env, "METRIK_GATEWAY_X402V2_PRICE");
745
+ // Startup validation: rate/duration > 0 and 0 < price <= rate * maxDuration.
746
+ assertValidTerms(terms);
747
+ assertValidPrice(price, terms);
748
+ const maxTimeoutSeconds = requirePositiveIntEnv(
749
+ env,
750
+ "METRIK_GATEWAY_X402V2_MAX_TIMEOUT_SECONDS",
751
+ 300,
752
+ );
753
+ const facilitatorTimeoutSeconds = requirePositiveIntEnv(
754
+ env,
755
+ "METRIK_GATEWAY_X402V2_FACILITATOR_TIMEOUT_SECONDS",
756
+ 120,
757
+ );
758
+ const resourceBaseUrl = env.METRIK_GATEWAY_X402V2_RESOURCE_BASE_URL?.trim();
759
+ if (resourceBaseUrl !== undefined && resourceBaseUrl.length > 0) {
760
+ // eslint-disable-next-line no-new -- validates the URL is well-formed.
761
+ new URL(resourceBaseUrl);
762
+ }
763
+ const registryUrl = env.METRIK_GATEWAY_X402V2_REGISTRY_URL?.trim();
764
+ if (registryUrl !== undefined && registryUrl.length > 0) {
765
+ // eslint-disable-next-line no-new -- validates the URL is well-formed.
766
+ new URL(registryUrl);
767
+ }
768
+ const optionalLink = (name: string): string | undefined => {
769
+ const value = env[name]?.trim();
770
+ if (value === undefined || value.length === 0) return undefined;
771
+ if (!isHttpLink(value)) {
772
+ throw new Error(`${name} must be an absolute http(s) URL`);
773
+ }
774
+ return value.replace(/\/+$/, "");
775
+ };
776
+ const refundBaseUrl = optionalLink("METRIK_GATEWAY_X402V2_REFUND_BASE_URL");
777
+ const docsUrl = optionalLink("METRIK_GATEWAY_X402V2_DOCS_URL");
778
+ const decimalsRaw = env.METRIK_GATEWAY_X402V2_ASSET_DECIMALS?.trim();
779
+ let assetDecimals: number | undefined;
780
+ if (decimalsRaw !== undefined && decimalsRaw.length > 0) {
781
+ assetDecimals = Number(decimalsRaw);
782
+ if (
783
+ !/^\d+$/.test(decimalsRaw) ||
784
+ !Number.isInteger(assetDecimals) ||
785
+ assetDecimals > 36
786
+ ) {
787
+ throw new Error(
788
+ `invalid METRIK_GATEWAY_X402V2_ASSET_DECIMALS: ${decimalsRaw} (expected an integer 0-36)`,
789
+ );
790
+ }
791
+ }
792
+ const description = env.METRIK_GATEWAY_X402V2_DESCRIPTION?.trim();
793
+ if (description !== undefined && description.length > 300) {
794
+ throw new Error(
795
+ "METRIK_GATEWAY_X402V2_DESCRIPTION must be at most 300 characters",
796
+ );
797
+ }
798
+ const schemaRaw = env.METRIK_GATEWAY_X402V2_SCHEMA?.trim();
799
+ let schemaDeclaration: X402SchemaDeclaration | undefined;
800
+ if (schemaRaw !== undefined && schemaRaw.length > 0) {
801
+ try {
802
+ const parsed: unknown = JSON.parse(schemaRaw);
803
+ assertSchemaDeclaration(parsed);
804
+ schemaDeclaration = parsed;
805
+ } catch (error) {
806
+ throw new Error(
807
+ `invalid METRIK_GATEWAY_X402V2_SCHEMA: ${error instanceof Error ? error.message : String(error)}`,
808
+ );
809
+ }
810
+ }
811
+ return {
812
+ facilitatorUrl,
813
+ facilitatorToken,
814
+ network,
815
+ factoryAddress,
816
+ asset,
817
+ assetName: requireAssetDomainField(
818
+ env.METRIK_GATEWAY_X402V2_ASSET_NAME,
819
+ registered?.usdc.eip712.name,
820
+ "METRIK_GATEWAY_X402V2_ASSET_NAME",
821
+ network,
822
+ ),
823
+ assetVersion: requireAssetDomainField(
824
+ env.METRIK_GATEWAY_X402V2_ASSET_VERSION,
825
+ registered?.usdc.eip712.version,
826
+ "METRIK_GATEWAY_X402V2_ASSET_VERSION",
827
+ network,
828
+ ),
829
+ terms,
830
+ price,
831
+ maxTimeoutSeconds,
832
+ facilitatorTimeoutMs: facilitatorTimeoutSeconds * 1000,
833
+ ...(resourceBaseUrl === undefined || resourceBaseUrl.length === 0
834
+ ? {}
835
+ : { resourceBaseUrl }),
836
+ ...(registryUrl === undefined || registryUrl.length === 0
837
+ ? {}
838
+ : { registryUrl }),
839
+ ...(refundBaseUrl === undefined ? {} : { refundBaseUrl }),
840
+ ...(docsUrl === undefined ? {} : { docsUrl }),
841
+ ...(assetDecimals === undefined ? {} : { assetDecimals }),
842
+ ...(description === undefined || description.length === 0
843
+ ? {}
844
+ : { description }),
845
+ ...(schemaDeclaration === undefined ? {} : { schemaDeclaration }),
363
846
  };
364
847
  }
365
848
 
849
+ function requireAssetDomainField(
850
+ raw: string | undefined,
851
+ registered: string | undefined,
852
+ name: string,
853
+ network: string,
854
+ ): string {
855
+ const value = raw?.trim();
856
+ if (value !== undefined && value.length > 0) return value;
857
+ if (registered === undefined) {
858
+ throw new Error(
859
+ `${name} is required on ${network}: the network registry does not know the asset's EIP-712 domain`,
860
+ );
861
+ }
862
+ return registered;
863
+ }
864
+
865
+ function requireUintEnv(env: NodeJS.ProcessEnv, name: string): bigint {
866
+ const raw = requireEnv(env, name).trim();
867
+ if (!/^(0|[1-9]\d*)$/.test(raw)) {
868
+ throw new Error(`invalid ${name}: ${raw} (expected a base-10 integer)`);
869
+ }
870
+ return BigInt(raw);
871
+ }
872
+
873
+ /**
874
+ * Oracle probe verification is OFF unless `METRIK_GATEWAY_PROBE_CAPABILITY` is
875
+ * truthy. Turning it on lets the neutral verifier exercise this gateway's PAID
876
+ * route with an oracle-signed, single-use, short-lived `ProbeCapability`
877
+ * instead of only its health route.
878
+ *
879
+ * There is NO credential to configure — authority is an EIP-712 signature by an
880
+ * address in the escrow's public `getOracleSigners()` set, read from the same
881
+ * `METRIK_GATEWAY_RPC_URL` the stream check already uses.
882
+ *
883
+ * - `METRIK_GATEWAY_PROBE_CAPABILITY` — `1`/`true`/`yes`/`on` to enable.
884
+ * - `METRIK_GATEWAY_PROBE_MAX_PER_MINUTE` (optional) — free-probe budget.
885
+ * Default {@link DEFAULT_MAX_PROBES_PER_MINUTE}.
886
+ * - `METRIK_GATEWAY_PROBE_SIGNER_CACHE_SECONDS` (optional) — how long the
887
+ * on-chain signer set is cached. Default
888
+ * {@link DEFAULT_ORACLE_SIGNER_CACHE_SECONDS}.
889
+ */
890
+ export function parseGatewayProbeEnvConfig(
891
+ env: NodeJS.ProcessEnv = process.env,
892
+ ): CallerAuthGatewayProbeEnvConfig | undefined {
893
+ const raw = env.METRIK_GATEWAY_PROBE_CAPABILITY?.trim().toLowerCase();
894
+ if (raw === undefined || raw.length === 0) {
895
+ return undefined;
896
+ }
897
+ if (!["1", "true", "yes", "on"].includes(raw)) {
898
+ if (["0", "false", "no", "off"].includes(raw)) {
899
+ return undefined;
900
+ }
901
+ throw new Error(
902
+ `invalid METRIK_GATEWAY_PROBE_CAPABILITY: ${raw} (expected a boolean)`,
903
+ );
904
+ }
905
+
906
+ const maxProbesPerMinute = requirePositiveIntEnv(
907
+ env,
908
+ "METRIK_GATEWAY_PROBE_MAX_PER_MINUTE",
909
+ DEFAULT_MAX_PROBES_PER_MINUTE,
910
+ );
911
+ const signerCacheSeconds = requirePositiveIntEnv(
912
+ env,
913
+ "METRIK_GATEWAY_PROBE_SIGNER_CACHE_SECONDS",
914
+ DEFAULT_ORACLE_SIGNER_CACHE_SECONDS,
915
+ );
916
+ return { maxProbesPerMinute, signerCacheSeconds };
917
+ }
918
+
919
+ function requirePositiveIntEnv(
920
+ env: NodeJS.ProcessEnv,
921
+ name: string,
922
+ fallback: number,
923
+ ): number {
924
+ const raw = env[name]?.trim();
925
+ if (raw === undefined || raw.length === 0) {
926
+ return fallback;
927
+ }
928
+ const value = Number(raw);
929
+ if (!Number.isInteger(value) || value <= 0) {
930
+ throw new Error(`invalid ${name}: ${raw} (expected a positive integer)`);
931
+ }
932
+ return value;
933
+ }
934
+
366
935
  function requireEnv(env: NodeJS.ProcessEnv, name: string): string {
367
936
  const value = env[name];
368
937
  if (value === undefined || value.trim().length === 0) {
@@ -376,7 +945,14 @@ export async function startCallerAuthGatewayServerFromEnv(
376
945
  env: NodeJS.ProcessEnv = process.env,
377
946
  ): Promise<CallerAuthGatewayServer & { readonly boundPort: number }> {
378
947
  const config = parseCallerAuthGatewayEnvConfig(env);
948
+ // Fail closed at startup: a configured descriptor that does not verify is a
949
+ // refusal to start, never a silently unenforced price band.
950
+ const signedRateBounds =
951
+ config.signedDescriptorFile === undefined
952
+ ? undefined
953
+ : await loadSignedRateBoundsFile(config.signedDescriptorFile);
379
954
  const gateway = new CallerAuthGateway({
955
+ ...(signedRateBounds === undefined ? {} : { signedRateBounds }),
380
956
  escrowAddress: config.escrowAddress,
381
957
  rpcUrl: config.rpcUrl,
382
958
  serviceRef: config.serviceRef,
@@ -387,7 +963,32 @@ export async function startCallerAuthGatewayServerFromEnv(
387
963
  ...(config.headerName === undefined
388
964
  ? {}
389
965
  : { headerName: config.headerName }),
966
+ ...(config.probe === undefined
967
+ ? {}
968
+ : {
969
+ probe: {
970
+ maxProbesPerMinute: config.probe.maxProbesPerMinute,
971
+ signerCacheSeconds: config.probe.signerCacheSeconds,
972
+ },
973
+ }),
390
974
  });
975
+ if (signedRateBounds === undefined) {
976
+ console.error(
977
+ "[metrik-caller-auth-gateway] WARNING: no METRIK_GATEWAY_SIGNED_DESCRIPTOR_FILE — the " +
978
+ "operator-signed minRatePerSecond/maxRatePerSecond are NOT enforced here. The escrow " +
979
+ "accepts any non-zero rate, so a buyer who opens a stream directly against it can be " +
980
+ "served below your signed price (metrik-contracts#41).",
981
+ );
982
+ } else {
983
+ console.error(
984
+ `[metrik-caller-auth-gateway] enforcing operator-signed rate bounds for ${signedRateBounds
985
+ .map(
986
+ (bounds) =>
987
+ `${bounds.serviceRef} [${bounds.minRatePerSecond ?? "-"}, ${bounds.maxRatePerSecond ?? "-"}]`,
988
+ )
989
+ .join(", ")}`,
990
+ );
991
+ }
391
992
  if (config.corroboratingRpcUrls.length === 0) {
392
993
  console.error(
393
994
  "[metrik-caller-auth-gateway] WARNING: no METRIK_GATEWAY_CORROBORATING_RPC_URLS " +
@@ -397,9 +998,91 @@ export async function startCallerAuthGatewayServerFromEnv(
397
998
  "one INDEPENDENT provider to close that window.",
398
999
  );
399
1000
  }
1001
+ if (config.probe !== undefined) {
1002
+ console.error(
1003
+ "[metrik-caller-auth-gateway] oracle probe verification ENABLED: a request carrying a " +
1004
+ "valid EIP-712 ProbeCapability signed by an address in this escrow's on-chain " +
1005
+ `getOracleSigners() set is proxied FREE, unmetered and unbilled, at most ` +
1006
+ `${config.probe.maxProbesPerMinute} per minute. This is what lets the neutral ` +
1007
+ "verifier check this gateway's PAID route instead of only its health route. " +
1008
+ "Leaving it off is safe — the oracle then reports this listing's paid route as " +
1009
+ "UNVERIFIABLE rather than as passing.",
1010
+ );
1011
+ } else {
1012
+ console.error(
1013
+ "[metrik-caller-auth-gateway] oracle probe verification is OFF " +
1014
+ "(METRIK_GATEWAY_PROBE_CAPABILITY unset). The verifier can only reach this service's " +
1015
+ "health route, so paid-route delivery here is UNVERIFIED.",
1016
+ );
1017
+ }
1018
+ let x402Opener: GatewayX402OpenerMode | undefined;
1019
+ if (config.x402Opener !== undefined) {
1020
+ // Loaded only when enabled, so a capability-only gateway never imports @x402/*.
1021
+ const { buildGatewayX402OpenerMode } =
1022
+ await import("./x402-opener-mode.js");
1023
+ x402Opener = await buildGatewayX402OpenerMode(
1024
+ {
1025
+ facilitatorUrl: config.x402Opener.facilitatorUrl,
1026
+ facilitatorToken: config.x402Opener.facilitatorToken,
1027
+ network: config.x402Opener.network,
1028
+ factoryAddress: config.x402Opener.factoryAddress,
1029
+ escrowAddress: config.escrowAddress,
1030
+ asset: config.x402Opener.asset,
1031
+ assetName: config.x402Opener.assetName,
1032
+ assetVersion: config.x402Opener.assetVersion,
1033
+ terms: config.x402Opener.terms,
1034
+ price: config.x402Opener.price,
1035
+ maxTimeoutSeconds: config.x402Opener.maxTimeoutSeconds,
1036
+ facilitatorTimeoutMs: config.x402Opener.facilitatorTimeoutMs,
1037
+ ...(config.x402Opener.resourceBaseUrl === undefined
1038
+ ? {}
1039
+ : { resourceBaseUrl: config.x402Opener.resourceBaseUrl }),
1040
+ ...(config.x402Opener.registryUrl === undefined
1041
+ ? {}
1042
+ : { registryUrl: config.x402Opener.registryUrl }),
1043
+ ...(config.x402Opener.refundBaseUrl === undefined
1044
+ ? {}
1045
+ : { refundBaseUrl: config.x402Opener.refundBaseUrl }),
1046
+ ...(config.x402Opener.docsUrl === undefined
1047
+ ? {}
1048
+ : { docsUrl: config.x402Opener.docsUrl }),
1049
+ ...(config.x402Opener.assetDecimals === undefined
1050
+ ? {}
1051
+ : { assetDecimals: config.x402Opener.assetDecimals }),
1052
+ ...(config.x402Opener.description === undefined
1053
+ ? {}
1054
+ : { description: config.x402Opener.description }),
1055
+ ...(config.x402Opener.schemaDeclaration === undefined
1056
+ ? {}
1057
+ : { schemaDeclaration: config.x402Opener.schemaDeclaration }),
1058
+ },
1059
+ { rpcUrl: config.rpcUrl },
1060
+ );
1061
+ console.error(
1062
+ "[metrik-caller-auth-gateway] x402 v2 opener mode ENABLED: a request with neither a " +
1063
+ "capability nor PAYMENT-SIGNATURE gets a 402 quoting payTo " +
1064
+ `${x402Opener.payTo} (the escrow opener for serviceRef ${config.serviceRef}), price ` +
1065
+ `${config.x402Opener.price} base units. Payments settle into StreamEscrowV2 via the Metrik ` +
1066
+ `opener facilitator at ${config.x402Opener.facilitatorUrl}, and only after the upstream answers < 400. ` +
1067
+ `Refunds (no ETH needed, never automatic): ${x402Opener.refundBaseUrl}/streams/:streamId.`,
1068
+ );
1069
+ const refundHost = new URL(x402Opener.refundBaseUrl).hostname;
1070
+ if (
1071
+ refundHost === "localhost" ||
1072
+ refundHost === "[::1]" ||
1073
+ refundHost.startsWith("127.")
1074
+ ) {
1075
+ console.error(
1076
+ "[metrik-caller-auth-gateway] WARNING: the 402 advertises a loopback refund URL " +
1077
+ `(${x402Opener.refundBaseUrl}), which payers cannot reach. Set ` +
1078
+ "METRIK_GATEWAY_X402V2_REFUND_BASE_URL to the facilitator's public origin.",
1079
+ );
1080
+ }
1081
+ }
400
1082
  const server = createCallerAuthGatewayServer({
401
1083
  gateway,
402
1084
  upstreamUrl: config.upstreamUrl,
1085
+ ...(x402Opener === undefined ? {} : { x402Opener }),
403
1086
  });
404
1087
  const { port } = await server.listen(config.port, config.host);
405
1088
  return Object.assign(server, { boundPort: port });