@absol-labs/agent 0.11.0 → 0.12.0-rc.2

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 (366) hide show
  1. package/README.md +43 -51
  2. package/dist/capability/invocation-capability.d.ts +2 -2
  3. package/dist/capability/probe-capability.d.ts +225 -0
  4. package/dist/capability/probe-capability.d.ts.map +1 -0
  5. package/dist/capability/probe-capability.js +248 -0
  6. package/dist/capability/probe-capability.js.map +1 -0
  7. package/dist/cli/args.d.ts +11 -0
  8. package/dist/cli/args.d.ts.map +1 -1
  9. package/dist/cli/args.js +23 -0
  10. package/dist/cli/args.js.map +1 -1
  11. package/dist/cli/commands/claim.d.ts +32 -0
  12. package/dist/cli/commands/claim.d.ts.map +1 -1
  13. package/dist/cli/commands/claim.js +116 -28
  14. package/dist/cli/commands/claim.js.map +1 -1
  15. package/dist/cli/commands/close.d.ts.map +1 -1
  16. package/dist/cli/commands/close.js +35 -3
  17. package/dist/cli/commands/close.js.map +1 -1
  18. package/dist/cli/commands/invoke.d.ts +2 -0
  19. package/dist/cli/commands/invoke.d.ts.map +1 -1
  20. package/dist/cli/commands/invoke.js +2 -2
  21. package/dist/cli/commands/invoke.js.map +1 -1
  22. package/dist/cli/commands/mcp.d.ts.map +1 -1
  23. package/dist/cli/commands/mcp.js +15 -2
  24. package/dist/cli/commands/mcp.js.map +1 -1
  25. package/dist/cli/commands/open.d.ts.map +1 -1
  26. package/dist/cli/commands/open.js +58 -4
  27. package/dist/cli/commands/open.js.map +1 -1
  28. package/dist/cli/commands/pay.d.ts +34 -0
  29. package/dist/cli/commands/pay.d.ts.map +1 -0
  30. package/dist/cli/commands/pay.js +479 -0
  31. package/dist/cli/commands/pay.js.map +1 -0
  32. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
  33. package/dist/cli/commands/reclaim-unverified.js +3 -2
  34. package/dist/cli/commands/reclaim-unverified.js.map +1 -1
  35. package/dist/cli/commands/reclaim.d.ts +23 -2
  36. package/dist/cli/commands/reclaim.d.ts.map +1 -1
  37. package/dist/cli/commands/reclaim.js +204 -20
  38. package/dist/cli/commands/reclaim.js.map +1 -1
  39. package/dist/cli/commands/refund.d.ts +31 -0
  40. package/dist/cli/commands/refund.d.ts.map +1 -0
  41. package/dist/cli/commands/refund.js +397 -0
  42. package/dist/cli/commands/refund.js.map +1 -0
  43. package/dist/cli/commands/status.d.ts.map +1 -1
  44. package/dist/cli/commands/status.js +21 -2
  45. package/dist/cli/commands/status.js.map +1 -1
  46. package/dist/cli/commands/streams.js +8 -12
  47. package/dist/cli/commands/streams.js.map +1 -1
  48. package/dist/cli/commands/watch.d.ts.map +1 -1
  49. package/dist/cli/commands/watch.js +4 -1
  50. package/dist/cli/commands/watch.js.map +1 -1
  51. package/dist/cli/config.d.ts +44 -26
  52. package/dist/cli/config.d.ts.map +1 -1
  53. package/dist/cli/config.js +88 -38
  54. package/dist/cli/config.js.map +1 -1
  55. package/dist/cli/context.d.ts +19 -0
  56. package/dist/cli/context.d.ts.map +1 -1
  57. package/dist/cli/deps.d.ts +53 -0
  58. package/dist/cli/deps.d.ts.map +1 -1
  59. package/dist/cli/deps.js +51 -1
  60. package/dist/cli/deps.js.map +1 -1
  61. package/dist/cli/errors.d.ts.map +1 -1
  62. package/dist/cli/errors.js +21 -1
  63. package/dist/cli/errors.js.map +1 -1
  64. package/dist/cli/escape-hatch.d.ts +6 -4
  65. package/dist/cli/escape-hatch.d.ts.map +1 -1
  66. package/dist/cli/escape-hatch.js +6 -2
  67. package/dist/cli/escape-hatch.js.map +1 -1
  68. package/dist/cli/listings.d.ts +18 -0
  69. package/dist/cli/listings.d.ts.map +1 -1
  70. package/dist/cli/listings.js +22 -12
  71. package/dist/cli/listings.js.map +1 -1
  72. package/dist/cli/preflight.d.ts +20 -14
  73. package/dist/cli/preflight.d.ts.map +1 -1
  74. package/dist/cli/preflight.js +34 -20
  75. package/dist/cli/preflight.js.map +1 -1
  76. package/dist/cli/privy.d.ts.map +1 -1
  77. package/dist/cli/privy.js +4 -2
  78. package/dist/cli/privy.js.map +1 -1
  79. package/dist/cli/progress.d.ts +13 -1
  80. package/dist/cli/progress.d.ts.map +1 -1
  81. package/dist/cli/progress.js +7 -1
  82. package/dist/cli/progress.js.map +1 -1
  83. package/dist/cli/reclaimability.d.ts +82 -0
  84. package/dist/cli/reclaimability.d.ts.map +1 -0
  85. package/dist/cli/reclaimability.js +80 -0
  86. package/dist/cli/reclaimability.js.map +1 -0
  87. package/dist/cli/run.d.ts +2 -1
  88. package/dist/cli/run.d.ts.map +1 -1
  89. package/dist/cli/run.js +62 -21
  90. package/dist/cli/run.js.map +1 -1
  91. package/dist/cli/scan-range.d.ts +2 -2
  92. package/dist/cli/scan-range.js +2 -2
  93. package/dist/cli/status.d.ts +106 -1
  94. package/dist/cli/status.d.ts.map +1 -1
  95. package/dist/cli/status.js +325 -44
  96. package/dist/cli/status.js.map +1 -1
  97. package/dist/cli/wallet.d.ts +2 -2
  98. package/dist/cli/wallet.d.ts.map +1 -1
  99. package/dist/cli/wallet.js +29 -5
  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 +83 -31
  121. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  122. package/dist/gateway/caller-auth-gateway.js +180 -27
  123. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  124. package/dist/gateway/http-server.d.ts +128 -44
  125. package/dist/gateway/http-server.d.ts.map +1 -1
  126. package/dist/gateway/http-server.js +506 -155
  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 +252 -0
  137. package/dist/gateway/x402-opener-mode.d.ts.map +1 -0
  138. package/dist/gateway/x402-opener-mode.js +772 -0
  139. package/dist/gateway/x402-opener-mode.js.map +1 -0
  140. package/dist/index.d.ts +6 -2
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +10 -5
  143. package/dist/index.js.map +1 -1
  144. package/dist/mcp/server.d.ts +64 -1
  145. package/dist/mcp/server.d.ts.map +1 -1
  146. package/dist/mcp/server.js +247 -37
  147. package/dist/mcp/server.js.map +1 -1
  148. package/dist/mcp/x402-tools.d.ts +109 -0
  149. package/dist/mcp/x402-tools.d.ts.map +1 -0
  150. package/dist/mcp/x402-tools.js +554 -0
  151. package/dist/mcp/x402-tools.js.map +1 -0
  152. package/dist/network.d.ts +119 -0
  153. package/dist/network.d.ts.map +1 -0
  154. package/dist/network.js +199 -0
  155. package/dist/network.js.map +1 -0
  156. package/dist/sdk/client.d.ts +13 -0
  157. package/dist/sdk/client.d.ts.map +1 -1
  158. package/dist/sdk/client.js +15 -0
  159. package/dist/sdk/client.js.map +1 -1
  160. package/dist/sdk/invoke.d.ts +4 -0
  161. package/dist/sdk/invoke.d.ts.map +1 -1
  162. package/dist/sdk/invoke.js +2 -5
  163. package/dist/sdk/invoke.js.map +1 -1
  164. package/dist/wallet/autonomous-wallet-broker.d.ts +3 -0
  165. package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -1
  166. package/dist/wallet/autonomous-wallet-broker.js +27 -23
  167. package/dist/wallet/autonomous-wallet-broker.js.map +1 -1
  168. package/dist/wallet/autonomous-wallet-protocol.d.ts +8 -2
  169. package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -1
  170. package/dist/wallet/autonomous-wallet-protocol.js +9 -2
  171. package/dist/wallet/autonomous-wallet-protocol.js.map +1 -1
  172. package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
  173. package/dist/wallet/autonomous-wallet.js +3 -2
  174. package/dist/wallet/autonomous-wallet.js.map +1 -1
  175. package/dist/wallet/broker-escrows.d.ts +222 -0
  176. package/dist/wallet/broker-escrows.d.ts.map +1 -0
  177. package/dist/wallet/broker-escrows.js +36 -0
  178. package/dist/wallet/broker-escrows.js.map +1 -0
  179. package/dist/wallet/lifecycle.d.ts +21 -5
  180. package/dist/wallet/lifecycle.d.ts.map +1 -1
  181. package/dist/wallet/lifecycle.js +32 -12
  182. package/dist/wallet/lifecycle.js.map +1 -1
  183. package/dist/wallet/privy-broker-server.d.ts.map +1 -1
  184. package/dist/wallet/privy-broker-server.js +31 -26
  185. package/dist/wallet/privy-broker-server.js.map +1 -1
  186. package/dist/wallet/privy-session-broker.d.ts +14 -5
  187. package/dist/wallet/privy-session-broker.d.ts.map +1 -1
  188. package/dist/wallet/privy-session-broker.js +29 -17
  189. package/dist/wallet/privy-session-broker.js.map +1 -1
  190. package/dist/wallet/privy-session-provider.d.ts +3 -1
  191. package/dist/wallet/privy-session-provider.d.ts.map +1 -1
  192. package/dist/wallet/privy-session-provider.js +5 -3
  193. package/dist/wallet/privy-session-provider.js.map +1 -1
  194. package/dist/wallet/provider.d.ts.map +1 -1
  195. package/dist/wallet/provider.js +15 -8
  196. package/dist/wallet/provider.js.map +1 -1
  197. package/dist/x402/facilitator.d.ts +3 -30
  198. package/dist/x402/facilitator.d.ts.map +1 -1
  199. package/dist/x402/facilitator.js +0 -27
  200. package/dist/x402/facilitator.js.map +1 -1
  201. package/dist/x402-buyer/guarded-fetch.d.ts +51 -0
  202. package/dist/x402-buyer/guarded-fetch.d.ts.map +1 -0
  203. package/dist/x402-buyer/guarded-fetch.js +147 -0
  204. package/dist/x402-buyer/guarded-fetch.js.map +1 -0
  205. package/dist/x402-buyer/index.d.ts +10 -0
  206. package/dist/x402-buyer/index.d.ts.map +1 -0
  207. package/dist/x402-buyer/index.js +10 -0
  208. package/dist/x402-buyer/index.js.map +1 -0
  209. package/dist/x402-buyer/pay.d.ts +157 -0
  210. package/dist/x402-buyer/pay.d.ts.map +1 -0
  211. package/dist/x402-buyer/pay.js +366 -0
  212. package/dist/x402-buyer/pay.js.map +1 -0
  213. package/dist/x402-opener/abi.d.ts +684 -0
  214. package/dist/x402-opener/abi.d.ts.map +1 -0
  215. package/dist/x402-opener/abi.js +880 -0
  216. package/dist/x402-opener/abi.js.map +1 -0
  217. package/dist/x402-opener/facilitator-entry.d.ts +3 -0
  218. package/dist/x402-opener/facilitator-entry.d.ts.map +1 -0
  219. package/dist/x402-opener/facilitator-entry.js +40 -0
  220. package/dist/x402-opener/facilitator-entry.js.map +1 -0
  221. package/dist/x402-opener/facilitator-server.d.ts +209 -0
  222. package/dist/x402-opener/facilitator-server.d.ts.map +1 -0
  223. package/dist/x402-opener/facilitator-server.js +762 -0
  224. package/dist/x402-opener/facilitator-server.js.map +1 -0
  225. package/dist/x402-opener/index.d.ts +14 -0
  226. package/dist/x402-opener/index.d.ts.map +1 -0
  227. package/dist/x402-opener/index.js +14 -0
  228. package/dist/x402-opener/index.js.map +1 -0
  229. package/dist/x402-opener/listing-resolver.d.ts +55 -0
  230. package/dist/x402-opener/listing-resolver.d.ts.map +1 -0
  231. package/dist/x402-opener/listing-resolver.js +125 -0
  232. package/dist/x402-opener/listing-resolver.js.map +1 -0
  233. package/dist/x402-opener/refund-relay.d.ts +266 -0
  234. package/dist/x402-opener/refund-relay.d.ts.map +1 -0
  235. package/dist/x402-opener/refund-relay.js +1341 -0
  236. package/dist/x402-opener/refund-relay.js.map +1 -0
  237. package/dist/x402-opener/relayer-queue.d.ts +14 -0
  238. package/dist/x402-opener/relayer-queue.d.ts.map +1 -0
  239. package/dist/x402-opener/relayer-queue.js +18 -0
  240. package/dist/x402-opener/relayer-queue.js.map +1 -0
  241. package/dist/x402-opener/scheme.d.ts +296 -0
  242. package/dist/x402-opener/scheme.d.ts.map +1 -0
  243. package/dist/x402-opener/scheme.js +1035 -0
  244. package/dist/x402-opener/scheme.js.map +1 -0
  245. package/dist/x402-opener/terms.d.ts +165 -0
  246. package/dist/x402-opener/terms.d.ts.map +1 -0
  247. package/dist/x402-opener/terms.js +304 -0
  248. package/dist/x402-opener/terms.js.map +1 -0
  249. package/dist/zktls/reclaim.d.ts +2 -2
  250. package/dist/zktls/t2-delivery-proof.d.ts +6 -6
  251. package/docs/agent-layer.md +3 -2
  252. package/docs/autonomous-privy-wallet.md +188 -0
  253. package/docs/cli.md +265 -34
  254. package/docs/langchain.md +5 -0
  255. package/docs/mcp-reference.md +91 -5
  256. package/docs/probe-capability.md +191 -0
  257. package/docs/quickstart.md +39 -4
  258. package/docs/threat-model.md +20 -0
  259. package/docs/x402.md +766 -593
  260. package/package.json +19 -17
  261. package/src/capability/probe-capability.ts +355 -0
  262. package/src/cli/args.ts +29 -0
  263. package/src/cli/commands/claim.ts +168 -33
  264. package/src/cli/commands/close.ts +36 -3
  265. package/src/cli/commands/invoke.ts +5 -2
  266. package/src/cli/commands/mcp.ts +18 -2
  267. package/src/cli/commands/open.ts +75 -4
  268. package/src/cli/commands/pay.ts +584 -0
  269. package/src/cli/commands/reclaim-unverified.ts +8 -2
  270. package/src/cli/commands/reclaim.ts +277 -19
  271. package/src/cli/commands/refund.ts +475 -0
  272. package/src/cli/commands/status.ts +28 -2
  273. package/src/cli/commands/streams.ts +10 -14
  274. package/src/cli/commands/watch.ts +11 -1
  275. package/src/cli/config.ts +139 -52
  276. package/src/cli/context.ts +16 -0
  277. package/src/cli/deps.ts +111 -1
  278. package/src/cli/errors.ts +24 -1
  279. package/src/cli/escape-hatch.ts +19 -5
  280. package/src/cli/listings.ts +43 -10
  281. package/src/cli/preflight.ts +36 -21
  282. package/src/cli/privy.ts +4 -2
  283. package/src/cli/progress.ts +21 -2
  284. package/src/cli/reclaimability.ts +154 -0
  285. package/src/cli/run.ts +77 -21
  286. package/src/cli/scan-range.ts +2 -2
  287. package/src/cli/status.ts +454 -44
  288. package/src/cli/wallet.ts +45 -8
  289. package/src/discovery/rate-band.ts +172 -0
  290. package/src/escrow-generation.ts +135 -0
  291. package/src/frameworks/agentkit.ts +45 -11
  292. package/src/frameworks/eliza.ts +27 -5
  293. package/src/frameworks/langchain.ts +14 -0
  294. package/src/gateway/caller-auth-gateway.ts +320 -68
  295. package/src/gateway/http-server.ts +717 -220
  296. package/src/gateway/probe-auth.ts +191 -0
  297. package/src/gateway/signed-rate-bounds.ts +164 -0
  298. package/src/gateway/x402-opener-mode.ts +1171 -0
  299. package/src/index.ts +82 -19
  300. package/src/mcp/server.ts +351 -42
  301. package/src/mcp/x402-tools.ts +785 -0
  302. package/src/network.ts +302 -0
  303. package/src/sdk/client.ts +25 -0
  304. package/src/sdk/invoke.ts +6 -5
  305. package/src/wallet/autonomous-wallet-broker.ts +46 -20
  306. package/src/wallet/autonomous-wallet-protocol.ts +11 -2
  307. package/src/wallet/autonomous-wallet.ts +4 -2
  308. package/src/wallet/broker-escrows.ts +66 -0
  309. package/src/wallet/lifecycle.ts +53 -18
  310. package/src/wallet/privy-broker-server.ts +35 -25
  311. package/src/wallet/privy-session-broker.ts +58 -25
  312. package/src/wallet/privy-session-provider.ts +7 -3
  313. package/src/wallet/provider.ts +20 -15
  314. package/src/x402/facilitator.ts +3 -43
  315. package/src/x402-buyer/guarded-fetch.ts +213 -0
  316. package/src/x402-buyer/index.ts +9 -0
  317. package/src/x402-buyer/pay.ts +555 -0
  318. package/src/x402-opener/abi.ts +881 -0
  319. package/src/x402-opener/facilitator-entry.ts +48 -0
  320. package/src/x402-opener/facilitator-server.ts +1114 -0
  321. package/src/x402-opener/index.ts +13 -0
  322. package/src/x402-opener/listing-resolver.ts +202 -0
  323. package/src/x402-opener/refund-relay.ts +1873 -0
  324. package/src/x402-opener/relayer-queue.ts +18 -0
  325. package/src/x402-opener/scheme.ts +1551 -0
  326. package/src/x402-opener/terms.ts +453 -0
  327. package/dist/gateway/x402-mode.d.ts +0 -123
  328. package/dist/gateway/x402-mode.d.ts.map +0 -1
  329. package/dist/gateway/x402-mode.js +0 -212
  330. package/dist/gateway/x402-mode.js.map +0 -1
  331. package/dist/x402/delivery-oracle.d.ts +0 -108
  332. package/dist/x402/delivery-oracle.d.ts.map +0 -1
  333. package/dist/x402/delivery-oracle.js +0 -174
  334. package/dist/x402/delivery-oracle.js.map +0 -1
  335. package/dist/x402/facilitator-server-entry.d.ts +0 -3
  336. package/dist/x402/facilitator-server-entry.d.ts.map +0 -1
  337. package/dist/x402/facilitator-server-entry.js +0 -27
  338. package/dist/x402/facilitator-server-entry.js.map +0 -1
  339. package/dist/x402/facilitator-server.d.ts +0 -73
  340. package/dist/x402/facilitator-server.d.ts.map +0 -1
  341. package/dist/x402/facilitator-server.js +0 -305
  342. package/dist/x402/facilitator-server.js.map +0 -1
  343. package/dist/x402/payment-requirements.d.ts +0 -106
  344. package/dist/x402/payment-requirements.d.ts.map +0 -1
  345. package/dist/x402/payment-requirements.js +0 -138
  346. package/dist/x402/payment-requirements.js.map +0 -1
  347. package/dist/x402/relayer.d.ts +0 -41
  348. package/dist/x402/relayer.d.ts.map +0 -1
  349. package/dist/x402/relayer.js +0 -64
  350. package/dist/x402/relayer.js.map +0 -1
  351. package/dist/x402/resource-index.d.ts +0 -75
  352. package/dist/x402/resource-index.d.ts.map +0 -1
  353. package/dist/x402/resource-index.js +0 -126
  354. package/dist/x402/resource-index.js.map +0 -1
  355. package/dist/x402/verified-delivery-facilitator.d.ts +0 -285
  356. package/dist/x402/verified-delivery-facilitator.d.ts.map +0 -1
  357. package/dist/x402/verified-delivery-facilitator.js +0 -423
  358. package/dist/x402/verified-delivery-facilitator.js.map +0 -1
  359. package/src/gateway/x402-mode.ts +0 -350
  360. package/src/x402/delivery-oracle.ts +0 -285
  361. package/src/x402/facilitator-server-entry.ts +0 -35
  362. package/src/x402/facilitator-server.ts +0 -445
  363. package/src/x402/payment-requirements.ts +0 -231
  364. package/src/x402/relayer.ts +0 -122
  365. package/src/x402/resource-index.ts +0 -228
  366. package/src/x402/verified-delivery-facilitator.ts +0 -744
@@ -0,0 +1,1551 @@
1
+ import {
2
+ streamEscrowV2Abi,
3
+ x402UpfrontStreamOpenerAbi,
4
+ x402UpfrontStreamOpenerFactoryAbi,
5
+ } from "@absol-labs/shared";
6
+ import type {
7
+ FacilitatorContext,
8
+ Network,
9
+ PaymentPayload,
10
+ PaymentRequirements,
11
+ SchemeNetworkFacilitator,
12
+ SettleResponse,
13
+ VerifyResponse,
14
+ } from "@x402/core/types";
15
+ import { deepEqual } from "@x402/core/utils";
16
+ import {
17
+ isEIP3009Payload,
18
+ toFacilitatorEvmSigner,
19
+ type ExactEIP3009Payload,
20
+ type ExactEvmPayloadV2,
21
+ } from "@x402/evm";
22
+ import { ExactEvmScheme } from "@x402/evm/exact/facilitator";
23
+ import {
24
+ BaseError,
25
+ ContractFunctionRevertedError,
26
+ decodeEventLog,
27
+ getAddress,
28
+ isAddress,
29
+ isErc6492Signature,
30
+ isHex,
31
+ type Account,
32
+ type Address,
33
+ type Chain,
34
+ type Hex,
35
+ type Log,
36
+ type PublicClient,
37
+ type Transport,
38
+ type WalletClient,
39
+ } from "viem";
40
+
41
+ import { x402StreamOpenerAbi, x402StreamOpenerFactoryAbi } from "./abi.js";
42
+ import type { OperatorSignedListingResolver } from "./listing-resolver.js";
43
+ import { RelayerQueue } from "./relayer-queue.js";
44
+ import {
45
+ chainIdFromNetwork,
46
+ assertValidUpfrontTerms,
47
+ maxEarnable,
48
+ OPENER_EXTRA_KEY,
49
+ OPENER_SCHEME,
50
+ OPENER_TERMS_VERSION,
51
+ parseUintString,
52
+ termsFromExtra,
53
+ termsTuple,
54
+ upfrontRefundable,
55
+ upfrontTermsTuple,
56
+ type OpenerKind,
57
+ type OpenerTerms,
58
+ type UpfrontOpenerTerms,
59
+ } from "./terms.js";
60
+
61
+ /**
62
+ * What one checked payment settles: the factory's kind and its terms. An
63
+ * `"upfront"` payment carries the 6-field terms (price + non-refundable
64
+ * upfrontAmount); a `"stream"` payment the 4-field ones.
65
+ */
66
+ export type OpenerSettlementPlan =
67
+ | { readonly kind: "stream"; readonly terms: OpenerTerms }
68
+ | { readonly kind: "upfront"; readonly terms: UpfrontOpenerTerms };
69
+
70
+ /**
71
+ * `MetrikOpenerExactScheme` — an x402 v2 `exact` facilitator scheme that
72
+ * settles every payment INTO a `StreamEscrowV2` stream through the ownerless
73
+ * `X402StreamOpenerFactory` (metrik-agent#116, metrik-contracts#48).
74
+ *
75
+ * ## What it composes, and what it refuses
76
+ *
77
+ * Standard `exact` checks (EIP-712 signature, recipient == `payTo`, value ==
78
+ * amount, validity window, asset is a contract, balance / nonce simulation of
79
+ * the transfer) are delegated to the official `@x402/evm` `ExactEvmScheme`
80
+ * verifier, never reimplemented. On top of it, every payment must satisfy:
81
+ *
82
+ * - `assetTransferMethod` is EIP-3009. Permit2 and every other method are
83
+ * refused: the opener only ever pulls an EIP-3009 authorization.
84
+ * - The signature is NOT ERC-6492-wrapped. A counterfactual smart wallet would
85
+ * need a factory deploy the opener cannot perform; refused with a reason.
86
+ * - `asset` is on the allowlist AND is the factory's own settlement token.
87
+ * - `payTo == factory.predict(terms)`, recomputed on-chain from the configured
88
+ * factory for the terms in `extra.metrikStreamOpener` — never compared by
89
+ * code or codehash (a byte-identical clone outside the factory proves
90
+ * nothing). The factory named in `extra` must be the configured one.
91
+ * - `(operator, serviceRef)` resolve to exactly ONE operator-signed listing via
92
+ * the existing discovery signature verification; the rate is inside the
93
+ * listing's signed commercial bounds when it declares them.
94
+ * - `0 < amount <= ratePerSecond * maxDuration`.
95
+ * - `factory.deployAndSettle` simulates cleanly from the relayer.
96
+ *
97
+ * Any doubt — an RPC error, an unreachable registry, a malformed field — is an
98
+ * invalid verdict. Nothing here ever fails open.
99
+ *
100
+ * ## Settlement
101
+ *
102
+ * `settle` re-runs the full verification, then calls `deployAndSettle` from the
103
+ * relayer, waits for the receipt, and returns success ONLY when the mined
104
+ * receipt carries both the opener's `StreamOpenedFor` and the escrow's
105
+ * `StreamOpened` for exactly this payer, nonce, amount and terms. The stock
106
+ * `ExactEvmScheme.settle` is never called: it would transfer the USDC straight
107
+ * to `payTo`, and USDC that reaches an opener any other way than
108
+ * `settleAndOpen` is unrecoverable. The signer handed to the stock scheme
109
+ * cannot broadcast at all, so that path is closed structurally too.
110
+ *
111
+ * ## At most one success per authorization
112
+ *
113
+ * An EIP-3009 authorization (chain + asset + payer + nonce) receives AT MOST
114
+ * ONE successful `SettleResponse` from this instance, ever. A settle of an
115
+ * authorization that is in flight, or already settled, returns `success:
116
+ * false` with `duplicate_settlement` (carrying the tx hash when there is one),
117
+ * and `verify` refuses it the same way — so one payment can never buy two
118
+ * served responses. After a `settlement_pending` (receipt wait timed out) the
119
+ * next settle of the SAME payload reconciles against the broadcast tx and may
120
+ * receive the one success, if none was delivered before. The ledger is
121
+ * in-memory and per instance: a multi-instance deployment needs a shared store
122
+ * and does not have one today.
123
+ *
124
+ * Relayer submissions (simulate + write) are serialized so concurrent payers
125
+ * never race for the relayer's account nonce.
126
+ *
127
+ * ## The upfront ("minimum charge") opener kind
128
+ *
129
+ * With `upfrontFactoryAddress` set, a quote may also name the
130
+ * `X402UpfrontStreamOpenerFactory` (metrik-contracts#54). Its 6-field terms
131
+ * add `price` and a non-refundable `upfrontAmount`; the kind is decided by
132
+ * WHICH configured factory the quote names, never by the wire fields. Verify
133
+ * additionally requires the factory's own `validateTerms` rules,
134
+ * `price - upfrontAmount <= rate * maxDuration`, `amount == price` and
135
+ * `payTo == upfrontFactory.predict(terms)`, and simulates the 6-tuple
136
+ * `deployAndSettle`. Settle succeeds only on a receipt that shows exactly the
137
+ * signed split (see {@link MetrikOpenerExactScheme.upfrontSettlement}); an
138
+ * upfront-only payment (`upfrontAmount == price`) opens no stream and is
139
+ * reported with `streamId: null`.
140
+ */
141
+
142
+ /** Reasons this scheme adds to the stock `exact` ones. All fail closed. */
143
+ export const OpenerInvalidReason = {
144
+ unsupportedScheme: "metrik_opener_unsupported_scheme",
145
+ unsupportedNetwork: "metrik_opener_unsupported_network",
146
+ unsupportedVersion: "metrik_opener_unsupported_x402_version",
147
+ acceptedMismatch: "metrik_opener_accepted_requirements_mismatch",
148
+ unsupportedAssetTransferMethod:
149
+ "metrik_opener_asset_transfer_method_not_eip3009",
150
+ erc6492Unsupported: "metrik_opener_erc6492_signature_unsupported",
151
+ missingSignature: "metrik_opener_missing_signature",
152
+ assetNotAllowed: "metrik_opener_asset_not_allowed",
153
+ invalidTerms: "metrik_opener_invalid_terms",
154
+ factoryMismatch: "metrik_opener_factory_mismatch",
155
+ payToMismatch: "metrik_opener_pay_to_not_predicted_opener",
156
+ invalidAmount: "metrik_opener_invalid_amount",
157
+ amountExceedsTerms: "metrik_opener_amount_exceeds_rate_times_duration",
158
+ listingUnavailable: "metrik_opener_listing_registry_unavailable",
159
+ listingNotFound: "metrik_opener_listing_not_found",
160
+ listingAmbiguous: "metrik_opener_listing_ambiguous",
161
+ listingRateOutOfBounds: "metrik_opener_listing_rate_out_of_bounds",
162
+ simulationFailed: "metrik_opener_settle_simulation_failed",
163
+ verifyError: "metrik_opener_verify_error",
164
+ transactionReverted: "metrik_opener_transaction_reverted",
165
+ streamEventMismatch: "metrik_opener_stream_event_mismatch",
166
+ settlementPending: "settlement_pending",
167
+ settleError: "metrik_opener_settle_error",
168
+ amountBelowMinimum: "metrik_opener_amount_below_minimum",
169
+ /**
170
+ * This authorization is already being settled, or already settled, by this
171
+ * facilitator instance. May carry the settlement transaction hash. Never a
172
+ * second success (see "At most one success" on the class).
173
+ */
174
+ duplicateSettlement: "duplicate_settlement",
175
+ settlementCapacity: "metrik_opener_settlement_capacity_exhausted",
176
+ } as const;
177
+
178
+ /** Default facilitator minimum payment: 10000 base units = 0.01 USDC. */
179
+ export const DEFAULT_OPENER_MIN_AMOUNT = 10_000n;
180
+
181
+ /** Hard cap on remembered authorizations; beyond it new settles fail closed. */
182
+ const MAX_LEDGER_ENTRIES = 100_000;
183
+
184
+ /** Grace after an authorization's `validBefore` before its ledger entry is dropped. */
185
+ const LEDGER_GRACE_SECONDS = 600n;
186
+
187
+ export interface MetrikOpenerExactSchemeConfig {
188
+ /** CAIP-2 network (`eip155:<chainId>`), as the registry gives it. Must match the RPC's chain id. */
189
+ readonly network: Network;
190
+ /** The deployed `X402StreamOpenerFactory`. */
191
+ readonly factoryAddress: Address;
192
+ /**
193
+ * The deployed `X402UpfrontStreamOpenerFactory` (metrik-contracts#54), bound
194
+ * to the SAME escrow. When set, upfront ("minimum charge") payments are
195
+ * accepted too and `/supported` advertises it as `upfrontOpener`. Unset = the
196
+ * stream factory only (the pre-upfront behaviour).
197
+ */
198
+ readonly upfrontFactoryAddress?: Address;
199
+ /**
200
+ * The implementation `upfrontFactoryAddress` must report
201
+ * (`factory.implementation()`), e.g. the registry's. Checked by
202
+ * {@link MetrikOpenerExactScheme.init} when given.
203
+ */
204
+ readonly upfrontImplementation?: Address;
205
+ /** The `StreamEscrowV2` the factory must point at (checked by {@link init}). */
206
+ readonly escrowAddress: Address;
207
+ /** Settlement tokens this facilitator accepts. Must contain `factory.usdc()`. */
208
+ readonly assetAllowlist: readonly Address[];
209
+ /** Read client for the network. */
210
+ readonly publicClient: PublicClient<Transport, Chain | undefined>;
211
+ /** Relayer wallet. Pays gas for `deployAndSettle`; never holds buyer funds. */
212
+ readonly walletClient: WalletClient<Transport, Chain | undefined, Account>;
213
+ readonly listingResolver: OperatorSignedListingResolver;
214
+ /** How long to wait for a settlement receipt before reporting `settlement_pending`. Default 60s. */
215
+ readonly receiptTimeoutMs?: number;
216
+ /**
217
+ * Smallest accepted payment, token base units. Default
218
+ * {@link DEFAULT_OPENER_MIN_AMOUNT}. Every settlement costs the relayer a
219
+ * full `deployAndSettle`, so dust payments would be a gas drain.
220
+ */
221
+ readonly minAmount?: bigint;
222
+ /** Injectable clock (unix seconds), for tests. */
223
+ readonly now?: () => number;
224
+ /**
225
+ * The relayer's submission queue. Pass the SAME queue to every component
226
+ * that broadcasts from this relayer key (the refund relay does), so their
227
+ * transactions never race for the account nonce. Default: a private queue.
228
+ */
229
+ readonly relayerQueue?: RelayerQueue;
230
+ /**
231
+ * Advertise `refundRelay: true` in the `/supported` marker: this service
232
+ * also runs the gasless refund relay (`/streams/:streamId/...`). Set only
233
+ * when the relay is actually mounted. Default false.
234
+ */
235
+ readonly advertiseRefundRelay?: boolean;
236
+ }
237
+
238
+ /**
239
+ * One remembered EIP-3009 authorization, keyed by chain + asset + payer +
240
+ * authorization nonce.
241
+ *
242
+ * - `in-flight`: a settle is actively running (verify → submit → receipt).
243
+ * - `pending`: submitted, but the receipt wait timed out; the next settle of
244
+ * the SAME payload reconciles against `hash` instead of broadcasting again.
245
+ * - `final`: settled (the one success was delivered) or otherwise terminal.
246
+ */
247
+ type LedgerEntry =
248
+ | {
249
+ readonly state: "in-flight";
250
+ readonly validBefore: bigint;
251
+ hash?: Hex;
252
+ }
253
+ | {
254
+ readonly state: "pending";
255
+ readonly validBefore: bigint;
256
+ readonly hash: Hex;
257
+ readonly payload: PaymentPayload;
258
+ readonly requirements: PaymentRequirements;
259
+ readonly auth: ExactEIP3009Payload & { signature: Hex };
260
+ readonly plan: OpenerSettlementPlan;
261
+ readonly opener: Address;
262
+ }
263
+ | {
264
+ readonly state: "final";
265
+ readonly validBefore: bigint;
266
+ readonly hash: Hex | "";
267
+ };
268
+
269
+ /** What {@link MetrikOpenerExactScheme.init} proved about the on-chain wiring. */
270
+ export interface OpenerFacilitatorBinding {
271
+ readonly chainId: number;
272
+ readonly factory: Address;
273
+ readonly escrow: Address;
274
+ readonly usdc: Address;
275
+ readonly relayer: Address;
276
+ /** The upfront factory, when configured (same escrow and token). */
277
+ readonly upfront?: {
278
+ readonly factory: Address;
279
+ readonly implementation: Address;
280
+ };
281
+ }
282
+
283
+ /** The upfront opener kind advertised in the `/supported` marker. */
284
+ export interface UpfrontOpenerMarker {
285
+ readonly factory: Address;
286
+ readonly implementation: Address;
287
+ readonly escrow: Address;
288
+ readonly termsVersion: typeof OPENER_TERMS_VERSION.upfront;
289
+ }
290
+
291
+ /**
292
+ * Marker this facilitator advertises in `GET /supported` (`kinds[].extra`), so
293
+ * a resource server can refuse to quote an opener `payTo` through any
294
+ * facilitator that would settle it with a plain `transferWithAuthorization`
295
+ * (which strands the funds). See {@link readOpenerFacilitatorMarker}.
296
+ */
297
+ export interface OpenerFacilitatorMarker {
298
+ readonly settlement: "x402-stream-opener";
299
+ readonly factory: Address;
300
+ readonly escrow: Address;
301
+ readonly usdc: Address;
302
+ readonly chainId: number;
303
+ /** Smallest accepted amount, base units (decimal string). */
304
+ readonly minAmount: string;
305
+ /** How long `settle` waits for a receipt before `settlement_pending`. */
306
+ readonly receiptTimeoutSeconds: number;
307
+ /**
308
+ * True when the same service runs the gasless refund relay
309
+ * (`GET /streams/:streamId`, `POST /streams/:streamId/close|reclaim`).
310
+ * Absent (read as false) otherwise.
311
+ */
312
+ readonly refundRelay?: true;
313
+ /**
314
+ * Present when this facilitator also settles the UPFRONT ("minimum charge")
315
+ * opener kind. `factory` above stays the stream factory, so a gateway that
316
+ * predates the upfront kind reads the marker exactly as before.
317
+ */
318
+ readonly upfrontOpener?: UpfrontOpenerMarker;
319
+ }
320
+
321
+ export class MetrikOpenerExactScheme implements SchemeNetworkFacilitator {
322
+ readonly scheme = OPENER_SCHEME;
323
+ readonly caipFamily = "eip155:*";
324
+
325
+ private readonly stock: ExactEvmScheme;
326
+ private readonly chainId: number;
327
+ private readonly allowlist: ReadonlySet<string>;
328
+ private readonly receiptTimeoutMs: number;
329
+ private readonly minAmount: bigint;
330
+ private readonly now: () => number;
331
+ private readonly ledger = new Map<string, LedgerEntry>();
332
+ /** Serializes relayer submissions (simulate + write) so nonces never collide. */
333
+ private readonly relayerQueue: RelayerQueue;
334
+ private binding: OpenerFacilitatorBinding | undefined;
335
+
336
+ constructor(private readonly config: MetrikOpenerExactSchemeConfig) {
337
+ this.chainId = chainIdFromNetwork(config.network);
338
+ if (!isAddress(config.factoryAddress)) {
339
+ throw new Error("factoryAddress must be an EVM address");
340
+ }
341
+ if (!isAddress(config.escrowAddress)) {
342
+ throw new Error("escrowAddress must be an EVM address");
343
+ }
344
+ if (
345
+ config.upfrontFactoryAddress !== undefined &&
346
+ (!isAddress(config.upfrontFactoryAddress) ||
347
+ getAddress(config.upfrontFactoryAddress) ===
348
+ getAddress(config.factoryAddress))
349
+ ) {
350
+ throw new Error(
351
+ "upfrontFactoryAddress must be an EVM address other than factoryAddress",
352
+ );
353
+ }
354
+ if (
355
+ config.upfrontImplementation !== undefined &&
356
+ !isAddress(config.upfrontImplementation)
357
+ ) {
358
+ throw new Error("upfrontImplementation must be an EVM address");
359
+ }
360
+ if (config.assetAllowlist.length === 0) {
361
+ throw new Error("assetAllowlist must not be empty");
362
+ }
363
+ for (const asset of config.assetAllowlist) {
364
+ if (!isAddress(asset)) {
365
+ throw new Error(`assetAllowlist entry is not an address: ${asset}`);
366
+ }
367
+ }
368
+ this.allowlist = new Set(
369
+ config.assetAllowlist.map((asset) => asset.toLowerCase()),
370
+ );
371
+ this.receiptTimeoutMs = config.receiptTimeoutMs ?? 60_000;
372
+ if (
373
+ !Number.isInteger(this.receiptTimeoutMs) ||
374
+ this.receiptTimeoutMs <= 0
375
+ ) {
376
+ throw new Error("receiptTimeoutMs must be a positive integer");
377
+ }
378
+ this.minAmount = config.minAmount ?? DEFAULT_OPENER_MIN_AMOUNT;
379
+ if (this.minAmount <= 0n) {
380
+ throw new Error("minAmount must be > 0");
381
+ }
382
+ this.now = config.now ?? (() => Math.floor(Date.now() / 1000));
383
+ this.relayerQueue = config.relayerQueue ?? new RelayerQueue();
384
+
385
+ // The stock verifier gets a READ-ONLY signer. Its settle path would send the
386
+ // USDC straight to payTo; making writes impossible closes that path even if
387
+ // a future refactor called it by mistake.
388
+ const publicClient = config.publicClient;
389
+ const refuseWrite = async (): Promise<never> => {
390
+ throw new Error(
391
+ "MetrikOpenerExactScheme: the stock exact scheme may not broadcast; opener payments settle only via deployAndSettle",
392
+ );
393
+ };
394
+ this.stock = new ExactEvmScheme(
395
+ toFacilitatorEvmSigner({
396
+ address: config.walletClient.account.address,
397
+ readContract: (args) =>
398
+ publicClient.readContract(
399
+ args as Parameters<typeof publicClient.readContract>[0],
400
+ ),
401
+ verifyTypedData: (args) =>
402
+ publicClient.verifyTypedData(
403
+ args as Parameters<typeof publicClient.verifyTypedData>[0],
404
+ ),
405
+ getCode: (args) => publicClient.getCode(args),
406
+ waitForTransactionReceipt: refuseWrite,
407
+ writeContract: refuseWrite,
408
+ sendTransaction: refuseWrite,
409
+ }),
410
+ // ERC-6492 factory deployment disabled: 6492 signatures are refused
411
+ // before the stock verifier is ever reached.
412
+ { eip6492AllowedFactories: [] },
413
+ );
414
+ }
415
+
416
+ /**
417
+ * Proves the on-chain wiring before the facilitator serves anything: the RPC
418
+ * is on the configured chain, the factory has code, points at the configured
419
+ * escrow, and its token is allowlisted. Throws otherwise (the service refuses
420
+ * to start).
421
+ */
422
+ async init(): Promise<OpenerFacilitatorBinding> {
423
+ const { publicClient, factoryAddress, escrowAddress } = this.config;
424
+ const rpcChainId = await publicClient.getChainId();
425
+ if (rpcChainId !== this.chainId) {
426
+ throw new Error(
427
+ `RPC chain id ${rpcChainId} does not match network ${this.config.network}`,
428
+ );
429
+ }
430
+ const code = await publicClient.getCode({ address: factoryAddress });
431
+ if (code === undefined || code === "0x") {
432
+ throw new Error(`no contract at factory address ${factoryAddress}`);
433
+ }
434
+ const [factoryEscrow, factoryUsdc] = await Promise.all([
435
+ publicClient.readContract({
436
+ address: factoryAddress,
437
+ abi: x402StreamOpenerFactoryAbi,
438
+ functionName: "escrow",
439
+ }),
440
+ publicClient.readContract({
441
+ address: factoryAddress,
442
+ abi: x402StreamOpenerFactoryAbi,
443
+ functionName: "usdc",
444
+ }),
445
+ ]);
446
+ if (getAddress(factoryEscrow) !== getAddress(escrowAddress)) {
447
+ throw new Error(
448
+ `factory escrow ${factoryEscrow} is not the configured escrow ${escrowAddress}`,
449
+ );
450
+ }
451
+ if (!this.allowlist.has(factoryUsdc.toLowerCase())) {
452
+ throw new Error(
453
+ `factory settlement token ${factoryUsdc} is not on the asset allowlist`,
454
+ );
455
+ }
456
+ let upfront: OpenerFacilitatorBinding["upfront"];
457
+ if (this.config.upfrontFactoryAddress !== undefined) {
458
+ const upfrontFactory = getAddress(this.config.upfrontFactoryAddress);
459
+ const upfrontCode = await publicClient.getCode({
460
+ address: upfrontFactory,
461
+ });
462
+ if (upfrontCode === undefined || upfrontCode === "0x") {
463
+ throw new Error(
464
+ `no contract at upfront factory address ${upfrontFactory}`,
465
+ );
466
+ }
467
+ const [upEscrow, upUsdc, upImplementation] = await Promise.all([
468
+ publicClient.readContract({
469
+ address: upfrontFactory,
470
+ abi: x402UpfrontStreamOpenerFactoryAbi,
471
+ functionName: "escrow",
472
+ }),
473
+ publicClient.readContract({
474
+ address: upfrontFactory,
475
+ abi: x402UpfrontStreamOpenerFactoryAbi,
476
+ functionName: "usdc",
477
+ }),
478
+ publicClient.readContract({
479
+ address: upfrontFactory,
480
+ abi: x402UpfrontStreamOpenerFactoryAbi,
481
+ functionName: "implementation",
482
+ }),
483
+ ]);
484
+ if (getAddress(upEscrow) !== getAddress(escrowAddress)) {
485
+ throw new Error(
486
+ `upfront factory escrow ${upEscrow} is not the configured escrow ${escrowAddress}`,
487
+ );
488
+ }
489
+ if (getAddress(upUsdc) !== getAddress(factoryUsdc)) {
490
+ throw new Error(
491
+ `upfront factory token ${upUsdc} is not the stream factory's ${factoryUsdc}`,
492
+ );
493
+ }
494
+ if (
495
+ this.config.upfrontImplementation !== undefined &&
496
+ getAddress(upImplementation) !==
497
+ getAddress(this.config.upfrontImplementation)
498
+ ) {
499
+ throw new Error(
500
+ `upfront factory implementation ${upImplementation} is not the expected ${this.config.upfrontImplementation}`,
501
+ );
502
+ }
503
+ upfront = {
504
+ factory: upfrontFactory,
505
+ implementation: getAddress(upImplementation),
506
+ };
507
+ }
508
+ this.binding = {
509
+ chainId: this.chainId,
510
+ factory: getAddress(factoryAddress),
511
+ escrow: getAddress(factoryEscrow),
512
+ usdc: getAddress(factoryUsdc),
513
+ relayer: this.config.walletClient.account.address,
514
+ ...(upfront === undefined ? {} : { upfront }),
515
+ };
516
+ return this.binding;
517
+ }
518
+
519
+ /** `/supported` extra: the marker a resource server checks before quoting an opener. */
520
+ getExtra(_network: string): Record<string, unknown> | undefined {
521
+ const binding = this.binding;
522
+ if (binding === undefined) {
523
+ return undefined;
524
+ }
525
+ const marker: OpenerFacilitatorMarker = {
526
+ settlement: "x402-stream-opener",
527
+ factory: binding.factory,
528
+ escrow: binding.escrow,
529
+ usdc: binding.usdc,
530
+ chainId: binding.chainId,
531
+ minAmount: this.minAmount.toString(),
532
+ receiptTimeoutSeconds: Math.ceil(this.receiptTimeoutMs / 1000),
533
+ ...(this.config.advertiseRefundRelay === true
534
+ ? { refundRelay: true as const }
535
+ : {}),
536
+ ...(binding.upfront === undefined
537
+ ? {}
538
+ : {
539
+ upfrontOpener: {
540
+ factory: binding.upfront.factory,
541
+ implementation: binding.upfront.implementation,
542
+ escrow: binding.escrow,
543
+ termsVersion: OPENER_TERMS_VERSION.upfront,
544
+ },
545
+ }),
546
+ };
547
+ return { [OPENER_EXTRA_KEY]: marker };
548
+ }
549
+
550
+ getSigners(_network: string): string[] {
551
+ return [this.config.walletClient.account.address];
552
+ }
553
+
554
+ async verify(
555
+ payload: PaymentPayload,
556
+ requirements: PaymentRequirements,
557
+ context?: FacilitatorContext,
558
+ ): Promise<VerifyResponse> {
559
+ try {
560
+ const key = this.ledgerKey(payload);
561
+ const entry = key === undefined ? undefined : this.ledger.get(key);
562
+ if (entry !== undefined) {
563
+ if (
564
+ entry.state === "pending" &&
565
+ deepEqual(entry.payload, payload) &&
566
+ deepEqual(entry.requirements, requirements)
567
+ ) {
568
+ // Submitted earlier, receipt not yet seen: the authorization may
569
+ // already be used on-chain, so the on-chain checks would refuse it.
570
+ // The retry is valid only so that settle can reconcile THIS tx.
571
+ return { isValid: true, payer: entry.auth.authorization.from };
572
+ }
573
+ return invalid(
574
+ OpenerInvalidReason.duplicateSettlement,
575
+ "this authorization is already being settled or was settled",
576
+ payerOf(payload),
577
+ );
578
+ }
579
+ const checked = await this.check(payload, requirements, context);
580
+ if (!checked.ok) {
581
+ return checked.response;
582
+ }
583
+ return { isValid: true, payer: checked.auth.authorization.from };
584
+ } catch (error) {
585
+ return invalid(
586
+ OpenerInvalidReason.verifyError,
587
+ describe(error),
588
+ payerOf(payload),
589
+ );
590
+ }
591
+ }
592
+
593
+ async settle(
594
+ payload: PaymentPayload,
595
+ requirements: PaymentRequirements,
596
+ context?: FacilitatorContext,
597
+ ): Promise<SettleResponse> {
598
+ const network = requirements.network;
599
+ const payer = payerOf(payload);
600
+ const key = this.ledgerKey(payload);
601
+ if (key === undefined) {
602
+ // Not a parseable EIP-3009 authorization: the full check refuses it.
603
+ const checked = await this.check(payload, requirements, context).catch(
604
+ (error: unknown) => ({
605
+ ok: false as const,
606
+ response: invalid(
607
+ OpenerInvalidReason.verifyError,
608
+ describe(error),
609
+ payer,
610
+ ),
611
+ }),
612
+ );
613
+ return checked.ok
614
+ ? failure(
615
+ OpenerInvalidReason.verifyError,
616
+ network,
617
+ payer,
618
+ "unparseable authorization",
619
+ )
620
+ : refusal(checked.response, network, payer);
621
+ }
622
+
623
+ // ── At most one success per authorization ─────────────────────────────
624
+ // Everything from here to the ledger write is synchronous, so two
625
+ // concurrent settles of the same authorization can never both claim it.
626
+ this.pruneLedger();
627
+ const existing = this.ledger.get(key);
628
+ if (existing !== undefined) {
629
+ if (
630
+ existing.state === "pending" &&
631
+ deepEqual(existing.payload, payload) &&
632
+ deepEqual(existing.requirements, requirements)
633
+ ) {
634
+ this.ledger.set(key, {
635
+ state: "in-flight",
636
+ validBefore: existing.validBefore,
637
+ hash: existing.hash,
638
+ });
639
+ return this.finish(
640
+ key,
641
+ payload,
642
+ requirements,
643
+ existing.validBefore,
644
+ await this.awaitSettlement(
645
+ existing.hash,
646
+ existing.auth,
647
+ existing.plan,
648
+ existing.opener,
649
+ network,
650
+ ),
651
+ existing,
652
+ );
653
+ }
654
+ return failure(
655
+ OpenerInvalidReason.duplicateSettlement,
656
+ network,
657
+ payer,
658
+ "this authorization is already being settled or was settled",
659
+ existing.hash ?? "",
660
+ );
661
+ }
662
+ if (this.ledger.size >= MAX_LEDGER_ENTRIES) {
663
+ return failure(
664
+ OpenerInvalidReason.settlementCapacity,
665
+ network,
666
+ payer,
667
+ undefined,
668
+ );
669
+ }
670
+ const validBefore = BigInt(
671
+ (eip3009(payload) as ExactEIP3009Payload).authorization.validBefore,
672
+ );
673
+ const entry: LedgerEntry = { state: "in-flight", validBefore };
674
+ this.ledger.set(key, entry);
675
+
676
+ try {
677
+ const checked = await this.check(payload, requirements, context);
678
+ if (!checked.ok) {
679
+ this.ledger.delete(key);
680
+ return refusal(checked.response, network, payer);
681
+ }
682
+
683
+ const { auth, plan, opener } = checked;
684
+ let hash: Hex;
685
+ try {
686
+ hash = await this.submitSerialized(auth, plan);
687
+ } catch (error) {
688
+ this.ledger.delete(key);
689
+ return failure(
690
+ OpenerInvalidReason.simulationFailed,
691
+ network,
692
+ auth.authorization.from,
693
+ revertReason(error),
694
+ );
695
+ }
696
+ entry.hash = hash;
697
+ return this.finish(
698
+ key,
699
+ payload,
700
+ requirements,
701
+ validBefore,
702
+ await this.awaitSettlement(hash, auth, plan, opener, network),
703
+ { auth, plan, opener },
704
+ );
705
+ } catch (error) {
706
+ // Unknown state after a possible broadcast: keep the authorization
707
+ // blocked (never a second success), but only if something was sent.
708
+ if (entry.hash === undefined) {
709
+ this.ledger.delete(key);
710
+ } else {
711
+ this.ledger.set(key, { state: "final", validBefore, hash: entry.hash });
712
+ }
713
+ return failure(
714
+ OpenerInvalidReason.settleError,
715
+ network,
716
+ payer,
717
+ describe(error),
718
+ entry.hash ?? "",
719
+ );
720
+ }
721
+ }
722
+
723
+ /** Records the outcome of one settle attempt in the ledger and returns it. */
724
+ private finish(
725
+ key: string,
726
+ payload: PaymentPayload,
727
+ requirements: PaymentRequirements,
728
+ validBefore: bigint,
729
+ result: SettleResponse,
730
+ context: {
731
+ readonly auth: ExactEIP3009Payload & { signature: Hex };
732
+ readonly plan: OpenerSettlementPlan;
733
+ readonly opener: Address;
734
+ },
735
+ ): SettleResponse {
736
+ if (result.success) {
737
+ // THE one success for this authorization. Nothing succeeds again.
738
+ this.ledger.set(key, {
739
+ state: "final",
740
+ validBefore,
741
+ hash: result.transaction as Hex,
742
+ });
743
+ return result;
744
+ }
745
+ if (result.errorReason === OpenerInvalidReason.settlementPending) {
746
+ this.ledger.set(key, {
747
+ state: "pending",
748
+ validBefore,
749
+ hash: result.transaction as Hex,
750
+ payload,
751
+ requirements,
752
+ auth: context.auth,
753
+ plan: context.plan,
754
+ opener: context.opener,
755
+ });
756
+ return result;
757
+ }
758
+ if (result.errorReason === OpenerInvalidReason.transactionReverted) {
759
+ // A reverted deployAndSettle consumed nothing (the whole call reverts),
760
+ // so the authorization may be settled again.
761
+ this.ledger.delete(key);
762
+ return result;
763
+ }
764
+ // Mined but not the expected stream, or anything else terminal: block.
765
+ this.ledger.set(key, {
766
+ state: "final",
767
+ validBefore,
768
+ hash: (result.transaction as Hex) || "",
769
+ });
770
+ return result;
771
+ }
772
+
773
+ /**
774
+ * Simulate + broadcast `deployAndSettle`, one at a time. Concurrent
775
+ * submissions from one relayer would otherwise race for the same account
776
+ * nonce and all but one would fail after the upstream already did the work.
777
+ */
778
+ private submitSerialized(
779
+ auth: ExactEIP3009Payload & { signature: Hex },
780
+ plan: OpenerSettlementPlan,
781
+ ): Promise<Hex> {
782
+ const run = async (): Promise<Hex> => {
783
+ const { request } = await this.simulateDeployAndSettle(auth, plan);
784
+ return this.config.walletClient.writeContract({
785
+ ...request,
786
+ chain: this.config.walletClient.chain ?? null,
787
+ } as Parameters<typeof this.config.walletClient.writeContract>[0]);
788
+ };
789
+ return this.relayerQueue.run(run);
790
+ }
791
+
792
+ /** chain + asset + payer + authorization nonce, or undefined if unparseable. */
793
+ private ledgerKey(payload: PaymentPayload): string | undefined {
794
+ const auth = eip3009(payload);
795
+ const asset = payload.accepted?.asset;
796
+ if (auth === undefined || typeof asset !== "string") return undefined;
797
+ return [
798
+ this.chainId,
799
+ asset.toLowerCase(),
800
+ auth.authorization.from.toLowerCase(),
801
+ auth.authorization.nonce.toLowerCase(),
802
+ ].join(":");
803
+ }
804
+
805
+ /**
806
+ * Drops entries whose authorization can no longer be settled by anyone
807
+ * (past `validBefore` plus a grace). Never drops an in-flight entry.
808
+ */
809
+ private pruneLedger(): void {
810
+ const now = BigInt(this.now());
811
+ for (const [key, entry] of this.ledger) {
812
+ if (
813
+ entry.state !== "in-flight" &&
814
+ entry.validBefore + LEDGER_GRACE_SECONDS < now
815
+ ) {
816
+ this.ledger.delete(key);
817
+ }
818
+ }
819
+ }
820
+
821
+ /**
822
+ * `eth_call` (simulate) `deployAndSettle` on the factory of the plan's kind:
823
+ * the 4-tuple stream factory, or the 6-tuple upfront factory.
824
+ */
825
+ private async simulateDeployAndSettle(
826
+ auth: ExactEIP3009Payload & { signature: Hex },
827
+ plan: OpenerSettlementPlan,
828
+ ) {
829
+ const { publicClient, walletClient } = this.config;
830
+ if (plan.kind === "upfront") {
831
+ const factory = this.binding?.upfront?.factory;
832
+ if (factory === undefined) {
833
+ throw new Error("the upfront opener factory is not configured");
834
+ }
835
+ return await publicClient.simulateContract({
836
+ account: walletClient.account,
837
+ address: factory,
838
+ abi: x402UpfrontStreamOpenerFactoryAbi,
839
+ functionName: "deployAndSettle",
840
+ args: [upfrontTermsTuple(plan.terms), authorizationTuple(auth)],
841
+ });
842
+ }
843
+ return await publicClient.simulateContract({
844
+ account: walletClient.account,
845
+ address: this.config.factoryAddress,
846
+ abi: x402StreamOpenerFactoryAbi,
847
+ functionName: "deployAndSettle",
848
+ args: [termsTuple(plan.terms), authorizationTuple(auth)],
849
+ });
850
+ }
851
+
852
+ private async awaitSettlement(
853
+ hash: Hex,
854
+ auth: ExactEIP3009Payload & { signature: Hex },
855
+ plan: OpenerSettlementPlan,
856
+ opener: Address,
857
+ network: Network,
858
+ ): Promise<SettleResponse> {
859
+ const payer = getAddress(auth.authorization.from);
860
+ let receipt;
861
+ try {
862
+ receipt = await this.config.publicClient.waitForTransactionReceipt({
863
+ hash,
864
+ timeout: this.receiptTimeoutMs,
865
+ });
866
+ } catch (error) {
867
+ // Broadcast but unconfirmed: keep the pending entry so a retry
868
+ // reconciles against this hash. Never success without a receipt.
869
+ return failure(
870
+ OpenerInvalidReason.settlementPending,
871
+ network,
872
+ payer,
873
+ describe(error),
874
+ hash,
875
+ );
876
+ }
877
+ if (receipt.status !== "success") {
878
+ return failure(
879
+ OpenerInvalidReason.transactionReverted,
880
+ network,
881
+ payer,
882
+ "deployAndSettle reverted on-chain",
883
+ hash,
884
+ );
885
+ }
886
+ if (plan.kind === "upfront") {
887
+ return this.upfrontSettlement(
888
+ receipt.logs,
889
+ hash,
890
+ auth,
891
+ plan.terms,
892
+ opener,
893
+ network,
894
+ );
895
+ }
896
+ const terms = plan.terms;
897
+ const opened = findStreamOpened(
898
+ receipt.logs,
899
+ opener,
900
+ getAddress(this.config.escrowAddress),
901
+ auth,
902
+ terms,
903
+ BigInt(auth.authorization.value),
904
+ );
905
+ if (opened === undefined) {
906
+ return failure(
907
+ OpenerInvalidReason.streamEventMismatch,
908
+ network,
909
+ payer,
910
+ "the mined receipt does not show the expected stream",
911
+ hash,
912
+ );
913
+ }
914
+ return {
915
+ success: true,
916
+ transaction: hash,
917
+ network,
918
+ payer,
919
+ amount: auth.authorization.value,
920
+ extra: {
921
+ [OPENER_EXTRA_KEY]: {
922
+ streamId: opened.streamId,
923
+ opener,
924
+ escrow: getAddress(this.config.escrowAddress),
925
+ deposit: auth.authorization.value,
926
+ ratePerSecond: terms.ratePerSecond.toString(),
927
+ // A JSON NUMBER: the SDK buyer (payWithX402) validates it as one.
928
+ expiresAt: Number(opened.expiresAt),
929
+ },
930
+ },
931
+ };
932
+ }
933
+
934
+ /**
935
+ * The success response of a mined UPFRONT settlement, or a mismatch failure.
936
+ * The receipt must show exactly what the signed terms imply:
937
+ *
938
+ * - `upfrontAmount > 0`: the opener's `UpfrontCharged` for this payer, nonce
939
+ * and operator, for exactly `upfrontAmount`, naming the stream it opened
940
+ * (or `bytes32(0)` when none was);
941
+ * - `price > upfrontAmount`: the escrow's `StreamOpened` (buyer = opener) and
942
+ * the opener's `StreamOpenedFor` with `deposit = price - upfrontAmount`;
943
+ * - `upfrontAmount == price` (upfront-only): NO stream at all. Success, with
944
+ * `streamId: null`: nothing was escrowed and there is nothing to refund.
945
+ *
946
+ * The reported `upfrontAmount` / `upfrontFee` are read from `UpfrontCharged`.
947
+ */
948
+ /** @internal */
949
+ private upfrontSettlement(
950
+ logs: readonly Log[],
951
+ hash: Hex,
952
+ auth: ExactEIP3009Payload & { signature: Hex },
953
+ terms: UpfrontOpenerTerms,
954
+ opener: Address,
955
+ network: Network,
956
+ ): SettleResponse {
957
+ const payer = getAddress(auth.authorization.from);
958
+ const escrow = getAddress(this.config.escrowAddress);
959
+ const refundable = upfrontRefundable(terms);
960
+ const mismatch = (why: string) =>
961
+ failure(
962
+ OpenerInvalidReason.streamEventMismatch,
963
+ network,
964
+ payer,
965
+ `the mined receipt does not show the expected upfront settlement: ${why}`,
966
+ hash,
967
+ );
968
+ const charged = findUpfrontCharged(logs, opener, auth, terms);
969
+ if (terms.upfrontAmount > 0n && charged === undefined) {
970
+ return mismatch(`no UpfrontCharged for ${terms.upfrontAmount}`);
971
+ }
972
+ if (terms.upfrontAmount === 0n && charged !== undefined) {
973
+ return mismatch("an UpfrontCharged for terms without an upfront part");
974
+ }
975
+ let streamId: Hex | null = null;
976
+ let expiresAt: bigint | null = null;
977
+ if (refundable > 0n) {
978
+ const opened = findStreamOpened(
979
+ logs,
980
+ opener,
981
+ escrow,
982
+ auth,
983
+ terms,
984
+ refundable,
985
+ );
986
+ if (opened === undefined) {
987
+ return mismatch(`no stream with deposit ${refundable}`);
988
+ }
989
+ if (
990
+ charged !== undefined &&
991
+ charged.streamId.toLowerCase() !== opened.streamId.toLowerCase()
992
+ ) {
993
+ return mismatch("UpfrontCharged names another stream");
994
+ }
995
+ streamId = opened.streamId;
996
+ expiresAt = opened.expiresAt;
997
+ } else {
998
+ // Upfront-only: the opener must have opened nothing.
999
+ if (charged === undefined || !/^0x0{64}$/i.test(charged.streamId)) {
1000
+ return mismatch("an upfront-only payment must report streamId 0");
1001
+ }
1002
+ if (
1003
+ findStreamOpened(logs, opener, escrow, auth, terms, undefined) !==
1004
+ undefined
1005
+ ) {
1006
+ return mismatch("an upfront-only payment opened a stream");
1007
+ }
1008
+ }
1009
+ return {
1010
+ success: true,
1011
+ transaction: hash,
1012
+ network,
1013
+ payer,
1014
+ amount: auth.authorization.value,
1015
+ extra: {
1016
+ [OPENER_EXTRA_KEY]: {
1017
+ // `null` (not bytes32(0)) for an upfront-only payment: no stream.
1018
+ streamId,
1019
+ opener,
1020
+ escrow,
1021
+ // The refundable, metered part — the most any refund can return.
1022
+ deposit: refundable.toString(),
1023
+ ratePerSecond: terms.ratePerSecond.toString(),
1024
+ ...(expiresAt === null ? {} : { expiresAt: Number(expiresAt) }),
1025
+ upfrontAmount: (charged?.amount ?? 0n).toString(),
1026
+ upfrontFee: (charged?.fee ?? 0n).toString(),
1027
+ },
1028
+ },
1029
+ };
1030
+ }
1031
+
1032
+ /** Every Metrik check plus the stock verifier. Cheapest, most local first. */
1033
+ private async check(
1034
+ payload: PaymentPayload,
1035
+ requirements: PaymentRequirements,
1036
+ context: FacilitatorContext | undefined,
1037
+ ): Promise<
1038
+ | {
1039
+ readonly ok: true;
1040
+ readonly auth: ExactEIP3009Payload & { signature: Hex };
1041
+ readonly plan: OpenerSettlementPlan;
1042
+ readonly opener: Address;
1043
+ }
1044
+ | { readonly ok: false; readonly response: VerifyResponse }
1045
+ > {
1046
+ const payer = payerOf(payload);
1047
+ const refuse = (reason: string, message?: string) =>
1048
+ ({ ok: false, response: invalid(reason, message, payer) }) as const;
1049
+
1050
+ if (payload.x402Version !== 2) {
1051
+ return refuse(OpenerInvalidReason.unsupportedVersion);
1052
+ }
1053
+ if (requirements.scheme !== OPENER_SCHEME) {
1054
+ return refuse(OpenerInvalidReason.unsupportedScheme);
1055
+ }
1056
+ if (
1057
+ requirements.network !== this.config.network ||
1058
+ payload.accepted?.network !== this.config.network
1059
+ ) {
1060
+ return refuse(OpenerInvalidReason.unsupportedNetwork);
1061
+ }
1062
+ // The payer signed against `accepted`; settle exactly that, nothing else.
1063
+ if (!deepEqual(payload.accepted, requirements)) {
1064
+ return refuse(OpenerInvalidReason.acceptedMismatch);
1065
+ }
1066
+ const method = requirements.extra?.assetTransferMethod;
1067
+ if (method !== undefined && method !== "eip3009") {
1068
+ return refuse(OpenerInvalidReason.unsupportedAssetTransferMethod);
1069
+ }
1070
+ const auth = eip3009(payload);
1071
+ if (auth === undefined) {
1072
+ return refuse(OpenerInvalidReason.unsupportedAssetTransferMethod);
1073
+ }
1074
+ if (auth.signature === undefined || !isHex(auth.signature)) {
1075
+ return refuse(OpenerInvalidReason.missingSignature);
1076
+ }
1077
+ if (isErc6492Signature(auth.signature)) {
1078
+ return refuse(
1079
+ OpenerInvalidReason.erc6492Unsupported,
1080
+ "ERC-6492 (counterfactual smart wallet) signatures cannot be settled through the stream opener; deploy the wallet first",
1081
+ );
1082
+ }
1083
+ const binding = this.binding;
1084
+ if (binding === undefined) {
1085
+ return refuse(
1086
+ OpenerInvalidReason.verifyError,
1087
+ "facilitator not initialized",
1088
+ );
1089
+ }
1090
+ if (
1091
+ !isAddress(requirements.asset) ||
1092
+ !this.allowlist.has(requirements.asset.toLowerCase()) ||
1093
+ getAddress(requirements.asset) !== binding.usdc
1094
+ ) {
1095
+ return refuse(OpenerInvalidReason.assetNotAllowed);
1096
+ }
1097
+
1098
+ let parsed: ReturnType<typeof termsFromExtra>;
1099
+ try {
1100
+ parsed = termsFromExtra(requirements.extra);
1101
+ } catch (error) {
1102
+ return refuse(OpenerInvalidReason.invalidTerms, describe(error));
1103
+ }
1104
+ const { terms, factory } = parsed;
1105
+ // Only a factory this facilitator is bound to (and proved in init) is
1106
+ // ever settled; its kind decides the terms shape — never the wire fields.
1107
+ let kind: OpenerKind;
1108
+ if (factory === binding.factory) {
1109
+ kind = "stream";
1110
+ } else if (
1111
+ binding.upfront !== undefined &&
1112
+ factory === binding.upfront.factory
1113
+ ) {
1114
+ kind = "upfront";
1115
+ } else {
1116
+ return refuse(OpenerInvalidReason.factoryMismatch);
1117
+ }
1118
+ if (
1119
+ parsed.termsVersion !== undefined &&
1120
+ parsed.termsVersion !== OPENER_TERMS_VERSION[kind]
1121
+ ) {
1122
+ return refuse(
1123
+ OpenerInvalidReason.invalidTerms,
1124
+ `termsVersion ${parsed.termsVersion} does not match the ${kind} opener factory (${OPENER_TERMS_VERSION[kind]})`,
1125
+ );
1126
+ }
1127
+ let plan: OpenerSettlementPlan;
1128
+ if (kind === "upfront") {
1129
+ if (parsed.upfront === undefined) {
1130
+ return refuse(
1131
+ OpenerInvalidReason.invalidTerms,
1132
+ "the upfront opener factory needs price and upfrontAmount in the terms",
1133
+ );
1134
+ }
1135
+ const upfrontTerms: UpfrontOpenerTerms = { ...terms, ...parsed.upfront };
1136
+ try {
1137
+ assertValidUpfrontTerms(upfrontTerms);
1138
+ } catch (error) {
1139
+ return refuse(OpenerInvalidReason.invalidTerms, describe(error));
1140
+ }
1141
+ plan = { kind, terms: upfrontTerms };
1142
+ } else {
1143
+ if (parsed.upfront !== undefined) {
1144
+ return refuse(
1145
+ OpenerInvalidReason.invalidTerms,
1146
+ "price/upfrontAmount are not terms of the stream opener factory (its payTo does not commit to them)",
1147
+ );
1148
+ }
1149
+ plan = { kind, terms };
1150
+ }
1151
+
1152
+ let amount: bigint;
1153
+ try {
1154
+ amount = parseUintString(requirements.amount, "amount");
1155
+ } catch (error) {
1156
+ return refuse(OpenerInvalidReason.invalidAmount, describe(error));
1157
+ }
1158
+ if (amount <= 0n) {
1159
+ return refuse(OpenerInvalidReason.invalidAmount, "amount must be > 0");
1160
+ }
1161
+ if (amount < this.minAmount) {
1162
+ return refuse(
1163
+ OpenerInvalidReason.amountBelowMinimum,
1164
+ `amount ${amount} is below this facilitator's minimum ${this.minAmount}`,
1165
+ );
1166
+ }
1167
+ if (plan.kind === "upfront") {
1168
+ // The opener accepts exactly its price; anything else reverts on-chain.
1169
+ if (amount !== plan.terms.price) {
1170
+ return refuse(
1171
+ OpenerInvalidReason.invalidAmount,
1172
+ `amount ${amount} is not the upfront opener's fixed price ${plan.terms.price}`,
1173
+ );
1174
+ }
1175
+ } else if (amount > maxEarnable(terms)) {
1176
+ return refuse(OpenerInvalidReason.amountExceedsTerms);
1177
+ }
1178
+
1179
+ let predicted: Address;
1180
+ try {
1181
+ predicted =
1182
+ plan.kind === "upfront"
1183
+ ? await this.config.publicClient.readContract({
1184
+ address: binding.upfront?.factory as Address,
1185
+ abi: x402UpfrontStreamOpenerFactoryAbi,
1186
+ functionName: "predict",
1187
+ args: [upfrontTermsTuple(plan.terms)],
1188
+ })
1189
+ : await this.config.publicClient.readContract({
1190
+ address: binding.factory,
1191
+ abi: x402StreamOpenerFactoryAbi,
1192
+ functionName: "predict",
1193
+ args: [termsTuple(terms)],
1194
+ });
1195
+ } catch (error) {
1196
+ // predict reverts InvalidTerms for terms no transaction can deploy.
1197
+ if (revertReason(error) === "InvalidTerms") {
1198
+ return refuse(OpenerInvalidReason.invalidTerms, "InvalidTerms");
1199
+ }
1200
+ throw error;
1201
+ }
1202
+ if (
1203
+ !isAddress(requirements.payTo) ||
1204
+ getAddress(requirements.payTo) !== getAddress(predicted)
1205
+ ) {
1206
+ return refuse(OpenerInvalidReason.payToMismatch);
1207
+ }
1208
+
1209
+ const listing = await this.config.listingResolver.resolve(terms);
1210
+ if (!listing.ok) {
1211
+ const reason = {
1212
+ "listing-registry-unavailable": OpenerInvalidReason.listingUnavailable,
1213
+ "listing-not-found": OpenerInvalidReason.listingNotFound,
1214
+ "listing-ambiguous": OpenerInvalidReason.listingAmbiguous,
1215
+ "listing-rate-out-of-bounds":
1216
+ OpenerInvalidReason.listingRateOutOfBounds,
1217
+ }[listing.reason];
1218
+ return refuse(reason, listing.message);
1219
+ }
1220
+
1221
+ // Standard exact checks: signature, to == payTo, value == amount, window,
1222
+ // asset contract, and a simulation of the EIP-3009 transfer itself.
1223
+ const stock = await this.stock.verify(payload, requirements, context);
1224
+ if (!stock.isValid) {
1225
+ return { ok: false, response: stock };
1226
+ }
1227
+
1228
+ const signed = auth as ExactEIP3009Payload & { signature: Hex };
1229
+ try {
1230
+ await this.simulateDeployAndSettle(signed, plan);
1231
+ } catch (error) {
1232
+ return refuse(OpenerInvalidReason.simulationFailed, revertReason(error));
1233
+ }
1234
+
1235
+ return { ok: true, auth: signed, plan, opener: getAddress(predicted) };
1236
+ }
1237
+ }
1238
+
1239
+ function authorizationTuple(auth: ExactEIP3009Payload & { signature: Hex }): {
1240
+ readonly from: Address;
1241
+ readonly value: bigint;
1242
+ readonly validAfter: bigint;
1243
+ readonly validBefore: bigint;
1244
+ readonly nonce: Hex;
1245
+ readonly signature: Hex;
1246
+ } {
1247
+ return {
1248
+ from: getAddress(auth.authorization.from),
1249
+ value: BigInt(auth.authorization.value),
1250
+ validAfter: BigInt(auth.authorization.validAfter),
1251
+ validBefore: BigInt(auth.authorization.validBefore),
1252
+ nonce: auth.authorization.nonce,
1253
+ signature: auth.signature,
1254
+ };
1255
+ }
1256
+
1257
+ /**
1258
+ * Finds the pair of events a genuine opener settlement emits in ONE receipt:
1259
+ * the escrow's `StreamOpened` with `buyer == opener` and exactly these terms,
1260
+ * and the opener's `StreamOpenedFor` binding that stream to this payer, nonce
1261
+ * and amount.
1262
+ */
1263
+ function findStreamOpened(
1264
+ logs: readonly Log[],
1265
+ opener: Address,
1266
+ escrow: Address,
1267
+ auth: ExactEIP3009Payload,
1268
+ terms: OpenerTerms,
1269
+ /** Expected stream deposit; `undefined` matches ANY deposit (an absence check). */
1270
+ deposit: bigint | undefined,
1271
+ ): { readonly streamId: Hex; readonly expiresAt: bigint } | undefined {
1272
+ const payer = getAddress(auth.authorization.from);
1273
+ const escrowStreams = new Map<string, bigint>();
1274
+ for (const log of logs) {
1275
+ if (getAddress(log.address) !== escrow) continue;
1276
+ try {
1277
+ const decoded = decodeEventLog({
1278
+ abi: streamEscrowV2Abi,
1279
+ data: log.data,
1280
+ topics: log.topics,
1281
+ });
1282
+ if (decoded.eventName !== "StreamOpened") continue;
1283
+ const args = decoded.args as unknown as {
1284
+ streamId: Hex;
1285
+ buyer: Address;
1286
+ operator: Address;
1287
+ serviceRef: Hex;
1288
+ deposit: bigint;
1289
+ ratePerSecond: bigint;
1290
+ expiresAt: bigint;
1291
+ };
1292
+ if (
1293
+ getAddress(args.buyer) === opener &&
1294
+ getAddress(args.operator) === getAddress(terms.operator) &&
1295
+ args.serviceRef.toLowerCase() === terms.serviceRef.toLowerCase() &&
1296
+ (deposit === undefined || args.deposit === deposit) &&
1297
+ args.ratePerSecond === terms.ratePerSecond
1298
+ ) {
1299
+ escrowStreams.set(args.streamId.toLowerCase(), BigInt(args.expiresAt));
1300
+ }
1301
+ } catch {
1302
+ continue;
1303
+ }
1304
+ }
1305
+ for (const log of logs) {
1306
+ if (getAddress(log.address) !== opener) continue;
1307
+ try {
1308
+ const decoded = decodeEventLog({
1309
+ abi: x402StreamOpenerAbi,
1310
+ data: log.data,
1311
+ topics: log.topics,
1312
+ });
1313
+ if (decoded.eventName !== "StreamOpenedFor") continue;
1314
+ const args = decoded.args;
1315
+ const expiresAt = escrowStreams.get(args.streamId.toLowerCase());
1316
+ if (
1317
+ expiresAt !== undefined &&
1318
+ getAddress(args.payer) === payer &&
1319
+ args.authorizationNonce.toLowerCase() ===
1320
+ auth.authorization.nonce.toLowerCase() &&
1321
+ (deposit === undefined || args.deposit === deposit)
1322
+ ) {
1323
+ return { streamId: args.streamId, expiresAt };
1324
+ }
1325
+ } catch {
1326
+ continue;
1327
+ }
1328
+ }
1329
+ return undefined;
1330
+ }
1331
+
1332
+ /**
1333
+ * The opener's `UpfrontCharged` for this payer, authorization nonce and
1334
+ * operator, for exactly the terms' `upfrontAmount` (its `fee` is the protocol
1335
+ * fee taken out of it). `undefined` when the receipt carries none.
1336
+ */
1337
+ function findUpfrontCharged(
1338
+ logs: readonly Log[],
1339
+ opener: Address,
1340
+ auth: ExactEIP3009Payload,
1341
+ terms: UpfrontOpenerTerms,
1342
+ ):
1343
+ | { readonly streamId: Hex; readonly amount: bigint; readonly fee: bigint }
1344
+ | undefined {
1345
+ const payer = getAddress(auth.authorization.from);
1346
+ for (const log of logs) {
1347
+ if (getAddress(log.address) !== opener) continue;
1348
+ try {
1349
+ const decoded = decodeEventLog({
1350
+ abi: x402UpfrontStreamOpenerAbi,
1351
+ data: log.data,
1352
+ topics: log.topics,
1353
+ });
1354
+ if (decoded.eventName !== "UpfrontCharged") continue;
1355
+ const args = decoded.args as unknown as {
1356
+ authorizationNonce: Hex;
1357
+ payer: Address;
1358
+ operator: Address;
1359
+ amount: bigint;
1360
+ fee: bigint;
1361
+ streamId: Hex;
1362
+ };
1363
+ if (
1364
+ getAddress(args.payer) === payer &&
1365
+ args.authorizationNonce.toLowerCase() ===
1366
+ auth.authorization.nonce.toLowerCase() &&
1367
+ getAddress(args.operator) === getAddress(terms.operator) &&
1368
+ args.amount === terms.upfrontAmount &&
1369
+ args.fee <= args.amount
1370
+ ) {
1371
+ return { streamId: args.streamId, amount: args.amount, fee: args.fee };
1372
+ }
1373
+ } catch {
1374
+ continue;
1375
+ }
1376
+ }
1377
+ return undefined;
1378
+ }
1379
+
1380
+ /**
1381
+ * Reads an opener facilitator marker out of a `/supported` kind's `extra`, or
1382
+ * `undefined` when the facilitator does not advertise opener settlement.
1383
+ */
1384
+ export function readOpenerFacilitatorMarker(
1385
+ extra: Record<string, unknown> | undefined,
1386
+ ): OpenerFacilitatorMarker | undefined {
1387
+ const raw = extra?.[OPENER_EXTRA_KEY];
1388
+ if (typeof raw !== "object" || raw === null) return undefined;
1389
+ const marker = raw as Record<string, unknown>;
1390
+ if (
1391
+ marker.settlement !== "x402-stream-opener" ||
1392
+ typeof marker.factory !== "string" ||
1393
+ !isAddress(marker.factory) ||
1394
+ typeof marker.escrow !== "string" ||
1395
+ !isAddress(marker.escrow) ||
1396
+ typeof marker.usdc !== "string" ||
1397
+ !isAddress(marker.usdc) ||
1398
+ typeof marker.chainId !== "number" ||
1399
+ typeof marker.minAmount !== "string" ||
1400
+ !/^[1-9]\d*$/.test(marker.minAmount) ||
1401
+ typeof marker.receiptTimeoutSeconds !== "number" ||
1402
+ !Number.isInteger(marker.receiptTimeoutSeconds) ||
1403
+ marker.receiptTimeoutSeconds <= 0
1404
+ ) {
1405
+ return undefined;
1406
+ }
1407
+ return {
1408
+ settlement: "x402-stream-opener",
1409
+ factory: getAddress(marker.factory),
1410
+ escrow: getAddress(marker.escrow),
1411
+ usdc: getAddress(marker.usdc),
1412
+ chainId: marker.chainId,
1413
+ minAmount: marker.minAmount,
1414
+ receiptTimeoutSeconds: marker.receiptTimeoutSeconds,
1415
+ ...(marker.refundRelay === true ? { refundRelay: true as const } : {}),
1416
+ ...(readUpfrontMarker(marker.upfrontOpener) ?? {}),
1417
+ };
1418
+ }
1419
+
1420
+ /** The optional `upfrontOpener` entry of a marker; a malformed one is ignored (absent). */
1421
+ function readUpfrontMarker(
1422
+ raw: unknown,
1423
+ ): { readonly upfrontOpener: UpfrontOpenerMarker } | undefined {
1424
+ if (typeof raw !== "object" || raw === null) return undefined;
1425
+ const entry = raw as Record<string, unknown>;
1426
+ if (
1427
+ typeof entry.factory !== "string" ||
1428
+ !isAddress(entry.factory) ||
1429
+ typeof entry.implementation !== "string" ||
1430
+ !isAddress(entry.implementation) ||
1431
+ typeof entry.escrow !== "string" ||
1432
+ !isAddress(entry.escrow) ||
1433
+ entry.termsVersion !== OPENER_TERMS_VERSION.upfront
1434
+ ) {
1435
+ return undefined;
1436
+ }
1437
+ return {
1438
+ upfrontOpener: {
1439
+ factory: getAddress(entry.factory),
1440
+ implementation: getAddress(entry.implementation),
1441
+ escrow: getAddress(entry.escrow),
1442
+ termsVersion: OPENER_TERMS_VERSION.upfront,
1443
+ },
1444
+ };
1445
+ }
1446
+
1447
+ function eip3009(payload: PaymentPayload): ExactEIP3009Payload | undefined {
1448
+ const inner = payload.payload as unknown;
1449
+ if (typeof inner !== "object" || inner === null) return undefined;
1450
+ const candidate = inner as ExactEvmPayloadV2;
1451
+ if (!isEIP3009Payload(candidate)) return undefined;
1452
+ const authorization = (candidate as ExactEIP3009Payload).authorization;
1453
+ if (
1454
+ typeof authorization !== "object" ||
1455
+ authorization === null ||
1456
+ typeof authorization.from !== "string" ||
1457
+ !isAddress(authorization.from) ||
1458
+ typeof authorization.to !== "string" ||
1459
+ typeof authorization.value !== "string" ||
1460
+ typeof authorization.validAfter !== "string" ||
1461
+ typeof authorization.validBefore !== "string" ||
1462
+ typeof authorization.nonce !== "string" ||
1463
+ !isHex(authorization.nonce) ||
1464
+ authorization.nonce.length !== 66
1465
+ ) {
1466
+ return undefined;
1467
+ }
1468
+ try {
1469
+ parseUintString(authorization.value, "authorization.value");
1470
+ parseUintString(authorization.validAfter, "authorization.validAfter");
1471
+ parseUintString(authorization.validBefore, "authorization.validBefore");
1472
+ } catch {
1473
+ return undefined;
1474
+ }
1475
+ return candidate as ExactEIP3009Payload;
1476
+ }
1477
+
1478
+ function payerOf(payload: PaymentPayload): string | undefined {
1479
+ const from = (
1480
+ payload.payload as { authorization?: { from?: unknown } } | undefined
1481
+ )?.authorization?.from;
1482
+ return typeof from === "string" && isAddress(from)
1483
+ ? getAddress(from)
1484
+ : undefined;
1485
+ }
1486
+
1487
+ function invalid(
1488
+ reason: string,
1489
+ message: string | undefined,
1490
+ payer: string | undefined,
1491
+ ): VerifyResponse {
1492
+ return {
1493
+ isValid: false,
1494
+ invalidReason: reason,
1495
+ ...(message === undefined ? {} : { invalidMessage: message }),
1496
+ ...(payer === undefined ? {} : { payer }),
1497
+ };
1498
+ }
1499
+
1500
+ function failure(
1501
+ reason: string,
1502
+ network: Network,
1503
+ payer: string | undefined,
1504
+ message: string | undefined,
1505
+ transaction = "",
1506
+ ): SettleResponse {
1507
+ return {
1508
+ success: false,
1509
+ errorReason: reason,
1510
+ ...(message === undefined ? {} : { errorMessage: message }),
1511
+ transaction,
1512
+ network,
1513
+ ...(payer === undefined ? {} : { payer }),
1514
+ };
1515
+ }
1516
+
1517
+ function refusal(
1518
+ response: VerifyResponse,
1519
+ network: Network,
1520
+ payer: string | undefined,
1521
+ ): SettleResponse {
1522
+ return {
1523
+ success: false,
1524
+ errorReason: response.invalidReason ?? OpenerInvalidReason.verifyError,
1525
+ ...(response.invalidMessage === undefined
1526
+ ? {}
1527
+ : { errorMessage: response.invalidMessage }),
1528
+ transaction: "",
1529
+ network,
1530
+ ...(payer === undefined ? {} : { payer }),
1531
+ };
1532
+ }
1533
+
1534
+ function revertReason(error: unknown): string {
1535
+ if (error instanceof BaseError) {
1536
+ const reverted = error.walk(
1537
+ (cause) => cause instanceof ContractFunctionRevertedError,
1538
+ );
1539
+ if (reverted instanceof ContractFunctionRevertedError) {
1540
+ return (
1541
+ reverted.data?.errorName ?? reverted.reason ?? reverted.shortMessage
1542
+ );
1543
+ }
1544
+ return error.shortMessage;
1545
+ }
1546
+ return describe(error);
1547
+ }
1548
+
1549
+ function describe(error: unknown): string {
1550
+ return error instanceof Error ? error.message : String(error);
1551
+ }