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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/README.md +43 -51
  2. package/dist/capability/invocation-capability.d.ts +2 -2
  3. package/dist/capability/probe-capability.d.ts +225 -0
  4. package/dist/capability/probe-capability.d.ts.map +1 -0
  5. package/dist/capability/probe-capability.js +248 -0
  6. package/dist/capability/probe-capability.js.map +1 -0
  7. package/dist/cli/args.d.ts +11 -0
  8. package/dist/cli/args.d.ts.map +1 -1
  9. package/dist/cli/args.js +23 -0
  10. package/dist/cli/args.js.map +1 -1
  11. package/dist/cli/commands/claim.d.ts +32 -0
  12. package/dist/cli/commands/claim.d.ts.map +1 -1
  13. package/dist/cli/commands/claim.js +116 -28
  14. package/dist/cli/commands/claim.js.map +1 -1
  15. package/dist/cli/commands/close.d.ts.map +1 -1
  16. package/dist/cli/commands/close.js +35 -3
  17. package/dist/cli/commands/close.js.map +1 -1
  18. package/dist/cli/commands/invoke.d.ts +2 -0
  19. package/dist/cli/commands/invoke.d.ts.map +1 -1
  20. package/dist/cli/commands/invoke.js +2 -2
  21. package/dist/cli/commands/invoke.js.map +1 -1
  22. package/dist/cli/commands/mcp.d.ts.map +1 -1
  23. package/dist/cli/commands/mcp.js +15 -2
  24. package/dist/cli/commands/mcp.js.map +1 -1
  25. package/dist/cli/commands/open.d.ts.map +1 -1
  26. package/dist/cli/commands/open.js +58 -4
  27. package/dist/cli/commands/open.js.map +1 -1
  28. package/dist/cli/commands/pay.d.ts +34 -0
  29. package/dist/cli/commands/pay.d.ts.map +1 -0
  30. package/dist/cli/commands/pay.js +479 -0
  31. package/dist/cli/commands/pay.js.map +1 -0
  32. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
  33. package/dist/cli/commands/reclaim-unverified.js +3 -2
  34. package/dist/cli/commands/reclaim-unverified.js.map +1 -1
  35. package/dist/cli/commands/reclaim.d.ts +23 -2
  36. package/dist/cli/commands/reclaim.d.ts.map +1 -1
  37. package/dist/cli/commands/reclaim.js +204 -20
  38. package/dist/cli/commands/reclaim.js.map +1 -1
  39. package/dist/cli/commands/refund.d.ts +31 -0
  40. package/dist/cli/commands/refund.d.ts.map +1 -0
  41. package/dist/cli/commands/refund.js +397 -0
  42. package/dist/cli/commands/refund.js.map +1 -0
  43. package/dist/cli/commands/status.d.ts.map +1 -1
  44. package/dist/cli/commands/status.js +21 -2
  45. package/dist/cli/commands/status.js.map +1 -1
  46. package/dist/cli/commands/streams.js +8 -12
  47. package/dist/cli/commands/streams.js.map +1 -1
  48. package/dist/cli/commands/watch.d.ts.map +1 -1
  49. package/dist/cli/commands/watch.js +4 -1
  50. package/dist/cli/commands/watch.js.map +1 -1
  51. package/dist/cli/config.d.ts +44 -26
  52. package/dist/cli/config.d.ts.map +1 -1
  53. package/dist/cli/config.js +88 -38
  54. package/dist/cli/config.js.map +1 -1
  55. package/dist/cli/context.d.ts +19 -0
  56. package/dist/cli/context.d.ts.map +1 -1
  57. package/dist/cli/deps.d.ts +53 -0
  58. package/dist/cli/deps.d.ts.map +1 -1
  59. package/dist/cli/deps.js +51 -1
  60. package/dist/cli/deps.js.map +1 -1
  61. package/dist/cli/errors.d.ts.map +1 -1
  62. package/dist/cli/errors.js +21 -1
  63. package/dist/cli/errors.js.map +1 -1
  64. package/dist/cli/escape-hatch.d.ts +6 -4
  65. package/dist/cli/escape-hatch.d.ts.map +1 -1
  66. package/dist/cli/escape-hatch.js +6 -2
  67. package/dist/cli/escape-hatch.js.map +1 -1
  68. package/dist/cli/listings.d.ts +18 -0
  69. package/dist/cli/listings.d.ts.map +1 -1
  70. package/dist/cli/listings.js +22 -12
  71. package/dist/cli/listings.js.map +1 -1
  72. package/dist/cli/preflight.d.ts +20 -14
  73. package/dist/cli/preflight.d.ts.map +1 -1
  74. package/dist/cli/preflight.js +34 -20
  75. package/dist/cli/preflight.js.map +1 -1
  76. package/dist/cli/privy.d.ts.map +1 -1
  77. package/dist/cli/privy.js +4 -2
  78. package/dist/cli/privy.js.map +1 -1
  79. package/dist/cli/progress.d.ts +13 -1
  80. package/dist/cli/progress.d.ts.map +1 -1
  81. package/dist/cli/progress.js +7 -1
  82. package/dist/cli/progress.js.map +1 -1
  83. package/dist/cli/reclaimability.d.ts +82 -0
  84. package/dist/cli/reclaimability.d.ts.map +1 -0
  85. package/dist/cli/reclaimability.js +80 -0
  86. package/dist/cli/reclaimability.js.map +1 -0
  87. package/dist/cli/run.d.ts +2 -1
  88. package/dist/cli/run.d.ts.map +1 -1
  89. package/dist/cli/run.js +62 -21
  90. package/dist/cli/run.js.map +1 -1
  91. package/dist/cli/scan-range.d.ts +2 -2
  92. package/dist/cli/scan-range.js +2 -2
  93. package/dist/cli/status.d.ts +106 -1
  94. package/dist/cli/status.d.ts.map +1 -1
  95. package/dist/cli/status.js +325 -44
  96. package/dist/cli/status.js.map +1 -1
  97. package/dist/cli/wallet.d.ts +2 -2
  98. package/dist/cli/wallet.d.ts.map +1 -1
  99. package/dist/cli/wallet.js +29 -5
  100. package/dist/cli/wallet.js.map +1 -1
  101. package/dist/discovery/rate-band.d.ts +74 -0
  102. package/dist/discovery/rate-band.d.ts.map +1 -0
  103. package/dist/discovery/rate-band.js +114 -0
  104. package/dist/discovery/rate-band.js.map +1 -0
  105. package/dist/escrow-generation.d.ts +81 -0
  106. package/dist/escrow-generation.d.ts.map +1 -0
  107. package/dist/escrow-generation.js +76 -0
  108. package/dist/escrow-generation.js.map +1 -0
  109. package/dist/frameworks/agentkit.d.ts +8 -4
  110. package/dist/frameworks/agentkit.d.ts.map +1 -1
  111. package/dist/frameworks/agentkit.js +27 -10
  112. package/dist/frameworks/agentkit.js.map +1 -1
  113. package/dist/frameworks/eliza.d.ts +3 -3
  114. package/dist/frameworks/eliza.d.ts.map +1 -1
  115. package/dist/frameworks/eliza.js +15 -6
  116. package/dist/frameworks/eliza.js.map +1 -1
  117. package/dist/frameworks/langchain.d.ts.map +1 -1
  118. package/dist/frameworks/langchain.js +8 -0
  119. package/dist/frameworks/langchain.js.map +1 -1
  120. package/dist/gateway/caller-auth-gateway.d.ts +83 -31
  121. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  122. package/dist/gateway/caller-auth-gateway.js +180 -27
  123. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  124. package/dist/gateway/http-server.d.ts +128 -44
  125. package/dist/gateway/http-server.d.ts.map +1 -1
  126. package/dist/gateway/http-server.js +506 -155
  127. package/dist/gateway/http-server.js.map +1 -1
  128. package/dist/gateway/probe-auth.d.ts +112 -0
  129. package/dist/gateway/probe-auth.d.ts.map +1 -0
  130. package/dist/gateway/probe-auth.js +127 -0
  131. package/dist/gateway/probe-auth.js.map +1 -0
  132. package/dist/gateway/signed-rate-bounds.d.ts +51 -0
  133. package/dist/gateway/signed-rate-bounds.d.ts.map +1 -0
  134. package/dist/gateway/signed-rate-bounds.js +94 -0
  135. package/dist/gateway/signed-rate-bounds.js.map +1 -0
  136. package/dist/gateway/x402-opener-mode.d.ts +252 -0
  137. package/dist/gateway/x402-opener-mode.d.ts.map +1 -0
  138. package/dist/gateway/x402-opener-mode.js +772 -0
  139. package/dist/gateway/x402-opener-mode.js.map +1 -0
  140. package/dist/index.d.ts +6 -2
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +10 -5
  143. package/dist/index.js.map +1 -1
  144. package/dist/mcp/server.d.ts +64 -1
  145. package/dist/mcp/server.d.ts.map +1 -1
  146. package/dist/mcp/server.js +247 -37
  147. package/dist/mcp/server.js.map +1 -1
  148. package/dist/mcp/x402-tools.d.ts +109 -0
  149. package/dist/mcp/x402-tools.d.ts.map +1 -0
  150. package/dist/mcp/x402-tools.js +554 -0
  151. package/dist/mcp/x402-tools.js.map +1 -0
  152. package/dist/network.d.ts +119 -0
  153. package/dist/network.d.ts.map +1 -0
  154. package/dist/network.js +199 -0
  155. package/dist/network.js.map +1 -0
  156. package/dist/sdk/client.d.ts +13 -0
  157. package/dist/sdk/client.d.ts.map +1 -1
  158. package/dist/sdk/client.js +15 -0
  159. package/dist/sdk/client.js.map +1 -1
  160. package/dist/sdk/invoke.d.ts +4 -0
  161. package/dist/sdk/invoke.d.ts.map +1 -1
  162. package/dist/sdk/invoke.js +2 -5
  163. package/dist/sdk/invoke.js.map +1 -1
  164. package/dist/wallet/autonomous-wallet-broker.d.ts +3 -0
  165. package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -1
  166. package/dist/wallet/autonomous-wallet-broker.js +27 -23
  167. package/dist/wallet/autonomous-wallet-broker.js.map +1 -1
  168. package/dist/wallet/autonomous-wallet-protocol.d.ts +8 -2
  169. package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -1
  170. package/dist/wallet/autonomous-wallet-protocol.js +9 -2
  171. package/dist/wallet/autonomous-wallet-protocol.js.map +1 -1
  172. package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
  173. package/dist/wallet/autonomous-wallet.js +3 -2
  174. package/dist/wallet/autonomous-wallet.js.map +1 -1
  175. package/dist/wallet/broker-escrows.d.ts +222 -0
  176. package/dist/wallet/broker-escrows.d.ts.map +1 -0
  177. package/dist/wallet/broker-escrows.js +36 -0
  178. package/dist/wallet/broker-escrows.js.map +1 -0
  179. package/dist/wallet/lifecycle.d.ts +21 -5
  180. package/dist/wallet/lifecycle.d.ts.map +1 -1
  181. package/dist/wallet/lifecycle.js +32 -12
  182. package/dist/wallet/lifecycle.js.map +1 -1
  183. package/dist/wallet/privy-broker-server.d.ts.map +1 -1
  184. package/dist/wallet/privy-broker-server.js +31 -26
  185. package/dist/wallet/privy-broker-server.js.map +1 -1
  186. package/dist/wallet/privy-session-broker.d.ts +14 -5
  187. package/dist/wallet/privy-session-broker.d.ts.map +1 -1
  188. package/dist/wallet/privy-session-broker.js +29 -17
  189. package/dist/wallet/privy-session-broker.js.map +1 -1
  190. package/dist/wallet/privy-session-provider.d.ts +3 -1
  191. package/dist/wallet/privy-session-provider.d.ts.map +1 -1
  192. package/dist/wallet/privy-session-provider.js +5 -3
  193. package/dist/wallet/privy-session-provider.js.map +1 -1
  194. package/dist/wallet/provider.d.ts.map +1 -1
  195. package/dist/wallet/provider.js +15 -8
  196. package/dist/wallet/provider.js.map +1 -1
  197. package/dist/x402/facilitator.d.ts +3 -30
  198. package/dist/x402/facilitator.d.ts.map +1 -1
  199. package/dist/x402/facilitator.js +0 -27
  200. package/dist/x402/facilitator.js.map +1 -1
  201. package/dist/x402-buyer/guarded-fetch.d.ts +51 -0
  202. package/dist/x402-buyer/guarded-fetch.d.ts.map +1 -0
  203. package/dist/x402-buyer/guarded-fetch.js +147 -0
  204. package/dist/x402-buyer/guarded-fetch.js.map +1 -0
  205. package/dist/x402-buyer/index.d.ts +10 -0
  206. package/dist/x402-buyer/index.d.ts.map +1 -0
  207. package/dist/x402-buyer/index.js +10 -0
  208. package/dist/x402-buyer/index.js.map +1 -0
  209. package/dist/x402-buyer/pay.d.ts +157 -0
  210. package/dist/x402-buyer/pay.d.ts.map +1 -0
  211. package/dist/x402-buyer/pay.js +366 -0
  212. package/dist/x402-buyer/pay.js.map +1 -0
  213. package/dist/x402-opener/abi.d.ts +684 -0
  214. package/dist/x402-opener/abi.d.ts.map +1 -0
  215. package/dist/x402-opener/abi.js +880 -0
  216. package/dist/x402-opener/abi.js.map +1 -0
  217. package/dist/x402-opener/facilitator-entry.d.ts +3 -0
  218. package/dist/x402-opener/facilitator-entry.d.ts.map +1 -0
  219. package/dist/x402-opener/facilitator-entry.js +40 -0
  220. package/dist/x402-opener/facilitator-entry.js.map +1 -0
  221. package/dist/x402-opener/facilitator-server.d.ts +209 -0
  222. package/dist/x402-opener/facilitator-server.d.ts.map +1 -0
  223. package/dist/x402-opener/facilitator-server.js +762 -0
  224. package/dist/x402-opener/facilitator-server.js.map +1 -0
  225. package/dist/x402-opener/index.d.ts +14 -0
  226. package/dist/x402-opener/index.d.ts.map +1 -0
  227. package/dist/x402-opener/index.js +14 -0
  228. package/dist/x402-opener/index.js.map +1 -0
  229. package/dist/x402-opener/listing-resolver.d.ts +55 -0
  230. package/dist/x402-opener/listing-resolver.d.ts.map +1 -0
  231. package/dist/x402-opener/listing-resolver.js +125 -0
  232. package/dist/x402-opener/listing-resolver.js.map +1 -0
  233. package/dist/x402-opener/refund-relay.d.ts +266 -0
  234. package/dist/x402-opener/refund-relay.d.ts.map +1 -0
  235. package/dist/x402-opener/refund-relay.js +1341 -0
  236. package/dist/x402-opener/refund-relay.js.map +1 -0
  237. package/dist/x402-opener/relayer-queue.d.ts +14 -0
  238. package/dist/x402-opener/relayer-queue.d.ts.map +1 -0
  239. package/dist/x402-opener/relayer-queue.js +18 -0
  240. package/dist/x402-opener/relayer-queue.js.map +1 -0
  241. package/dist/x402-opener/scheme.d.ts +296 -0
  242. package/dist/x402-opener/scheme.d.ts.map +1 -0
  243. package/dist/x402-opener/scheme.js +1035 -0
  244. package/dist/x402-opener/scheme.js.map +1 -0
  245. package/dist/x402-opener/terms.d.ts +165 -0
  246. package/dist/x402-opener/terms.d.ts.map +1 -0
  247. package/dist/x402-opener/terms.js +304 -0
  248. package/dist/x402-opener/terms.js.map +1 -0
  249. package/dist/zktls/reclaim.d.ts +2 -2
  250. package/dist/zktls/t2-delivery-proof.d.ts +6 -6
  251. package/docs/agent-layer.md +3 -2
  252. package/docs/autonomous-privy-wallet.md +188 -0
  253. package/docs/cli.md +265 -34
  254. package/docs/langchain.md +5 -0
  255. package/docs/mcp-reference.md +91 -5
  256. package/docs/probe-capability.md +191 -0
  257. package/docs/quickstart.md +39 -4
  258. package/docs/threat-model.md +20 -0
  259. package/docs/x402.md +766 -593
  260. package/package.json +19 -17
  261. package/src/capability/probe-capability.ts +355 -0
  262. package/src/cli/args.ts +29 -0
  263. package/src/cli/commands/claim.ts +168 -33
  264. package/src/cli/commands/close.ts +36 -3
  265. package/src/cli/commands/invoke.ts +5 -2
  266. package/src/cli/commands/mcp.ts +18 -2
  267. package/src/cli/commands/open.ts +75 -4
  268. package/src/cli/commands/pay.ts +584 -0
  269. package/src/cli/commands/reclaim-unverified.ts +8 -2
  270. package/src/cli/commands/reclaim.ts +277 -19
  271. package/src/cli/commands/refund.ts +475 -0
  272. package/src/cli/commands/status.ts +28 -2
  273. package/src/cli/commands/streams.ts +10 -14
  274. package/src/cli/commands/watch.ts +11 -1
  275. package/src/cli/config.ts +139 -52
  276. package/src/cli/context.ts +16 -0
  277. package/src/cli/deps.ts +111 -1
  278. package/src/cli/errors.ts +24 -1
  279. package/src/cli/escape-hatch.ts +19 -5
  280. package/src/cli/listings.ts +43 -10
  281. package/src/cli/preflight.ts +36 -21
  282. package/src/cli/privy.ts +4 -2
  283. package/src/cli/progress.ts +21 -2
  284. package/src/cli/reclaimability.ts +154 -0
  285. package/src/cli/run.ts +77 -21
  286. package/src/cli/scan-range.ts +2 -2
  287. package/src/cli/status.ts +454 -44
  288. package/src/cli/wallet.ts +45 -8
  289. package/src/discovery/rate-band.ts +172 -0
  290. package/src/escrow-generation.ts +135 -0
  291. package/src/frameworks/agentkit.ts +45 -11
  292. package/src/frameworks/eliza.ts +27 -5
  293. package/src/frameworks/langchain.ts +14 -0
  294. package/src/gateway/caller-auth-gateway.ts +320 -68
  295. package/src/gateway/http-server.ts +717 -220
  296. package/src/gateway/probe-auth.ts +191 -0
  297. package/src/gateway/signed-rate-bounds.ts +164 -0
  298. package/src/gateway/x402-opener-mode.ts +1171 -0
  299. package/src/index.ts +82 -19
  300. package/src/mcp/server.ts +351 -42
  301. package/src/mcp/x402-tools.ts +785 -0
  302. package/src/network.ts +302 -0
  303. package/src/sdk/client.ts +25 -0
  304. package/src/sdk/invoke.ts +6 -5
  305. package/src/wallet/autonomous-wallet-broker.ts +46 -20
  306. package/src/wallet/autonomous-wallet-protocol.ts +11 -2
  307. package/src/wallet/autonomous-wallet.ts +4 -2
  308. package/src/wallet/broker-escrows.ts +66 -0
  309. package/src/wallet/lifecycle.ts +53 -18
  310. package/src/wallet/privy-broker-server.ts +35 -25
  311. package/src/wallet/privy-session-broker.ts +58 -25
  312. package/src/wallet/privy-session-provider.ts +7 -3
  313. package/src/wallet/provider.ts +20 -15
  314. package/src/x402/facilitator.ts +3 -43
  315. package/src/x402-buyer/guarded-fetch.ts +213 -0
  316. package/src/x402-buyer/index.ts +9 -0
  317. package/src/x402-buyer/pay.ts +555 -0
  318. package/src/x402-opener/abi.ts +881 -0
  319. package/src/x402-opener/facilitator-entry.ts +48 -0
  320. package/src/x402-opener/facilitator-server.ts +1114 -0
  321. package/src/x402-opener/index.ts +13 -0
  322. package/src/x402-opener/listing-resolver.ts +202 -0
  323. package/src/x402-opener/refund-relay.ts +1873 -0
  324. package/src/x402-opener/relayer-queue.ts +18 -0
  325. package/src/x402-opener/scheme.ts +1551 -0
  326. package/src/x402-opener/terms.ts +453 -0
  327. package/dist/gateway/x402-mode.d.ts +0 -123
  328. package/dist/gateway/x402-mode.d.ts.map +0 -1
  329. package/dist/gateway/x402-mode.js +0 -212
  330. package/dist/gateway/x402-mode.js.map +0 -1
  331. package/dist/x402/delivery-oracle.d.ts +0 -108
  332. package/dist/x402/delivery-oracle.d.ts.map +0 -1
  333. package/dist/x402/delivery-oracle.js +0 -174
  334. package/dist/x402/delivery-oracle.js.map +0 -1
  335. package/dist/x402/facilitator-server-entry.d.ts +0 -3
  336. package/dist/x402/facilitator-server-entry.d.ts.map +0 -1
  337. package/dist/x402/facilitator-server-entry.js +0 -27
  338. package/dist/x402/facilitator-server-entry.js.map +0 -1
  339. package/dist/x402/facilitator-server.d.ts +0 -73
  340. package/dist/x402/facilitator-server.d.ts.map +0 -1
  341. package/dist/x402/facilitator-server.js +0 -305
  342. package/dist/x402/facilitator-server.js.map +0 -1
  343. package/dist/x402/payment-requirements.d.ts +0 -106
  344. package/dist/x402/payment-requirements.d.ts.map +0 -1
  345. package/dist/x402/payment-requirements.js +0 -138
  346. package/dist/x402/payment-requirements.js.map +0 -1
  347. package/dist/x402/relayer.d.ts +0 -41
  348. package/dist/x402/relayer.d.ts.map +0 -1
  349. package/dist/x402/relayer.js +0 -64
  350. package/dist/x402/relayer.js.map +0 -1
  351. package/dist/x402/resource-index.d.ts +0 -75
  352. package/dist/x402/resource-index.d.ts.map +0 -1
  353. package/dist/x402/resource-index.js +0 -126
  354. package/dist/x402/resource-index.js.map +0 -1
  355. package/dist/x402/verified-delivery-facilitator.d.ts +0 -285
  356. package/dist/x402/verified-delivery-facilitator.d.ts.map +0 -1
  357. package/dist/x402/verified-delivery-facilitator.js +0 -423
  358. package/dist/x402/verified-delivery-facilitator.js.map +0 -1
  359. package/src/gateway/x402-mode.ts +0 -350
  360. package/src/x402/delivery-oracle.ts +0 -285
  361. package/src/x402/facilitator-server-entry.ts +0 -35
  362. package/src/x402/facilitator-server.ts +0 -445
  363. package/src/x402/payment-requirements.ts +0 -231
  364. package/src/x402/relayer.ts +0 -122
  365. package/src/x402/resource-index.ts +0 -228
  366. package/src/x402/verified-delivery-facilitator.ts +0 -744
@@ -0,0 +1,785 @@
1
+ import type { MetrikNetwork } from "@absol-labs/sdk";
2
+ import type {
3
+ RefundClient,
4
+ RefundRelayResult,
5
+ RefundStatus,
6
+ X402PayerSigner,
7
+ } from "@absol-labs/sdk/x402";
8
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { z } from "zod";
10
+
11
+ import {
12
+ verifyMandateOwnerSignature,
13
+ type SignedSpendMandate,
14
+ } from "../mandates/mandate.js";
15
+ import {
16
+ createPublicClient,
17
+ http,
18
+ keccak256,
19
+ toHex,
20
+ type PublicClient,
21
+ } from "viem";
22
+
23
+ import {
24
+ assertOutboundUrlAllowed,
25
+ createGuardedFetch,
26
+ OutboundRequestRefusedError,
27
+ type LookupFn,
28
+ } from "../x402-buyer/guarded-fetch.js";
29
+ import type {
30
+ PayX402WithMandateInput,
31
+ PayX402WithMandateResult,
32
+ X402PostSignatureError,
33
+ } from "../x402-buyer/pay.js";
34
+
35
+ /**
36
+ * The MCP surface for x402 (metrik-agent#121 item 4): `x402_pay`,
37
+ * `refund_status`, `refund`. Registered only when the server is given an
38
+ * {@link McpX402Options}; every existing tool is unchanged.
39
+ *
40
+ * `x402_pay` is gated exactly like `hire_verified_service`: the SAME server
41
+ * mandate, the SAME per-owner serialization, the SAME cumulative spend
42
+ * (`getSpentSoFarUsdc`, which counts the stream this payment opens once it is
43
+ * recorded), and `checkMandate()` on the payment's opener terms before the SDK
44
+ * signs anything (see `../x402-buyer/pay.ts`).
45
+ */
46
+
47
+ export const X402_PAY_TOOL_DESCRIPTION =
48
+ "Pay an x402 endpoint (HTTP 402 Payment Required, x402 v2 'exact') and return its response. A Metrik x402 seller quotes a payment that opens an escrow STREAM; the seller earns ratePerSecond x VERIFIED seconds, capped at the deposit, and the unearned remainder stays recoverable by this server's wallet. An UPFRONT ('minimum charge') quote splits the price: upfrontAmount is a NON-REFUNDABLE per-call charge paid to the seller at settlement, and only price - upfrontAmount is escrowed and refundable; when upfrontAmount == price no stream opens at all (streamId null, nothing to refund). Subject to the agent's spend mandate (the WHOLE price vs per-stream and total caps, rate, duration, operator) and a cap on the non-refundable part (maxUpfrontAmountUsdc, default 0.10 USDC), checked BEFORE anything is signed. Refunds are NOT automatic: use refund_status and refund with the returned streamId.";
49
+ export const REFUND_STATUS_TOOL_DESCRIPTION =
50
+ "Read the refund state of a stream this server paid for over x402, from the seller's gasless refund relay: whether it is running, what is reclaimable, and the next step (close / wait / reclaim). An upfront-only payment (no stream) has nothing to refund. Moves no funds.";
51
+ export const REFUND_TOOL_DESCRIPTION =
52
+ "Recover the unearned part of an x402 payment through the seller's gasless refund relay: close=true stops the stream (this wallet signs the opener's Close; the relay pays the gas), reclaim=true sends the permissionless reclaim (the refund always goes to the payer). Only the escrowed part can come back: an upfront charge is final, and an upfront-only payment has nothing to refund. Nothing is refunded automatically; reclaim waits for the settlement window and may say when to retry.";
53
+
54
+ export interface McpX402Options {
55
+ /** The payer: this server's own wallet (must own the server mandate). */
56
+ readonly signer: X402PayerSigner;
57
+ readonly network: MetrikNetwork;
58
+ /** Mandate-gated payer. Default: `payX402WithMandate` (lazily loaded). */
59
+ readonly pay?: (
60
+ input: PayX402WithMandateInput,
61
+ ) => Promise<PayX402WithMandateResult>;
62
+ /** Refund relay client factory. Default: the SDK's `refundClient`. */
63
+ readonly refundClient?: (target: {
64
+ readonly refundUrl: string;
65
+ readonly streamId: `0x${string}`;
66
+ readonly opener?: `0x${string}`;
67
+ /** The guarded fetch the relay client must use. */
68
+ readonly fetch?: typeof fetch;
69
+ }) => Promise<RefundClient>;
70
+ /** Passed to the SDK payer (e.g. a transport for the on-chain checks). */
71
+ readonly sdkOptions?: PayX402WithMandateInput["sdkOptions"];
72
+ /** Chain reads after a post-signature failure (see `payX402WithMandate`). */
73
+ readonly chainReader?: PayX402WithMandateInput["chainReader"];
74
+ /**
75
+ * `METRIK_AGENT_X402_ALLOWED_HOSTS`: the only hosts x402_pay / refund /
76
+ * refund_status may contact (exact, or `*.domain`). Unset = any PUBLIC host.
77
+ */
78
+ readonly allowedHosts?: readonly string[];
79
+ /** Base fetch under the outbound guard (tests). Default `globalThis.fetch`. */
80
+ readonly fetch?: typeof fetch;
81
+ /** DNS resolution for the outbound guard (tests). */
82
+ readonly lookup?: LookupFn;
83
+ /**
84
+ * Local development / fork tests ONLY: let the outbound guard reach
85
+ * loopback and private addresses (and plain http). Never set from env.
86
+ */
87
+ readonly allowPrivateNetworks?: boolean;
88
+ }
89
+
90
+ /** What the server shares with these tools (its mandate state and registry). */
91
+ export interface McpX402Context {
92
+ readonly x402: McpX402Options;
93
+ readonly resolveMandate: () =>
94
+ | SignedSpendMandate
95
+ | Promise<SignedSpendMandate>;
96
+ readonly getSpentSoFarUsdc: (owner: `0x${string}`) => Promise<bigint>;
97
+ readonly getRevokedMandateIds: (
98
+ owner: `0x${string}`,
99
+ ) => Promise<readonly `0x${string}`[]>;
100
+ readonly serialize: <T>(
101
+ owner: `0x${string}`,
102
+ task: () => Promise<T>,
103
+ ) => Promise<T>;
104
+ readonly recordStream: (record: {
105
+ readonly streamId: `0x${string}`;
106
+ readonly buyer: `0x${string}`;
107
+ readonly operator: `0x${string}`;
108
+ readonly serviceRef: `0x${string}`;
109
+ readonly budgetUsdc: bigint;
110
+ readonly ratePerSecondUsdc: bigint;
111
+ readonly maxDurationSeconds: number;
112
+ readonly openedAtSeconds: number;
113
+ readonly mandateId: `0x${string}`;
114
+ readonly refundUrl?: string;
115
+ readonly opener?: `0x${string}`;
116
+ readonly x402Reservation?: boolean;
117
+ /** Not a stream: a settled upfront-only charge (counted spend, nothing to refund). */
118
+ readonly x402Charge?: "upfront-only";
119
+ /** The non-refundable part of the payment (the upfront charge), base units. */
120
+ readonly nonRefundableUsdc?: bigint;
121
+ }) => Promise<void>;
122
+ /** Drop a record (a released reservation). Absent = reservations are kept. */
123
+ readonly removeStream?: (streamId: `0x${string}`) => Promise<void>;
124
+ readonly findStream: (streamId: `0x${string}`) => Promise<{
125
+ readonly refundUrl?: string;
126
+ readonly opener?: `0x${string}`;
127
+ readonly x402Charge?: "upfront-only";
128
+ } | null>;
129
+ readonly now: () => number;
130
+ readonly ok: (
131
+ text: string,
132
+ structured?: Record<string, unknown>,
133
+ ) => { content: { type: "text"; text: string }[] };
134
+ readonly fail: (text: string) => {
135
+ content: { type: "text"; text: string }[];
136
+ isError: boolean;
137
+ };
138
+ readonly unexpected: (error: unknown) => {
139
+ content: { type: "text"; text: string }[];
140
+ isError: boolean;
141
+ };
142
+ }
143
+
144
+ /** Longest response body returned to the model. */
145
+ const MAX_BODY_CHARS = 32 * 1024;
146
+
147
+ const atomicSchema = z
148
+ .string()
149
+ .regex(/^[1-9]\d*$/, "a positive integer of USDC base units (6 decimals)");
150
+ const bytes32Schema = z
151
+ .string()
152
+ .regex(/^0x[0-9a-fA-F]{64}$/, "a 0x-prefixed 32-byte hex value");
153
+
154
+ const x402PaySchema = {
155
+ url: z
156
+ .string()
157
+ .url()
158
+ .describe("The x402 endpoint. https only (http only for localhost)."),
159
+ method: z
160
+ .enum(["GET", "POST", "PUT", "PATCH", "DELETE"])
161
+ .optional()
162
+ .describe("HTTP method. Default POST with a body, otherwise GET."),
163
+ headers: z.record(z.string()).optional(),
164
+ body: z.string().max(1_000_000).optional().describe("Request body."),
165
+ maxAmountUsdc: atomicSchema
166
+ .optional()
167
+ .describe(
168
+ "Optional price ceiling, base units. Can only TIGHTEN the server mandate's per-stream cap.",
169
+ ),
170
+ maxRatePerSecondUsdc: atomicSchema
171
+ .optional()
172
+ .describe("Optional rate ceiling, base units/second. Tightens only."),
173
+ minDurationSeconds: z
174
+ .number()
175
+ .int()
176
+ .positive()
177
+ .optional()
178
+ .describe(
179
+ "Refuse a quote the seller can fully earn in fewer verified seconds.",
180
+ ),
181
+ maxUpfrontAmountUsdc: z
182
+ .string()
183
+ .regex(/^(0|[1-9]\d*)$/, "a non-negative integer of USDC base units")
184
+ .optional()
185
+ .describe(
186
+ "Cap on the NON-REFUNDABLE per-call part of an upfront-opener quote, base units. Default 100000 (0.10 USDC); 0 refuses any upfront charge.",
187
+ ),
188
+ } as const;
189
+
190
+ const ZERO_STREAM_ID = `0x${"00".repeat(32)}` as const;
191
+
192
+ const refundSchema = {
193
+ streamId: bytes32Schema,
194
+ refundUrl: z
195
+ .string()
196
+ .url()
197
+ .optional()
198
+ .describe(
199
+ "The seller's refund relay URL. Optional for a stream this server paid for (it was recorded).",
200
+ ),
201
+ } as const;
202
+
203
+ const refundActionSchema = {
204
+ ...refundSchema,
205
+ close: z
206
+ .boolean()
207
+ .optional()
208
+ .describe("Stop the stream first (this wallet signs Close)."),
209
+ reclaim: z
210
+ .boolean()
211
+ .optional()
212
+ .describe("Send the permissionless reclaim. Default true."),
213
+ } as const;
214
+
215
+ const PAYMENT_HEADERS = new Set(["payment-signature", "x-payment"]);
216
+
217
+ export function registerX402Tools(
218
+ mcpServer: McpServer,
219
+ ctx: McpX402Context,
220
+ ): void {
221
+ const pay =
222
+ ctx.x402.pay ??
223
+ (async (input: PayX402WithMandateInput) => {
224
+ const { payX402WithMandate } = await import("../x402-buyer/pay.js");
225
+ return await payX402WithMandate(input);
226
+ });
227
+ // Every outbound request these tools make — the seller, and the refund
228
+ // relay — goes through the guard: https only, public addresses only, the
229
+ // optional host allowlist, and never a redirect.
230
+ const guard = {
231
+ ...(ctx.x402.allowedHosts === undefined
232
+ ? {}
233
+ : { allowedHosts: ctx.x402.allowedHosts }),
234
+ ...(ctx.x402.allowPrivateNetworks === true
235
+ ? { allowPrivateNetworks: true }
236
+ : {}),
237
+ ...(ctx.x402.fetch === undefined ? {} : { fetch: ctx.x402.fetch }),
238
+ ...(ctx.x402.lookup === undefined ? {} : { lookup: ctx.x402.lookup }),
239
+ };
240
+ const guardedFetch = createGuardedFetch(guard);
241
+ const openRefunds =
242
+ ctx.x402.refundClient ??
243
+ (async (target: {
244
+ refundUrl: string;
245
+ streamId: `0x${string}`;
246
+ opener?: `0x${string}`;
247
+ fetch?: typeof fetch;
248
+ }) => {
249
+ const { refundClient } = await import("@absol-labs/sdk/x402");
250
+ return refundClient(target.refundUrl, {
251
+ streamId: target.streamId,
252
+ ...(target.opener === undefined ? {} : { opener: target.opener }),
253
+ ...(target.fetch === undefined ? {} : { fetch: target.fetch }),
254
+ expectedChainId: ctx.x402.network.chain.id,
255
+ // Chain access is what lets the SDK read the opener's KIND on-chain and
256
+ // sign an upfront opener's Close under its own EIP-712 domain; without
257
+ // it, such a Close is refused.
258
+ publicClient: refundReader(ctx.x402),
259
+ network: ctx.x402.network,
260
+ });
261
+ });
262
+
263
+ mcpServer.registerTool(
264
+ "x402_pay",
265
+ {
266
+ description: X402_PAY_TOOL_DESCRIPTION,
267
+ inputSchema: x402PaySchema,
268
+ annotations: {
269
+ destructiveHint: true,
270
+ idempotentHint: false,
271
+ openWorldHint: true,
272
+ readOnlyHint: false,
273
+ title: "Pay x402 Endpoint",
274
+ },
275
+ },
276
+ async (args) => {
277
+ const headers: Record<string, string> = {};
278
+ for (const [name, value] of Object.entries(args.headers ?? {})) {
279
+ if (PAYMENT_HEADERS.has(name.toLowerCase())) {
280
+ return ctx.fail(
281
+ `do not set the ${name} header: this server signs the payment itself, after the quote passes the spend mandate.`,
282
+ );
283
+ }
284
+ headers[name.toLowerCase()] = value;
285
+ }
286
+ if (args.body !== undefined && headers["content-type"] === undefined) {
287
+ headers["content-type"] = "application/json";
288
+ }
289
+ try {
290
+ // Refuse a forbidden target before the mandate, the registry or the
291
+ // seller is touched.
292
+ await assertOutboundUrlAllowed(args.url, guard);
293
+ const signedMandate = await ctx.resolveMandate();
294
+ const owner = signedMandate.owner as `0x${string}`;
295
+ return await ctx.serialize(owner, async () => {
296
+ const spentSoFarUsdc = await ctx.getSpentSoFarUsdc(owner);
297
+ const revokedMandateIds = await ctx.getRevokedMandateIds(owner);
298
+ const nowSeconds = ctx.now();
299
+ // The reservation: written BEFORE the SDK signs (onApproved), inside
300
+ // this owner's serialized queue, under a key of its own. It is
301
+ // released only when the authorization provably never moved money,
302
+ // and replaced by the real stream record once the stream is known.
303
+ let reservation: `0x${string}` | null = null;
304
+ const release = async () => {
305
+ if (reservation !== null) await ctx.removeStream?.(reservation);
306
+ };
307
+ let result: PayX402WithMandateResult;
308
+ try {
309
+ result = await pay({
310
+ url: args.url,
311
+ init: {
312
+ method:
313
+ args.method ?? (args.body === undefined ? "GET" : "POST"),
314
+ headers,
315
+ ...(args.body === undefined ? {} : { body: args.body }),
316
+ },
317
+ signer: ctx.x402.signer,
318
+ network: ctx.x402.network,
319
+ signedMandate,
320
+ spentSoFarUsdc,
321
+ nowSeconds,
322
+ revokedMandateIds,
323
+ ...(args.maxAmountUsdc === undefined
324
+ ? {}
325
+ : { maxAmount: BigInt(args.maxAmountUsdc) }),
326
+ ...(args.maxRatePerSecondUsdc === undefined
327
+ ? {}
328
+ : { maxRatePerSecond: BigInt(args.maxRatePerSecondUsdc) }),
329
+ ...(args.minDurationSeconds === undefined
330
+ ? {}
331
+ : { minDurationSeconds: args.minDurationSeconds }),
332
+ ...(args.maxUpfrontAmountUsdc === undefined
333
+ ? {}
334
+ : { maxUpfrontAmount: BigInt(args.maxUpfrontAmountUsdc) }),
335
+ sdkOptions: { ...ctx.x402.sdkOptions, fetch: guardedFetch },
336
+ ...(ctx.x402.chainReader === undefined
337
+ ? {}
338
+ : { chainReader: ctx.x402.chainReader }),
339
+ onApproved: async ({ request, quote }) => {
340
+ reservation = keccak256(
341
+ toHex(
342
+ `x402-reservation:${owner}:${quote.payTo}:${nowSeconds}:${crypto.randomUUID()}`,
343
+ ),
344
+ );
345
+ await ctx.recordStream({
346
+ streamId: reservation,
347
+ buyer: owner,
348
+ operator: request.operator,
349
+ serviceRef:
350
+ quote.serviceRef ?? (`0x${"00".repeat(32)}` as const),
351
+ budgetUsdc: request.budgetUsdc,
352
+ ratePerSecondUsdc: request.ratePerSecondUsdc,
353
+ maxDurationSeconds: request.maxDurationSeconds,
354
+ openedAtSeconds: nowSeconds,
355
+ mandateId: signedMandate.mandateId as `0x${string}`,
356
+ opener: quote.payTo,
357
+ x402Reservation: true,
358
+ ...(quote.upfrontAmount === null || quote.upfrontAmount === 0n
359
+ ? {}
360
+ : { nonRefundableUsdc: quote.upfrontAmount }),
361
+ });
362
+ },
363
+ });
364
+ } catch (error) {
365
+ return await settleFailure(ctx, error, {
366
+ release,
367
+ reservation,
368
+ owner,
369
+ mandateId: signedMandate.mandateId as `0x${string}`,
370
+ nowSeconds,
371
+ });
372
+ }
373
+ const opener = result.metrikStreamOpener;
374
+ const upfrontAmount = result.quote?.upfrontAmount ?? null;
375
+ if (
376
+ result.paid &&
377
+ opener !== null &&
378
+ opener.streamId === null &&
379
+ result.approved !== null
380
+ ) {
381
+ // Upfront-only: there is NO stream to record or refund. The spend
382
+ // still counts against the mandate (the whole price is gone), so
383
+ // the reservation is replaced by a permanent, non-stream charge.
384
+ await ctx.recordStream({
385
+ streamId: chargeKey(owner, opener.opener, result),
386
+ buyer: owner,
387
+ operator: result.approved.operator,
388
+ serviceRef:
389
+ result.quote?.serviceRef ?? (`0x${"00".repeat(32)}` as const),
390
+ budgetUsdc: result.approved.budgetUsdc,
391
+ ratePerSecondUsdc: result.approved.ratePerSecondUsdc,
392
+ maxDurationSeconds: result.approved.maxDurationSeconds,
393
+ openedAtSeconds: nowSeconds,
394
+ mandateId: signedMandate.mandateId as `0x${string}`,
395
+ opener: opener.opener,
396
+ x402Charge: "upfront-only",
397
+ nonRefundableUsdc: result.approved.budgetUsdc,
398
+ });
399
+ await release();
400
+ } else if (
401
+ result.paid &&
402
+ opener !== null &&
403
+ opener.streamId !== null &&
404
+ result.approved !== null
405
+ ) {
406
+ // Replace the reservation with the real stream (same amount).
407
+ await ctx.recordStream({
408
+ streamId: opener.streamId,
409
+ buyer: owner,
410
+ operator: result.approved.operator,
411
+ serviceRef:
412
+ result.quote?.serviceRef ?? (`0x${"00".repeat(32)}` as const),
413
+ budgetUsdc: result.approved.budgetUsdc,
414
+ ratePerSecondUsdc: result.approved.ratePerSecondUsdc,
415
+ maxDurationSeconds: result.approved.maxDurationSeconds,
416
+ openedAtSeconds: nowSeconds,
417
+ mandateId: signedMandate.mandateId as `0x${string}`,
418
+ ...(opener.refundUrl === null
419
+ ? {}
420
+ : { refundUrl: opener.refundUrl }),
421
+ opener: opener.opener,
422
+ ...(upfrontAmount === null || upfrontAmount === 0n
423
+ ? {}
424
+ : { nonRefundableUsdc: upfrontAmount }),
425
+ });
426
+ await release();
427
+ } else if (!result.paid) {
428
+ // No 402 was answered, so nothing was approved or signed.
429
+ await release();
430
+ }
431
+ if (!result.paid) {
432
+ // Not a payment: do not relay a body this server fetched on the
433
+ // model's behalf (it would make the tool a general-purpose
434
+ // fetcher of whatever the URL serves).
435
+ return ctx.fail(
436
+ `the endpoint answered HTTP ${result.response.status} without asking for an x402 payment; nothing was signed or paid, and its body is not returned.`,
437
+ );
438
+ }
439
+ const body = await result.response.text();
440
+ const truncated = body.length > MAX_BODY_CHARS;
441
+ const structured = {
442
+ paid: result.paid,
443
+ httpStatus: result.response.status,
444
+ contentType: result.response.headers.get("content-type"),
445
+ body: truncated ? body.slice(0, MAX_BODY_CHARS) : body,
446
+ bodyTruncated: truncated,
447
+ streamId: opener?.streamId ?? null,
448
+ opener: opener?.opener ?? null,
449
+ refundUrl: opener?.refundUrl ?? null,
450
+ depositUsdc: opener?.deposit?.toString() ?? null,
451
+ ratePerSecondUsdc: opener?.ratePerSecond?.toString() ?? null,
452
+ expiresAt: opener?.expiresAt ?? null,
453
+ minEarnSeconds: result.quote?.minEarnSeconds?.toString() ?? null,
454
+ openerKind: result.quote?.openerKind ?? null,
455
+ priceUsdc: result.quote?.amount.toString() ?? null,
456
+ nonRefundableUsdc: upfrontAmount?.toString() ?? null,
457
+ refundableUsdc: result.quote?.refundableAmount?.toString() ?? null,
458
+ upfrontFeeUsdc: opener?.upfrontFee?.toString() ?? null,
459
+ upfrontOnly: opener?.upfrontOnly ?? false,
460
+ refundIsAutomatic: false,
461
+ };
462
+ if (opener !== null && opener.streamId === null) {
463
+ return ctx.ok(
464
+ `paid ${result.quote?.amount.toString() ?? "?"} USDC base units as an UPFRONT-ONLY charge; HTTP ${result.response.status}. The whole price was a non-refundable per-call charge paid to the seller at settlement: no stream was opened, the payment is final and there is nothing to refund.`,
465
+ structured,
466
+ );
467
+ }
468
+ const split =
469
+ upfrontAmount !== null && upfrontAmount > 0n
470
+ ? ` Of the ${result.quote?.amount.toString() ?? "?"} paid, ${upfrontAmount.toString()} was a NON-REFUNDABLE per-call charge (final); only the ${opener?.deposit?.toString() ?? "?"} escrowed can come back.`
471
+ : "";
472
+ return ctx.ok(
473
+ `paid ${opener?.deposit?.toString() ?? "?"} USDC base units into stream ${opener?.streamId ?? "?"}; HTTP ${result.response.status}.${split} The seller earns only for VERIFIED seconds; the unearned remainder is NOT refunded automatically — call refund (or refund_status) with this streamId.`,
474
+ structured,
475
+ );
476
+ });
477
+ } catch (error) {
478
+ if (error instanceof OutboundRequestRefusedError) {
479
+ return ctx.fail(error.message);
480
+ }
481
+ return ctx.unexpected(error);
482
+ }
483
+ },
484
+ );
485
+
486
+ const bind = async (args: {
487
+ streamId: string;
488
+ refundUrl?: string | undefined;
489
+ }): Promise<
490
+ | RefundClient
491
+ | { readonly error: string }
492
+ | { readonly nothingToRefund: string }
493
+ > => {
494
+ const streamId = args.streamId.toLowerCase() as `0x${string}`;
495
+ // Stream id 0 is an upfront-only payment: nothing was escrowed. Never
496
+ // contact a relay or sign anything for it.
497
+ if (streamId === ZERO_STREAM_ID) {
498
+ return { nothingToRefund: streamId };
499
+ }
500
+ const record = await ctx.findStream(streamId);
501
+ if (record?.x402Charge === "upfront-only") {
502
+ return { nothingToRefund: streamId };
503
+ }
504
+ const refundUrl = args.refundUrl ?? record?.refundUrl;
505
+ if (refundUrl === undefined) {
506
+ return {
507
+ error: `no refund relay is known for stream ${streamId}: pass refundUrl (x402_pay returned it).`,
508
+ };
509
+ }
510
+ try {
511
+ await assertOutboundUrlAllowed(
512
+ refundUrl.replace("{streamId}", streamId),
513
+ guard,
514
+ );
515
+ } catch (error) {
516
+ if (error instanceof OutboundRequestRefusedError) {
517
+ return { error: error.message };
518
+ }
519
+ throw error;
520
+ }
521
+ return await openRefunds({
522
+ refundUrl,
523
+ streamId,
524
+ ...(record?.opener === undefined ? {} : { opener: record.opener }),
525
+ fetch: guardedFetch,
526
+ });
527
+ };
528
+
529
+ mcpServer.registerTool(
530
+ "refund_status",
531
+ {
532
+ description: REFUND_STATUS_TOOL_DESCRIPTION,
533
+ inputSchema: refundSchema,
534
+ annotations: {
535
+ idempotentHint: true,
536
+ openWorldHint: true,
537
+ readOnlyHint: true,
538
+ title: "x402 Refund Status",
539
+ },
540
+ },
541
+ async (args) => {
542
+ try {
543
+ const refunds = await bind(args);
544
+ if ("error" in refunds) return ctx.fail(refunds.error);
545
+ if ("nothingToRefund" in refunds) return nothingToRefund(ctx, args);
546
+ const status = await refunds.status();
547
+ return ctx.ok(describeStatus(status), serializeStatus(status));
548
+ } catch (error) {
549
+ if (error instanceof Error && error.name === "NothingToRefundError") {
550
+ return nothingToRefund(ctx, args);
551
+ }
552
+ return explainRefundError(ctx, error);
553
+ }
554
+ },
555
+ );
556
+
557
+ mcpServer.registerTool(
558
+ "refund",
559
+ {
560
+ description: REFUND_TOOL_DESCRIPTION,
561
+ inputSchema: refundActionSchema,
562
+ annotations: {
563
+ destructiveHint: true,
564
+ idempotentHint: false,
565
+ openWorldHint: true,
566
+ readOnlyHint: false,
567
+ title: "x402 Refund",
568
+ },
569
+ },
570
+ async (args) => {
571
+ try {
572
+ const refunds = await bind(args);
573
+ if ("error" in refunds) return ctx.fail(refunds.error);
574
+ if ("nothingToRefund" in refunds) return nothingToRefund(ctx, args);
575
+ let closeResult: RefundRelayResult | null = null;
576
+ if (args.close === true) {
577
+ // Buyer recovery, authorized on the mandate owner's signature alone
578
+ // (never on caps or expiry, so a lapsed mandate cannot strand the
579
+ // payer's own money) — the same rule as reclaim_unspent.
580
+ const signedMandate = await ctx.resolveMandate();
581
+ const decision = await verifyMandateOwnerSignature(signedMandate);
582
+ if (!decision.allowed) {
583
+ return ctx.fail(`mandate denied: ${decision.reason ?? "unknown"}`);
584
+ }
585
+ closeResult = await refunds.close({ signer: ctx.x402.signer });
586
+ }
587
+ let reclaimResult: RefundRelayResult | null = null;
588
+ if (args.reclaim !== false) {
589
+ reclaimResult = await refunds.reclaim();
590
+ }
591
+ const status = await refunds.status();
592
+ return ctx.ok(
593
+ `${closeResult === null ? "" : `closed (${closeResult.transaction}). `}${reclaimResult === null ? "" : `reclaimed ${reclaimResult.amount?.toString() ?? "?"} USDC base units to the payer (${reclaimResult.transaction}). `}${describeStatus(status)}`,
594
+ {
595
+ ...serializeStatus(status),
596
+ close:
597
+ closeResult === null
598
+ ? null
599
+ : {
600
+ transaction: closeResult.transaction,
601
+ verified: closeResult.verified,
602
+ },
603
+ reclaim:
604
+ reclaimResult === null
605
+ ? null
606
+ : {
607
+ transaction: reclaimResult.transaction,
608
+ amountUsdc: reclaimResult.amount?.toString() ?? null,
609
+ verified: reclaimResult.verified,
610
+ },
611
+ },
612
+ );
613
+ } catch (error) {
614
+ if (error instanceof Error && error.name === "NothingToRefundError") {
615
+ return nothingToRefund(ctx, args);
616
+ }
617
+ return explainRefundError(ctx, error);
618
+ }
619
+ },
620
+ );
621
+ }
622
+
623
+ /** Upfront-only payments: a finished, successful answer — not an error. */
624
+ function nothingToRefund(ctx: McpX402Context, args: { streamId: string }) {
625
+ return ctx.ok(
626
+ "nothing to refund: this was an upfront-only payment — the whole price was a non-refundable per-call charge paid to the seller at settlement, no stream was opened and nothing was escrowed. Nothing was requested, signed or sent.",
627
+ {
628
+ streamId: args.streamId.toLowerCase(),
629
+ nothingToRefund: true,
630
+ upfrontOnly: true,
631
+ refundIsAutomatic: false,
632
+ },
633
+ );
634
+ }
635
+
636
+ /** Registry key of a settled upfront-only charge (not an escrow stream id). */
637
+ function chargeKey(
638
+ owner: `0x${string}`,
639
+ opener: `0x${string}`,
640
+ result: PayX402WithMandateResult,
641
+ ): `0x${string}` {
642
+ return keccak256(
643
+ toHex(
644
+ `x402-upfront-charge:${owner}:${opener}:${result.authorization?.nonce ?? crypto.randomUUID()}`,
645
+ ),
646
+ );
647
+ }
648
+
649
+ /** Read-only chain access for the SDK refund client's opener-kind check. */
650
+ function refundReader(options: McpX402Options): PublicClient {
651
+ return (
652
+ options.sdkOptions?.publicClient ??
653
+ (createPublicClient({
654
+ chain: options.network.chain,
655
+ transport:
656
+ options.sdkOptions?.transport ?? http(options.network.rpcUrls[0]),
657
+ }) as PublicClient)
658
+ );
659
+ }
660
+
661
+ function serializeStatus(status: RefundStatus): Record<string, unknown> {
662
+ return {
663
+ streamId: status.streamId,
664
+ opener: status.opener,
665
+ payer: status.payer,
666
+ status: status.status,
667
+ reclaimed: status.reclaimed,
668
+ reclaimableUsdc: status.reclaimable?.toString() ?? null,
669
+ next: status.next,
670
+ refundIsAutomatic: false,
671
+ };
672
+ }
673
+
674
+ function describeStatus(status: RefundStatus): string {
675
+ if (status.reclaimed) {
676
+ return `stream ${status.streamId} is reclaimed: the unearned remainder went back to ${status.payer}.`;
677
+ }
678
+ return `stream ${status.streamId} is ${status.status}; next step: ${status.next.action}${status.next.retryAt === undefined ? "" : ` (from ${status.next.retryAt})`}. Nothing is refunded automatically.`;
679
+ }
680
+
681
+ /**
682
+ * A failed x402_pay. Before the signature was handed out (mandate or SDK
683
+ * refusal) the reservation is released. After it, the reservation is KEPT
684
+ * unless the authorization provably can never move funds, the real stream is
685
+ * recorded when it can be identified on-chain, and the model is never told
686
+ * "not settled" when the money may have moved.
687
+ */
688
+ async function settleFailure(
689
+ ctx: McpX402Context,
690
+ error: unknown,
691
+ state: {
692
+ readonly release: () => Promise<void>;
693
+ readonly reservation: `0x${string}` | null;
694
+ readonly owner: `0x${string}`;
695
+ readonly mandateId: `0x${string}`;
696
+ readonly nowSeconds: number;
697
+ },
698
+ ) {
699
+ if (error instanceof Error && error.name === "X402PostSignatureError") {
700
+ const post = error as X402PostSignatureError;
701
+ if (post.releasable) {
702
+ await state.release();
703
+ } else if (post.upfrontOnly && post.state === "used") {
704
+ // Settled as an upfront-only charge: no stream to record, but the spend
705
+ // is real and stays counted — as a non-stream charge.
706
+ await ctx.recordStream({
707
+ streamId: keccak256(
708
+ toHex(
709
+ `x402-upfront-charge:${state.owner}:${post.opener}:${post.authorization?.nonce ?? crypto.randomUUID()}`,
710
+ ),
711
+ ),
712
+ buyer: state.owner,
713
+ operator: post.approved.operator,
714
+ serviceRef: post.quote.serviceRef ?? (`0x${"00".repeat(32)}` as const),
715
+ budgetUsdc: post.approved.budgetUsdc,
716
+ ratePerSecondUsdc: post.approved.ratePerSecondUsdc,
717
+ maxDurationSeconds: post.approved.maxDurationSeconds,
718
+ openedAtSeconds: state.nowSeconds,
719
+ mandateId: state.mandateId,
720
+ opener: post.opener,
721
+ x402Charge: "upfront-only",
722
+ nonRefundableUsdc: post.approved.budgetUsdc,
723
+ });
724
+ await state.release();
725
+ } else if (post.streamId !== null) {
726
+ await ctx.recordStream({
727
+ streamId: post.streamId,
728
+ buyer: state.owner,
729
+ operator: post.approved.operator,
730
+ serviceRef: post.quote.serviceRef ?? (`0x${"00".repeat(32)}` as const),
731
+ budgetUsdc: post.approved.budgetUsdc,
732
+ ratePerSecondUsdc: post.approved.ratePerSecondUsdc,
733
+ maxDurationSeconds: post.approved.maxDurationSeconds,
734
+ openedAtSeconds: state.nowSeconds,
735
+ mandateId: state.mandateId,
736
+ opener: post.opener,
737
+ ...(post.quote.upfrontAmount === null || post.quote.upfrontAmount === 0n
738
+ ? {}
739
+ : { nonRefundableUsdc: post.quote.upfrontAmount }),
740
+ });
741
+ await state.release();
742
+ }
743
+ return ctx.fail(
744
+ `${post.message} ${post.releasable ? "The spend reservation was released." : `The ${post.approved.budgetUsdc.toString()} USDC base units stay COUNTED against the spend mandate.`}${post.streamId === null ? "" : ` streamId: ${post.streamId} (opener ${post.opener}); recover the unearned part with refund / refund_status.`} Do not retry this payment blindly.`,
745
+ );
746
+ }
747
+ // Anything else was raised before a signature existed: nothing moved.
748
+ await state.release();
749
+ if (error instanceof OutboundRequestRefusedError)
750
+ return ctx.fail(error.message);
751
+ return explainPayError(ctx, error);
752
+ }
753
+
754
+ function explainPayError(ctx: McpX402Context, error: unknown) {
755
+ if (error instanceof Error) {
756
+ switch (error.name) {
757
+ case "X402MandateDeniedError":
758
+ return ctx.fail(
759
+ `${error.message} Raise the server's METRIK_AGENT_MANDATE_* caps only if that payment is intended.`,
760
+ );
761
+ case "X402PaymentRefusedError":
762
+ return ctx.fail(error.message);
763
+ }
764
+ }
765
+ return ctx.unexpected(error);
766
+ }
767
+
768
+ function explainRefundError(ctx: McpX402Context, error: unknown) {
769
+ if (error instanceof Error) {
770
+ switch (error.name) {
771
+ case "RefundNotReadyError": {
772
+ const retryAt = (error as { retryAt?: number | null }).retryAt;
773
+ return ctx.fail(
774
+ `${error.message}${retryAt === null || retryAt === undefined ? "" : ` Retry at ${retryAt} (unix seconds).`} Nothing is refunded automatically.`,
775
+ );
776
+ }
777
+ case "OutboundRequestRefusedError":
778
+ case "RefundRelayError":
779
+ case "NotStreamPayerError":
780
+ case "InvalidInputError":
781
+ return ctx.fail(error.message);
782
+ }
783
+ }
784
+ return ctx.unexpected(error);
785
+ }