@absol-labs/agent 0.9.1 → 0.11.0

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 (293) hide show
  1. package/README.md +184 -26
  2. package/dist/capability/invocation-capability.d.ts +18 -6
  3. package/dist/capability/invocation-capability.d.ts.map +1 -1
  4. package/dist/capability/invocation-capability.js.map +1 -1
  5. package/dist/cli/amounts.d.ts +26 -0
  6. package/dist/cli/amounts.d.ts.map +1 -0
  7. package/dist/cli/amounts.js +71 -0
  8. package/dist/cli/amounts.js.map +1 -0
  9. package/dist/cli/args.d.ts +32 -0
  10. package/dist/cli/args.d.ts.map +1 -0
  11. package/dist/cli/args.js +119 -0
  12. package/dist/cli/args.js.map +1 -0
  13. package/dist/cli/attesters.d.ts +51 -0
  14. package/dist/cli/attesters.d.ts.map +1 -0
  15. package/dist/cli/attesters.js +58 -0
  16. package/dist/cli/attesters.js.map +1 -0
  17. package/dist/cli/bin.d.ts +3 -0
  18. package/dist/cli/bin.d.ts.map +1 -0
  19. package/dist/cli/bin.js +5 -0
  20. package/dist/cli/bin.js.map +1 -0
  21. package/dist/cli/commands/claim.d.ts +13 -0
  22. package/dist/cli/commands/claim.d.ts.map +1 -0
  23. package/dist/cli/commands/claim.js +102 -0
  24. package/dist/cli/commands/claim.js.map +1 -0
  25. package/dist/cli/commands/close.d.ts +3 -0
  26. package/dist/cli/commands/close.d.ts.map +1 -0
  27. package/dist/cli/commands/close.js +55 -0
  28. package/dist/cli/commands/close.js.map +1 -0
  29. package/dist/cli/commands/discover.d.ts +3 -0
  30. package/dist/cli/commands/discover.d.ts.map +1 -0
  31. package/dist/cli/commands/discover.js +183 -0
  32. package/dist/cli/commands/discover.js.map +1 -0
  33. package/dist/cli/commands/invoke.d.ts +3 -0
  34. package/dist/cli/commands/invoke.d.ts.map +1 -0
  35. package/dist/cli/commands/invoke.js +142 -0
  36. package/dist/cli/commands/invoke.js.map +1 -0
  37. package/dist/cli/commands/mcp.d.ts +3 -0
  38. package/dist/cli/commands/mcp.d.ts.map +1 -0
  39. package/dist/cli/commands/mcp.js +113 -0
  40. package/dist/cli/commands/mcp.js.map +1 -0
  41. package/dist/cli/commands/open.d.ts +3 -0
  42. package/dist/cli/commands/open.d.ts.map +1 -0
  43. package/dist/cli/commands/open.js +331 -0
  44. package/dist/cli/commands/open.js.map +1 -0
  45. package/dist/cli/commands/reclaim-unverified.d.ts +3 -0
  46. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -0
  47. package/dist/cli/commands/reclaim-unverified.js +172 -0
  48. package/dist/cli/commands/reclaim-unverified.js.map +1 -0
  49. package/dist/cli/commands/reclaim.d.ts +33 -0
  50. package/dist/cli/commands/reclaim.d.ts.map +1 -0
  51. package/dist/cli/commands/reclaim.js +308 -0
  52. package/dist/cli/commands/reclaim.js.map +1 -0
  53. package/dist/cli/commands/status.d.ts +3 -0
  54. package/dist/cli/commands/status.d.ts.map +1 -0
  55. package/dist/cli/commands/status.js +43 -0
  56. package/dist/cli/commands/status.js.map +1 -0
  57. package/dist/cli/commands/streams.d.ts +3 -0
  58. package/dist/cli/commands/streams.d.ts.map +1 -0
  59. package/dist/cli/commands/streams.js +257 -0
  60. package/dist/cli/commands/streams.js.map +1 -0
  61. package/dist/cli/commands/watch.d.ts +3 -0
  62. package/dist/cli/commands/watch.d.ts.map +1 -0
  63. package/dist/cli/commands/watch.js +55 -0
  64. package/dist/cli/commands/watch.js.map +1 -0
  65. package/dist/cli/config.d.ts +61 -0
  66. package/dist/cli/config.d.ts.map +1 -0
  67. package/dist/cli/config.js +140 -0
  68. package/dist/cli/config.js.map +1 -0
  69. package/dist/cli/context.d.ts +29 -0
  70. package/dist/cli/context.d.ts.map +1 -0
  71. package/dist/cli/context.js +2 -0
  72. package/dist/cli/context.js.map +1 -0
  73. package/dist/cli/deps.d.ts +191 -0
  74. package/dist/cli/deps.d.ts.map +1 -0
  75. package/dist/cli/deps.js +277 -0
  76. package/dist/cli/deps.js.map +1 -0
  77. package/dist/cli/descriptor.d.ts +61 -0
  78. package/dist/cli/descriptor.d.ts.map +1 -0
  79. package/dist/cli/descriptor.js +78 -0
  80. package/dist/cli/descriptor.js.map +1 -0
  81. package/dist/cli/errors.d.ts +48 -0
  82. package/dist/cli/errors.d.ts.map +1 -0
  83. package/dist/cli/errors.js +129 -0
  84. package/dist/cli/errors.js.map +1 -0
  85. package/dist/cli/escape-hatch.d.ts +50 -0
  86. package/dist/cli/escape-hatch.d.ts.map +1 -0
  87. package/dist/cli/escape-hatch.js +45 -0
  88. package/dist/cli/escape-hatch.js.map +1 -0
  89. package/dist/cli/index.d.ts +8 -0
  90. package/dist/cli/index.d.ts.map +1 -0
  91. package/dist/cli/index.js +7 -0
  92. package/dist/cli/index.js.map +1 -0
  93. package/dist/cli/listings.d.ts +15 -0
  94. package/dist/cli/listings.d.ts.map +1 -0
  95. package/dist/cli/listings.js +56 -0
  96. package/dist/cli/listings.js.map +1 -0
  97. package/dist/cli/mandate.d.ts +66 -0
  98. package/dist/cli/mandate.d.ts.map +1 -0
  99. package/dist/cli/mandate.js +61 -0
  100. package/dist/cli/mandate.js.map +1 -0
  101. package/dist/cli/output.d.ts +33 -0
  102. package/dist/cli/output.d.ts.map +1 -0
  103. package/dist/cli/output.js +34 -0
  104. package/dist/cli/output.js.map +1 -0
  105. package/dist/cli/preflight.d.ts +38 -0
  106. package/dist/cli/preflight.d.ts.map +1 -0
  107. package/dist/cli/preflight.js +72 -0
  108. package/dist/cli/preflight.js.map +1 -0
  109. package/dist/cli/privy.d.ts +83 -0
  110. package/dist/cli/privy.d.ts.map +1 -0
  111. package/dist/cli/privy.js +230 -0
  112. package/dist/cli/privy.js.map +1 -0
  113. package/dist/cli/progress.d.ts +36 -0
  114. package/dist/cli/progress.d.ts.map +1 -0
  115. package/dist/cli/progress.js +40 -0
  116. package/dist/cli/progress.js.map +1 -0
  117. package/dist/cli/run.d.ts +25 -0
  118. package/dist/cli/run.d.ts.map +1 -0
  119. package/dist/cli/run.js +173 -0
  120. package/dist/cli/run.js.map +1 -0
  121. package/dist/cli/scan-range.d.ts +55 -0
  122. package/dist/cli/scan-range.d.ts.map +1 -0
  123. package/dist/cli/scan-range.js +57 -0
  124. package/dist/cli/scan-range.js.map +1 -0
  125. package/dist/cli/spend-policy.d.ts +60 -0
  126. package/dist/cli/spend-policy.d.ts.map +1 -0
  127. package/dist/cli/spend-policy.js +65 -0
  128. package/dist/cli/spend-policy.js.map +1 -0
  129. package/dist/cli/status.d.ts +90 -0
  130. package/dist/cli/status.d.ts.map +1 -0
  131. package/dist/cli/status.js +252 -0
  132. package/dist/cli/status.js.map +1 -0
  133. package/dist/cli/timeout.d.ts +30 -0
  134. package/dist/cli/timeout.d.ts.map +1 -0
  135. package/dist/cli/timeout.js +55 -0
  136. package/dist/cli/timeout.js.map +1 -0
  137. package/dist/cli/version.d.ts +12 -0
  138. package/dist/cli/version.d.ts.map +1 -0
  139. package/dist/cli/version.js +22 -0
  140. package/dist/cli/version.js.map +1 -0
  141. package/dist/cli/wallet.d.ts +164 -0
  142. package/dist/cli/wallet.d.ts.map +1 -0
  143. package/dist/cli/wallet.js +234 -0
  144. package/dist/cli/wallet.js.map +1 -0
  145. package/dist/frameworks/crewai.d.ts +10 -0
  146. package/dist/frameworks/crewai.d.ts.map +1 -1
  147. package/dist/frameworks/crewai.js +12 -2
  148. package/dist/frameworks/crewai.js.map +1 -1
  149. package/dist/gateway/caller-auth-gateway.d.ts +38 -3
  150. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  151. package/dist/gateway/caller-auth-gateway.js +25 -1
  152. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  153. package/dist/gateway/http-server.d.ts +53 -0
  154. package/dist/gateway/http-server.d.ts.map +1 -1
  155. package/dist/gateway/http-server.js +189 -4
  156. package/dist/gateway/http-server.js.map +1 -1
  157. package/dist/gateway/server-entry.d.ts +1 -0
  158. package/dist/gateway/server-entry.js +1 -0
  159. package/dist/gateway/server-entry.js.map +1 -1
  160. package/dist/gateway/x402-mode.d.ts +123 -0
  161. package/dist/gateway/x402-mode.d.ts.map +1 -0
  162. package/dist/gateway/x402-mode.js +212 -0
  163. package/dist/gateway/x402-mode.js.map +1 -0
  164. package/dist/index.d.ts +6 -4
  165. package/dist/index.d.ts.map +1 -1
  166. package/dist/index.js +8 -3
  167. package/dist/index.js.map +1 -1
  168. package/dist/mandates/env.d.ts +3 -2
  169. package/dist/mandates/env.d.ts.map +1 -1
  170. package/dist/mandates/env.js +3 -2
  171. package/dist/mandates/env.js.map +1 -1
  172. package/dist/mcp/http-server.d.ts +1 -0
  173. package/dist/mcp/http-server.js +1 -0
  174. package/dist/mcp/http-server.js.map +1 -1
  175. package/dist/mcp/server.d.ts +49 -0
  176. package/dist/mcp/server.d.ts.map +1 -1
  177. package/dist/mcp/server.js +533 -25
  178. package/dist/mcp/server.js.map +1 -1
  179. package/dist/mcp/service-invoker.d.ts +97 -0
  180. package/dist/mcp/service-invoker.d.ts.map +1 -0
  181. package/dist/mcp/service-invoker.js +167 -0
  182. package/dist/mcp/service-invoker.js.map +1 -0
  183. package/dist/mcp/stdio.d.ts +1 -0
  184. package/dist/mcp/stdio.js +1 -0
  185. package/dist/mcp/stdio.js.map +1 -1
  186. package/dist/sdk/client.d.ts +25 -1
  187. package/dist/sdk/client.d.ts.map +1 -1
  188. package/dist/sdk/client.js +20 -1
  189. package/dist/sdk/client.js.map +1 -1
  190. package/dist/sdk/invoke.d.ts +19 -4
  191. package/dist/sdk/invoke.d.ts.map +1 -1
  192. package/dist/sdk/invoke.js +69 -5
  193. package/dist/sdk/invoke.js.map +1 -1
  194. package/dist/x402/delivery-oracle.d.ts +108 -0
  195. package/dist/x402/delivery-oracle.d.ts.map +1 -0
  196. package/dist/x402/delivery-oracle.js +174 -0
  197. package/dist/x402/delivery-oracle.js.map +1 -0
  198. package/dist/x402/facilitator-server-entry.d.ts +3 -0
  199. package/dist/x402/facilitator-server-entry.d.ts.map +1 -0
  200. package/dist/x402/facilitator-server-entry.js +27 -0
  201. package/dist/x402/facilitator-server-entry.js.map +1 -0
  202. package/dist/x402/facilitator-server.d.ts +73 -0
  203. package/dist/x402/facilitator-server.d.ts.map +1 -0
  204. package/dist/x402/facilitator-server.js +305 -0
  205. package/dist/x402/facilitator-server.js.map +1 -0
  206. package/dist/x402/facilitator.d.ts +69 -17
  207. package/dist/x402/facilitator.d.ts.map +1 -1
  208. package/dist/x402/facilitator.js +76 -33
  209. package/dist/x402/facilitator.js.map +1 -1
  210. package/dist/x402/payment-requirements.d.ts +106 -0
  211. package/dist/x402/payment-requirements.d.ts.map +1 -0
  212. package/dist/x402/payment-requirements.js +138 -0
  213. package/dist/x402/payment-requirements.js.map +1 -0
  214. package/dist/x402/relayer.d.ts +41 -0
  215. package/dist/x402/relayer.d.ts.map +1 -0
  216. package/dist/x402/relayer.js +64 -0
  217. package/dist/x402/relayer.js.map +1 -0
  218. package/dist/x402/resource-index.d.ts +75 -0
  219. package/dist/x402/resource-index.d.ts.map +1 -0
  220. package/dist/x402/resource-index.js +126 -0
  221. package/dist/x402/resource-index.js.map +1 -0
  222. package/dist/x402/verified-delivery-facilitator.d.ts +285 -0
  223. package/dist/x402/verified-delivery-facilitator.d.ts.map +1 -0
  224. package/dist/x402/verified-delivery-facilitator.js +423 -0
  225. package/dist/x402/verified-delivery-facilitator.js.map +1 -0
  226. package/dist/zktls/t2-delivery-proof.d.ts +6 -6
  227. package/docs/agent-layer.md +14 -9
  228. package/docs/cli.md +705 -0
  229. package/docs/crewai.md +6 -2
  230. package/docs/eliza.md +6 -1
  231. package/docs/mcp-hosted.md +7 -5
  232. package/docs/mcp-reference.md +175 -0
  233. package/docs/quickstart.md +36 -9
  234. package/docs/threat-model.md +4 -3
  235. package/docs/x402.md +648 -0
  236. package/package.json +33 -10
  237. package/src/capability/invocation-capability.ts +17 -1
  238. package/src/cli/amounts.ts +102 -0
  239. package/src/cli/args.ts +150 -0
  240. package/src/cli/attesters.ts +86 -0
  241. package/src/cli/bin.ts +5 -0
  242. package/src/cli/commands/claim.ts +126 -0
  243. package/src/cli/commands/close.ts +69 -0
  244. package/src/cli/commands/discover.ts +228 -0
  245. package/src/cli/commands/invoke.ts +178 -0
  246. package/src/cli/commands/mcp.ts +135 -0
  247. package/src/cli/commands/open.ts +397 -0
  248. package/src/cli/commands/reclaim-unverified.ts +218 -0
  249. package/src/cli/commands/reclaim.ts +377 -0
  250. package/src/cli/commands/status.ts +56 -0
  251. package/src/cli/commands/streams.ts +344 -0
  252. package/src/cli/commands/watch.ts +70 -0
  253. package/src/cli/config.ts +208 -0
  254. package/src/cli/context.ts +30 -0
  255. package/src/cli/deps.ts +528 -0
  256. package/src/cli/descriptor.ts +133 -0
  257. package/src/cli/errors.ts +162 -0
  258. package/src/cli/escape-hatch.ts +92 -0
  259. package/src/cli/index.ts +27 -0
  260. package/src/cli/listings.ts +77 -0
  261. package/src/cli/mandate.ts +126 -0
  262. package/src/cli/output.ts +63 -0
  263. package/src/cli/preflight.ts +88 -0
  264. package/src/cli/privy.ts +396 -0
  265. package/src/cli/progress.ts +75 -0
  266. package/src/cli/run.ts +209 -0
  267. package/src/cli/scan-range.ts +76 -0
  268. package/src/cli/spend-policy.ts +139 -0
  269. package/src/cli/status.ts +381 -0
  270. package/src/cli/timeout.ts +64 -0
  271. package/src/cli/version.ts +21 -0
  272. package/src/cli/wallet.ts +372 -0
  273. package/src/frameworks/crewai.ts +13 -2
  274. package/src/gateway/caller-auth-gateway.ts +77 -6
  275. package/src/gateway/http-server.ts +280 -6
  276. package/src/gateway/server-entry.ts +1 -0
  277. package/src/gateway/x402-mode.ts +350 -0
  278. package/src/index.ts +40 -5
  279. package/src/mandates/env.ts +3 -2
  280. package/src/mcp/http-server.ts +1 -0
  281. package/src/mcp/server.ts +706 -30
  282. package/src/mcp/service-invoker.ts +285 -0
  283. package/src/mcp/stdio.ts +1 -0
  284. package/src/sdk/client.ts +33 -1
  285. package/src/sdk/invoke.ts +90 -12
  286. package/src/x402/delivery-oracle.ts +285 -0
  287. package/src/x402/facilitator-server-entry.ts +35 -0
  288. package/src/x402/facilitator-server.ts +445 -0
  289. package/src/x402/facilitator.ts +125 -46
  290. package/src/x402/payment-requirements.ts +231 -0
  291. package/src/x402/relayer.ts +122 -0
  292. package/src/x402/resource-index.ts +228 -0
  293. package/src/x402/verified-delivery-facilitator.ts +744 -0
@@ -0,0 +1,397 @@
1
+ import { MandateDeniedError } from "../../sdk/client.js";
2
+ import { booleanFlag, requiredStringFlag, stringFlag } from "../args.js";
3
+ import {
4
+ formatDuration,
5
+ formatUsdc,
6
+ parseAmountUnits,
7
+ parseDurationSeconds,
8
+ parseUsdcAmount,
9
+ } from "../amounts.js";
10
+ import { assertTestnetRpc, parseBytes32 } from "../config.js";
11
+ import { writeClientOptions } from "../deps.js";
12
+ import { CliError, configError } from "../errors.js";
13
+ import type { PrivySpendPolicy } from "../privy.js";
14
+ import {
15
+ assertWithinOperatorCaps,
16
+ assertWithinSpendPolicy,
17
+ } from "../spend-policy.js";
18
+ import { cliMandateCaps, signCliMandate } from "../mandate.js";
19
+ import { assertCanFundStream, assertCanPayGas } from "../preflight.js";
20
+ import {
21
+ isPolicyEnforced,
22
+ optionalCliWallet,
23
+ resolveCliWallet,
24
+ PRIVATE_KEY_ENV,
25
+ } from "../wallet.js";
26
+ import { resolveListing } from "../listings.js";
27
+ import type { CommandContext, CommandSpec } from "../context.js";
28
+
29
+ export const openCommand: CommandSpec = {
30
+ name: "open",
31
+ summary: "Hire a verified service: escrow USDC into a metered stream",
32
+ usage:
33
+ "metrik open --service <serviceRef> --rate <usdc/s> --budget <usdc> --duration <seconds> [--units atomic] [--dry-run]",
34
+ flags: [
35
+ "service",
36
+ "rate",
37
+ "budget",
38
+ "duration",
39
+ "units",
40
+ "operator",
41
+ "dry-run",
42
+ ],
43
+ signs: true,
44
+ async run(context: CommandContext): Promise<void> {
45
+ const { args, config, env, out, deps } = context;
46
+ const units = parseAmountUnits(stringFlag(args, "units"));
47
+ const serviceRef = parseBytes32(
48
+ requiredStringFlag(args, "service"),
49
+ "--service",
50
+ );
51
+ const budgetUsdc = parseUsdcAmount(
52
+ requiredStringFlag(args, "budget"),
53
+ units,
54
+ "budget",
55
+ );
56
+ const ratePerSecondUsdc = parseUsdcAmount(
57
+ requiredStringFlag(args, "rate"),
58
+ units,
59
+ "rate",
60
+ );
61
+ const durationSeconds = parseDurationSeconds(
62
+ requiredStringFlag(args, "duration"),
63
+ );
64
+ const dryRun = booleanFlag(args, "dry-run");
65
+
66
+ if (budgetUsdc <= 0n || ratePerSecondUsdc <= 0n) {
67
+ throw new CliError("--budget and --rate must both be greater than zero", {
68
+ exitCode: 2,
69
+ });
70
+ }
71
+ const maxSpend = ratePerSecondUsdc * BigInt(durationSeconds);
72
+ if (maxSpend > budgetUsdc) {
73
+ out.note(
74
+ `note: --rate x --duration (${formatUsdc(maxSpend)}) exceeds --budget; accrual is capped at the deposit and the stream simply runs out of budget before it expires.`,
75
+ );
76
+ }
77
+
78
+ // `--dry-run` broadcasts nothing, so it does NOT require a key: it is the
79
+ // look-before-you-leap path, and the person most in need of it is exactly
80
+ // the one who has not funded a wallet yet. Without a key the preview prints
81
+ // the intended call and says which parts (allowance, simulation, mandate
82
+ // signature) could not be produced, rather than inventing them.
83
+ const wallet = dryRun
84
+ ? await optionalCliWallet(env, { note: out.note, rpcUrl: config.rpcUrl })
85
+ : await resolveCliWallet(env, { note: out.note, rpcUrl: config.rpcUrl });
86
+ const account = wallet?.account ?? null;
87
+ const read = deps.createReadClient(config);
88
+ await assertTestnetRpc(config, () => read.getChainId());
89
+ if (account !== null && !dryRun) {
90
+ // Both balances, before anything is signed. Gas first: a wallet with no
91
+ // ETH cannot send the approval that the USDC check exists to protect.
92
+ await assertCanPayGas(read, account.address);
93
+ await assertCanFundStream(read, account.address, budgetUsdc);
94
+ }
95
+
96
+ const operatorOverride = stringFlag(args, "operator");
97
+ const listing = await resolveListing(context, serviceRef, {
98
+ required: operatorOverride === undefined,
99
+ });
100
+ const operator = (operatorOverride ?? listing?.operator) as
101
+ | `0x${string}`
102
+ | undefined;
103
+ if (operator === undefined) {
104
+ throw configError(
105
+ `serviceRef ${serviceRef} is not in the registry and no --operator was given`,
106
+ "run `metrik discover` to find a listed service, or pass --operator explicitly.",
107
+ );
108
+ }
109
+
110
+ const spend = { operator, budgetUsdc, ratePerSecondUsdc, durationSeconds };
111
+ const mandateCaps = cliMandateCaps(spend, env);
112
+
113
+ // A policy-enforced wallet (Privy) is capped by its broker, not by a
114
+ // mandate this process signs for itself. Read the REAL ceiling and hold it
115
+ // to that instead — see `../spend-policy.ts` for why that substitution is
116
+ // the stronger of the two, and why re-adding the self-signed mandate here
117
+ // would both weaken it and break the wallet outright.
118
+ const policy: PrivySpendPolicy | null =
119
+ wallet !== null && isPolicyEnforced(wallet)
120
+ ? await wallet.readSpendPolicy()
121
+ : null;
122
+ if (policy !== null && !dryRun) {
123
+ assertWithinSpendPolicy(policy, spend);
124
+ // The operator's own standing ceiling still applies, and may be TIGHTER
125
+ // than the broker's. Enforced here because no mandate is signed to carry
126
+ // it — "policy-enforced" retires the self-signed mandate, not the caps
127
+ // the operator deliberately configured.
128
+ if (mandateCaps.source === "env") {
129
+ assertWithinOperatorCaps(mandateCaps.caps, spend);
130
+ }
131
+ }
132
+
133
+ const mandate =
134
+ account === null || policy !== null
135
+ ? null
136
+ : await signCliMandate(account, spend, {
137
+ chainId: config.chainId,
138
+ env,
139
+ });
140
+
141
+ const write = deps.createWriteClient(
142
+ config,
143
+ account,
144
+ writeClientOptions(wallet),
145
+ );
146
+ const hire = {
147
+ operator,
148
+ serviceRef,
149
+ budgetUsdc,
150
+ ratePerSecond: ratePerSecondUsdc,
151
+ maxDurationSeconds: durationSeconds,
152
+ };
153
+
154
+ if (dryRun) {
155
+ const preview = await write.buildHireTx(hire);
156
+ // A dry run simulates `openStream` from the CURRENT chain state, which
157
+ // does not include the USDC approval the real command sends first. So the
158
+ // simulation reverts on the allowance every time the allowance is not
159
+ // already in place — which, on a first hire, is always. Reporting that as
160
+ // `simulationError` made the one command meant to build confidence
161
+ // predict failure on first use. It is a known ordering step, not a fault.
162
+ const approvalBlocksSimulation = isApprovalOrdering(preview);
163
+ // Reported, never enforced: a dry run broadcasts nothing, so an
164
+ // underfunded wallet is information here, not a refusal. The same read
165
+ // IS a gate on the real path (`assertCanFundStream` above).
166
+ const usdcBalance =
167
+ account === null
168
+ ? null
169
+ : await read.usdcBalance(account.address).catch(() => null);
170
+ out.result({
171
+ dryRun: true,
172
+ buyer: account?.address ?? null,
173
+ operator,
174
+ serviceRef,
175
+ budgetUsdc,
176
+ ratePerSecondUsdc,
177
+ durationSeconds,
178
+ mandateSource: mandateCaps.source,
179
+ mandateSigned: mandate !== null,
180
+ spendPolicy:
181
+ policy === null
182
+ ? null
183
+ : {
184
+ brokerUrl: policy.brokerUrl,
185
+ maxTotalUsdc: policy.maxTotalUsdc,
186
+ maxRatePerSecondUsdc: policy.maxRatePerSecondUsdc,
187
+ maxDurationSeconds: policy.maxDurationSeconds,
188
+ fits:
189
+ budgetUsdc <= policy.maxTotalUsdc &&
190
+ ratePerSecondUsdc <= policy.maxRatePerSecondUsdc &&
191
+ durationSeconds <= policy.maxDurationSeconds,
192
+ },
193
+ to: preview.to,
194
+ data: preview.data,
195
+ chainId: preview.chainId,
196
+ approval: {
197
+ required: preview.approval.required,
198
+ amount: preview.approval.amount,
199
+ spender: preview.approval.spender,
200
+ token: preview.approval.token,
201
+ },
202
+ usdcBalance,
203
+ usdcSufficient: usdcBalance === null ? null : usdcBalance >= budgetUsdc,
204
+ simulatedStreamId: preview.simulatedStreamId,
205
+ // Mutually exclusive by construction: an allowance-ordering revert is
206
+ // reported as `simulationPendingApproval`, never as an error, so a
207
+ // script branching on `simulationError` does not treat the expected
208
+ // first-hire path as a failure.
209
+ simulationPendingApproval: approvalBlocksSimulation,
210
+ ...(preview.simulationError === undefined || approvalBlocksSimulation
211
+ ? {}
212
+ : { simulationError: preview.simulationError }),
213
+ });
214
+ out.print("Dry run — nothing was broadcast.");
215
+ out.table([
216
+ [
217
+ "buyer",
218
+ account?.address ??
219
+ `unknown (no wallet configured — set ${PRIVATE_KEY_ENV} or the CDP variables to preview from a real address)`,
220
+ ],
221
+ ["operator", operator],
222
+ ["serviceRef", serviceRef],
223
+ ["budget", formatUsdc(budgetUsdc)],
224
+ ["rate", `${formatUsdc(ratePerSecondUsdc)} / second`],
225
+ ["duration", formatDuration(durationSeconds)],
226
+ [
227
+ "mandate",
228
+ mandate !== null
229
+ ? `self-signed, caps from ${mandateCaps.source}`
230
+ : policy !== null
231
+ ? "not signed — this wallet's ceiling is enforced by its broker, which is stronger"
232
+ : `not signed (no wallet); caps would come from ${mandateCaps.source}`,
233
+ ],
234
+ ...(policy === null
235
+ ? []
236
+ : ([
237
+ [
238
+ "spend policy",
239
+ `${formatUsdc(policy.maxTotalUsdc)} total, ${policy.maxRatePerSecondUsdc} atomic/second, ${formatDuration(policy.maxDurationSeconds)} max — enforced by ${policy.brokerUrl}`,
240
+ ],
241
+ [
242
+ "fits policy",
243
+ budgetUsdc <= policy.maxTotalUsdc &&
244
+ ratePerSecondUsdc <= policy.maxRatePerSecondUsdc &&
245
+ durationSeconds <= policy.maxDurationSeconds
246
+ ? "yes"
247
+ : "NO — a real open would be refused before broadcasting",
248
+ ],
249
+ ] as [string, string][])),
250
+ ["escrow", preview.to],
251
+ [
252
+ "usdc balance",
253
+ usdcBalance === null
254
+ ? "unread (no wallet configured)"
255
+ : usdcBalance >= budgetUsdc
256
+ ? `${formatUsdc(usdcBalance)} (covers the budget)`
257
+ : `${formatUsdc(usdcBalance)} — SHORT of the ${formatUsdc(budgetUsdc)} budget; a real open would be refused`,
258
+ ],
259
+ [
260
+ "usdc approval",
261
+ preview.approval.required === null
262
+ ? `${formatUsdc(preview.approval.amount)} (allowance unread)`
263
+ : preview.approval.required
264
+ ? `${formatUsdc(preview.approval.amount)} (required)`
265
+ : "already sufficient",
266
+ ],
267
+ [
268
+ "simulated streamId",
269
+ preview.simulatedStreamId ??
270
+ (approvalBlocksSimulation
271
+ ? "not simulated yet — the bounded USDC approval above is sent FIRST by a real open, and the escrow cannot pull the deposit until it lands. Expected on a first hire, not a failure."
272
+ : `unavailable (${preview.simulationError ?? "simulation did not run"})`),
273
+ ],
274
+ ]);
275
+ out.print("");
276
+ out.print(METERING_NOTE);
277
+ return;
278
+ }
279
+
280
+ out.note(
281
+ `opening a stream on chain ${config.chainId} for ${formatUsdc(budgetUsdc)} …`,
282
+ );
283
+ // Unreachable without a wallet: `resolveCliWallet` above throws when the
284
+ // command is not a dry run. The narrowing keeps that a type-level fact.
285
+ if (account === null) {
286
+ throw configError(
287
+ "this command signs a transaction and needs a wallet, and none was resolved",
288
+ );
289
+ }
290
+
291
+ let result;
292
+ try {
293
+ result = await write.openVerifiedStream({
294
+ operator,
295
+ serviceRef,
296
+ budgetUsdc,
297
+ ratePerSecondUsdc,
298
+ maxDurationSeconds: durationSeconds,
299
+ // `null` for a policy-enforced wallet: the ceiling was checked against
300
+ // the broker's own policy above, and the broker checks it again on the
301
+ // transaction it signs.
302
+ signedMandate: mandate === null ? null : mandate.signed,
303
+ spentSoFarUsdc: 0n,
304
+ nowSeconds: deps.nowSeconds(),
305
+ });
306
+ } catch (error) {
307
+ if (error instanceof MandateDeniedError) {
308
+ throw new CliError(`spend mandate denied this hire: ${error.reason}`, {
309
+ exitCode: 3,
310
+ hint:
311
+ mandateCaps.source === "env"
312
+ ? "the requested hire exceeds your METRIK_AGENT_MANDATE_* ceiling."
313
+ : "the hire does not satisfy the mandate signed for it.",
314
+ cause: error,
315
+ });
316
+ }
317
+ throw error;
318
+ }
319
+
320
+ out.result({
321
+ streamId: result.streamId,
322
+ txHash: result.txHash,
323
+ approveTxHash: result.approveTxHash,
324
+ buyer: account.address,
325
+ operator,
326
+ serviceRef,
327
+ budgetUsdc,
328
+ ratePerSecondUsdc,
329
+ durationSeconds,
330
+ escrow: result.handle.escrow,
331
+ chainId: result.handle.chainId,
332
+ });
333
+ out.print("Stream opened.");
334
+ out.table([
335
+ ["streamId", result.streamId],
336
+ ["tx", result.txHash],
337
+ ["approve tx", result.approveTxHash ?? "not needed"],
338
+ ["buyer", account.address],
339
+ ["operator", operator],
340
+ ["budget", formatUsdc(budgetUsdc)],
341
+ ["rate", `${formatUsdc(ratePerSecondUsdc)} / second`],
342
+ ["duration", formatDuration(durationSeconds)],
343
+ ]);
344
+ out.print("");
345
+ out.print(METERING_NOTE);
346
+ out.print("");
347
+ out.print(`Next: metrik status ${result.streamId}`);
348
+ },
349
+ };
350
+
351
+ /**
352
+ * What the buyer is actually paying for.
353
+ *
354
+ * A live acceptance run made ONE `invoke` call and paid for 302 seconds, which
355
+ * is exactly the design — and exactly what nothing in the CLI said. "Pay for
356
+ * verified delivery" reads as "pay for delivery TO ME", so a buyer expects
357
+ * per-call billing and gets a rental meter. The meter runs on the oracle's
358
+ * probes of the endpoint, not on the buyer's own requests; one call and a
359
+ * thousand calls cost the same second.
360
+ */
361
+ const METERING_NOTE =
362
+ "How you are billed: this is TIME-metered rental of an endpoint the oracle keeps probing — not per-call billing. The meter runs on verified availability, so it charges the same whether you send one request or a thousand, and it keeps charging while you send none. `metrik close <streamId>` stops the clock; nothing else does.";
363
+
364
+ /**
365
+ * Shapes that mean "the spender's allowance is too small", across the OpenZeppelin
366
+ * string revert and the ERC-6093 custom error. Matched as phrases, never on the
367
+ * bare word `allowance`: the no-wallet preview reports `no account configured —
368
+ * allowance and simulation skipped`, which contains the word and is a COMPLETELY
369
+ * different fact. Excusing that as "an approval will be sent first" would state
370
+ * a reason the CLI does not have.
371
+ */
372
+ const ALLOWANCE_SHORTFALL: readonly RegExp[] = [
373
+ /transfer amount exceeds allowance/i,
374
+ /insufficient allowance/i,
375
+ /ERC20InsufficientAllowance/,
376
+ ];
377
+
378
+ /**
379
+ * True when the dry run's simulation failed ONLY because the USDC approval a
380
+ * real open sends first has not been sent.
381
+ *
382
+ * Deliberately narrow: it requires an actual REVERT, whose reason is an
383
+ * allowance shortfall, on a preview that says an approval is (or may be)
384
+ * needed. Anything else keeps being reported as a simulation error, because
385
+ * every other failure is a real one the buyer should see before spending.
386
+ */
387
+ function isApprovalOrdering(preview: {
388
+ readonly simulationError?: string | undefined;
389
+ readonly simulatedStreamId?: string | null | undefined;
390
+ readonly approval: { readonly required: boolean | null };
391
+ }): boolean {
392
+ const error = preview.simulationError;
393
+ if (error === undefined || preview.simulatedStreamId != null) return false;
394
+ if (preview.approval.required === false) return false;
395
+ if (!/\brevert(ed)?\b/i.test(error)) return false;
396
+ return ALLOWANCE_SHORTFALL.some((pattern) => pattern.test(error));
397
+ }
@@ -0,0 +1,218 @@
1
+ import { booleanFlag } from "../args.js";
2
+ import { formatDuration, formatUsdc } from "../amounts.js";
3
+ import { assertTestnetRpc, parseBytes32 } from "../config.js";
4
+ import { CliError, usageError } from "../errors.js";
5
+ import {
6
+ escapeHatchWindow,
7
+ formatTimestamp,
8
+ readChainNow,
9
+ type EscapeHatchWindow,
10
+ } from "../escape-hatch.js";
11
+ import { signRecoveryMandate } from "../mandate.js";
12
+ import { assertCanPayGas } from "../preflight.js";
13
+ import { writeClientOptions } from "../deps.js";
14
+ import { isPolicyEnforced, resolveCliWallet } from "../wallet.js";
15
+ import type { CommandContext, CommandSpec } from "../context.js";
16
+ import type { CliReadClient } from "../deps.js";
17
+
18
+ /**
19
+ * The acknowledgement this command will not run without.
20
+ *
21
+ * `reclaim-unverified` is deliberately BOTH a command of its own and a command
22
+ * that refuses to run on its arguments alone:
23
+ *
24
+ * - it is not `metrik reclaim --unverified`, because that is one token away
25
+ * from the safe path, sits in the same command's flag list (so a script that
26
+ * templates flags can pick it up), and shell completion offers it beside
27
+ * `--close-first`;
28
+ * - and the flag below has no default and no `--no-` form, so the only way to
29
+ * reach the transaction is to type a phrase that says what it costs.
30
+ *
31
+ * Nothing about this command is reachable by fat-fingering the ordinary one.
32
+ */
33
+ const ACKNOWLEDGEMENT_FLAG = "forfeit-unclaimed";
34
+
35
+ /**
36
+ * Exactly what the escrow does, so the warning does not have to be trusted.
37
+ *
38
+ * `StreamEscrowV2.reclaimUnverified` sets `entitlement = stream.claimedCumulative`
39
+ * and then `settledCumulative = entitlement; deposit = entitlement`. The
40
+ * operator's own redemption path caps at that value once the stream is
41
+ * reclaimed (`if (stream.reclaimed && payableCumulative > stream.settledCumulative)
42
+ * payableCumulative = stream.settledCumulative;`) and then requires
43
+ * `payableCumulative > stream.claimedCumulative` — which can never hold again.
44
+ * Every voucher earned but not yet redeemed on-chain is therefore forfeit
45
+ * PERMANENTLY, and its value is paid to the buyer.
46
+ */
47
+ const FORFEIT_WARNING = [
48
+ "This pays you deposit - claimedCumulative with NO proof and NO oracle.",
49
+ "What it costs the operator: everything they verifiably earned but have not yet",
50
+ "claimed on-chain is forfeited to you, permanently. The escrow caps their",
51
+ "entitlement at what they had already claimed, so every later claim reverts",
52
+ "NothingToClaim(). There is no way to pay it back afterwards.",
53
+ "",
54
+ "Use it when the oracle cannot give you a proof. Where `metrik reclaim` works,",
55
+ "use that instead — it settles the operator honestly and pays you the rest.",
56
+ ];
57
+
58
+ export const reclaimUnverifiedCommand: CommandSpec = {
59
+ name: "reclaim-unverified",
60
+ summary:
61
+ "Escape hatch: reclaim with no proof when the oracle cannot give you one",
62
+ usage: `metrik reclaim-unverified <streamId> --${ACKNOWLEDGEMENT_FLAG}`,
63
+ flags: [ACKNOWLEDGEMENT_FLAG],
64
+ signs: true,
65
+ async run(context: CommandContext): Promise<void> {
66
+ const { args, config, env, out, deps } = context;
67
+ const [streamIdArg] = args.positionals;
68
+ if (streamIdArg === undefined) {
69
+ throw usageError("metrik reclaim-unverified requires a <streamId>");
70
+ }
71
+ const streamId = parseBytes32(streamIdArg, "<streamId>");
72
+
73
+ if (!booleanFlag(args, ACKNOWLEDGEMENT_FLAG)) {
74
+ throw usageError(
75
+ `metrik reclaim-unverified forfeits the operator's unclaimed earnings, so it requires --${ACKNOWLEDGEMENT_FLAG}`,
76
+ [
77
+ ...FORFEIT_WARNING,
78
+ "",
79
+ `Re-run with --${ACKNOWLEDGEMENT_FLAG} once that is what you intend.`,
80
+ ].join("\n "),
81
+ );
82
+ }
83
+
84
+ const wallet = await resolveCliWallet(env, {
85
+ note: out.note,
86
+ rpcUrl: config.rpcUrl,
87
+ });
88
+ const { account } = wallet;
89
+ const read = deps.createReadClient(config);
90
+ await assertTestnetRpc(config, () => read.getChainId());
91
+ await assertCanPayGas(read, account.address);
92
+
93
+ const stream = await read.getStreamV2(streamId);
94
+ if (stream.reclaimed) {
95
+ throw new CliError(
96
+ "this stream has already been reclaimed — there is nothing left to take back",
97
+ {
98
+ exitCode: 4,
99
+ hint: `check where it went: metrik status ${streamId}`,
100
+ },
101
+ );
102
+ }
103
+
104
+ // Readiness is computed against the chain's clock, because the escrow
105
+ // compares against `block.timestamp`. Spending gas on a guaranteed
106
+ // `EscapeHatchNotReady()` revert is exactly the failure this gate exists to
107
+ // prevent.
108
+ const grace = await readEscapeGrace(read);
109
+ const now = await readChainNow(read, deps.nowSeconds());
110
+ const window = escapeHatchWindow(stream, grace, now.seconds);
111
+ if (!window.ready) {
112
+ throw notReady(streamId, window, now.source);
113
+ }
114
+
115
+ // The amount at stake, when it is knowable. It usually is not: the reason to
116
+ // be on this path at all is that the oracle is not answering, and the
117
+ // operator's redeemable balance is derived from the oracle's proof.
118
+ const forfeit = await readForfeitEstimate(read, streamId);
119
+ out.note("");
120
+ for (const line of FORFEIT_WARNING) out.note(line);
121
+ out.note(
122
+ forfeit === null
123
+ ? "Operator balance at stake: unknown — the oracle did not answer, which is the situation this command is for."
124
+ : `Operator balance at stake right now: ${formatUsdc(forfeit)} (net of protocol fee). This transaction forfeits it.`,
125
+ );
126
+ out.note("");
127
+
128
+ const write = deps.createWriteClient(
129
+ config,
130
+ account,
131
+ writeClientOptions(wallet),
132
+ );
133
+ const result = await write.reclaimUnverifiedStream({
134
+ streamId,
135
+ // `null` for a policy-enforced wallet — see `../spend-policy.ts`.
136
+ signedMandate: isPolicyEnforced(wallet)
137
+ ? null
138
+ : await signRecoveryMandate(account, config.chainId),
139
+ spentSoFarUsdc: 0n,
140
+ nowSeconds: deps.nowSeconds(),
141
+ });
142
+
143
+ out.result({
144
+ streamId,
145
+ txHash: result.txHash,
146
+ unverified: true,
147
+ escapeHatchReadyAt: window.readyAt,
148
+ escapeGraceSeconds: window.graceSeconds,
149
+ forfeitedOperatorClaimable: forfeit,
150
+ });
151
+ out.print("Reclaimed without a proof.");
152
+ out.table([
153
+ ["streamId", streamId],
154
+ ["tx", result.txHash],
155
+ ["escape hatch opened", formatTimestamp(window.readyAt)],
156
+ [
157
+ "operator earnings forfeited",
158
+ forfeit === null ? "unknown (oracle unreachable)" : formatUsdc(forfeit),
159
+ ],
160
+ ]);
161
+ },
162
+ };
163
+
164
+ function notReady(
165
+ streamId: `0x${string}`,
166
+ window: EscapeHatchWindow,
167
+ clock: "chain" | "local",
168
+ ): CliError {
169
+ return new CliError(
170
+ `the no-proof escape hatch is not open yet: it unlocks at ${formatTimestamp(
171
+ window.readyAt,
172
+ )}, in ${formatDuration(window.secondsRemaining)}`,
173
+ {
174
+ exitCode: 4,
175
+ hint: [
176
+ `The escrow gates this on expiresAt + escapeGraceSeconds (${window.graceSeconds}s), never on when you closed${
177
+ clock === "chain"
178
+ ? ""
179
+ : " — and the chain's own clock could not be read, so this is your machine's"
180
+ }.`,
181
+ `Until then the honest path is the proof-based one: metrik reclaim ${streamId}`,
182
+ ].join(" "),
183
+ },
184
+ );
185
+ }
186
+
187
+ /** `escapeGraceSeconds` is a public immutable; failing to read it is fatal here. */
188
+ async function readEscapeGrace(read: CliReadClient): Promise<number> {
189
+ try {
190
+ return await read.escapeGraceSeconds();
191
+ } catch (error) {
192
+ throw new CliError(
193
+ "could not read escapeGraceSeconds from the escrow, so whether the escape hatch is open is unknown",
194
+ {
195
+ exitCode: 4,
196
+ hint: "this command refuses to broadcast on a guess: retry when the RPC endpoint answers.",
197
+ cause: error,
198
+ },
199
+ );
200
+ }
201
+ }
202
+
203
+ /**
204
+ * What the operator could redeem right now, i.e. what this transaction takes
205
+ * from them. `null` when the oracle cannot be reached — which is the normal case
206
+ * on this path and is reported as unknown rather than as zero.
207
+ */
208
+ async function readForfeitEstimate(
209
+ read: CliReadClient,
210
+ streamId: `0x${string}`,
211
+ ): Promise<bigint | null> {
212
+ try {
213
+ const proof = await read.checkpointProof(streamId);
214
+ return await read.claimableForV2(streamId, proof.cumulativeAmount);
215
+ } catch {
216
+ return null;
217
+ }
218
+ }