@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
@@ -1,4 +1,9 @@
1
- import { SettlementWindowOpenError } from "@absol-labs/sdk";
1
+ import {
2
+ CheckpointNotReadyError,
3
+ ReclaimFailedError,
4
+ SettlementWindowOpenError,
5
+ StreamNotInCheckpointError,
6
+ } from "@absol-labs/sdk";
2
7
 
3
8
  import { booleanFlag, integerFlag } from "../args.js";
4
9
  import { formatUsdc } from "../amounts.js";
@@ -11,7 +16,14 @@ import {
11
16
  } from "../escape-hatch.js";
12
17
  import { signRecoveryMandate } from "../mandate.js";
13
18
  import { assertCanPayGas } from "../preflight.js";
14
- import { resolveCliWallet } from "../wallet.js";
19
+ import { writeClientOptions } from "../deps.js";
20
+ import {
21
+ describeAwaitingCheckpoint,
22
+ describeTerminalBlocker,
23
+ readReclaimBlocker,
24
+ type ReclaimBlocker,
25
+ } from "../reclaimability.js";
26
+ import { isPolicyEnforced, resolveCliWallet } from "../wallet.js";
15
27
  import type { CommandContext, CommandSpec } from "../context.js";
16
28
  import type { CliReadClient, CliWriteClient } from "../deps.js";
17
29
  import type {
@@ -19,6 +31,9 @@ import type {
19
31
  StreamProofTransactionResult,
20
32
  } from "../../sdk/client.js";
21
33
  import type { StreamV2 } from "@absol-labs/sdk";
34
+ import { streamEscrowV2Abi } from "@absol-labs/shared";
35
+ import { parseEventLogs, type Log } from "viem";
36
+ import { escrowGenerationFor } from "../../escrow-generation.js";
22
37
 
23
38
  /**
24
39
  * How long to keep retrying a reclaim that is blocked only by an open
@@ -48,12 +63,45 @@ export const reclaimCommand: CommandSpec = {
48
63
  if (attempts < 1) throw usageError("--attempts must be at least 1");
49
64
  if (interval < 1) throw usageError("--interval must be at least 1 second");
50
65
 
51
- const { account } = await resolveCliWallet(env, { note: out.note });
66
+ const wallet = await resolveCliWallet(env, {
67
+ note: out.note,
68
+ rpcUrl: config.rpcUrl,
69
+ });
70
+ const { account } = wallet;
52
71
  const read = deps.createReadClient(config);
53
72
  await assertTestnetRpc(config, () => read.getChainId());
54
- await assertCanPayGas(read, account.address);
55
73
 
56
- const write = deps.createWriteClient(config, account);
74
+ // Name the condition from chain state BEFORE anything is built or spent.
75
+ // The public endpoint's revert data is not dependable enough to tell
76
+ // "already reclaimed" from "still running" from "not checkpointed yet", and
77
+ // those three have nothing in common but the word "reverted". See
78
+ // `../reclaimability.ts`.
79
+ const blocker = await readReclaimBlocker(read, streamId, deps.nowSeconds());
80
+ // `--close-first` exists precisely to clear a running stream, so it is the
81
+ // one caller allowed past that refusal. Nothing gets past "already
82
+ // reclaimed": there is no money left to take, with or without a close.
83
+ if (blocker !== null && !(closeFirst && blocker.kind === "running")) {
84
+ throwIfTerminal(blocker, streamId);
85
+ if (blocker.kind === "awaiting-checkpoint") {
86
+ out.note(describeAwaitingCheckpoint(blocker));
87
+ }
88
+ }
89
+
90
+ await assertCanPayGas(read, account.address, config.network);
91
+
92
+ const write = deps.createWriteClient(
93
+ config,
94
+ account,
95
+ writeClientOptions(wallet),
96
+ );
97
+
98
+ // A policy-enforced wallet (Privy) signs no mandate of its own: these are
99
+ // buyer-recovery actions, gated by the escrow on the buyer address and by
100
+ // the broker on `stream.buyer == wallet`. See `../spend-policy.ts`.
101
+ const recoveryMandate = async () =>
102
+ isPolicyEnforced(wallet)
103
+ ? null
104
+ : await signRecoveryMandate(account, config.chainId);
57
105
 
58
106
  // `--close-first` is TWO transactions, and the second one only simulates
59
107
  // successfully against post-close state. Sequencing them inside a single SDK
@@ -75,7 +123,7 @@ export const reclaimCommand: CommandSpec = {
75
123
  } else {
76
124
  closeResult = await closeBeforeReclaim(write, {
77
125
  streamId,
78
- signedMandate: await signRecoveryMandate(account, config.chainId),
126
+ signedMandate: await recoveryMandate(),
79
127
  spentSoFarUsdc: 0n,
80
128
  nowSeconds: deps.nowSeconds(),
81
129
  });
@@ -89,6 +137,19 @@ export const reclaimCommand: CommandSpec = {
89
137
  `closed in ${closeResult.txHash} — waiting for it to land …`,
90
138
  );
91
139
  await awaitClosed(read, streamId, deps.sleep, closeResult.txHash);
140
+ // A stream closed before any checkpoint covers its stop time is the
141
+ // ORDINARY outcome of a short trial run, not an exotic one — and it
142
+ // is the state `--close-first` leaves behind by construction. Say
143
+ // what is being waited for, so the retries below read as the
144
+ // advertised wait rather than as a command that has stalled.
145
+ const afterClose = await readReclaimBlocker(
146
+ read,
147
+ streamId,
148
+ deps.nowSeconds(),
149
+ );
150
+ if (afterClose?.kind === "awaiting-checkpoint") {
151
+ out.note(describeAwaitingCheckpoint(afterClose));
152
+ }
92
153
  }
93
154
  }
94
155
  }
@@ -97,7 +158,7 @@ export const reclaimCommand: CommandSpec = {
97
158
  write,
98
159
  async () => ({
99
160
  streamId,
100
- signedMandate: await signRecoveryMandate(account, config.chainId),
161
+ signedMandate: await recoveryMandate(),
101
162
  spentSoFarUsdc: 0n,
102
163
  nowSeconds: deps.nowSeconds(),
103
164
  }),
@@ -105,6 +166,15 @@ export const reclaimCommand: CommandSpec = {
105
166
  attempts,
106
167
  intervalMs: interval * 1_000,
107
168
  sleep: deps.sleep,
169
+ isRetryable: async (error) =>
170
+ await isWaitingForCheckpoint(
171
+ error,
172
+ read,
173
+ streamId,
174
+ deps.nowSeconds(),
175
+ ),
176
+ explainFailure: async (error) =>
177
+ await explainReclaimFailure(error, read, streamId, deps.nowSeconds()),
108
178
  onRetry: (attempt, reason) =>
109
179
  out.note(
110
180
  `reclaim blocked: no checkpoint covers the stream's stop time yet (attempt ${attempt}/${attempts}) — ${reason}`,
@@ -114,18 +184,31 @@ export const reclaimCommand: CommandSpec = {
114
184
  // give-up message names the path that needs no proof, and says whether
115
185
  // it is open yet.
116
186
  exhaustedHint: async () =>
117
- await describeLastResort(read, streamId, deps.nowSeconds()),
187
+ await describeLastResort(
188
+ read,
189
+ streamId,
190
+ deps.nowSeconds(),
191
+ escrowGenerationFor(config.chainId, config.escrow),
192
+ ),
118
193
  },
119
194
  );
120
195
 
121
196
  // Read the settled figure back only once the node we are reading from has
122
- // actually applied the reclaim. `sepolia.base.org` is load-balanced, so an
197
+ // actually applied the reclaim. The public RPC is load-balanced, so an
123
198
  // immediate read can land on a lagged node and report `settledCumulative: 0`
124
199
  // for a stream that just settled a non-zero amount — printing "settled to
125
200
  // operator 0 USDC" when it was not zero. Poll for `reclaimed` (which the
126
201
  // transaction we just landed sets) so the number beside it is from the same
127
202
  // post-reclaim state. Bounded, and falls back to the last value read.
128
203
  const stream = await readSettledStream(read, streamId, deps.sleep);
204
+ // The refund comes from the reclaim's own BuyerReclaimed event, never from
205
+ // a post-reclaim read: reclaim overwrites `deposit` with the operator's
206
+ // entitlement, so deposit − settled reads 0 after every reclaim.
207
+ const refunded = refundFromReceipt(
208
+ result.reclaimResult.receipt,
209
+ config.escrow,
210
+ streamId,
211
+ );
129
212
  out.result({
130
213
  streamId,
131
214
  txHash: result.reclaimResult.txHash,
@@ -135,6 +218,14 @@ export const reclaimCommand: CommandSpec = {
135
218
  // asked for".
136
219
  closeSkippedAlreadyClosed: closeFirst ? alreadyClosed : null,
137
220
  settledCumulative: stream?.settledCumulative ?? null,
221
+ // What the reclaim sent back: everything not settled to the operator.
222
+ // Only reported once the read reflects the reclaim, so a lagged node can
223
+ // never print a refund figure computed from pre-reclaim state.
224
+ refundedAmount: refunded,
225
+ // For an x402 stream the refund goes to the opener's payer, whoever sent
226
+ // the (permissionless) reclaim.
227
+ opener: stream?.opener ?? null,
228
+ refundedTo: stream === null ? null : (stream.payer ?? stream.buyer),
138
229
  });
139
230
  out.print("Reclaimed.");
140
231
  out.table([
@@ -149,6 +240,22 @@ export const reclaimCommand: CommandSpec = {
149
240
  ? []
150
241
  : ([
151
242
  ["settled to operator", formatUsdc(stream.settledCumulative)],
243
+ ...(refunded === null
244
+ ? []
245
+ : ([
246
+ [
247
+ (stream.opener ?? null) === null
248
+ ? "returned to buyer"
249
+ : "returned to payer",
250
+ formatUsdc(refunded),
251
+ ],
252
+ ] as const)),
253
+ ...((stream.opener ?? null) === null
254
+ ? []
255
+ : ([
256
+ ["via opener", stream.opener as string],
257
+ ["refunded to payer", stream.payer],
258
+ ] as const)),
152
259
  ] as const)),
153
260
  ]);
154
261
  },
@@ -190,7 +297,7 @@ function isAlreadyClosed(error: unknown): boolean {
190
297
  * Block until the node we read from reports the stream as closed.
191
298
  *
192
299
  * The close transaction's own receipt is awaited inside the SDK, but
193
- * `sepolia.base.org` is load-balanced: the receipt can come back from one node
300
+ * the public RPC is load-balanced: the receipt can come back from one node
194
301
  * while the very next `simulateContract` lands on another that has not imported
195
302
  * the block yet. That node simulates `reclaim` against PRE-close state and
196
303
  * reverts `StreamNotReclaimable()`. Confirming the close through the same read
@@ -226,8 +333,16 @@ export async function awaitClosed(
226
333
  * time. That is not an error condition — it is the normal ordering of a
227
334
  * checkpoint rail, and it happens on the FIRST attempt essentially every time.
228
335
  * A CLI that surfaced the raw revert would teach every user that reclaim is
229
- * broken, so the wait is built in and narrated. Any other failure is rethrown
230
- * immediately: only an open settlement window is retried.
336
+ * broken, so the wait is built in and narrated.
337
+ *
338
+ * What counts as "wait for the next checkpoint" is decided by `isRetryable`,
339
+ * NOT by this function. It used to be `instanceof SettlementWindowOpenError`
340
+ * alone, which missed the two ways the wait actually shows up in practice: the
341
+ * ORACLE refusing a proof for a stream it has not checkpointed yet (the SDK
342
+ * fetches the proof before it ever simulates, so the escrow's revert never
343
+ * happens), and the endpoint returning revert data the ABI cannot decode. Both
344
+ * killed the advertised retry on its first attempt — the second one right
345
+ * after `--close-first` had already spent the buyer's close.
231
346
  */
232
347
  export async function reclaimWithBoundedRetry(
233
348
  write: CliWriteClient,
@@ -236,6 +351,10 @@ export async function reclaimWithBoundedRetry(
236
351
  readonly attempts: number;
237
352
  readonly intervalMs: number;
238
353
  readonly sleep: (ms: number) => Promise<void>;
354
+ /** True when this failure is the checkpoint wait and will clear on its own. */
355
+ readonly isRetryable: (error: unknown) => Promise<boolean>;
356
+ /** Turn a failure that will NOT clear into something worth reading. */
357
+ readonly explainFailure: (error: unknown) => Promise<unknown>;
239
358
  readonly onRetry: (attempt: number, reason: string) => void;
240
359
  /** What to tell the buyer once the budget is spent. See `describeLastResort`. */
241
360
  readonly exhaustedHint?: () => Promise<string>;
@@ -246,7 +365,8 @@ export async function reclaimWithBoundedRetry(
246
365
  try {
247
366
  return await write.reclaimStream(await buildInput());
248
367
  } catch (error) {
249
- if (!isSettlementWindowOpen(error)) throw explainReclaimFailure(error);
368
+ if (!(await options.isRetryable(error)))
369
+ throw await options.explainFailure(error);
250
370
  lastError = error;
251
371
  if (attempt === options.attempts) break;
252
372
  options.onRetry(attempt, describeError(error));
@@ -282,25 +402,123 @@ async function describeLastResort(
282
402
  read: CliReadClient,
283
403
  streamId: `0x${string}`,
284
404
  fallbackNowSeconds: number,
405
+ generation: string,
285
406
  ): Promise<string> {
286
407
  const [stream, grace] = await Promise.all([
287
408
  read.getStreamV2(streamId),
288
409
  read.escapeGraceSeconds(),
289
410
  ]);
290
411
  const now = await readChainNow(read, fallbackNowSeconds);
291
- const window = escapeHatchWindow(stream, grace, now.seconds);
412
+ const window = escapeHatchWindow(stream, grace, now.seconds, generation);
292
413
  const escapeCommand = `metrik reclaim-unverified ${streamId} --forfeit-unclaimed`;
293
414
  return window.ready
294
415
  ? `the funds are not lost. The no-proof escape hatch is OPEN (${describeEscapeHatch(window)}): \`${escapeCommand}\` takes them back without the oracle — at the cost of forfeiting whatever the operator earned but has not yet claimed. Retrying this command only helps if the oracle comes back.`
295
416
  : `the funds are not lost, but retrying only helps if the oracle comes back — this path needs a proof from it. The no-proof escape hatch ${describeEscapeHatch(window)}; from then on \`${escapeCommand}\` takes them back without the oracle, forfeiting whatever the operator earned but has not yet claimed.`;
296
417
  }
297
418
 
419
+ /** Raise a blocker that will not clear on its own as a named CliError. */
420
+ function throwIfTerminal(
421
+ blocker: ReclaimBlocker,
422
+ streamId: `0x${string}`,
423
+ cause?: unknown,
424
+ ): void {
425
+ const terminal = describeTerminalBlocker(blocker, streamId);
426
+ if (terminal === null) return;
427
+ throw new CliError(terminal.message, {
428
+ exitCode: 4,
429
+ hint: terminal.hint,
430
+ ...(cause === undefined ? {} : { cause }),
431
+ });
432
+ }
433
+
434
+ /**
435
+ * Is this failure the checkpoint wait, i.e. will it clear on its own?
436
+ *
437
+ * Three shapes, in increasing order of how little the endpoint told us:
438
+ *
439
+ * 1. `SettlementWindowOpenError` — the escrow said so and the ABI decoded it.
440
+ * 2. `StreamNotInCheckpointError` / `CheckpointNotReadyError` — the ORACLE has
441
+ * no proof for this stream yet, so the SDK never reached the escrow at all.
442
+ * This is the state `--close-first` creates by construction on a stream
443
+ * closed before its first covering checkpoint, and treating it as fatal is
444
+ * what stranded a buyer's deposit in escrow for three minutes of manual
445
+ * retries while the CLI's own help promised it would wait.
446
+ * 3. A reclaim revert the ABI could not decode. Nothing in the error names the
447
+ * condition, so ask the chain instead — the state is authoritative and the
448
+ * answer is the same one the pre-flight computes.
449
+ *
450
+ * Case 3 is deliberately narrowed to `ReclaimFailedError`: an unrelated failure
451
+ * (no gas, a dead RPC) must still fail on its first attempt rather than burn
452
+ * twelve of them.
453
+ */
454
+ async function isWaitingForCheckpoint(
455
+ error: unknown,
456
+ read: Pick<
457
+ CliReadClient,
458
+ "getStreamV2" | "latestCheckpointCheckedAt" | "latestBlockTimestamp"
459
+ >,
460
+ streamId: `0x${string}`,
461
+ nowSeconds: number,
462
+ ): Promise<boolean> {
463
+ if (isSettlementWindowOpen(error)) return true;
464
+ if (hasNoCheckpointProofYet(error)) return true;
465
+ if (!isReclaimRevert(error)) return false;
466
+ const blocker = await readReclaimBlocker(read, streamId, nowSeconds).catch(
467
+ () => null,
468
+ );
469
+ return blocker?.kind === "awaiting-checkpoint";
470
+ }
471
+
472
+ /**
473
+ * The oracle could not supply a proof yet: it has no leaf for this stream in
474
+ * the latest checkpoint (404), or no internally consistent checkpoint is ready
475
+ * (503). Both resolve when the next checkpoint lands, ~15-22s apart.
476
+ */
477
+ function hasNoCheckpointProofYet(error: unknown): boolean {
478
+ for (let current: unknown = error, depth = 0; depth < 8; depth++) {
479
+ if (
480
+ current instanceof StreamNotInCheckpointError ||
481
+ current instanceof CheckpointNotReadyError
482
+ ) {
483
+ return true;
484
+ }
485
+ if (!(current instanceof Error) || current.cause === undefined)
486
+ return false;
487
+ current = current.cause;
488
+ }
489
+ return false;
490
+ }
491
+
492
+ /** True when the escrow rejected the reclaim, whatever the reason decoded to. */
493
+ function isReclaimRevert(error: unknown): boolean {
494
+ for (let current: unknown = error, depth = 0; depth < 8; depth++) {
495
+ if (current instanceof ReclaimFailedError) return true;
496
+ if (!(current instanceof Error) || current.cause === undefined)
497
+ return false;
498
+ current = current.cause;
499
+ }
500
+ return false;
501
+ }
502
+
298
503
  /**
299
- * `StreamNotReclaimable()` means exactly one thing: the stream is neither closed
300
- * nor past its expiry. Say that, and say which command fixes it, instead of
301
- * handing the reader a selector.
504
+ * Name a failure that will not clear.
505
+ *
506
+ * `StreamNotReclaimable()` means exactly one thing when the ABI decodes it: the
507
+ * stream is neither closed nor past its expiry. When it does NOT decode — the
508
+ * live failure mode on the public endpoint — the chain is asked the same
509
+ * question, so "already reclaimed" and "still running" are still told apart and
510
+ * still say which command fixes them. The raw error is kept as the `cause`
511
+ * either way.
302
512
  */
303
- function explainReclaimFailure(error: unknown): unknown {
513
+ async function explainReclaimFailure(
514
+ error: unknown,
515
+ read: Pick<
516
+ CliReadClient,
517
+ "getStreamV2" | "latestCheckpointCheckedAt" | "latestBlockTimestamp"
518
+ >,
519
+ streamId: `0x${string}`,
520
+ nowSeconds: number,
521
+ ): Promise<unknown> {
304
522
  if (!(error instanceof Error)) return error;
305
523
  for (let current: unknown = error, depth = 0; depth < 8; depth++) {
306
524
  if (!(current instanceof Error)) break;
@@ -309,7 +527,7 @@ function explainReclaimFailure(error: unknown): unknown {
309
527
  "the stream is still running, so there is nothing to reclaim yet",
310
528
  {
311
529
  exitCode: 4,
312
- hint: "stop the clock first — `metrik close <streamId>`, or re-run this command with --close-first — or wait for the stream to expire.",
530
+ hint: `stop the clock first — \`metrik close ${streamId}\`, or re-run this command with --close-first — or wait for the stream to expire.`,
313
531
  cause: error,
314
532
  },
315
533
  );
@@ -317,6 +535,18 @@ function explainReclaimFailure(error: unknown): unknown {
317
535
  if (current.cause === undefined) break;
318
536
  current = current.cause;
319
537
  }
538
+ if (isReclaimRevert(error)) {
539
+ const blocker = await readReclaimBlocker(read, streamId, nowSeconds).catch(
540
+ () => null,
541
+ );
542
+ if (blocker !== null) {
543
+ try {
544
+ throwIfTerminal(blocker, streamId, error);
545
+ } catch (named) {
546
+ return named;
547
+ }
548
+ }
549
+ }
320
550
  return error;
321
551
  }
322
552
 
@@ -342,6 +572,31 @@ const SETTLED_READ_ATTEMPTS = 5;
342
572
  // `--interval`), so the two waits are tellable apart in tests and in logs.
343
573
  const SETTLED_READ_INTERVAL_MS = 500;
344
574
 
575
+ /**
576
+ * The amount the reclaim returned, decoded from its BuyerReclaimed event (same
577
+ * signature on V2 and V3). Null when the receipt carries no matching log, so
578
+ * the CLI prints nothing rather than a guessed figure.
579
+ */
580
+ export function refundFromReceipt(
581
+ receipt: { logs?: readonly Log[] } | undefined,
582
+ escrow: string,
583
+ streamId: `0x${string}`,
584
+ ): bigint | null {
585
+ const logs = receipt?.logs;
586
+ if (!Array.isArray(logs)) return null;
587
+ const events = parseEventLogs({
588
+ abi: streamEscrowV2Abi,
589
+ eventName: "BuyerReclaimed",
590
+ logs: logs.filter(
591
+ (log) => log.address?.toLowerCase() === escrow.toLowerCase(),
592
+ ),
593
+ });
594
+ const match = events.find(
595
+ (event) => event.args.streamId.toLowerCase() === streamId.toLowerCase(),
596
+ );
597
+ return match === undefined ? null : match.args.amount;
598
+ }
599
+
345
600
  async function readSettledStream(
346
601
  read: { getStreamV2(streamId: `0x${string}`): Promise<StreamV2> },
347
602
  streamId: `0x${string}`,