@absol-labs/agent 0.10.0 → 0.12.0-rc.1
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 +106 -30
- package/dist/capability/invocation-capability.d.ts +20 -8
- package/dist/capability/invocation-capability.d.ts.map +1 -1
- package/dist/capability/invocation-capability.js.map +1 -1
- package/dist/capability/probe-capability.d.ts +225 -0
- package/dist/capability/probe-capability.d.ts.map +1 -0
- package/dist/capability/probe-capability.js +248 -0
- package/dist/capability/probe-capability.js.map +1 -0
- package/dist/cli/args.d.ts +11 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +23 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/commands/claim.d.ts.map +1 -1
- package/dist/cli/commands/claim.js +21 -5
- package/dist/cli/commands/claim.js.map +1 -1
- package/dist/cli/commands/close.d.ts.map +1 -1
- package/dist/cli/commands/close.js +51 -7
- package/dist/cli/commands/close.js.map +1 -1
- package/dist/cli/commands/invoke.d.ts +2 -0
- package/dist/cli/commands/invoke.d.ts.map +1 -1
- package/dist/cli/commands/invoke.js +3 -2
- package/dist/cli/commands/invoke.js.map +1 -1
- package/dist/cli/commands/mcp.d.ts.map +1 -1
- package/dist/cli/commands/mcp.js +15 -2
- package/dist/cli/commands/mcp.js.map +1 -1
- package/dist/cli/commands/open.d.ts.map +1 -1
- package/dist/cli/commands/open.js +121 -15
- package/dist/cli/commands/open.js.map +1 -1
- package/dist/cli/commands/pay.d.ts +19 -0
- package/dist/cli/commands/pay.d.ts.map +1 -0
- package/dist/cli/commands/pay.js +355 -0
- package/dist/cli/commands/pay.js.map +1 -0
- package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
- package/dist/cli/commands/reclaim-unverified.js +15 -6
- package/dist/cli/commands/reclaim-unverified.js.map +1 -1
- package/dist/cli/commands/reclaim.d.ts +23 -2
- package/dist/cli/commands/reclaim.d.ts.map +1 -1
- package/dist/cli/commands/reclaim.js +200 -21
- package/dist/cli/commands/reclaim.js.map +1 -1
- package/dist/cli/commands/refund.d.ts +13 -0
- package/dist/cli/commands/refund.d.ts.map +1 -0
- package/dist/cli/commands/refund.js +224 -0
- package/dist/cli/commands/refund.js.map +1 -0
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +10 -1
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/commands/streams.js +9 -13
- package/dist/cli/commands/streams.js.map +1 -1
- package/dist/cli/commands/watch.d.ts.map +1 -1
- package/dist/cli/commands/watch.js +2 -1
- package/dist/cli/commands/watch.js.map +1 -1
- package/dist/cli/config.d.ts +44 -26
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +88 -38
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/deps.d.ts +86 -7
- package/dist/cli/deps.d.ts.map +1 -1
- package/dist/cli/deps.js +102 -16
- package/dist/cli/deps.js.map +1 -1
- package/dist/cli/errors.d.ts.map +1 -1
- package/dist/cli/errors.js +21 -1
- package/dist/cli/errors.js.map +1 -1
- package/dist/cli/escape-hatch.d.ts +6 -4
- package/dist/cli/escape-hatch.d.ts.map +1 -1
- package/dist/cli/escape-hatch.js +6 -2
- package/dist/cli/escape-hatch.js.map +1 -1
- package/dist/cli/listings.d.ts +18 -0
- package/dist/cli/listings.d.ts.map +1 -1
- package/dist/cli/listings.js +22 -12
- package/dist/cli/listings.js.map +1 -1
- package/dist/cli/preflight.d.ts +20 -14
- package/dist/cli/preflight.d.ts.map +1 -1
- package/dist/cli/preflight.js +34 -20
- package/dist/cli/preflight.js.map +1 -1
- package/dist/cli/privy.d.ts +83 -0
- package/dist/cli/privy.d.ts.map +1 -0
- package/dist/cli/privy.js +232 -0
- package/dist/cli/privy.js.map +1 -0
- package/dist/cli/reclaimability.d.ts +82 -0
- package/dist/cli/reclaimability.d.ts.map +1 -0
- package/dist/cli/reclaimability.js +80 -0
- package/dist/cli/reclaimability.js.map +1 -0
- package/dist/cli/run.d.ts +2 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +40 -11
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/scan-range.d.ts +2 -2
- package/dist/cli/scan-range.js +2 -2
- 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 +3 -1
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +15 -3
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/wallet.d.ts +109 -40
- package/dist/cli/wallet.d.ts.map +1 -1
- package/dist/cli/wallet.js +109 -44
- package/dist/cli/wallet.js.map +1 -1
- package/dist/discovery/rate-band.d.ts +74 -0
- package/dist/discovery/rate-band.d.ts.map +1 -0
- package/dist/discovery/rate-band.js +114 -0
- package/dist/discovery/rate-band.js.map +1 -0
- package/dist/escrow-generation.d.ts +81 -0
- package/dist/escrow-generation.d.ts.map +1 -0
- package/dist/escrow-generation.js +76 -0
- package/dist/escrow-generation.js.map +1 -0
- package/dist/frameworks/agentkit.d.ts +8 -4
- package/dist/frameworks/agentkit.d.ts.map +1 -1
- package/dist/frameworks/agentkit.js +27 -10
- package/dist/frameworks/agentkit.js.map +1 -1
- package/dist/frameworks/eliza.d.ts +3 -3
- package/dist/frameworks/eliza.d.ts.map +1 -1
- package/dist/frameworks/eliza.js +15 -6
- package/dist/frameworks/eliza.js.map +1 -1
- package/dist/frameworks/langchain.d.ts.map +1 -1
- package/dist/frameworks/langchain.js +8 -0
- package/dist/frameworks/langchain.js.map +1 -1
- package/dist/gateway/caller-auth-gateway.d.ts +91 -4
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +184 -7
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +121 -1
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +474 -5
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/gateway/probe-auth.d.ts +112 -0
- package/dist/gateway/probe-auth.d.ts.map +1 -0
- package/dist/gateway/probe-auth.js +127 -0
- package/dist/gateway/probe-auth.js.map +1 -0
- package/dist/gateway/signed-rate-bounds.d.ts +51 -0
- package/dist/gateway/signed-rate-bounds.d.ts.map +1 -0
- package/dist/gateway/signed-rate-bounds.js +94 -0
- package/dist/gateway/signed-rate-bounds.js.map +1 -0
- package/dist/gateway/x402-opener-mode.d.ts +215 -0
- package/dist/gateway/x402-opener-mode.d.ts.map +1 -0
- package/dist/gateway/x402-opener-mode.js +639 -0
- package/dist/gateway/x402-opener-mode.js.map +1 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -5
- 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/server.d.ts +100 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +719 -39
- 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/x402-tools.d.ts +104 -0
- package/dist/mcp/x402-tools.d.ts.map +1 -0
- package/dist/mcp/x402-tools.js +434 -0
- package/dist/mcp/x402-tools.js.map +1 -0
- package/dist/network.d.ts +119 -0
- package/dist/network.d.ts.map +1 -0
- package/dist/network.js +199 -0
- package/dist/network.js.map +1 -0
- package/dist/sdk/invoke.d.ts +23 -4
- package/dist/sdk/invoke.d.ts.map +1 -1
- package/dist/sdk/invoke.js +71 -10
- package/dist/sdk/invoke.js.map +1 -1
- package/dist/wallet/autonomous-wallet-broker.d.ts +3 -0
- package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet-broker.js +27 -23
- package/dist/wallet/autonomous-wallet-broker.js.map +1 -1
- package/dist/wallet/autonomous-wallet-protocol.d.ts +8 -2
- package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet-protocol.js +9 -2
- package/dist/wallet/autonomous-wallet-protocol.js.map +1 -1
- package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
- package/dist/wallet/autonomous-wallet.js +3 -2
- package/dist/wallet/autonomous-wallet.js.map +1 -1
- package/dist/wallet/broker-escrows.d.ts +222 -0
- package/dist/wallet/broker-escrows.d.ts.map +1 -0
- package/dist/wallet/broker-escrows.js +36 -0
- package/dist/wallet/broker-escrows.js.map +1 -0
- package/dist/wallet/lifecycle.d.ts +21 -5
- package/dist/wallet/lifecycle.d.ts.map +1 -1
- package/dist/wallet/lifecycle.js +32 -12
- package/dist/wallet/lifecycle.js.map +1 -1
- package/dist/wallet/privy-broker-server.d.ts.map +1 -1
- package/dist/wallet/privy-broker-server.js +31 -26
- package/dist/wallet/privy-broker-server.js.map +1 -1
- package/dist/wallet/privy-session-broker.d.ts +14 -5
- package/dist/wallet/privy-session-broker.d.ts.map +1 -1
- package/dist/wallet/privy-session-broker.js +29 -17
- package/dist/wallet/privy-session-broker.js.map +1 -1
- package/dist/wallet/privy-session-provider.d.ts +3 -1
- package/dist/wallet/privy-session-provider.d.ts.map +1 -1
- package/dist/wallet/privy-session-provider.js +5 -3
- package/dist/wallet/privy-session-provider.js.map +1 -1
- package/dist/wallet/provider.d.ts.map +1 -1
- package/dist/wallet/provider.js +15 -8
- package/dist/wallet/provider.js.map +1 -1
- package/dist/x402/facilitator.d.ts +42 -17
- package/dist/x402/facilitator.d.ts.map +1 -1
- package/dist/x402/facilitator.js +49 -33
- package/dist/x402/facilitator.js.map +1 -1
- package/dist/x402-buyer/guarded-fetch.d.ts +51 -0
- package/dist/x402-buyer/guarded-fetch.d.ts.map +1 -0
- package/dist/x402-buyer/guarded-fetch.js +147 -0
- package/dist/x402-buyer/guarded-fetch.js.map +1 -0
- package/dist/x402-buyer/index.d.ts +10 -0
- package/dist/x402-buyer/index.d.ts.map +1 -0
- package/dist/x402-buyer/index.js +10 -0
- package/dist/x402-buyer/index.js.map +1 -0
- package/dist/x402-buyer/pay.d.ts +135 -0
- package/dist/x402-buyer/pay.d.ts.map +1 -0
- package/dist/x402-buyer/pay.js +336 -0
- package/dist/x402-buyer/pay.js.map +1 -0
- package/dist/x402-opener/abi.d.ts +684 -0
- package/dist/x402-opener/abi.d.ts.map +1 -0
- package/dist/x402-opener/abi.js +880 -0
- package/dist/x402-opener/abi.js.map +1 -0
- package/dist/x402-opener/facilitator-entry.d.ts +3 -0
- package/dist/x402-opener/facilitator-entry.d.ts.map +1 -0
- package/dist/x402-opener/facilitator-entry.js +37 -0
- package/dist/x402-opener/facilitator-entry.js.map +1 -0
- package/dist/x402-opener/facilitator-server.d.ts +180 -0
- package/dist/x402-opener/facilitator-server.d.ts.map +1 -0
- package/dist/x402-opener/facilitator-server.js +696 -0
- package/dist/x402-opener/facilitator-server.js.map +1 -0
- package/dist/x402-opener/index.d.ts +14 -0
- package/dist/x402-opener/index.d.ts.map +1 -0
- package/dist/x402-opener/index.js +14 -0
- package/dist/x402-opener/index.js.map +1 -0
- package/dist/x402-opener/listing-resolver.d.ts +55 -0
- package/dist/x402-opener/listing-resolver.d.ts.map +1 -0
- package/dist/x402-opener/listing-resolver.js +125 -0
- package/dist/x402-opener/listing-resolver.js.map +1 -0
- package/dist/x402-opener/refund-relay.d.ts +240 -0
- package/dist/x402-opener/refund-relay.d.ts.map +1 -0
- package/dist/x402-opener/refund-relay.js +1240 -0
- package/dist/x402-opener/refund-relay.js.map +1 -0
- package/dist/x402-opener/relayer-queue.d.ts +14 -0
- package/dist/x402-opener/relayer-queue.d.ts.map +1 -0
- package/dist/x402-opener/relayer-queue.js +18 -0
- package/dist/x402-opener/relayer-queue.js.map +1 -0
- package/dist/x402-opener/scheme.d.ts +217 -0
- package/dist/x402-opener/scheme.d.ts.map +1 -0
- package/dist/x402-opener/scheme.js +749 -0
- package/dist/x402-opener/scheme.js.map +1 -0
- package/dist/x402-opener/terms.d.ts +105 -0
- package/dist/x402-opener/terms.d.ts.map +1 -0
- package/dist/x402-opener/terms.js +212 -0
- package/dist/x402-opener/terms.js.map +1 -0
- package/dist/zktls/reclaim.d.ts +2 -2
- package/docs/agent-layer.md +13 -8
- package/docs/autonomous-privy-wallet.md +188 -0
- package/docs/cli.md +342 -53
- package/docs/langchain.md +5 -0
- package/docs/mcp-hosted.md +4 -3
- package/docs/mcp-reference.md +245 -0
- package/docs/probe-capability.md +191 -0
- package/docs/quickstart.md +54 -5
- package/docs/threat-model.md +24 -3
- package/docs/x402.md +681 -0
- package/package.json +19 -6
- package/src/capability/invocation-capability.ts +17 -1
- package/src/capability/probe-capability.ts +355 -0
- package/src/cli/args.ts +29 -0
- package/src/cli/commands/claim.ts +31 -5
- package/src/cli/commands/close.ts +56 -7
- package/src/cli/commands/invoke.ts +6 -2
- package/src/cli/commands/mcp.ts +18 -2
- package/src/cli/commands/open.ts +151 -14
- package/src/cli/commands/pay.ts +442 -0
- package/src/cli/commands/reclaim-unverified.ts +24 -6
- package/src/cli/commands/reclaim.ts +274 -19
- package/src/cli/commands/refund.ts +277 -0
- package/src/cli/commands/status.ts +17 -1
- package/src/cli/commands/streams.ts +11 -15
- package/src/cli/commands/watch.ts +7 -1
- package/src/cli/config.ts +139 -52
- package/src/cli/deps.ts +189 -25
- package/src/cli/errors.ts +24 -1
- package/src/cli/escape-hatch.ts +19 -5
- package/src/cli/listings.ts +43 -10
- package/src/cli/preflight.ts +36 -21
- package/src/cli/privy.ts +398 -0
- package/src/cli/reclaimability.ts +154 -0
- package/src/cli/run.ts +54 -11
- package/src/cli/scan-range.ts +2 -2
- package/src/cli/spend-policy.ts +139 -0
- package/src/cli/status.ts +14 -1
- package/src/cli/wallet.ts +189 -58
- package/src/discovery/rate-band.ts +172 -0
- package/src/escrow-generation.ts +135 -0
- package/src/frameworks/agentkit.ts +45 -11
- package/src/frameworks/eliza.ts +27 -5
- package/src/frameworks/langchain.ts +14 -0
- package/src/gateway/caller-auth-gateway.ts +336 -13
- package/src/gateway/http-server.ts +690 -7
- package/src/gateway/probe-auth.ts +191 -0
- package/src/gateway/signed-rate-bounds.ts +164 -0
- package/src/gateway/x402-opener-mode.ts +974 -0
- package/src/index.ts +112 -14
- package/src/mandates/env.ts +3 -2
- package/src/mcp/server.ts +973 -44
- package/src/mcp/service-invoker.ts +285 -0
- package/src/mcp/x402-tools.ts +621 -0
- package/src/network.ts +302 -0
- package/src/sdk/invoke.ts +96 -17
- package/src/wallet/autonomous-wallet-broker.ts +46 -20
- package/src/wallet/autonomous-wallet-protocol.ts +11 -2
- package/src/wallet/autonomous-wallet.ts +4 -2
- package/src/wallet/broker-escrows.ts +66 -0
- package/src/wallet/lifecycle.ts +53 -18
- package/src/wallet/privy-broker-server.ts +35 -25
- package/src/wallet/privy-session-broker.ts +58 -25
- package/src/wallet/privy-session-provider.ts +7 -3
- package/src/wallet/provider.ts +20 -15
- package/src/x402/facilitator.ts +85 -46
- package/src/x402-buyer/guarded-fetch.ts +213 -0
- package/src/x402-buyer/index.ts +9 -0
- package/src/x402-buyer/pay.ts +517 -0
- package/src/x402-opener/abi.ts +881 -0
- package/src/x402-opener/facilitator-entry.ts +45 -0
- package/src/x402-opener/facilitator-server.ts +1014 -0
- package/src/x402-opener/index.ts +13 -0
- package/src/x402-opener/listing-resolver.ts +202 -0
- package/src/x402-opener/refund-relay.ts +1743 -0
- package/src/x402-opener/relayer-queue.ts +18 -0
- package/src/x402-opener/scheme.ts +1133 -0
- package/src/x402-opener/terms.ts +300 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# MCP tool reference
|
|
2
|
+
|
|
3
|
+
The canonical description of what `@absol-labs/agent`'s MCP server offers an
|
|
4
|
+
agent, what each tool costs, and what it refuses to do. Both transports — stdio
|
|
5
|
+
(`metrik-mcp`) and hosted Streamable HTTP (`metrik-mcp-http`, see
|
|
6
|
+
[`mcp-hosted.md`](./mcp-hosted.md)) — serve exactly this set.
|
|
7
|
+
|
|
8
|
+
The names and descriptions here are the ones in `METRIK_MCP_TOOLS`
|
|
9
|
+
(`src/mcp/server.ts`); a clean-install check asserts the published `metrik-mcp`
|
|
10
|
+
binary really offers them (`scripts/check-packed-package.mjs`).
|
|
11
|
+
|
|
12
|
+
## What you are buying: TIME, not calls
|
|
13
|
+
|
|
14
|
+
A stream rents an endpoint that the oracle keeps probing. **The meter runs on
|
|
15
|
+
availability that VERIFIED, not on the requests you send.** Cost is
|
|
16
|
+
`ratePerSecondUsdc x verified seconds`, capped at the escrowed budget. One
|
|
17
|
+
request and a thousand requests cost the same second, and the clock keeps
|
|
18
|
+
running while you send none.
|
|
19
|
+
|
|
20
|
+
A reviewer who opened a stream, made a single `invoke_service` call and was
|
|
21
|
+
billed for 302 seconds was billed exactly as designed. `reclaim_unspent` stops
|
|
22
|
+
the clock; nothing else does, and **nothing pushes money back on its own**.
|
|
23
|
+
|
|
24
|
+
The escrow proves _delivery_, not _correctness_. An endpoint that answers, on
|
|
25
|
+
time, in the right shape, is what gets paid — whether its answer is any good is
|
|
26
|
+
an economic question, not a cryptographic one.
|
|
27
|
+
|
|
28
|
+
## The tools
|
|
29
|
+
|
|
30
|
+
| Tool | Moves funds | What it does |
|
|
31
|
+
| ----------------------- | ----------- | ------------------------------------------------------------------------------- |
|
|
32
|
+
| `discover_services` | no | Lists marketplace services whose operator signature verifies. |
|
|
33
|
+
| `hire_verified_service` | **yes** | Escrows USDC and opens a per-second stream. **Starts the meter.** |
|
|
34
|
+
| `invoke_service` | no | Calls the service the stream is paying for. Adds no cost. |
|
|
35
|
+
| `check_stream_status` | no | Reads live status, settled amount, claimable/reclaimable. |
|
|
36
|
+
| `reclaim_unspent` | **yes** | Closes the stream (**stops the meter**) and returns unspent USDC to the buyer. |
|
|
37
|
+
| `list_streams` | no | Lists the streams this server opened. |
|
|
38
|
+
| `prove_https_response` | no | Buyer-side zkTLS proof of a response. Registered only with Reclaim credentials. |
|
|
39
|
+
| `x402_pay` | **yes** | Pays an x402 endpoint (opener path, docs/x402.md) under the spend mandate. |
|
|
40
|
+
| `refund_status` | no | Refund state of an x402 stream, from the seller's gasless refund relay. |
|
|
41
|
+
| `refund` | **yes** | Gasless close (payer-signed) and reclaim of an opener stream via the relay. |
|
|
42
|
+
|
|
43
|
+
### `discover_services`
|
|
44
|
+
|
|
45
|
+
`category?`, `limit?`. Returns `serviceRef`, `operator`, `publicUrl`, `access`,
|
|
46
|
+
`accessUrl` per listing, plus `ratePerSecondBand` (`minRatePerSecondUsdc` /
|
|
47
|
+
`maxRatePerSecondUsdc`, atomic units, `null` for an unsigned side) when the
|
|
48
|
+
operator's signed descriptor carries one.
|
|
49
|
+
|
|
50
|
+
The registry is an **untrusted transport**: every `serviceRef` is re-derived
|
|
51
|
+
from the signed record and the operator's signature recovered, so a compromised
|
|
52
|
+
registry can neither invent an operator to pay nor redirect an invocation. Rows
|
|
53
|
+
that fail verification are dropped. An unreachable registry is an **error**, not
|
|
54
|
+
an empty list — "zero services" and "could not reach the marketplace" must never
|
|
55
|
+
read the same.
|
|
56
|
+
|
|
57
|
+
### `hire_verified_service`
|
|
58
|
+
|
|
59
|
+
`operator`, `serviceRef`, `budgetUsdc`, `ratePerSecondUsdc`, `durationSeconds?`.
|
|
60
|
+
All USDC amounts are **atomic 6-decimal units** (`1 USDC = "1000000"`).
|
|
61
|
+
|
|
62
|
+
Bounded by the server's own signed spend mandate (per-stream, cumulative, rate,
|
|
63
|
+
duration, operator allowlist, expiry), configured through
|
|
64
|
+
`METRIK_AGENT_MANDATE_*`. An MCP caller cannot produce an EIP-712 signature, so
|
|
65
|
+
the mandate is never a tool argument — the server holds it and fails closed
|
|
66
|
+
without one.
|
|
67
|
+
|
|
68
|
+
`operator` is the wallet that gets paid, and it arrives as a separate argument
|
|
69
|
+
from `serviceRef`. The pair is cross-checked against the verified registry, and
|
|
70
|
+
the check **fails closed**: the hire proceeds only on a positive match.
|
|
71
|
+
|
|
72
|
+
A contradicted pairing escrows a buyer's USDC to a stranger for someone else's
|
|
73
|
+
service, and nothing downstream catches it — the escrow does not know who owns a
|
|
74
|
+
`serviceRef`, and the oracle verifies the real endpoint while the money accrues
|
|
75
|
+
elsewhere. A registry read that fails is no evidence the pairing is sound:
|
|
76
|
+
settlement does not depend on that HTTP endpoint being reachable from the MCP
|
|
77
|
+
host, so during an outage the bug would be fully live. An unlisted `serviceRef`
|
|
78
|
+
is refused for the same reason — a hostile registry cannot forge a match, but it
|
|
79
|
+
can suppress a row, which would otherwise downgrade a contradiction into a pass.
|
|
80
|
+
|
|
81
|
+
Refusing strands nothing: no USDC has moved and the same call succeeds once the
|
|
82
|
+
registry answers. If you must accept that risk,
|
|
83
|
+
`METRIK_AGENT_ALLOW_UNVERIFIED_OPERATOR=true` downgrades "could not verify" to a
|
|
84
|
+
warning on the result. It is operator-set input only — never a tool argument,
|
|
85
|
+
because the caller being asked to relax a fund-safety check is the LLM the check
|
|
86
|
+
exists to constrain — and it never overrides a positive contradiction.
|
|
87
|
+
|
|
88
|
+
`ratePerSecondUsdc` must sit inside the listing's operator-signed rate band
|
|
89
|
+
(inclusive). The descriptor is re-verified with the gateway's own check, and an
|
|
90
|
+
out-of-band rate is refused before any approval or open is sent: the escrow
|
|
91
|
+
accepts any non-zero rate, but the seller's gateway answers every request on
|
|
92
|
+
such a stream with `403 stream-rate-out-of-bounds`. A descriptor that does not
|
|
93
|
+
verify is a refusal. Under `METRIK_AGENT_ALLOW_UNVERIFIED_OPERATOR=true` an
|
|
94
|
+
unverifiable pairing has no listing to read a band from, and the result's
|
|
95
|
+
warning says the rate was not checked; the override never widens a band the
|
|
96
|
+
server can read.
|
|
97
|
+
|
|
98
|
+
The result carries a `metering` block: the rate, the budget, the maximum this
|
|
99
|
+
stream can ever spend, and what starts and stops the clock.
|
|
100
|
+
|
|
101
|
+
### `invoke_service`
|
|
102
|
+
|
|
103
|
+
`streamId`, `path?` (default `/`), `method?` (default `GET`), `headers?`,
|
|
104
|
+
`body?`, `ttlSeconds?` (default 60, max 300).
|
|
105
|
+
|
|
106
|
+
Signs a single-use EIP-712 `InvocationCapability` as the stream's buyer — scoped
|
|
107
|
+
to exactly this method and path — and calls the service through the operator's
|
|
108
|
+
caller-auth gateway.
|
|
109
|
+
|
|
110
|
+
- **The origin is not a parameter.** It is resolved from the operator-signed
|
|
111
|
+
listing for the stream's own `serviceRef`. An LLM-chosen base URL is precisely
|
|
112
|
+
the injection this refuses; there is no override.
|
|
113
|
+
- **The path cannot leave that origin.** The URL is resolved first and the
|
|
114
|
+
result is then proven to carry the signed origin and stay under the signed
|
|
115
|
+
base path — a prefix check on the raw path is not sufficient, because the URL
|
|
116
|
+
parser reads a backslash as a host separator for http(s). Backslashes and
|
|
117
|
+
control characters are rejected outright as well. Resolution happens **before**
|
|
118
|
+
the capability is signed, so a path that escapes never causes a buyer-signed
|
|
119
|
+
credential to exist.
|
|
120
|
+
- **Fails closed before any network call** if the stream is not active, has
|
|
121
|
+
expired, or does not belong to this server's wallet — each with an error that
|
|
122
|
+
names the next step.
|
|
123
|
+
- A gateway rejection is surfaced with its machine-readable reason
|
|
124
|
+
(`stream-not-funded`, `capability-expired`, `nonce-replayed`, …) and a next
|
|
125
|
+
step, rather than a bare `403`.
|
|
126
|
+
- Responses are capped at 64 KiB with `bodyTruncated` reported explicitly.
|
|
127
|
+
- It signs no transaction and moves no funds.
|
|
128
|
+
|
|
129
|
+
### `check_stream_status`
|
|
130
|
+
|
|
131
|
+
`streamId`. Returns the stream, `claimable`, `reclaimable`, and a `metering`
|
|
132
|
+
block. A stream id that does not exist on the configured escrow is reported as
|
|
133
|
+
such rather than as "closed" — an unset escrow slot decodes as an all-zero
|
|
134
|
+
struct instead of reverting.
|
|
135
|
+
|
|
136
|
+
### `reclaim_unspent`
|
|
137
|
+
|
|
138
|
+
`streamId`, `closeFirst?`. Closing is what stops the meter.
|
|
139
|
+
|
|
140
|
+
Authorized on the mandate owner's signature **alone** — never gated by mandate
|
|
141
|
+
expiry, revocation, or caps, because those must never strand a buyer's own
|
|
142
|
+
funds. It also works for a stream this server has no local record of (the
|
|
143
|
+
escrow itself enforces buyer-only recovery), so losing in-memory state cannot
|
|
144
|
+
stand between a buyer and their escrow.
|
|
145
|
+
|
|
146
|
+
### `list_streams`
|
|
147
|
+
|
|
148
|
+
No arguments. Every active stream listed is still accruing cost per verified
|
|
149
|
+
second, whether or not it is being invoked.
|
|
150
|
+
|
|
151
|
+
### `prove_https_response`
|
|
152
|
+
|
|
153
|
+
Registered **only** when Reclaim credentials are configured
|
|
154
|
+
(`RECLAIM_APP_ID` + `RECLAIM_APP_SECRET`). Generates a buyer-side zkTLS proof of an HTTPS response.
|
|
155
|
+
|
|
156
|
+
**Public listings only.** The Reclaim attestor fetches the URL itself, from its
|
|
157
|
+
own network position, and cannot carry this stream's single-use caller-auth
|
|
158
|
+
capability — so against a gated listing the only thing it could prove is its own
|
|
159
|
+
`403`. The tool says that instead of producing one. Use `invoke_service` for
|
|
160
|
+
gated services; that delivery is covered by the oracle's own verification.
|
|
161
|
+
|
|
162
|
+
The proof target is pinned to the stream's verified listing origin. A proof of
|
|
163
|
+
some other origin, stamped with this stream's delivery binding, would be
|
|
164
|
+
evidence of a delivery that did not happen here.
|
|
165
|
+
|
|
166
|
+
This is an L2 signal to be cross-checked with oracle/L1 evidence — not a
|
|
167
|
+
substitute for it.
|
|
168
|
+
|
|
169
|
+
### `x402_pay`
|
|
170
|
+
|
|
171
|
+
Arguments: `url` (https; http only for localhost), optional `method`, `headers`,
|
|
172
|
+
`body`, `maxAmountUsdc`, `maxRatePerSecondUsdc` (base units; these can only TIGHTEN
|
|
173
|
+
the server mandate) and `minDurationSeconds`. Pays through `@absol-labs/sdk/x402`'s
|
|
174
|
+
`payWithX402` with every SDK protection intact, and is gated exactly like
|
|
175
|
+
`hire_verified_service`: the same server mandate, the same cumulative spend, the
|
|
176
|
+
same per-owner serialization. The payment's opener terms are checked by
|
|
177
|
+
`checkMandate()` before anything is signed — `amount` (the deposit) against the
|
|
178
|
+
per-stream and total caps, `ratePerSecond`, `maxDuration` and `operator` against
|
|
179
|
+
theirs — and the stream it opens is recorded, so it counts against `maxTotalUsdc`.
|
|
180
|
+
A caller-supplied `PAYMENT-SIGNATURE` / `X-PAYMENT` header is refused. The result
|
|
181
|
+
carries the response (bounded), `streamId`, `opener`, `refundUrl`, the deposit and
|
|
182
|
+
rate, and `refundIsAutomatic: false`.
|
|
183
|
+
|
|
184
|
+
### `refund_status` / `refund`
|
|
185
|
+
|
|
186
|
+
Arguments: `streamId`, optional `refundUrl` (not needed for a stream this server
|
|
187
|
+
paid for — it was recorded); `refund` also takes `close` (stop the stream first;
|
|
188
|
+
this wallet signs the opener's `Close`, authorized on the mandate owner's signature
|
|
189
|
+
alone like `reclaim_unspent`) and `reclaim` (default `true`; the permissionless
|
|
190
|
+
reclaim, the refund always goes to the payer). Every relay success is verified
|
|
191
|
+
against the transaction receipt. A settlement window that is still open is reported
|
|
192
|
+
with when to retry. Nothing is refunded automatically.
|
|
193
|
+
|
|
194
|
+
The three x402 tools are **opt-in**: set `METRIK_AGENT_X402=1` to register them
|
|
195
|
+
(any other value, or none, leaves them off).
|
|
196
|
+
|
|
197
|
+
**Spend is reserved before anything is signed.** When the mandate approves a
|
|
198
|
+
quote, `x402_pay` records a reservation for its amount in the stream registry
|
|
199
|
+
— inside the same per-owner queue as `hire_verified_service` — before the SDK
|
|
200
|
+
signs the EIP-3009 authorization. If anything fails after the signature has
|
|
201
|
+
been handed out (a malformed or missing `PAYMENT-RESPONSE`, an unconfirmed
|
|
202
|
+
settlement, a dropped connection), the outcome is established from the chain,
|
|
203
|
+
never from the seller: USDC `authorizationState(from, nonce)` and the opener's
|
|
204
|
+
`StreamOpenedFor(streamId, payer, authorizationNonce, …)` log. The reservation
|
|
205
|
+
is released only if the authorization was never used AND has expired; a stream
|
|
206
|
+
found on-chain replaces it under its real `streamId`; otherwise it stays
|
|
207
|
+
counted, and the tool says the money may have moved — never "not settled".
|
|
208
|
+
|
|
209
|
+
**Outbound requests are guarded.** The URLs the payment tool, `refund` and
|
|
210
|
+
`refund_status` contact come from the model, so every request must be `https:`
|
|
211
|
+
to a host whose every resolved address is public (loopback, private,
|
|
212
|
+
link-local, unique-local, CGNAT, multicast and reserved ranges are refused),
|
|
213
|
+
redirects are never followed, and `METRIK_AGENT_X402_ALLOWED_HOSTS`
|
|
214
|
+
(comma-separated, exact or `*.domain`) restricts it further — list the seller
|
|
215
|
+
AND its refund relay. A response that did not ask for payment is not returned
|
|
216
|
+
(status only). The address is checked at lookup time; use the allowlist where
|
|
217
|
+
DNS rebinding matters.
|
|
218
|
+
|
|
219
|
+
## Configuration
|
|
220
|
+
|
|
221
|
+
`metrik mcp --print-config` prints the `mcpServers` block to paste into an MCP
|
|
222
|
+
client, already carrying every variable the server fails closed without. Secrets
|
|
223
|
+
in it are placeholders by design.
|
|
224
|
+
|
|
225
|
+
| Variable | Required | Meaning |
|
|
226
|
+
| ---------------------------------------- | -------- | ----------------------------------------------------------------------------------------------- |
|
|
227
|
+
| `METRIK_AGENT_RPC_URL` | **yes** | RPC of the selected network. |
|
|
228
|
+
| `METRIK_AGENT_ESCROW` | **yes** | The network's registered **StreamEscrowV2**. Anything else is refused (see below). |
|
|
229
|
+
| `METRIK_AGENT_USDC` | **yes** | Settlement token. |
|
|
230
|
+
| `METRIK_AGENT_ESCROW_GENERATION` | no | `v2` (default, and the only supported value). Declared, not probed. |
|
|
231
|
+
| `METRIK_NETWORK` | no | Registry id, chain id or CAIP-2 id; default the registry's default network. Testnets only. |
|
|
232
|
+
| `METRIK_AGENT_CHAIN_ID` | no | Alternative selector; must agree with `METRIK_NETWORK` when both are set. |
|
|
233
|
+
| `METRIK_AGENT_ALLOW_UNREGISTERED_ESCROW` | no | `true` accepts an escrow the registry does not record (a deliberate private deployment). |
|
|
234
|
+
| `METRIK_AGENT_X402` | no | `1` registers `x402_pay`, `refund_status`, `refund`. **Off by default.** |
|
|
235
|
+
| `METRIK_AGENT_X402_ALLOWED_HOSTS` | no | Comma-separated hosts (`*.domain` allowed) the x402 tools may contact. Unset = any public host. |
|
|
236
|
+
| `METRIK_AGENT_REGISTRY_URL` | no | Registry the invocation origin is verified against. |
|
|
237
|
+
| `METRIK_AGENT_ALLOW_UNVERIFIED_OPERATOR` | no | `true` downgrades an unverifiable operator pairing from a refusal to a warning. Off by default. |
|
|
238
|
+
| `METRIK_AGENT_MCP_STATE_FILE` | no | Persists the stream registry across restarts. |
|
|
239
|
+
| `METRIK_AGENT_MANDATE_*` | **yes** | The four spend ceilings. No ceiling, no server. |
|
|
240
|
+
| wallet vars | **yes** | `METRIK_AGENT_PRIVATE_KEY`, or the CDP/Privy set. |
|
|
241
|
+
|
|
242
|
+
The escrow generation is a **declared fact, not an inference**: the server
|
|
243
|
+
refuses any escrow that is not the network's registered `StreamEscrowV2` (the
|
|
244
|
+
retired V1 escrow included) unless `METRIK_AGENT_ALLOW_UNREGISTERED_ESCROW=true`, because a wrong-generation read does not
|
|
245
|
+
revert — it returns confident nonsense about someone's money.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# `ProbeCapability` — letting the verifier check a gated paid route
|
|
2
|
+
|
|
3
|
+
> **What this changes for you as a seller:** if your listing is `gated`, the
|
|
4
|
+
> Metrik oracle currently cannot call your paid route at all. It can only reach
|
|
5
|
+
> your health route. Adopting `ProbeCapability` is what turns "the health
|
|
6
|
+
> endpoint answered" into "the paid route delivered". It is **opt-in**, and a
|
|
7
|
+
> gateway that does not adopt it is reported by the oracle as **unverifiable**
|
|
8
|
+
> for the paid route — never as passing.
|
|
9
|
+
|
|
10
|
+
## The problem
|
|
11
|
+
|
|
12
|
+
A gated listing's paid route requires an `InvocationCapability` (see
|
|
13
|
+
[`../src/capability/invocation-capability.ts`](../src/capability/invocation-capability.ts))
|
|
14
|
+
signed by **the stream's buyer**: 60s TTL, single-use nonce, bound to one method
|
|
15
|
+
and one path. The gateway recovers that signature and checks it against
|
|
16
|
+
`stream.buyer` on-chain.
|
|
17
|
+
|
|
18
|
+
The oracle is not a buyer. It holds no buyer key and has no stream. So for a
|
|
19
|
+
gated listing there is no credential a regional probe worker could safely
|
|
20
|
+
present, and the paid route goes unprobed by anybody — not remotely, and not
|
|
21
|
+
even by the coordinator.
|
|
22
|
+
|
|
23
|
+
That is a real gap in a claim the product makes. "The seller earns only for
|
|
24
|
+
intervals that verified" must not quietly mean "the health endpoint answered".
|
|
25
|
+
|
|
26
|
+
## The mechanism
|
|
27
|
+
|
|
28
|
+
A second EIP-712 type, `ProbeCapability`, signed by an **oracle signer**:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
ProbeCapability {
|
|
32
|
+
bytes32 serviceRef; // the service being probed
|
|
33
|
+
address oracle; // the minting signer; must equal the recovered signer
|
|
34
|
+
string method; // exactly one HTTP method
|
|
35
|
+
bytes32 pathHash; // keccak256 of exactly one request path
|
|
36
|
+
bytes32 nonce; // single-use
|
|
37
|
+
uint64 expiry; // ~30s
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
EIP-712 domain: `name = "Metrik Probe Capability"`, `version = "1"`, the
|
|
42
|
+
gateway's `chainId`, and `verifyingContract` = the `StreamEscrowV2` the gateway
|
|
43
|
+
already checks streams against. The domain name is deliberately **different**
|
|
44
|
+
from `"Metrik Invocation Capability"`, so a buyer's invocation signature can
|
|
45
|
+
never be replayed as a free probe and vice versa.
|
|
46
|
+
|
|
47
|
+
Transport: base64url in the `x-metrik-probe-capability` request header.
|
|
48
|
+
|
|
49
|
+
### Authority is public, not a shared secret
|
|
50
|
+
|
|
51
|
+
The gateway authorizes the probe iff the recovered signer is in
|
|
52
|
+
`StreamEscrowV2.getOracleSigners()` — a **public `view`** on the escrow. On the
|
|
53
|
+
live Base Sepolia deployment (`0x0f09f36Ccc05A7c9882F438721C08De314dFd46C`) that
|
|
54
|
+
returns three addresses today.
|
|
55
|
+
|
|
56
|
+
This is the whole reason the design beats "give the workers a credential":
|
|
57
|
+
|
|
58
|
+
- **Nothing to distribute.** No API key, no registration, no onboarding call.
|
|
59
|
+
A third-party seller performs the identical on-chain read and reaches the
|
|
60
|
+
identical verdict, with no trust in the oracle's say-so.
|
|
61
|
+
- **Nothing to leak.** The only secret is the oracle coordinator's signing key,
|
|
62
|
+
which never leaves it.
|
|
63
|
+
- **Revocation is already solved.** Remove a signer on the escrow and it stops
|
|
64
|
+
being able to probe within one signer-cache TTL (60s by default).
|
|
65
|
+
|
|
66
|
+
### No signing key leaves the coordinator
|
|
67
|
+
|
|
68
|
+
The oracle coordinator mints **one fresh capability per probe, per vantage** and
|
|
69
|
+
hands the encoded header to the worker alongside the nonce. Workers hold a
|
|
70
|
+
bearer token for the coordinator and nothing else. Compromising every regional
|
|
71
|
+
worker yields no ability to mint a capability, and no ability to probe anything
|
|
72
|
+
the coordinator did not already ask for — the capability is bound to one
|
|
73
|
+
service, one method, one path, one nonce and ~30 seconds.
|
|
74
|
+
|
|
75
|
+
One capability **per vantage**, not one per probe, because the nonce is
|
|
76
|
+
single-use: two vantages sharing a capability would see the second rejected as a
|
|
77
|
+
replay.
|
|
78
|
+
|
|
79
|
+
### A probe never consumes a buyer's budget
|
|
80
|
+
|
|
81
|
+
Three independent reasons, all structural rather than conventional:
|
|
82
|
+
|
|
83
|
+
1. A `ProbeCapability` carries **no `streamId` and no `buyer`**. There is no
|
|
84
|
+
stream identified for it to be billed against.
|
|
85
|
+
2. Accrual is only ever produced by an **oracle checkpoint keyed to a stream**.
|
|
86
|
+
A gateway call produces none, ever — the gateway is an authorizer and proxy,
|
|
87
|
+
not a meter.
|
|
88
|
+
3. In the reference gateway the probe branch is evaluated **first and returns
|
|
89
|
+
unconditionally**, so it can never fall through into the stream branch. An
|
|
90
|
+
authorized probe decision carries only the probe — no `stream`, no
|
|
91
|
+
capability, nothing billable. A test asserts the stream reader is never even
|
|
92
|
+
called.
|
|
93
|
+
|
|
94
|
+
The gateway additionally sets `x-metrik-probe: 1` on the **upstream** request so
|
|
95
|
+
your own metering and analytics can exclude verification traffic. It is
|
|
96
|
+
gateway-asserted: any inbound copy from a client is stripped first, so a paying
|
|
97
|
+
caller cannot mark its own billable request as a probe.
|
|
98
|
+
|
|
99
|
+
### Rate limiting: why 60/minute
|
|
100
|
+
|
|
101
|
+
Without a bound, an oracle-signed capability is a licence to consume your
|
|
102
|
+
service indefinitely for free.
|
|
103
|
+
|
|
104
|
+
The oracle checkpoints every ~15–22 seconds, so **one** vantage probes a given
|
|
105
|
+
service at most ~4 times a minute. T1 multi-vantage fans that out across at most
|
|
106
|
+
9 vantages (the coordinator plus the oracle's 8-worker cap), giving a worst-case
|
|
107
|
+
honest load of **~36 probes/minute**. The default budget is **60 per minute**:
|
|
108
|
+
comfortable headroom for a re-execution sample and clock jitter, while capping
|
|
109
|
+
the free-service surface at one request per second — far below anything
|
|
110
|
+
economically interesting as a way to get the service for nothing.
|
|
111
|
+
|
|
112
|
+
Tune it with `METRIK_GATEWAY_PROBE_MAX_PER_MINUTE`. A useful side effect: since
|
|
113
|
+
every capability is single-use and ~30s-lived, the rate limit is also what
|
|
114
|
+
bounds the replay cache — at most `maxPerMinute × ttl/60` probe nonces live at
|
|
115
|
+
once.
|
|
116
|
+
|
|
117
|
+
### Replay protection reuses the existing cache
|
|
118
|
+
|
|
119
|
+
The probe path calls the **same** `NonceReplayCache` the buyer path uses, so a
|
|
120
|
+
nonce can never be spent once as a probe and once as an invocation. The nonce is
|
|
121
|
+
consumed _before_ the rate-limit check, so a captured capability replayed at
|
|
122
|
+
volume is rejected as a replay rather than eating the honest probe budget, and
|
|
123
|
+
_before_ the on-chain signer read, so a flood cannot amplify into RPC load.
|
|
124
|
+
|
|
125
|
+
## Enabling it
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
METRIK_GATEWAY_PROBE_CAPABILITY=true # off by default
|
|
129
|
+
METRIK_GATEWAY_PROBE_MAX_PER_MINUTE=60 # optional
|
|
130
|
+
METRIK_GATEWAY_PROBE_SIGNER_CACHE_SECONDS=60 # optional
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
There is **no credential to configure**. The signer set is read from the
|
|
134
|
+
`METRIK_GATEWAY_RPC_URL` and escrow the gateway already uses.
|
|
135
|
+
|
|
136
|
+
Not using the reference gateway? Implement the verification in this exact order
|
|
137
|
+
— cheapest and most local first, so an unsigned or replayed flood cannot amplify
|
|
138
|
+
into RPC load:
|
|
139
|
+
|
|
140
|
+
1. Decode the `x-metrik-probe-capability` header; malformed → `403`.
|
|
141
|
+
2. `now >= expiry` → `403`.
|
|
142
|
+
3. Recover the EIP-712 signer; it must equal `capability.oracle` → else `403`.
|
|
143
|
+
4. `capability.method` must equal the request method → else `403`.
|
|
144
|
+
5. `keccak256(path)` must equal `capability.pathHash` (path only — no query, no
|
|
145
|
+
fragment, no trailing slash except root) → else `403`.
|
|
146
|
+
6. `capability.serviceRef` must be one you front → else `403`.
|
|
147
|
+
7. Consume `capability.nonce` single-use → replay is `403`.
|
|
148
|
+
8. Rate-limit → `429`.
|
|
149
|
+
9. `capability.oracle` must be in `getOracleSigners()` → else `403`. If you
|
|
150
|
+
cannot read the set, **deny** (`403`); do not serve on a stale set.
|
|
151
|
+
|
|
152
|
+
Then proxy upstream and **do not meter, bill or settle the call**.
|
|
153
|
+
|
|
154
|
+
## Failure modes, and what each one means to the oracle
|
|
155
|
+
|
|
156
|
+
Every denial is a distinct machine-readable `reason` in the JSON body:
|
|
157
|
+
|
|
158
|
+
| Reason | Status | What it means |
|
|
159
|
+
| ----------------------------------------------- | ------ | -------------------------------------------------------------------------------------------------------- |
|
|
160
|
+
| `probe-not-enabled` | 403 | This gateway has not adopted `ProbeCapability`. The oracle reports the paid route **unverifiable**. |
|
|
161
|
+
| `malformed-probe-capability` | 403 | Header was not a decodable signed capability. |
|
|
162
|
+
| `probe-capability-expired` | 403 | Past `expiry`. |
|
|
163
|
+
| `probe-invalid-signature` | 403 | Signature does not recover to the claimed `oracle`. |
|
|
164
|
+
| `probe-method-mismatch` / `probe-path-mismatch` | 403 | Capability was not minted for this route. |
|
|
165
|
+
| `probe-service-ref-mismatch` | 403 | Minted for another service. |
|
|
166
|
+
| `probe-nonce-replayed` | 403 | Nonce already spent. |
|
|
167
|
+
| `probe-rate-limited` | 429 | Free-probe budget for this window is spent. |
|
|
168
|
+
| `probe-signer-not-authorized` | 403 | Signer is not in `getOracleSigners()`. |
|
|
169
|
+
| `probe-signer-set-unavailable` | 403 | The set could not be read fresh enough. Fail closed. |
|
|
170
|
+
| `ambiguous-probe-mode` | 400 | Request carried a probe capability _and_ a buyer capability or `X-PAYMENT`. Refused rather than guessed. |
|
|
171
|
+
|
|
172
|
+
## Honest limitations
|
|
173
|
+
|
|
174
|
+
- **This is an adoption ask.** We ship the reference implementation because we
|
|
175
|
+
run the reference gateway, but a third-party seller has to adopt it. Until
|
|
176
|
+
they do, their gated paid route stays health-route-only and the oracle says so
|
|
177
|
+
out loud rather than implying coverage it does not have.
|
|
178
|
+
- **A gated seller can stay unverifiable on purpose.** A hostile gateway can
|
|
179
|
+
simply answer `403` to every probe and remain in the "unverifiable" bucket.
|
|
180
|
+
That gains it nothing it does not already have today — under the oracle's
|
|
181
|
+
`strict` enforcement an unverifiable paid route accrues nothing, and under
|
|
182
|
+
`eligible` it is exactly today's health-route-only behaviour, counted and
|
|
183
|
+
surfaced. What it can never do is be counted as _passing_.
|
|
184
|
+
- **Delivery, not correctness.** A passing probe is evidence the paid route
|
|
185
|
+
produced a fresh, spec-conforming response for _this_ challenge. It is not a
|
|
186
|
+
proof the output is correct. See
|
|
187
|
+
[`../../streamproof-protocol/docs/verification-model.md`](https://github.com/Absol-Labs/metrik-protocol/blob/main/docs/verification-model.md).
|
|
188
|
+
- **The signer set is federated today.** All three addresses in
|
|
189
|
+
`getOracleSigners()` are keys held by one process, and one of them operates
|
|
190
|
+
the live listings. `ProbeCapability` inherits that posture exactly — it makes
|
|
191
|
+
the _paid route_ checkable, not the verifier independent.
|
package/docs/quickstart.md
CHANGED
|
@@ -16,9 +16,15 @@ Base Sepolia constants used throughout (chainId **84532**):
|
|
|
16
16
|
|
|
17
17
|
| Thing | Address |
|
|
18
18
|
| ------ | -------------------------------------------- |
|
|
19
|
-
| Escrow | `
|
|
19
|
+
| Escrow | `0x0f09f36Ccc05A7c9882F438721C08De314dFd46C` |
|
|
20
20
|
| USDC | `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
|
|
21
21
|
|
|
22
|
+
The escrow above is **StreamEscrowV2**, the checkpoint-settled contract the
|
|
23
|
+
oracle and the dApp actually settle against. The retired V1 metered escrow
|
|
24
|
+
(`0x21948a5E…`) is not a valid target: its stream tuple diverges from V2's at
|
|
25
|
+
index 5, so a V2 read of it does not revert — it decodes other fields into
|
|
26
|
+
correctly-named ones and reports a confident wrong answer.
|
|
27
|
+
|
|
22
28
|
A complete, runnable version of this journey lives in
|
|
23
29
|
[`scripts/e2e-cdp.ts`](../scripts/e2e-cdp.ts) (the live CI E2E, metrik-agent#32) — treat
|
|
24
30
|
it as the working reference.
|
|
@@ -209,7 +215,10 @@ const sdkConfig: StreamProofClientConfig = {
|
|
|
209
215
|
chain: baseSepolia,
|
|
210
216
|
transport: http("https://sepolia.base.org"),
|
|
211
217
|
account,
|
|
212
|
-
escrow: "
|
|
218
|
+
escrow: "0x0f09f36Ccc05A7c9882F438721C08De314dFd46C",
|
|
219
|
+
// Selects the checkpoint decoder. Without it the SDK reads through the legacy
|
|
220
|
+
// V1 ABI, and `claimable()`/`reclaimable()` revert against StreamEscrowV2.
|
|
221
|
+
escrowV2: "0x0f09f36Ccc05A7c9882F438721C08De314dFd46C",
|
|
213
222
|
usdc: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
|
|
214
223
|
};
|
|
215
224
|
|
|
@@ -284,6 +293,40 @@ const { reclaimResult } = await agent.reclaimStream({
|
|
|
284
293
|
escape-window rules, and `claimed + fees + reclaimed == deposit` conserves the escrowed
|
|
285
294
|
funds exactly.
|
|
286
295
|
|
|
296
|
+
### 7b. Or pay a seller over x402 (no gas needed)
|
|
297
|
+
|
|
298
|
+
A Metrik x402 seller (an HTTP `402` with an x402 v2 `exact` quote) can be paid in one
|
|
299
|
+
call. The payment opens the same kind of stream — `deposit = price`, earned per verified
|
|
300
|
+
second — and the same spend mandate bounds it before anything is signed:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
import { defaultNetwork } from "@absol-labs/agent";
|
|
304
|
+
import { payX402WithMandate } from "@absol-labs/agent/x402-buyer";
|
|
305
|
+
|
|
306
|
+
const paid = await payX402WithMandate({
|
|
307
|
+
url: "https://x402.137.23.50.249.sslip.io/transform",
|
|
308
|
+
init: {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers: { "content-type": "application/json" },
|
|
311
|
+
body: '{"text":"# hi"}',
|
|
312
|
+
},
|
|
313
|
+
signer: account, // USDC only, no ETH
|
|
314
|
+
network: defaultNetwork(),
|
|
315
|
+
signedMandate,
|
|
316
|
+
spentSoFarUsdc: 0n,
|
|
317
|
+
nowSeconds: Math.floor(Date.now() / 1000),
|
|
318
|
+
});
|
|
319
|
+
console.log(paid.metrikStreamOpener); // streamId, opener, refundUrl, deposit, rate
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Getting the unearned part back is not automatic: close, then reclaim, through the seller's
|
|
323
|
+
gasless relay (the `metrik refund` command below, or the SDK's `refundClient`).
|
|
324
|
+
|
|
325
|
+
Full details, including the mandate mapping: [the buyer section](./x402.md).
|
|
326
|
+
|
|
327
|
+
From a terminal: `metrik pay <url> --json '{"text":"# hi"}' --max-amount 0.1`, then
|
|
328
|
+
`metrik refund <streamId> --refund-url <url> --close --reclaim`.
|
|
329
|
+
|
|
287
330
|
---
|
|
288
331
|
|
|
289
332
|
## 8. Five ways to integrate
|
|
@@ -336,8 +379,13 @@ pnpm mcp:stdio # same server, from inside this repository (src/mcp/stdio.ts)
|
|
|
336
379
|
pnpm mcp:http # hosted: multi-tenant Streamable HTTP, per-caller bearer auth
|
|
337
380
|
```
|
|
338
381
|
|
|
339
|
-
Tools: `discover_services`, `hire_verified_service`, `
|
|
340
|
-
`list_streams` (+ `prove_https_response` when
|
|
382
|
+
Tools: `discover_services`, `hire_verified_service`, `invoke_service`,
|
|
383
|
+
`check_stream_status`, `reclaim_unspent`, `list_streams` (+ `prove_https_response` when
|
|
384
|
+
Reclaim creds are set) — full reference in [`docs/mcp-reference.md`](./mcp-reference.md).
|
|
385
|
+
`invoke_service` is the "use" half: it signs a stream-scoped capability and calls the
|
|
386
|
+
operator-signed origin, so an agent never needs an out-of-band credential. Note the meter
|
|
387
|
+
runs on verified SECONDS, not on calls — invoking more often is free, and invoking never
|
|
388
|
+
costs the same. Env is read via
|
|
341
389
|
`parseMetrikAgentEnv` (`METRIK_AGENT_RPC_URL` / `_ESCROW` / `_USDC` + wallet vars). The
|
|
342
390
|
hosted endpoint requires `Authorization: Bearer <token>` before anything runs — see
|
|
343
391
|
[`docs/mcp-hosted.md`](./mcp-hosted.md).
|
|
@@ -380,4 +428,5 @@ Metrik proves **verified delivery** (responded, correctly-shaped, within SLA to
|
|
|
380
428
|
traffic) — **not output correctness** (that the model's answer is right). Pitch and treat
|
|
381
429
|
it accordingly. The full journey, run live against the real deployed escrow on Base
|
|
382
430
|
Sepolia, is [`scripts/e2e-cdp.ts`](../scripts/e2e-cdp.ts) (`pnpm build:e2e && pnpm
|
|
383
|
-
e2e:cdp`) — the working reference for everything above
|
|
431
|
+
e2e:cdp`) — the working reference for everything above; its x402 leg is
|
|
432
|
+
[`scripts/e2e-x402.ts`](../scripts/e2e-x402.ts) (`pnpm e2e:x402`).
|
package/docs/threat-model.md
CHANGED
|
@@ -20,7 +20,11 @@ In scope:
|
|
|
20
20
|
|
|
21
21
|
- signed spend mandates in [`src/mandates/mandate.ts`](../src/mandates/mandate.ts)
|
|
22
22
|
- the mandate-gated SDK wrapper in [`src/sdk/client.ts`](../src/sdk/client.ts)
|
|
23
|
-
-
|
|
23
|
+
- Metrik `402` verified-stream opens in [`src/x402/facilitator.ts`](../src/x402/facilitator.ts)
|
|
24
|
+
(a Metrik-native challenge flow, not the x402 protocol — see [docs/x402.md](./x402.md))
|
|
25
|
+
- the x402 v2 opener facilitator in [`src/x402-opener/`](../src/x402-opener/) and the
|
|
26
|
+
gateway's x402 v2 mode in [`src/gateway/x402-opener-mode.ts`](../src/gateway/x402-opener-mode.ts)
|
|
27
|
+
(both off by default — see [docs/x402.md](./x402.md))
|
|
24
28
|
- MCP fund-moving tools in [`src/mcp/server.ts`](../src/mcp/server.ts)
|
|
25
29
|
- zkTLS consumer delivery proofs in [`src/zktls/reclaim.ts`](../src/zktls/reclaim.ts)
|
|
26
30
|
- wallet custody in [`src/wallet/provider.ts`](../src/wallet/provider.ts)
|
|
@@ -38,10 +42,27 @@ Out of scope:
|
|
|
38
42
|
| Runaway spend | A prompt or tool call opens streams beyond the owner's intent. | Every autonomous fund-moving path calls `checkMandate()` before the SDK can write: `openVerifiedStream()`, `claimStream()`, `closeStream()`, `reclaimStream()`, MCP `hire_verified_service`, and MCP `reclaim_unspent`. Mandates cap per-stream, cumulative spend, rate, duration, and operator. Denial is a hard stop. | [`test/mandate.test.ts`](../test/mandate.test.ts), [`test/sdk-client.test.ts`](../test/sdk-client.test.ts), [`test/mcp.test.ts`](../test/mcp.test.ts), [`test/x402.int.test.ts`](../test/x402.int.test.ts) | `spentSoFarUsdc` and revocation data are resolver inputs. If an integrator provides stale values, the agent still fails closed on signature/expiry/operator checks, but cumulative-cap freshness depends on the integrator's state source. |
|
|
39
43
|
| Mandate bypass | A caller skips the mandate layer and reaches the SDK write path directly from the agent layer. | The agent wrapper no longer exposes ungated autonomous write helpers. Existing-stream actions re-read the stream and authorize against the actual operator before `claim`, `close`, or `reclaim`. MCP also prechecks before dispatch. | [`src/sdk/client.ts`](../src/sdk/client.ts), [`test/sdk-client.test.ts`](../test/sdk-client.test.ts), [`test/mcp.test.ts`](../test/mcp.test.ts) | A caller can still use `@absol-labs/sdk` directly outside this repo. That is expected; this document only claims the agent layer itself is mandate-gated. |
|
|
40
44
|
| Key exposure | The agent leaks a private key through logs, env parsing, tool args, or local persistence. | No MCP tool accepts a raw key. Wallet selection is explicit: injected viem account, CDP Server Wallet v2, or an authenticated Privy embedded EOA provider. CDP/Privy paths remote-sign and never export private keys into Metrik code. `.env.example` contains names only. | [`src/wallet/provider.ts`](../src/wallet/provider.ts), [`test/wallet.test.ts`](../test/wallet.test.ts), [`test/privy-wallet.test.ts`](../test/privy-wallet.test.ts), [`README.md`](../README.md) | Injected-key mode is still as safe as the caller's own host. Privy authentication/session revocation remains the host/provider's responsibility. |
|
|
41
|
-
| Prompt-injection-driven spend | A hostile response or tool output tries to coerce the agent into paying a different operator or budget. | Structured inputs are validated,
|
|
45
|
+
| Prompt-injection-driven spend | A hostile response or tool output tries to coerce the agent into paying a different operator or budget. | Structured inputs are validated, challenge settlement targets must match the configured chain/escrow, and any spend still must satisfy the owner's signed mandate. | [`src/x402/facilitator.ts`](../src/x402/facilitator.ts), [`test/x402.test.ts`](../test/x402.test.ts), [`test/sdk-client.test.ts`](../test/sdk-client.test.ts) | Prompt injection can still trigger tool invocation attempts. The safety claim is not "the prompt cannot be attacked"; it is "the attack cannot exceed the mandate or redirect settlement silently." |
|
|
42
46
|
| Mandate replay | A previously valid signed mandate is replayed after revocation or outside the intended lifetime. | Mandates are EIP-712 signed over `mandateId`, `owner`, `chainId`, `issuedAt`, expiry, and policy hashes. `checkMandate()` verifies signature, revocation, expiry, then policy. Integrators can revoke by `mandateId` and track cumulative spend. | [`src/mandates/mandate.ts`](../src/mandates/mandate.ts), [`test/mandate.test.ts`](../test/mandate.test.ts), [`test/mcp.test.ts`](../test/mcp.test.ts) | Replay protection is only as strong as revocation and cumulative-spend state freshness. The signed payload itself is bound to one chain and one owner, but revocation is local state today. |
|
|
43
47
|
| False or weak delivery evidence | A consumer submits a proof that is validly signed by the zkTLS system but not bound to the intended request or stream context. | The Reclaim adapter re-verifies every proof locally against the exact URL/method/body/match/redaction spec supplied by the caller, and binds the proof context to buyer/stream metadata before returning it. MCP only exposes the public-request subset so secrets do not leak through tool arguments. | [`src/zktls/reclaim.ts`](../src/zktls/reclaim.ts), [`src/x402/facilitator.ts`](../src/x402/facilitator.ts), [`src/mcp/server.ts`](../src/mcp/server.ts), [`test/reclaim.test.ts`](../test/reclaim.test.ts), [`test/x402.test.ts`](../test/x402.test.ts), [`test/mcp.test.ts`](../test/mcp.test.ts) | L2 is still consumer-supplied evidence. A malicious consumer can withhold proofs, and settlement must still treat L1/oracle evidence as authoritative on failure, ambiguity, and expiry. |
|
|
44
48
|
|
|
49
|
+
## x402 v2 opener threats
|
|
50
|
+
|
|
51
|
+
The opener path moves a buyer's USDC only under that buyer's own EIP-3009
|
|
52
|
+
signature, so no agent-held key spends and no spend mandate applies; the
|
|
53
|
+
threats are about where the signed payment can end up.
|
|
54
|
+
|
|
55
|
+
| Threat | Mitigation | Code / tests |
|
|
56
|
+
| ------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
57
|
+
| A stock x402 facilitator (or anyone holding the signed payload) submits `transferWithAuthorization` to the opener directly, stranding the payment | Only `MetrikOpenerExactScheme` settles, via `deployAndSettle`; its stock verifier gets a signer that cannot broadcast. The gateway refuses to start, and fails closed (503) later, unless the facilitator's `/supported` marker names its exact factory/escrow/token/chain. `PAYMENT-SIGNATURE` is never forwarded upstream. No discovery extension is ever declared. | `src/x402-opener/scheme.ts`, `src/gateway/x402-opener-mode.ts`; `test/x402-opener-gateway.test.ts`, `test/x402-opener.fork.int.test.ts` |
|
|
58
|
+
| A forged or tampered `payTo`, terms or listing redirects funds | `payTo` must equal `factory.predict(terms)` read on-chain (never code/codehash); `(operator, serviceRef)` must resolve to exactly one operator-signed listing via discovery signature verification; amount bounded by `rate × maxDuration` | `test/x402-opener-scheme.test.ts`, `test/x402-opener-config.test.ts` |
|
|
59
|
+
| Success reported without a stream | `settle` returns `success: true` only from a mined receipt carrying the escrow `StreamOpened` (buyer = opener) and the opener `StreamOpenedFor` for this payer/nonce/amount | `test/x402-opener-scheme.test.ts` |
|
|
60
|
+
| One payment buys several served responses (a replayed `PAYMENT-SIGNATURE`) | At most one successful settle per authorization (chain + asset + payer + nonce) per facilitator instance; the gateway refuses an in-flight or settled authorization with `409` before the upstream runs. In-memory, per instance: run one instance | `test/x402-opener-scheme.test.ts`, `test/x402-opener-gateway.test.ts`, `test/x402-opener-regressions.fork.int.test.ts` |
|
|
61
|
+
| Concurrent payers fail on the relayer nonce after being served | Relayer submissions are serialized | same |
|
|
62
|
+
| Dust payments drain relayer gas; unauthenticated `/verify` amplifies RPC load | Minimum amount (default 0.01 USDC, advertised; the gateway refuses a lower price), `/settle` rate limit per token, `/verify` requires the token by default | `test/x402-opener-config.test.ts`, fork regressions |
|
|
63
|
+
| A failed upstream still charges the buyer | The gateway settles only when the upstream status is `< 400`, and a failed settle sends no body | `test/x402-opener-gateway.test.ts`, fork E2E |
|
|
64
|
+
| Relayer key or facilitator token leaks | Env only, never logged or echoed in errors; `/settle` requires the bearer token (constant-time compared) | `src/x402-opener/facilitator-server.ts`; `test/x402-opener-config.test.ts` |
|
|
65
|
+
|
|
45
66
|
## Fund-Moving Paths
|
|
46
67
|
|
|
47
68
|
These are the autonomous fund-moving paths in the current agent layer:
|
|
@@ -52,7 +73,7 @@ These are the autonomous fund-moving paths in the current agent layer:
|
|
|
52
73
|
4. `VerifiedStreamAgentClient.reclaimStream()`
|
|
53
74
|
5. MCP `hire_verified_service`
|
|
54
75
|
6. MCP `reclaim_unspent`
|
|
55
|
-
7.
|
|
76
|
+
7. Metrik `402` `open()` via `VerifiedStreamFacilitator`
|
|
56
77
|
|
|
57
78
|
All of them now pass a signed mandate check before the SDK can send a write.
|
|
58
79
|
|