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