@absol-labs/agent 0.10.0 → 0.12.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/README.md +106 -30
  2. package/dist/capability/invocation-capability.d.ts +20 -8
  3. package/dist/capability/invocation-capability.d.ts.map +1 -1
  4. package/dist/capability/invocation-capability.js.map +1 -1
  5. package/dist/capability/probe-capability.d.ts +225 -0
  6. package/dist/capability/probe-capability.d.ts.map +1 -0
  7. package/dist/capability/probe-capability.js +248 -0
  8. package/dist/capability/probe-capability.js.map +1 -0
  9. package/dist/cli/args.d.ts +11 -0
  10. package/dist/cli/args.d.ts.map +1 -1
  11. package/dist/cli/args.js +23 -0
  12. package/dist/cli/args.js.map +1 -1
  13. package/dist/cli/commands/claim.d.ts.map +1 -1
  14. package/dist/cli/commands/claim.js +21 -5
  15. package/dist/cli/commands/claim.js.map +1 -1
  16. package/dist/cli/commands/close.d.ts.map +1 -1
  17. package/dist/cli/commands/close.js +51 -7
  18. package/dist/cli/commands/close.js.map +1 -1
  19. package/dist/cli/commands/invoke.d.ts +2 -0
  20. package/dist/cli/commands/invoke.d.ts.map +1 -1
  21. package/dist/cli/commands/invoke.js +3 -2
  22. package/dist/cli/commands/invoke.js.map +1 -1
  23. package/dist/cli/commands/mcp.d.ts.map +1 -1
  24. package/dist/cli/commands/mcp.js +15 -2
  25. package/dist/cli/commands/mcp.js.map +1 -1
  26. package/dist/cli/commands/open.d.ts.map +1 -1
  27. package/dist/cli/commands/open.js +121 -15
  28. package/dist/cli/commands/open.js.map +1 -1
  29. package/dist/cli/commands/pay.d.ts +19 -0
  30. package/dist/cli/commands/pay.d.ts.map +1 -0
  31. package/dist/cli/commands/pay.js +355 -0
  32. package/dist/cli/commands/pay.js.map +1 -0
  33. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
  34. package/dist/cli/commands/reclaim-unverified.js +15 -6
  35. package/dist/cli/commands/reclaim-unverified.js.map +1 -1
  36. package/dist/cli/commands/reclaim.d.ts +23 -2
  37. package/dist/cli/commands/reclaim.d.ts.map +1 -1
  38. package/dist/cli/commands/reclaim.js +200 -21
  39. package/dist/cli/commands/reclaim.js.map +1 -1
  40. package/dist/cli/commands/refund.d.ts +13 -0
  41. package/dist/cli/commands/refund.d.ts.map +1 -0
  42. package/dist/cli/commands/refund.js +224 -0
  43. package/dist/cli/commands/refund.js.map +1 -0
  44. package/dist/cli/commands/status.d.ts.map +1 -1
  45. package/dist/cli/commands/status.js +10 -1
  46. package/dist/cli/commands/status.js.map +1 -1
  47. package/dist/cli/commands/streams.js +9 -13
  48. package/dist/cli/commands/streams.js.map +1 -1
  49. package/dist/cli/commands/watch.d.ts.map +1 -1
  50. package/dist/cli/commands/watch.js +2 -1
  51. package/dist/cli/commands/watch.js.map +1 -1
  52. package/dist/cli/config.d.ts +44 -26
  53. package/dist/cli/config.d.ts.map +1 -1
  54. package/dist/cli/config.js +88 -38
  55. package/dist/cli/config.js.map +1 -1
  56. package/dist/cli/deps.d.ts +86 -7
  57. package/dist/cli/deps.d.ts.map +1 -1
  58. package/dist/cli/deps.js +102 -16
  59. package/dist/cli/deps.js.map +1 -1
  60. package/dist/cli/errors.d.ts.map +1 -1
  61. package/dist/cli/errors.js +21 -1
  62. package/dist/cli/errors.js.map +1 -1
  63. package/dist/cli/escape-hatch.d.ts +6 -4
  64. package/dist/cli/escape-hatch.d.ts.map +1 -1
  65. package/dist/cli/escape-hatch.js +6 -2
  66. package/dist/cli/escape-hatch.js.map +1 -1
  67. package/dist/cli/listings.d.ts +18 -0
  68. package/dist/cli/listings.d.ts.map +1 -1
  69. package/dist/cli/listings.js +22 -12
  70. package/dist/cli/listings.js.map +1 -1
  71. package/dist/cli/preflight.d.ts +20 -14
  72. package/dist/cli/preflight.d.ts.map +1 -1
  73. package/dist/cli/preflight.js +34 -20
  74. package/dist/cli/preflight.js.map +1 -1
  75. package/dist/cli/privy.d.ts +83 -0
  76. package/dist/cli/privy.d.ts.map +1 -0
  77. package/dist/cli/privy.js +232 -0
  78. package/dist/cli/privy.js.map +1 -0
  79. package/dist/cli/reclaimability.d.ts +82 -0
  80. package/dist/cli/reclaimability.d.ts.map +1 -0
  81. package/dist/cli/reclaimability.js +80 -0
  82. package/dist/cli/reclaimability.js.map +1 -0
  83. package/dist/cli/run.d.ts +2 -1
  84. package/dist/cli/run.d.ts.map +1 -1
  85. package/dist/cli/run.js +40 -11
  86. package/dist/cli/run.js.map +1 -1
  87. package/dist/cli/scan-range.d.ts +2 -2
  88. package/dist/cli/scan-range.js +2 -2
  89. package/dist/cli/spend-policy.d.ts +60 -0
  90. package/dist/cli/spend-policy.d.ts.map +1 -0
  91. package/dist/cli/spend-policy.js +65 -0
  92. package/dist/cli/spend-policy.js.map +1 -0
  93. package/dist/cli/status.d.ts +3 -1
  94. package/dist/cli/status.d.ts.map +1 -1
  95. package/dist/cli/status.js +15 -3
  96. package/dist/cli/status.js.map +1 -1
  97. package/dist/cli/wallet.d.ts +109 -40
  98. package/dist/cli/wallet.d.ts.map +1 -1
  99. package/dist/cli/wallet.js +109 -44
  100. package/dist/cli/wallet.js.map +1 -1
  101. package/dist/discovery/rate-band.d.ts +74 -0
  102. package/dist/discovery/rate-band.d.ts.map +1 -0
  103. package/dist/discovery/rate-band.js +114 -0
  104. package/dist/discovery/rate-band.js.map +1 -0
  105. package/dist/escrow-generation.d.ts +81 -0
  106. package/dist/escrow-generation.d.ts.map +1 -0
  107. package/dist/escrow-generation.js +76 -0
  108. package/dist/escrow-generation.js.map +1 -0
  109. package/dist/frameworks/agentkit.d.ts +8 -4
  110. package/dist/frameworks/agentkit.d.ts.map +1 -1
  111. package/dist/frameworks/agentkit.js +27 -10
  112. package/dist/frameworks/agentkit.js.map +1 -1
  113. package/dist/frameworks/eliza.d.ts +3 -3
  114. package/dist/frameworks/eliza.d.ts.map +1 -1
  115. package/dist/frameworks/eliza.js +15 -6
  116. package/dist/frameworks/eliza.js.map +1 -1
  117. package/dist/frameworks/langchain.d.ts.map +1 -1
  118. package/dist/frameworks/langchain.js +8 -0
  119. package/dist/frameworks/langchain.js.map +1 -1
  120. package/dist/gateway/caller-auth-gateway.d.ts +91 -4
  121. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  122. package/dist/gateway/caller-auth-gateway.js +184 -7
  123. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  124. package/dist/gateway/http-server.d.ts +121 -1
  125. package/dist/gateway/http-server.d.ts.map +1 -1
  126. package/dist/gateway/http-server.js +474 -5
  127. package/dist/gateway/http-server.js.map +1 -1
  128. package/dist/gateway/probe-auth.d.ts +112 -0
  129. package/dist/gateway/probe-auth.d.ts.map +1 -0
  130. package/dist/gateway/probe-auth.js +127 -0
  131. package/dist/gateway/probe-auth.js.map +1 -0
  132. package/dist/gateway/signed-rate-bounds.d.ts +51 -0
  133. package/dist/gateway/signed-rate-bounds.d.ts.map +1 -0
  134. package/dist/gateway/signed-rate-bounds.js +94 -0
  135. package/dist/gateway/signed-rate-bounds.js.map +1 -0
  136. package/dist/gateway/x402-opener-mode.d.ts +215 -0
  137. package/dist/gateway/x402-opener-mode.d.ts.map +1 -0
  138. package/dist/gateway/x402-opener-mode.js +639 -0
  139. package/dist/gateway/x402-opener-mode.js.map +1 -0
  140. package/dist/index.d.ts +11 -5
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +15 -5
  143. package/dist/index.js.map +1 -1
  144. package/dist/mandates/env.d.ts +3 -2
  145. package/dist/mandates/env.d.ts.map +1 -1
  146. package/dist/mandates/env.js +3 -2
  147. package/dist/mandates/env.js.map +1 -1
  148. package/dist/mcp/server.d.ts +100 -0
  149. package/dist/mcp/server.d.ts.map +1 -1
  150. package/dist/mcp/server.js +719 -39
  151. package/dist/mcp/server.js.map +1 -1
  152. package/dist/mcp/service-invoker.d.ts +97 -0
  153. package/dist/mcp/service-invoker.d.ts.map +1 -0
  154. package/dist/mcp/service-invoker.js +167 -0
  155. package/dist/mcp/service-invoker.js.map +1 -0
  156. package/dist/mcp/x402-tools.d.ts +104 -0
  157. package/dist/mcp/x402-tools.d.ts.map +1 -0
  158. package/dist/mcp/x402-tools.js +434 -0
  159. package/dist/mcp/x402-tools.js.map +1 -0
  160. package/dist/network.d.ts +119 -0
  161. package/dist/network.d.ts.map +1 -0
  162. package/dist/network.js +199 -0
  163. package/dist/network.js.map +1 -0
  164. package/dist/sdk/invoke.d.ts +23 -4
  165. package/dist/sdk/invoke.d.ts.map +1 -1
  166. package/dist/sdk/invoke.js +71 -10
  167. package/dist/sdk/invoke.js.map +1 -1
  168. package/dist/wallet/autonomous-wallet-broker.d.ts +3 -0
  169. package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -1
  170. package/dist/wallet/autonomous-wallet-broker.js +27 -23
  171. package/dist/wallet/autonomous-wallet-broker.js.map +1 -1
  172. package/dist/wallet/autonomous-wallet-protocol.d.ts +8 -2
  173. package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -1
  174. package/dist/wallet/autonomous-wallet-protocol.js +9 -2
  175. package/dist/wallet/autonomous-wallet-protocol.js.map +1 -1
  176. package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
  177. package/dist/wallet/autonomous-wallet.js +3 -2
  178. package/dist/wallet/autonomous-wallet.js.map +1 -1
  179. package/dist/wallet/broker-escrows.d.ts +222 -0
  180. package/dist/wallet/broker-escrows.d.ts.map +1 -0
  181. package/dist/wallet/broker-escrows.js +36 -0
  182. package/dist/wallet/broker-escrows.js.map +1 -0
  183. package/dist/wallet/lifecycle.d.ts +21 -5
  184. package/dist/wallet/lifecycle.d.ts.map +1 -1
  185. package/dist/wallet/lifecycle.js +32 -12
  186. package/dist/wallet/lifecycle.js.map +1 -1
  187. package/dist/wallet/privy-broker-server.d.ts.map +1 -1
  188. package/dist/wallet/privy-broker-server.js +31 -26
  189. package/dist/wallet/privy-broker-server.js.map +1 -1
  190. package/dist/wallet/privy-session-broker.d.ts +14 -5
  191. package/dist/wallet/privy-session-broker.d.ts.map +1 -1
  192. package/dist/wallet/privy-session-broker.js +29 -17
  193. package/dist/wallet/privy-session-broker.js.map +1 -1
  194. package/dist/wallet/privy-session-provider.d.ts +3 -1
  195. package/dist/wallet/privy-session-provider.d.ts.map +1 -1
  196. package/dist/wallet/privy-session-provider.js +5 -3
  197. package/dist/wallet/privy-session-provider.js.map +1 -1
  198. package/dist/wallet/provider.d.ts.map +1 -1
  199. package/dist/wallet/provider.js +15 -8
  200. package/dist/wallet/provider.js.map +1 -1
  201. package/dist/x402/facilitator.d.ts +42 -17
  202. package/dist/x402/facilitator.d.ts.map +1 -1
  203. package/dist/x402/facilitator.js +49 -33
  204. package/dist/x402/facilitator.js.map +1 -1
  205. package/dist/x402-buyer/guarded-fetch.d.ts +51 -0
  206. package/dist/x402-buyer/guarded-fetch.d.ts.map +1 -0
  207. package/dist/x402-buyer/guarded-fetch.js +147 -0
  208. package/dist/x402-buyer/guarded-fetch.js.map +1 -0
  209. package/dist/x402-buyer/index.d.ts +10 -0
  210. package/dist/x402-buyer/index.d.ts.map +1 -0
  211. package/dist/x402-buyer/index.js +10 -0
  212. package/dist/x402-buyer/index.js.map +1 -0
  213. package/dist/x402-buyer/pay.d.ts +135 -0
  214. package/dist/x402-buyer/pay.d.ts.map +1 -0
  215. package/dist/x402-buyer/pay.js +336 -0
  216. package/dist/x402-buyer/pay.js.map +1 -0
  217. package/dist/x402-opener/abi.d.ts +684 -0
  218. package/dist/x402-opener/abi.d.ts.map +1 -0
  219. package/dist/x402-opener/abi.js +880 -0
  220. package/dist/x402-opener/abi.js.map +1 -0
  221. package/dist/x402-opener/facilitator-entry.d.ts +3 -0
  222. package/dist/x402-opener/facilitator-entry.d.ts.map +1 -0
  223. package/dist/x402-opener/facilitator-entry.js +37 -0
  224. package/dist/x402-opener/facilitator-entry.js.map +1 -0
  225. package/dist/x402-opener/facilitator-server.d.ts +180 -0
  226. package/dist/x402-opener/facilitator-server.d.ts.map +1 -0
  227. package/dist/x402-opener/facilitator-server.js +696 -0
  228. package/dist/x402-opener/facilitator-server.js.map +1 -0
  229. package/dist/x402-opener/index.d.ts +14 -0
  230. package/dist/x402-opener/index.d.ts.map +1 -0
  231. package/dist/x402-opener/index.js +14 -0
  232. package/dist/x402-opener/index.js.map +1 -0
  233. package/dist/x402-opener/listing-resolver.d.ts +55 -0
  234. package/dist/x402-opener/listing-resolver.d.ts.map +1 -0
  235. package/dist/x402-opener/listing-resolver.js +125 -0
  236. package/dist/x402-opener/listing-resolver.js.map +1 -0
  237. package/dist/x402-opener/refund-relay.d.ts +240 -0
  238. package/dist/x402-opener/refund-relay.d.ts.map +1 -0
  239. package/dist/x402-opener/refund-relay.js +1240 -0
  240. package/dist/x402-opener/refund-relay.js.map +1 -0
  241. package/dist/x402-opener/relayer-queue.d.ts +14 -0
  242. package/dist/x402-opener/relayer-queue.d.ts.map +1 -0
  243. package/dist/x402-opener/relayer-queue.js +18 -0
  244. package/dist/x402-opener/relayer-queue.js.map +1 -0
  245. package/dist/x402-opener/scheme.d.ts +217 -0
  246. package/dist/x402-opener/scheme.d.ts.map +1 -0
  247. package/dist/x402-opener/scheme.js +749 -0
  248. package/dist/x402-opener/scheme.js.map +1 -0
  249. package/dist/x402-opener/terms.d.ts +105 -0
  250. package/dist/x402-opener/terms.d.ts.map +1 -0
  251. package/dist/x402-opener/terms.js +212 -0
  252. package/dist/x402-opener/terms.js.map +1 -0
  253. package/dist/zktls/reclaim.d.ts +2 -2
  254. package/docs/agent-layer.md +13 -8
  255. package/docs/autonomous-privy-wallet.md +188 -0
  256. package/docs/cli.md +342 -53
  257. package/docs/langchain.md +5 -0
  258. package/docs/mcp-hosted.md +4 -3
  259. package/docs/mcp-reference.md +245 -0
  260. package/docs/probe-capability.md +191 -0
  261. package/docs/quickstart.md +54 -5
  262. package/docs/threat-model.md +24 -3
  263. package/docs/x402.md +681 -0
  264. package/package.json +19 -6
  265. package/src/capability/invocation-capability.ts +17 -1
  266. package/src/capability/probe-capability.ts +355 -0
  267. package/src/cli/args.ts +29 -0
  268. package/src/cli/commands/claim.ts +31 -5
  269. package/src/cli/commands/close.ts +56 -7
  270. package/src/cli/commands/invoke.ts +6 -2
  271. package/src/cli/commands/mcp.ts +18 -2
  272. package/src/cli/commands/open.ts +151 -14
  273. package/src/cli/commands/pay.ts +442 -0
  274. package/src/cli/commands/reclaim-unverified.ts +24 -6
  275. package/src/cli/commands/reclaim.ts +274 -19
  276. package/src/cli/commands/refund.ts +277 -0
  277. package/src/cli/commands/status.ts +17 -1
  278. package/src/cli/commands/streams.ts +11 -15
  279. package/src/cli/commands/watch.ts +7 -1
  280. package/src/cli/config.ts +139 -52
  281. package/src/cli/deps.ts +189 -25
  282. package/src/cli/errors.ts +24 -1
  283. package/src/cli/escape-hatch.ts +19 -5
  284. package/src/cli/listings.ts +43 -10
  285. package/src/cli/preflight.ts +36 -21
  286. package/src/cli/privy.ts +398 -0
  287. package/src/cli/reclaimability.ts +154 -0
  288. package/src/cli/run.ts +54 -11
  289. package/src/cli/scan-range.ts +2 -2
  290. package/src/cli/spend-policy.ts +139 -0
  291. package/src/cli/status.ts +14 -1
  292. package/src/cli/wallet.ts +189 -58
  293. package/src/discovery/rate-band.ts +172 -0
  294. package/src/escrow-generation.ts +135 -0
  295. package/src/frameworks/agentkit.ts +45 -11
  296. package/src/frameworks/eliza.ts +27 -5
  297. package/src/frameworks/langchain.ts +14 -0
  298. package/src/gateway/caller-auth-gateway.ts +336 -13
  299. package/src/gateway/http-server.ts +690 -7
  300. package/src/gateway/probe-auth.ts +191 -0
  301. package/src/gateway/signed-rate-bounds.ts +164 -0
  302. package/src/gateway/x402-opener-mode.ts +974 -0
  303. package/src/index.ts +112 -14
  304. package/src/mandates/env.ts +3 -2
  305. package/src/mcp/server.ts +973 -44
  306. package/src/mcp/service-invoker.ts +285 -0
  307. package/src/mcp/x402-tools.ts +621 -0
  308. package/src/network.ts +302 -0
  309. package/src/sdk/invoke.ts +96 -17
  310. package/src/wallet/autonomous-wallet-broker.ts +46 -20
  311. package/src/wallet/autonomous-wallet-protocol.ts +11 -2
  312. package/src/wallet/autonomous-wallet.ts +4 -2
  313. package/src/wallet/broker-escrows.ts +66 -0
  314. package/src/wallet/lifecycle.ts +53 -18
  315. package/src/wallet/privy-broker-server.ts +35 -25
  316. package/src/wallet/privy-session-broker.ts +58 -25
  317. package/src/wallet/privy-session-provider.ts +7 -3
  318. package/src/wallet/provider.ts +20 -15
  319. package/src/x402/facilitator.ts +85 -46
  320. package/src/x402-buyer/guarded-fetch.ts +213 -0
  321. package/src/x402-buyer/index.ts +9 -0
  322. package/src/x402-buyer/pay.ts +517 -0
  323. package/src/x402-opener/abi.ts +881 -0
  324. package/src/x402-opener/facilitator-entry.ts +45 -0
  325. package/src/x402-opener/facilitator-server.ts +1014 -0
  326. package/src/x402-opener/index.ts +13 -0
  327. package/src/x402-opener/listing-resolver.ts +202 -0
  328. package/src/x402-opener/refund-relay.ts +1743 -0
  329. package/src/x402-opener/relayer-queue.ts +18 -0
  330. package/src/x402-opener/scheme.ts +1133 -0
  331. package/src/x402-opener/terms.ts +300 -0
package/docs/x402.md ADDED
@@ -0,0 +1,681 @@
1
+ # Metrik and x402
2
+
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.
12
+ >
13
+ > The separate Metrik-native `402` flow (scheme `streamproof-verified-stream`,
14
+ > header `X-METRIK-PAYMENT`) described at the end is **not** x402 and is
15
+ > unchanged.
16
+
17
+ ## The hard rule: an opener `payTo` is settled ONLY by the Metrik opener facilitator
18
+
19
+ **Read this before running any of it.** The x402 `payTo` in this mode is the
20
+ address of an `X402StreamOpener`. Every stock x402 `exact` facilitator settles
21
+ by calling `USDC.transferWithAuthorization(to = payTo)` directly. Pointed at an
22
+ opener, that transfer succeeds and **strands 100% of the payer's payment in the
23
+ opener permanently**: the opener deliberately has no sweep or "claim stray
24
+ funds" path (one would be exploitable, because a used and a cancelled EIP-3009
25
+ nonce are indistinguishable on-chain). The same happens if anyone who sees the
26
+ signed payload submits it to USDC first. See metrik-contracts
27
+ `evm/docs/x402-opener.md`.
28
+
29
+ So, in this package, and for anyone integrating it:
30
+
31
+ 1. **Only the Metrik opener facilitator may settle an opener `payTo`.** It
32
+ calls `factory.deployAndSettle(terms, authorization)`, which pulls the
33
+ payment into the opener and opens the stream in the same transaction. Its
34
+ `GET /supported` advertises a marker —
35
+ `kinds[].extra.metrikStreamOpener = {settlement: "x402-stream-opener",
36
+ factory, escrow, usdc, chainId, minAmount, receiptTimeoutSeconds, refundRelay?}` — so a resource server can tell it apart
37
+ from a stock facilitator. `refundRelay: true` means the same service also
38
+ runs the gasless refund relay (see below).
39
+ 2. **The gateway refuses to start** unless its configured facilitator's
40
+ `/supported` carries that marker for exactly the gateway's factory, escrow,
41
+ token and chain. It re-checks the marker on a timer (default every 300 s)
42
+ and **fails closed** whenever it cannot prove it: `503
43
+ x402-facilitator-unavailable`, no `PAYMENT-REQUIRED`, nothing verified,
44
+ nothing served, nothing settled anywhere else. A facilitator that is
45
+ unreachable when a payment arrives produces `402`, never a free call.
46
+ 3. **Never advertise an opener `payTo` to third-party facilitators or discovery
47
+ catalogs** (the x402 Bazaar extension or any other). The gateway registers
48
+ no discovery extension and never submits its route to any catalog. Only
49
+ when the operator configures `METRIK_GATEWAY_X402V2_SCHEMA` does the 402
50
+ _declare_ an input/output schema, in the standard `extensions.bazaar` shape,
51
+ so a client can read what to send; the only facilitator that ever sees the
52
+ payment is the Metrik opener facilitator, and it catalogs nothing. Do not
53
+ register the route with a discovery service.
54
+ 4. **Authenticate an opener only as `factory.predict(terms) == payTo`** (or
55
+ `factory.isOpener(address)`), never by code or codehash: anyone can clone the
56
+ implementation outside the factory with byte-identical code. The
57
+ facilitator recomputes `predict` on-chain for every payment.
58
+ 5. **The signed payment never leaves the gateway→facilitator path.** The
59
+ gateway strips `PAYMENT-SIGNATURE` (and `X-PAYMENT`) before proxying to the
60
+ seller's upstream.
61
+
62
+ ## How a payment flows
63
+
64
+ ```
65
+ buyer (@x402/fetch) gateway (x402 mode) opener facilitator chain
66
+ │ GET /resource ───────────▶ 402 + PAYMENT-REQUIRED
67
+ │ payTo = factory.predict(terms)
68
+ │ PAYMENT-SIGNATURE ─────────▶ POST /verify ───────────────▶ all checks below
69
+ │ upstream request (status < 400?)
70
+ │ POST /settle (bearer) ──────▶ deployAndSettle ────────────▶ USDC → opener → StreamEscrowV2
71
+ │ 200 + body + PAYMENT-RESPONSE ◀── receipt shows the stream ◀─────────────────────────── StreamOpened / StreamOpenedFor
72
+ ```
73
+
74
+ - **Terms.** One listing's immutable terms are `{operator, serviceRef,
75
+ ratePerSecond, maxDuration}`; the opener for them lives at
76
+ `factory.predict(terms)` whether or not it is deployed yet. The terms travel
77
+ in `PaymentRequirements.extra.metrikStreamOpener` (with the factory address);
78
+ the payer's EIP-3009 signature binds `to = payTo`, so it commits to them.
79
+ - **Price = deposit.** Each paid request opens one stream with `deposit =
80
+ price`, on the listing's terms. The escrow records the **opener** as the
81
+ stream's buyer; the opener records the payer (`payerOf(streamId)`).
82
+ - **Earning.** Unchanged: the operator earns `ratePerSecond × verified
83
+ seconds`, capped at the deposit, claimed with `claimWithProof` against the
84
+ oracle's checkpoints, minus the 0.5% protocol fee.
85
+ - **Settle after serve.** The gateway settles only when the upstream answered
86
+ `< 400`, and before any upstream byte reaches the buyer (the reference
87
+ `@x402/*` middleware order). An upstream `>= 400` submits nothing, so no
88
+ funds move. A failed settlement sends no upstream body.
89
+
90
+ ### What the facilitator checks (every payment, fail closed)
91
+
92
+ Standard `exact` checks are delegated to the official `@x402/evm`
93
+ `ExactEvmScheme` verifier (signature, `to == payTo`, `value == amount`,
94
+ validity window, asset contract, a simulation of the transfer). The stock
95
+ scheme is given a signer that cannot broadcast, so its direct-transfer settle
96
+ path is structurally unreachable. On top of it:
97
+
98
+ | Check | Refusal reason |
99
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
100
+ | x402 v2, scheme `exact`, the configured CAIP-2 network | `metrik_opener_unsupported_*` |
101
+ | `payload.accepted` equals the requirements exactly | `metrik_opener_accepted_requirements_mismatch` |
102
+ | Asset transfer method is EIP-3009 (Permit2 and everything else refused) | `metrik_opener_asset_transfer_method_not_eip3009` |
103
+ | Not an ERC-6492 (counterfactual smart wallet) signature | `metrik_opener_erc6492_signature_unsupported` |
104
+ | Asset is allowlisted and equals `factory.usdc()` | `metrik_opener_asset_not_allowed` |
105
+ | Terms are well formed; the factory named is the configured one | `metrik_opener_invalid_terms` / `_factory_mismatch` |
106
+ | `0 < amount <= ratePerSecond × maxDuration` | `metrik_opener_invalid_amount` / `_amount_exceeds_rate_times_duration` |
107
+ | `payTo == factory.predict(terms)`, read on-chain | `metrik_opener_pay_to_not_predicted_opener` |
108
+ | `(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_*` |
109
+ | `deployAndSettle` simulates cleanly from the relayer | `metrik_opener_settle_simulation_failed` |
110
+
111
+ `settle` re-runs all of it, broadcasts `deployAndSettle`, waits for the
112
+ receipt, and reports `success: true` only when the mined receipt contains both
113
+ the escrow's `StreamOpened` (buyer = opener, exactly these terms and deposit)
114
+ and the opener's `StreamOpenedFor` (this payer, nonce and amount). The
115
+ `SettleResponse` carries `extra.metrikStreamOpener = {streamId, opener, escrow,
116
+ deposit, ratePerSecond, expiresAt}`. A revert (for example an authorization that
117
+ was already used) is `success: false` with the reason. A receipt that does not
118
+ arrive in time is `settlement_pending`; a later settle of the same payload
119
+ reconciles against that transaction instead of broadcasting a second one.
120
+
121
+ ### At most one success per authorization
122
+
123
+ Each EIP-3009 authorization — keyed by chain, asset, payer and authorization
124
+ nonce — receives **at most one successful `SettleResponse` from a facilitator
125
+ instance, ever**:
126
+
127
+ - A settle (or verify) of an authorization that is already in flight or already
128
+ settled returns `success: false` / `isValid: false` with
129
+ `duplicate_settlement` (carrying the tx hash when there is one). One payment
130
+ can never buy two served responses.
131
+ - After `settlement_pending`, a retry of the SAME payload verifies (even though
132
+ the authorization may already be used on-chain) and its settle reconciles to
133
+ the mined outcome; it receives the one success only if none was delivered
134
+ before.
135
+ - The gateway enforces the same rule on its side: a `PAYMENT-SIGNATURE` whose
136
+ authorization is in flight or already settled gets `409
137
+ x402-payment-already-used` **without** an upstream call; a retry after
138
+ `settlement_pending` is let through so it can reconcile.
139
+ - Relayer submissions (simulate + broadcast) are serialized, so concurrent
140
+ payers never collide on the relayer's account nonce.
141
+
142
+ **Scope of the guarantee: both ledgers are in-memory and per instance.** They
143
+ do not survive a restart and are not shared between replicas. Run exactly one
144
+ facilitator instance (and one gateway instance per listing); a multi-instance
145
+ deployment would need a shared store, which does not exist today. Entries are
146
+ kept until the authorization's `validBefore` has passed (plus a grace), after
147
+ which USDC itself refuses the authorization.
148
+
149
+ ## Getting unearned money back: the gasless refund relay
150
+
151
+ **There is no automatic refund.** Nothing pushes money back to a payer, and no
152
+ keeper runs in the background. Unearned money comes back only when someone
153
+ calls the relay endpoints below (or calls the opener directly). What the relay
154
+ removes is the need for ETH: a typical x402 payer holds only USDC, so the
155
+ facilitator's relayer pays the gas for the two transactions a refund needs.
156
+
157
+ The escrow's buyer of record is the opener, so a refund goes through it:
158
+
159
+ 1. **Close** (optional — only to stop before `expiresAt`). `opener.close` is
160
+ payer-only on-chain, because anyone else could cut a payer's service. The
161
+ relay submits `opener.closeWithSig` with the payer's EIP-712 signature.
162
+ 2. **Reclaim**, once the settlement window allows it: a checkpoint covering the
163
+ stop time (close, or `expiresAt`) is posted — the oracle posts every
164
+ ~15–22 s — or `escapeGraceSeconds` (900 s on the live escrow) have passed
165
+ since the stop. `opener.reclaim(streamId, cumulativeAmount, proof)` is
166
+ permissionless: the escrow fixes the amount (a Merkle proof against its
167
+ **latest** checkpoint root) and the opener fixes the destination
168
+ (`payerOf(streamId)`). The relay fetches the proof from the oracle itself.
169
+ 3. **Escape hatch**, only if no usable proof ever appears: after
170
+ `expiresAt + escapeGraceSeconds` the payer may sign `ReclaimUnverified` and
171
+ the relay submits `reclaimUnverifiedWithSig`. It returns `deposit -
172
+ claimedCumulative` and forfeits the seller's earned-but-unclaimed revenue, so
173
+ it is **never** relayed without the payer's signature; the unsigned
174
+ `reclaimUnverified` is payer-only on-chain and the relay never calls it.
175
+
176
+ The streamId and the refund URL arrive with the paid response:
177
+ `PAYMENT-RESPONSE.extra.metrikStreamOpener = {streamId, opener, escrow,
178
+ deposit, ratePerSecond, expiresAt, refundUrl}`.
179
+
180
+ ### Endpoints (on the facilitator service; public, no bearer token)
181
+
182
+ | Method + path | Body | Does |
183
+ | -------------------------------------------- | ---------------------------------------------- | --------------------------------------------------------------- |
184
+ | `GET /streams/:streamId` | — | Status, and exactly what the payer can do next |
185
+ | `POST /streams/:streamId/close` | `{"deadline": "<unix s>", "signature": "0x…"}` | Relays `closeWithSig` (payer's EIP-712 `Close`) |
186
+ | `POST /streams/:streamId/reclaim` | none | Fetches the oracle proof, relays `reclaim` |
187
+ | `POST /streams/:streamId/reclaim-unverified` | `{"deadline": "<unix s>", "signature": "0x…"}` | Relays `reclaimUnverifiedWithSig` (payer's `ReclaimUnverified`) |
188
+
189
+ The payer signs, with the opener as the EIP-712 domain
190
+ (`{name: "Metrik X402StreamOpener", version: "1", chainId, verifyingContract:
191
+ <opener>}`):
192
+
193
+ ```
194
+ Close(bytes32 streamId,uint256 nonce,uint256 deadline)
195
+ ReclaimUnverified(bytes32 streamId,uint256 nonce,uint256 deadline)
196
+ ```
197
+
198
+ `nonce` is the payer's current `opener.nonces(payer)`; `deadline` must be at
199
+ least 60 s after the current chain time. `GET /streams/:streamId` returns the
200
+ ready-to-sign typed data (with the current nonce) under `next.typedData`, so a
201
+ client signs it as-is after choosing a deadline. An optional `"nonce"` in the
202
+ body turns a stale nonce into a clear `409 nonce-mismatch`.
203
+
204
+ `GET /streams/:streamId` answers `200` with:
205
+
206
+ ```json
207
+ {
208
+ "streamId": "0x…",
209
+ "chainId": 84532,
210
+ "escrow": "0x…",
211
+ "factory": "0x…",
212
+ "opener": "0x…",
213
+ "payer": "0x…",
214
+ "operator": "0x…",
215
+ "serviceRef": "0x…",
216
+ "deposit": "60000",
217
+ "ratePerSecond": "1000",
218
+ "claimedCumulative": "0",
219
+ "openedAt": 1790452888,
220
+ "expiresAt": 1790452948,
221
+ "closedAt": 1790452902,
222
+ "status": "closed",
223
+ "reclaimed": false,
224
+ "chainTime": 1790452910,
225
+ "settlement": {
226
+ "stopAt": 1790452902,
227
+ "latestCheckpointCheckedAt": 1790452904,
228
+ "checkpointCoversStop": true,
229
+ "fallbackAt": 1790453802,
230
+ "escapeHatchAt": 1790453848
231
+ },
232
+ "oracle": {
233
+ "cumulativeAmount": "14000",
234
+ "checkpointId": "61222",
235
+ "checkedAt": 1790452904,
236
+ "root": "0x…"
237
+ },
238
+ "reclaimable": "46000",
239
+ "payerNonce": "1",
240
+ "next": {
241
+ "action": "reclaim",
242
+ "method": "POST",
243
+ "path": "/streams/0x…/reclaim",
244
+ "message": "…"
245
+ },
246
+ "refunds": "There is no automatic refund. …"
247
+ }
248
+ ```
249
+
250
+ `closedAt` is `null` while active; `oracle` and `reclaimable` are `null` when
251
+ no proof that matches the escrow's on-chain latest root is available (with an
252
+ `oracleNote`). `next.action` is one of `close` (with `typedData` and the body
253
+ shape), `wait` (with `retryAt` / `retryAfterSeconds`), `reclaim`,
254
+ `reclaim-unverified` (with `typedData`), or `none`.
255
+
256
+ A successful relayed transaction answers `200` only from its **mined** receipt:
257
+
258
+ ```json
259
+ { "ok": true, "action": "close", "streamId": "0x…", "opener": "0x…",
260
+ "payer": "0x…", "transaction": "0x…", "message": "…", "next": { "path": "/streams/0x…/reclaim" } }
261
+ { "ok": true, "action": "reclaim", "streamId": "0x…", "opener": "0x…",
262
+ "payer": "0x…", "amount": "46000", "entitlement": "14000", "transaction": "0x…", "message": "…" }
263
+ ```
264
+
265
+ (`action` is `reclaim-unverified` for the escape hatch.) Every refusal is
266
+ `{"ok": false, "error": "<code>", "message": "…", …}`, with `Retry-After`
267
+ where a retry makes sense:
268
+
269
+ | Status | `error` | Meaning |
270
+ | ------ | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
271
+ | 400 | `invalid-stream-id`, `invalid-request`, `invalid-json` | Malformed input |
272
+ | 403 | `invalid-signature`, `signature-expired` | Not the payer's EIP-712 signature for this opener, stream and nonce; or deadline too near |
273
+ | 404 | `not-found` | Unknown stream, or its buyer is not an opener of the configured factory |
274
+ | 409 | `already-closed`, `stream-expired`, `already-reclaimed`, `nothing-to-reclaim`, `nonce-mismatch` | Would have no effect; nothing sent |
275
+ | 409 | `not-reclaimable-yet`, `settlement-window-open`, `proof-unavailable`, `escape-hatch-not-ready` | Not yet; `retryAt` (unix s) says when at the latest |
276
+ | 409 | `simulation-failed`, `in-progress`, `blocked` | The simulation reverted; a relayed tx is in flight; an earlier tx had an unexpected outcome |
277
+ | 202 | `pending` | Sent, not mined yet (`transaction`); repeat the same request — it is never re-sent |
278
+ | 429 | `rate-limited` | Per-IP, per-stream or global budget exhausted |
279
+ | 502 | `transaction-reverted`, `unexpected-receipt`, `submission-failed` | Never a success without the expected mined receipt |
280
+ | 503 | `chain-unavailable`, `oracle-unavailable`, `opener-domain-mismatch` | Fail closed |
281
+
282
+ ### Bounded against gas abuse
283
+
284
+ - **Opener streams only.** `factory.isOpener(stream.buyer)` is read on-chain
285
+ first; every other stream is `404` before any signature check, oracle call or
286
+ simulation.
287
+ - **No effect, no transaction.** Every precondition the contract checks is read
288
+ first (status, expiry, settlement window, `reclaimed`, a non-zero refund). A
289
+ reclaim's simulation must return exactly the non-zero `reclaimableFor`
290
+ amount. Nothing is broadcast without a successful simulation.
291
+ - **Proofs are checked, not trusted.** The oracle's proof must be for this
292
+ stream, verify under the escrow's leaf
293
+ `keccak256(bytes.concat(keccak256(abi.encode(streamId, cumulative))))`, and
294
+ its root must equal the escrow's on-chain `latestCheckpointRoot`. A tampered
295
+ proof (for example one claiming less delivery) is refused without a
296
+ transaction; the contract would refuse it anyway.
297
+ - **At most one successful close and one successful reclaim per stream** from
298
+ a facilitator instance (the proof and escape-hatch paths share the reclaim
299
+ slot, as the escrow allows one). Concurrent duplicates are refused
300
+ synchronously; a repeat after success is refused without any transaction.
301
+ - **Read fan-out is bounded.** Every `/streams` request (read or write) counts
302
+ against a per-client-IP limit AND an IP-independent global limit (default
303
+ 120/min, `429` + `Retry-After`). A stream's status and its oracle proof are
304
+ reused for a few seconds (default 5 s), so repeated requests for one stream
305
+ do not re-hit the RPC or the oracle; the cache for a stream is dropped as
306
+ soon as a relayed transaction for it is mined, and a broadcast is still
307
+ decided by a fresh simulation.
308
+ - **No internal errors leak.** RPC, oracle and simulation failures are
309
+ answered with a generic message and an error code (`chain-unavailable`,
310
+ `oracle-unavailable`, `simulation-failed`, `submission-failed`); the detail
311
+ is logged server-side with URL paths redacted, never the relayer key. Only
312
+ a decoded contract custom-error name (for example `StreamClosedAlready`) is
313
+ returned.
314
+ - **Budgets.** Per client IP (all `/streams` routes), per stream (relayed
315
+ submission attempts) and globally per minute. Refused requests — a forged
316
+ signature, a request with no effect — do not consume a stream's budget, so
317
+ spamming cannot lock a payer out.
318
+ - **Receipts.** Success only from a mined receipt with the escrow's
319
+ `StreamClosed` / `BuyerReclaimed` and the opener's `StreamCloseRequested` /
320
+ `ReclaimForwarded` for exactly this stream, payer and amount.
321
+ - **One relayer queue.** Relay submissions and `deployAndSettle` go through the
322
+ same serialized queue, so they never race for the relayer's nonce.
323
+
324
+ ## Running the facilitator
325
+
326
+ ```bash
327
+ METRIK_OPENER_FACILITATOR_ENABLED=1 \
328
+ METRIK_OPENER_FACILITATOR_RPC_URL=https://sepolia.base.org \
329
+ METRIK_OPENER_FACILITATOR_RELAYER_PRIVATE_KEY=<from your secret store> \
330
+ METRIK_OPENER_FACILITATOR_AUTH_TOKEN=<at least 32 random characters> \
331
+ npx metrik-x402-opener-facilitator
332
+ ```
333
+
334
+ | Variable | Default | Meaning |
335
+ | -------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------ |
336
+ | `METRIK_OPENER_FACILITATOR_ENABLED` | off | Must be truthy or the service refuses to start. |
337
+ | `METRIK_OPENER_FACILITATOR_RPC_URL` | required | RPC for reads, simulation and the settlement transaction. |
338
+ | `METRIK_OPENER_FACILITATOR_RELAYER_PRIVATE_KEY` | required | Pays gas for `deployAndSettle`. Never holds buyer funds; never logged. |
339
+ | `METRIK_OPENER_FACILITATOR_AUTH_TOKEN` | required (≥ 32 chars) | Bearer token required on `/settle`. |
340
+ | `METRIK_OPENER_FACILITATOR_FACTORY_ADDRESS` | the Base Sepolia factory | Required on any other chain. |
341
+ | `METRIK_OPENER_FACILITATOR_ESCROW_ADDRESS` | live Base Sepolia `StreamEscrowV2` | Must equal `factory.escrow()`. |
342
+ | `METRIK_OPENER_FACILITATOR_NETWORK` | `eip155:84532` | Must match the RPC's chain id. |
343
+ | `METRIK_OPENER_FACILITATOR_ASSETS` | Circle test USDC | Comma-separated allowlist; must contain `factory.usdc()`. |
344
+ | `METRIK_OPENER_FACILITATOR_REGISTRY_URL` | `https://oracle.metrik.live/listings` | Where operator-signed listings are read. |
345
+ | `METRIK_OPENER_FACILITATOR_LISTING_CACHE_SECONDS` | `60` | Reuse of a successful registry read (failures are never cached). |
346
+ | `METRIK_OPENER_FACILITATOR_RECEIPT_TIMEOUT_SECONDS` | `60` | Receipt wait before `settlement_pending`. |
347
+ | `METRIK_OPENER_FACILITATOR_REQUIRE_AUTH_ON_VERIFY` | **on** | Require the token on `/verify` too; `0` opts out explicitly. |
348
+ | `METRIK_OPENER_FACILITATOR_MIN_AMOUNT` | `10000` (0.01 USDC) | Smallest accepted payment; advertised in the marker. |
349
+ | `METRIK_OPENER_FACILITATOR_SETTLE_RATE_LIMIT_PER_MINUTE` | `60` | Authorized `/settle` calls per token per minute; beyond it `429`. |
350
+ | `METRIK_OPENER_FACILITATOR_HOST` / `_PORT` (or `PORT`) | `127.0.0.1` / `8788` | Bind address. Put TLS in front of it. |
351
+ | `METRIK_OPENER_FACILITATOR_REFUND_RELAY` | **on** | The gasless refund relay; `0` switches it off (a gateway then refuses to start). |
352
+ | `METRIK_OPENER_FACILITATOR_ORACLE_URL` | `https://oracle.metrik.live` | Serves `GET /checkpoint/proof?streamId=`. Required off Base Sepolia. |
353
+ | `METRIK_OPENER_FACILITATOR_PUBLIC_URL` | unset | Public origin, for absolute links (`next.url`) in relay responses. |
354
+ | `METRIK_OPENER_FACILITATOR_REFUND_RATE_LIMIT_PER_IP_PER_MINUTE` | `30` | `/streams/*` requests per client IP per minute. |
355
+ | `METRIK_OPENER_FACILITATOR_REFUND_GLOBAL_RATE_LIMIT_PER_MINUTE` | `120` | `/streams/*` requests per minute from all clients together (IP-independent). |
356
+ | `METRIK_OPENER_FACILITATOR_REFUND_READ_CACHE_SECONDS` | `5` | Reuse of one stream's status and oracle proof; `0` disables. |
357
+ | `METRIK_OPENER_FACILITATOR_REFUND_SUBMISSIONS_PER_STREAM_PER_MINUTE` | `3` | Relayed transactions per stream per minute. |
358
+ | `METRIK_OPENER_FACILITATOR_REFUND_SUBMISSIONS_PER_MINUTE` | `30` | Relayed refund transactions per minute, all streams. |
359
+ | `METRIK_OPENER_FACILITATOR_TRUST_PROXY` | off | Key the per-IP limit on the right-most `X-Forwarded-For` hop. See the warning below. |
360
+
361
+ > **`TRUST_PROXY=1` is only safe when the service is bound to loopback
362
+ > (`METRIK_OPENER_FACILITATOR_HOST=127.0.0.1`, the default) behind a reverse
363
+ > proxy that overwrites `X-Forwarded-For` with the address it actually saw
364
+ > (Caddy's `reverse_proxy` does).** Never enable it when the port is reachable
365
+ > directly: any client could then send its own `X-Forwarded-For` and get a
366
+ > fresh per-IP budget on every request. The global cap still applies either
367
+ > way.
368
+
369
+ At startup it proves the wiring (RPC chain id, factory code, `factory.escrow()`,
370
+ `factory.usdc()` allowlisted; for the relay, `factory.escrow()` and the escrow's
371
+ `escapeGraceSeconds`) and refuses to start otherwise. Endpoints:
372
+ `GET /supported`, `POST /verify`, `POST /settle` (bearer), `GET /health`, and
373
+ the refund relay's `GET /streams/:streamId` and
374
+ `POST /streams/:streamId/{close,reclaim,reclaim-unverified}` (public).
375
+
376
+ ## Running the gateway in x402 mode
377
+
378
+ The gateway's normal variables (`METRIK_GATEWAY_ESCROW_ADDRESS`,
379
+ `METRIK_GATEWAY_RPC_URL`, `METRIK_GATEWAY_SERVICE_REF`,
380
+ `METRIK_GATEWAY_UPSTREAM_URL`, …) are unchanged. The listing is the primary
381
+ `METRIK_GATEWAY_SERVICE_REF`. Add:
382
+
383
+ | Variable | Default | Meaning |
384
+ | ---------------------------------------------------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
385
+ | `METRIK_GATEWAY_X402V2_OPENER` | off | Truthy to enable the mode. |
386
+ | `METRIK_GATEWAY_X402V2_FACILITATOR_URL` | required | The Metrik opener facilitator. |
387
+ | `METRIK_GATEWAY_X402V2_FACILITATOR_TOKEN` | required | Its `/settle` bearer token. |
388
+ | `METRIK_GATEWAY_X402V2_OPERATOR` | required | Must be the operator that signed the listing. |
389
+ | `METRIK_GATEWAY_X402V2_RATE_PER_SECOND` | required | USDC base units per second. |
390
+ | `METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS` | required | Stream lifetime. |
391
+ | `METRIK_GATEWAY_X402V2_PRICE` | required | Base units per paid request (= deposit); `0 < price <= rate × maxDuration`. |
392
+ | `METRIK_GATEWAY_X402V2_FACTORY_ADDRESS` | the Base Sepolia factory | Required on any other chain. |
393
+ | `METRIK_GATEWAY_X402V2_NETWORK` | `eip155:<METRIK_GATEWAY_CHAIN_ID>` | Must match the chain id. |
394
+ | `METRIK_GATEWAY_X402V2_ASSET` / `_ASSET_NAME` / `_ASSET_VERSION` | Circle test USDC / `USDC` / `2` | Token and its EIP-712 domain. |
395
+ | `METRIK_GATEWAY_X402V2_MAX_TIMEOUT_SECONDS` | `300` | Authorization validity the client signs for. |
396
+ | `METRIK_GATEWAY_X402V2_FACILITATOR_TIMEOUT_SECONDS` | `120` | Per-request facilitator timeout; must exceed its receipt wait by 30 s. |
397
+ | `METRIK_GATEWAY_X402V2_RESOURCE_BASE_URL` | derived from `Host` | Public origin used in the 402 `resource.url`. |
398
+ | `METRIK_GATEWAY_X402V2_REGISTRY_URL` | the public registry | Where the listing is proven at startup. |
399
+ | `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`. |
400
+ | `METRIK_GATEWAY_X402V2_DOCS_URL` | this document on GitHub | Docs link in the 402. |
401
+ | `METRIK_GATEWAY_X402V2_ASSET_DECIMALS` | `6` | Decimals for the amounts in the 402 description. |
402
+ | `METRIK_GATEWAY_X402V2_DESCRIPTION` | unset | One sentence on what the service does, prefixed to the fixed disclosure. |
403
+ | `METRIK_GATEWAY_X402V2_SCHEMA` | unset | JSON `{info: {input, output}, schema}`, declared as `extensions.bazaar` (declared only, never registered). |
404
+
405
+ These names deliberately differ from the removed v1 mode's variables, so a
406
+ leftover v1 environment can never switch a payment mode on.
407
+
408
+ At startup the gateway validates the terms and price, proves the listing is
409
+ operator-signed, checks `factory.escrow()` and `factory.usdc()`, computes
410
+ `payTo = factory.predict(terms)`, and checks the facilitator's marker —
411
+ including `refundRelay: true`, because the 402 promises a refund path without
412
+ ETH — plus the marker at a separate refund base URL; any failure stops it from
413
+ starting. The advertised window is `METRIK_GATEWAY_X402V2_MAX_DURATION_SECONDS`
414
+ (for example `60`, with `RATE_PER_SECOND=1000` and `PRICE=60000`).
415
+
416
+ ### What the 402 says
417
+
418
+ The resource `description` states plainly what one payment buys; an operator
419
+ `DESCRIPTION` can only be prefixed to it. With a 60 s window at 1000 base
420
+ units/s and a price of 60000:
421
+
422
+ > Pays for one call. Your payment of 0.06 USDC funds a Metrik escrow stream of
423
+ > 60 seconds at 0.001 USDC/s; the seller earns only for seconds Metrik's
424
+ > verifier confirms the service is delivering, and anything unearned is
425
+ > refundable to you (no ETH needed) via
426
+ > https://&lt;facilitator&gt;/streams/{streamId}. Refunds are not automatic: close
427
+ > the stream and reclaim there, using the streamId from the PAYMENT-RESPONSE.
428
+ > Docs: &lt;docs URL&gt;
429
+
430
+ When the price is below `rate × window`, it adds "(the seller can earn at most
431
+ &lt;price&gt;)". `extra.metrikStreamOpener` also carries `refundUrl` (the
432
+ template) and `docsUrl`; the facilitator accepts exactly these two
433
+ informational http(s) links on top of the terms, never uses them for
434
+ settlement, and still refuses any other key. "Verified" here means verified
435
+ delivery, not verified output.
436
+
437
+ | Request carries | Result |
438
+ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------- |
439
+ | nothing | `402`, `PAYMENT-REQUIRED` quoting the opener `payTo` and the price |
440
+ | `PAYMENT-SIGNATURE` | verified → upstream → settled if upstream `< 400` → `200` + body + `PAYMENT-RESPONSE` |
441
+ | `PAYMENT-SIGNATURE`, upstream `>= 400` | upstream response, nothing settled |
442
+ | `PAYMENT-SIGNATURE`, settle fails | `402` + `PAYMENT-RESPONSE`, no upstream body |
443
+ | `x-metrik-capability` | the capability path, unchanged |
444
+ | capability **and** `PAYMENT-SIGNATURE` | `400 ambiguous-capability-and-payment` |
445
+ | probe capability **and** `PAYMENT-SIGNATURE` | `400 ambiguous-probe-mode` |
446
+ | any request while the facilitator marker cannot be proven | `503 x402-facilitator-unavailable` |
447
+ | a `PAYMENT-SIGNATURE` already in flight or already settled | `409 x402-payment-already-used`, no upstream call |
448
+
449
+ With `METRIK_GATEWAY_X402V2_OPENER` unset the gateway is byte for byte the
450
+ capability-only gateway (`test/gateway-http-baseline.test.ts`), and the
451
+ `@x402/*` packages are never loaded.
452
+
453
+ ## Paying as a buyer: `metrik pay`, MCP `x402_pay`, `@absol-labs/agent/x402-buyer`
454
+
455
+ The buyer side is the SDK's `payWithX402` (`@absol-labs/sdk/x402`) with every SDK
456
+ protection intact, wrapped by `payX402WithMandate` (`@absol-labs/agent/x402-buyer`)
457
+ so the package's golden rule holds: **no spend without a mandate**. A Metrik x402
458
+ payment opens a stream, so it is checked as the stream open it is:
459
+
460
+ | Mandate cap | x402 quote field |
461
+ | ---------------------- | --------------------------------------------- |
462
+ | `maxPerStreamUsdc` | `amount` (the deposit, the price of the call) |
463
+ | `maxTotalUsdc` | spent so far + `amount` |
464
+ | `maxRatePerSecondUsdc` | `ratePerSecond` |
465
+ | `maxDurationSeconds` | `maxDuration` |
466
+ | `allowedOperators` | `operator` |
467
+
468
+ The mandate's signature, expiry, revocation, owner (= the payer) and chain are
469
+ verified before the seller is contacted. The per-quote check runs in the SDK's
470
+ `onQuote` hook — after the SDK has proven `payTo == factory.predict(terms)` on
471
+ chain and before anything is signed — and the caps are also passed to the SDK as
472
+ `maxAmount` / `maxRatePerSecond`. A quote without opener terms is refused
473
+ (`X402MandateDeniedError` reason `not-an-opener-quote`).
474
+
475
+ ```bash
476
+ metrik pay https://x402.137.23.50.249.sslip.io/transform --json '{"text":"# hi"}' --max-amount 0.1
477
+ metrik refund <streamId> --refund-url <url printed above> --close --reclaim
478
+ ```
479
+
480
+ The MCP server offers the same as `x402_pay`, `refund_status` and `refund`
481
+ (see [mcp-reference.md](./mcp-reference.md)); `x402_pay` shares the server
482
+ mandate and cumulative spend with `hire_verified_service`. Refunds are still
483
+ **requested, never pushed**: `metrik refund` / `refund` call the relay; nothing
484
+ calls it for you.
485
+
486
+ The facilitator's `PAYMENT-RESPONSE` `extra.metrikStreamOpener.expiresAt` is a
487
+ JSON **number** (it was a decimal string before metrik-agent#121, which the SDK's
488
+ strict parser rejects after settlement — a facilitator deployed before that fix
489
+ must be redeployed).
490
+
491
+ ## Network configuration
492
+
493
+ Network, USDC (and its EIP-712 domain), escrow, opener factory and oracle all
494
+ default from the `@absol-labs/sdk` network registry entry for the configured
495
+ CAIP-2 network (`METRIK_OPENER_FACILITATOR_NETWORK`, `METRIK_GATEWAY_CHAIN_ID` /
496
+ `METRIK_GATEWAY_X402V2_NETWORK`); every one has an env override, and on a network
497
+ the registry does not know (or one with no opener factory) each must be given
498
+ explicitly. The refund relay computes the no-proof escape hatch per escrow
499
+ generation (`escrow.generation`): V2 `expiresAt + escapeGraceSeconds`, V3
500
+ `min(expiresAt, closedAt) + escapeGraceSeconds` read from the escrow's
501
+ `escapeHatchReadyAt` view.
502
+
503
+ ## Programmatic use
504
+
505
+ Everything lives under the `@absol-labs/agent/x402-opener` subpath (the root
506
+ barrel does not load `@x402/*`): `MetrikOpenerExactScheme`,
507
+ `buildOpenerFacilitator`, `createOpenerFacilitatorServer`, `OpenerRefundRelay`,
508
+ `openerActionTypedData`, `checkpointLeaf`, `RelayerQueue`,
509
+ `RegistryListingResolver`, `readOpenerFacilitatorMarker`, the vendored
510
+ `x402StreamOpenerFactoryAbi` / `x402StreamOpenerAbi`, and the terms helpers.
511
+ The gateway mode is `buildGatewayX402OpenerMode` in
512
+ `@absol-labs/agent/gateway/x402-opener-mode`, passed to
513
+ `createCallerAuthGatewayServer({ x402Opener })`.
514
+
515
+ ## Honest limits
516
+
517
+ - **Refunds are requested, never pushed.** There is no automatic refund: the
518
+ relay acts only when an endpoint is called, and a payer who never calls it
519
+ (or never has anyone call `reclaim` for them) gets nothing back. `metrik
520
+ refund` and the MCP `refund` tool call it on request; `metrik close` /
521
+ `metrik reclaim` also route an opener stream through the opener directly.
522
+ - **The relay depends on the oracle for proofs.** If the oracle's latest
523
+ checkpoint never includes the stream, the proof-based reclaim cannot run and
524
+ the payer-signed escape hatch (after `expiresAt + escapeGraceSeconds`) is the
525
+ way out. The relay ledgers are in-memory and per instance, like the
526
+ settlement ledger; the on-chain contracts still allow only one close and one
527
+ reclaim per stream regardless.
528
+ - **Stray funds are unrecoverable**: USDC that reaches an opener any way other
529
+ than `deployAndSettle` / `settleAndOpen` stays there (see the hard rule).
530
+ - **Blacklist blast radius**: one opener is the escrow buyer for every payer of
531
+ a listing's terms; if the token issuer blacklists it, refunds through it
532
+ freeze until it is un-blacklisted.
533
+ - **ERC-6492 counterfactual wallets are refused**; deploy the wallet first. A
534
+ deployed ERC-1271 wallet works (USDC verifies it).
535
+ - **Single instance**: one relayer, and the at-most-one-success ledgers are
536
+ in-memory per instance (see above). Do not run replicas.
537
+ - **Listing freshness**: a successful registry read is reused for 60 s by
538
+ default, so a withdrawn listing can keep verifying for up to that long.
539
+ Discovery prefers a gated listing over a public one for the same operator and
540
+ origin; a listing hidden that way does not resolve (fail closed).
541
+
542
+ ---
543
+
544
+ ## History: the removed x402 v1 integration
545
+
546
+ Until metrik-agent#115 this package shipped an x402 **v1** integration: a
547
+ standard `exact` facilitator (`metrik-x402-facilitator`, exported as
548
+ `@absol-labs/agent/x402-facilitator`) that consulted Metrik's oracle before
549
+ settling, and an opt-in mode on the seller gateway that answered an
550
+ uncapabilitied request with an x402 `402`. It was removed, before it ever ran in
551
+ production, for four reasons:
552
+
553
+ 1. **It targeted a superseded protocol version.** It was built on `x402@1.2.0`
554
+ (`x402Version: 1`, `X-PAYMENT` / `X-PAYMENT-RESPONSE`, bare network names
555
+ such as `base-sepolia`). The protocol is at v2 (`PAYMENT-REQUIRED` /
556
+ `PAYMENT-SIGNATURE` / `PAYMENT-RESPONSE`, CAIP-2 network identifiers such as
557
+ `eip155:84532`).
558
+ 2. **It paid the seller directly.** `payTo` was the operator, so the money never
559
+ touched `StreamEscrowV2`: no escrow, no protocol fee, and nothing a buyer
560
+ could get back.
561
+ 3. **Its delivery gate gated nothing in practice.** On absent evidence it
562
+ defaulted to _allow_, and the live oracle's `GET /services` is built from
563
+ active streams, which was empty — so every payment would have passed.
564
+ 4. **Leaving it in place invited someone to enable it.** It was default-off
565
+ behind environment switches and had never broadcast a settlement.
566
+
567
+ What changed for integrators:
568
+
569
+ - The `metrik-x402-facilitator` bin, the `./x402-facilitator` subpath export,
570
+ the `x402:facilitator` script and every module behind them are gone.
571
+ - The seller gateway no longer reads any `METRIK_GATEWAY_X402_*` variable, and
572
+ the facilitator's `METRIK_X402_*` variables have no reader. A gateway that
573
+ never set them behaves exactly as before: a request with no capability gets
574
+ `403 {"authorized":false,"reason":"missing-capability-header"}`, and a request
575
+ with a valid capability is proxied unchanged
576
+ (`test/gateway-http-baseline.test.ts` pins this over a real socket).
577
+ - A request carrying an oracle probe capability **and** an `X-PAYMENT` header is
578
+ still refused with `400 ambiguous-probe-mode` (and, in the v2 mode, so is a
579
+ probe carrying `PAYMENT-SIGNATURE`). A probe is free verification
580
+ traffic and is never mixed with anything that looks like a payment.
581
+ - The deprecated `X402*` / `x402*` aliases of the native flow's exports were
582
+ removed at the same time. Use the canonical names listed below.
583
+
584
+ `test/x402-v1-removed.test.ts` fails if any source, test, script or example
585
+ imports the v1 `x402` / `x402-fetch` packages again (the v2 `@x402/*` packages
586
+ are what the opener path uses), or if `package.json` re-declares them or the
587
+ removed bin, script or export. `scripts/check-packed-package.mjs` refuses to
588
+ publish a tarball that still contains any removed artifact.
589
+
590
+ ---
591
+
592
+ ## The Metrik-native verified-stream `402` flow — NOT x402
593
+
594
+ In [`src/x402/facilitator.ts`](../src/x402/facilitator.ts) (the directory name is
595
+ historical):
596
+
597
+ - scheme string **`streamproof-verified-stream`** — deliberately _not_ an x402
598
+ scheme name, and not registered with x402;
599
+ - a challenge body of `{scheme, requirements}` where `requirements` is
600
+ Metrik-specific (`operator`, `serviceRef`, `ratePerSecondUsdc`,
601
+ `maxDurationSeconds`, `maxBudgetUsdc`, `escrow`, `chainId`) — **not** x402
602
+ `PaymentRequirements`;
603
+ - a payload header of `` `${scheme} ${base64url(JSON)}` `` carrying the challenge
604
+ plus an owner-signed `SpendMandate` — **not** an x402 `PaymentPayload`;
605
+ - no `/verify`, no `/settle`, no `/supported` endpoint.
606
+
607
+ This flow is real, it is tested, and it opens real escrowed streams on Base
608
+ Sepolia. It does not interoperate with `x402-fetch`, `x402-axios`,
609
+ `x402-express` or any other x402 client or facilitator.
610
+
611
+ Exports: `VerifiedStreamFacilitator`, `METRIK_STREAM_SCHEME`,
612
+ `METRIK_PAYMENT_HEADER`, `encodeStreamPayloadHeader`,
613
+ `parseStreamPayloadHeader`, `parseStreamChallengeJson`, `streamChallengeSchema`,
614
+ `streamOpenPayloadSchema`, `verifiedStreamRequirementsSchema`,
615
+ `StreamPayloadError`, `SpendControlUnavailableError`,
616
+ `SettlementTargetMismatchError`, `DeliveryProofUnavailableError`, and the types
617
+ `StreamFacilitator`, `StreamChallenge`, `StreamOpenPayload`,
618
+ `ConsumedHttpsResponseProofInput`, `VerifiedStreamFacilitatorOptions`,
619
+ `VerifiedStreamRequirements`.
620
+
621
+ Two runtime error names still carry the old prefix: a `StreamPayloadError` has
622
+ `.name === "X402PayloadError"` and a `SpendControlUnavailableError` has
623
+ `.name === "X402SpendControlUnavailableError"`. They were left unchanged so code
624
+ that matches on `error.name` keeps working; prefer `instanceof`.
625
+
626
+ ### The header name
627
+
628
+ Metrik's payload travels as **`X-METRIK-PAYMENT`**. Metrik puts nothing on the
629
+ x402 v1 `X-PAYMENT` header, and `X-METRIK-PAYMENT` also stays clear of the v2
630
+ header names (`PAYMENT-REQUIRED`, `PAYMENT-SIGNATURE`, `PAYMENT-RESPONSE`).
631
+
632
+ ---
633
+
634
+ ## Why the metered stream does not fit a shipped x402 scheme
635
+
636
+ This analysis is why "support x402" was never a wire-format exercise for the
637
+ stream itself. Package facts were gathered on **2026-09-18** from
638
+ `x402-foundation/x402@main` and from `x402@1.2.0` as published on npm; the test
639
+ that re-checked them against the real package was removed with the dependency
640
+ in #115, so re-verify before relying on them.
641
+
642
+ | Thing | Reality at the date above |
643
+ | ----------------------------------- | ----------------------------------------------------------------- |
644
+ | Spec repository | [`x402-foundation/x402`](https://github.com/x402-foundation/x402) |
645
+ | Schemes implemented by `x402@1.2.0` | **`exact` only** |
646
+ | Schemes that exist _in the spec_ | `exact`, `upto`, `batch-settlement`, `auth-capture` |
647
+ | Protocol versions in `x402@1.2.0` | `x402Version: 1` only; v2 was specified, not yet in that package |
648
+
649
+ ### `exact`
650
+
651
+ `exact` on EVM is an [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009)
652
+ `transferWithAuthorization`: one fixed amount, one transfer, straight to
653
+ `payTo`. No hold, no conditional release, no partial capture, no reclaim.
654
+
655
+ Metrik's unit of payment is a **metered stream**: the buyer escrows a budget
656
+ into `StreamEscrowV2` at a per-second rate with an expiry, a verifier probes the
657
+ seller each interval, the seller earns only for intervals that verified
658
+ (claimed against Merkle checkpoints), and whatever was never earned is
659
+ recoverable by the buyer via a withdrawal transaction. Collapsing that into one
660
+ up-front transfer to the seller deletes the product — which is exactly what the
661
+ removed v1 integration did, and why it went. The opener path at the top of this
662
+ document keeps the `exact` payment but points its `payTo` at an opener that
663
+ opens the stream, instead of at the seller.
664
+
665
+ ### `upto`
666
+
667
+ `upto` (authorize a maximum, settle actual usage) fits a single metered
668
+ _request_, but its spec explicitly puts "multi-settlement / streaming" out of
669
+ scope and requires each authorization to be settled at most once. A Metrik
670
+ stream settles the same authorization repeatedly, once per verified checkpoint.
671
+
672
+ ### `auth-capture`
673
+
674
+ `auth-capture` in its `escrow` flow is the closest conceptual match —
675
+ `authorize`/`capture`/`void`/`reclaim` map onto `openStream`/`claim`/`close`/
676
+ `reclaim` — but its EVM binding is pinned to the canonical `AuthCaptureEscrow`
677
+ contract (Base's Commerce Payments Protocol), which a conformant facilitator
678
+ must read and whose events it must observe. Metrik settles in `StreamEscrowV2`
679
+ (`0x0f09f36Ccc05A7c9882F438721C08De314dFd46C`), a different contract with a
680
+ different state model, so conformance there would be a contract deployment, not
681
+ a code change in this package.