@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
package/docs/cli.md ADDED
@@ -0,0 +1,705 @@
1
+ # The `metrik` CLI
2
+
3
+ `metrik` runs the whole Metrik loop from a terminal: find a verified service, escrow
4
+ USDC into a metered stream, call the service, watch what it actually earned, stop the
5
+ clock, and take back what was never delivered.
6
+
7
+ **What you are buying: TIME, not calls.** A stream rents an endpoint that the oracle keeps
8
+ probing, and the meter runs on availability that VERIFIED — not on the requests you send.
9
+ One request and a thousand requests cost the same second, and the clock keeps running
10
+ while you send none. A reviewer who made a single `invoke` call and paid for 302 seconds
11
+ was billed exactly as designed. `metrik close <streamId>` stops the clock; nothing else
12
+ does, and nothing pushes money back on its own.
13
+
14
+ It is the **first surface that completes that loop**. The MCP tool set can discover,
15
+ hire, check status and reclaim — but it cannot invoke the paid service and it cannot
16
+ close a stream, so a reviewer or a new integrator had no single place to see the product
17
+ end to end.
18
+
19
+ The CLI is a thin shell over this package's existing exports (`discoverServices`,
20
+ `VerifiedStreamAgentClient`, `invoke`, `MetrikClient`). It re-implements no protocol
21
+ logic and defines no stream type of its own.
22
+
23
+ ## Install
24
+
25
+ ```bash
26
+ npx --package @absol-labs/agent metrik --help # no install
27
+ pnpm add -g @absol-labs/agent && metrik --help # installed
28
+ pnpm cli --help # inside this repository
29
+ ```
30
+
31
+ The package publishes four executables. Each is declared in `bin`, shipped in the
32
+ tarball, and — the part that matters — proved to resolve from a clean consumer's
33
+ `node_modules/.bin` by `pnpm test:package`:
34
+
35
+ | Bin | What it starts |
36
+ | ----------------- | ----------------------------------------------------------------- |
37
+ | `metrik` | this CLI |
38
+ | `metrik-mcp` | the stdio MCP server (`src/mcp/stdio.ts`) |
39
+ | `metrik-mcp-http` | the hosted multi-tenant MCP server (`src/mcp/http-server.ts`) |
40
+ | `metrik-gateway` | the reference caller-auth gateway (`src/gateway/server-entry.ts`) |
41
+
42
+ The last two were documented as `node dist/…/x.js` against a checkout, which a
43
+ consumer who installed the package could not run at all. `metrik mcp
44
+ --print-config` prints the MCP client block that starts `metrik-mcp`.
45
+
46
+ ## Testnet only
47
+
48
+ The CLI signs with a raw private key and moves USDC, so the chain is an **allowlist**,
49
+ not a default: only Base Sepolia (`84532`) is accepted. There is deliberately **no
50
+ override flag** — an escape hatch that re-enabled real funds would defeat the guarantee,
51
+ and there is no mainnet deployment for it to reach anyway.
52
+
53
+ The rule is enforced twice:
54
+
55
+ 1. against the chain id you declare (`--chain-id`, or `METRIK_AGENT_CHAIN_ID`), and
56
+ 2. against the chain id the RPC endpoint **actually reports**, re-read before any
57
+ command that signs. A declared chain id is just a number someone typed; the endpoint
58
+ is the thing that settles. This is what stops
59
+ `--chain-id 84532 --rpc-url <a mainnet RPC>`.
60
+
61
+ ## Wallet
62
+
63
+ Three wallet sources, resolved in a **fixed order** — first match wins:
64
+
65
+ | Order | Variables | Wallet |
66
+ | ----- | ------------------------------------------------------------------------------------------ | --------------------------------------- |
67
+ | 1 | `METRIK_AGENT_PRIVATE_KEY` | a raw Base Sepolia test key |
68
+ | 2 | `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, `CDP_WALLET_SECRET`, `METRIK_AGENT_CDP_OWNER_NAME` | a Coinbase CDP Server Wallet v2 account |
69
+ | 3 | `METRIK_AGENT_PRIVY_*` (below) | a Privy wallet reached through a broker |
70
+
71
+ The first two are the same order `parseAgentWalletEnv()` uses, so one environment
72
+ configures the CLI, the MCP server and the framework adapters identically. The order is
73
+ **fixed** rather than "whichever is configured": an environment carrying two is ambiguous
74
+ about which address funds the stream, and that ambiguity is a fund-moving one. When more
75
+ than one is present the earlier entry wins and the CLI says so on stderr rather than
76
+ choosing quietly.
77
+
78
+ Misconfigurations are refused up front, at exit `3`, instead of failing somewhere deeper:
79
+
80
+ - **A half-configured wallet** ("you set three of the four CDP variables", "you set the
81
+ Privy session token but not the address") is reported as exactly that, naming the
82
+ missing variables. Rendering it as "no wallet is configured" would send the reader
83
+ looking in the wrong place.
84
+ - **A CDP _smart_ account** (`METRIK_AGENT_CDP_CREATE_SMART_ACCOUNT` /
85
+ `METRIK_AGENT_CDP_SMART_ACCOUNT_NAME`) is refused. It is an ERC-4337 account at a
86
+ different address from its owner, reached through a bundler; this CLI broadcasts
87
+ through `--rpc-url`, so it would sign and spend from the **owner** while you funded the
88
+ smart account.
89
+ - **An account that cannot sign** is refused with the reason, rather than accepted and
90
+ failed later inside viem. The check is about capability, not locality: every wallet must
91
+ be able to sign EIP-712 typed data, and only the paths that broadcast through
92
+ `--rpc-url` additionally need a local signer.
93
+
94
+ There is no `--private-key` flag, and there will not be one — nor a flag for any other
95
+ credential: a secret on the command line lands in shell history, in `ps` output, and in
96
+ any CI log that echoes the command. Commands print the derived **address**; the
97
+ credential is never echoed, including by `metrik mcp --print-config`.
98
+
99
+ ### Privy
100
+
101
+ A Privy wallet differs from the other two in one structural way: its account has no local
102
+ `signTransaction`. It signs and broadcasts by sending `eth_sendTransaction` /
103
+ `eth_signTypedData_v4` to a provider, so for a Privy wallet the **provider is the wallet
104
+ client's transport** instead of the plain `http(--rpc-url)` a local signer uses. That is
105
+ the whole of the wiring. Private-key and CDP runs are unchanged, byte for byte.
106
+
107
+ There are two shapes. Set one; setting both uses the agent-owned wallet and says so.
108
+
109
+ **Agent-owned (`privy-autonomous`)** — headless, no browser, no user login:
110
+
111
+ | Variable | Required | What it is |
112
+ | ------------------------------- | -------- | ------------------------------------------------------------------------ |
113
+ | `METRIK_AGENT_PRIVY_WALLET_KEY` | yes | the credential-store key of a wallet you already provisioned |
114
+ | `METRIK_AGENT_PRIVY_BROKER_URL` | no | overrides the broker the wallet was provisioned against |
115
+ | `METRIK_WALLET_ENCRYPTION_KEY` | no | selects the encrypted-file store (the durable option on a headless host) |
116
+
117
+ **The CLI never provisions a wallet for you.** A fund-moving command is the wrong place to
118
+ mint one, so if nothing is stored under that key the CLI says so and stops. Provision once,
119
+ deliberately — see [autonomous-privy-wallet.md](./autonomous-privy-wallet.md):
120
+
121
+ ```ts
122
+ import { provisionMetrikAutonomousWallet } from "@absol-labs/agent";
123
+
124
+ const wallet = await provisionMetrikAutonomousWallet({
125
+ storeKey: "my-agent/base-sepolia",
126
+ });
127
+ console.log(wallet.address); // fund this with test USDC and Base Sepolia ETH
128
+ ```
129
+
130
+ ```bash
131
+ export METRIK_AGENT_PRIVY_WALLET_KEY=my-agent/base-sepolia
132
+ metrik invoke 0x… --path /health
133
+ ```
134
+
135
+ Read the credential-store durability section of that document before funding anything: on
136
+ a headless Linux host the OS store is **not** durable and provisioning is refused, which
137
+ is why `METRIK_WALLET_ENCRYPTION_KEY` exists. The CLI selects the same store the same way
138
+ — set that variable and it reads the encrypted file, unset and it reads the OS store — so
139
+ a mismatch between provisioning and use simply finds no wallet and says so.
140
+
141
+ **User-owned broker session (`privy-session`)** — a scoped session minted from a Privy
142
+ login (today that means the dApp, which holds the user's Privy access token):
143
+
144
+ | Variable | Required | What it is |
145
+ | ---------------------------------- | -------- | ------------------------------------------------------------------ |
146
+ | `METRIK_AGENT_PRIVY_BROKER_URL` | yes | the broker that minted the token; a token is valid nowhere else |
147
+ | `METRIK_AGENT_PRIVY_SESSION_TOKEN` | yes | the token from the broker's `POST /v1/sessions` |
148
+ | `METRIK_AGENT_PRIVY_ADDRESS` | yes | the embedded wallet address the session is bound to |
149
+ | `METRIK_AGENT_PRIVY_APP_ID` | yes | the **public** Privy app id — the broker serves it at `/v1/config` |
150
+
151
+ #### Which broker the CLI talks to
152
+
153
+ The agent-owned path defaults to the broker URL the wallet was provisioned against, which
154
+ for `provisionMetrikAutonomousWallet()` is the Metrik broker at
155
+ `https://wallet.137.23.50.249.sslip.io`. Its `GET /health` and `GET /v1/config` answer
156
+ publicly; `/v1/config` carries only the public app id, the public policy id and the fixed
157
+ policy ceilings, and nothing there is a secret. The Privy **authorization key** stays on
158
+ the broker and is never served.
159
+
160
+ If you would rather not trust that deployment, run your own: it is
161
+ `metrik-privy-broker` from this package (`src/wallet/privy-broker-server.ts`), it needs
162
+ your own `PRIVY_APP_ID` / `PRIVY_APP_SECRET` / authorization key / policy id, and you then
163
+ point `METRIK_AGENT_PRIVY_BROKER_URL` at it. A wallet provisioned against one broker
164
+ cannot be authorized by another — only the broker holding that wallet's Privy
165
+ authorization key can sign for it — so switching brokers means provisioning a new wallet.
166
+
167
+ #### The spend mandate, and why a Privy wallet does not sign one
168
+
169
+ Every fund-moving command in this package normally self-signs an EIP-712
170
+ `SpendMandate` first. Read why, from `src/cli/mandate.ts`: _"the person typing
171
+ the command IS the mandate owner: they hold the key and they just stated the
172
+ exact spend on the command line."_ The mandate exists because, for a key-holding
173
+ wallet, the client is the last line of defence — so the agent is made to state
174
+ its own ceiling and is held to it.
175
+
176
+ **For a Privy wallet that premise is false.** The user does not hold the key: it
177
+ lives at Privy and is exercised by the broker, which caps every `openStream`
178
+ against a policy the client cannot reach and cannot raise. A self-signed mandate
179
+ on top of that is an agent attesting to its own limits when a stronger limit is
180
+ already enforced somewhere the agent cannot touch.
181
+
182
+ So for a policy-enforced wallet the CLI **does not sign a mandate**, and
183
+ **substitutes the real ceiling instead of dropping the check**: it reads the
184
+ broker's public `GET /v1/config` and refuses an over-ceiling hire locally, in the
185
+ same shape as the gas and USDC preflights — fail early with an actionable
186
+ message rather than paying gas to learn a knowable fact. `open --dry-run`
187
+ reports the ceiling and whether the hire fits; a real `open` refuses.
188
+
189
+ ```
190
+ mandate not signed — this wallet's ceiling is enforced by its broker, which is stronger
191
+ spend policy 1 USDC (1000000 atomic) total, 1000 atomic/second, 1h max — enforced by https://wallet.137.23.50.249.sslip.io
192
+ fits policy yes
193
+ ```
194
+
195
+ Two things this deliberately does NOT do:
196
+
197
+ - **It does not widen the broker.** Both brokers restrict `eth_signTypedData_v4`
198
+ to `primaryType: "InvocationCapability"`. Teaching them to sign a
199
+ `SpendMandate` would weaken the exact property that makes the wallet
200
+ non-custodial, to reinstate a check that is the weaker of the two.
201
+ - **It does not retire `METRIK_AGENT_MANDATE_*`.** Those caps are a standing
202
+ ceiling the operator deliberately chose, and can be TIGHTER than the broker
203
+ policy. They still apply, enforced locally, because no mandate is signed to
204
+ carry them.
205
+
206
+ Do not "restore" the mandate for consistency. It would both weaken the guarantee
207
+ and break every fund-moving command against the broker as deployed.
208
+
209
+ #### What works, and what does not
210
+
211
+ `open`, `close`, `reclaim`, `reclaim-unverified` and `invoke` all work with a
212
+ Privy wallet.
213
+
214
+ `metrik claim` does not, and that is not a Privy defect: `claim` is the
215
+ **operator's** side, and a broker policy allowlists the **buyer** actions only
216
+ (`openStream`, `close`, `reclaim`, `reclaimUnverified`, and a bounded USDC
217
+ approval to the escrow). An operator claims with the operator's own key. The CLI
218
+ says this before it runs rather than letting you discover it at the broker.
219
+
220
+ Privy support is Base Sepolia (84532) only, EOA only. No ERC-4337, no EIP-1271,
221
+ no gas sponsorship claim.
222
+
223
+ #### Trust model, honestly
224
+
225
+ A raw `METRIK_AGENT_PRIVATE_KEY` is the simplest thing that can possibly work and the CLI
226
+ holds the whole key: anything that reads your environment can move every token in that
227
+ wallet, forever.
228
+
229
+ Privy is not "no custody" — it is **different** custody. The signing key lives at Privy and
230
+ is exercised by the broker, so the parties who can move funds are Privy and whoever runs
231
+ the broker, together. What the CLI holds is a P-256 authorization key (agent-owned) or a
232
+ short-lived session token (session) — credentials that authorize a **policy-bounded**
233
+ request, not credentials that can drain the wallet. Leaking one is bounded by the broker's
234
+ policy: Base Sepolia only, the escrow and USDC contracts only, zero native value, and the
235
+ installed ceilings (`/v1/config`). Losing the agent-owned P-256 key is the mirror risk: the
236
+ wallet can never be authorized again and anything it holds is unspendable.
237
+
238
+ Pick a raw key if you are the only party and you want no third party in the path. Pick
239
+ Privy if you want a policy boundary between the machine running the agent and the money.
240
+
241
+ `invoke` needs the key too, even though it broadcasts nothing: it signs an **off-chain
242
+ EIP-712 invocation capability** as the stream's buyer. `open --dry-run` is the one path
243
+ that needs no key at all — it builds and prints the intended call and says which parts
244
+ (allowance, simulation, mandate signature) it could not produce without one.
245
+
246
+ ## Commands
247
+
248
+ | Command | Needs a wallet? | Broadcasts a tx? | What it does |
249
+ | -------------------- | ------------------------------ | ---------------- | ------------------------------------------------ |
250
+ | `discover` | no | no | Lists verified registry listings |
251
+ | `open` | **yes** (not with `--dry-run`) | **yes** | Escrows USDC into a metered stream |
252
+ | `status` | no | no | One stream's on-chain state + verified accrual |
253
+ | `streams` | no (with `--address`) | no | Lists a wallet's streams from the escrow's logs |
254
+ | `watch` | no | no | Polls that until the stream closes |
255
+ | `invoke` | **yes** (off-chain capability) | no | Calls the paid service, authorized by the stream |
256
+ | `close` | **yes** | **yes** | Stops the clock |
257
+ | `reclaim` | **yes** | **yes** | Takes back what was never earned |
258
+ | `reclaim-unverified` | **yes** | **yes** | Buyer escape hatch: reclaim with no proof |
259
+ | `claim` | **yes** (the operator's) | **yes** | Operator: redeem what verified |
260
+ | `mcp` | no | no | Prints the MCP client config for `metrik-mcp` |
261
+
262
+ ### `metrik discover`
263
+
264
+ ```bash
265
+ metrik discover # everything the registry serves
266
+ metrik discover --min-tier 2 --limit 5 # verified-only, five strongest
267
+ metrik discover --operator 0x… # one operator's listings
268
+ ```
269
+
270
+ `--category` filters the registry's unsigned `category` column. The public discovery
271
+ endpoint serves **signed records only** and carries no category column, so that filter
272
+ matches nothing there — it is for an authenticated PostgREST read.
273
+
274
+ Each row prints what the operator's **signed descriptor** already carries: the callable
275
+ endpoints (method + path + description), one worked example call, and the accepted
276
+ `ratePerSecond` band in USDC **base units** — which is what `--rate … --units atomic`
277
+ takes. Nothing unsigned is presented as fact.
278
+
279
+ Listings are ordered **strongest declared verification first** (re-execution, then schema,
280
+ then SLA, then canaries), ties broken on `serviceRef`, so the order is reproducible and
281
+ `--limit 1` returns the best-attested listing rather than whatever the registry happened to
282
+ serve first.
283
+
284
+ Every row is schema-validated, its `serviceRef` re-derived from the signed record and the
285
+ operator signature recovered — a compromised registry can neither invent an operator to
286
+ pay nor redirect an invocation URL. Rows that fail are dropped and reported on stderr.
287
+
288
+ An unreachable registry is an **error**, never an empty list. "The marketplace is empty"
289
+ and "the registry was down" are different facts and are never rendered the same way.
290
+
291
+ **Who attests is disclosed.** `discover` reads `getOracleSigners()` off the escrow and, when
292
+ a listing's operator is ALSO one of those signers, says so on stderr and sets
293
+ `operatorIsOracleSigner: true` in `--json`. On the current Base Sepolia deployment that
294
+ overlap is real: the escrow has three oracle signers and one of them operates the live
295
+ listings, so the party being paid is among the parties attesting that delivery happened.
296
+ Verification there is not independent of the seller, and a tool that prints "tier 2" and
297
+ "cryptographically verified" without saying so is letting a buyer assume otherwise. The
298
+ CLI does not refuse such a listing and does not score it — it states the fact.
299
+
300
+ If the signer set cannot be read, `operatorIsOracleSigner` is `null` and the warning says
301
+ **UNKNOWN**. "We checked and there is no overlap" and "we could not check" are different
302
+ facts; an escrow outage never renders as a clean bill of health.
303
+
304
+ **SLA text is the operator's own claim.** Anything under `sla` comes from the operator's
305
+ signed descriptor, and a signature proves only that the operator SAID it. Metrik verifies
306
+ delivery, not promises made about it — one live listing claims that two consecutive failed
307
+ intervals auto-pause the stream, and StreamEscrowV2 has no paused state at all, so that
308
+ claim cannot be true. Every rendering of it is labelled unverified, in human output and in
309
+ `--json` (`slaClaimVerified: false`).
310
+
311
+ ### `metrik open`
312
+
313
+ ```bash
314
+ metrik open --service <serviceRef> --rate 0.000001 --budget 0.27 --duration 1h
315
+ metrik open --service <serviceRef> --rate 1 --budget 270000 --duration 3600 --units atomic
316
+ metrik open ... --dry-run
317
+ ```
318
+
319
+ The operator is taken from the **verified listing** for that `serviceRef` (override with
320
+ `--operator`, which then skips the registry lookup). `--dry-run` builds and simulates the
321
+ transaction and broadcasts nothing — it prints the escrow calldata, the bounded USDC
322
+ approval, and the `streamId` a real hire would produce.
323
+
324
+ **`--dry-run` needs no wallet.** It is the look-before-you-leap path, and the person who
325
+ most needs it is the one who has not funded a key yet. Without `METRIK_AGENT_PRIVATE_KEY`
326
+ it prints the intended call and reports `buyer: null`, `mandateSigned: false`, and the
327
+ SDK's own `no account configured — allowance and simulation skipped`, rather than
328
+ inventing an address or a signature. With a key, the preview is a real simulation from a
329
+ real address.
330
+
331
+ **Two pre-flights, both before anything is signed.** Gas and the deposit are separate
332
+ balances on separate tokens, so passing one says nothing about the other:
333
+
334
+ 1. Before a real open, close or reclaim, the CLI reads the wallet's **native** balance and
335
+ refuses at exit `3` if it is zero, naming the Base Sepolia faucet — instead of letting
336
+ the node answer with `gas required exceeds allowance (0)`, which reads like a USDC
337
+ approval problem and is not one.
338
+ 2. Before a real open, it reads the wallet's **USDC** balance and refuses at exit `3` if it
339
+ is below `--budget`, naming Circle's faucet. Without it the open path sends the USDC
340
+ approval first — which succeeds, because approving an amount you do not hold is legal —
341
+ and then reverts inside `transferFrom`, having already spent gas to learn something
342
+ that was one `balanceOf` away.
343
+
344
+ The second check is deliberately **not** an allowance check: the CLI's own open path grants
345
+ the bounded approval it needs (`approveTxHash` in the result), so a missing allowance is a
346
+ step of the command, not a precondition for it. Gating on it would refuse a hire that was
347
+ about to work. Balance is the fact the CLI cannot fix for you.
348
+
349
+ `--dry-run` reports the USDC balance instead of enforcing it — it broadcasts nothing, so an
350
+ underfunded wallet is information there, not a refusal.
351
+
352
+ **A dry run does not predict failure on a first hire.** The simulation runs against current
353
+ chain state, which does not include the bounded USDC approval a real open sends FIRST, so
354
+ `openStream` reverts on the allowance every time that approval is not already in place.
355
+ That is the command's own ordering, not a fault, and reporting it as `simulationError` made
356
+ the one command meant to build confidence forecast failure on first use. It is now reported
357
+ as `simulationPendingApproval: true`, with no `simulationError` — a script branching on
358
+ that field no longer sees the expected path as a failure. An allowance-shaped revert when
359
+ the allowance IS already sufficient, and every other revert, are still surfaced as errors.
360
+
361
+ **Amounts.** `--budget` and `--rate` are decimal USDC by default; `--units atomic`
362
+ switches both to raw 6-decimal base units (what the escrow, the oracle and every
363
+ `METRIK_E2E_*` fixture use). The unit is explicit rather than inferred from magnitude,
364
+ because guessing would be a silent 1,000,000x error on a fund-moving command.
365
+
366
+ **Spend mandates.** Every fund-moving path in this package goes through `checkMandate()`,
367
+ and the CLI is not exempt. The person typing the command is the mandate owner: the
368
+ mandate is self-signed from the same wallet that funds the stream, exactly as the MCP
369
+ server does. If `METRIK_AGENT_MANDATE_*` caps are set, they bind and an oversized hire is
370
+ denied; with no caps configured, the mandate is sized to exactly this hire and scoped to
371
+ exactly this operator. Neither path is an unbounded mandate.
372
+
373
+ ### `metrik status` / `metrik watch`
374
+
375
+ ```bash
376
+ metrik status <streamId>
377
+ metrik watch <streamId> --interval 15 --max-polls 40
378
+ ```
379
+
380
+ `--max-polls` has **no default**: `watch` runs until the stream closes or you interrupt
381
+ it. Both commands apply the same Base Sepolia allowlist as the signing paths — the V1 and
382
+ V2 stream tuples diverge from index 5 onward, so a wrong-chain read does not revert, it
383
+ silently decodes something else into fields with the right names.
384
+
385
+ Accrual is read from the **oracle's checkpoint proof**, not from the stream tuple.
386
+ `StreamEscrowV2.getStream` reports `claimedCumulative = 0` and `settledCumulative = 0`
387
+ for a stream's entire active life (`settledCumulative` is written by `reclaim`), so a
388
+ status built from stream fields would print "earned: 0" for a stream that had been
389
+ delivering for an hour.
390
+
391
+ Before the oracle's first covering checkpoint there is simply no proof, and the CLI says
392
+ so rather than printing a zero that looks like a verdict. The protocol fee is read live
393
+ off the escrow (`protocolFeeBps`), never hardcoded.
394
+
395
+ **No settled figure is printed while the final checkpoint is missing.** Once a stream stops
396
+ — closed, or past `expiresAt` — its accrual is final only after a checkpoint whose
397
+ `checkedAt` reaches that stop time. Until then the latest checkpoint describes a SHORTER
398
+ stream than the one that exists, so a reclaimable computed from it is too large and falls
399
+ when the covering checkpoint lands. A live run read `reclaimable: 1400` straight after
400
+ `close`, `0` two minutes later, and got `NothingToReclaim()` from the reclaim in between.
401
+
402
+ So while that is pending, `buyer reclaimable` reads `pending final checkpoint` with the two
403
+ timestamps that decide it, and `--json` sets `reclaimable: null` plus an explicit
404
+ `settlement` object:
405
+
406
+ ```json
407
+ {
408
+ "reclaimable": null,
409
+ "settlement": {
410
+ "pending": true,
411
+ "stopAt": 1789671000,
412
+ "coveredUpTo": 1789670980,
413
+ "provisionalReclaimable": "1400"
414
+ }
415
+ }
416
+ ```
417
+
418
+ The superseded number is kept under a name that cannot be mistaken for money. If the
419
+ escrow will not say how far the oracle has published, that counts as pending — withholding
420
+ a figure we cannot stand behind is the buyer-favouring error.
421
+
422
+ `operator claimable` is treated differently on purpose: `claimWithProof` accepts any
423
+ retained root, so the operator can act on it now and it can only grow. It is shown, marked
424
+ `not final until the checkpoint covering the stop time lands`, rather than withheld.
425
+
426
+ **The seller's side of the escape hatch.** When a stream has unclaimed earnings, `status`
427
+ also prints an `operator deadline` row: `reclaimUnverified` pays the buyer the whole
428
+ remaining deposit with no proof from `expiresAt + escapeGraceSeconds`, and on the live
429
+ escrow that grace is 900 seconds and immutable. An operator who has earned but not claimed
430
+ loses it at that moment, and the only defence is to `metrik claim` before it — which
431
+ requires knowing the deadline exists.
432
+
433
+ StreamEscrowV2 has exactly two states, `active` and `closed`. There is no paused state
434
+ and the CLI never renders one.
435
+
436
+ Two rows exist so that a buyer can diagnose a stuck reclaim from `status` alone. **oracle
437
+ checkpoint** is how far the oracle has actually published; a value well behind the
438
+ stream's stop time IS the reason a `reclaim` is blocked. **escape hatch** is when the
439
+ no-proof path opens. Both are printed for every stream, not only broken ones — a buyer
440
+ should learn the escape hatch exists before the day they need it.
441
+
442
+ Every deadline on that screen is resolved against the **chain's** clock, not this
443
+ machine's, because the escrow compares against `block.timestamp`. If the chain will not
444
+ answer, the local clock is used and `clockSource` says so rather than hiding it.
445
+
446
+ ### `metrik streams`
447
+
448
+ ```bash
449
+ metrik streams # the configured wallet's streams
450
+ metrik streams --address 0x… --role buyer # anyone's, as buyer only
451
+ metrik streams --limit 5 --json
452
+ ```
453
+
454
+ Answers "which streams are mine?" from the escrow's own `StreamOpened` logs. `buyer` and
455
+ `operator` are **indexed** on that event, so `--role` becomes an `eth_getLogs` topic
456
+ filter the node evaluates: the response carries only that wallet's streams no matter how
457
+ wide the window is.
458
+
459
+ It signs nothing and broadcasts nothing, so `--address` is enough — no wallet needed.
460
+ With no `--address`, the configured wallet is the subject.
461
+
462
+ **The scan is anchored at the escrow's deploy block**, not at a fixed lookback. A
463
+ lookback cannot describe coverage: a wallet whose stream predates the window is shown an
464
+ empty list, indistinguishable from a wallet that never opened one, and telling someone
465
+ their escrowed funds do not exist is the worst failure a payments tool has. The contract
466
+ has no history before its deploy block, so whether the scan reached it is a fact the
467
+ command can state. Pass `--from-block` for a non-default `--escrow`; without one, a
468
+ different escrow is refused rather than scanned from a block that means nothing for it.
469
+
470
+ **An empty result is only reported as "no streams" when it can be trusted.** Three things
471
+ can make a scan partial, and each is named on stderr in both output modes, with
472
+ `degraded: true` and a `degradedReasons` array in `--json`:
473
+
474
+ - the window budget could not reach the anchor (4,000,000 blocks — the live escrow's whole
475
+ history is ~1.06M, so the default scan is complete);
476
+ - a log query failed three times (one transient 429 is retried, not counted);
477
+ - the RPC has **pruned its log index**. A pruning node answers a range it no longer
478
+ retains with a _successful empty result_, so the CLI probes the deploy block first —
479
+ measured on Base Sepolia, that block carries 6 escrow logs, and an endpoint that returns
480
+ none for it has pruned the history and its empty windows cannot be believed.
481
+ `https://base-sepolia-rpc.publicnode.com` fails this probe today; `https://sepolia.base.org`
482
+ passes it.
483
+
484
+ The `--json` output carries `logRetentionProven` (`true` / `false` / `null` when no canary
485
+ was possible). A row whose `getStream` read failed is kept and marked `status: "unread"` —
486
+ never dropped, because a stream that vanishes from this list is exactly what the command
487
+ exists to prevent.
488
+
489
+ `originalDepositUsdc` comes from the **event**, not the stream: StreamEscrowV2 mutates
490
+ `Stream.deposit` on reclaim, so the log is the only surviving record of what was escrowed.
491
+ Earned-so-far is not listed here — that is a per-stream call to the oracle, and
492
+ `metrik status <streamId>` is the command that makes it.
493
+
494
+ ### `metrik mcp --print-config`
495
+
496
+ ```bash
497
+ metrik mcp # same thing: a bare invocation prints it
498
+ metrik mcp --print-config # the block to paste into an MCP client
499
+ metrik mcp --print-config --name metrik-b # a second server key, for a second wallet
500
+ metrik mcp --print-config --json | jq .
501
+ ```
502
+
503
+ Prints the `mcpServers` block for Claude Desktop (or any MCP client) that starts this
504
+ package's stdio MCP server. A **bare `metrik mcp` prints it too** — it used to exit `2`
505
+ demanding `--print-config` while the help said the command prints a config — and states on
506
+ stderr that it printed a config rather than starting anything. The server itself is the
507
+ separate `metrik-mcp` executable, which the host launches. It describes an **installed** package, not this repository:
508
+ the command is `npx -y --package @absol-labs/agent@<this version> metrik-mcp`, pinned to
509
+ the version that printed it — `latest` would drift under an MCP host that signs spend
510
+ mandates.
511
+
512
+ The block carries every variable the server fails closed without: `METRIK_AGENT_RPC_URL`,
513
+ `METRIK_AGENT_ESCROW` and `METRIK_AGENT_USDC` (which have **no** defaults there, unlike in
514
+ this CLI) filled from the settlement config the CLI is already pointed at, plus the four
515
+ `METRIK_AGENT_MANDATE_*` caps without which the server refuses to sign a mandate at all.
516
+ A printed config that omitted them would be a config that fails on first launch, which is
517
+ the failure this command exists to prevent.
518
+
519
+ Configured mandate ceilings are carried through verbatim — they are policy, not secrets,
520
+ and re-typing four caps from memory is where a wrong ceiling comes from. **Credentials are
521
+ always placeholders.** This command never reads a key out of your environment and prints
522
+ it back; the wallet block is simply shaped like the environment it ran in (the CDP
523
+ variables if those are what you have set, `METRIK_AGENT_PRIVATE_KEY` otherwise).
524
+
525
+ ### `metrik invoke`
526
+
527
+ ```bash
528
+ metrik invoke <streamId> --method POST --path /transform --body '{"text":"hi"}'
529
+ metrik invoke <streamId> --path /v1/status --header "accept: application/json"
530
+ metrik invoke <streamId> --path /health --ttl 120
531
+ ```
532
+
533
+ `--path` defaults to `/`, `--method` to `GET`, and `--ttl` (the capability's lifetime in
534
+ seconds) to `60`. `--header` may be repeated.
535
+
536
+ Resolves the invocation origin from the operator's **signed** listing, signs an EIP-712
537
+ `InvocationCapability` scoped to exactly this method and path, and sends it in the
538
+ `x-metrik-capability` header. No out-of-band credential is involved: the stream is the
539
+ credential.
540
+
541
+ `--base-url` is an escape hatch for a local gateway. It bypasses verified listing
542
+ routing, so the CLI warns when it is used.
543
+
544
+ A non-2xx response is an error exit, so a script can branch on it. This command signs a
545
+ capability but broadcasts no transaction — it still requires `METRIK_AGENT_PRIVATE_KEY`,
546
+ because the capability is signed by the stream's buyer.
547
+
548
+ **Not shipped: a `prove` command.** `invokeWithT2DeliveryProof` throws for gated
549
+ listings ("consumer-attested T2 invocation is not supported for gated listings"), and the
550
+ live listings are gated — so a `metrik prove` would be a command that fails for almost
551
+ everyone who ran it. It is left out until that constraint lifts.
552
+
553
+ ### `metrik close` and `metrik reclaim`
554
+
555
+ ```bash
556
+ metrik close <streamId>
557
+ metrik reclaim <streamId> --attempts 12 --interval 10
558
+ metrik reclaim <streamId> --close-first
559
+ ```
560
+
561
+ Nothing pushes money back. `close` stops the clock; `reclaim` is the transaction that
562
+ returns the unspent budget, and the buyer sends it. `close` now points at
563
+ `--close-first` for next time, rather than leaving the one-shot form visible only in
564
+ `metrik reclaim --help`.
565
+
566
+ **`reclaim` waits, by design.** `reclaimWithProof` reverts with `SettlementWindowOpen`
567
+ until a checkpoint whose `checkedAt` is at or after the stream's stop time has posted.
568
+ That is the normal ordering of a checkpoint rail, not a fault — in practice the first
569
+ attempt fails and one roughly 30 seconds later succeeds. The CLI retries **that one
570
+ condition** on a bounded schedule and narrates the wait on stderr; every other failure is
571
+ surfaced immediately and unretried.
572
+
573
+ If it is still blocked when the budget runs out, the message does **not** just say "retry
574
+ later" — every attempt this command makes needs a proof from the oracle, so when the
575
+ oracle is gone, retrying forever never pays. The give-up message names
576
+ `reclaim-unverified`, the path that needs no proof, and says whether its window is open
577
+ yet and what taking it would cost.
578
+
579
+ **`--close-first` is two transactions, sequenced by the CLI.** The reclaim only simulates
580
+ against post-close state, so the close is sent, then confirmed through the same read path
581
+ the reclaim will use, and only then is the reclaim attempted. (`sepolia.base.org` is
582
+ load-balanced: the close receipt can come back from one node while the next simulation
583
+ lands on another that has not imported the block.) If the stream is **already** closed,
584
+ the close is skipped and the command goes straight to reclaim — so re-running it is always
585
+ safe and never dead-ends on `StreamClosedAlready()`.
586
+
587
+ `reclaim` on a stream that is still running reports that in words, with the command that
588
+ fixes it, rather than surfacing the `StreamNotReclaimable()` selector.
589
+
590
+ Close and reclaim are buyer-recovery actions: `VerifiedStreamAgentClient` authorizes them
591
+ on the mandate owner's signature alone, never on caps or expiry, so a lapsed mandate can
592
+ never strand a buyer's own money.
593
+
594
+ ### `metrik claim`
595
+
596
+ ```bash
597
+ metrik claim <streamId>
598
+ ```
599
+
600
+ The seller's side of the loop, and the only redemption surface in this package — the MCP
601
+ server has no claim tool either. `status` has always printed an `operator claimable` row;
602
+ until now nothing in the CLI could act on it.
603
+
604
+ `claimWithProof` is operator-gated on-chain, so the command reads the stream first and
605
+ refuses a wallet that is not the operator **before** it broadcasts. An address mismatch is
606
+ knowable with one read; letting it become a raw `NotOperator()` revert would charge gas to
607
+ learn a fact that was already free. Same for a missing checkpoint: accrual lives in the
608
+ oracle's proof, never in the stream tuple, so no proof means no claim is possible yet and
609
+ the command says that instead of paying for `NothingToClaim()`.
610
+
611
+ ### `metrik reclaim-unverified`
612
+
613
+ ```bash
614
+ metrik reclaim-unverified <streamId> --forfeit-unclaimed
615
+ ```
616
+
617
+ **The escape hatch, for when the oracle cannot give you a proof.** Every other route this
618
+ CLI has to a buyer's money goes through `reclaimWithProof`, so the whole ordinary path is
619
+ only as available as the oracle is. `StreamEscrowV2.reclaimUnverified` is the one that is
620
+ not: buyer-only, no proof, no signer quorum, no guardian.
621
+
622
+ **What it costs.** The escrow sets the operator's entitlement to what they had _already
623
+ claimed on-chain_, and their later claims then revert `NothingToClaim()` forever. So every
624
+ voucher the operator verifiably earned but had not yet redeemed is **permanently forfeit,
625
+ and paid to the buyer**. Nothing recovers it afterwards. That is why this is a separate
626
+ command rather than a `metrik reclaim --unverified` flag one token away from the safe path,
627
+ and why `--forfeit-unclaimed` has no default and no `--no-` form: the only way to reach the
628
+ transaction is to type a phrase that says what it does. Where `metrik reclaim` works, use
629
+ that instead — it settles the operator honestly and still pays you the rest.
630
+
631
+ **When it opens.** The escrow gates it on
632
+
633
+ ```
634
+ block.timestamp >= expiresAt + escapeGraceSeconds
635
+ ```
636
+
637
+ — on **`expiresAt`, never on `closedAt`**. Closing a stream early does **not** bring the
638
+ hatch forward: close a 24-hour stream after ten minutes and the hatch still opens roughly
639
+ 24 hours out, not `escapeGraceSeconds` later. Before then there is genuinely no no-proof
640
+ route, and no CLI change can invent one. Readiness is fully computable before any gas is
641
+ spent (`escapeGraceSeconds` is a public immutable), so the command refuses early rather
642
+ than broadcasting into a guaranteed `EscapeHatchNotReady()`.
643
+
644
+ Before broadcasting it prints what the operator stands to lose, read live. On this path
645
+ that figure is usually **unknown** — the reason to be here at all is that the oracle is not
646
+ answering, and the operator's redeemable balance is derived from the oracle's proof. It is
647
+ reported as unknown, never as zero.
648
+
649
+ > **Deployment note.** `escapeGraceSeconds` is **900 s** on the live Base Sepolia escrow,
650
+ > while the contract's own comment says it should be "typically >= a day" — the point being
651
+ > to give an operator time to redeem before forfeiture. Both values are immutable, so this
652
+ > cannot change without a redeploy. The forfeiture above therefore bites far sooner than
653
+ > the contract author intended.
654
+
655
+ ## Output
656
+
657
+ Human-readable by default. `--json` puts one JSON document on stdout; warnings, progress
658
+ and errors always go to **stderr**, so the stream stays parseable. `metrik watch --json`
659
+ emits **JSON Lines** — one compact object per poll, each on its own line, so every line
660
+ parses standalone:
661
+
662
+ ```bash
663
+ metrik watch <streamId> --json | while read -r line; do
664
+ echo "$line" | jq -r '.verifiedAccrual.cumulativeAmount // "not checkpointed yet"'
665
+ done
666
+ ```
667
+
668
+ Exit codes: `0` success, `2` usage error, `3` refused configuration (wrong chain, no
669
+ wallet, mandate denied), `4` the operation failed.
670
+
671
+ **Nothing waits silently.** Every RPC call carries a 15s budget with two bounded retries,
672
+ and the oracle fetch — an HTTP call the RPC transport does not cover — has its own 20s
673
+ budget that names the endpoint when it expires. Any command still working after 8 seconds
674
+ starts a heartbeat on stderr with the elapsed time, repeating every 10s, because with no
675
+ output at all a slow endpoint and a wedged process are indistinguishable. The heartbeat is
676
+ stderr in both modes, so `--json` stdout stays a single parseable document.
677
+
678
+ Read paths time out; **broadcasts do not.** A transaction that has been sent may already be
679
+ in the mempool, and a CLI that walked away from it on a timer would report a failure for
680
+ something that in fact settled. Write paths keep waiting, and the heartbeat is what makes
681
+ that wait legible.
682
+
683
+ ## Settlement configuration
684
+
685
+ Everything defaults to Metrik's live Base Sepolia deployment, and every value is
686
+ overridable per command or through the environment:
687
+
688
+ | Flag | Env | Default |
689
+ | ---------------- | --------------------------- | ------------------------------------- |
690
+ | `--chain-id` | `METRIK_AGENT_CHAIN_ID` | `84532` |
691
+ | `--rpc-url` | `METRIK_AGENT_RPC_URL` | Base Sepolia public RPC |
692
+ | `--escrow` | `METRIK_AGENT_ESCROW` | `BASE_SEPOLIA_ESCROW_V2` |
693
+ | `--usdc` | `METRIK_AGENT_USDC` | `BASE_SEPOLIA_USDC` |
694
+ | `--oracle-url` | `METRIK_AGENT_ORACLE_URL` | `https://oracle.metrik.live` |
695
+ | `--registry-url` | `METRIK_AGENT_REGISTRY_URL` | the oracle's public listings endpoint |
696
+
697
+ ## Embedding it
698
+
699
+ The runner is exported, with every side-effecting dependency injected:
700
+
701
+ ```ts
702
+ import { runCli } from "@absol-labs/agent/cli";
703
+
704
+ const exitCode = await runCli({ argv: ["status", streamId, "--json"] });
705
+ ```