@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
package/docs/cli.md ADDED
@@ -0,0 +1,569 @@
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
+ Two 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
+
70
+ This is the same order `parseAgentWalletEnv()` uses, so one environment configures the
71
+ CLI, the MCP server and the framework adapters identically. The order is **fixed**
72
+ rather than "whichever is configured": an environment carrying both is ambiguous about
73
+ which address funds the stream, and that ambiguity is a fund-moving one. When both are
74
+ present the private key wins and the CLI says so on stderr rather than choosing quietly.
75
+
76
+ Three misconfigurations are refused up front, at exit `3`, instead of failing somewhere
77
+ deeper:
78
+
79
+ - **A half-configured CDP wallet** ("you set three of the four variables") is reported as
80
+ exactly that. Rendering it as "no wallet is configured" would send the reader looking
81
+ in the wrong place.
82
+ - **A CDP _smart_ account** (`METRIK_AGENT_CDP_CREATE_SMART_ACCOUNT` /
83
+ `METRIK_AGENT_CDP_SMART_ACCOUNT_NAME`) is refused. It is an ERC-4337 account at a
84
+ different address from its owner, reached through a bundler; this CLI broadcasts
85
+ through `--rpc-url`, so it would sign and spend from the **owner** while you funded the
86
+ smart account.
87
+ - **A non-local signer** is refused with the reason, rather than accepted and failed
88
+ later inside viem.
89
+
90
+ There is no `--private-key` flag, and there will not be one — nor a flag for any other
91
+ credential: a secret on the command line lands in shell history, in `ps` output, and in
92
+ any CI log that echoes the command. Commands print the derived **address**; the
93
+ credential is never echoed, including by `metrik mcp --print-config`.
94
+
95
+ **Privy is not wired into the CLI, and this is not an oversight.** Both Privy adapters in
96
+ `src/wallet/` produce a JSON-RPC account with no `signTransaction`: they sign and
97
+ broadcast by sending `eth_sendTransaction` / `eth_signTypedData_v4` to a provider, so
98
+ they need the wallet client's **transport** to be that provider rather than a plain RPC.
99
+ That is not three more environment variables — it changes the transport for every
100
+ fund-moving command, and `metrik invoke` additionally requires a local signer
101
+ (`InvokeOptions.buyer: LocalAccount`). CDP has neither problem (its account signs
102
+ locally) and is the only wallet path in this package with live CI evidence, which is why
103
+ it is the one that is wired.
104
+
105
+ `invoke` needs the key too, even though it broadcasts nothing: it signs an **off-chain
106
+ EIP-712 invocation capability** as the stream's buyer. `open --dry-run` is the one path
107
+ that needs no key at all — it builds and prints the intended call and says which parts
108
+ (allowance, simulation, mandate signature) it could not produce without one.
109
+
110
+ ## Commands
111
+
112
+ | Command | Needs a wallet? | Broadcasts a tx? | What it does |
113
+ | -------------------- | ------------------------------ | ---------------- | ------------------------------------------------ |
114
+ | `discover` | no | no | Lists verified registry listings |
115
+ | `open` | **yes** (not with `--dry-run`) | **yes** | Escrows USDC into a metered stream |
116
+ | `status` | no | no | One stream's on-chain state + verified accrual |
117
+ | `streams` | no (with `--address`) | no | Lists a wallet's streams from the escrow's logs |
118
+ | `watch` | no | no | Polls that until the stream closes |
119
+ | `invoke` | **yes** (off-chain capability) | no | Calls the paid service, authorized by the stream |
120
+ | `close` | **yes** | **yes** | Stops the clock |
121
+ | `reclaim` | **yes** | **yes** | Takes back what was never earned |
122
+ | `reclaim-unverified` | **yes** | **yes** | Buyer escape hatch: reclaim with no proof |
123
+ | `claim` | **yes** (the operator's) | **yes** | Operator: redeem what verified |
124
+ | `mcp` | no | no | Prints the MCP client config for `metrik-mcp` |
125
+
126
+ ### `metrik discover`
127
+
128
+ ```bash
129
+ metrik discover # everything the registry serves
130
+ metrik discover --min-tier 2 --limit 5 # verified-only, five strongest
131
+ metrik discover --operator 0x… # one operator's listings
132
+ ```
133
+
134
+ `--category` filters the registry's unsigned `category` column. The public discovery
135
+ endpoint serves **signed records only** and carries no category column, so that filter
136
+ matches nothing there — it is for an authenticated PostgREST read.
137
+
138
+ Each row prints what the operator's **signed descriptor** already carries: the callable
139
+ endpoints (method + path + description), one worked example call, and the accepted
140
+ `ratePerSecond` band in USDC **base units** — which is what `--rate … --units atomic`
141
+ takes. Nothing unsigned is presented as fact.
142
+
143
+ Listings are ordered **strongest declared verification first** (re-execution, then schema,
144
+ then SLA, then canaries), ties broken on `serviceRef`, so the order is reproducible and
145
+ `--limit 1` returns the best-attested listing rather than whatever the registry happened to
146
+ serve first.
147
+
148
+ Every row is schema-validated, its `serviceRef` re-derived from the signed record and the
149
+ operator signature recovered — a compromised registry can neither invent an operator to
150
+ pay nor redirect an invocation URL. Rows that fail are dropped and reported on stderr.
151
+
152
+ An unreachable registry is an **error**, never an empty list. "The marketplace is empty"
153
+ and "the registry was down" are different facts and are never rendered the same way.
154
+
155
+ **Who attests is disclosed.** `discover` reads `getOracleSigners()` off the escrow and, when
156
+ a listing's operator is ALSO one of those signers, says so on stderr and sets
157
+ `operatorIsOracleSigner: true` in `--json`. On the current Base Sepolia deployment that
158
+ overlap is real: the escrow has three oracle signers and one of them operates the live
159
+ listings, so the party being paid is among the parties attesting that delivery happened.
160
+ Verification there is not independent of the seller, and a tool that prints "tier 2" and
161
+ "cryptographically verified" without saying so is letting a buyer assume otherwise. The
162
+ CLI does not refuse such a listing and does not score it — it states the fact.
163
+
164
+ If the signer set cannot be read, `operatorIsOracleSigner` is `null` and the warning says
165
+ **UNKNOWN**. "We checked and there is no overlap" and "we could not check" are different
166
+ facts; an escrow outage never renders as a clean bill of health.
167
+
168
+ **SLA text is the operator's own claim.** Anything under `sla` comes from the operator's
169
+ signed descriptor, and a signature proves only that the operator SAID it. Metrik verifies
170
+ delivery, not promises made about it — one live listing claims that two consecutive failed
171
+ intervals auto-pause the stream, and StreamEscrowV2 has no paused state at all, so that
172
+ claim cannot be true. Every rendering of it is labelled unverified, in human output and in
173
+ `--json` (`slaClaimVerified: false`).
174
+
175
+ ### `metrik open`
176
+
177
+ ```bash
178
+ metrik open --service <serviceRef> --rate 0.000001 --budget 0.27 --duration 1h
179
+ metrik open --service <serviceRef> --rate 1 --budget 270000 --duration 3600 --units atomic
180
+ metrik open ... --dry-run
181
+ ```
182
+
183
+ The operator is taken from the **verified listing** for that `serviceRef` (override with
184
+ `--operator`, which then skips the registry lookup). `--dry-run` builds and simulates the
185
+ transaction and broadcasts nothing — it prints the escrow calldata, the bounded USDC
186
+ approval, and the `streamId` a real hire would produce.
187
+
188
+ **`--dry-run` needs no wallet.** It is the look-before-you-leap path, and the person who
189
+ most needs it is the one who has not funded a key yet. Without `METRIK_AGENT_PRIVATE_KEY`
190
+ it prints the intended call and reports `buyer: null`, `mandateSigned: false`, and the
191
+ SDK's own `no account configured — allowance and simulation skipped`, rather than
192
+ inventing an address or a signature. With a key, the preview is a real simulation from a
193
+ real address.
194
+
195
+ **Two pre-flights, both before anything is signed.** Gas and the deposit are separate
196
+ balances on separate tokens, so passing one says nothing about the other:
197
+
198
+ 1. Before a real open, close or reclaim, the CLI reads the wallet's **native** balance and
199
+ refuses at exit `3` if it is zero, naming the Base Sepolia faucet — instead of letting
200
+ the node answer with `gas required exceeds allowance (0)`, which reads like a USDC
201
+ approval problem and is not one.
202
+ 2. Before a real open, it reads the wallet's **USDC** balance and refuses at exit `3` if it
203
+ is below `--budget`, naming Circle's faucet. Without it the open path sends the USDC
204
+ approval first — which succeeds, because approving an amount you do not hold is legal —
205
+ and then reverts inside `transferFrom`, having already spent gas to learn something
206
+ that was one `balanceOf` away.
207
+
208
+ The second check is deliberately **not** an allowance check: the CLI's own open path grants
209
+ the bounded approval it needs (`approveTxHash` in the result), so a missing allowance is a
210
+ step of the command, not a precondition for it. Gating on it would refuse a hire that was
211
+ about to work. Balance is the fact the CLI cannot fix for you.
212
+
213
+ `--dry-run` reports the USDC balance instead of enforcing it — it broadcasts nothing, so an
214
+ underfunded wallet is information there, not a refusal.
215
+
216
+ **A dry run does not predict failure on a first hire.** The simulation runs against current
217
+ chain state, which does not include the bounded USDC approval a real open sends FIRST, so
218
+ `openStream` reverts on the allowance every time that approval is not already in place.
219
+ That is the command's own ordering, not a fault, and reporting it as `simulationError` made
220
+ the one command meant to build confidence forecast failure on first use. It is now reported
221
+ as `simulationPendingApproval: true`, with no `simulationError` — a script branching on
222
+ that field no longer sees the expected path as a failure. An allowance-shaped revert when
223
+ the allowance IS already sufficient, and every other revert, are still surfaced as errors.
224
+
225
+ **Amounts.** `--budget` and `--rate` are decimal USDC by default; `--units atomic`
226
+ switches both to raw 6-decimal base units (what the escrow, the oracle and every
227
+ `METRIK_E2E_*` fixture use). The unit is explicit rather than inferred from magnitude,
228
+ because guessing would be a silent 1,000,000x error on a fund-moving command.
229
+
230
+ **Spend mandates.** Every fund-moving path in this package goes through `checkMandate()`,
231
+ and the CLI is not exempt. The person typing the command is the mandate owner: the
232
+ mandate is self-signed from the same wallet that funds the stream, exactly as the MCP
233
+ server does. If `METRIK_AGENT_MANDATE_*` caps are set, they bind and an oversized hire is
234
+ denied; with no caps configured, the mandate is sized to exactly this hire and scoped to
235
+ exactly this operator. Neither path is an unbounded mandate.
236
+
237
+ ### `metrik status` / `metrik watch`
238
+
239
+ ```bash
240
+ metrik status <streamId>
241
+ metrik watch <streamId> --interval 15 --max-polls 40
242
+ ```
243
+
244
+ `--max-polls` has **no default**: `watch` runs until the stream closes or you interrupt
245
+ it. Both commands apply the same Base Sepolia allowlist as the signing paths — the V1 and
246
+ V2 stream tuples diverge from index 5 onward, so a wrong-chain read does not revert, it
247
+ silently decodes something else into fields with the right names.
248
+
249
+ Accrual is read from the **oracle's checkpoint proof**, not from the stream tuple.
250
+ `StreamEscrowV2.getStream` reports `claimedCumulative = 0` and `settledCumulative = 0`
251
+ for a stream's entire active life (`settledCumulative` is written by `reclaim`), so a
252
+ status built from stream fields would print "earned: 0" for a stream that had been
253
+ delivering for an hour.
254
+
255
+ Before the oracle's first covering checkpoint there is simply no proof, and the CLI says
256
+ so rather than printing a zero that looks like a verdict. The protocol fee is read live
257
+ off the escrow (`protocolFeeBps`), never hardcoded.
258
+
259
+ **No settled figure is printed while the final checkpoint is missing.** Once a stream stops
260
+ — closed, or past `expiresAt` — its accrual is final only after a checkpoint whose
261
+ `checkedAt` reaches that stop time. Until then the latest checkpoint describes a SHORTER
262
+ stream than the one that exists, so a reclaimable computed from it is too large and falls
263
+ when the covering checkpoint lands. A live run read `reclaimable: 1400` straight after
264
+ `close`, `0` two minutes later, and got `NothingToReclaim()` from the reclaim in between.
265
+
266
+ So while that is pending, `buyer reclaimable` reads `pending final checkpoint` with the two
267
+ timestamps that decide it, and `--json` sets `reclaimable: null` plus an explicit
268
+ `settlement` object:
269
+
270
+ ```json
271
+ {
272
+ "reclaimable": null,
273
+ "settlement": {
274
+ "pending": true,
275
+ "stopAt": 1789671000,
276
+ "coveredUpTo": 1789670980,
277
+ "provisionalReclaimable": "1400"
278
+ }
279
+ }
280
+ ```
281
+
282
+ The superseded number is kept under a name that cannot be mistaken for money. If the
283
+ escrow will not say how far the oracle has published, that counts as pending — withholding
284
+ a figure we cannot stand behind is the buyer-favouring error.
285
+
286
+ `operator claimable` is treated differently on purpose: `claimWithProof` accepts any
287
+ retained root, so the operator can act on it now and it can only grow. It is shown, marked
288
+ `not final until the checkpoint covering the stop time lands`, rather than withheld.
289
+
290
+ **The seller's side of the escape hatch.** When a stream has unclaimed earnings, `status`
291
+ also prints an `operator deadline` row: `reclaimUnverified` pays the buyer the whole
292
+ remaining deposit with no proof from `expiresAt + escapeGraceSeconds`, and on the live
293
+ escrow that grace is 900 seconds and immutable. An operator who has earned but not claimed
294
+ loses it at that moment, and the only defence is to `metrik claim` before it — which
295
+ requires knowing the deadline exists.
296
+
297
+ StreamEscrowV2 has exactly two states, `active` and `closed`. There is no paused state
298
+ and the CLI never renders one.
299
+
300
+ Two rows exist so that a buyer can diagnose a stuck reclaim from `status` alone. **oracle
301
+ checkpoint** is how far the oracle has actually published; a value well behind the
302
+ stream's stop time IS the reason a `reclaim` is blocked. **escape hatch** is when the
303
+ no-proof path opens. Both are printed for every stream, not only broken ones — a buyer
304
+ should learn the escape hatch exists before the day they need it.
305
+
306
+ Every deadline on that screen is resolved against the **chain's** clock, not this
307
+ machine's, because the escrow compares against `block.timestamp`. If the chain will not
308
+ answer, the local clock is used and `clockSource` says so rather than hiding it.
309
+
310
+ ### `metrik streams`
311
+
312
+ ```bash
313
+ metrik streams # the configured wallet's streams
314
+ metrik streams --address 0x… --role buyer # anyone's, as buyer only
315
+ metrik streams --limit 5 --json
316
+ ```
317
+
318
+ Answers "which streams are mine?" from the escrow's own `StreamOpened` logs. `buyer` and
319
+ `operator` are **indexed** on that event, so `--role` becomes an `eth_getLogs` topic
320
+ filter the node evaluates: the response carries only that wallet's streams no matter how
321
+ wide the window is.
322
+
323
+ It signs nothing and broadcasts nothing, so `--address` is enough — no wallet needed.
324
+ With no `--address`, the configured wallet is the subject.
325
+
326
+ **The scan is anchored at the escrow's deploy block**, not at a fixed lookback. A
327
+ lookback cannot describe coverage: a wallet whose stream predates the window is shown an
328
+ empty list, indistinguishable from a wallet that never opened one, and telling someone
329
+ their escrowed funds do not exist is the worst failure a payments tool has. The contract
330
+ has no history before its deploy block, so whether the scan reached it is a fact the
331
+ command can state. Pass `--from-block` for a non-default `--escrow`; without one, a
332
+ different escrow is refused rather than scanned from a block that means nothing for it.
333
+
334
+ **An empty result is only reported as "no streams" when it can be trusted.** Three things
335
+ can make a scan partial, and each is named on stderr in both output modes, with
336
+ `degraded: true` and a `degradedReasons` array in `--json`:
337
+
338
+ - the window budget could not reach the anchor (4,000,000 blocks — the live escrow's whole
339
+ history is ~1.06M, so the default scan is complete);
340
+ - a log query failed three times (one transient 429 is retried, not counted);
341
+ - the RPC has **pruned its log index**. A pruning node answers a range it no longer
342
+ retains with a _successful empty result_, so the CLI probes the deploy block first —
343
+ measured on Base Sepolia, that block carries 6 escrow logs, and an endpoint that returns
344
+ none for it has pruned the history and its empty windows cannot be believed.
345
+ `https://base-sepolia-rpc.publicnode.com` fails this probe today; `https://sepolia.base.org`
346
+ passes it.
347
+
348
+ The `--json` output carries `logRetentionProven` (`true` / `false` / `null` when no canary
349
+ was possible). A row whose `getStream` read failed is kept and marked `status: "unread"` —
350
+ never dropped, because a stream that vanishes from this list is exactly what the command
351
+ exists to prevent.
352
+
353
+ `originalDepositUsdc` comes from the **event**, not the stream: StreamEscrowV2 mutates
354
+ `Stream.deposit` on reclaim, so the log is the only surviving record of what was escrowed.
355
+ Earned-so-far is not listed here — that is a per-stream call to the oracle, and
356
+ `metrik status <streamId>` is the command that makes it.
357
+
358
+ ### `metrik mcp --print-config`
359
+
360
+ ```bash
361
+ metrik mcp # same thing: a bare invocation prints it
362
+ metrik mcp --print-config # the block to paste into an MCP client
363
+ metrik mcp --print-config --name metrik-b # a second server key, for a second wallet
364
+ metrik mcp --print-config --json | jq .
365
+ ```
366
+
367
+ Prints the `mcpServers` block for Claude Desktop (or any MCP client) that starts this
368
+ package's stdio MCP server. A **bare `metrik mcp` prints it too** — it used to exit `2`
369
+ demanding `--print-config` while the help said the command prints a config — and states on
370
+ stderr that it printed a config rather than starting anything. The server itself is the
371
+ separate `metrik-mcp` executable, which the host launches. It describes an **installed** package, not this repository:
372
+ the command is `npx -y --package @absol-labs/agent@<this version> metrik-mcp`, pinned to
373
+ the version that printed it — `latest` would drift under an MCP host that signs spend
374
+ mandates.
375
+
376
+ The block carries every variable the server fails closed without: `METRIK_AGENT_RPC_URL`,
377
+ `METRIK_AGENT_ESCROW` and `METRIK_AGENT_USDC` (which have **no** defaults there, unlike in
378
+ this CLI) filled from the settlement config the CLI is already pointed at, plus the four
379
+ `METRIK_AGENT_MANDATE_*` caps without which the server refuses to sign a mandate at all.
380
+ A printed config that omitted them would be a config that fails on first launch, which is
381
+ the failure this command exists to prevent.
382
+
383
+ Configured mandate ceilings are carried through verbatim — they are policy, not secrets,
384
+ and re-typing four caps from memory is where a wrong ceiling comes from. **Credentials are
385
+ always placeholders.** This command never reads a key out of your environment and prints
386
+ it back; the wallet block is simply shaped like the environment it ran in (the CDP
387
+ variables if those are what you have set, `METRIK_AGENT_PRIVATE_KEY` otherwise).
388
+
389
+ ### `metrik invoke`
390
+
391
+ ```bash
392
+ metrik invoke <streamId> --method POST --path /transform --body '{"text":"hi"}'
393
+ metrik invoke <streamId> --path /v1/status --header "accept: application/json"
394
+ metrik invoke <streamId> --path /health --ttl 120
395
+ ```
396
+
397
+ `--path` defaults to `/`, `--method` to `GET`, and `--ttl` (the capability's lifetime in
398
+ seconds) to `60`. `--header` may be repeated.
399
+
400
+ Resolves the invocation origin from the operator's **signed** listing, signs an EIP-712
401
+ `InvocationCapability` scoped to exactly this method and path, and sends it in the
402
+ `x-metrik-capability` header. No out-of-band credential is involved: the stream is the
403
+ credential.
404
+
405
+ `--base-url` is an escape hatch for a local gateway. It bypasses verified listing
406
+ routing, so the CLI warns when it is used.
407
+
408
+ A non-2xx response is an error exit, so a script can branch on it. This command signs a
409
+ capability but broadcasts no transaction — it still requires `METRIK_AGENT_PRIVATE_KEY`,
410
+ because the capability is signed by the stream's buyer.
411
+
412
+ **Not shipped: a `prove` command.** `invokeWithT2DeliveryProof` throws for gated
413
+ listings ("consumer-attested T2 invocation is not supported for gated listings"), and the
414
+ live listings are gated — so a `metrik prove` would be a command that fails for almost
415
+ everyone who ran it. It is left out until that constraint lifts.
416
+
417
+ ### `metrik close` and `metrik reclaim`
418
+
419
+ ```bash
420
+ metrik close <streamId>
421
+ metrik reclaim <streamId> --attempts 12 --interval 10
422
+ metrik reclaim <streamId> --close-first
423
+ ```
424
+
425
+ Nothing pushes money back. `close` stops the clock; `reclaim` is the transaction that
426
+ returns the unspent budget, and the buyer sends it. `close` now points at
427
+ `--close-first` for next time, rather than leaving the one-shot form visible only in
428
+ `metrik reclaim --help`.
429
+
430
+ **`reclaim` waits, by design.** `reclaimWithProof` reverts with `SettlementWindowOpen`
431
+ until a checkpoint whose `checkedAt` is at or after the stream's stop time has posted.
432
+ That is the normal ordering of a checkpoint rail, not a fault — in practice the first
433
+ attempt fails and one roughly 30 seconds later succeeds. The CLI retries **that one
434
+ condition** on a bounded schedule and narrates the wait on stderr; every other failure is
435
+ surfaced immediately and unretried.
436
+
437
+ If it is still blocked when the budget runs out, the message does **not** just say "retry
438
+ later" — every attempt this command makes needs a proof from the oracle, so when the
439
+ oracle is gone, retrying forever never pays. The give-up message names
440
+ `reclaim-unverified`, the path that needs no proof, and says whether its window is open
441
+ yet and what taking it would cost.
442
+
443
+ **`--close-first` is two transactions, sequenced by the CLI.** The reclaim only simulates
444
+ against post-close state, so the close is sent, then confirmed through the same read path
445
+ the reclaim will use, and only then is the reclaim attempted. (`sepolia.base.org` is
446
+ load-balanced: the close receipt can come back from one node while the next simulation
447
+ lands on another that has not imported the block.) If the stream is **already** closed,
448
+ the close is skipped and the command goes straight to reclaim — so re-running it is always
449
+ safe and never dead-ends on `StreamClosedAlready()`.
450
+
451
+ `reclaim` on a stream that is still running reports that in words, with the command that
452
+ fixes it, rather than surfacing the `StreamNotReclaimable()` selector.
453
+
454
+ Close and reclaim are buyer-recovery actions: `VerifiedStreamAgentClient` authorizes them
455
+ on the mandate owner's signature alone, never on caps or expiry, so a lapsed mandate can
456
+ never strand a buyer's own money.
457
+
458
+ ### `metrik claim`
459
+
460
+ ```bash
461
+ metrik claim <streamId>
462
+ ```
463
+
464
+ The seller's side of the loop, and the only redemption surface in this package — the MCP
465
+ server has no claim tool either. `status` has always printed an `operator claimable` row;
466
+ until now nothing in the CLI could act on it.
467
+
468
+ `claimWithProof` is operator-gated on-chain, so the command reads the stream first and
469
+ refuses a wallet that is not the operator **before** it broadcasts. An address mismatch is
470
+ knowable with one read; letting it become a raw `NotOperator()` revert would charge gas to
471
+ learn a fact that was already free. Same for a missing checkpoint: accrual lives in the
472
+ oracle's proof, never in the stream tuple, so no proof means no claim is possible yet and
473
+ the command says that instead of paying for `NothingToClaim()`.
474
+
475
+ ### `metrik reclaim-unverified`
476
+
477
+ ```bash
478
+ metrik reclaim-unverified <streamId> --forfeit-unclaimed
479
+ ```
480
+
481
+ **The escape hatch, for when the oracle cannot give you a proof.** Every other route this
482
+ CLI has to a buyer's money goes through `reclaimWithProof`, so the whole ordinary path is
483
+ only as available as the oracle is. `StreamEscrowV2.reclaimUnverified` is the one that is
484
+ not: buyer-only, no proof, no signer quorum, no guardian.
485
+
486
+ **What it costs.** The escrow sets the operator's entitlement to what they had _already
487
+ claimed on-chain_, and their later claims then revert `NothingToClaim()` forever. So every
488
+ voucher the operator verifiably earned but had not yet redeemed is **permanently forfeit,
489
+ and paid to the buyer**. Nothing recovers it afterwards. That is why this is a separate
490
+ command rather than a `metrik reclaim --unverified` flag one token away from the safe path,
491
+ and why `--forfeit-unclaimed` has no default and no `--no-` form: the only way to reach the
492
+ transaction is to type a phrase that says what it does. Where `metrik reclaim` works, use
493
+ that instead — it settles the operator honestly and still pays you the rest.
494
+
495
+ **When it opens.** The escrow gates it on
496
+
497
+ ```
498
+ block.timestamp >= expiresAt + escapeGraceSeconds
499
+ ```
500
+
501
+ — on **`expiresAt`, never on `closedAt`**. Closing a stream early does **not** bring the
502
+ hatch forward: close a 24-hour stream after ten minutes and the hatch still opens roughly
503
+ 24 hours out, not `escapeGraceSeconds` later. Before then there is genuinely no no-proof
504
+ route, and no CLI change can invent one. Readiness is fully computable before any gas is
505
+ spent (`escapeGraceSeconds` is a public immutable), so the command refuses early rather
506
+ than broadcasting into a guaranteed `EscapeHatchNotReady()`.
507
+
508
+ Before broadcasting it prints what the operator stands to lose, read live. On this path
509
+ that figure is usually **unknown** — the reason to be here at all is that the oracle is not
510
+ answering, and the operator's redeemable balance is derived from the oracle's proof. It is
511
+ reported as unknown, never as zero.
512
+
513
+ > **Deployment note.** `escapeGraceSeconds` is **900 s** on the live Base Sepolia escrow,
514
+ > while the contract's own comment says it should be "typically >= a day" — the point being
515
+ > to give an operator time to redeem before forfeiture. Both values are immutable, so this
516
+ > cannot change without a redeploy. The forfeiture above therefore bites far sooner than
517
+ > the contract author intended.
518
+
519
+ ## Output
520
+
521
+ Human-readable by default. `--json` puts one JSON document on stdout; warnings, progress
522
+ and errors always go to **stderr**, so the stream stays parseable. `metrik watch --json`
523
+ emits **JSON Lines** — one compact object per poll, each on its own line, so every line
524
+ parses standalone:
525
+
526
+ ```bash
527
+ metrik watch <streamId> --json | while read -r line; do
528
+ echo "$line" | jq -r '.verifiedAccrual.cumulativeAmount // "not checkpointed yet"'
529
+ done
530
+ ```
531
+
532
+ Exit codes: `0` success, `2` usage error, `3` refused configuration (wrong chain, no
533
+ wallet, mandate denied), `4` the operation failed.
534
+
535
+ **Nothing waits silently.** Every RPC call carries a 15s budget with two bounded retries,
536
+ and the oracle fetch — an HTTP call the RPC transport does not cover — has its own 20s
537
+ budget that names the endpoint when it expires. Any command still working after 8 seconds
538
+ starts a heartbeat on stderr with the elapsed time, repeating every 10s, because with no
539
+ output at all a slow endpoint and a wedged process are indistinguishable. The heartbeat is
540
+ stderr in both modes, so `--json` stdout stays a single parseable document.
541
+
542
+ Read paths time out; **broadcasts do not.** A transaction that has been sent may already be
543
+ in the mempool, and a CLI that walked away from it on a timer would report a failure for
544
+ something that in fact settled. Write paths keep waiting, and the heartbeat is what makes
545
+ that wait legible.
546
+
547
+ ## Settlement configuration
548
+
549
+ Everything defaults to Metrik's live Base Sepolia deployment, and every value is
550
+ overridable per command or through the environment:
551
+
552
+ | Flag | Env | Default |
553
+ | ---------------- | --------------------------- | ------------------------------------- |
554
+ | `--chain-id` | `METRIK_AGENT_CHAIN_ID` | `84532` |
555
+ | `--rpc-url` | `METRIK_AGENT_RPC_URL` | Base Sepolia public RPC |
556
+ | `--escrow` | `METRIK_AGENT_ESCROW` | `BASE_SEPOLIA_ESCROW_V2` |
557
+ | `--usdc` | `METRIK_AGENT_USDC` | `BASE_SEPOLIA_USDC` |
558
+ | `--oracle-url` | `METRIK_AGENT_ORACLE_URL` | `https://oracle.metrik.live` |
559
+ | `--registry-url` | `METRIK_AGENT_REGISTRY_URL` | the oracle's public listings endpoint |
560
+
561
+ ## Embedding it
562
+
563
+ The runner is exported, with every side-effecting dependency injected:
564
+
565
+ ```ts
566
+ import { runCli } from "@absol-labs/agent/cli";
567
+
568
+ const exitCode = await runCli({ argv: ["status", streamId, "--json"] });
569
+ ```
package/docs/crewai.md CHANGED
@@ -33,9 +33,13 @@ from crewai import Agent, Crew, Task
33
33
  from crewai.mcp import MCPServerStdio
34
34
  from crewai.mcp.filters import create_static_tool_filter
35
35
 
36
+ # `metrik-mcp` is the executable this package publishes. It resolves anywhere
37
+ # `@absol-labs/agent` is installed (npm/pnpm link it into node_modules/.bin) and
38
+ # via `npx --package @absol-labs/agent metrik-mcp`. The old default was
39
+ # `pnpm mcp:stdio`, which only ever resolved inside the agent repository.
36
40
  metrik_mcp = MCPServerStdio(
37
- command="pnpm",
38
- args=["mcp:stdio"],
41
+ command="metrik-mcp",
42
+ args=[],
39
43
  env={
40
44
  "METRIK_AGENT_RPC_URL": "https://base-sepolia.example.invalid",
41
45
  "METRIK_AGENT_ESCROW": "0x0000000000000000000000000000000000000000",
package/docs/eliza.md CHANGED
@@ -50,6 +50,7 @@ runnable-shaped example lives in
50
50
 
51
51
  ```ts
52
52
  import { http } from "viem";
53
+ import { privateKeyToAccount } from "viem/accounts";
53
54
  import { baseSepolia } from "viem/chains";
54
55
  import { createMetrikElizaPlugin } from "@absol-labs/agent/eliza";
55
56
  import { createWalletBackedAgentClient } from "@absol-labs/agent";
@@ -65,7 +66,11 @@ const { agentClient } = await createWalletBackedAgentClient(
65
66
  escrow: process.env.METRIK_AGENT_ESCROW! as `0x${string}`,
66
67
  },
67
68
  },
68
- { privateKey: process.env.METRIK_AGENT_PRIVATE_KEY as `0x${string}` },
69
+ {
70
+ injectedAccount: privateKeyToAccount(
71
+ process.env.METRIK_AGENT_PRIVATE_KEY as `0x${string}`,
72
+ ),
73
+ },
69
74
  );
70
75
 
71
76
  const metrikPlugin = createMetrikElizaPlugin({ agentClient });
@@ -4,8 +4,9 @@ The agent lane ships two MCP transports over the **same** tool set
4
4
  (`hire_verified_service`, `check_stream_status`, `reclaim_unspent`, `list_streams`,
5
5
  and the optional `prove_https_response`):
6
6
 
7
- - **stdio** (`src/mcp/stdio.ts`, `pnpm mcp:stdio`) — local dev, one process per
8
- caller. Unchanged.
7
+ - **stdio** (`src/mcp/stdio.ts`) — local dev, one process per caller. Started with
8
+ the published `metrik-mcp` executable (`npx --package @absol-labs/agent metrik-mcp`),
9
+ or `pnpm mcp:stdio` inside this repository.
9
10
  - **Streamable HTTP** (`src/mcp/http.ts` + `src/mcp/http-server.ts`,
10
11
  `pnpm mcp:http`) — a hosted, **multi-tenant**, per-caller-authenticated
11
12
  endpoint suitable for deployment. This document covers the HTTP transport.
@@ -34,8 +34,8 @@ pnpm add @absol-labs/agent
34
34
  `@absol-labs/agent` and its `@absol-labs/*` dependencies are published to the **public
35
35
  npm registry** — no `.npmrc`, scope registry, or auth token is required to install.
36
36
 
37
- **Runtime:** Node **20.x** (the package pins `engines.node` to `>=20 <21`; 20.19+
38
- recommended). The Coinbase AgentKit action reads decorator metadata via
37
+ **Runtime:** Node **20.19+, 22 LTS or 24** (the package pins `engines.node` to
38
+ `>=20.19 <25`). The Coinbase AgentKit action reads decorator metadata via
39
39
  `reflect-metadata`, which only exists in the **built** output (`tsc` emits decorator
40
40
  metadata; `tsx`/esbuild does not) — so `import "reflect-metadata"` **before** importing
41
41
  the package, and for the AgentKit path import from the built package, not raw `.ts`.
@@ -286,11 +286,23 @@ funds exactly.
286
286
 
287
287
  ---
288
288
 
289
- ## 8. Four ways to integrate
289
+ ## 8. Five ways to integrate
290
290
 
291
- Pick the surface that matches your stack — all four sit on the **same** mandate-gated
291
+ Pick the surface that matches your stack — all five sit on the **same** mandate-gated
292
292
  client and business logic.
293
293
 
294
+ **(a0) The `metrik` CLI** — no code at all, and the only surface that runs the whole
295
+ loop (`discover -> open -> invoke -> watch -> close -> reclaim`):
296
+
297
+ ```bash
298
+ npx --package @absol-labs/agent metrik discover --min-tier 2
299
+ npx --package @absol-labs/agent metrik open --service <serviceRef> \
300
+ --rate 0.000001 --budget 0.27 --duration 1h --dry-run
301
+ ```
302
+
303
+ Testnet-only, key read from `METRIK_AGENT_PRIVATE_KEY`, `--json` for scripting. Full
304
+ reference: [`docs/cli.md`](./cli.md).
305
+
294
306
  **(a) SDK client directly** — the path used above:
295
307
 
296
308
  ```ts
@@ -319,7 +331,8 @@ const agentKit = await createMetrikAgentKit({
319
331
  **(c) MCP** — local stdio for dev, hosted HTTP for deployment:
320
332
 
321
333
  ```bash
322
- pnpm mcp:stdio # local: one process per caller (src/mcp/stdio.ts)
334
+ metrik-mcp # local: one process per caller — the published executable
335
+ pnpm mcp:stdio # same server, from inside this repository (src/mcp/stdio.ts)
323
336
  pnpm mcp:http # hosted: multi-tenant Streamable HTTP, per-caller bearer auth
324
337
  ```
325
338