@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,1035 @@
1
+ import { streamEscrowV2Abi, x402UpfrontStreamOpenerAbi, x402UpfrontStreamOpenerFactoryAbi, } from "@absol-labs/shared";
2
+ import { deepEqual } from "@x402/core/utils";
3
+ import { isEIP3009Payload, toFacilitatorEvmSigner, } from "@x402/evm";
4
+ import { ExactEvmScheme } from "@x402/evm/exact/facilitator";
5
+ import { BaseError, ContractFunctionRevertedError, decodeEventLog, getAddress, isAddress, isErc6492Signature, isHex, } from "viem";
6
+ import { x402StreamOpenerAbi, x402StreamOpenerFactoryAbi } from "./abi.js";
7
+ import { RelayerQueue } from "./relayer-queue.js";
8
+ import { chainIdFromNetwork, assertValidUpfrontTerms, maxEarnable, OPENER_EXTRA_KEY, OPENER_SCHEME, OPENER_TERMS_VERSION, parseUintString, termsFromExtra, termsTuple, upfrontRefundable, upfrontTermsTuple, } from "./terms.js";
9
+ /**
10
+ * `MetrikOpenerExactScheme` — an x402 v2 `exact` facilitator scheme that
11
+ * settles every payment INTO a `StreamEscrowV2` stream through the ownerless
12
+ * `X402StreamOpenerFactory` (metrik-agent#116, metrik-contracts#48).
13
+ *
14
+ * ## What it composes, and what it refuses
15
+ *
16
+ * Standard `exact` checks (EIP-712 signature, recipient == `payTo`, value ==
17
+ * amount, validity window, asset is a contract, balance / nonce simulation of
18
+ * the transfer) are delegated to the official `@x402/evm` `ExactEvmScheme`
19
+ * verifier, never reimplemented. On top of it, every payment must satisfy:
20
+ *
21
+ * - `assetTransferMethod` is EIP-3009. Permit2 and every other method are
22
+ * refused: the opener only ever pulls an EIP-3009 authorization.
23
+ * - The signature is NOT ERC-6492-wrapped. A counterfactual smart wallet would
24
+ * need a factory deploy the opener cannot perform; refused with a reason.
25
+ * - `asset` is on the allowlist AND is the factory's own settlement token.
26
+ * - `payTo == factory.predict(terms)`, recomputed on-chain from the configured
27
+ * factory for the terms in `extra.metrikStreamOpener` — never compared by
28
+ * code or codehash (a byte-identical clone outside the factory proves
29
+ * nothing). The factory named in `extra` must be the configured one.
30
+ * - `(operator, serviceRef)` resolve to exactly ONE operator-signed listing via
31
+ * the existing discovery signature verification; the rate is inside the
32
+ * listing's signed commercial bounds when it declares them.
33
+ * - `0 < amount <= ratePerSecond * maxDuration`.
34
+ * - `factory.deployAndSettle` simulates cleanly from the relayer.
35
+ *
36
+ * Any doubt — an RPC error, an unreachable registry, a malformed field — is an
37
+ * invalid verdict. Nothing here ever fails open.
38
+ *
39
+ * ## Settlement
40
+ *
41
+ * `settle` re-runs the full verification, then calls `deployAndSettle` from the
42
+ * relayer, waits for the receipt, and returns success ONLY when the mined
43
+ * receipt carries both the opener's `StreamOpenedFor` and the escrow's
44
+ * `StreamOpened` for exactly this payer, nonce, amount and terms. The stock
45
+ * `ExactEvmScheme.settle` is never called: it would transfer the USDC straight
46
+ * to `payTo`, and USDC that reaches an opener any other way than
47
+ * `settleAndOpen` is unrecoverable. The signer handed to the stock scheme
48
+ * cannot broadcast at all, so that path is closed structurally too.
49
+ *
50
+ * ## At most one success per authorization
51
+ *
52
+ * An EIP-3009 authorization (chain + asset + payer + nonce) receives AT MOST
53
+ * ONE successful `SettleResponse` from this instance, ever. A settle of an
54
+ * authorization that is in flight, or already settled, returns `success:
55
+ * false` with `duplicate_settlement` (carrying the tx hash when there is one),
56
+ * and `verify` refuses it the same way — so one payment can never buy two
57
+ * served responses. After a `settlement_pending` (receipt wait timed out) the
58
+ * next settle of the SAME payload reconciles against the broadcast tx and may
59
+ * receive the one success, if none was delivered before. The ledger is
60
+ * in-memory and per instance: a multi-instance deployment needs a shared store
61
+ * and does not have one today.
62
+ *
63
+ * Relayer submissions (simulate + write) are serialized so concurrent payers
64
+ * never race for the relayer's account nonce.
65
+ *
66
+ * ## The upfront ("minimum charge") opener kind
67
+ *
68
+ * With `upfrontFactoryAddress` set, a quote may also name the
69
+ * `X402UpfrontStreamOpenerFactory` (metrik-contracts#54). Its 6-field terms
70
+ * add `price` and a non-refundable `upfrontAmount`; the kind is decided by
71
+ * WHICH configured factory the quote names, never by the wire fields. Verify
72
+ * additionally requires the factory's own `validateTerms` rules,
73
+ * `price - upfrontAmount <= rate * maxDuration`, `amount == price` and
74
+ * `payTo == upfrontFactory.predict(terms)`, and simulates the 6-tuple
75
+ * `deployAndSettle`. Settle succeeds only on a receipt that shows exactly the
76
+ * signed split (see {@link MetrikOpenerExactScheme.upfrontSettlement}); an
77
+ * upfront-only payment (`upfrontAmount == price`) opens no stream and is
78
+ * reported with `streamId: null`.
79
+ */
80
+ /** Reasons this scheme adds to the stock `exact` ones. All fail closed. */
81
+ export const OpenerInvalidReason = {
82
+ unsupportedScheme: "metrik_opener_unsupported_scheme",
83
+ unsupportedNetwork: "metrik_opener_unsupported_network",
84
+ unsupportedVersion: "metrik_opener_unsupported_x402_version",
85
+ acceptedMismatch: "metrik_opener_accepted_requirements_mismatch",
86
+ unsupportedAssetTransferMethod: "metrik_opener_asset_transfer_method_not_eip3009",
87
+ erc6492Unsupported: "metrik_opener_erc6492_signature_unsupported",
88
+ missingSignature: "metrik_opener_missing_signature",
89
+ assetNotAllowed: "metrik_opener_asset_not_allowed",
90
+ invalidTerms: "metrik_opener_invalid_terms",
91
+ factoryMismatch: "metrik_opener_factory_mismatch",
92
+ payToMismatch: "metrik_opener_pay_to_not_predicted_opener",
93
+ invalidAmount: "metrik_opener_invalid_amount",
94
+ amountExceedsTerms: "metrik_opener_amount_exceeds_rate_times_duration",
95
+ listingUnavailable: "metrik_opener_listing_registry_unavailable",
96
+ listingNotFound: "metrik_opener_listing_not_found",
97
+ listingAmbiguous: "metrik_opener_listing_ambiguous",
98
+ listingRateOutOfBounds: "metrik_opener_listing_rate_out_of_bounds",
99
+ simulationFailed: "metrik_opener_settle_simulation_failed",
100
+ verifyError: "metrik_opener_verify_error",
101
+ transactionReverted: "metrik_opener_transaction_reverted",
102
+ streamEventMismatch: "metrik_opener_stream_event_mismatch",
103
+ settlementPending: "settlement_pending",
104
+ settleError: "metrik_opener_settle_error",
105
+ amountBelowMinimum: "metrik_opener_amount_below_minimum",
106
+ /**
107
+ * This authorization is already being settled, or already settled, by this
108
+ * facilitator instance. May carry the settlement transaction hash. Never a
109
+ * second success (see "At most one success" on the class).
110
+ */
111
+ duplicateSettlement: "duplicate_settlement",
112
+ settlementCapacity: "metrik_opener_settlement_capacity_exhausted",
113
+ };
114
+ /** Default facilitator minimum payment: 10000 base units = 0.01 USDC. */
115
+ export const DEFAULT_OPENER_MIN_AMOUNT = 10000n;
116
+ /** Hard cap on remembered authorizations; beyond it new settles fail closed. */
117
+ const MAX_LEDGER_ENTRIES = 100_000;
118
+ /** Grace after an authorization's `validBefore` before its ledger entry is dropped. */
119
+ const LEDGER_GRACE_SECONDS = 600n;
120
+ export class MetrikOpenerExactScheme {
121
+ config;
122
+ scheme = OPENER_SCHEME;
123
+ caipFamily = "eip155:*";
124
+ stock;
125
+ chainId;
126
+ allowlist;
127
+ receiptTimeoutMs;
128
+ minAmount;
129
+ now;
130
+ ledger = new Map();
131
+ /** Serializes relayer submissions (simulate + write) so nonces never collide. */
132
+ relayerQueue;
133
+ binding;
134
+ constructor(config) {
135
+ this.config = config;
136
+ this.chainId = chainIdFromNetwork(config.network);
137
+ if (!isAddress(config.factoryAddress)) {
138
+ throw new Error("factoryAddress must be an EVM address");
139
+ }
140
+ if (!isAddress(config.escrowAddress)) {
141
+ throw new Error("escrowAddress must be an EVM address");
142
+ }
143
+ if (config.upfrontFactoryAddress !== undefined &&
144
+ (!isAddress(config.upfrontFactoryAddress) ||
145
+ getAddress(config.upfrontFactoryAddress) ===
146
+ getAddress(config.factoryAddress))) {
147
+ throw new Error("upfrontFactoryAddress must be an EVM address other than factoryAddress");
148
+ }
149
+ if (config.upfrontImplementation !== undefined &&
150
+ !isAddress(config.upfrontImplementation)) {
151
+ throw new Error("upfrontImplementation must be an EVM address");
152
+ }
153
+ if (config.assetAllowlist.length === 0) {
154
+ throw new Error("assetAllowlist must not be empty");
155
+ }
156
+ for (const asset of config.assetAllowlist) {
157
+ if (!isAddress(asset)) {
158
+ throw new Error(`assetAllowlist entry is not an address: ${asset}`);
159
+ }
160
+ }
161
+ this.allowlist = new Set(config.assetAllowlist.map((asset) => asset.toLowerCase()));
162
+ this.receiptTimeoutMs = config.receiptTimeoutMs ?? 60_000;
163
+ if (!Number.isInteger(this.receiptTimeoutMs) ||
164
+ this.receiptTimeoutMs <= 0) {
165
+ throw new Error("receiptTimeoutMs must be a positive integer");
166
+ }
167
+ this.minAmount = config.minAmount ?? DEFAULT_OPENER_MIN_AMOUNT;
168
+ if (this.minAmount <= 0n) {
169
+ throw new Error("minAmount must be > 0");
170
+ }
171
+ this.now = config.now ?? (() => Math.floor(Date.now() / 1000));
172
+ this.relayerQueue = config.relayerQueue ?? new RelayerQueue();
173
+ // The stock verifier gets a READ-ONLY signer. Its settle path would send the
174
+ // USDC straight to payTo; making writes impossible closes that path even if
175
+ // a future refactor called it by mistake.
176
+ const publicClient = config.publicClient;
177
+ const refuseWrite = async () => {
178
+ throw new Error("MetrikOpenerExactScheme: the stock exact scheme may not broadcast; opener payments settle only via deployAndSettle");
179
+ };
180
+ this.stock = new ExactEvmScheme(toFacilitatorEvmSigner({
181
+ address: config.walletClient.account.address,
182
+ readContract: (args) => publicClient.readContract(args),
183
+ verifyTypedData: (args) => publicClient.verifyTypedData(args),
184
+ getCode: (args) => publicClient.getCode(args),
185
+ waitForTransactionReceipt: refuseWrite,
186
+ writeContract: refuseWrite,
187
+ sendTransaction: refuseWrite,
188
+ }),
189
+ // ERC-6492 factory deployment disabled: 6492 signatures are refused
190
+ // before the stock verifier is ever reached.
191
+ { eip6492AllowedFactories: [] });
192
+ }
193
+ /**
194
+ * Proves the on-chain wiring before the facilitator serves anything: the RPC
195
+ * is on the configured chain, the factory has code, points at the configured
196
+ * escrow, and its token is allowlisted. Throws otherwise (the service refuses
197
+ * to start).
198
+ */
199
+ async init() {
200
+ const { publicClient, factoryAddress, escrowAddress } = this.config;
201
+ const rpcChainId = await publicClient.getChainId();
202
+ if (rpcChainId !== this.chainId) {
203
+ throw new Error(`RPC chain id ${rpcChainId} does not match network ${this.config.network}`);
204
+ }
205
+ const code = await publicClient.getCode({ address: factoryAddress });
206
+ if (code === undefined || code === "0x") {
207
+ throw new Error(`no contract at factory address ${factoryAddress}`);
208
+ }
209
+ const [factoryEscrow, factoryUsdc] = await Promise.all([
210
+ publicClient.readContract({
211
+ address: factoryAddress,
212
+ abi: x402StreamOpenerFactoryAbi,
213
+ functionName: "escrow",
214
+ }),
215
+ publicClient.readContract({
216
+ address: factoryAddress,
217
+ abi: x402StreamOpenerFactoryAbi,
218
+ functionName: "usdc",
219
+ }),
220
+ ]);
221
+ if (getAddress(factoryEscrow) !== getAddress(escrowAddress)) {
222
+ throw new Error(`factory escrow ${factoryEscrow} is not the configured escrow ${escrowAddress}`);
223
+ }
224
+ if (!this.allowlist.has(factoryUsdc.toLowerCase())) {
225
+ throw new Error(`factory settlement token ${factoryUsdc} is not on the asset allowlist`);
226
+ }
227
+ let upfront;
228
+ if (this.config.upfrontFactoryAddress !== undefined) {
229
+ const upfrontFactory = getAddress(this.config.upfrontFactoryAddress);
230
+ const upfrontCode = await publicClient.getCode({
231
+ address: upfrontFactory,
232
+ });
233
+ if (upfrontCode === undefined || upfrontCode === "0x") {
234
+ throw new Error(`no contract at upfront factory address ${upfrontFactory}`);
235
+ }
236
+ const [upEscrow, upUsdc, upImplementation] = await Promise.all([
237
+ publicClient.readContract({
238
+ address: upfrontFactory,
239
+ abi: x402UpfrontStreamOpenerFactoryAbi,
240
+ functionName: "escrow",
241
+ }),
242
+ publicClient.readContract({
243
+ address: upfrontFactory,
244
+ abi: x402UpfrontStreamOpenerFactoryAbi,
245
+ functionName: "usdc",
246
+ }),
247
+ publicClient.readContract({
248
+ address: upfrontFactory,
249
+ abi: x402UpfrontStreamOpenerFactoryAbi,
250
+ functionName: "implementation",
251
+ }),
252
+ ]);
253
+ if (getAddress(upEscrow) !== getAddress(escrowAddress)) {
254
+ throw new Error(`upfront factory escrow ${upEscrow} is not the configured escrow ${escrowAddress}`);
255
+ }
256
+ if (getAddress(upUsdc) !== getAddress(factoryUsdc)) {
257
+ throw new Error(`upfront factory token ${upUsdc} is not the stream factory's ${factoryUsdc}`);
258
+ }
259
+ if (this.config.upfrontImplementation !== undefined &&
260
+ getAddress(upImplementation) !==
261
+ getAddress(this.config.upfrontImplementation)) {
262
+ throw new Error(`upfront factory implementation ${upImplementation} is not the expected ${this.config.upfrontImplementation}`);
263
+ }
264
+ upfront = {
265
+ factory: upfrontFactory,
266
+ implementation: getAddress(upImplementation),
267
+ };
268
+ }
269
+ this.binding = {
270
+ chainId: this.chainId,
271
+ factory: getAddress(factoryAddress),
272
+ escrow: getAddress(factoryEscrow),
273
+ usdc: getAddress(factoryUsdc),
274
+ relayer: this.config.walletClient.account.address,
275
+ ...(upfront === undefined ? {} : { upfront }),
276
+ };
277
+ return this.binding;
278
+ }
279
+ /** `/supported` extra: the marker a resource server checks before quoting an opener. */
280
+ getExtra(_network) {
281
+ const binding = this.binding;
282
+ if (binding === undefined) {
283
+ return undefined;
284
+ }
285
+ const marker = {
286
+ settlement: "x402-stream-opener",
287
+ factory: binding.factory,
288
+ escrow: binding.escrow,
289
+ usdc: binding.usdc,
290
+ chainId: binding.chainId,
291
+ minAmount: this.minAmount.toString(),
292
+ receiptTimeoutSeconds: Math.ceil(this.receiptTimeoutMs / 1000),
293
+ ...(this.config.advertiseRefundRelay === true
294
+ ? { refundRelay: true }
295
+ : {}),
296
+ ...(binding.upfront === undefined
297
+ ? {}
298
+ : {
299
+ upfrontOpener: {
300
+ factory: binding.upfront.factory,
301
+ implementation: binding.upfront.implementation,
302
+ escrow: binding.escrow,
303
+ termsVersion: OPENER_TERMS_VERSION.upfront,
304
+ },
305
+ }),
306
+ };
307
+ return { [OPENER_EXTRA_KEY]: marker };
308
+ }
309
+ getSigners(_network) {
310
+ return [this.config.walletClient.account.address];
311
+ }
312
+ async verify(payload, requirements, context) {
313
+ try {
314
+ const key = this.ledgerKey(payload);
315
+ const entry = key === undefined ? undefined : this.ledger.get(key);
316
+ if (entry !== undefined) {
317
+ if (entry.state === "pending" &&
318
+ deepEqual(entry.payload, payload) &&
319
+ deepEqual(entry.requirements, requirements)) {
320
+ // Submitted earlier, receipt not yet seen: the authorization may
321
+ // already be used on-chain, so the on-chain checks would refuse it.
322
+ // The retry is valid only so that settle can reconcile THIS tx.
323
+ return { isValid: true, payer: entry.auth.authorization.from };
324
+ }
325
+ return invalid(OpenerInvalidReason.duplicateSettlement, "this authorization is already being settled or was settled", payerOf(payload));
326
+ }
327
+ const checked = await this.check(payload, requirements, context);
328
+ if (!checked.ok) {
329
+ return checked.response;
330
+ }
331
+ return { isValid: true, payer: checked.auth.authorization.from };
332
+ }
333
+ catch (error) {
334
+ return invalid(OpenerInvalidReason.verifyError, describe(error), payerOf(payload));
335
+ }
336
+ }
337
+ async settle(payload, requirements, context) {
338
+ const network = requirements.network;
339
+ const payer = payerOf(payload);
340
+ const key = this.ledgerKey(payload);
341
+ if (key === undefined) {
342
+ // Not a parseable EIP-3009 authorization: the full check refuses it.
343
+ const checked = await this.check(payload, requirements, context).catch((error) => ({
344
+ ok: false,
345
+ response: invalid(OpenerInvalidReason.verifyError, describe(error), payer),
346
+ }));
347
+ return checked.ok
348
+ ? failure(OpenerInvalidReason.verifyError, network, payer, "unparseable authorization")
349
+ : refusal(checked.response, network, payer);
350
+ }
351
+ // ── At most one success per authorization ─────────────────────────────
352
+ // Everything from here to the ledger write is synchronous, so two
353
+ // concurrent settles of the same authorization can never both claim it.
354
+ this.pruneLedger();
355
+ const existing = this.ledger.get(key);
356
+ if (existing !== undefined) {
357
+ if (existing.state === "pending" &&
358
+ deepEqual(existing.payload, payload) &&
359
+ deepEqual(existing.requirements, requirements)) {
360
+ this.ledger.set(key, {
361
+ state: "in-flight",
362
+ validBefore: existing.validBefore,
363
+ hash: existing.hash,
364
+ });
365
+ return this.finish(key, payload, requirements, existing.validBefore, await this.awaitSettlement(existing.hash, existing.auth, existing.plan, existing.opener, network), existing);
366
+ }
367
+ return failure(OpenerInvalidReason.duplicateSettlement, network, payer, "this authorization is already being settled or was settled", existing.hash ?? "");
368
+ }
369
+ if (this.ledger.size >= MAX_LEDGER_ENTRIES) {
370
+ return failure(OpenerInvalidReason.settlementCapacity, network, payer, undefined);
371
+ }
372
+ const validBefore = BigInt(eip3009(payload).authorization.validBefore);
373
+ const entry = { state: "in-flight", validBefore };
374
+ this.ledger.set(key, entry);
375
+ try {
376
+ const checked = await this.check(payload, requirements, context);
377
+ if (!checked.ok) {
378
+ this.ledger.delete(key);
379
+ return refusal(checked.response, network, payer);
380
+ }
381
+ const { auth, plan, opener } = checked;
382
+ let hash;
383
+ try {
384
+ hash = await this.submitSerialized(auth, plan);
385
+ }
386
+ catch (error) {
387
+ this.ledger.delete(key);
388
+ return failure(OpenerInvalidReason.simulationFailed, network, auth.authorization.from, revertReason(error));
389
+ }
390
+ entry.hash = hash;
391
+ return this.finish(key, payload, requirements, validBefore, await this.awaitSettlement(hash, auth, plan, opener, network), { auth, plan, opener });
392
+ }
393
+ catch (error) {
394
+ // Unknown state after a possible broadcast: keep the authorization
395
+ // blocked (never a second success), but only if something was sent.
396
+ if (entry.hash === undefined) {
397
+ this.ledger.delete(key);
398
+ }
399
+ else {
400
+ this.ledger.set(key, { state: "final", validBefore, hash: entry.hash });
401
+ }
402
+ return failure(OpenerInvalidReason.settleError, network, payer, describe(error), entry.hash ?? "");
403
+ }
404
+ }
405
+ /** Records the outcome of one settle attempt in the ledger and returns it. */
406
+ finish(key, payload, requirements, validBefore, result, context) {
407
+ if (result.success) {
408
+ // THE one success for this authorization. Nothing succeeds again.
409
+ this.ledger.set(key, {
410
+ state: "final",
411
+ validBefore,
412
+ hash: result.transaction,
413
+ });
414
+ return result;
415
+ }
416
+ if (result.errorReason === OpenerInvalidReason.settlementPending) {
417
+ this.ledger.set(key, {
418
+ state: "pending",
419
+ validBefore,
420
+ hash: result.transaction,
421
+ payload,
422
+ requirements,
423
+ auth: context.auth,
424
+ plan: context.plan,
425
+ opener: context.opener,
426
+ });
427
+ return result;
428
+ }
429
+ if (result.errorReason === OpenerInvalidReason.transactionReverted) {
430
+ // A reverted deployAndSettle consumed nothing (the whole call reverts),
431
+ // so the authorization may be settled again.
432
+ this.ledger.delete(key);
433
+ return result;
434
+ }
435
+ // Mined but not the expected stream, or anything else terminal: block.
436
+ this.ledger.set(key, {
437
+ state: "final",
438
+ validBefore,
439
+ hash: result.transaction || "",
440
+ });
441
+ return result;
442
+ }
443
+ /**
444
+ * Simulate + broadcast `deployAndSettle`, one at a time. Concurrent
445
+ * submissions from one relayer would otherwise race for the same account
446
+ * nonce and all but one would fail after the upstream already did the work.
447
+ */
448
+ submitSerialized(auth, plan) {
449
+ const run = async () => {
450
+ const { request } = await this.simulateDeployAndSettle(auth, plan);
451
+ return this.config.walletClient.writeContract({
452
+ ...request,
453
+ chain: this.config.walletClient.chain ?? null,
454
+ });
455
+ };
456
+ return this.relayerQueue.run(run);
457
+ }
458
+ /** chain + asset + payer + authorization nonce, or undefined if unparseable. */
459
+ ledgerKey(payload) {
460
+ const auth = eip3009(payload);
461
+ const asset = payload.accepted?.asset;
462
+ if (auth === undefined || typeof asset !== "string")
463
+ return undefined;
464
+ return [
465
+ this.chainId,
466
+ asset.toLowerCase(),
467
+ auth.authorization.from.toLowerCase(),
468
+ auth.authorization.nonce.toLowerCase(),
469
+ ].join(":");
470
+ }
471
+ /**
472
+ * Drops entries whose authorization can no longer be settled by anyone
473
+ * (past `validBefore` plus a grace). Never drops an in-flight entry.
474
+ */
475
+ pruneLedger() {
476
+ const now = BigInt(this.now());
477
+ for (const [key, entry] of this.ledger) {
478
+ if (entry.state !== "in-flight" &&
479
+ entry.validBefore + LEDGER_GRACE_SECONDS < now) {
480
+ this.ledger.delete(key);
481
+ }
482
+ }
483
+ }
484
+ /**
485
+ * `eth_call` (simulate) `deployAndSettle` on the factory of the plan's kind:
486
+ * the 4-tuple stream factory, or the 6-tuple upfront factory.
487
+ */
488
+ async simulateDeployAndSettle(auth, plan) {
489
+ const { publicClient, walletClient } = this.config;
490
+ if (plan.kind === "upfront") {
491
+ const factory = this.binding?.upfront?.factory;
492
+ if (factory === undefined) {
493
+ throw new Error("the upfront opener factory is not configured");
494
+ }
495
+ return await publicClient.simulateContract({
496
+ account: walletClient.account,
497
+ address: factory,
498
+ abi: x402UpfrontStreamOpenerFactoryAbi,
499
+ functionName: "deployAndSettle",
500
+ args: [upfrontTermsTuple(plan.terms), authorizationTuple(auth)],
501
+ });
502
+ }
503
+ return await publicClient.simulateContract({
504
+ account: walletClient.account,
505
+ address: this.config.factoryAddress,
506
+ abi: x402StreamOpenerFactoryAbi,
507
+ functionName: "deployAndSettle",
508
+ args: [termsTuple(plan.terms), authorizationTuple(auth)],
509
+ });
510
+ }
511
+ async awaitSettlement(hash, auth, plan, opener, network) {
512
+ const payer = getAddress(auth.authorization.from);
513
+ let receipt;
514
+ try {
515
+ receipt = await this.config.publicClient.waitForTransactionReceipt({
516
+ hash,
517
+ timeout: this.receiptTimeoutMs,
518
+ });
519
+ }
520
+ catch (error) {
521
+ // Broadcast but unconfirmed: keep the pending entry so a retry
522
+ // reconciles against this hash. Never success without a receipt.
523
+ return failure(OpenerInvalidReason.settlementPending, network, payer, describe(error), hash);
524
+ }
525
+ if (receipt.status !== "success") {
526
+ return failure(OpenerInvalidReason.transactionReverted, network, payer, "deployAndSettle reverted on-chain", hash);
527
+ }
528
+ if (plan.kind === "upfront") {
529
+ return this.upfrontSettlement(receipt.logs, hash, auth, plan.terms, opener, network);
530
+ }
531
+ const terms = plan.terms;
532
+ const opened = findStreamOpened(receipt.logs, opener, getAddress(this.config.escrowAddress), auth, terms, BigInt(auth.authorization.value));
533
+ if (opened === undefined) {
534
+ return failure(OpenerInvalidReason.streamEventMismatch, network, payer, "the mined receipt does not show the expected stream", hash);
535
+ }
536
+ return {
537
+ success: true,
538
+ transaction: hash,
539
+ network,
540
+ payer,
541
+ amount: auth.authorization.value,
542
+ extra: {
543
+ [OPENER_EXTRA_KEY]: {
544
+ streamId: opened.streamId,
545
+ opener,
546
+ escrow: getAddress(this.config.escrowAddress),
547
+ deposit: auth.authorization.value,
548
+ ratePerSecond: terms.ratePerSecond.toString(),
549
+ // A JSON NUMBER: the SDK buyer (payWithX402) validates it as one.
550
+ expiresAt: Number(opened.expiresAt),
551
+ },
552
+ },
553
+ };
554
+ }
555
+ /**
556
+ * The success response of a mined UPFRONT settlement, or a mismatch failure.
557
+ * The receipt must show exactly what the signed terms imply:
558
+ *
559
+ * - `upfrontAmount > 0`: the opener's `UpfrontCharged` for this payer, nonce
560
+ * and operator, for exactly `upfrontAmount`, naming the stream it opened
561
+ * (or `bytes32(0)` when none was);
562
+ * - `price > upfrontAmount`: the escrow's `StreamOpened` (buyer = opener) and
563
+ * the opener's `StreamOpenedFor` with `deposit = price - upfrontAmount`;
564
+ * - `upfrontAmount == price` (upfront-only): NO stream at all. Success, with
565
+ * `streamId: null`: nothing was escrowed and there is nothing to refund.
566
+ *
567
+ * The reported `upfrontAmount` / `upfrontFee` are read from `UpfrontCharged`.
568
+ */
569
+ /** @internal */
570
+ upfrontSettlement(logs, hash, auth, terms, opener, network) {
571
+ const payer = getAddress(auth.authorization.from);
572
+ const escrow = getAddress(this.config.escrowAddress);
573
+ const refundable = upfrontRefundable(terms);
574
+ const mismatch = (why) => failure(OpenerInvalidReason.streamEventMismatch, network, payer, `the mined receipt does not show the expected upfront settlement: ${why}`, hash);
575
+ const charged = findUpfrontCharged(logs, opener, auth, terms);
576
+ if (terms.upfrontAmount > 0n && charged === undefined) {
577
+ return mismatch(`no UpfrontCharged for ${terms.upfrontAmount}`);
578
+ }
579
+ if (terms.upfrontAmount === 0n && charged !== undefined) {
580
+ return mismatch("an UpfrontCharged for terms without an upfront part");
581
+ }
582
+ let streamId = null;
583
+ let expiresAt = null;
584
+ if (refundable > 0n) {
585
+ const opened = findStreamOpened(logs, opener, escrow, auth, terms, refundable);
586
+ if (opened === undefined) {
587
+ return mismatch(`no stream with deposit ${refundable}`);
588
+ }
589
+ if (charged !== undefined &&
590
+ charged.streamId.toLowerCase() !== opened.streamId.toLowerCase()) {
591
+ return mismatch("UpfrontCharged names another stream");
592
+ }
593
+ streamId = opened.streamId;
594
+ expiresAt = opened.expiresAt;
595
+ }
596
+ else {
597
+ // Upfront-only: the opener must have opened nothing.
598
+ if (charged === undefined || !/^0x0{64}$/i.test(charged.streamId)) {
599
+ return mismatch("an upfront-only payment must report streamId 0");
600
+ }
601
+ if (findStreamOpened(logs, opener, escrow, auth, terms, undefined) !==
602
+ undefined) {
603
+ return mismatch("an upfront-only payment opened a stream");
604
+ }
605
+ }
606
+ return {
607
+ success: true,
608
+ transaction: hash,
609
+ network,
610
+ payer,
611
+ amount: auth.authorization.value,
612
+ extra: {
613
+ [OPENER_EXTRA_KEY]: {
614
+ // `null` (not bytes32(0)) for an upfront-only payment: no stream.
615
+ streamId,
616
+ opener,
617
+ escrow,
618
+ // The refundable, metered part — the most any refund can return.
619
+ deposit: refundable.toString(),
620
+ ratePerSecond: terms.ratePerSecond.toString(),
621
+ ...(expiresAt === null ? {} : { expiresAt: Number(expiresAt) }),
622
+ upfrontAmount: (charged?.amount ?? 0n).toString(),
623
+ upfrontFee: (charged?.fee ?? 0n).toString(),
624
+ },
625
+ },
626
+ };
627
+ }
628
+ /** Every Metrik check plus the stock verifier. Cheapest, most local first. */
629
+ async check(payload, requirements, context) {
630
+ const payer = payerOf(payload);
631
+ const refuse = (reason, message) => ({ ok: false, response: invalid(reason, message, payer) });
632
+ if (payload.x402Version !== 2) {
633
+ return refuse(OpenerInvalidReason.unsupportedVersion);
634
+ }
635
+ if (requirements.scheme !== OPENER_SCHEME) {
636
+ return refuse(OpenerInvalidReason.unsupportedScheme);
637
+ }
638
+ if (requirements.network !== this.config.network ||
639
+ payload.accepted?.network !== this.config.network) {
640
+ return refuse(OpenerInvalidReason.unsupportedNetwork);
641
+ }
642
+ // The payer signed against `accepted`; settle exactly that, nothing else.
643
+ if (!deepEqual(payload.accepted, requirements)) {
644
+ return refuse(OpenerInvalidReason.acceptedMismatch);
645
+ }
646
+ const method = requirements.extra?.assetTransferMethod;
647
+ if (method !== undefined && method !== "eip3009") {
648
+ return refuse(OpenerInvalidReason.unsupportedAssetTransferMethod);
649
+ }
650
+ const auth = eip3009(payload);
651
+ if (auth === undefined) {
652
+ return refuse(OpenerInvalidReason.unsupportedAssetTransferMethod);
653
+ }
654
+ if (auth.signature === undefined || !isHex(auth.signature)) {
655
+ return refuse(OpenerInvalidReason.missingSignature);
656
+ }
657
+ if (isErc6492Signature(auth.signature)) {
658
+ return refuse(OpenerInvalidReason.erc6492Unsupported, "ERC-6492 (counterfactual smart wallet) signatures cannot be settled through the stream opener; deploy the wallet first");
659
+ }
660
+ const binding = this.binding;
661
+ if (binding === undefined) {
662
+ return refuse(OpenerInvalidReason.verifyError, "facilitator not initialized");
663
+ }
664
+ if (!isAddress(requirements.asset) ||
665
+ !this.allowlist.has(requirements.asset.toLowerCase()) ||
666
+ getAddress(requirements.asset) !== binding.usdc) {
667
+ return refuse(OpenerInvalidReason.assetNotAllowed);
668
+ }
669
+ let parsed;
670
+ try {
671
+ parsed = termsFromExtra(requirements.extra);
672
+ }
673
+ catch (error) {
674
+ return refuse(OpenerInvalidReason.invalidTerms, describe(error));
675
+ }
676
+ const { terms, factory } = parsed;
677
+ // Only a factory this facilitator is bound to (and proved in init) is
678
+ // ever settled; its kind decides the terms shape — never the wire fields.
679
+ let kind;
680
+ if (factory === binding.factory) {
681
+ kind = "stream";
682
+ }
683
+ else if (binding.upfront !== undefined &&
684
+ factory === binding.upfront.factory) {
685
+ kind = "upfront";
686
+ }
687
+ else {
688
+ return refuse(OpenerInvalidReason.factoryMismatch);
689
+ }
690
+ if (parsed.termsVersion !== undefined &&
691
+ parsed.termsVersion !== OPENER_TERMS_VERSION[kind]) {
692
+ return refuse(OpenerInvalidReason.invalidTerms, `termsVersion ${parsed.termsVersion} does not match the ${kind} opener factory (${OPENER_TERMS_VERSION[kind]})`);
693
+ }
694
+ let plan;
695
+ if (kind === "upfront") {
696
+ if (parsed.upfront === undefined) {
697
+ return refuse(OpenerInvalidReason.invalidTerms, "the upfront opener factory needs price and upfrontAmount in the terms");
698
+ }
699
+ const upfrontTerms = { ...terms, ...parsed.upfront };
700
+ try {
701
+ assertValidUpfrontTerms(upfrontTerms);
702
+ }
703
+ catch (error) {
704
+ return refuse(OpenerInvalidReason.invalidTerms, describe(error));
705
+ }
706
+ plan = { kind, terms: upfrontTerms };
707
+ }
708
+ else {
709
+ if (parsed.upfront !== undefined) {
710
+ return refuse(OpenerInvalidReason.invalidTerms, "price/upfrontAmount are not terms of the stream opener factory (its payTo does not commit to them)");
711
+ }
712
+ plan = { kind, terms };
713
+ }
714
+ let amount;
715
+ try {
716
+ amount = parseUintString(requirements.amount, "amount");
717
+ }
718
+ catch (error) {
719
+ return refuse(OpenerInvalidReason.invalidAmount, describe(error));
720
+ }
721
+ if (amount <= 0n) {
722
+ return refuse(OpenerInvalidReason.invalidAmount, "amount must be > 0");
723
+ }
724
+ if (amount < this.minAmount) {
725
+ return refuse(OpenerInvalidReason.amountBelowMinimum, `amount ${amount} is below this facilitator's minimum ${this.minAmount}`);
726
+ }
727
+ if (plan.kind === "upfront") {
728
+ // The opener accepts exactly its price; anything else reverts on-chain.
729
+ if (amount !== plan.terms.price) {
730
+ return refuse(OpenerInvalidReason.invalidAmount, `amount ${amount} is not the upfront opener's fixed price ${plan.terms.price}`);
731
+ }
732
+ }
733
+ else if (amount > maxEarnable(terms)) {
734
+ return refuse(OpenerInvalidReason.amountExceedsTerms);
735
+ }
736
+ let predicted;
737
+ try {
738
+ predicted =
739
+ plan.kind === "upfront"
740
+ ? await this.config.publicClient.readContract({
741
+ address: binding.upfront?.factory,
742
+ abi: x402UpfrontStreamOpenerFactoryAbi,
743
+ functionName: "predict",
744
+ args: [upfrontTermsTuple(plan.terms)],
745
+ })
746
+ : await this.config.publicClient.readContract({
747
+ address: binding.factory,
748
+ abi: x402StreamOpenerFactoryAbi,
749
+ functionName: "predict",
750
+ args: [termsTuple(terms)],
751
+ });
752
+ }
753
+ catch (error) {
754
+ // predict reverts InvalidTerms for terms no transaction can deploy.
755
+ if (revertReason(error) === "InvalidTerms") {
756
+ return refuse(OpenerInvalidReason.invalidTerms, "InvalidTerms");
757
+ }
758
+ throw error;
759
+ }
760
+ if (!isAddress(requirements.payTo) ||
761
+ getAddress(requirements.payTo) !== getAddress(predicted)) {
762
+ return refuse(OpenerInvalidReason.payToMismatch);
763
+ }
764
+ const listing = await this.config.listingResolver.resolve(terms);
765
+ if (!listing.ok) {
766
+ const reason = {
767
+ "listing-registry-unavailable": OpenerInvalidReason.listingUnavailable,
768
+ "listing-not-found": OpenerInvalidReason.listingNotFound,
769
+ "listing-ambiguous": OpenerInvalidReason.listingAmbiguous,
770
+ "listing-rate-out-of-bounds": OpenerInvalidReason.listingRateOutOfBounds,
771
+ }[listing.reason];
772
+ return refuse(reason, listing.message);
773
+ }
774
+ // Standard exact checks: signature, to == payTo, value == amount, window,
775
+ // asset contract, and a simulation of the EIP-3009 transfer itself.
776
+ const stock = await this.stock.verify(payload, requirements, context);
777
+ if (!stock.isValid) {
778
+ return { ok: false, response: stock };
779
+ }
780
+ const signed = auth;
781
+ try {
782
+ await this.simulateDeployAndSettle(signed, plan);
783
+ }
784
+ catch (error) {
785
+ return refuse(OpenerInvalidReason.simulationFailed, revertReason(error));
786
+ }
787
+ return { ok: true, auth: signed, plan, opener: getAddress(predicted) };
788
+ }
789
+ }
790
+ function authorizationTuple(auth) {
791
+ return {
792
+ from: getAddress(auth.authorization.from),
793
+ value: BigInt(auth.authorization.value),
794
+ validAfter: BigInt(auth.authorization.validAfter),
795
+ validBefore: BigInt(auth.authorization.validBefore),
796
+ nonce: auth.authorization.nonce,
797
+ signature: auth.signature,
798
+ };
799
+ }
800
+ /**
801
+ * Finds the pair of events a genuine opener settlement emits in ONE receipt:
802
+ * the escrow's `StreamOpened` with `buyer == opener` and exactly these terms,
803
+ * and the opener's `StreamOpenedFor` binding that stream to this payer, nonce
804
+ * and amount.
805
+ */
806
+ function findStreamOpened(logs, opener, escrow, auth, terms,
807
+ /** Expected stream deposit; `undefined` matches ANY deposit (an absence check). */
808
+ deposit) {
809
+ const payer = getAddress(auth.authorization.from);
810
+ const escrowStreams = new Map();
811
+ for (const log of logs) {
812
+ if (getAddress(log.address) !== escrow)
813
+ continue;
814
+ try {
815
+ const decoded = decodeEventLog({
816
+ abi: streamEscrowV2Abi,
817
+ data: log.data,
818
+ topics: log.topics,
819
+ });
820
+ if (decoded.eventName !== "StreamOpened")
821
+ continue;
822
+ const args = decoded.args;
823
+ if (getAddress(args.buyer) === opener &&
824
+ getAddress(args.operator) === getAddress(terms.operator) &&
825
+ args.serviceRef.toLowerCase() === terms.serviceRef.toLowerCase() &&
826
+ (deposit === undefined || args.deposit === deposit) &&
827
+ args.ratePerSecond === terms.ratePerSecond) {
828
+ escrowStreams.set(args.streamId.toLowerCase(), BigInt(args.expiresAt));
829
+ }
830
+ }
831
+ catch {
832
+ continue;
833
+ }
834
+ }
835
+ for (const log of logs) {
836
+ if (getAddress(log.address) !== opener)
837
+ continue;
838
+ try {
839
+ const decoded = decodeEventLog({
840
+ abi: x402StreamOpenerAbi,
841
+ data: log.data,
842
+ topics: log.topics,
843
+ });
844
+ if (decoded.eventName !== "StreamOpenedFor")
845
+ continue;
846
+ const args = decoded.args;
847
+ const expiresAt = escrowStreams.get(args.streamId.toLowerCase());
848
+ if (expiresAt !== undefined &&
849
+ getAddress(args.payer) === payer &&
850
+ args.authorizationNonce.toLowerCase() ===
851
+ auth.authorization.nonce.toLowerCase() &&
852
+ (deposit === undefined || args.deposit === deposit)) {
853
+ return { streamId: args.streamId, expiresAt };
854
+ }
855
+ }
856
+ catch {
857
+ continue;
858
+ }
859
+ }
860
+ return undefined;
861
+ }
862
+ /**
863
+ * The opener's `UpfrontCharged` for this payer, authorization nonce and
864
+ * operator, for exactly the terms' `upfrontAmount` (its `fee` is the protocol
865
+ * fee taken out of it). `undefined` when the receipt carries none.
866
+ */
867
+ function findUpfrontCharged(logs, opener, auth, terms) {
868
+ const payer = getAddress(auth.authorization.from);
869
+ for (const log of logs) {
870
+ if (getAddress(log.address) !== opener)
871
+ continue;
872
+ try {
873
+ const decoded = decodeEventLog({
874
+ abi: x402UpfrontStreamOpenerAbi,
875
+ data: log.data,
876
+ topics: log.topics,
877
+ });
878
+ if (decoded.eventName !== "UpfrontCharged")
879
+ continue;
880
+ const args = decoded.args;
881
+ if (getAddress(args.payer) === payer &&
882
+ args.authorizationNonce.toLowerCase() ===
883
+ auth.authorization.nonce.toLowerCase() &&
884
+ getAddress(args.operator) === getAddress(terms.operator) &&
885
+ args.amount === terms.upfrontAmount &&
886
+ args.fee <= args.amount) {
887
+ return { streamId: args.streamId, amount: args.amount, fee: args.fee };
888
+ }
889
+ }
890
+ catch {
891
+ continue;
892
+ }
893
+ }
894
+ return undefined;
895
+ }
896
+ /**
897
+ * Reads an opener facilitator marker out of a `/supported` kind's `extra`, or
898
+ * `undefined` when the facilitator does not advertise opener settlement.
899
+ */
900
+ export function readOpenerFacilitatorMarker(extra) {
901
+ const raw = extra?.[OPENER_EXTRA_KEY];
902
+ if (typeof raw !== "object" || raw === null)
903
+ return undefined;
904
+ const marker = raw;
905
+ if (marker.settlement !== "x402-stream-opener" ||
906
+ typeof marker.factory !== "string" ||
907
+ !isAddress(marker.factory) ||
908
+ typeof marker.escrow !== "string" ||
909
+ !isAddress(marker.escrow) ||
910
+ typeof marker.usdc !== "string" ||
911
+ !isAddress(marker.usdc) ||
912
+ typeof marker.chainId !== "number" ||
913
+ typeof marker.minAmount !== "string" ||
914
+ !/^[1-9]\d*$/.test(marker.minAmount) ||
915
+ typeof marker.receiptTimeoutSeconds !== "number" ||
916
+ !Number.isInteger(marker.receiptTimeoutSeconds) ||
917
+ marker.receiptTimeoutSeconds <= 0) {
918
+ return undefined;
919
+ }
920
+ return {
921
+ settlement: "x402-stream-opener",
922
+ factory: getAddress(marker.factory),
923
+ escrow: getAddress(marker.escrow),
924
+ usdc: getAddress(marker.usdc),
925
+ chainId: marker.chainId,
926
+ minAmount: marker.minAmount,
927
+ receiptTimeoutSeconds: marker.receiptTimeoutSeconds,
928
+ ...(marker.refundRelay === true ? { refundRelay: true } : {}),
929
+ ...(readUpfrontMarker(marker.upfrontOpener) ?? {}),
930
+ };
931
+ }
932
+ /** The optional `upfrontOpener` entry of a marker; a malformed one is ignored (absent). */
933
+ function readUpfrontMarker(raw) {
934
+ if (typeof raw !== "object" || raw === null)
935
+ return undefined;
936
+ const entry = raw;
937
+ if (typeof entry.factory !== "string" ||
938
+ !isAddress(entry.factory) ||
939
+ typeof entry.implementation !== "string" ||
940
+ !isAddress(entry.implementation) ||
941
+ typeof entry.escrow !== "string" ||
942
+ !isAddress(entry.escrow) ||
943
+ entry.termsVersion !== OPENER_TERMS_VERSION.upfront) {
944
+ return undefined;
945
+ }
946
+ return {
947
+ upfrontOpener: {
948
+ factory: getAddress(entry.factory),
949
+ implementation: getAddress(entry.implementation),
950
+ escrow: getAddress(entry.escrow),
951
+ termsVersion: OPENER_TERMS_VERSION.upfront,
952
+ },
953
+ };
954
+ }
955
+ function eip3009(payload) {
956
+ const inner = payload.payload;
957
+ if (typeof inner !== "object" || inner === null)
958
+ return undefined;
959
+ const candidate = inner;
960
+ if (!isEIP3009Payload(candidate))
961
+ return undefined;
962
+ const authorization = candidate.authorization;
963
+ if (typeof authorization !== "object" ||
964
+ authorization === null ||
965
+ typeof authorization.from !== "string" ||
966
+ !isAddress(authorization.from) ||
967
+ typeof authorization.to !== "string" ||
968
+ typeof authorization.value !== "string" ||
969
+ typeof authorization.validAfter !== "string" ||
970
+ typeof authorization.validBefore !== "string" ||
971
+ typeof authorization.nonce !== "string" ||
972
+ !isHex(authorization.nonce) ||
973
+ authorization.nonce.length !== 66) {
974
+ return undefined;
975
+ }
976
+ try {
977
+ parseUintString(authorization.value, "authorization.value");
978
+ parseUintString(authorization.validAfter, "authorization.validAfter");
979
+ parseUintString(authorization.validBefore, "authorization.validBefore");
980
+ }
981
+ catch {
982
+ return undefined;
983
+ }
984
+ return candidate;
985
+ }
986
+ function payerOf(payload) {
987
+ const from = payload.payload?.authorization?.from;
988
+ return typeof from === "string" && isAddress(from)
989
+ ? getAddress(from)
990
+ : undefined;
991
+ }
992
+ function invalid(reason, message, payer) {
993
+ return {
994
+ isValid: false,
995
+ invalidReason: reason,
996
+ ...(message === undefined ? {} : { invalidMessage: message }),
997
+ ...(payer === undefined ? {} : { payer }),
998
+ };
999
+ }
1000
+ function failure(reason, network, payer, message, transaction = "") {
1001
+ return {
1002
+ success: false,
1003
+ errorReason: reason,
1004
+ ...(message === undefined ? {} : { errorMessage: message }),
1005
+ transaction,
1006
+ network,
1007
+ ...(payer === undefined ? {} : { payer }),
1008
+ };
1009
+ }
1010
+ function refusal(response, network, payer) {
1011
+ return {
1012
+ success: false,
1013
+ errorReason: response.invalidReason ?? OpenerInvalidReason.verifyError,
1014
+ ...(response.invalidMessage === undefined
1015
+ ? {}
1016
+ : { errorMessage: response.invalidMessage }),
1017
+ transaction: "",
1018
+ network,
1019
+ ...(payer === undefined ? {} : { payer }),
1020
+ };
1021
+ }
1022
+ function revertReason(error) {
1023
+ if (error instanceof BaseError) {
1024
+ const reverted = error.walk((cause) => cause instanceof ContractFunctionRevertedError);
1025
+ if (reverted instanceof ContractFunctionRevertedError) {
1026
+ return (reverted.data?.errorName ?? reverted.reason ?? reverted.shortMessage);
1027
+ }
1028
+ return error.shortMessage;
1029
+ }
1030
+ return describe(error);
1031
+ }
1032
+ function describe(error) {
1033
+ return error instanceof Error ? error.message : String(error);
1034
+ }
1035
+ //# sourceMappingURL=scheme.js.map