@absol-labs/agent 0.8.0 → 0.9.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 (49) hide show
  1. package/dist/discovery/registry.d.ts +110 -305
  2. package/dist/discovery/registry.d.ts.map +1 -1
  3. package/dist/discovery/registry.js +141 -318
  4. package/dist/discovery/registry.js.map +1 -1
  5. package/dist/frameworks/agentkit.d.ts.map +1 -1
  6. package/dist/frameworks/agentkit.js +23 -6
  7. package/dist/frameworks/agentkit.js.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/wallet/cdp-sdk.d.ts +23 -0
  13. package/dist/wallet/cdp-sdk.d.ts.map +1 -0
  14. package/dist/wallet/cdp-sdk.js +27 -0
  15. package/dist/wallet/cdp-sdk.js.map +1 -0
  16. package/dist/wallet/provider.d.ts +1 -1
  17. package/dist/wallet/provider.d.ts.map +1 -1
  18. package/dist/wallet/provider.js +8 -3
  19. package/dist/wallet/provider.js.map +1 -1
  20. package/dist/zktls/reclaim-js-sdk.d.ts +24 -0
  21. package/dist/zktls/reclaim-js-sdk.d.ts.map +1 -0
  22. package/dist/zktls/reclaim-js-sdk.js +29 -0
  23. package/dist/zktls/reclaim-js-sdk.js.map +1 -0
  24. package/dist/zktls/reclaim.d.ts +14 -2
  25. package/dist/zktls/reclaim.d.ts.map +1 -1
  26. package/dist/zktls/reclaim.js +29 -6
  27. package/dist/zktls/reclaim.js.map +1 -1
  28. package/dist/zktls/t2-delivery-proof.d.ts +8 -1
  29. package/dist/zktls/t2-delivery-proof.d.ts.map +1 -1
  30. package/dist/zktls/t2-delivery-proof.js +22 -6
  31. package/dist/zktls/t2-delivery-proof.js.map +1 -1
  32. package/docs/agent-layer.md +150 -0
  33. package/docs/autonomous-privy-wallet.md +133 -0
  34. package/docs/crewai.md +70 -0
  35. package/docs/eliza.md +109 -0
  36. package/docs/langchain.md +63 -0
  37. package/docs/mcp-hosted.md +137 -0
  38. package/docs/privy-embedded-wallet.md +102 -0
  39. package/docs/quickstart.md +370 -0
  40. package/docs/threat-model.md +160 -0
  41. package/package.json +19 -6
  42. package/src/discovery/registry.ts +242 -414
  43. package/src/frameworks/agentkit.ts +24 -5
  44. package/src/index.ts +5 -0
  45. package/src/wallet/cdp-sdk.ts +33 -0
  46. package/src/wallet/provider.ts +16 -9
  47. package/src/zktls/reclaim-js-sdk.ts +50 -0
  48. package/src/zktls/reclaim.ts +57 -23
  49. package/src/zktls/t2-delivery-proof.ts +28 -10
@@ -0,0 +1,137 @@
1
+ # Hosted MCP over Streamable HTTP
2
+
3
+ The agent lane ships two MCP transports over the **same** tool set
4
+ (`hire_verified_service`, `check_stream_status`, `reclaim_unspent`, `list_streams`,
5
+ and the optional `prove_https_response`):
6
+
7
+ - **stdio** (`src/mcp/stdio.ts`, `pnpm mcp:stdio`) — local dev, one process per
8
+ caller. Unchanged.
9
+ - **Streamable HTTP** (`src/mcp/http.ts` + `src/mcp/http-server.ts`,
10
+ `pnpm mcp:http`) — a hosted, **multi-tenant**, per-caller-authenticated
11
+ endpoint suitable for deployment. This document covers the HTTP transport.
12
+
13
+ The HTTP transport uses the MCP SDK's `StreamableHTTPServerTransport` (the
14
+ current remote transport; the deprecated HTTP+SSE transport is not used). It
15
+ speaks the standard Streamable HTTP session contract: `POST /mcp` for JSON-RPC
16
+ requests (an `initialize` opens a session and the server returns an
17
+ `Mcp-Session-Id` header), `GET /mcp` for the server→client SSE stream, and
18
+ `DELETE /mcp` to end a session — all carrying the `Mcp-Session-Id` header once a
19
+ session exists.
20
+
21
+ ## Security model (fail-closed)
22
+
23
+ - **Per-caller auth before anything runs.** Every request must carry
24
+ `Authorization: Bearer <token>`. A missing or invalid token is rejected with
25
+ `401` **before** any MCP session is created or any tool executes — so an
26
+ unauthenticated request can never move funds or read another caller's state.
27
+ - **Constant-time token comparison.** Tokens are compared via SHA-256 +
28
+ `crypto.timingSafeEqual` across all configured tenants with no early exit;
29
+ tokens are never logged.
30
+ - **Tenant isolation.** Each valid token maps to its **own** tenant context —
31
+ its own wallet-backed agent client and its own stream registry. One caller
32
+ can never drive another caller's funds or observe another caller's streams.
33
+ MCP sessions are bound to the tenant that opened them: presenting another
34
+ tenant's `Mcp-Session-Id` is rejected with `403`; an unknown session id is
35
+ `404`.
36
+ - **Refuses to run open.** If no tenants/tokens are configured, the server
37
+ throws before binding — it will never serve an unauthenticated endpoint.
38
+ - **No stack traces leak.** Errors are returned as generic JSON-RPC error
39
+ envelopes; internal detail stays server-side.
40
+ - **CORS** is off by default (same-origin / non-browser clients). Set
41
+ `METRIK_MCP_ALLOWED_ORIGINS` to opt specific browser origins in.
42
+
43
+ ## Environment contract
44
+
45
+ | Variable | Required | Default | Meaning |
46
+ | ---------------------------- | -------- | --------- | ---------------------------------------------------------------------- |
47
+ | `METRIK_MCP_TENANTS` | **yes** | — | JSON array of tenant configs (below). Empty/missing ⇒ refuse to start. |
48
+ | `PORT` / `METRIK_MCP_PORT` | no | `8080` | Listen port. |
49
+ | `METRIK_MCP_HOST` | no | `0.0.0.0` | Bind host. |
50
+ | `METRIK_MCP_PATH` | no | `/mcp` | MCP endpoint path. |
51
+ | `METRIK_MCP_ALLOWED_ORIGINS` | no | _(none)_ | Comma-separated CORS origins, or `*`. |
52
+
53
+ Each entry in `METRIK_MCP_TENANTS` is:
54
+
55
+ ```jsonc
56
+ {
57
+ "id": "acme", // stable tenant id (routing/logging only)
58
+ "token": "<secret-bearer>", // the caller's bearer token — never logged
59
+ "env": {
60
+ // optional: layered over the process env to
61
+ "METRIK_AGENT_CDP_OWNER_NAME": "acme-agent", // build THIS tenant's wallet
62
+ },
63
+ }
64
+ ```
65
+
66
+ A tenant's `env` is merged over the process environment and used to build that
67
+ tenant's isolated, wallet-backed agent client via the same
68
+ `METRIK_AGENT_*` / wallet / Reclaim variables the stdio server uses
69
+ (`METRIK_AGENT_RPC_URL`, `METRIK_AGENT_ESCROW`, `METRIK_AGENT_USDC`,
70
+ `METRIK_AGENT_PRIVATE_KEY` **or** the `CDP_*` wallet vars, etc. — see
71
+ `src/mcp/server.ts` and `src/wallet/provider.ts`). Tenants that share the same
72
+ network but need distinct wallets override only the wallet vars in their `env`.
73
+
74
+ > Provide tokens and wallet secrets through your host's secret manager / env UI.
75
+ > Never commit them, and never bake them into the image.
76
+
77
+ ## Run it
78
+
79
+ ```bash
80
+ export METRIK_AGENT_RPC_URL="https://base-sepolia.example/rpc"
81
+ export METRIK_AGENT_ESCROW="0x21948a5E6AE8d9A3D1050791AB6138657Fb54286"
82
+ export METRIK_AGENT_USDC="0x036CbD53842c5426634e7929541eC2318f3dCF7e"
83
+ # ... wallet vars (private key or CDP) ...
84
+ export METRIK_MCP_TENANTS='[{"id":"acme","token":"REPLACE_WITH_SECRET"}]'
85
+ export PORT=8080
86
+
87
+ pnpm mcp:http # dev (tsx)
88
+ # or, from a build:
89
+ node dist/mcp/http-server.js
90
+ ```
91
+
92
+ The process binds `0.0.0.0:$PORT` and logs only its address (never secrets).
93
+ `SIGINT`/`SIGTERM` trigger a graceful shutdown of all live sessions.
94
+
95
+ ## Connect an MCP client
96
+
97
+ Point any Streamable HTTP MCP client at `http(s)://<host>:<port>/mcp` with the
98
+ tenant's bearer token. With the TypeScript SDK:
99
+
100
+ ```ts
101
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
102
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
103
+
104
+ const transport = new StreamableHTTPClientTransport(
105
+ new URL("https://mcp.example.com/mcp"),
106
+ {
107
+ requestInit: {
108
+ headers: { Authorization: `Bearer ${process.env.METRIK_MCP_TOKEN}` },
109
+ },
110
+ },
111
+ );
112
+ const client = new Client({ name: "my-agent", version: "1.0.0" });
113
+ await client.connect(transport);
114
+ await client.listTools();
115
+ await client.callTool({ name: "list_streams", arguments: {} });
116
+ ```
117
+
118
+ Claude Desktop / other config-driven clients: add a remote MCP server pointing
119
+ at the `/mcp` URL and set the `Authorization: Bearer <token>` header. The
120
+ transport handles the session id, POST/GET/DELETE lifecycle automatically.
121
+
122
+ ## Programmatic embedding
123
+
124
+ The transport is also exported from the package root for hosting inside another
125
+ service:
126
+
127
+ - `createHostedMcpHttpServer({ resolver, path?, allowedOrigins? })` — build a
128
+ Node `http.Server` around a `TenantResolver`; `listen(port, host)` /
129
+ `close()`.
130
+ - `StaticTenantResolver` — constant-time, in-memory resolver over a list of
131
+ `HostedMcpTenant`s.
132
+ - `parseHostedMcpServerConfig(env)` / `createHostedMcpTenants(config)` /
133
+ `startHostedMcpHttpServerFromEnv(env)` — the env-driven wiring used by
134
+ `pnpm mcp:http`.
135
+
136
+ Supply your own `TenantResolver` to source tokens/tenants from a database or a
137
+ secrets service instead of env.
@@ -0,0 +1,102 @@
1
+ # Privy embedded-wallet integration
2
+
3
+ `@absol-labs/agent` provides a small, provider-neutral adapter for a
4
+ user-owned Privy embedded **EOA**. The site or host application must first
5
+ authenticate the user with Privy's client SDK and obtain that user's EIP-1193
6
+ wallet provider. The agent package does not authenticate users, create wallets
7
+ from an app id alone, receive an app secret, or persist key material.
8
+
9
+ ```ts
10
+ import { http } from "viem";
11
+ import { baseSepolia } from "viem/chains";
12
+ import {
13
+ createWalletBackedAgentClient,
14
+ type PrivyEip1193Provider,
15
+ } from "@absol-labs/agent";
16
+
17
+ // The authenticated Privy wallet/session is created by the host application.
18
+ const provider: PrivyEip1193Provider =
19
+ await host.getAuthenticatedPrivyProvider();
20
+ const address = await host.getAuthenticatedPrivyAddress();
21
+
22
+ const { wallet, agentClient } = await createWalletBackedAgentClient(
23
+ {
24
+ chain: baseSepolia,
25
+ // The Privy mode replaces this transport with the authenticated provider
26
+ // so writes use eth_sendTransaction. It remains the default for other modes.
27
+ transport: http(process.env.BASE_SEPOLIA_RPC_URL!),
28
+ escrow: "0x…",
29
+ usdc: "0x…",
30
+ },
31
+ {
32
+ privy: {
33
+ appId: "your-public-privy-app-id",
34
+ address,
35
+ provider,
36
+ chainId: 84532,
37
+ },
38
+ },
39
+ );
40
+
41
+ // `wallet.account` is a viem-compatible JSON-RPC EOA. Every autonomous write
42
+ // still goes through the signed Metrik spend-mandate checks in agentClient.
43
+ await wallet.account.signMessage({ message: "user-approved test" });
44
+ ```
45
+
46
+ The adapter verifies Base Sepolia (`chainId: 84532`) and the configured address
47
+ against `eth_chainId` and `eth_accounts` before construction and before every
48
+ `personal_sign` or `eth_signTypedData_v4` request. Contract writes are sent
49
+ through the same authenticated provider with `eth_sendTransaction`; the adapter
50
+ does not claim access to raw transaction signatures. A network or account switch
51
+ fails closed with `PrivyEmbeddedWalletError`.
52
+
53
+ ## Headless external-agent session
54
+
55
+ The Metrik dApp can attach a Privy additional signer under a non-empty Privy policy,
56
+ collect an owner-signed Metrik spend mandate, and exchange those approvals for a
57
+ short-lived broker token. An external agent adapts the copied session JSON without any
58
+ Privy app secret or wallet key:
59
+
60
+ ```ts
61
+ import {
62
+ createPrivySessionProvider,
63
+ createWalletBackedAgentClient,
64
+ } from "@absol-labs/agent";
65
+
66
+ const session = JSON.parse(process.env.METRIK_AGENT_SESSION_JSON!);
67
+ const provider = createPrivySessionProvider({
68
+ brokerUrl: session.brokerUrl,
69
+ sessionToken: session.sessionToken,
70
+ address: session.address,
71
+ });
72
+
73
+ const { agentClient } = await createWalletBackedAgentClient(sdkConfig, {
74
+ privy: {
75
+ appId: session.appId,
76
+ address: session.address,
77
+ provider,
78
+ chainId: session.chainId,
79
+ },
80
+ });
81
+ ```
82
+
83
+ The broker permits only bounded USDC approval, V2 stream open/close/reclaim, and
84
+ short-lived invocation-capability signatures for an active buyer stream. It stores a
85
+ hash—not the plaintext—of the session token. Revoked or expired sessions fail closed.
86
+ It asks Privy to sponsor only the first successful write in the session.
87
+
88
+ ## Boundary and limitations
89
+
90
+ - The adapter supports a user-owned Privy EOA only.
91
+ - It does not claim ERC-4337, EIP-1271, or smart-account support.
92
+ - The browser adapter alone does not provide headless access. Headless operation requires
93
+ the deployed broker, a real Privy additional-signer policy, and user approval in the dApp.
94
+ - The provider must implement the standard `eth_sendTransaction` EIP-1193 path.
95
+ In Privy mode, `createWalletBackedAgentClient` replaces the SDK transport with
96
+ that authenticated provider so a separate public RPC cannot bypass the wallet.
97
+ - The host must keep the Privy app secret and any authorization material
98
+ server-side. Only the public app id belongs in browser configuration.
99
+
100
+ Do not use an unauthenticated or arbitrary EIP-1193 provider as a substitute
101
+ for Privy's authenticated user wallet. Do not put a Privy app secret in
102
+ `PrivyEmbeddedWalletConfig`.
@@ -0,0 +1,370 @@
1
+ # Quickstart — hire a verified service in <10 minutes
2
+
3
+ Get an AI agent from zero to a **hired, oracle-verified, buyer-recoverable** service stream
4
+ on Base Sepolia. Every code block below uses the real, current exports of
5
+ `@absol-labs/agent` — no pseudocode.
6
+
7
+ > _x402 proves the payment, Metrik proves the delivery._
8
+
9
+ **What you get:** an owner-signed spend mandate caps how much/how fast/how long/with whom
10
+ your agent can spend; the agent escrows USDC and pays a seller **only for the delivery an
11
+ independent oracle proves**. In V2, failed or unproven intervals do not advance cumulative
12
+ entitlement. The stream remains active until the buyer closes it or it expires, and reclaim
13
+ follows the checkpoint finalization or escape-window rules.
14
+
15
+ Base Sepolia constants used throughout (chainId **84532**):
16
+
17
+ | Thing | Address |
18
+ | ------ | -------------------------------------------- |
19
+ | Escrow | `0x21948a5E6AE8d9A3D1050791AB6138657Fb54286` |
20
+ | USDC | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
21
+
22
+ A complete, runnable version of this journey lives in
23
+ [`scripts/e2e-cdp.ts`](../scripts/e2e-cdp.ts) (the live CI E2E, metrik-agent#32) — treat
24
+ it as the working reference.
25
+
26
+ ---
27
+
28
+ ## 1. Install
29
+
30
+ ```bash
31
+ pnpm add @absol-labs/agent
32
+ ```
33
+
34
+ `@absol-labs/agent` and its `@absol-labs/*` dependencies are published to the **public
35
+ npm registry** — no `.npmrc`, scope registry, or auth token is required to install.
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
39
+ `reflect-metadata`, which only exists in the **built** output (`tsc` emits decorator
40
+ metadata; `tsx`/esbuild does not) — so `import "reflect-metadata"` **before** importing
41
+ the package, and for the AgentKit path import from the built package, not raw `.ts`.
42
+
43
+ ---
44
+
45
+ ## 2. Provision a wallet
46
+
47
+ Two supported wallet sources, both resolved through `resolveAgentWallet` /
48
+ `parseAgentWalletEnv`. Metrik never reads or exports a private key in CDP mode.
49
+
50
+ **Injected key (fastest for testnet):**
51
+
52
+ ```ts
53
+ import { privateKeyToAccount } from "viem/accounts";
54
+ import { resolveAgentWallet } from "@absol-labs/agent";
55
+
56
+ const account = privateKeyToAccount(
57
+ process.env.METRIK_AGENT_PRIVATE_KEY as `0x${string}`,
58
+ );
59
+ const wallet = await resolveAgentWallet({ injectedAccount: account });
60
+ // wallet.source === "injected"; wallet.account is a viem Account
61
+ ```
62
+
63
+ **Coinbase CDP Server Wallet v2 (production, remote signing):**
64
+
65
+ ```ts
66
+ import { CdpClient } from "@coinbase/cdp-sdk";
67
+ import { resolveAgentWallet, type CdpClientLike } from "@absol-labs/agent";
68
+
69
+ const cdp = new CdpClient({
70
+ apiKeyId: process.env.CDP_API_KEY_ID,
71
+ apiKeySecret: process.env.CDP_API_KEY_SECRET,
72
+ walletSecret: process.env.CDP_WALLET_SECRET,
73
+ });
74
+ const wallet = await resolveAgentWallet(
75
+ { cdp: { ownerName: process.env.METRIK_AGENT_CDP_OWNER_NAME! } },
76
+ { createCdpClient: () => cdp as unknown as CdpClientLike },
77
+ );
78
+ // under the hood: cdp.evm.getOrCreateAccount({ name: ownerName })
79
+ ```
80
+
81
+ CDP env: `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, `CDP_WALLET_SECRET`,
82
+ `METRIK_AGENT_CDP_OWNER_NAME` (+ optional `METRIK_AGENT_CDP_SMART_ACCOUNT_NAME`,
83
+ `METRIK_AGENT_CDP_CREATE_SMART_ACCOUNT`). Or set `METRIK_AGENT_PRIVATE_KEY` and let
84
+ `parseAgentWalletEnv(process.env)` pick the injected path automatically.
85
+
86
+ ---
87
+
88
+ ## 3. Sign a spend mandate
89
+
90
+ The mandate is the guardrail: caps + allowlists checked **before any transaction**,
91
+ fail-closed. Build the EIP-712 payload with `createSpendMandateTypedData`, sign it with
92
+ the wallet, then validate the shape with `signedSpendMandateSchema`.
93
+
94
+ ```ts
95
+ import {
96
+ spendMandateSchema,
97
+ createSpendMandateTypedData,
98
+ signedSpendMandateSchema,
99
+ } from "@absol-labs/agent";
100
+ import { generatePrivateKey } from "viem/accounts"; // handy for a random bytes32 mandateId
101
+
102
+ const nowSeconds = Math.floor(Date.now() / 1000);
103
+ const account = wallet.account;
104
+
105
+ const mandate = spendMandateSchema.parse({
106
+ maxPerStreamUsdc: 100_000n, // 0.1 USDC (6 decimals) — max deposit per stream
107
+ maxTotalUsdc: 100_000n, // cumulative cap across all streams
108
+ maxRatePerSecondUsdc: 1n, // max accrual rate the agent may agree to
109
+ maxDurationSeconds: 3600, // max stream lifetime
110
+ allowedOperators: [account.address as `0x${string}`], // omit/empty ⇒ any operator
111
+ expiresAt: nowSeconds + 365 * 24 * 60 * 60,
112
+ });
113
+
114
+ const unsigned = {
115
+ mandateId: generatePrivateKey(), // any 32-byte hex
116
+ owner: account.address as `0x${string}`,
117
+ chainId: 84532,
118
+ issuedAt: nowSeconds,
119
+ mandate,
120
+ };
121
+
122
+ const signature = await account.signTypedData!(
123
+ createSpendMandateTypedData(unsigned),
124
+ );
125
+ const signedMandate = signedSpendMandateSchema.parse({
126
+ ...unsigned,
127
+ signature,
128
+ });
129
+ ```
130
+
131
+ Denials are machine-readable (`MandateDecision.reason`): `exceeds-per-stream-cap`,
132
+ `exceeds-total-cap`, `exceeds-rate-cap`, `exceeds-duration-cap`, `operator-not-allowed`,
133
+ `mandate-expired`, `mandate-revoked`, `invalid-signature`.
134
+
135
+ ---
136
+
137
+ ## 4. Discover a service
138
+
139
+ Before you can hire, you need a seller. `discoverServices` reads the Metrik listings
140
+ registry and returns typed, **verified** services — it recovers each row's operator
141
+ signature and drops any listing whose signature does not match its operator or whose
142
+ `serviceRef` is not the one derived from its signed record. It never returns an
143
+ unverifiable listing.
144
+
145
+ **Zero configuration**: the default read source is
146
+ `https://oracle.metrik.live/listings`, the oracle's public, credential-free,
147
+ CORS-enabled endpoint. You need no API key and no setup.
148
+
149
+ **It fails loudly.** An unreachable, erroring, or non-200 registry throws
150
+ `RegistryUnavailableError` rather than returning `[]`. "The marketplace was never
151
+ reached" and "the marketplace is empty" are different facts, and a caller that reads
152
+ `[]` and gives up has been misled. Catch it only if your code can genuinely handle
153
+ not having reached the marketplace at all.
154
+
155
+ ```ts
156
+ import { discoverServices, RegistryUnavailableError } from "@absol-labs/agent";
157
+
158
+ let services;
159
+ try {
160
+ services = await discoverServices({ limit: 10 });
161
+ } catch (error) {
162
+ if (error instanceof RegistryUnavailableError) {
163
+ // The registry was never read — do NOT treat this as "no services".
164
+ throw error;
165
+ }
166
+ throw error;
167
+ }
168
+
169
+ for (const s of services) {
170
+ console.log(`${s.serviceRef} → ${s.operator} @ ${s.accessUrl} [${s.access}]`);
171
+ }
172
+
173
+ const target = services[0]; // pick one to hire
174
+ ```
175
+
176
+ Each `ServiceListing` is
177
+ `{ serviceRef, operator, publicUrl, access, accessUrl, signed, category }`, with
178
+ `serviceRef`/`operator`/`publicUrl`/`accessUrl` taken from the **signed** record, never
179
+ from the unsigned envelope columns. Invoke against `accessUrl` — for a `gated` listing
180
+ that is the operator-signed gateway origin, and a gated listing with no signed access
181
+ URL is dropped rather than invoked against an unsigned one.
182
+
183
+ Need the registry's full answer — `registryAvailable`, `source`, tier and verification
184
+ summaries, and a note explaining when a non-empty source filtered to nothing? Call
185
+ `discoverServicesDetailed` instead; it returns the SDK result verbatim.
186
+
187
+ > The `category` filter matches only on an authenticated PostgREST read; the public
188
+ > endpoint serves signed records and carries no category column. Supplying `apiKey`
189
+ > also requires an explicit `registryUrl` (a Supabase project URL) — the public
190
+ > default is not a PostgREST base, so that combination is rejected rather than
191
+ > silently 404ing.
192
+
193
+ ---
194
+
195
+ ## 5. Hire a verified service
196
+
197
+ `VerifiedStreamAgentClient.openVerifiedStream` runs the mandate guard, then escrows USDC
198
+ and opens the metered stream through `@absol-labs/sdk`. It throws `MandateDeniedError`
199
+ before any transaction if the hire breaches the mandate. Feed it the `operator` +
200
+ `serviceRef` from a discovered service.
201
+
202
+ ```ts
203
+ import { VerifiedStreamAgentClient } from "@absol-labs/agent";
204
+ import type { StreamProofClientConfig } from "@absol-labs/sdk";
205
+ import { baseSepolia } from "viem/chains";
206
+ import { http } from "viem";
207
+
208
+ const sdkConfig: StreamProofClientConfig = {
209
+ chain: baseSepolia,
210
+ transport: http("https://sepolia.base.org"),
211
+ account,
212
+ escrow: "0x21948a5E6AE8d9A3D1050791AB6138657Fb54286",
213
+ usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
214
+ };
215
+
216
+ const agent = new VerifiedStreamAgentClient(sdkConfig);
217
+
218
+ const hire = await agent.openVerifiedStream({
219
+ operator: target.operator, // seller payout wallet from discovery
220
+ serviceRef: target.serviceRef, // bytes32 the oracle verifies delivery against
221
+ budgetUsdc: 100_000n,
222
+ ratePerSecondUsdc: 1n,
223
+ maxDurationSeconds: 3600,
224
+ signedMandate,
225
+ spentSoFarUsdc: 0n,
226
+ nowSeconds: Math.floor(Date.now() / 1000),
227
+ });
228
+
229
+ console.log(`stream ${hire.streamId} opened, tx=${hire.txHash}`);
230
+ ```
231
+
232
+ Metrik is infra-agnostic (Model A): a stream targets a seller's payout wallet +
233
+ `serviceRef` only — there is no service-network field. Settlement is single-chain
234
+ on Base Sepolia.
235
+
236
+ ---
237
+
238
+ ## 6. Monitor
239
+
240
+ `getStreamStatus` returns the on-chain stream plus derived `claimable`/`reclaimable`:
241
+
242
+ ```ts
243
+ const { stream, claimable, reclaimable } = await agent.getStreamStatus(
244
+ hire.streamId,
245
+ );
246
+
247
+ console.log(stream.status); // "active" | "closed" in StreamEscrowV2
248
+ console.log(stream.accrued); // USDC accrued for oracle-verified delivery
249
+ console.log(stream.deposit); // total escrowed
250
+ console.log(claimable); // claimable by the seller now
251
+ console.log(reclaimable); // reclaimable by the buyer now
252
+ ```
253
+
254
+ Payment only ever accrues for verified seconds. In StreamEscrowV2, a checkpoint advances
255
+ the operator's cumulative entitlement; a failed or unproven interval does not. Failure does
256
+ not change the stream from active to paused: the stream stays active until buyer close or
257
+ expiry. The buyer reclaims after checkpoint finalization, or through the escape-window path
258
+ when checkpoints are unavailable.
259
+
260
+ ---
261
+
262
+ ## 7. Settle
263
+
264
+ **Seller** claims earned USDC (net of protocol fee); **buyer** closes and reclaims the
265
+ unspent balance. All three are mandate-gated.
266
+
267
+ ```ts
268
+ const now = () => Math.floor(Date.now() / 1000);
269
+ const auth = { streamId: hire.streamId, signedMandate, spentSoFarUsdc: 0n };
270
+
271
+ // Seller side — claim what delivery earned:
272
+ await agent.claimStream({ ...auth, nowSeconds: now() });
273
+
274
+ // Buyer side — close, then reclaim every unspent cent:
275
+ await agent.closeStream({ ...auth, nowSeconds: now() });
276
+ const { reclaimResult } = await agent.reclaimStream({
277
+ ...auth,
278
+ nowSeconds: now(),
279
+ });
280
+ // or in one call: agent.reclaimStream({ ...auth, nowSeconds: now(), closeFirst: true })
281
+ ```
282
+
283
+ **Buyer-protection guarantee:** the buyer can recover unspent USDC under the checkpoint /
284
+ escape-window rules, and `claimed + fees + reclaimed == deposit` conserves the escrowed
285
+ funds exactly.
286
+
287
+ ---
288
+
289
+ ## 8. Four ways to integrate
290
+
291
+ Pick the surface that matches your stack — all four sit on the **same** mandate-gated
292
+ client and business logic.
293
+
294
+ **(a) SDK client directly** — the path used above:
295
+
296
+ ```ts
297
+ import { VerifiedStreamAgentClient } from "@absol-labs/agent";
298
+ const agent = new VerifiedStreamAgentClient(sdkConfig);
299
+ await agent.openVerifiedStream({
300
+ /* ... */
301
+ });
302
+ ```
303
+
304
+ **(b) Coinbase AgentKit** — exposes `discover_services`, `hire_verified_service`,
305
+ `check_stream_status`, `reclaim_unspent`, `close_stream` as native actions:
306
+
307
+ ```ts
308
+ import "reflect-metadata";
309
+ import { createMetrikAgentKit } from "@absol-labs/agent/agentkit";
310
+
311
+ const agentKit = await createMetrikAgentKit({
312
+ walletProvider, // a CdpEvmWalletProvider / CdpSmartWalletProvider
313
+ metrik: { signedMandate, sdkConfig }, // chainId defaults to 84532
314
+ });
315
+ // then agentKit.getVercelAITools() or getLangChainTools() — or LangChain directly:
316
+ // import { createLangChainVerifiedStreamTools } from "@absol-labs/agent/langchain";
317
+ ```
318
+
319
+ **(c) MCP** — local stdio for dev, hosted HTTP for deployment:
320
+
321
+ ```bash
322
+ pnpm mcp:stdio # local: one process per caller (src/mcp/stdio.ts)
323
+ pnpm mcp:http # hosted: multi-tenant Streamable HTTP, per-caller bearer auth
324
+ ```
325
+
326
+ Tools: `discover_services`, `hire_verified_service`, `check_stream_status`, `reclaim_unspent`,
327
+ `list_streams` (+ `prove_https_response` when Reclaim creds are set). Env is read via
328
+ `parseMetrikAgentEnv` (`METRIK_AGENT_RPC_URL` / `_ESCROW` / `_USDC` + wallet vars). The
329
+ hosted endpoint requires `Authorization: Bearer <token>` before anything runs — see
330
+ [`docs/mcp-hosted.md`](./mcp-hosted.md).
331
+
332
+ **Spend mandate (required).** An MCP caller (an LLM) cannot produce an EIP-712 signature,
333
+ so the mandate is **not** a tool argument — the server holds it, self-signed from its own
334
+ funding wallet and bounded by env caps you set. A fund-moving MCP server **refuses to
335
+ start** without an explicit spend ceiling:
336
+
337
+ ```bash
338
+ METRIK_AGENT_MANDATE_MAX_PER_STREAM_USDC=100000000 # 100 USDC (atomic, 6dp) per stream
339
+ METRIK_AGENT_MANDATE_MAX_TOTAL_USDC=1000000000 # 1000 USDC cumulative across streams
340
+ METRIK_AGENT_MANDATE_MAX_RATE_PER_SECOND_USDC=1000000 # 1 USDC/sec ceiling
341
+ METRIK_AGENT_MANDATE_MAX_DURATION_SECONDS=86400 # max stream lifetime
342
+ METRIK_AGENT_MANDATE_ALLOWED_OPERATORS=0xabc...,0xdef... # optional CSV allowlist; omit = any
343
+ METRIK_AGENT_MANDATE_TTL_SECONDS=86400 # optional mandate lifetime (default 24h)
344
+ ```
345
+
346
+ The `hire_verified_service` tool then takes only the hire terms (`operator`, `serviceRef`,
347
+ `budgetUsdc`, `ratePerSecondUsdc`, optional `durationSeconds`), all USDC in **atomic
348
+ 6-decimal units**. The cumulative cap (`maxTotalUsdc`) is enforced by default; mandate
349
+ revocation requires wiring a revocation resolver (the server warns once on stderr if none
350
+ is set).
351
+
352
+ **(d) ElizaOS** — a plugin with `DISCOVER_SERVICES`, `HIRE_VERIFIED_SERVICE`,
353
+ `CHECK_STREAM_STATUS`, `RECLAIM_UNSPENT`, `CLOSE_STREAM` actions:
354
+
355
+ ```ts
356
+ import { metrikElizaPlugin } from "@absol-labs/agent/eliza"; // === createMetrikElizaPlugin
357
+
358
+ const plugin = metrikElizaPlugin({ agentClient: agent }); // agent = VerifiedStreamAgentClient
359
+ // register `plugin` on your Eliza character — see docs/eliza.md + examples/metrik-character.ts
360
+ ```
361
+
362
+ ---
363
+
364
+ ## Honesty note
365
+
366
+ Metrik proves **verified delivery** (responded, correctly-shaped, within SLA to real
367
+ traffic) — **not output correctness** (that the model's answer is right). Pitch and treat
368
+ it accordingly. The full journey, run live against the real deployed escrow on Base
369
+ Sepolia, is [`scripts/e2e-cdp.ts`](../scripts/e2e-cdp.ts) (`pnpm build:e2e && pnpm
370
+ e2e:cdp`) — the working reference for everything above.