@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
package/docs/x402.md CHANGED
@@ -1,519 +1,738 @@
1
1
  # Metrik and x402
2
2
 
3
- > **Metrik ships a real, spec-compliant x402 facilitator — one that refuses to
4
- > settle payment for a service its own oracle says is not delivering.** It does
5
- > NOT express its metered escrow stream as an x402 payment, and it never will
6
- > under the schemes x402 ships today.
3
+ > **Status.** This package implements **x402 v2 `exact` payments that settle
4
+ > into a `StreamEscrowV2` stream**, through the ownerless
5
+ > `X402StreamOpenerFactory` (metrik-contracts#48). It ships a facilitator
6
+ > service (`metrik-x402-opener-facilitator`) and an opt-in x402 v2 mode on the
7
+ > seller gateway. **Both are OFF by default** and have not yet run in
8
+ > production. The factory is deployed and Sourcify-verified on Base Sepolia at
9
+ > `0x43DA0D9602f7fc5ac0bb90461e39b033EB493d24` (implementation
10
+ > `0xbD8B7B2F49Fdb954bda6bE5ce22218d44345DC43`, block 47337415); everything
11
+ > here is testnet only.
7
12
  >
8
- > Those two sentences are the whole story, and the rest of this document is the
9
- > evidence for both halves. Earlier versions of this file said flatly that "this
10
- > package does not ship an x402 facilitator". That was true when it was written
11
- > and is now out of date; the analysis it was built on is unchanged and still
12
- > below.
13
-
14
- The tagline — _x402 proves the payment, Metrik proves the delivery_ — used to be
15
- positioning only. It is now literally what the code does: a standard `exact`
16
- payment, verified by a standard facilitator, with one extra question asked
17
- before settlement that no other facilitator can ask.
18
-
19
- ---
20
-
21
- ## The two things Metrik does with 402, and how they differ
22
-
23
- | | **Verified stream** (`src/x402/facilitator.ts`) | **x402 `exact` + delivery gate** (this document's subject) |
24
- | ------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- |
25
- | Protocol | Metrik-native, scheme `streamproof-verified-stream` | **Standard x402 v1, scheme `exact`** |
26
- | Header | `X-METRIK-PAYMENT` | `X-PAYMENT` / `X-PAYMENT-RESPONSE` |
27
- | Works with `x402-fetch`? | No | **Yes, untouched** |
28
- | Money | Escrowed in `StreamEscrowV2`, released per verified second | One EIP-3009 transfer, payer -> payee |
29
- | If the service stops delivering | Unearned escrow is **recoverable** by the buyer | The payment is **prevented**, never recovered |
30
- | Buyer protection | Strong | **Strictly weaker — see below** |
31
-
32
- **The x402 path is weaker buyer protection than the stream, and that is not a
33
- detail to bury.** It holds none of the buyer's money, so it can only stop a
34
- payment that has not happened yet. Money already paid through it is gone,
35
- exactly as it would be through any other facilitator. The stream remains the
36
- only path where "the service stopped delivering" and "you get your money back"
37
- are the same sentence.
38
-
39
- What the gate buys is reach: an agent that already speaks x402 gets a meaningful
40
- safety property for free, with no SDK, no escrow and no Metrik-specific code.
41
-
42
- ---
43
-
44
- ## The unlock: nothing constrains _why_ a facilitator says no
45
-
46
- The x402 facilitator interface is `POST /verify`, `POST /settle`,
47
- `GET /supported`. `/verify` answers with:
13
+ > **Upfront ("minimum charge") opener.** A listing can instead quote the
14
+ > `X402UpfrontStreamOpenerFactory` (metrik-contracts#54; Base Sepolia
15
+ > `0x17126c80C12a51183d3664EEE2b602552592c2Aa`, implementation
16
+ > `0xA9C58A2554Eb869B0D1ECBa46a0435cFFC90341d`, bound to StreamEscrowV3
17
+ > `0x0643ba482C6A7aF31a1f8225e7e7e79Cfc467b13`): part of each payment is a
18
+ > NON-REFUNDABLE per-call charge, only the rest is escrowed and metered. See
19
+ > [The upfront (minimum-charge) opener](#the-upfront-minimum-charge-opener).
20
+ > The V3-bound stream factory `0x3d3a…cFB2` stays live, and its clones are
21
+ > still served and refunded.
22
+ >
23
+ > The separate Metrik-native `402` flow (scheme `streamproof-verified-stream`,
24
+ > header `X-METRIK-PAYMENT`) described at the end is **not** x402 and is
25
+ > unchanged.
26
+
27
+ ## The hard rule: an opener `payTo` is settled ONLY by the Metrik opener facilitator
28
+
29
+ **Read this before running any of it.** The x402 `payTo` in this mode is the
30
+ address of an `X402StreamOpener`. Every stock x402 `exact` facilitator settles
31
+ by calling `USDC.transferWithAuthorization(to = payTo)` directly. Pointed at an
32
+ opener, that transfer succeeds and **strands 100% of the payer's payment in the
33
+ opener permanently**: the opener deliberately has no sweep or "claim stray
34
+ funds" path (one would be exploitable, because a used and a cancelled EIP-3009
35
+ nonce are indistinguishable on-chain). The same happens if anyone who sees the
36
+ signed payload submits it to USDC first. See metrik-contracts
37
+ `evm/docs/x402-opener.md`.
38
+
39
+ So, in this package, and for anyone integrating it:
40
+
41
+ 1. **Only the Metrik opener facilitator may settle an opener `payTo`.** It
42
+ calls `factory.deployAndSettle(terms, authorization)`, which pulls the
43
+ payment into the opener and opens the stream in the same transaction. Its
44
+ `GET /supported` advertises a marker —
45
+ `kinds[].extra.metrikStreamOpener = {settlement: "x402-stream-opener",
46
+ factory, escrow, usdc, chainId, minAmount, receiptTimeoutSeconds, refundRelay?, upfrontOpener?}` — so a resource server can tell it apart
47
+ from a stock facilitator. `refundRelay: true` means the same service also
48
+ runs the gasless refund relay (see below). `upfrontOpener = {factory,
49
+ implementation, escrow, termsVersion: "upfront-1"}` is present when it also
50
+ settles the upfront opener kind; `factory` stays the stream factory.
51
+ 2. **The gateway refuses to start** unless its configured facilitator's
52
+ `/supported` carries that marker for exactly the gateway's factory, escrow,
53
+ token and chain. It re-checks the marker on a timer (default every 300 s)
54
+ and **fails closed** whenever it cannot prove it: `503
55
+ x402-facilitator-unavailable`, no `PAYMENT-REQUIRED`, nothing verified,
56
+ nothing served, nothing settled anywhere else. A facilitator that is
57
+ unreachable when a payment arrives produces `402`, never a free call.
58
+ 3. **Never advertise an opener `payTo` to third-party facilitators or discovery
59
+ catalogs** (the x402 Bazaar extension or any other). The gateway registers
60
+ no discovery extension and never submits its route to any catalog. Only
61
+ when the operator configures `METRIK_GATEWAY_X402V2_SCHEMA` does the 402
62
+ _declare_ an input/output schema, in the standard `extensions.bazaar` shape,
63
+ so a client can read what to send; the only facilitator that ever sees the
64
+ payment is the Metrik opener facilitator, and it catalogs nothing. Do not
65
+ register the route with a discovery service.
66
+ 4. **Authenticate an opener only as `factory.predict(terms) == payTo`** (or
67
+ `factory.isOpener(address)`), never by code or codehash: anyone can clone the
68
+ implementation outside the factory with byte-identical code. The
69
+ facilitator recomputes `predict` on-chain for every payment.
70
+ 5. **The signed payment never leaves the gateway→facilitator path.** The
71
+ gateway strips `PAYMENT-SIGNATURE` (and `X-PAYMENT`) before proxying to the
72
+ seller's upstream.
73
+
74
+ ## How a payment flows
48
75
 
49
- ```ts
50
- VerifyResponse = { isValid: boolean, invalidReason?: ErrorReason, payer?: string }
76
+ ```
77
+ buyer (@x402/fetch) gateway (x402 mode) opener facilitator chain
78
+ │ GET /resource ───────────▶ 402 + PAYMENT-REQUIRED
79
+ │ payTo = factory.predict(terms)
80
+ │ PAYMENT-SIGNATURE ─────────▶ POST /verify ───────────────▶ all checks below
81
+ │ upstream request (status < 400?)
82
+ │ POST /settle (bearer) ──────▶ deployAndSettle ────────────▶ USDC → opener → StreamEscrowV2
83
+ │ 200 + body + PAYMENT-RESPONSE ◀── receipt shows the stream ◀─────────────────────────── StreamOpened / StreamOpenedFor
51
84
  ```
52
85
 
53
- `isValid` is a boolean and the spec does not constrain what a facilitator may
54
- consider before returning `false`. A facilitator is the party a resource server
55
- delegates its "should this payment go through" decision to. Metrik simply knows
56
- one more thing than every other facilitator, because it runs a prober.
57
-
58
- So: standard scheme, standard headers, standard client, no contract change — and
59
- a property no other facilitator can offer, because no other facilitator has an
60
- oracle.
61
-
62
- ### What `isValid: true` from Metrik does and does not mean
63
-
64
- It means **"this payment is well-formed and Metrik has no fresh evidence that
65
- this service is failing."** It is not an endorsement, not a correctness
66
- guarantee, and not a promise about the next call. The gate only ever subtracts.
67
-
68
- And per the standing rule: Metrik verifies **delivery**, not **correctness**. A
69
- service that returns a fast, well-formed, wrong answer passes every check Metrik
70
- makes.
71
-
72
- ---
73
-
74
- ## Tier 1: the gateway speaks 402
75
-
76
- `CallerAuthGateway` authorizes requests carrying a Metrik `InvocationCapability`
77
- — a buyer with an open, funded, verified stream. That path is **unchanged**. A
78
- request with a valid capability behaves today exactly as it did before.
79
-
80
- What is new is opt-in: set `METRIK_GATEWAY_X402_PRICE_BASE_UNITS` and a request
81
- with **no** capability gets a real x402 `402` instead of a `403`:
86
+ - **Terms.** One listing's immutable terms are `{operator, serviceRef,
87
+ ratePerSecond, maxDuration}`; the opener for them lives at
88
+ `factory.predict(terms)` whether or not it is deployed yet. The terms travel
89
+ in `PaymentRequirements.extra.metrikStreamOpener` (with the factory address);
90
+ the payer's EIP-3009 signature binds `to = payTo`, so it commits to them.
91
+ - **Price = deposit.** Each paid request opens one stream with `deposit =
92
+ price`, on the listing's terms. The escrow records the **opener** as the
93
+ stream's buyer; the opener records the payer (`payerOf(streamId)`).
94
+ - **Earning.** Unchanged: the operator earns `ratePerSecond × verified
95
+ seconds`, capped at the deposit, claimed with `claimWithProof` against the
96
+ oracle's checkpoints, minus the 0.5% protocol fee.
97
+ - **Settle after serve.** The gateway settles only when the upstream answered
98
+ `< 400`, and before any upstream byte reaches the buyer (the reference
99
+ `@x402/*` middleware order). An upstream `>= 400` submits nothing, so no
100
+ funds move. A failed settlement sends no upstream body.
101
+
102
+ ### What the facilitator checks (every payment, fail closed)
103
+
104
+ Standard `exact` checks are delegated to the official `@x402/evm`
105
+ `ExactEvmScheme` verifier (signature, `to == payTo`, `value == amount`,
106
+ validity window, asset contract, a simulation of the transfer). The stock
107
+ scheme is given a signer that cannot broadcast, so its direct-transfer settle
108
+ path is structurally unreachable. On top of it:
109
+
110
+ | Check | Refusal reason |
111
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
112
+ | x402 v2, scheme `exact`, the configured CAIP-2 network | `metrik_opener_unsupported_*` |
113
+ | `payload.accepted` equals the requirements exactly | `metrik_opener_accepted_requirements_mismatch` |
114
+ | Asset transfer method is EIP-3009 (Permit2 and everything else refused) | `metrik_opener_asset_transfer_method_not_eip3009` |
115
+ | Not an ERC-6492 (counterfactual smart wallet) signature | `metrik_opener_erc6492_signature_unsupported` |
116
+ | Asset is allowlisted and equals `factory.usdc()` | `metrik_opener_asset_not_allowed` |
117
+ | Terms are well formed; the factory named is the configured one | `metrik_opener_invalid_terms` / `_factory_mismatch` |
118
+ | `0 < amount <= ratePerSecond × maxDuration` | `metrik_opener_invalid_amount` / `_amount_exceeds_rate_times_duration` |
119
+ | `payTo == factory.predict(terms)`, read on-chain | `metrik_opener_pay_to_not_predicted_opener` |
120
+ | `(operator, serviceRef)` resolve to exactly one **operator-signed** listing (the existing discovery verification: schema, re-derived `serviceRef`, recovered EIP-712 operator signature), and the rate is inside its signed `commercial` bounds | `metrik_opener_listing_*` |
121
+ | `deployAndSettle` simulates cleanly from the relayer | `metrik_opener_settle_simulation_failed` |
122
+
123
+ `settle` re-runs all of it, broadcasts `deployAndSettle`, waits for the
124
+ receipt, and reports `success: true` only when the mined receipt contains both
125
+ the escrow's `StreamOpened` (buyer = opener, exactly these terms and deposit)
126
+ and the opener's `StreamOpenedFor` (this payer, nonce and amount). The
127
+ `SettleResponse` carries `extra.metrikStreamOpener = {streamId, opener, escrow,
128
+ deposit, ratePerSecond, expiresAt}`. A revert (for example an authorization that
129
+ was already used) is `success: false` with the reason. A receipt that does not
130
+ arrive in time is `settlement_pending`; a later settle of the same payload
131
+ reconciles against that transaction instead of broadcasting a second one.
132
+
133
+ ### At most one success per authorization
134
+
135
+ Each EIP-3009 authorization — keyed by chain, asset, payer and authorization
136
+ nonce — receives **at most one successful `SettleResponse` from a facilitator
137
+ instance, ever**:
138
+
139
+ - A settle (or verify) of an authorization that is already in flight or already
140
+ settled returns `success: false` / `isValid: false` with
141
+ `duplicate_settlement` (carrying the tx hash when there is one). One payment
142
+ can never buy two served responses.
143
+ - After `settlement_pending`, a retry of the SAME payload verifies (even though
144
+ the authorization may already be used on-chain) and its settle reconciles to
145
+ the mined outcome; it receives the one success only if none was delivered
146
+ before.
147
+ - The gateway enforces the same rule on its side: a `PAYMENT-SIGNATURE` whose
148
+ authorization is in flight or already settled gets `409
149
+ x402-payment-already-used` **without** an upstream call; a retry after
150
+ `settlement_pending` is let through so it can reconcile.
151
+ - Relayer submissions (simulate + broadcast) are serialized, so concurrent
152
+ payers never collide on the relayer's account nonce.
153
+
154
+ **Scope of the guarantee: both ledgers are in-memory and per instance.** They
155
+ do not survive a restart and are not shared between replicas. Run exactly one
156
+ facilitator instance (and one gateway instance per listing); a multi-instance
157
+ deployment would need a shared store, which does not exist today. Entries are
158
+ kept until the authorization's `validBefore` has passed (plus a grace), after
159
+ which USDC itself refuses the authorization.
160
+
161
+ ## The upfront (minimum-charge) opener
162
+
163
+ A stream-opener payment funds a stream of the WHOLE payment, and accrual starts
164
+ only at the first verified interval: a buyer who takes the response and closes
165
+ within seconds reclaims almost all of it, so one delivered call can cost
166
+ nothing. The upfront opener (metrik-contracts#54, design note
167
+ `docs/x402-opener-min-charge.md`) fixes that with a new opener and factory;
168
+ nothing deployed changed.
169
+
170
+ - **Terms** gain two fields: `{operator, serviceRef, ratePerSecond,
171
+ maxDuration, price, upfrontAmount}`. All six are committed by `payTo =
172
+ upfrontFactory.predict(terms)` (CREATE2 over `abi.encode(terms)`), so the
173
+ payer's EIP-3009 signature binds the split too.
174
+ - **`amount == price`**, exactly: the opener rejects any other value on-chain
175
+ (`PriceMismatch`) before anything moves.
176
+ - **At settlement, in the same transaction**, `upfrontAmount` is paid to the
177
+ seller (minus the escrow's protocol fee, rounded up, to its fee recipient)
178
+ and is **NOT refundable**: it pays for the call whatever is delivered. Only
179
+ `price - upfrontAmount` funds a `StreamEscrowV3` stream, metered exactly like
180
+ a stream opener's, and that deposit is the most any refund can return.
181
+ - **`upfrontAmount == price` is upfront-only**: no stream is opened at all. The
182
+ settlement reports `streamId: null` (on-chain `bytes32(0)`), the response is
183
+ served, no metered session exists, and there is nothing to close, reclaim or
184
+ refund.
185
+
186
+ ### The 402 `extra` (what the SDK enforces before signing)
82
187
 
83
188
  ```json
84
189
  {
85
- "x402Version": 1,
86
- "error": "X-PAYMENT header is required",
87
- "accepts": [
88
- {
89
- "scheme": "exact",
90
- "network": "base-sepolia",
91
- "maxAmountRequired": "1000",
92
- "resource": "https://livedata.example/price",
93
- "payTo": "0x...",
94
- "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
95
- "maxTimeoutSeconds": 60,
96
- "extra": { "name": "USDC", "version": "2", "metrikServiceRef": "0x..." }
97
- }
98
- ]
190
+ "factory": "<registry upfront factory>",
191
+ "termsVersion": "upfront-1",
192
+ "operator": "0x…",
193
+ "serviceRef": "0x…",
194
+ "ratePerSecond": "100",
195
+ "maxDuration": "3600",
196
+ "price": "60000",
197
+ "upfrontAmount": "15000",
198
+ "refundUrl": "https://<relay>/streams/{streamId}"
99
199
  }
100
200
  ```
101
201
 
102
- Three rules govern it:
103
-
104
- 1. **The two paths never overlap.** Capability present -> stream path. Capability
105
- absent -> `402`. **Both present -> `400 ambiguous-payment-mode`**, refused
106
- rather than guessed: the failure mode of guessing is charging the caller
107
- through one path while they believe they paid through the other.
108
- 2. **`payTo` is derived from the operator-signed descriptor**, resolved at
109
- startup from the registry for this gateway's `serviceRef`. There is no
110
- `payTo` environment variable, and the gateway **refuses to start** if the
111
- signed listing cannot be resolved. `payTo` is the one field whose corruption
112
- silently redirects money, so it is never configurable and never caller-supplied.
113
- 3. **The price is the seller's and is not signed.** The descriptor schema has
114
- `minRatePerSecond`/`maxRatePerSecond` for metered streams and no per-call
115
- price field, so a per-call amount can only come from the seller running the
116
- gateway. Legitimate — it is their price on their gateway — but a buyer
117
- **cannot** cross-check it against the registry the way they can cross-check
118
- the payee.
119
-
120
- Ordering on a paid request is `verify -> call upstream -> settle -> flush`.
121
- Settling before the response is flushed means a call that produced nothing is
122
- never charged for, and a payment that cannot be settled returns `402` rather
123
- than handing over the goods for free. Both edges favour the buyer.
124
-
125
- That ordering has one sharp edge, and it is closed rather than merely noted.
126
- Because the upstream runs _before_ settlement, an upstream slower than the
127
- signed authorization's own `validBefore` would produce its **side effects** and
128
- then be unsettleable — the caller never gets the response body, which is flushed
129
- only after settlement, but the work would have happened for free. The `exact`
130
- verifier needs only `validBefore >= now + 6s` while the gateway's upstream
131
- timeout defaults to 30 s, so the two can genuinely cross. The gateway therefore
132
- caps each upstream call at `min(upstreamTimeout, validBefore - now - 10s)` and
133
- returns `402` outright when that leaves no usable window. A payment that cannot
134
- survive its own round trip is refused before the upstream is touched.
135
-
136
- It also refuses before the upstream when the facilitator reports
137
- `metrik.settleable: false` — see below.
138
-
139
- `extra.metrikServiceRef` is published so a client can look the listing up. **The
140
- facilitator ignores it**, deliberately — it arrives inside a caller-supplied
141
- body, and reading it would let a caller choose which service's delivery record
142
- gets checked. There is a test for exactly that.
202
+ under `extra.metrikStreamOpener`, with `amount == price` and `payTo ==
203
+ predict(6 terms)`. An upfront-only quote carries no `refundUrl`. The SDK buyer
204
+ (`payWithX402`, 0.10.0-rc.3) recomputes `payTo` OFFLINE from the registry's
205
+ factory and implementation, runs `validateUpfrontTerms`, requires `price -
206
+ upfrontAmount <= ratePerSecond × maxDuration`, and refuses an upfront part above
207
+ `maxUpfrontAmount` (default 0.10 USDC; `0` refuses any).
208
+
209
+ The settlement's `PAYMENT-RESPONSE.extra.metrikStreamOpener` is `{streamId
210
+ (null when upfront-only), opener, escrow, deposit (= price - upfrontAmount),
211
+ ratePerSecond, expiresAt (only with a stream), upfrontAmount, upfrontFee,
212
+ refundUrl (only with a stream)}`; `upfrontAmount` and `upfrontFee` are read from
213
+ the opener's `UpfrontCharged` event in the mined receipt.
214
+
215
+ ### What the facilitator checks for an upfront payment
216
+
217
+ Everything in the table above, plus: the factory is the upfront factory this
218
+ facilitator is bound to (anything not one of its configured factories is
219
+ `metrik_opener_factory_mismatch`); the six terms pass the factory's own
220
+ `validateTerms` rules and `price - upfrontAmount <= ratePerSecond ×
221
+ maxDuration` (`metrik_opener_invalid_terms`); a `termsVersion`, when present,
222
+ is `upfront-1`; `amount == price` (`metrik_opener_invalid_amount`); `payTo ==
223
+ upfrontFactory.predict(terms)` on-chain; and an `eth_call` of the 6-tuple
224
+ `deployAndSettle`. Price/upfront fields on a STREAM-factory quote are refused
225
+ (that `payTo` does not commit to them). `settle` reports success only when the
226
+ receipt shows exactly the signed split: `UpfrontCharged` for this payer, nonce,
227
+ operator and `upfrontAmount` (when non-zero), and — unless upfront-only — the
228
+ escrow's `StreamOpened` and the opener's `StreamOpenedFor` for a deposit of
229
+ exactly `price - upfrontAmount`; an upfront-only receipt must show
230
+ `UpfrontCharged` with `streamId 0` and no stream.
231
+
232
+ ### Running an upfront listing
233
+
234
+ On the gateway, set `METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT` (base units) next to
235
+ the usual `METRIK_GATEWAY_X402V2_*` variables; its presence selects the upfront
236
+ kind, and the factory defaults to the registry's upfront factory
237
+ (`METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS` overrides it;
238
+ `METRIK_GATEWAY_X402V2_FACTORY_ADDRESS`, which names the stream factory, is
239
+ refused alongside it). At startup the gateway validates the six terms, reads
240
+ the factory's `escrow()`, `usdc()` and `implementation()`, and requires its
241
+ on-chain `predict` to equal the offline CREATE2 derivation the SDK uses; the
242
+ facilitator's marker must advertise exactly this upfront factory in
243
+ `upfrontOpener`. Size `upfrontAmount` to at least the marginal cost of serving
244
+ one call; use `upfrontAmount == price` for calls with nothing ongoing to meter.
245
+ The facilitator settles the registry's upfront factory by default (see
246
+ `METRIK_OPENER_FACILITATOR_UPFRONT_FACTORY_ADDRESS`).
247
+
248
+ ## Getting unearned money back: the gasless refund relay
249
+
250
+ **There is no automatic refund.** Nothing pushes money back to a payer, and no
251
+ keeper runs in the background. Unearned money comes back only when someone
252
+ calls the relay endpoints below (or calls the opener directly). What the relay
253
+ removes is the need for ETH: a typical x402 payer holds only USDC, so the
254
+ facilitator's relayer pays the gas for the two transactions a refund needs.
255
+
256
+ The escrow's buyer of record is the opener, so a refund goes through it:
257
+
258
+ 1. **Close** (optional — only to stop before `expiresAt`). `opener.close` is
259
+ payer-only on-chain, because anyone else could cut a payer's service. The
260
+ relay submits `opener.closeWithSig` with the payer's EIP-712 signature.
261
+ 2. **Reclaim**, once the settlement window allows it: a checkpoint covering the
262
+ stop time (close, or `expiresAt`) is posted — the oracle posts every
263
+ ~15–22 s — or `escapeGraceSeconds` (900 s on the live escrow) have passed
264
+ since the stop. `opener.reclaim(streamId, cumulativeAmount, proof)` is
265
+ permissionless: the escrow fixes the amount (a Merkle proof against its
266
+ **latest** checkpoint root) and the opener fixes the destination
267
+ (`payerOf(streamId)`). The relay fetches the proof from the oracle itself.
268
+ 3. **Escape hatch**, only if no usable proof ever appears: after
269
+ `expiresAt + escapeGraceSeconds` the payer may sign `ReclaimUnverified` and
270
+ the relay submits `reclaimUnverifiedWithSig`. It returns `deposit -
271
+ claimedCumulative` and forfeits the seller's earned-but-unclaimed revenue, so
272
+ it is **never** relayed without the payer's signature; the unsigned
273
+ `reclaimUnverified` is payer-only on-chain and the relay never calls it.
274
+
275
+ The streamId and the refund URL arrive with the paid response:
276
+ `PAYMENT-RESPONSE.extra.metrikStreamOpener = {streamId, opener, escrow,
277
+ deposit, ratePerSecond, expiresAt, refundUrl}`.
278
+
279
+ **Upfront openers.** The relay serves the upfront factory's clones too. Which
280
+ factory's `isOpener` claims the stream's buyer, read on-chain, decides the
281
+ opener's kind and so the EIP-712 domain the payer signs under (`"Metrik
282
+ X402UpfrontStreamOpener"` instead of `"Metrik X402StreamOpener"`); the status
283
+ reports `openerKind`, `price`, `upfrontAmount` and `maxRefund` (the deposit, =
284
+ `price - upfrontAmount`) — the upfront part was paid at settlement and can
285
+ never come back. **Stream id 0** is an upfront-only payment: every route
286
+ answers `nothing-to-refund` (`404` for the status, `409` for the actions)
287
+ without a chain read, and no close or reclaim is ever submitted for it.
288
+
289
+ ### Endpoints (on the facilitator service; public, no bearer token)
290
+
291
+ | Method + path | Body | Does |
292
+ | -------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------- |
293
+ | `GET /streams/:streamId` | — | Status, and exactly what the payer can do next |
294
+ | `POST /streams/:streamId/close` | `{"deadline": "<unix s>", "signature": "0x…"}` | Relays `closeWithSig` (payer's EIP-712 `Close`) |
295
+ | `POST /streams/:streamId/reclaim` | none | Fetches the oracle proof, relays `reclaim` |
296
+ | `POST /streams/:streamId/reclaim-unverified` | `{"deadline": "<unix s>", "signature": "0x…"}` | Relays `reclaimUnverifiedWithSig` (payer's `ReclaimUnverified`) |
297
+
298
+ The payer signs, with the opener as the EIP-712 domain
299
+ (`{name: "Metrik X402StreamOpener", version: "1", chainId, verifyingContract:
300
+ <opener>}`):
143
301
 
144
- ---
302
+ ```
303
+ Close(bytes32 streamId,uint256 nonce,uint256 deadline)
304
+ ReclaimUnverified(bytes32 streamId,uint256 nonce,uint256 deadline)
305
+ ```
145
306
 
146
- ## Tier 2: the verified-delivery facilitator
147
-
148
- `POST /verify` does what any facilitator does — delegated **verbatim** to the
149
- real `x402/facilitator` `verify()`, so Metrik does not reimplement EIP-3009 —
150
- and then:
151
-
152
- 1. Resolve `PaymentRequirements.resource` to a Metrik listing, **by origin,
153
- against Metrik's own verified registry view**. Not a Metrik listing? Return
154
- the ordinary answer and apply no gate.
155
- 2. Check `payTo` equals the operator who **signed** that listing. If not:
156
- `isValid: false, invalidReason: "invalid_exact_evm_payload_recipient_mismatch"`.
157
- This is the one check here that protects the payer: without it, anyone could
158
- quote a Metrik-listed resource and redirect its payment to their own address.
159
- 3. Ask the oracle for that `serviceRef`'s current delivery verdict.
160
- Fresh failure -> `isValid: false`.
161
-
162
- `POST /settle` re-runs the entire gate before broadcasting. A facilitator must
163
- never settle something it would not verify, and `/settle` is reachable without
164
- `/verify` ever having been called.
165
-
166
- ### `/settle` spends the operator's money, and is fenced accordingly
167
-
168
- This is the part a security review caught, and it is worth stating plainly
169
- because the shape of the hole is not obvious.
170
-
171
- `PaymentRequirementsSchema` accepts **any** `asset`, **any** `payTo`,
172
- `maxAmountRequired: "0"`, and any valid-URL `resource`. The library's `verify`
173
- only checks the payload against those same caller-supplied requirements: the
174
- signature recovers to `authorization.from`, `to === payTo`, the window is valid,
175
- and `balanceOf(from) >= maxAmountRequired` **on the caller-supplied asset**. An
176
- attacker signing from their own key to their own address satisfies every one of
177
- those checks. `settle` then does
178
- `writeContract({ address: paymentRequirements.asset, ... })`.
179
-
180
- So an unfenced `/settle` bound to `0.0.0.0` is a permissionless _"make the
181
- relayer sign a call to a contract I chose, and pay the gas"_ endpoint. Deploy
182
- something whose `balanceOf` returns a huge number and whose
183
- `transferWithAuthorization` burns gas in a loop, sign an authorization over its
184
- domain from your own key to your own address, and POST it until the relayer is
185
- dry — at which point legitimate settlement stops too. No funds are stolen, but
186
- it is the operator's money and it induces the operator's key to sign
187
- transactions to contracts they never chose.
188
-
189
- Four controls, all enforced before the settler is reached:
190
-
191
- | Control | Env | Why |
192
- | ----------------------------------- | ------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
193
- | **Asset allowlist** | `METRIK_X402_SETTLEMENT_ASSETS` (default: the chain's USDC) | **The one that removes the arbitrary-contract-call primitive.** The relayer will only ever be asked to call an approved token. |
194
- | **Bearer token on `/settle`** | `METRIK_X402_FACILITATOR_TOKEN` (>= 32 chars) | `/settle` is not a public read. Constant-time compare; a missing configured token DENIES. `/verify` and `/supported` stay public. |
195
- | **Settlement scope** | `METRIK_X402_SETTLEMENT_SCOPE` (default `tracked-resources`) | The relayer pays gas only for resources that resolve to exactly one signed Metrik listing. |
196
- | **Network match + non-zero amount** | — | A zero-value settlement moves nothing and only burns gas; a foreign network is not ours to settle. |
197
-
198
- A settling facilitator **refuses to start** without a token, and the facilitator
199
- object refuses to be constructed with an empty asset allowlist. Both are startup
200
- failures rather than request-time ones, so a misconfiguration is loud.
201
-
202
- **The scope default is deliberately asymmetric with the delivery gate, and the
203
- asymmetry is the point.** An OPINION about a service costs nothing, so `/verify`
204
- stays universal and fails open on absent evidence. BROADCASTING costs the
205
- operator real gas, so `/settle` fails closed — including when the registry
206
- cannot be read, where `/verify` still answers and `/settle` does not. This is
207
- not a censorship lever: Metrik declining to spend _its own_ gas does not stop
208
- anyone being paid, because any other facilitator (or the payer's own relayer)
209
- can settle the same payment. Operators who want a general-purpose facilitator on
210
- a relayer they are content to fund for anyone's traffic set
211
- `METRIK_X402_SETTLEMENT_SCOPE=any-resource`; the asset allowlist still applies.
212
-
213
- `/verify` reports `metrik.settleable` so a resource server learns **before it
214
- does any work** whether this relayer would actually broadcast, instead of
215
- executing the request and then discovering it cannot be paid for.
216
-
217
- ### Origin resolution, and origin squatting
218
-
219
- A listing owns an ORIGIN (scheme + host + port); matching on origin means a
220
- seller does not have to re-register every route. An origin claimed by **two
221
- different services is unresolvable**, not first-come-first-served. Picking the
222
- first claimant made the outcome depend on the registry's read order, so a second
223
- signed listing claiming a victim's origin could take over the victim's gate and
224
- permanently refuse their payments. That is not exploitable while listing
225
- insertion requires an operator-held key — but marking the origin unresolvable
226
- costs nothing and removes the primitive before registration ever opens. A single
227
- listing naming the same origin through several fields (`publicUrl`,
228
- `interface.baseUrl`, `callerAuth.accessUrl`) is not a conflict.
229
-
230
- ### Only gate what Metrik actually tracks
231
-
232
- A resource that maps to no Metrik listing gets the ordinary facilitator answer,
233
- unchanged. Refusing payments for services Metrik has no opinion about would not
234
- be caution — it would be a broken facilitator.
235
-
236
- ### Which `invalidReason`
237
-
238
- `invalidReason` is a **closed enum**. There is no member for "this service is not
239
- delivering", and inventing one breaks every conformant client that parses the
240
- field. The choice is between imperfect members:
241
-
242
- | Candidate | Why not |
243
- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
244
- | `insufficient_funds`, `invalid_payload`, `invalid_exact_evm_payload_signature` | Active lies. They send a developer to debug a wallet that is working correctly. |
245
- | `unexpected_verify_error` | Also false. This refusal is deliberate and expected, not an error. |
246
- | **`invalid_payment_requirements`** | **Chosen.** The only member pointing at the _requirements_ side of the exchange — which is exactly where the problem is: Metrik will not settle a payment quoted for _this resource_ to _this payee_ right now. |
247
-
248
- The real explanation travels in a `metrik` object alongside the standard fields:
307
+ `nonce` is the payer's current `opener.nonces(payer)`; `deadline` must be at
308
+ least 60 s after the current chain time. `GET /streams/:streamId` returns the
309
+ ready-to-sign typed data (with the current nonce) under `next.typedData`, so a
310
+ client signs it as-is after choosing a deadline. An optional `"nonce"` in the
311
+ body turns a stale nonce into a clear `409 nonce-mismatch`.
312
+
313
+ `GET /streams/:streamId` answers `200` with:
249
314
 
250
315
  ```json
251
316
  {
252
- "isValid": false,
253
- "invalidReason": "invalid_payment_requirements",
254
- "payer": "0x...",
255
- "metrik": {
256
- "gate": "refused",
257
- "serviceRef": "0x...",
258
- "deliveryVerdict": "failing",
259
- "deliveryReason": "fresh-failing",
260
- "verdictAgeSeconds": 18,
261
- "freshnessWindowSeconds": 90,
262
- "detail": "Metrik's oracle reports this service as FAILING its delivery checks ..."
263
- }
317
+ "streamId": "0x…",
318
+ "chainId": 84532,
319
+ "escrow": "0x…",
320
+ "factory": "0x…",
321
+ "opener": "0x…",
322
+ "payer": "0x…",
323
+ "operator": "0x…",
324
+ "serviceRef": "0x…",
325
+ "deposit": "60000",
326
+ "ratePerSecond": "1000",
327
+ "claimedCumulative": "0",
328
+ "openedAt": 1790452888,
329
+ "expiresAt": 1790452948,
330
+ "closedAt": 1790452902,
331
+ "status": "closed",
332
+ "reclaimed": false,
333
+ "chainTime": 1790452910,
334
+ "settlement": {
335
+ "stopAt": 1790452902,
336
+ "latestCheckpointCheckedAt": 1790452904,
337
+ "checkpointCoversStop": true,
338
+ "fallbackAt": 1790453802,
339
+ "escapeHatchAt": 1790453848
340
+ },
341
+ "oracle": {
342
+ "cumulativeAmount": "14000",
343
+ "checkpointId": "61222",
344
+ "checkedAt": 1790452904,
345
+ "root": "0x…"
346
+ },
347
+ "reclaimable": "46000",
348
+ "payerNonce": "1",
349
+ "next": {
350
+ "action": "reclaim",
351
+ "method": "POST",
352
+ "path": "/streams/0x…/reclaim",
353
+ "message": "…"
354
+ },
355
+ "refunds": "There is no automatic refund. …"
264
356
  }
265
357
  ```
266
358
 
267
- `VerifyResponseSchema` is a `strip` (not `strict`) Zod object, so this is
268
- spec-tolerated: a conformant client parses the response successfully and simply
269
- drops `metrik`. A client that wants the reason reads it off the raw body. There
270
- is a test asserting exactly that.
271
-
272
- ### Staleness: a 90-second window, and why
273
-
274
- A verdict from ten minutes ago is not evidence that a service is alive now, so
275
- freshness is the crux. The oracle records a status per active stream on its
276
- checkpoint cadence of **~15–22 s**. The window has to be wide enough that
277
- ordinary jitter does not flap the gate and narrow enough that "verified" still
278
- means "now":
279
-
280
- - three worst-case probe intervals (3 x 22 s = **66 s**) absorbs two consecutive
281
- missed or slow ticks, which is operational noise rather than a dead service;
282
- - plus the oracle's record-and-serve latency and the facilitator's fetch round
283
- trip, rounded up: **90 s**.
284
-
285
- Tighter (say 30 s) and one slow tick turns a healthy service indeterminate.
286
- Looser and a service that died four minutes ago still reads `delivering`, which
287
- is precisely the claim this gate exists to avoid making. Note the oracle's own
288
- TTL is **300 s**, so this window is strictly stricter: a verdict aged 90–300 s
289
- reads `delivering` at the source and `indeterminate` here.
290
-
291
- Staleness is applied **symmetrically**. A stale _failure_ is not evidence either,
292
- so one bad probe cannot silently refuse forever.
293
-
294
- ### No fresh evidence: allow, by default
295
-
296
- There are three distinct ways to have no opinion, and they are never collapsed
297
- into "failing":
298
-
299
- | Situation | `metrik.gate` |
300
- | ------------------------------------------------------------------------- | -------------------------------------- |
301
- | Service has no row at all (no active stream), or oracle says `unverified` | `indeterminate` |
302
- | Verdict older than the freshness window | `indeterminate` (`verdict-stale`) |
303
- | Oracle unreachable or malformed | `indeterminate` (`oracle-unreachable`) |
304
- | Metrik cannot read its own registry | `registry-unavailable` |
305
-
306
- All of them **allow** by default. The standing Metrik rule is buyer-favouring
307
- and still is — but that rule governs the case where Metrik **holds** the buyer's
308
- money and must decide whether to release it. Here Metrik holds nothing, so
309
- refusing does not protect anyone's funds; it only denies a service the buyer
310
- asked to buy, on a rail that works fine without Metrik. Four reasons:
311
-
312
- 1. **Absent evidence is the normal case.** The oracle's `/services` projection is
313
- built from ACTIVE STREAMS. A service with no live Metrik stream has no row at
314
- all — on the live oracle today, `GET /services` returns `{"services":[]}`.
315
- Failing closed would refuse **100% of payments**. That is a broken
316
- facilitator, not a careful one.
317
- 2. **Fail-closed is a censorship lever.** If "oracle down" meant "no payments",
318
- anyone who could degrade Metrik's oracle could block payments to a competitor
319
- listed on it. `allow` removes that lever.
320
- 3. **The harms are asymmetric.** A wrong refusal denies the service outright. A
321
- wrong allowance passes one `exact` payment, at a price the buyer already
322
- agreed to, worth one call.
323
- 4. **A pass never claims anything.** `isValid: true` with `gate: "indeterminate"`
324
- says "no fresh evidence", in the response, in words.
325
-
326
- A **fresh** failure is refused under every policy.
327
-
328
- Set `METRIK_X402_INDETERMINATE_POLICY=deny` to require positive fresh evidence
329
- instead. That is a real, supported, tested choice — it is just not a safe
330
- default.
359
+ `closedAt` is `null` while active; `oracle` and `reclaimable` are `null` when
360
+ no proof that matches the escrow's on-chain latest root is available (with an
361
+ `oracleNote`). `next.action` is one of `close` (with `typedData` and the body
362
+ shape), `wait` (with `retryAt` / `retryAfterSeconds`), `reclaim`,
363
+ `reclaim-unverified` (with `typedData`), or `none`.
331
364
 
332
- ---
365
+ A successful relayed transaction answers `200` only from its **mined** receipt:
333
366
 
334
- ## Running it
335
-
336
- ```bash
337
- # The facilitator
338
- METRIK_X402_RPC_URL=https://sepolia.base.org \
339
- METRIK_X402_RELAYER_PRIVATE_KEY=0x... \
340
- npx metrik-x402-facilitator
341
-
342
- # A seller's gateway, quoting 0.001 USDC per call through it
343
- METRIK_GATEWAY_X402_PRICE_BASE_UNITS=1000 \
344
- METRIK_GATEWAY_X402_FACILITATOR_URL=https://facilitator.example \
345
- METRIK_GATEWAY_X402_RESOURCE_BASE_URL=https://svc.example \
346
- npx metrik-gateway
367
+ ```json
368
+ { "ok": true, "action": "close", "streamId": "0x…", "opener": "0x…",
369
+ "payer": "0x…", "transaction": "0x…", "message": "…", "next": { "path": "/streams/0x…/reclaim" } }
370
+ { "ok": true, "action": "reclaim", "streamId": "0x…", "opener": "0x…",
371
+ "payer": "0x…", "amount": "46000", "entitlement": "14000", "transaction": "0x…", "message": "…" }
347
372
  ```
348
373
 
349
- The facilitator **refuses to start** without `METRIK_X402_RELAYER_PRIVATE_KEY`
350
- unless `METRIK_X402_ALLOW_VERIFY_ONLY=true` is set explicitly. A facilitator that
351
- accepts `/settle` and quietly never broadcasts is worse than one that is absent.
352
-
353
- ### Settlement is built but has never broadcast anything
374
+ (`action` is `reclaim-unverified` for the escape hatch.) Every refusal is
375
+ `{"ok": false, "error": "<code>", "message": "…", …}`, with `Retry-After`
376
+ where a retry makes sense:
377
+
378
+ | Status | `error` | Meaning |
379
+ | ------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
380
+ | 400 | `invalid-stream-id`, `invalid-request`, `invalid-json` | Malformed input |
381
+ | 403 | `invalid-signature`, `signature-expired` | Not the payer's EIP-712 signature for this opener, stream and nonce; or deadline too near |
382
+ | 404 | `not-found` | Unknown stream, or its buyer is not an opener of the configured factory |
383
+ | 409 | `already-closed`, `stream-expired`, `already-reclaimed`, `nothing-to-reclaim`, `nonce-mismatch` | Would have no effect; nothing sent |
384
+ | 409 | `not-reclaimable-yet`, `settlement-window-open`, `proof-unavailable`, `escape-hatch-not-ready` | Not yet; `retryAt` (unix s) says when at the latest |
385
+ | 409 | `simulation-failed`, `in-progress`, `blocked` | The simulation reverted; a relayed tx is in flight; an earlier tx had an unexpected outcome |
386
+ | 202 | `pending` | Sent, not mined yet (`transaction`); repeat the same request — it is never re-sent |
387
+ | 429 | `rate-limited` | Per-IP, per-stream or global budget exhausted |
388
+ | 502 | `transaction-reverted`, `unexpected-receipt`, `submission-failed` | Never a success without the expected mined receipt |
389
+ | 503 | `chain-unavailable`, `oracle-unavailable`, `opener-domain-mismatch` | Fail closed |
390
+
391
+ ### Bounded against gas abuse
392
+
393
+ - **Opener streams only.** `factory.isOpener(stream.buyer)` is read on-chain
394
+ first; every other stream is `404` before any signature check, oracle call or
395
+ simulation.
396
+ - **No effect, no transaction.** Every precondition the contract checks is read
397
+ first (status, expiry, settlement window, `reclaimed`, a non-zero refund). A
398
+ reclaim's simulation must return exactly the non-zero `reclaimableFor`
399
+ amount. Nothing is broadcast without a successful simulation.
400
+ - **Proofs are checked, not trusted.** The oracle's proof must be for this
401
+ stream, verify under the escrow's leaf
402
+ `keccak256(bytes.concat(keccak256(abi.encode(streamId, cumulative))))`, and
403
+ its root must equal the escrow's on-chain `latestCheckpointRoot`. A tampered
404
+ proof (for example one claiming less delivery) is refused without a
405
+ transaction; the contract would refuse it anyway.
406
+ - **At most one successful close and one successful reclaim per stream** from
407
+ a facilitator instance (the proof and escape-hatch paths share the reclaim
408
+ slot, as the escrow allows one). Concurrent duplicates are refused
409
+ synchronously; a repeat after success is refused without any transaction.
410
+ - **Read fan-out is bounded.** Every `/streams` request (read or write) counts
411
+ against a per-client-IP limit AND an IP-independent global limit (default
412
+ 120/min, `429` + `Retry-After`). A stream's status and its oracle proof are
413
+ reused for a few seconds (default 5 s), so repeated requests for one stream
414
+ do not re-hit the RPC or the oracle; the cache for a stream is dropped as
415
+ soon as a relayed transaction for it is mined, and a broadcast is still
416
+ decided by a fresh simulation.
417
+ - **No internal errors leak.** RPC, oracle and simulation failures are
418
+ answered with a generic message and an error code (`chain-unavailable`,
419
+ `oracle-unavailable`, `simulation-failed`, `submission-failed`); the detail
420
+ is logged server-side with URL paths redacted, never the relayer key. Only
421
+ a decoded contract custom-error name (for example `StreamClosedAlready`) is
422
+ returned.
423
+ - **Budgets.** Per client IP (all `/streams` routes), per stream (relayed
424
+ submission attempts) and globally per minute. Refused requests — a forged
425
+ signature, a request with no effect — do not consume a stream's budget, so
426
+ spamming cannot lock a payer out.
427
+ - **Receipts.** Success only from a mined receipt with the escrow's
428
+ `StreamClosed` / `BuyerReclaimed` and the opener's `StreamCloseRequested` /
429
+ `ReclaimForwarded` for exactly this stream, payer and amount.
430
+ - **One relayer queue.** Relay submissions and `deployAndSettle` go through the
431
+ same serialized queue, so they never race for the relayer's nonce.
432
+
433
+ ## Running the facilitator
354
434
 
355
- `POST /settle` is implemented end to end and delegates to the real `x402`
356
- `settle()`, which submits the EIP-3009 `transferWithAuthorization`. **It has
357
- never been run against a live chain from this repo**, because that needs a
358
- relayer wallet funded with gas, which does not exist yet. What is missing is a
359
- key and gas, not code. Until then:
360
-
361
- - no relayer key -> the server will not start (or runs verify-only and returns
362
- `503` on `/settle`);
363
- - every test settles through a recording fixture that broadcasts nothing.
364
-
365
- Nothing in this repo has ever sent an x402 settlement transaction.
366
-
367
- ---
435
+ ```bash
436
+ METRIK_OPENER_FACILITATOR_ENABLED=1 \
437
+ METRIK_OPENER_FACILITATOR_RPC_URL=https://sepolia.base.org \
438
+ METRIK_OPENER_FACILITATOR_RELAYER_PRIVATE_KEY=<from your secret store> \
439
+ METRIK_OPENER_FACILITATOR_AUTH_TOKEN=<at least 32 random characters> \
440
+ npx metrik-x402-opener-facilitator
441
+ ```
368
442
 
369
- ## What x402 actually is, as shipped
370
-
371
- All of the following was verified against the published packages and the
372
- specification repository (see [Evidence](#evidence) for how, and when).
373
-
374
- | Thing | Reality |
375
- | ---------------------------------------- | --------------------------------------------------------------------------------------------------- |
376
- | Spec repository | [`x402-foundation/x402`](https://github.com/x402-foundation/x402) |
377
- | Published client/server packages | `x402`, `x402-fetch`, `x402-axios`, `x402-express`, `x402-hono`, `x402-next` |
378
- | Version of `x402` on npm | **1.2.0**, Apache-2.0, no `engines` constraint |
379
- | Protocol versions the package implements | **`x402Version: 1` only** (`x402Versions === [1]`) |
380
- | Payment schemes the package implements | **`exact` only** (`schemes === ["exact"]`) |
381
- | Schemes that exist _in the spec_ | `exact`, `upto`, `batch-settlement`, `auth-capture` |
382
- | Transport (v1, shipped) | HTTP `402` + `X-PAYMENT` request header + `X-PAYMENT-RESPONSE` response header |
383
- | Transport (v2, spec-only) | `PAYMENT-REQUIRED` / `PAYMENT-SIGNATURE` / `PAYMENT-RESPONSE` headers — **renamed**, no `X-` prefix |
384
- | Facilitator interface | `POST /verify`, `POST /settle`, `GET /supported` |
385
-
386
- Two gaps matter enormously and are easy to miss:
387
-
388
- 1. **The spec is ahead of the shipped code.** `auth-capture` and `upto` are
389
- written up under `specs/schemes/` but are _not_ in `x402@1.2.0`. The package's
390
- `PaymentRequirementsSchema` parses `scheme` as a closed Zod enum containing
391
- exactly one value, `"exact"`. A `PaymentRequirements` naming any other scheme —
392
- including `auth-capture` and `upto` — is **rejected by the real library**.
393
- 2. **x402 v2 is specified but not shipped.** `x402-specification-v2.md` defines
394
- `x402Version: 2` with CAIP-2 network identifiers (`eip155:8453`) and an
395
- explicitly extensible `scheme` _string_. `x402@1.2.0` implements v1, whose
396
- `network` is a closed enum of bare names (`base-sepolia`, `base`, …). So the
397
- extensibility that would make room for a Metrik scheme exists on paper, not in
398
- the library your users will install.
399
-
400
- ### The `exact` scheme, concretely
443
+ | Variable | Default | Meaning |
444
+ | -------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------ |
445
+ | `METRIK_OPENER_FACILITATOR_ENABLED` | off | Must be truthy or the service refuses to start. |
446
+ | `METRIK_OPENER_FACILITATOR_RPC_URL` | required | RPC for reads, simulation and the settlement transaction. |
447
+ | `METRIK_OPENER_FACILITATOR_RELAYER_PRIVATE_KEY` | required | Pays gas for `deployAndSettle`. Never holds buyer funds; never logged. |
448
+ | `METRIK_OPENER_FACILITATOR_AUTH_TOKEN` | required (≥ 32 chars) | Bearer token required on `/settle`. |
449
+ | `METRIK_OPENER_FACILITATOR_FACTORY_ADDRESS` | the Base Sepolia factory | Required on any other chain. |
450
+ | `METRIK_OPENER_FACILITATOR_UPFRONT_FACTORY_ADDRESS` | the registry's, for the escrow | Upfront opener factory settled too (same escrow and token); `off` disables it. |
451
+ | `METRIK_OPENER_FACILITATOR_ESCROW_ADDRESS` | live Base Sepolia `StreamEscrowV2` | Must equal `factory.escrow()`. |
452
+ | `METRIK_OPENER_FACILITATOR_NETWORK` | `eip155:84532` | Must match the RPC's chain id. |
453
+ | `METRIK_OPENER_FACILITATOR_ASSETS` | Circle test USDC | Comma-separated allowlist; must contain `factory.usdc()`. |
454
+ | `METRIK_OPENER_FACILITATOR_REGISTRY_URL` | `https://oracle.metrik.live/listings` | Where operator-signed listings are read. |
455
+ | `METRIK_OPENER_FACILITATOR_LISTING_CACHE_SECONDS` | `60` | Reuse of a successful registry read (failures are never cached). |
456
+ | `METRIK_OPENER_FACILITATOR_RECEIPT_TIMEOUT_SECONDS` | `60` | Receipt wait before `settlement_pending`. |
457
+ | `METRIK_OPENER_FACILITATOR_REQUIRE_AUTH_ON_VERIFY` | **on** | Require the token on `/verify` too; `0` opts out explicitly. |
458
+ | `METRIK_OPENER_FACILITATOR_MIN_AMOUNT` | `10000` (0.01 USDC) | Smallest accepted payment; advertised in the marker. |
459
+ | `METRIK_OPENER_FACILITATOR_SETTLE_RATE_LIMIT_PER_MINUTE` | `60` | Authorized `/settle` calls per token per minute; beyond it `429`. |
460
+ | `METRIK_OPENER_FACILITATOR_HOST` / `_PORT` (or `PORT`) | `127.0.0.1` / `8788` | Bind address. Put TLS in front of it. |
461
+ | `METRIK_OPENER_FACILITATOR_REFUND_RELAY` | **on** | The gasless refund relay; `0` switches it off (a gateway then refuses to start). |
462
+ | `METRIK_OPENER_FACILITATOR_ORACLE_URL` | `https://oracle.metrik.live` | Serves `GET /checkpoint/proof?streamId=`. Required off Base Sepolia. |
463
+ | `METRIK_OPENER_FACILITATOR_PUBLIC_URL` | unset | Public origin, for absolute links (`next.url`) in relay responses. |
464
+ | `METRIK_OPENER_FACILITATOR_REFUND_RATE_LIMIT_PER_IP_PER_MINUTE` | `30` | `/streams/*` requests per client IP per minute. |
465
+ | `METRIK_OPENER_FACILITATOR_REFUND_GLOBAL_RATE_LIMIT_PER_MINUTE` | `120` | `/streams/*` requests per minute from all clients together (IP-independent). |
466
+ | `METRIK_OPENER_FACILITATOR_REFUND_READ_CACHE_SECONDS` | `5` | Reuse of one stream's status and oracle proof; `0` disables. |
467
+ | `METRIK_OPENER_FACILITATOR_REFUND_SUBMISSIONS_PER_STREAM_PER_MINUTE` | `3` | Relayed transactions per stream per minute. |
468
+ | `METRIK_OPENER_FACILITATOR_REFUND_SUBMISSIONS_PER_MINUTE` | `30` | Relayed refund transactions per minute, all streams. |
469
+ | `METRIK_OPENER_FACILITATOR_TRUST_PROXY` | off | Key the per-IP limit on the right-most `X-Forwarded-For` hop. See the warning below. |
470
+
471
+ > **`TRUST_PROXY=1` is only safe when the service is bound to loopback
472
+ > (`METRIK_OPENER_FACILITATOR_HOST=127.0.0.1`, the default) behind a reverse
473
+ > proxy that overwrites `X-Forwarded-For` with the address it actually saw
474
+ > (Caddy's `reverse_proxy` does).** Never enable it when the port is reachable
475
+ > directly: any client could then send its own `X-Forwarded-For` and get a
476
+ > fresh per-IP budget on every request. The global cap still applies either
477
+ > way.
478
+
479
+ At startup it proves the wiring (RPC chain id, factory code, `factory.escrow()`,
480
+ `factory.usdc()` allowlisted; for the relay, `factory.escrow()` and the escrow's
481
+ `escapeGraceSeconds`) and refuses to start otherwise. Endpoints:
482
+ `GET /supported`, `POST /verify`, `POST /settle` (bearer), `GET /health`, and
483
+ the refund relay's `GET /streams/:streamId` and
484
+ `POST /streams/:streamId/{close,reclaim,reclaim-unverified}` (public).
485
+
486
+ ## Running the gateway in x402 mode
487
+
488
+ The gateway's normal variables (`METRIK_GATEWAY_ESCROW_ADDRESS`,
489
+ `METRIK_GATEWAY_RPC_URL`, `METRIK_GATEWAY_SERVICE_REF`,
490
+ `METRIK_GATEWAY_UPSTREAM_URL`, …) are unchanged. The listing is the primary
491
+ `METRIK_GATEWAY_SERVICE_REF`. Add:
492
+
493
+ | Variable | Default | Meaning |
494
+ | ---------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
495
+ | `METRIK_GATEWAY_X402V2_OPENER` | off | Truthy to enable the mode. |
496
+ | `METRIK_GATEWAY_X402V2_FACILITATOR_URL` | required | The Metrik opener facilitator. |
497
+ | `METRIK_GATEWAY_X402V2_FACILITATOR_TOKEN` | required | Its `/settle` bearer token. |
498
+ | `METRIK_GATEWAY_X402V2_OPERATOR` | required | Must be the operator that signed the listing. |
499
+ | `METRIK_GATEWAY_X402V2_RATE_PER_SECOND` | required | USDC base units per second. |
500
+ | `METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS` | required | Stream lifetime. |
501
+ | `METRIK_GATEWAY_X402V2_PRICE` | required | Base units per paid request (= deposit); `0 < price <= rate × maxDuration`. |
502
+ | `METRIK_GATEWAY_X402V2_FACTORY_ADDRESS` | the Base Sepolia factory | Required on any other chain. |
503
+ | `METRIK_GATEWAY_X402V2_UPFRONT_AMOUNT` | unset (stream kind) | Set = the upfront kind: the non-refundable per-call part, base units, `<= PRICE`; `PRICE - it <= rate × maxDuration`. |
504
+ | `METRIK_GATEWAY_X402V2_UPFRONT_FACTORY_ADDRESS` | the registry's upfront factory | Only with `_UPFRONT_AMOUNT`. |
505
+ | `METRIK_GATEWAY_X402V2_NETWORK` | `eip155:<METRIK_GATEWAY_CHAIN_ID>` | Must match the chain id. |
506
+ | `METRIK_GATEWAY_X402V2_ASSET` / `_ASSET_NAME` / `_ASSET_VERSION` | Circle test USDC / `USDC` / `2` | Token and its EIP-712 domain. |
507
+ | `METRIK_GATEWAY_X402V2_MAX_TIMEOUT_SECONDS` | `300` | Authorization validity the client signs for. |
508
+ | `METRIK_GATEWAY_X402V2_FACILITATOR_TIMEOUT_SECONDS` | `120` | Per-request facilitator timeout; must exceed its receipt wait by 30 s. |
509
+ | `METRIK_GATEWAY_X402V2_RESOURCE_BASE_URL` | derived from `Host` | Public origin used in the 402 `resource.url`. |
510
+ | `METRIK_GATEWAY_X402V2_REGISTRY_URL` | the public registry | Where the listing is proven at startup. |
511
+ | `METRIK_GATEWAY_X402V2_REFUND_BASE_URL` | the facilitator URL | Public base of the refund relay advertised in the 402. If different, its `/supported` must show the same marker with `refundRelay: true`. |
512
+ | `METRIK_GATEWAY_X402V2_DOCS_URL` | this document on GitHub | Docs link in the 402. |
513
+ | `METRIK_GATEWAY_X402V2_ASSET_DECIMALS` | `6` | Decimals for the amounts in the 402 description. |
514
+ | `METRIK_GATEWAY_X402V2_DESCRIPTION` | unset | One sentence on what the service does, prefixed to the fixed disclosure. |
515
+ | `METRIK_GATEWAY_X402V2_SCHEMA` | unset | JSON `{info: {input, output}, schema}`, declared as `extensions.bazaar` (declared only, never registered). |
516
+
517
+ These names deliberately differ from the removed v1 mode's variables, so a
518
+ leftover v1 environment can never switch a payment mode on.
519
+
520
+ At startup the gateway validates the terms and price, proves the listing is
521
+ operator-signed, checks `factory.escrow()` and `factory.usdc()`, computes
522
+ `payTo = factory.predict(terms)`, and checks the facilitator's marker —
523
+ including `refundRelay: true`, because the 402 promises a refund path without
524
+ ETH — plus the marker at a separate refund base URL; any failure stops it from
525
+ starting. The advertised window is `METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS`
526
+ (for example `60`, with `RATE_PER_SECOND=1000` and `PRICE=60000`).
527
+
528
+ ### What the 402 says
529
+
530
+ The resource `description` states plainly what one payment buys; an operator
531
+ `DESCRIPTION` can only be prefixed to it. With a 60 s window at 1000 base
532
+ units/s and a price of 60000:
533
+
534
+ > Pays for one call. Your payment of 0.06 USDC funds a Metrik escrow stream of
535
+ > 60 seconds at 0.001 USDC/s; the seller earns only for seconds Metrik's
536
+ > verifier confirms the service is delivering, and anything unearned is
537
+ > refundable to you (no ETH needed) via
538
+ > https://&lt;facilitator&gt;/streams/{streamId}. Refunds are not automatic: close
539
+ > the stream and reclaim there, using the streamId from the PAYMENT-RESPONSE.
540
+ > Docs: &lt;docs URL&gt;
541
+
542
+ When the price is below `rate × window`, it adds "(the seller can earn at most
543
+ &lt;price&gt;)". An upfront listing (price 0.06, upfront 0.015) states the split:
544
+
545
+ > Pays for one call: 0.06 USDC in total. 0.015 USDC is a NON-REFUNDABLE per-call
546
+ > charge, paid to the seller at settlement whatever is delivered. The remaining
547
+ > 0.045 USDC is metered and refundable: it funds a Metrik escrow stream of …;
548
+ > the seller earns it only for seconds Metrik's verifier confirms the service is
549
+ > delivering, and any unearned part of it (at most 0.045 USDC) is refundable to
550
+ > you (no ETH needed) via …. Refunds are not automatic: …
551
+
552
+ and an upfront-only one says the whole price is charged in full at settlement,
553
+ is not refundable, and that nothing is metered or refundable. `extra.metrikStreamOpener` also carries `refundUrl` (the
554
+ template) and `docsUrl`; the facilitator accepts exactly these two
555
+ informational http(s) links on top of the terms, never uses them for
556
+ settlement, and still refuses any other key. "Verified" here means verified
557
+ delivery, not verified output.
558
+
559
+ | Request carries | Result |
560
+ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- |
561
+ | nothing | `402`, `PAYMENT-REQUIRED` quoting the opener `payTo` and the price |
562
+ | `PAYMENT-SIGNATURE` | verified → upstream → settled if upstream `< 400` → `200` + body + `PAYMENT-RESPONSE` |
563
+ | `PAYMENT-SIGNATURE`, upstream `>= 400` | upstream response, nothing settled |
564
+ | `PAYMENT-SIGNATURE`, settle fails | `402` + `PAYMENT-RESPONSE`, no upstream body |
565
+ | `x-metrik-capability` | the capability path, unchanged |
566
+ | capability **and** `PAYMENT-SIGNATURE` | `400 ambiguous-capability-and-payment` |
567
+ | probe capability **and** `PAYMENT-SIGNATURE` | `400 ambiguous-probe-mode` |
568
+ | any request while the facilitator marker cannot be proven | `503 x402-facilitator-unavailable` |
569
+ | a `PAYMENT-SIGNATURE` already in flight or already settled | `409 x402-payment-already-used`, no upstream call |
570
+
571
+ With `METRIK_GATEWAY_X402V2_OPENER` unset the gateway is byte for byte the
572
+ capability-only gateway (`test/gateway-http-baseline.test.ts`), and the
573
+ `@x402/*` packages are never loaded.
574
+
575
+ ## Paying as a buyer: `metrik pay`, MCP `x402_pay`, `@absol-labs/agent/x402-buyer`
576
+
577
+ The buyer side is the SDK's `payWithX402` (`@absol-labs/sdk/x402`) with every SDK
578
+ protection intact, wrapped by `payX402WithMandate` (`@absol-labs/agent/x402-buyer`)
579
+ so the package's golden rule holds: **no spend without a mandate**. A Metrik x402
580
+ payment opens a stream, so it is checked as the stream open it is:
581
+
582
+ | Mandate cap | x402 quote field |
583
+ | ---------------------- | -------------------------------------- |
584
+ | `maxPerStreamUsdc` | `amount` (the whole price of the call) |
585
+ | `maxTotalUsdc` | spent so far + `amount` |
586
+ | `maxRatePerSecondUsdc` | `ratePerSecond` |
587
+ | `maxDurationSeconds` | `maxDuration` |
588
+ | `allowedOperators` | `operator` |
589
+
590
+ The mandate's signature, expiry, revocation, owner (= the payer) and chain are
591
+ verified before the seller is contacted. The per-quote check runs in the SDK's
592
+ `onQuote` hook — after the SDK has proven `payTo == factory.predict(terms)` on
593
+ chain and before anything is signed — and the caps are also passed to the SDK as
594
+ `maxAmount` / `maxRatePerSecond`. A quote without opener terms is refused
595
+ (`X402MandateDeniedError` reason `not-an-opener-quote`).
596
+
597
+ For an upfront quote the mandate counts the WHOLE price, upfront part included,
598
+ as spend (`budgetUsdc = price`); that part is non-recoverable and nothing a
599
+ refund returns is credited back. Its size is also capped separately before
600
+ signing by `maxUpfrontAmount` (`metrik pay --max-upfront`, MCP
601
+ `maxUpfrontAmountUsdc`; default 0.10 USDC, `0` refuses any upfront charge). The
602
+ split is shown before signing and in the result. An upfront-only payment
603
+ (`streamId: null`) is final: `metrik pay` prints no refund or reclaim command,
604
+ `metrik refund 0x00…00` and the MCP refund tools answer "nothing to refund"
605
+ (exit 0 / not an error), and the MCP server records no stream for it — only a
606
+ non-stream charge that still counts against the mandate.
401
607
 
402
- `exact` on EVM is an [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009)
403
- `transferWithAuthorization`: the client signs `{from, to, value, validAfter,
404
- validBefore, nonce}`, and the facilitator submits exactly that one transfer.
608
+ ```bash
609
+ metrik pay https://x402.137.23.50.249.sslip.io/transform --json '{"text":"# hi"}' --max-amount 0.1
610
+ metrik refund <streamId> --refund-url <url printed above> --close --reclaim
611
+ ```
405
612
 
406
- That is the whole scheme. One fixed amount. One transfer. Straight to the payee.
407
- **No escrow, no hold, no conditional release, no partial capture, no refund, no
408
- reclaim.**
613
+ The MCP server offers the same as `x402_pay`, `refund_status` and `refund`
614
+ (see [mcp-reference.md](./mcp-reference.md)); `x402_pay` shares the server
615
+ mandate and cumulative spend with `hire_verified_service`. Refunds are still
616
+ **requested, never pushed**: `metrik refund` / `refund` call the relay; nothing
617
+ calls it for you.
618
+
619
+ The facilitator's `PAYMENT-RESPONSE` `extra.metrikStreamOpener.expiresAt` is a
620
+ JSON **number** (it was a decimal string before metrik-agent#121, which the SDK's
621
+ strict parser rejects after settlement — a facilitator deployed before that fix
622
+ must be redeployed).
623
+
624
+ ## Network configuration
625
+
626
+ Network, USDC (and its EIP-712 domain), escrow, opener factory and oracle all
627
+ default from the `@absol-labs/sdk` network registry entry for the configured
628
+ CAIP-2 network (`METRIK_OPENER_FACILITATOR_NETWORK`, `METRIK_GATEWAY_CHAIN_ID` /
629
+ `METRIK_GATEWAY_X402V2_NETWORK`); every one has an env override, and on a network
630
+ the registry does not know (or one with no opener factory) each must be given
631
+ explicitly. The refund relay computes the no-proof escape hatch per escrow
632
+ generation (`escrow.generation`): V2 `expiresAt + escapeGraceSeconds`, V3
633
+ `min(expiresAt, closedAt) + escapeGraceSeconds` read from the escrow's
634
+ `escapeHatchReadyAt` view.
635
+
636
+ ## Programmatic use
637
+
638
+ Everything lives under the `@absol-labs/agent/x402-opener` subpath (the root
639
+ barrel does not load `@x402/*`): `MetrikOpenerExactScheme`,
640
+ `buildOpenerFacilitator`, `createOpenerFacilitatorServer`, `OpenerRefundRelay`,
641
+ `openerActionTypedData`, `checkpointLeaf`, `RelayerQueue`,
642
+ `RegistryListingResolver`, `readOpenerFacilitatorMarker`, the vendored
643
+ `x402StreamOpenerFactoryAbi` / `x402StreamOpenerAbi`, and the terms helpers.
644
+ The gateway mode is `buildGatewayX402OpenerMode` in
645
+ `@absol-labs/agent/gateway/x402-opener-mode`, passed to
646
+ `createCallerAuthGatewayServer({ x402Opener })`.
647
+
648
+ ## Honest limits
649
+
650
+ - **Refunds are requested, never pushed.** There is no automatic refund: the
651
+ relay acts only when an endpoint is called, and a payer who never calls it
652
+ (or never has anyone call `reclaim` for them) gets nothing back. `metrik
653
+ refund` and the MCP `refund` tool call it on request; `metrik close` /
654
+ `metrik reclaim` also route an opener stream through the opener directly.
655
+ - **The relay depends on the oracle for proofs.** If the oracle's latest
656
+ checkpoint never includes the stream, the proof-based reclaim cannot run and
657
+ the payer-signed escape hatch (after `expiresAt + escapeGraceSeconds`) is the
658
+ way out. The relay ledgers are in-memory and per instance, like the
659
+ settlement ledger; the on-chain contracts still allow only one close and one
660
+ reclaim per stream regardless.
661
+ - **An upfront charge is final.** With an upfront listing the `upfrontAmount`
662
+ is paid to the seller at settlement whatever is delivered; only `price -
663
+ upfrontAmount` is escrowed. The contract enforces the terms, not the
664
+ marketing text: a buyer's protection is the SDK recomputing `payTo` from the
665
+ advertised six terms and refusing on any difference, plus its
666
+ `maxUpfrontAmount` cap. The listing's signed `commercial` rate band bounds
667
+ only `ratePerSecond`, not the upfront part.
668
+ - **Stray funds are unrecoverable**: USDC that reaches an opener any way other
669
+ than `deployAndSettle` / `settleAndOpen` stays there (see the hard rule).
670
+ - **Blacklist blast radius**: one opener is the escrow buyer for every payer of
671
+ a listing's terms; if the token issuer blacklists it, refunds through it
672
+ freeze until it is un-blacklisted.
673
+ - **ERC-6492 counterfactual wallets are refused**; deploy the wallet first. A
674
+ deployed ERC-1271 wallet works (USDC verifies it).
675
+ - **Single instance**: one relayer, and the at-most-one-success ledgers are
676
+ in-memory per instance (see above). Do not run replicas.
677
+ - **Listing freshness**: a successful registry read is reused for 60 s by
678
+ default, so a withdrawn listing can keep verifying for up to that long.
679
+ Discovery prefers a gated listing over a public one for the same operator and
680
+ origin; a listing hidden that way does not resolve (fail closed).
409
681
 
410
682
  ---
411
683
 
412
- ## Why Metrik's STREAM does not fit `exact`
413
-
414
- Metrik's unit of payment is a **metered stream**, not a transfer:
415
-
416
- - the buyer escrows a budget into `StreamEscrowV2` at a per-second rate with an expiry;
417
- - a verifier probes the seller's endpoint each interval;
418
- - the seller earns **only for intervals that verified**, claimed incrementally
419
- against Merkle checkpoints;
420
- - whatever was never earned is **recoverable by the buyer** via a withdrawal
421
- transaction.
422
-
423
- Mapping THAT onto `exact` would require collapsing it to a single fixed payment
424
- made up front, directly to the seller, with no escrow and no recoverability —
425
- which deletes the product. So the stream is not, and will not be, expressed as an
426
- x402 payment.
427
-
428
- This is exactly why the delivery gate exists in the shape it does. The danger was
429
- never "support x402"; it was a developer seeing `x402-fetch` succeed against a
430
- Metrik endpoint and concluding their money was protected by verified delivery.
431
- The gate is honest about being weaker: it prevents a bad payment, it does not
432
- recover one, and it says so in the response (`metrik.detail`), in this document,
433
- and in the table at the top.
434
-
435
- ## Why `upto` does not fit either
436
-
437
- `upto` (authorize a maximum, settle the actual usage) is closer, and for a
438
- single metered _request_ it would be a good fit. But its spec rules it out
439
- explicitly:
440
-
441
- > **Out of Scope** … **Multi-settlement / streaming**: Settling the same
442
- > authorization multiple times (e.g., pay-per-chunk streaming)
443
-
444
- and
445
-
446
- > ### 1. Single-Use Authorization
447
- >
448
- > Each authorization MUST be settled at most once.
449
-
450
- A Metrik stream settles the _same_ authorization repeatedly, once per verified
451
- checkpoint, for its whole lifetime. That is precisely the case `upto` excludes.
452
-
453
- ## `auth-capture` is the right shape for the STREAM — and still out of reach
454
-
455
- Of the four specified schemes, [`auth-capture`](https://github.com/x402-foundation/x402/blob/main/specs/schemes/auth-capture/scheme_auth_capture.md)
456
- in its `escrow` payment flow is a near-exact conceptual match for Metrik:
457
-
458
- | `auth-capture` concept | Metrik equivalent |
459
- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------- |
460
- | `authorize` — reserve client funds into a hold | `openStream` — escrow the budget |
461
- | `capture` — pay held funds out, **repeatable up to the held total** | `claimStream` — claim verified accrual at each checkpoint |
462
- | `void` — release the remaining hold | `closeStream` |
463
- | `reclaim` — _client_ recovers its own hold after the capture deadline, never relayed through the facilitator | `reclaimStream`, gated by `escapeGraceSeconds` |
464
- | `extra.captureDeadline` | stream expiry |
465
- | "Metered work priced only once it completes: hold the ceiling, capture the actual cost" | the entire Metrik thesis |
466
-
467
- The scheme's own summary could be describing Metrik. So why not implement it?
468
-
469
- **Because the EVM binding is pinned to a specific escrow contract that Metrik is
470
- not.** [`scheme_auth_capture_evm.md`](https://github.com/x402-foundation/x402/blob/main/specs/schemes/auth-capture/scheme_auth_capture_evm.md)
471
- requires a canonical `AuthCaptureEscrow` singleton — Base's Commerce Payments
472
- Protocol — at one of two fixed CREATE2 addresses:
473
-
474
- | Constant | v1.1 | v1.0 |
475
- | ----------------------------- | -------------------------------------------- | -------------------------------------------- |
476
- | `AUTH_CAPTURE_ESCROW_ADDRESS` | `0xf96815976523E00e65Be8f34cA5e64b4f41EB19c` | `0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff` |
477
-
478
- A conformant facilitator must read `AuthCaptureEscrow.paymentState(paymentInfoHash)`,
479
- and must simulate each operation and assert that **that** escrow emitted
480
- `PaymentAuthorized` / `PaymentCharged` with the expected payment hash before
481
- relaying. Metrik settles in `StreamEscrowV2` at
482
- `0x0f09f36Ccc05A7c9882F438721C08De314dFd46C`, which is a different contract with
483
- a different state model and no such functions or events.
484
-
485
- So `auth-capture` conformance is **not a wire-format exercise. It is a contract
486
- deployment.** Metrik would have to either settle through the canonical
487
- `AuthCaptureEscrow` (giving up the per-second metering and checkpoint model that
488
- is the product) or get a Metrik binding specified and adopted. Neither is a code
489
- change in this package, and the contracts are frozen.
684
+ ## History: the removed x402 v1 integration
685
+
686
+ Until metrik-agent#115 this package shipped an x402 **v1** integration: a
687
+ standard `exact` facilitator (`metrik-x402-facilitator`, exported as
688
+ `@absol-labs/agent/x402-facilitator`) that consulted Metrik's oracle before
689
+ settling, and an opt-in mode on the seller gateway that answered an
690
+ uncapabilitied request with an x402 `402`. It was removed, before it ever ran in
691
+ production, for four reasons:
692
+
693
+ 1. **It targeted a superseded protocol version.** It was built on `x402@1.2.0`
694
+ (`x402Version: 1`, `X-PAYMENT` / `X-PAYMENT-RESPONSE`, bare network names
695
+ such as `base-sepolia`). The protocol is at v2 (`PAYMENT-REQUIRED` /
696
+ `PAYMENT-SIGNATURE` / `PAYMENT-RESPONSE`, CAIP-2 network identifiers such as
697
+ `eip155:84532`).
698
+ 2. **It paid the seller directly.** `payTo` was the operator, so the money never
699
+ touched `StreamEscrowV2`: no escrow, no protocol fee, and nothing a buyer
700
+ could get back.
701
+ 3. **Its delivery gate gated nothing in practice.** On absent evidence it
702
+ defaulted to _allow_, and the live oracle's `GET /services` is built from
703
+ active streams, which was empty — so every payment would have passed.
704
+ 4. **Leaving it in place invited someone to enable it.** It was default-off
705
+ behind environment switches and had never broadcast a settlement.
706
+
707
+ What changed for integrators:
708
+
709
+ - The `metrik-x402-facilitator` bin, the `./x402-facilitator` subpath export,
710
+ the `x402:facilitator` script and every module behind them are gone.
711
+ - The seller gateway no longer reads any `METRIK_GATEWAY_X402_*` variable, and
712
+ the facilitator's `METRIK_X402_*` variables have no reader. A gateway that
713
+ never set them behaves exactly as before: a request with no capability gets
714
+ `403 {"authorized":false,"reason":"missing-capability-header"}`, and a request
715
+ with a valid capability is proxied unchanged
716
+ (`test/gateway-http-baseline.test.ts` pins this over a real socket).
717
+ - A request carrying an oracle probe capability **and** an `X-PAYMENT` header is
718
+ still refused with `400 ambiguous-probe-mode` (and, in the v2 mode, so is a
719
+ probe carrying `PAYMENT-SIGNATURE`). A probe is free verification
720
+ traffic and is never mixed with anything that looks like a payment.
721
+ - The deprecated `X402*` / `x402*` aliases of the native flow's exports were
722
+ removed at the same time. Use the canonical names listed below.
723
+
724
+ `test/x402-v1-removed.test.ts` fails if any source, test, script or example
725
+ imports the v1 `x402` / `x402-fetch` packages again (the v2 `@x402/*` packages
726
+ are what the opener path uses), or if `package.json` re-declares them or the
727
+ removed bin, script or export. `scripts/check-packed-package.mjs` refuses to
728
+ publish a tarball that still contains any removed artifact.
490
729
 
491
730
  ---
492
731
 
493
- ## What this package actually ships
494
-
495
- **Two separate flows.** Do not confuse them.
496
-
497
- ### 1. Standard x402 `exact`, with a delivery gate — the subject of the top half
732
+ ## The Metrik-native verified-stream `402` flow — NOT x402
498
733
 
499
- | Piece | File |
500
- | --------------------------------------------------- | ------------------------------------------------------------------------------------------- |
501
- | `PaymentRequirements` / `402` body builder | [`src/x402/payment-requirements.ts`](../src/x402/payment-requirements.ts) |
502
- | Oracle delivery verdict + freshness | [`src/x402/delivery-oracle.ts`](../src/x402/delivery-oracle.ts) |
503
- | resource URL -> signed listing | [`src/x402/resource-index.ts`](../src/x402/resource-index.ts) |
504
- | The gate (`/verify`, `/settle`, `/supported`) | [`src/x402/verified-delivery-facilitator.ts`](../src/x402/verified-delivery-facilitator.ts) |
505
- | Chain wiring (delegates to real `x402/facilitator`) | [`src/x402/relayer.ts`](../src/x402/relayer.ts) |
506
- | HTTP server + env contract | [`src/x402/facilitator-server.ts`](../src/x402/facilitator-server.ts) |
507
- | Gateway 402 mode | [`src/gateway/x402-mode.ts`](../src/gateway/x402-mode.ts) |
508
-
509
- Standard scheme, standard headers, standard client. `x402` is an **optional peer
510
- dependency**: the facilitator lives behind its own entry point
511
- (`@absol-labs/agent/x402-facilitator`) and the `metrik-x402-facilitator` bin, so
512
- a plain `npm install @absol-labs/agent` never pulls it in.
513
-
514
- ### 2. The Metrik-native verified-stream `402` flow — NOT x402
515
-
516
- In [`src/x402/facilitator.ts`](../src/x402/facilitator.ts):
734
+ In [`src/x402/facilitator.ts`](../src/x402/facilitator.ts) (the directory name is
735
+ historical):
517
736
 
518
737
  - scheme string **`streamproof-verified-stream`** — deliberately _not_ an x402
519
738
  scheme name, and not registered with x402;
@@ -525,124 +744,78 @@ In [`src/x402/facilitator.ts`](../src/x402/facilitator.ts):
525
744
  plus an owner-signed `SpendMandate` — **not** an x402 `PaymentPayload`;
526
745
  - no `/verify`, no `/settle`, no `/supported` endpoint.
527
746
 
528
- This flow is real, it is tested, and it does open real escrowed streams on Base
529
- Sepolia. It is simply **not x402**. It is named for the HTTP status code it
530
- reuses, and for the x402 design it was modelled on.
531
-
532
- The class is exported as `VerifiedStreamFacilitator`. The older
533
- `VerifiedStreamX402Facilitator` name and the other `X402*` exports still work and
534
- are kept as deprecated aliases so nothing breaks, but they are misnomers and the
535
- non-`X402` names are canonical.
747
+ This flow is real, it is tested, and it opens real escrowed streams on Base
748
+ Sepolia. It does not interoperate with `x402-fetch`, `x402-axios`,
749
+ `x402-express` or any other x402 client or facilitator.
750
+
751
+ Exports: `VerifiedStreamFacilitator`, `METRIK_STREAM_SCHEME`,
752
+ `METRIK_PAYMENT_HEADER`, `encodeStreamPayloadHeader`,
753
+ `parseStreamPayloadHeader`, `parseStreamChallengeJson`, `streamChallengeSchema`,
754
+ `streamOpenPayloadSchema`, `verifiedStreamRequirementsSchema`,
755
+ `StreamPayloadError`, `SpendControlUnavailableError`,
756
+ `SettlementTargetMismatchError`, `DeliveryProofUnavailableError`, and the types
757
+ `StreamFacilitator`, `StreamChallenge`, `StreamOpenPayload`,
758
+ `ConsumedHttpsResponseProofInput`, `VerifiedStreamFacilitatorOptions`,
759
+ `VerifiedStreamRequirements`.
760
+
761
+ Two runtime error names still carry the old prefix: a `StreamPayloadError` has
762
+ `.name === "X402PayloadError"` and a `SpendControlUnavailableError` has
763
+ `.name === "X402SpendControlUnavailableError"`. They were left unchanged so code
764
+ that matches on `error.name` keeps working; prefer `instanceof`.
536
765
 
537
766
  ### The header name
538
767
 
539
- Until this change, the integration test sent the Metrik payload under the real
540
- x402 header name `X-PAYMENT`. That was the most actively misleading part of the
541
- whole surface — a real x402 header carrying a payload no x402 facilitator can
542
- parse. Metrik's payload now travels as **`X-METRIK-PAYMENT`**. Metrik does not
543
- put anything on `X-PAYMENT` and will not until it genuinely speaks x402.
768
+ Metrik's payload travels as **`X-METRIK-PAYMENT`**. Metrik puts nothing on the
769
+ x402 v1 `X-PAYMENT` header, and `X-METRIK-PAYMENT` also stays clear of the v2
770
+ header names (`PAYMENT-REQUIRED`, `PAYMENT-SIGNATURE`, `PAYMENT-RESPONSE`).
544
771
 
545
- Note that x402 v2 renames its headers entirely — `PAYMENT-REQUIRED`,
546
- `PAYMENT-SIGNATURE`, `PAYMENT-RESPONSE`, with no `X-` prefix — so
547
- `X-METRIK-PAYMENT` stays clear of both the v1 and the v2 namespace.
772
+ ---
548
773
 
549
- The gateway's x402 mode uses the real `X-PAYMENT` / `X-PAYMENT-RESPONSE` headers,
550
- because there it really is x402. The two never appear on the same request: a
551
- request carrying both a Metrik capability and an `X-PAYMENT` is refused with
552
- `400 ambiguous-payment-mode`.
774
+ ## Why the metered stream does not fit a shipped x402 scheme
553
775
 
554
- ---
776
+ This analysis is why "support x402" was never a wire-format exercise for the
777
+ stream itself. Package facts were gathered on **2026-09-18** from
778
+ `x402-foundation/x402@main` and from `x402@1.2.0` as published on npm; the test
779
+ that re-checked them against the real package was removed with the dependency
780
+ in #115, so re-verify before relying on them.
555
781
 
556
- ## What would have to be true for the STREAM to be an x402 payment
557
-
558
- In rough order of cost:
559
-
560
- 1. **`x402@>=?` ships a scheme with repeatable settlement.** Today `schemes ===
561
- ["exact"]`. Nothing Metrik-shaped can be expressed until `auth-capture` (or an
562
- equivalent) is in the published package, not just the spec.
563
- 2. **A Metrik / generic-escrow network binding for `auth-capture`,** or a
564
- redeployment of Metrik settlement behind the canonical `AuthCaptureEscrow`.
565
- This is a contracts decision, not a package decision — and today's
566
- `AuthCaptureEscrow` binding has no room for per-second metering or
567
- oracle-attested checkpoints.
568
- 3. **x402 v2 adoption in the client libraries,** so the extensible `scheme` string
569
- and CAIP-2 networks in `x402-specification-v2.md` are actually usable.
570
-
571
- The cheaper intermediate step described in an earlier revision of this document
572
- — "serve an ordinary `exact` x402 payment for one-shot endpoints alongside the
573
- Metrik stream flow for metered ones" — **is the step that has now been taken**,
574
- with one addition that changes its character: the `exact` path is not an
575
- unverified payment. It is gated on Metrik's own delivery verdict. It is still
576
- weaker than the stream, for the reason stated at the top: it prevents a payment
577
- rather than recovering one.
782
+ | Thing | Reality at the date above |
783
+ | ----------------------------------- | ----------------------------------------------------------------- |
784
+ | Spec repository | [`x402-foundation/x402`](https://github.com/x402-foundation/x402) |
785
+ | Schemes implemented by `x402@1.2.0` | **`exact` only** |
786
+ | Schemes that exist _in the spec_ | `exact`, `upto`, `batch-settlement`, `auth-capture` |
787
+ | Protocol versions in `x402@1.2.0` | `x402Version: 1` only; v2 was specified, not yet in that package |
578
788
 
579
- ---
789
+ ### `exact`
580
790
 
581
- ## Evidence
582
-
583
- Everything here is checked against the **real** published `x402` package and the
584
- **real** published `x402-fetch` client — devDependencies, not fixtures.
585
-
586
- ### The stream is not x402 — [`test/x402-conformance.test.ts`](../test/x402-conformance.test.ts)
587
-
588
- - `schemes` is exactly `["exact"]`;
589
- - `x402Versions` is exactly `[1]`;
590
- - the real `PaymentRequirementsSchema` **rejects** Metrik's stream scheme string;
591
- - it also rejects `auth-capture` and `upto`;
592
- - the real `PaymentPayloadSchema` rejects Metrik's stream payload shape;
593
- - the stream flow publishes nothing on the `X-PAYMENT` header.
594
-
595
- That test is a tripwire. **When x402 ships a scheme that fits the Metrik stream,
596
- those assertions start failing** — the signal to revisit this document.
597
-
598
- ### What Metrik emits IS x402 — [`test/x402-facilitator-conformance.test.ts`](../test/x402-facilitator-conformance.test.ts)
599
-
600
- Everything Metrik puts on the wire is parsed with the real schemas: the
601
- `PaymentRequirements`, the `402` body the way `x402-fetch` parses it, and both
602
- the passing and the refusing `VerifyResponse`. It also pins two places where the
603
- shipped library contradicts itself, so they are recorded decisions and not
604
- accidents:
605
-
606
- - `x402ResponseSchema` types `error` as the closed `ErrorReasons` enum, which no
607
- real x402 server obeys (`x402-express` puts human text there, `x402-fetch`
608
- never reads the field). Metrik follows the shipped behaviour.
609
- - `SettleResponseSchema` requires `transaction` to match an address-shaped regex,
610
- so a FAILED settlement cannot be expressed conformantly. `x402`'s own `settle`
611
- returns `transaction: ""` on every error path, which its own schema rejects.
612
- Metrik emits the same `""` rather than inventing a placeholder that looks like
613
- a real transaction hash.
614
-
615
- ### The gate — [`test/x402-delivery-gate.test.ts`](../test/x402-delivery-gate.test.ts)
616
-
617
- 24 assertions over the real oracle projection and the real decision tree: refuse
618
- on a fresh failure; allow on a fresh pass; no gate for an untracked resource;
619
- payee-mismatch refusal; a lying `extra.metrikServiceRef` changing nothing;
620
- freshness symmetry at the 90 s boundary; every no-evidence case under both
621
- policies; and `/settle` re-running the gate, refusing to broadcast for a failing
622
- service, and throwing rather than faking success with no relayer.
623
-
624
- ### The whole loop — [`test/x402-gateway.test.ts`](../test/x402-gateway.test.ts)
625
-
626
- `x402-fetch@1.2.0` — the published client — reads Metrik's `402`, parses
627
- `accepts` with the real `PaymentRequirementsSchema`, signs an EIP-3009
628
- authorization, retries with `X-PAYMENT`, gets `200` plus a
629
- `X-PAYMENT-RESPONSE` it decodes with the library's own
630
- `decodeXPaymentResponse`. Nothing in that file hand-rolls a payment header: if
631
- Metrik's `402` were not conformant, the client would throw instead of paying.
632
- The same test then flips the oracle to `failing` and asserts the identical, valid
633
- payment is refused with `402`, nothing is settled, and the seller's upstream is
634
- never called.
635
-
636
- **What is real in that test**: the gateway, its HTTP server, the facilitator HTTP
637
- wire over a real socket, the oracle projection and freshness arithmetic, and the
638
- EIP-712 signature check. **What is a fixture**: the payer's on-chain balance, and
639
- the broadcast. Nothing is broadcast, anywhere, ever.
640
-
641
- Spec and package facts were gathered on **2026-09-18** from
642
- `x402-foundation/x402@main` and from `x402@1.2.0` as published on npm.
643
-
644
- One incidental packaging note, true at `x402@1.2.0`: the package's `exports` map
645
- has no `"."` entry, so `import { … } from "x402"` fails with
646
- `ERR_PACKAGE_PATH_NOT_EXPORTED` under Node ESM. The subpaths (`x402/types`,
647
- `x402/facilitator`, `x402/verify`, `x402/schemes`, `x402/client`) are what
648
- resolve.
791
+ `exact` on EVM is an [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009)
792
+ `transferWithAuthorization`: one fixed amount, one transfer, straight to
793
+ `payTo`. No hold, no conditional release, no partial capture, no reclaim.
794
+
795
+ Metrik's unit of payment is a **metered stream**: the buyer escrows a budget
796
+ into `StreamEscrowV2` at a per-second rate with an expiry, a verifier probes the
797
+ seller each interval, the seller earns only for intervals that verified
798
+ (claimed against Merkle checkpoints), and whatever was never earned is
799
+ recoverable by the buyer via a withdrawal transaction. Collapsing that into one
800
+ up-front transfer to the seller deletes the product — which is exactly what the
801
+ removed v1 integration did, and why it went. The opener path at the top of this
802
+ document keeps the `exact` payment but points its `payTo` at an opener that
803
+ opens the stream, instead of at the seller.
804
+
805
+ ### `upto`
806
+
807
+ `upto` (authorize a maximum, settle actual usage) fits a single metered
808
+ _request_, but its spec explicitly puts "multi-settlement / streaming" out of
809
+ scope and requires each authorization to be settled at most once. A Metrik
810
+ stream settles the same authorization repeatedly, once per verified checkpoint.
811
+
812
+ ### `auth-capture`
813
+
814
+ `auth-capture` in its `escrow` flow is the closest conceptual match —
815
+ `authorize`/`capture`/`void`/`reclaim` map onto `openStream`/`claim`/`close`/
816
+ `reclaim` — but its EVM binding is pinned to the canonical `AuthCaptureEscrow`
817
+ contract (Base's Commerce Payments Protocol), which a conformant facilitator
818
+ must read and whose events it must observe. Metrik settles in `StreamEscrowV2`
819
+ (`0x0f09f36Ccc05A7c9882F438721C08De314dFd46C`), a different contract with a
820
+ different state model, so conformance there would be a contract deployment, not
821
+ a code change in this package.