@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.
- package/README.md +184 -26
- package/dist/capability/invocation-capability.d.ts +18 -6
- package/dist/capability/invocation-capability.d.ts.map +1 -1
- package/dist/capability/invocation-capability.js.map +1 -1
- package/dist/cli/amounts.d.ts +26 -0
- package/dist/cli/amounts.d.ts.map +1 -0
- package/dist/cli/amounts.js +71 -0
- package/dist/cli/amounts.js.map +1 -0
- package/dist/cli/args.d.ts +32 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +119 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/attesters.d.ts +51 -0
- package/dist/cli/attesters.d.ts.map +1 -0
- package/dist/cli/attesters.js +58 -0
- package/dist/cli/attesters.js.map +1 -0
- package/dist/cli/bin.d.ts +3 -0
- package/dist/cli/bin.d.ts.map +1 -0
- package/dist/cli/bin.js +5 -0
- package/dist/cli/bin.js.map +1 -0
- package/dist/cli/commands/claim.d.ts +13 -0
- package/dist/cli/commands/claim.d.ts.map +1 -0
- package/dist/cli/commands/claim.js +102 -0
- package/dist/cli/commands/claim.js.map +1 -0
- package/dist/cli/commands/close.d.ts +3 -0
- package/dist/cli/commands/close.d.ts.map +1 -0
- package/dist/cli/commands/close.js +55 -0
- package/dist/cli/commands/close.js.map +1 -0
- package/dist/cli/commands/discover.d.ts +3 -0
- package/dist/cli/commands/discover.d.ts.map +1 -0
- package/dist/cli/commands/discover.js +183 -0
- package/dist/cli/commands/discover.js.map +1 -0
- package/dist/cli/commands/invoke.d.ts +3 -0
- package/dist/cli/commands/invoke.d.ts.map +1 -0
- package/dist/cli/commands/invoke.js +142 -0
- package/dist/cli/commands/invoke.js.map +1 -0
- package/dist/cli/commands/mcp.d.ts +3 -0
- package/dist/cli/commands/mcp.d.ts.map +1 -0
- package/dist/cli/commands/mcp.js +113 -0
- package/dist/cli/commands/mcp.js.map +1 -0
- package/dist/cli/commands/open.d.ts +3 -0
- package/dist/cli/commands/open.d.ts.map +1 -0
- package/dist/cli/commands/open.js +331 -0
- package/dist/cli/commands/open.js.map +1 -0
- package/dist/cli/commands/reclaim-unverified.d.ts +3 -0
- package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -0
- package/dist/cli/commands/reclaim-unverified.js +172 -0
- package/dist/cli/commands/reclaim-unverified.js.map +1 -0
- package/dist/cli/commands/reclaim.d.ts +33 -0
- package/dist/cli/commands/reclaim.d.ts.map +1 -0
- package/dist/cli/commands/reclaim.js +308 -0
- package/dist/cli/commands/reclaim.js.map +1 -0
- package/dist/cli/commands/status.d.ts +3 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +43 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/streams.d.ts +3 -0
- package/dist/cli/commands/streams.d.ts.map +1 -0
- package/dist/cli/commands/streams.js +257 -0
- package/dist/cli/commands/streams.js.map +1 -0
- package/dist/cli/commands/watch.d.ts +3 -0
- package/dist/cli/commands/watch.d.ts.map +1 -0
- package/dist/cli/commands/watch.js +55 -0
- package/dist/cli/commands/watch.js.map +1 -0
- package/dist/cli/config.d.ts +61 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +140 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/context.d.ts +29 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/context.js +2 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/deps.d.ts +191 -0
- package/dist/cli/deps.d.ts.map +1 -0
- package/dist/cli/deps.js +277 -0
- package/dist/cli/deps.js.map +1 -0
- package/dist/cli/descriptor.d.ts +61 -0
- package/dist/cli/descriptor.d.ts.map +1 -0
- package/dist/cli/descriptor.js +78 -0
- package/dist/cli/descriptor.js.map +1 -0
- package/dist/cli/errors.d.ts +48 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +129 -0
- package/dist/cli/errors.js.map +1 -0
- package/dist/cli/escape-hatch.d.ts +50 -0
- package/dist/cli/escape-hatch.d.ts.map +1 -0
- package/dist/cli/escape-hatch.js +45 -0
- package/dist/cli/escape-hatch.js.map +1 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +7 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/listings.d.ts +15 -0
- package/dist/cli/listings.d.ts.map +1 -0
- package/dist/cli/listings.js +56 -0
- package/dist/cli/listings.js.map +1 -0
- package/dist/cli/mandate.d.ts +66 -0
- package/dist/cli/mandate.d.ts.map +1 -0
- package/dist/cli/mandate.js +61 -0
- package/dist/cli/mandate.js.map +1 -0
- package/dist/cli/output.d.ts +33 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +34 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/preflight.d.ts +38 -0
- package/dist/cli/preflight.d.ts.map +1 -0
- package/dist/cli/preflight.js +72 -0
- package/dist/cli/preflight.js.map +1 -0
- package/dist/cli/privy.d.ts +83 -0
- package/dist/cli/privy.d.ts.map +1 -0
- package/dist/cli/privy.js +230 -0
- package/dist/cli/privy.js.map +1 -0
- package/dist/cli/progress.d.ts +36 -0
- package/dist/cli/progress.d.ts.map +1 -0
- package/dist/cli/progress.js +40 -0
- package/dist/cli/progress.js.map +1 -0
- package/dist/cli/run.d.ts +25 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +173 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/scan-range.d.ts +55 -0
- package/dist/cli/scan-range.d.ts.map +1 -0
- package/dist/cli/scan-range.js +57 -0
- package/dist/cli/scan-range.js.map +1 -0
- package/dist/cli/spend-policy.d.ts +60 -0
- package/dist/cli/spend-policy.d.ts.map +1 -0
- package/dist/cli/spend-policy.js +65 -0
- package/dist/cli/spend-policy.js.map +1 -0
- package/dist/cli/status.d.ts +90 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +252 -0
- package/dist/cli/status.js.map +1 -0
- package/dist/cli/timeout.d.ts +30 -0
- package/dist/cli/timeout.d.ts.map +1 -0
- package/dist/cli/timeout.js +55 -0
- package/dist/cli/timeout.js.map +1 -0
- package/dist/cli/version.d.ts +12 -0
- package/dist/cli/version.d.ts.map +1 -0
- package/dist/cli/version.js +22 -0
- package/dist/cli/version.js.map +1 -0
- package/dist/cli/wallet.d.ts +164 -0
- package/dist/cli/wallet.d.ts.map +1 -0
- package/dist/cli/wallet.js +234 -0
- package/dist/cli/wallet.js.map +1 -0
- package/dist/frameworks/crewai.d.ts +10 -0
- package/dist/frameworks/crewai.d.ts.map +1 -1
- package/dist/frameworks/crewai.js +12 -2
- package/dist/frameworks/crewai.js.map +1 -1
- package/dist/gateway/caller-auth-gateway.d.ts +38 -3
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +25 -1
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +53 -0
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +189 -4
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/gateway/server-entry.d.ts +1 -0
- package/dist/gateway/server-entry.js +1 -0
- package/dist/gateway/server-entry.js.map +1 -1
- package/dist/gateway/x402-mode.d.ts +123 -0
- package/dist/gateway/x402-mode.d.ts.map +1 -0
- package/dist/gateway/x402-mode.js +212 -0
- package/dist/gateway/x402-mode.js.map +1 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/index.js.map +1 -1
- package/dist/mandates/env.d.ts +3 -2
- package/dist/mandates/env.d.ts.map +1 -1
- package/dist/mandates/env.js +3 -2
- package/dist/mandates/env.js.map +1 -1
- package/dist/mcp/http-server.d.ts +1 -0
- package/dist/mcp/http-server.js +1 -0
- package/dist/mcp/http-server.js.map +1 -1
- package/dist/mcp/server.d.ts +49 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +533 -25
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/service-invoker.d.ts +97 -0
- package/dist/mcp/service-invoker.d.ts.map +1 -0
- package/dist/mcp/service-invoker.js +167 -0
- package/dist/mcp/service-invoker.js.map +1 -0
- package/dist/mcp/stdio.d.ts +1 -0
- package/dist/mcp/stdio.js +1 -0
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/sdk/client.d.ts +25 -1
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/client.js +20 -1
- package/dist/sdk/client.js.map +1 -1
- package/dist/sdk/invoke.d.ts +19 -4
- package/dist/sdk/invoke.d.ts.map +1 -1
- package/dist/sdk/invoke.js +69 -5
- package/dist/sdk/invoke.js.map +1 -1
- package/dist/x402/delivery-oracle.d.ts +108 -0
- package/dist/x402/delivery-oracle.d.ts.map +1 -0
- package/dist/x402/delivery-oracle.js +174 -0
- package/dist/x402/delivery-oracle.js.map +1 -0
- package/dist/x402/facilitator-server-entry.d.ts +3 -0
- package/dist/x402/facilitator-server-entry.d.ts.map +1 -0
- package/dist/x402/facilitator-server-entry.js +27 -0
- package/dist/x402/facilitator-server-entry.js.map +1 -0
- package/dist/x402/facilitator-server.d.ts +73 -0
- package/dist/x402/facilitator-server.d.ts.map +1 -0
- package/dist/x402/facilitator-server.js +305 -0
- package/dist/x402/facilitator-server.js.map +1 -0
- package/dist/x402/facilitator.d.ts +69 -17
- package/dist/x402/facilitator.d.ts.map +1 -1
- package/dist/x402/facilitator.js +76 -33
- package/dist/x402/facilitator.js.map +1 -1
- package/dist/x402/payment-requirements.d.ts +106 -0
- package/dist/x402/payment-requirements.d.ts.map +1 -0
- package/dist/x402/payment-requirements.js +138 -0
- package/dist/x402/payment-requirements.js.map +1 -0
- package/dist/x402/relayer.d.ts +41 -0
- package/dist/x402/relayer.d.ts.map +1 -0
- package/dist/x402/relayer.js +64 -0
- package/dist/x402/relayer.js.map +1 -0
- package/dist/x402/resource-index.d.ts +75 -0
- package/dist/x402/resource-index.d.ts.map +1 -0
- package/dist/x402/resource-index.js +126 -0
- package/dist/x402/resource-index.js.map +1 -0
- package/dist/x402/verified-delivery-facilitator.d.ts +285 -0
- package/dist/x402/verified-delivery-facilitator.d.ts.map +1 -0
- package/dist/x402/verified-delivery-facilitator.js +423 -0
- package/dist/x402/verified-delivery-facilitator.js.map +1 -0
- package/dist/zktls/t2-delivery-proof.d.ts +6 -6
- package/docs/agent-layer.md +14 -9
- package/docs/cli.md +705 -0
- package/docs/crewai.md +6 -2
- package/docs/eliza.md +6 -1
- package/docs/mcp-hosted.md +7 -5
- package/docs/mcp-reference.md +175 -0
- package/docs/quickstart.md +36 -9
- package/docs/threat-model.md +4 -3
- package/docs/x402.md +648 -0
- package/package.json +33 -10
- package/src/capability/invocation-capability.ts +17 -1
- package/src/cli/amounts.ts +102 -0
- package/src/cli/args.ts +150 -0
- package/src/cli/attesters.ts +86 -0
- package/src/cli/bin.ts +5 -0
- package/src/cli/commands/claim.ts +126 -0
- package/src/cli/commands/close.ts +69 -0
- package/src/cli/commands/discover.ts +228 -0
- package/src/cli/commands/invoke.ts +178 -0
- package/src/cli/commands/mcp.ts +135 -0
- package/src/cli/commands/open.ts +397 -0
- package/src/cli/commands/reclaim-unverified.ts +218 -0
- package/src/cli/commands/reclaim.ts +377 -0
- package/src/cli/commands/status.ts +56 -0
- package/src/cli/commands/streams.ts +344 -0
- package/src/cli/commands/watch.ts +70 -0
- package/src/cli/config.ts +208 -0
- package/src/cli/context.ts +30 -0
- package/src/cli/deps.ts +528 -0
- package/src/cli/descriptor.ts +133 -0
- package/src/cli/errors.ts +162 -0
- package/src/cli/escape-hatch.ts +92 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/listings.ts +77 -0
- package/src/cli/mandate.ts +126 -0
- package/src/cli/output.ts +63 -0
- package/src/cli/preflight.ts +88 -0
- package/src/cli/privy.ts +396 -0
- package/src/cli/progress.ts +75 -0
- package/src/cli/run.ts +209 -0
- package/src/cli/scan-range.ts +76 -0
- package/src/cli/spend-policy.ts +139 -0
- package/src/cli/status.ts +381 -0
- package/src/cli/timeout.ts +64 -0
- package/src/cli/version.ts +21 -0
- package/src/cli/wallet.ts +372 -0
- package/src/frameworks/crewai.ts +13 -2
- package/src/gateway/caller-auth-gateway.ts +77 -6
- package/src/gateway/http-server.ts +280 -6
- package/src/gateway/server-entry.ts +1 -0
- package/src/gateway/x402-mode.ts +350 -0
- package/src/index.ts +40 -5
- package/src/mandates/env.ts +3 -2
- package/src/mcp/http-server.ts +1 -0
- package/src/mcp/server.ts +706 -30
- package/src/mcp/service-invoker.ts +285 -0
- package/src/mcp/stdio.ts +1 -0
- package/src/sdk/client.ts +33 -1
- package/src/sdk/invoke.ts +90 -12
- package/src/x402/delivery-oracle.ts +285 -0
- package/src/x402/facilitator-server-entry.ts +35 -0
- package/src/x402/facilitator-server.ts +445 -0
- package/src/x402/facilitator.ts +125 -46
- package/src/x402/payment-requirements.ts +231 -0
- package/src/x402/relayer.ts +122 -0
- package/src/x402/resource-index.ts +228 -0
- 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
|
+
```
|