@absol-labs/agent 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/README.md +184 -26
  2. package/dist/capability/invocation-capability.d.ts +18 -6
  3. package/dist/capability/invocation-capability.d.ts.map +1 -1
  4. package/dist/capability/invocation-capability.js.map +1 -1
  5. package/dist/cli/amounts.d.ts +26 -0
  6. package/dist/cli/amounts.d.ts.map +1 -0
  7. package/dist/cli/amounts.js +71 -0
  8. package/dist/cli/amounts.js.map +1 -0
  9. package/dist/cli/args.d.ts +32 -0
  10. package/dist/cli/args.d.ts.map +1 -0
  11. package/dist/cli/args.js +119 -0
  12. package/dist/cli/args.js.map +1 -0
  13. package/dist/cli/attesters.d.ts +51 -0
  14. package/dist/cli/attesters.d.ts.map +1 -0
  15. package/dist/cli/attesters.js +58 -0
  16. package/dist/cli/attesters.js.map +1 -0
  17. package/dist/cli/bin.d.ts +3 -0
  18. package/dist/cli/bin.d.ts.map +1 -0
  19. package/dist/cli/bin.js +5 -0
  20. package/dist/cli/bin.js.map +1 -0
  21. package/dist/cli/commands/claim.d.ts +13 -0
  22. package/dist/cli/commands/claim.d.ts.map +1 -0
  23. package/dist/cli/commands/claim.js +102 -0
  24. package/dist/cli/commands/claim.js.map +1 -0
  25. package/dist/cli/commands/close.d.ts +3 -0
  26. package/dist/cli/commands/close.d.ts.map +1 -0
  27. package/dist/cli/commands/close.js +55 -0
  28. package/dist/cli/commands/close.js.map +1 -0
  29. package/dist/cli/commands/discover.d.ts +3 -0
  30. package/dist/cli/commands/discover.d.ts.map +1 -0
  31. package/dist/cli/commands/discover.js +183 -0
  32. package/dist/cli/commands/discover.js.map +1 -0
  33. package/dist/cli/commands/invoke.d.ts +3 -0
  34. package/dist/cli/commands/invoke.d.ts.map +1 -0
  35. package/dist/cli/commands/invoke.js +142 -0
  36. package/dist/cli/commands/invoke.js.map +1 -0
  37. package/dist/cli/commands/mcp.d.ts +3 -0
  38. package/dist/cli/commands/mcp.d.ts.map +1 -0
  39. package/dist/cli/commands/mcp.js +113 -0
  40. package/dist/cli/commands/mcp.js.map +1 -0
  41. package/dist/cli/commands/open.d.ts +3 -0
  42. package/dist/cli/commands/open.d.ts.map +1 -0
  43. package/dist/cli/commands/open.js +331 -0
  44. package/dist/cli/commands/open.js.map +1 -0
  45. package/dist/cli/commands/reclaim-unverified.d.ts +3 -0
  46. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -0
  47. package/dist/cli/commands/reclaim-unverified.js +172 -0
  48. package/dist/cli/commands/reclaim-unverified.js.map +1 -0
  49. package/dist/cli/commands/reclaim.d.ts +33 -0
  50. package/dist/cli/commands/reclaim.d.ts.map +1 -0
  51. package/dist/cli/commands/reclaim.js +308 -0
  52. package/dist/cli/commands/reclaim.js.map +1 -0
  53. package/dist/cli/commands/status.d.ts +3 -0
  54. package/dist/cli/commands/status.d.ts.map +1 -0
  55. package/dist/cli/commands/status.js +43 -0
  56. package/dist/cli/commands/status.js.map +1 -0
  57. package/dist/cli/commands/streams.d.ts +3 -0
  58. package/dist/cli/commands/streams.d.ts.map +1 -0
  59. package/dist/cli/commands/streams.js +257 -0
  60. package/dist/cli/commands/streams.js.map +1 -0
  61. package/dist/cli/commands/watch.d.ts +3 -0
  62. package/dist/cli/commands/watch.d.ts.map +1 -0
  63. package/dist/cli/commands/watch.js +55 -0
  64. package/dist/cli/commands/watch.js.map +1 -0
  65. package/dist/cli/config.d.ts +61 -0
  66. package/dist/cli/config.d.ts.map +1 -0
  67. package/dist/cli/config.js +140 -0
  68. package/dist/cli/config.js.map +1 -0
  69. package/dist/cli/context.d.ts +29 -0
  70. package/dist/cli/context.d.ts.map +1 -0
  71. package/dist/cli/context.js +2 -0
  72. package/dist/cli/context.js.map +1 -0
  73. package/dist/cli/deps.d.ts +191 -0
  74. package/dist/cli/deps.d.ts.map +1 -0
  75. package/dist/cli/deps.js +277 -0
  76. package/dist/cli/deps.js.map +1 -0
  77. package/dist/cli/descriptor.d.ts +61 -0
  78. package/dist/cli/descriptor.d.ts.map +1 -0
  79. package/dist/cli/descriptor.js +78 -0
  80. package/dist/cli/descriptor.js.map +1 -0
  81. package/dist/cli/errors.d.ts +48 -0
  82. package/dist/cli/errors.d.ts.map +1 -0
  83. package/dist/cli/errors.js +129 -0
  84. package/dist/cli/errors.js.map +1 -0
  85. package/dist/cli/escape-hatch.d.ts +50 -0
  86. package/dist/cli/escape-hatch.d.ts.map +1 -0
  87. package/dist/cli/escape-hatch.js +45 -0
  88. package/dist/cli/escape-hatch.js.map +1 -0
  89. package/dist/cli/index.d.ts +8 -0
  90. package/dist/cli/index.d.ts.map +1 -0
  91. package/dist/cli/index.js +7 -0
  92. package/dist/cli/index.js.map +1 -0
  93. package/dist/cli/listings.d.ts +15 -0
  94. package/dist/cli/listings.d.ts.map +1 -0
  95. package/dist/cli/listings.js +56 -0
  96. package/dist/cli/listings.js.map +1 -0
  97. package/dist/cli/mandate.d.ts +66 -0
  98. package/dist/cli/mandate.d.ts.map +1 -0
  99. package/dist/cli/mandate.js +61 -0
  100. package/dist/cli/mandate.js.map +1 -0
  101. package/dist/cli/output.d.ts +33 -0
  102. package/dist/cli/output.d.ts.map +1 -0
  103. package/dist/cli/output.js +34 -0
  104. package/dist/cli/output.js.map +1 -0
  105. package/dist/cli/preflight.d.ts +38 -0
  106. package/dist/cli/preflight.d.ts.map +1 -0
  107. package/dist/cli/preflight.js +72 -0
  108. package/dist/cli/preflight.js.map +1 -0
  109. package/dist/cli/privy.d.ts +83 -0
  110. package/dist/cli/privy.d.ts.map +1 -0
  111. package/dist/cli/privy.js +230 -0
  112. package/dist/cli/privy.js.map +1 -0
  113. package/dist/cli/progress.d.ts +36 -0
  114. package/dist/cli/progress.d.ts.map +1 -0
  115. package/dist/cli/progress.js +40 -0
  116. package/dist/cli/progress.js.map +1 -0
  117. package/dist/cli/run.d.ts +25 -0
  118. package/dist/cli/run.d.ts.map +1 -0
  119. package/dist/cli/run.js +173 -0
  120. package/dist/cli/run.js.map +1 -0
  121. package/dist/cli/scan-range.d.ts +55 -0
  122. package/dist/cli/scan-range.d.ts.map +1 -0
  123. package/dist/cli/scan-range.js +57 -0
  124. package/dist/cli/scan-range.js.map +1 -0
  125. package/dist/cli/spend-policy.d.ts +60 -0
  126. package/dist/cli/spend-policy.d.ts.map +1 -0
  127. package/dist/cli/spend-policy.js +65 -0
  128. package/dist/cli/spend-policy.js.map +1 -0
  129. package/dist/cli/status.d.ts +90 -0
  130. package/dist/cli/status.d.ts.map +1 -0
  131. package/dist/cli/status.js +252 -0
  132. package/dist/cli/status.js.map +1 -0
  133. package/dist/cli/timeout.d.ts +30 -0
  134. package/dist/cli/timeout.d.ts.map +1 -0
  135. package/dist/cli/timeout.js +55 -0
  136. package/dist/cli/timeout.js.map +1 -0
  137. package/dist/cli/version.d.ts +12 -0
  138. package/dist/cli/version.d.ts.map +1 -0
  139. package/dist/cli/version.js +22 -0
  140. package/dist/cli/version.js.map +1 -0
  141. package/dist/cli/wallet.d.ts +164 -0
  142. package/dist/cli/wallet.d.ts.map +1 -0
  143. package/dist/cli/wallet.js +234 -0
  144. package/dist/cli/wallet.js.map +1 -0
  145. package/dist/frameworks/crewai.d.ts +10 -0
  146. package/dist/frameworks/crewai.d.ts.map +1 -1
  147. package/dist/frameworks/crewai.js +12 -2
  148. package/dist/frameworks/crewai.js.map +1 -1
  149. package/dist/gateway/caller-auth-gateway.d.ts +38 -3
  150. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  151. package/dist/gateway/caller-auth-gateway.js +25 -1
  152. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  153. package/dist/gateway/http-server.d.ts +53 -0
  154. package/dist/gateway/http-server.d.ts.map +1 -1
  155. package/dist/gateway/http-server.js +189 -4
  156. package/dist/gateway/http-server.js.map +1 -1
  157. package/dist/gateway/server-entry.d.ts +1 -0
  158. package/dist/gateway/server-entry.js +1 -0
  159. package/dist/gateway/server-entry.js.map +1 -1
  160. package/dist/gateway/x402-mode.d.ts +123 -0
  161. package/dist/gateway/x402-mode.d.ts.map +1 -0
  162. package/dist/gateway/x402-mode.js +212 -0
  163. package/dist/gateway/x402-mode.js.map +1 -0
  164. package/dist/index.d.ts +6 -4
  165. package/dist/index.d.ts.map +1 -1
  166. package/dist/index.js +8 -3
  167. package/dist/index.js.map +1 -1
  168. package/dist/mandates/env.d.ts +3 -2
  169. package/dist/mandates/env.d.ts.map +1 -1
  170. package/dist/mandates/env.js +3 -2
  171. package/dist/mandates/env.js.map +1 -1
  172. package/dist/mcp/http-server.d.ts +1 -0
  173. package/dist/mcp/http-server.js +1 -0
  174. package/dist/mcp/http-server.js.map +1 -1
  175. package/dist/mcp/server.d.ts +49 -0
  176. package/dist/mcp/server.d.ts.map +1 -1
  177. package/dist/mcp/server.js +533 -25
  178. package/dist/mcp/server.js.map +1 -1
  179. package/dist/mcp/service-invoker.d.ts +97 -0
  180. package/dist/mcp/service-invoker.d.ts.map +1 -0
  181. package/dist/mcp/service-invoker.js +167 -0
  182. package/dist/mcp/service-invoker.js.map +1 -0
  183. package/dist/mcp/stdio.d.ts +1 -0
  184. package/dist/mcp/stdio.js +1 -0
  185. package/dist/mcp/stdio.js.map +1 -1
  186. package/dist/sdk/client.d.ts +25 -1
  187. package/dist/sdk/client.d.ts.map +1 -1
  188. package/dist/sdk/client.js +20 -1
  189. package/dist/sdk/client.js.map +1 -1
  190. package/dist/sdk/invoke.d.ts +19 -4
  191. package/dist/sdk/invoke.d.ts.map +1 -1
  192. package/dist/sdk/invoke.js +69 -5
  193. package/dist/sdk/invoke.js.map +1 -1
  194. package/dist/x402/delivery-oracle.d.ts +108 -0
  195. package/dist/x402/delivery-oracle.d.ts.map +1 -0
  196. package/dist/x402/delivery-oracle.js +174 -0
  197. package/dist/x402/delivery-oracle.js.map +1 -0
  198. package/dist/x402/facilitator-server-entry.d.ts +3 -0
  199. package/dist/x402/facilitator-server-entry.d.ts.map +1 -0
  200. package/dist/x402/facilitator-server-entry.js +27 -0
  201. package/dist/x402/facilitator-server-entry.js.map +1 -0
  202. package/dist/x402/facilitator-server.d.ts +73 -0
  203. package/dist/x402/facilitator-server.d.ts.map +1 -0
  204. package/dist/x402/facilitator-server.js +305 -0
  205. package/dist/x402/facilitator-server.js.map +1 -0
  206. package/dist/x402/facilitator.d.ts +69 -17
  207. package/dist/x402/facilitator.d.ts.map +1 -1
  208. package/dist/x402/facilitator.js +76 -33
  209. package/dist/x402/facilitator.js.map +1 -1
  210. package/dist/x402/payment-requirements.d.ts +106 -0
  211. package/dist/x402/payment-requirements.d.ts.map +1 -0
  212. package/dist/x402/payment-requirements.js +138 -0
  213. package/dist/x402/payment-requirements.js.map +1 -0
  214. package/dist/x402/relayer.d.ts +41 -0
  215. package/dist/x402/relayer.d.ts.map +1 -0
  216. package/dist/x402/relayer.js +64 -0
  217. package/dist/x402/relayer.js.map +1 -0
  218. package/dist/x402/resource-index.d.ts +75 -0
  219. package/dist/x402/resource-index.d.ts.map +1 -0
  220. package/dist/x402/resource-index.js +126 -0
  221. package/dist/x402/resource-index.js.map +1 -0
  222. package/dist/x402/verified-delivery-facilitator.d.ts +285 -0
  223. package/dist/x402/verified-delivery-facilitator.d.ts.map +1 -0
  224. package/dist/x402/verified-delivery-facilitator.js +423 -0
  225. package/dist/x402/verified-delivery-facilitator.js.map +1 -0
  226. package/dist/zktls/t2-delivery-proof.d.ts +6 -6
  227. package/docs/agent-layer.md +14 -9
  228. package/docs/cli.md +705 -0
  229. package/docs/crewai.md +6 -2
  230. package/docs/eliza.md +6 -1
  231. package/docs/mcp-hosted.md +7 -5
  232. package/docs/mcp-reference.md +175 -0
  233. package/docs/quickstart.md +36 -9
  234. package/docs/threat-model.md +4 -3
  235. package/docs/x402.md +648 -0
  236. package/package.json +33 -10
  237. package/src/capability/invocation-capability.ts +17 -1
  238. package/src/cli/amounts.ts +102 -0
  239. package/src/cli/args.ts +150 -0
  240. package/src/cli/attesters.ts +86 -0
  241. package/src/cli/bin.ts +5 -0
  242. package/src/cli/commands/claim.ts +126 -0
  243. package/src/cli/commands/close.ts +69 -0
  244. package/src/cli/commands/discover.ts +228 -0
  245. package/src/cli/commands/invoke.ts +178 -0
  246. package/src/cli/commands/mcp.ts +135 -0
  247. package/src/cli/commands/open.ts +397 -0
  248. package/src/cli/commands/reclaim-unverified.ts +218 -0
  249. package/src/cli/commands/reclaim.ts +377 -0
  250. package/src/cli/commands/status.ts +56 -0
  251. package/src/cli/commands/streams.ts +344 -0
  252. package/src/cli/commands/watch.ts +70 -0
  253. package/src/cli/config.ts +208 -0
  254. package/src/cli/context.ts +30 -0
  255. package/src/cli/deps.ts +528 -0
  256. package/src/cli/descriptor.ts +133 -0
  257. package/src/cli/errors.ts +162 -0
  258. package/src/cli/escape-hatch.ts +92 -0
  259. package/src/cli/index.ts +27 -0
  260. package/src/cli/listings.ts +77 -0
  261. package/src/cli/mandate.ts +126 -0
  262. package/src/cli/output.ts +63 -0
  263. package/src/cli/preflight.ts +88 -0
  264. package/src/cli/privy.ts +396 -0
  265. package/src/cli/progress.ts +75 -0
  266. package/src/cli/run.ts +209 -0
  267. package/src/cli/scan-range.ts +76 -0
  268. package/src/cli/spend-policy.ts +139 -0
  269. package/src/cli/status.ts +381 -0
  270. package/src/cli/timeout.ts +64 -0
  271. package/src/cli/version.ts +21 -0
  272. package/src/cli/wallet.ts +372 -0
  273. package/src/frameworks/crewai.ts +13 -2
  274. package/src/gateway/caller-auth-gateway.ts +77 -6
  275. package/src/gateway/http-server.ts +280 -6
  276. package/src/gateway/server-entry.ts +1 -0
  277. package/src/gateway/x402-mode.ts +350 -0
  278. package/src/index.ts +40 -5
  279. package/src/mandates/env.ts +3 -2
  280. package/src/mcp/http-server.ts +1 -0
  281. package/src/mcp/server.ts +706 -30
  282. package/src/mcp/service-invoker.ts +285 -0
  283. package/src/mcp/stdio.ts +1 -0
  284. package/src/sdk/client.ts +33 -1
  285. package/src/sdk/invoke.ts +90 -12
  286. package/src/x402/delivery-oracle.ts +285 -0
  287. package/src/x402/facilitator-server-entry.ts +35 -0
  288. package/src/x402/facilitator-server.ts +445 -0
  289. package/src/x402/facilitator.ts +125 -46
  290. package/src/x402/payment-requirements.ts +231 -0
  291. package/src/x402/relayer.ts +122 -0
  292. package/src/x402/resource-index.ts +228 -0
  293. package/src/x402/verified-delivery-facilitator.ts +744 -0
@@ -12,8 +12,18 @@ 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
+ createHttpFacilitatorClient,
20
+ createListingPriceQuoter,
21
+ encodeSettleResponseHeader,
22
+ X402_PAYMENT_RESPONSE_HEADER,
23
+ X402_SETTLEMENT_HEADROOM_SECONDS,
24
+ type GatewayX402Mode,
25
+ } from "./x402-mode.js";
26
+ import { discoverServices } from "../discovery/registry.js";
17
27
 
18
28
  /**
19
29
  * Reference standalone reverse-proxy gateway server (metrik-agent#63).
@@ -49,6 +59,12 @@ export interface CallerAuthGatewayServerConfig {
49
59
  readonly upstreamUrl: string;
50
60
  /** Extra request-timeout for the upstream fetch, in ms. Default 30000. */
51
61
  readonly upstreamTimeoutMs?: number;
62
+ /**
63
+ * The same {@link GatewayX402Mode} given to the gateway. Required for the
64
+ * x402 path: the gateway VERIFIES a payment, this server SETTLES it once the
65
+ * upstream has produced a response.
66
+ */
67
+ readonly x402?: GatewayX402Mode;
52
68
  }
53
69
 
54
70
  export interface CallerAuthGatewayServer {
@@ -70,6 +86,7 @@ export function createCallerAuthGatewayServer(
70
86
  config.gateway,
71
87
  upstreamBase,
72
88
  upstreamTimeoutMs,
89
+ config.x402,
73
90
  ).catch((error) => {
74
91
  logError("unhandled request error", error);
75
92
  if (!res.headersSent) {
@@ -105,6 +122,7 @@ async function handleRequest(
105
122
  gateway: CallerAuthGateway,
106
123
  upstreamBase: URL,
107
124
  upstreamTimeoutMs: number,
125
+ x402: GatewayX402Mode | undefined,
108
126
  ): Promise<void> {
109
127
  const requestUrl = new URL(req.url ?? "/", "http://localhost");
110
128
  const method = req.method ?? "GET";
@@ -117,6 +135,14 @@ async function handleRequest(
117
135
  });
118
136
 
119
137
  if (!decision.authorized) {
138
+ // The x402 challenge is a PROTOCOL body, not Metrik's `{authorized,reason}`
139
+ // shape: `x402-fetch` parses `{x402Version, accepts}` and nothing else.
140
+ if (decision.x402PaymentRequired !== undefined) {
141
+ res
142
+ .writeHead(402, { "Content-Type": "application/json" })
143
+ .end(JSON.stringify(decision.x402PaymentRequired));
144
+ return;
145
+ }
120
146
  writeJsonError(
121
147
  res,
122
148
  decision.httpStatus ?? 403,
@@ -143,8 +169,23 @@ async function handleRequest(
143
169
  upstreamBase,
144
170
  );
145
171
 
172
+ // Bound the upstream by the payment's OWN deadline, not just by the server's
173
+ // timeout. Settlement happens after the upstream returns, so an upstream that
174
+ // outlives the signed authorization would produce its side effects and then
175
+ // be unsettleable — the caller would get the work for free (never the response
176
+ // body, which is only flushed after settlement, but the side effects are
177
+ // real). Aborting early turns that into an ordinary 502.
178
+ const effectiveTimeoutMs = boundedUpstreamTimeoutMs(
179
+ upstreamTimeoutMs,
180
+ decision.x402Payment?.validBefore,
181
+ );
182
+ if (effectiveTimeoutMs <= 0) {
183
+ writeJsonError(res, 402, "x402-authorization-too-short");
184
+ return;
185
+ }
186
+
146
187
  const controller = new AbortController();
147
- const timeout = setTimeout(() => controller.abort(), upstreamTimeoutMs);
188
+ const timeout = setTimeout(() => controller.abort(), effectiveTimeoutMs);
148
189
  try {
149
190
  const upstreamResponse = await fetch(upstreamUrl, {
150
191
  method,
@@ -153,11 +194,26 @@ async function handleRequest(
153
194
  signal: controller.signal,
154
195
  });
155
196
 
156
- res.writeHead(
157
- upstreamResponse.status,
158
- headersToObject(upstreamResponse.headers),
159
- );
160
197
  const buffer = Buffer.from(await upstreamResponse.arrayBuffer());
198
+ const responseHeaders = headersToObject(upstreamResponse.headers);
199
+
200
+ // Settle AFTER the upstream produced a response and BEFORE that response is
201
+ // flushed. Buyer-favouring on both edges: a call that never produced a
202
+ // response is never charged for, and a payment that cannot be settled
203
+ // returns `402` instead of handing over the goods for free.
204
+ if (decision.x402Payment !== undefined) {
205
+ const settled = await settleX402(decision, x402);
206
+ if (settled === undefined) {
207
+ writeJsonError(res, 402, "x402-settlement-failed");
208
+ return;
209
+ }
210
+ responseHeaders[X402_PAYMENT_RESPONSE_HEADER] =
211
+ encodeSettleResponseHeader(settled);
212
+ responseHeaders["access-control-expose-headers"] =
213
+ X402_PAYMENT_RESPONSE_HEADER;
214
+ }
215
+
216
+ res.writeHead(upstreamResponse.status, responseHeaders);
161
217
  res.end(buffer);
162
218
  } catch (error) {
163
219
  logError("upstream request failed", error);
@@ -167,6 +223,67 @@ async function handleRequest(
167
223
  }
168
224
  }
169
225
 
226
+ /**
227
+ * The upstream budget, capped so the signed authorization is still settleable
228
+ * when the call returns. Returns <= 0 when there is no usable window at all.
229
+ */
230
+ export function boundedUpstreamTimeoutMs(
231
+ upstreamTimeoutMs: number,
232
+ validBeforeSeconds: number | undefined,
233
+ nowSeconds: number = Math.floor(Date.now() / 1000),
234
+ ): number {
235
+ if (validBeforeSeconds === undefined) {
236
+ return upstreamTimeoutMs;
237
+ }
238
+ const remainingSeconds =
239
+ validBeforeSeconds - nowSeconds - X402_SETTLEMENT_HEADROOM_SECONDS;
240
+ if (remainingSeconds <= 0) {
241
+ return 0;
242
+ }
243
+ return Math.min(upstreamTimeoutMs, remainingSeconds * 1000);
244
+ }
245
+
246
+ /** Returns the settlement result, or `undefined` if nothing was settled. */
247
+ async function settleX402(
248
+ decision: CallerAuthDecision,
249
+ x402: GatewayX402Mode | undefined,
250
+ ): Promise<
251
+ Awaited<ReturnType<GatewayX402Mode["facilitator"]["settle"]>> | undefined
252
+ > {
253
+ const payment = decision.x402Payment;
254
+ if (payment === undefined) {
255
+ return undefined;
256
+ }
257
+ if (x402 === undefined) {
258
+ // The gateway accepted an x402 payment but this server was given no
259
+ // facilitator to settle it with. Refuse: serving here would be free service.
260
+ logError(
261
+ "x402 settlement",
262
+ new Error(
263
+ "gateway accepted an x402 payment but the server has no x402 facilitator configured",
264
+ ),
265
+ );
266
+ return undefined;
267
+ }
268
+ try {
269
+ const settled = await x402.facilitator.settle(
270
+ payment.payload,
271
+ payment.requirements,
272
+ );
273
+ if (settled.success !== true) {
274
+ logError(
275
+ "x402 settlement",
276
+ new Error(settled.errorReason ?? "facilitator reported failure"),
277
+ );
278
+ return undefined;
279
+ }
280
+ return settled;
281
+ } catch (error) {
282
+ logError("x402 settlement", error);
283
+ return undefined;
284
+ }
285
+ }
286
+
170
287
  function normalizeIncomingHeaders(
171
288
  headers: IncomingMessage["headers"],
172
289
  ): Record<string, string | undefined> {
@@ -210,7 +327,9 @@ function writeJsonError(
210
327
  | CallerAuthDenialReason
211
328
  | "unauthorized"
212
329
  | "internal-error"
213
- | "upstream-unavailable",
330
+ | "upstream-unavailable"
331
+ | "x402-settlement-failed"
332
+ | "x402-authorization-too-short",
214
333
  ): void {
215
334
  if (res.headersSent) {
216
335
  res.end();
@@ -242,6 +361,22 @@ export interface CallerAuthGatewayEnvConfig {
242
361
  readonly corroboratingRpcUrls: readonly string[];
243
362
  /** Freshness bound for on-chain stream state, in seconds. */
244
363
  readonly maxStateStalenessSeconds: number;
364
+ /** Set only when standard-x402 mode is enabled. */
365
+ readonly x402?: CallerAuthGatewayX402EnvConfig;
366
+ }
367
+
368
+ export interface CallerAuthGatewayX402EnvConfig {
369
+ /** Per-call price in the asset's base units (micro-USDC). Seller-set, unsigned. */
370
+ readonly pricePerCallBaseUnits: bigint;
371
+ /** A facilitator exposing `/verify` and `/settle` — e.g. Metrik's own. */
372
+ readonly facilitatorUrl: string;
373
+ /** The public origin buyers reach this gateway on. Becomes `resource`. */
374
+ readonly resourceBaseUrl: string;
375
+ /** Listing source used to derive `payTo` from the operator-signed descriptor. */
376
+ readonly registryUrl?: string;
377
+ readonly maxTimeoutSeconds?: number;
378
+ /** Bearer token for the facilitator's `/settle`. Required by Metrik's own. */
379
+ readonly facilitatorToken?: string;
245
380
  }
246
381
 
247
382
  /**
@@ -346,6 +481,8 @@ export function parseCallerAuthGatewayEnvConfig(
346
481
  );
347
482
  }
348
483
 
484
+ const x402 = parseGatewayX402EnvConfig(env);
485
+
349
486
  return {
350
487
  host,
351
488
  port,
@@ -360,6 +497,132 @@ export function parseCallerAuthGatewayEnvConfig(
360
497
  corroboratingRpcUrls,
361
498
  maxStateStalenessSeconds,
362
499
  ...(headerName === undefined ? {} : { headerName }),
500
+ ...(x402 === undefined ? {} : { x402 }),
501
+ };
502
+ }
503
+
504
+ /**
505
+ * x402 mode is OFF unless `METRIK_GATEWAY_X402_PRICE_BASE_UNITS` is set. Once
506
+ * on, every other x402 var is required: a half-configured paywall would answer
507
+ * `402` with a quote it cannot settle.
508
+ *
509
+ * Note what is NOT here: `payTo`. It is never configurable — it is resolved at
510
+ * startup from the operator-signed descriptor for this gateway's `serviceRef`
511
+ * (see {@link resolveSignedListingPayee}), and the gateway refuses to start if
512
+ * that lookup fails.
513
+ */
514
+ export function parseGatewayX402EnvConfig(
515
+ env: NodeJS.ProcessEnv = process.env,
516
+ ): CallerAuthGatewayX402EnvConfig | undefined {
517
+ const rawPrice = env.METRIK_GATEWAY_X402_PRICE_BASE_UNITS?.trim();
518
+ if (rawPrice === undefined || rawPrice.length === 0) {
519
+ return undefined;
520
+ }
521
+ let pricePerCallBaseUnits: bigint;
522
+ try {
523
+ pricePerCallBaseUnits = BigInt(rawPrice);
524
+ } catch {
525
+ throw new Error(
526
+ `invalid METRIK_GATEWAY_X402_PRICE_BASE_UNITS: ${rawPrice} (integer base units, e.g. 1000 = 0.001 USDC)`,
527
+ );
528
+ }
529
+ if (pricePerCallBaseUnits <= 0n) {
530
+ throw new Error("METRIK_GATEWAY_X402_PRICE_BASE_UNITS must be positive");
531
+ }
532
+ const facilitatorUrl = requireEnv(env, "METRIK_GATEWAY_X402_FACILITATOR_URL");
533
+ const resourceBaseUrl = requireEnv(
534
+ env,
535
+ "METRIK_GATEWAY_X402_RESOURCE_BASE_URL",
536
+ );
537
+ for (const candidate of [facilitatorUrl, resourceBaseUrl]) {
538
+ // eslint-disable-next-line no-new -- validates the URL is well-formed.
539
+ new URL(candidate);
540
+ }
541
+ const registryUrl = env.METRIK_GATEWAY_X402_REGISTRY_URL;
542
+ const facilitatorToken = env.METRIK_GATEWAY_X402_FACILITATOR_TOKEN?.trim();
543
+ const rawTimeout = env.METRIK_GATEWAY_X402_MAX_TIMEOUT_SECONDS;
544
+ let maxTimeoutSeconds: number | undefined;
545
+ if (rawTimeout !== undefined && rawTimeout.trim().length > 0) {
546
+ maxTimeoutSeconds = Number(rawTimeout);
547
+ if (!Number.isInteger(maxTimeoutSeconds) || maxTimeoutSeconds <= 0) {
548
+ throw new Error(
549
+ `invalid METRIK_GATEWAY_X402_MAX_TIMEOUT_SECONDS: ${rawTimeout}`,
550
+ );
551
+ }
552
+ }
553
+
554
+ return {
555
+ pricePerCallBaseUnits,
556
+ facilitatorUrl,
557
+ resourceBaseUrl,
558
+ ...(registryUrl === undefined ? {} : { registryUrl }),
559
+ ...(maxTimeoutSeconds === undefined ? {} : { maxTimeoutSeconds }),
560
+ ...(facilitatorToken === undefined || facilitatorToken.length === 0
561
+ ? {}
562
+ : { facilitatorToken }),
563
+ };
564
+ }
565
+
566
+ export class UnresolvedListingPayeeError extends Error {
567
+ constructor(serviceRef: string) {
568
+ super(
569
+ `no operator-signed listing found for serviceRef ${serviceRef}. ` +
570
+ "x402 mode refuses to start: payTo is derived from the signed descriptor and " +
571
+ "is never taken from configuration, so an unresolved listing has no payee.",
572
+ );
573
+ this.name = "UnresolvedListingPayeeError";
574
+ }
575
+ }
576
+
577
+ /**
578
+ * Resolves `{serviceRef, operator}` from the registry. `discoverServices`
579
+ * re-derives `serviceRef` from the signed content and recovers the operator's
580
+ * EIP-712 signature, dropping anything that fails — so what comes back here is
581
+ * signed, not merely served.
582
+ */
583
+ export async function resolveSignedListingPayee(
584
+ serviceRef: Hex,
585
+ registryUrl?: string,
586
+ ): Promise<{ readonly serviceRef: Hex; readonly operator: Address }> {
587
+ const listings = await discoverServices(
588
+ registryUrl === undefined ? {} : { registryUrl },
589
+ );
590
+ const match = listings.find(
591
+ (listing) => listing.serviceRef.toLowerCase() === serviceRef.toLowerCase(),
592
+ );
593
+ if (match === undefined) {
594
+ throw new UnresolvedListingPayeeError(serviceRef);
595
+ }
596
+ return { serviceRef: match.serviceRef, operator: match.operator };
597
+ }
598
+
599
+ /** Builds the gateway's x402 mode, deriving `payTo` from the signed listing. */
600
+ export async function buildGatewayX402Mode(
601
+ config: CallerAuthGatewayEnvConfig,
602
+ ): Promise<GatewayX402Mode | undefined> {
603
+ if (config.x402 === undefined) {
604
+ return undefined;
605
+ }
606
+ const listing = await resolveSignedListingPayee(
607
+ config.serviceRef,
608
+ config.x402.registryUrl,
609
+ );
610
+ return {
611
+ quote: createListingPriceQuoter({
612
+ listing,
613
+ resourceBaseUrl: config.x402.resourceBaseUrl,
614
+ pricePerCallBaseUnits: config.x402.pricePerCallBaseUnits,
615
+ chainId: config.chainId,
616
+ ...(config.x402.maxTimeoutSeconds === undefined
617
+ ? {}
618
+ : { maxTimeoutSeconds: config.x402.maxTimeoutSeconds }),
619
+ }),
620
+ facilitator: createHttpFacilitatorClient({
621
+ url: config.x402.facilitatorUrl,
622
+ ...(config.x402.facilitatorToken === undefined
623
+ ? {}
624
+ : { token: config.x402.facilitatorToken }),
625
+ }),
363
626
  };
364
627
  }
365
628
 
@@ -376,6 +639,7 @@ export async function startCallerAuthGatewayServerFromEnv(
376
639
  env: NodeJS.ProcessEnv = process.env,
377
640
  ): Promise<CallerAuthGatewayServer & { readonly boundPort: number }> {
378
641
  const config = parseCallerAuthGatewayEnvConfig(env);
642
+ const x402 = await buildGatewayX402Mode(config);
379
643
  const gateway = new CallerAuthGateway({
380
644
  escrowAddress: config.escrowAddress,
381
645
  rpcUrl: config.rpcUrl,
@@ -387,6 +651,7 @@ export async function startCallerAuthGatewayServerFromEnv(
387
651
  ...(config.headerName === undefined
388
652
  ? {}
389
653
  : { headerName: config.headerName }),
654
+ ...(x402 === undefined ? {} : { x402 }),
390
655
  });
391
656
  if (config.corroboratingRpcUrls.length === 0) {
392
657
  console.error(
@@ -397,9 +662,18 @@ export async function startCallerAuthGatewayServerFromEnv(
397
662
  "one INDEPENDENT provider to close that window.",
398
663
  );
399
664
  }
665
+ if (x402 !== undefined && config.x402 !== undefined) {
666
+ console.error(
667
+ "[metrik-caller-auth-gateway] standard x402 mode ENABLED: callers with no Metrik " +
668
+ `capability are quoted ${config.x402.pricePerCallBaseUnits} base units per call, ` +
669
+ `settled through ${config.x402.facilitatorUrl}. This is a per-call payment with NO ` +
670
+ "escrow and NO recoverability — the stream path remains the only recoverable one.",
671
+ );
672
+ }
400
673
  const server = createCallerAuthGatewayServer({
401
674
  gateway,
402
675
  upstreamUrl: config.upstreamUrl,
676
+ ...(x402 === undefined ? {} : { x402 }),
403
677
  });
404
678
  const { port } = await server.listen(config.port, config.host);
405
679
  return Object.assign(server, { boundPort: port });
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import { startCallerAuthGatewayServerFromEnv } from "./http-server.js";
2
3
 
3
4
  /**