@absol-labs/agent 0.9.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/README.md +184 -26
  2. package/dist/capability/invocation-capability.d.ts +18 -6
  3. package/dist/capability/invocation-capability.d.ts.map +1 -1
  4. package/dist/capability/invocation-capability.js.map +1 -1
  5. package/dist/cli/amounts.d.ts +26 -0
  6. package/dist/cli/amounts.d.ts.map +1 -0
  7. package/dist/cli/amounts.js +71 -0
  8. package/dist/cli/amounts.js.map +1 -0
  9. package/dist/cli/args.d.ts +32 -0
  10. package/dist/cli/args.d.ts.map +1 -0
  11. package/dist/cli/args.js +119 -0
  12. package/dist/cli/args.js.map +1 -0
  13. package/dist/cli/attesters.d.ts +51 -0
  14. package/dist/cli/attesters.d.ts.map +1 -0
  15. package/dist/cli/attesters.js +58 -0
  16. package/dist/cli/attesters.js.map +1 -0
  17. package/dist/cli/bin.d.ts +3 -0
  18. package/dist/cli/bin.d.ts.map +1 -0
  19. package/dist/cli/bin.js +5 -0
  20. package/dist/cli/bin.js.map +1 -0
  21. package/dist/cli/commands/claim.d.ts +13 -0
  22. package/dist/cli/commands/claim.d.ts.map +1 -0
  23. package/dist/cli/commands/claim.js +102 -0
  24. package/dist/cli/commands/claim.js.map +1 -0
  25. package/dist/cli/commands/close.d.ts +3 -0
  26. package/dist/cli/commands/close.d.ts.map +1 -0
  27. package/dist/cli/commands/close.js +55 -0
  28. package/dist/cli/commands/close.js.map +1 -0
  29. package/dist/cli/commands/discover.d.ts +3 -0
  30. package/dist/cli/commands/discover.d.ts.map +1 -0
  31. package/dist/cli/commands/discover.js +183 -0
  32. package/dist/cli/commands/discover.js.map +1 -0
  33. package/dist/cli/commands/invoke.d.ts +3 -0
  34. package/dist/cli/commands/invoke.d.ts.map +1 -0
  35. package/dist/cli/commands/invoke.js +142 -0
  36. package/dist/cli/commands/invoke.js.map +1 -0
  37. package/dist/cli/commands/mcp.d.ts +3 -0
  38. package/dist/cli/commands/mcp.d.ts.map +1 -0
  39. package/dist/cli/commands/mcp.js +113 -0
  40. package/dist/cli/commands/mcp.js.map +1 -0
  41. package/dist/cli/commands/open.d.ts +3 -0
  42. package/dist/cli/commands/open.d.ts.map +1 -0
  43. package/dist/cli/commands/open.js +331 -0
  44. package/dist/cli/commands/open.js.map +1 -0
  45. package/dist/cli/commands/reclaim-unverified.d.ts +3 -0
  46. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -0
  47. package/dist/cli/commands/reclaim-unverified.js +172 -0
  48. package/dist/cli/commands/reclaim-unverified.js.map +1 -0
  49. package/dist/cli/commands/reclaim.d.ts +33 -0
  50. package/dist/cli/commands/reclaim.d.ts.map +1 -0
  51. package/dist/cli/commands/reclaim.js +308 -0
  52. package/dist/cli/commands/reclaim.js.map +1 -0
  53. package/dist/cli/commands/status.d.ts +3 -0
  54. package/dist/cli/commands/status.d.ts.map +1 -0
  55. package/dist/cli/commands/status.js +43 -0
  56. package/dist/cli/commands/status.js.map +1 -0
  57. package/dist/cli/commands/streams.d.ts +3 -0
  58. package/dist/cli/commands/streams.d.ts.map +1 -0
  59. package/dist/cli/commands/streams.js +257 -0
  60. package/dist/cli/commands/streams.js.map +1 -0
  61. package/dist/cli/commands/watch.d.ts +3 -0
  62. package/dist/cli/commands/watch.d.ts.map +1 -0
  63. package/dist/cli/commands/watch.js +55 -0
  64. package/dist/cli/commands/watch.js.map +1 -0
  65. package/dist/cli/config.d.ts +61 -0
  66. package/dist/cli/config.d.ts.map +1 -0
  67. package/dist/cli/config.js +140 -0
  68. package/dist/cli/config.js.map +1 -0
  69. package/dist/cli/context.d.ts +29 -0
  70. package/dist/cli/context.d.ts.map +1 -0
  71. package/dist/cli/context.js +2 -0
  72. package/dist/cli/context.js.map +1 -0
  73. package/dist/cli/deps.d.ts +191 -0
  74. package/dist/cli/deps.d.ts.map +1 -0
  75. package/dist/cli/deps.js +277 -0
  76. package/dist/cli/deps.js.map +1 -0
  77. package/dist/cli/descriptor.d.ts +61 -0
  78. package/dist/cli/descriptor.d.ts.map +1 -0
  79. package/dist/cli/descriptor.js +78 -0
  80. package/dist/cli/descriptor.js.map +1 -0
  81. package/dist/cli/errors.d.ts +48 -0
  82. package/dist/cli/errors.d.ts.map +1 -0
  83. package/dist/cli/errors.js +129 -0
  84. package/dist/cli/errors.js.map +1 -0
  85. package/dist/cli/escape-hatch.d.ts +50 -0
  86. package/dist/cli/escape-hatch.d.ts.map +1 -0
  87. package/dist/cli/escape-hatch.js +45 -0
  88. package/dist/cli/escape-hatch.js.map +1 -0
  89. package/dist/cli/index.d.ts +8 -0
  90. package/dist/cli/index.d.ts.map +1 -0
  91. package/dist/cli/index.js +7 -0
  92. package/dist/cli/index.js.map +1 -0
  93. package/dist/cli/listings.d.ts +15 -0
  94. package/dist/cli/listings.d.ts.map +1 -0
  95. package/dist/cli/listings.js +56 -0
  96. package/dist/cli/listings.js.map +1 -0
  97. package/dist/cli/mandate.d.ts +66 -0
  98. package/dist/cli/mandate.d.ts.map +1 -0
  99. package/dist/cli/mandate.js +61 -0
  100. package/dist/cli/mandate.js.map +1 -0
  101. package/dist/cli/output.d.ts +33 -0
  102. package/dist/cli/output.d.ts.map +1 -0
  103. package/dist/cli/output.js +34 -0
  104. package/dist/cli/output.js.map +1 -0
  105. package/dist/cli/preflight.d.ts +38 -0
  106. package/dist/cli/preflight.d.ts.map +1 -0
  107. package/dist/cli/preflight.js +72 -0
  108. package/dist/cli/preflight.js.map +1 -0
  109. package/dist/cli/privy.d.ts +83 -0
  110. package/dist/cli/privy.d.ts.map +1 -0
  111. package/dist/cli/privy.js +230 -0
  112. package/dist/cli/privy.js.map +1 -0
  113. package/dist/cli/progress.d.ts +36 -0
  114. package/dist/cli/progress.d.ts.map +1 -0
  115. package/dist/cli/progress.js +40 -0
  116. package/dist/cli/progress.js.map +1 -0
  117. package/dist/cli/run.d.ts +25 -0
  118. package/dist/cli/run.d.ts.map +1 -0
  119. package/dist/cli/run.js +173 -0
  120. package/dist/cli/run.js.map +1 -0
  121. package/dist/cli/scan-range.d.ts +55 -0
  122. package/dist/cli/scan-range.d.ts.map +1 -0
  123. package/dist/cli/scan-range.js +57 -0
  124. package/dist/cli/scan-range.js.map +1 -0
  125. package/dist/cli/spend-policy.d.ts +60 -0
  126. package/dist/cli/spend-policy.d.ts.map +1 -0
  127. package/dist/cli/spend-policy.js +65 -0
  128. package/dist/cli/spend-policy.js.map +1 -0
  129. package/dist/cli/status.d.ts +90 -0
  130. package/dist/cli/status.d.ts.map +1 -0
  131. package/dist/cli/status.js +252 -0
  132. package/dist/cli/status.js.map +1 -0
  133. package/dist/cli/timeout.d.ts +30 -0
  134. package/dist/cli/timeout.d.ts.map +1 -0
  135. package/dist/cli/timeout.js +55 -0
  136. package/dist/cli/timeout.js.map +1 -0
  137. package/dist/cli/version.d.ts +12 -0
  138. package/dist/cli/version.d.ts.map +1 -0
  139. package/dist/cli/version.js +22 -0
  140. package/dist/cli/version.js.map +1 -0
  141. package/dist/cli/wallet.d.ts +164 -0
  142. package/dist/cli/wallet.d.ts.map +1 -0
  143. package/dist/cli/wallet.js +234 -0
  144. package/dist/cli/wallet.js.map +1 -0
  145. package/dist/frameworks/crewai.d.ts +10 -0
  146. package/dist/frameworks/crewai.d.ts.map +1 -1
  147. package/dist/frameworks/crewai.js +12 -2
  148. package/dist/frameworks/crewai.js.map +1 -1
  149. package/dist/gateway/caller-auth-gateway.d.ts +38 -3
  150. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  151. package/dist/gateway/caller-auth-gateway.js +25 -1
  152. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  153. package/dist/gateway/http-server.d.ts +53 -0
  154. package/dist/gateway/http-server.d.ts.map +1 -1
  155. package/dist/gateway/http-server.js +189 -4
  156. package/dist/gateway/http-server.js.map +1 -1
  157. package/dist/gateway/server-entry.d.ts +1 -0
  158. package/dist/gateway/server-entry.js +1 -0
  159. package/dist/gateway/server-entry.js.map +1 -1
  160. package/dist/gateway/x402-mode.d.ts +123 -0
  161. package/dist/gateway/x402-mode.d.ts.map +1 -0
  162. package/dist/gateway/x402-mode.js +212 -0
  163. package/dist/gateway/x402-mode.js.map +1 -0
  164. package/dist/index.d.ts +6 -4
  165. package/dist/index.d.ts.map +1 -1
  166. package/dist/index.js +8 -3
  167. package/dist/index.js.map +1 -1
  168. package/dist/mandates/env.d.ts +3 -2
  169. package/dist/mandates/env.d.ts.map +1 -1
  170. package/dist/mandates/env.js +3 -2
  171. package/dist/mandates/env.js.map +1 -1
  172. package/dist/mcp/http-server.d.ts +1 -0
  173. package/dist/mcp/http-server.js +1 -0
  174. package/dist/mcp/http-server.js.map +1 -1
  175. package/dist/mcp/server.d.ts +49 -0
  176. package/dist/mcp/server.d.ts.map +1 -1
  177. package/dist/mcp/server.js +533 -25
  178. package/dist/mcp/server.js.map +1 -1
  179. package/dist/mcp/service-invoker.d.ts +97 -0
  180. package/dist/mcp/service-invoker.d.ts.map +1 -0
  181. package/dist/mcp/service-invoker.js +167 -0
  182. package/dist/mcp/service-invoker.js.map +1 -0
  183. package/dist/mcp/stdio.d.ts +1 -0
  184. package/dist/mcp/stdio.js +1 -0
  185. package/dist/mcp/stdio.js.map +1 -1
  186. package/dist/sdk/client.d.ts +25 -1
  187. package/dist/sdk/client.d.ts.map +1 -1
  188. package/dist/sdk/client.js +20 -1
  189. package/dist/sdk/client.js.map +1 -1
  190. package/dist/sdk/invoke.d.ts +19 -4
  191. package/dist/sdk/invoke.d.ts.map +1 -1
  192. package/dist/sdk/invoke.js +69 -5
  193. package/dist/sdk/invoke.js.map +1 -1
  194. package/dist/x402/delivery-oracle.d.ts +108 -0
  195. package/dist/x402/delivery-oracle.d.ts.map +1 -0
  196. package/dist/x402/delivery-oracle.js +174 -0
  197. package/dist/x402/delivery-oracle.js.map +1 -0
  198. package/dist/x402/facilitator-server-entry.d.ts +3 -0
  199. package/dist/x402/facilitator-server-entry.d.ts.map +1 -0
  200. package/dist/x402/facilitator-server-entry.js +27 -0
  201. package/dist/x402/facilitator-server-entry.js.map +1 -0
  202. package/dist/x402/facilitator-server.d.ts +73 -0
  203. package/dist/x402/facilitator-server.d.ts.map +1 -0
  204. package/dist/x402/facilitator-server.js +305 -0
  205. package/dist/x402/facilitator-server.js.map +1 -0
  206. package/dist/x402/facilitator.d.ts +69 -17
  207. package/dist/x402/facilitator.d.ts.map +1 -1
  208. package/dist/x402/facilitator.js +76 -33
  209. package/dist/x402/facilitator.js.map +1 -1
  210. package/dist/x402/payment-requirements.d.ts +106 -0
  211. package/dist/x402/payment-requirements.d.ts.map +1 -0
  212. package/dist/x402/payment-requirements.js +138 -0
  213. package/dist/x402/payment-requirements.js.map +1 -0
  214. package/dist/x402/relayer.d.ts +41 -0
  215. package/dist/x402/relayer.d.ts.map +1 -0
  216. package/dist/x402/relayer.js +64 -0
  217. package/dist/x402/relayer.js.map +1 -0
  218. package/dist/x402/resource-index.d.ts +75 -0
  219. package/dist/x402/resource-index.d.ts.map +1 -0
  220. package/dist/x402/resource-index.js +126 -0
  221. package/dist/x402/resource-index.js.map +1 -0
  222. package/dist/x402/verified-delivery-facilitator.d.ts +285 -0
  223. package/dist/x402/verified-delivery-facilitator.d.ts.map +1 -0
  224. package/dist/x402/verified-delivery-facilitator.js +423 -0
  225. package/dist/x402/verified-delivery-facilitator.js.map +1 -0
  226. package/dist/zktls/t2-delivery-proof.d.ts +6 -6
  227. package/docs/agent-layer.md +14 -9
  228. package/docs/cli.md +705 -0
  229. package/docs/crewai.md +6 -2
  230. package/docs/eliza.md +6 -1
  231. package/docs/mcp-hosted.md +7 -5
  232. package/docs/mcp-reference.md +175 -0
  233. package/docs/quickstart.md +36 -9
  234. package/docs/threat-model.md +4 -3
  235. package/docs/x402.md +648 -0
  236. package/package.json +33 -10
  237. package/src/capability/invocation-capability.ts +17 -1
  238. package/src/cli/amounts.ts +102 -0
  239. package/src/cli/args.ts +150 -0
  240. package/src/cli/attesters.ts +86 -0
  241. package/src/cli/bin.ts +5 -0
  242. package/src/cli/commands/claim.ts +126 -0
  243. package/src/cli/commands/close.ts +69 -0
  244. package/src/cli/commands/discover.ts +228 -0
  245. package/src/cli/commands/invoke.ts +178 -0
  246. package/src/cli/commands/mcp.ts +135 -0
  247. package/src/cli/commands/open.ts +397 -0
  248. package/src/cli/commands/reclaim-unverified.ts +218 -0
  249. package/src/cli/commands/reclaim.ts +377 -0
  250. package/src/cli/commands/status.ts +56 -0
  251. package/src/cli/commands/streams.ts +344 -0
  252. package/src/cli/commands/watch.ts +70 -0
  253. package/src/cli/config.ts +208 -0
  254. package/src/cli/context.ts +30 -0
  255. package/src/cli/deps.ts +528 -0
  256. package/src/cli/descriptor.ts +133 -0
  257. package/src/cli/errors.ts +162 -0
  258. package/src/cli/escape-hatch.ts +92 -0
  259. package/src/cli/index.ts +27 -0
  260. package/src/cli/listings.ts +77 -0
  261. package/src/cli/mandate.ts +126 -0
  262. package/src/cli/output.ts +63 -0
  263. package/src/cli/preflight.ts +88 -0
  264. package/src/cli/privy.ts +396 -0
  265. package/src/cli/progress.ts +75 -0
  266. package/src/cli/run.ts +209 -0
  267. package/src/cli/scan-range.ts +76 -0
  268. package/src/cli/spend-policy.ts +139 -0
  269. package/src/cli/status.ts +381 -0
  270. package/src/cli/timeout.ts +64 -0
  271. package/src/cli/version.ts +21 -0
  272. package/src/cli/wallet.ts +372 -0
  273. package/src/frameworks/crewai.ts +13 -2
  274. package/src/gateway/caller-auth-gateway.ts +77 -6
  275. package/src/gateway/http-server.ts +280 -6
  276. package/src/gateway/server-entry.ts +1 -0
  277. package/src/gateway/x402-mode.ts +350 -0
  278. package/src/index.ts +40 -5
  279. package/src/mandates/env.ts +3 -2
  280. package/src/mcp/http-server.ts +1 -0
  281. package/src/mcp/server.ts +706 -30
  282. package/src/mcp/service-invoker.ts +285 -0
  283. package/src/mcp/stdio.ts +1 -0
  284. package/src/sdk/client.ts +33 -1
  285. package/src/sdk/invoke.ts +90 -12
  286. package/src/x402/delivery-oracle.ts +285 -0
  287. package/src/x402/facilitator-server-entry.ts +35 -0
  288. package/src/x402/facilitator-server.ts +445 -0
  289. package/src/x402/facilitator.ts +125 -46
  290. package/src/x402/payment-requirements.ts +231 -0
  291. package/src/x402/relayer.ts +122 -0
  292. package/src/x402/resource-index.ts +228 -0
  293. package/src/x402/verified-delivery-facilitator.ts +744 -0
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@absol-labs/agent",
3
- "version": "0.9.1",
4
- "description": "Metrik agent layer: x402 verified-streaming payments, an MCP server, framework tools, and spend mandates so AI agents can hire and pay verified services safely.",
3
+ "version": "0.11.0",
4
+ "description": "Metrik agent layer: verified-streaming USDC payments metered by delivery, an MCP server, framework tools, and spend mandates so AI agents can hire and pay verified services safely.",
5
5
  "license": "MIT",
6
6
  "author": "Absol Labs",
7
7
  "keywords": [
8
8
  "metrik",
9
- "x402",
10
9
  "mcp",
11
10
  "ai-agents",
12
11
  "spend-mandate",
@@ -17,6 +16,13 @@
17
16
  "type": "module",
18
17
  "main": "dist/index.js",
19
18
  "types": "dist/index.d.ts",
19
+ "bin": {
20
+ "metrik": "dist/cli/bin.js",
21
+ "metrik-mcp": "dist/mcp/stdio.js",
22
+ "metrik-mcp-http": "dist/mcp/http-server.js",
23
+ "metrik-gateway": "dist/gateway/server-entry.js",
24
+ "metrik-x402-facilitator": "dist/x402/facilitator-server-entry.js"
25
+ },
20
26
  "exports": {
21
27
  ".": {
22
28
  "types": "./dist/index.d.ts",
@@ -34,10 +40,18 @@
34
40
  "types": "./dist/frameworks/langchain.d.ts",
35
41
  "import": "./dist/frameworks/langchain.js"
36
42
  },
43
+ "./cli": {
44
+ "types": "./dist/cli/index.d.ts",
45
+ "import": "./dist/cli/index.js"
46
+ },
37
47
  "./crewai": {
38
48
  "types": "./dist/frameworks/crewai.d.ts",
39
49
  "import": "./dist/frameworks/crewai.js"
40
50
  },
51
+ "./x402-facilitator": {
52
+ "types": "./dist/x402/facilitator-server.d.ts",
53
+ "import": "./dist/x402/facilitator-server.js"
54
+ },
41
55
  "./package.json": "./package.json",
42
56
  "./*": {
43
57
  "types": "./dist/*.d.ts",
@@ -63,12 +77,12 @@
63
77
  "access": "public"
64
78
  },
65
79
  "engines": {
66
- "node": ">=20 <21",
80
+ "node": ">=20.19 <25",
67
81
  "pnpm": "9.15.x"
68
82
  },
69
83
  "dependencies": {
70
- "@absol-labs/sdk": "^0.9.0",
71
- "@absol-labs/shared": "^0.12.1",
84
+ "@absol-labs/sdk": "^0.9.2",
85
+ "@absol-labs/shared": "^0.13.1",
72
86
  "@modelcontextprotocol/sdk": "^1.29.0",
73
87
  "@napi-rs/keyring": "1.3.0",
74
88
  "@privy-io/node": "0.33.0",
@@ -82,7 +96,8 @@
82
96
  "@elizaos/core": "^1.7.2",
83
97
  "@langchain/core": "^1.2.0",
84
98
  "@reclaimprotocol/js-sdk": "^5.6.0",
85
- "@reclaimprotocol/zk-fetch": "^1.0.0"
99
+ "@reclaimprotocol/zk-fetch": "^1.0.0",
100
+ "x402": "^1.2.0"
86
101
  },
87
102
  "peerDependenciesMeta": {
88
103
  "@coinbase/agentkit": {
@@ -102,6 +117,9 @@
102
117
  },
103
118
  "@reclaimprotocol/zk-fetch": {
104
119
  "optional": true
120
+ },
121
+ "x402": {
122
+ "optional": true
105
123
  }
106
124
  },
107
125
  "devDependencies": {
@@ -116,11 +134,14 @@
116
134
  "prettier": "^3.4.0",
117
135
  "tsx": "^4.19.0",
118
136
  "typescript": "^5.7.0",
119
- "vitest": "^2.1.0"
137
+ "vitest": "^2.1.0",
138
+ "x402": "1.2.0",
139
+ "x402-fetch": "1.2.0"
120
140
  },
121
141
  "scripts": {
122
142
  "build": "tsc -p tsconfig.json",
123
143
  "build:e2e": "tsc -p tsconfig.e2e.build.json",
144
+ "cli": "tsx src/cli/bin.ts",
124
145
  "mcp:stdio": "tsx src/mcp/stdio.ts",
125
146
  "mcp:http": "tsx src/mcp/http-server.ts",
126
147
  "gateway": "tsx src/gateway/server-entry.ts",
@@ -129,9 +150,11 @@
129
150
  "l2:proof": "node scripts/l2-proof.mjs",
130
151
  "test": "vitest run",
131
152
  "test:package": "node scripts/test-packed-package.mjs",
132
- "typecheck": "tsc -p tsconfig.json --noEmit",
153
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.examples.json --noEmit",
154
+ "typecheck:examples": "tsc -p tsconfig.examples.json --noEmit",
133
155
  "typecheck:e2e": "tsc -p tsconfig.e2e.json --noEmit",
134
156
  "lint": "tsc -p tsconfig.json --noEmit",
135
- "format": "prettier --write ."
157
+ "format": "prettier --write .",
158
+ "x402:facilitator": "tsx src/x402/facilitator-server-entry.ts"
136
159
  }
137
160
  }
@@ -172,11 +172,27 @@ export function buildInvocationCapabilityTypedData(
172
172
  };
173
173
  }
174
174
 
175
+ /**
176
+ * The minimum an account must expose to sign a capability: its address, and
177
+ * EIP-712 signing.
178
+ *
179
+ * Deliberately narrower than viem's `LocalAccount`. Every wallet this package
180
+ * resolves signs typed data — a raw key, a CDP server account via `toAccount()`,
181
+ * a Privy embedded EOA — but only the raw-key one is a `LocalAccount` by TYPE.
182
+ * Demanding the full interface would have meant the MCP server could only ever
183
+ * invoke with a raw key, which is the wallet an MCP host is least likely to be
184
+ * configured with. Widening a parameter type breaks no existing caller.
185
+ */
186
+ export type InvocationCapabilitySigner = Pick<
187
+ LocalAccount,
188
+ "address" | "signTypedData"
189
+ >;
190
+
175
191
  /** Signs an `InvocationCapability` with the buyer's own wallet. */
176
192
  export async function signInvocationCapability(
177
193
  capability: InvocationCapability,
178
194
  domain: InvocationCapabilityDomainInput,
179
- account: LocalAccount,
195
+ account: InvocationCapabilitySigner,
180
196
  ): Promise<Hex> {
181
197
  const typedData = buildInvocationCapabilityTypedData(capability, domain);
182
198
  return account.signTypedData(typedData);
@@ -0,0 +1,102 @@
1
+ import { formatUnits, parseUnits } from "viem";
2
+
3
+ import { usageError } from "./errors.js";
4
+
5
+ /** Circle USDC — 6 decimals on every deployment Metrik settles against. */
6
+ export const USDC_DECIMALS = 6;
7
+
8
+ /**
9
+ * How `--budget` / `--rate` are interpreted.
10
+ *
11
+ * - `"usdc"` (default): decimal USDC, e.g. `--budget 0.27`.
12
+ * - `"atomic"`: raw 6-decimal base units, e.g. `--budget 270000`.
13
+ *
14
+ * Both are offered because the two audiences count differently: a human reads
15
+ * USDC, while the escrow, the oracle and every `METRIK_E2E_*` fixture are in
16
+ * atomic units. Guessing from the magnitude of the number would be a silent
17
+ * 1,000,000x mistake on a fund-moving command, so the unit is explicit.
18
+ */
19
+ export type AmountUnits = "usdc" | "atomic";
20
+
21
+ export function parseAmountUnits(raw: string | undefined): AmountUnits {
22
+ if (raw === undefined || raw === "usdc") return "usdc";
23
+ if (raw === "atomic") return "atomic";
24
+ throw usageError(`--units must be "usdc" or "atomic" (got "${raw}")`);
25
+ }
26
+
27
+ /** Parses a `--budget`/`--rate` value into atomic USDC units. */
28
+ export function parseUsdcAmount(
29
+ raw: string,
30
+ units: AmountUnits,
31
+ flagName: string,
32
+ ): bigint {
33
+ if (units === "atomic") {
34
+ if (!/^\d+$/.test(raw)) {
35
+ throw usageError(
36
+ `--${flagName} must be a non-negative integer of atomic USDC units (got "${raw}")`,
37
+ );
38
+ }
39
+ return BigInt(raw);
40
+ }
41
+
42
+ if (!/^\d+(\.\d+)?$/.test(raw)) {
43
+ throw usageError(
44
+ `--${flagName} must be a decimal USDC amount (got "${raw}")`,
45
+ "pass --units atomic to supply raw 6-decimal base units instead.",
46
+ );
47
+ }
48
+ const fraction = raw.split(".")[1];
49
+ if (fraction !== undefined && fraction.length > USDC_DECIMALS) {
50
+ throw usageError(
51
+ `--${flagName} has more precision than USDC supports (${USDC_DECIMALS} decimals): ${raw}`,
52
+ );
53
+ }
54
+ return parseUnits(raw, USDC_DECIMALS);
55
+ }
56
+
57
+ /** `270000n` -> `"0.27 USDC (270000 atomic)"`. */
58
+ export function formatUsdc(atomic: bigint): string {
59
+ return `${formatUnits(atomic, USDC_DECIMALS)} USDC (${atomic.toString()} atomic)`;
60
+ }
61
+
62
+ /**
63
+ * Parses `--duration`: bare seconds, or a `s`/`m`/`h`/`d` suffix.
64
+ */
65
+ export function parseDurationSeconds(raw: string): number {
66
+ const match = /^(\d+)([smhd]?)$/.exec(raw);
67
+ if (match === null) {
68
+ throw usageError(
69
+ `--duration must be seconds, optionally suffixed s/m/h/d (got "${raw}")`,
70
+ );
71
+ }
72
+ const value = Number(match[1]);
73
+ const multiplier = { "": 1, s: 1, m: 60, h: 3_600, d: 86_400 }[
74
+ match[2] ?? ""
75
+ ];
76
+ if (multiplier === undefined) {
77
+ throw usageError(`--duration has an unsupported unit: ${raw}`);
78
+ }
79
+ const seconds = value * multiplier;
80
+ if (!Number.isSafeInteger(seconds) || seconds <= 0) {
81
+ throw usageError(`--duration must be a positive number of seconds: ${raw}`);
82
+ }
83
+ return seconds;
84
+ }
85
+
86
+ /** Seconds -> a compact human duration (`3600` -> `1h`). */
87
+ export function formatDuration(seconds: number): string {
88
+ if (seconds <= 0) return "0s";
89
+ const parts: string[] = [];
90
+ let remaining = seconds;
91
+ for (const [unit, size] of [
92
+ ["d", 86_400],
93
+ ["h", 3_600],
94
+ ["m", 60],
95
+ ["s", 1],
96
+ ] as const) {
97
+ const count = Math.floor(remaining / size);
98
+ if (count > 0) parts.push(`${count}${unit}`);
99
+ remaining -= count * size;
100
+ }
101
+ return parts.join(" ");
102
+ }
@@ -0,0 +1,150 @@
1
+ import { usageError } from "./errors.js";
2
+
3
+ /**
4
+ * A deliberately small GNU-style argument parser. The CLI ships no argument
5
+ * parsing dependency: the surface is seven commands with a handful of flags, and
6
+ * the packed tarball should stay as light as the library it wraps.
7
+ *
8
+ * Supported forms: `--flag`, `--flag value`, `--flag=value`, `--no-flag`, and
9
+ * positional arguments. `--` ends flag parsing.
10
+ */
11
+ export interface ParsedArgs {
12
+ readonly command: string | null;
13
+ readonly positionals: readonly string[];
14
+ /**
15
+ * Every occurrence of each flag, in order. Repeats are kept rather than
16
+ * overwritten so `--header` can be given more than once; scalar readers take
17
+ * the last occurrence.
18
+ */
19
+ readonly flags: ReadonlyMap<string, readonly (string | boolean)[]>;
20
+ }
21
+
22
+ export function parseArgs(argv: readonly string[]): ParsedArgs {
23
+ const positionals: string[] = [];
24
+ const flags = new Map<string, (string | boolean)[]>();
25
+ const push = (name: string, value: string | boolean): void => {
26
+ const existing = flags.get(name);
27
+ if (existing === undefined) flags.set(name, [value]);
28
+ else existing.push(value);
29
+ };
30
+ let passthrough = false;
31
+
32
+ for (let index = 0; index < argv.length; index++) {
33
+ const token = argv[index];
34
+ if (token === undefined) continue;
35
+
36
+ if (passthrough || !token.startsWith("--")) {
37
+ positionals.push(token);
38
+ continue;
39
+ }
40
+ if (token === "--") {
41
+ passthrough = true;
42
+ continue;
43
+ }
44
+
45
+ const body = token.slice(2);
46
+ const equals = body.indexOf("=");
47
+ if (equals !== -1) {
48
+ const name = body.slice(0, equals);
49
+ assertFlagName(name, token);
50
+ push(name, body.slice(equals + 1));
51
+ continue;
52
+ }
53
+ assertFlagName(body, token);
54
+
55
+ if (body.startsWith("no-")) {
56
+ push(body.slice(3), false);
57
+ continue;
58
+ }
59
+
60
+ const next = argv[index + 1];
61
+ if (next === undefined || next.startsWith("--")) {
62
+ push(body, true);
63
+ continue;
64
+ }
65
+ push(body, next);
66
+ index++;
67
+ }
68
+
69
+ const [command = null, ...rest] = positionals;
70
+ return { command, positionals: rest, flags };
71
+ }
72
+
73
+ function assertFlagName(name: string, token: string): void {
74
+ if (name.length === 0) {
75
+ throw usageError(`not a valid flag: ${token}`);
76
+ }
77
+ }
78
+
79
+ /** Reads a flag that must carry a string value. */
80
+ export function stringFlag(args: ParsedArgs, name: string): string | undefined {
81
+ const values = args.flags.get(name);
82
+ if (values === undefined) return undefined;
83
+ const value = values[values.length - 1];
84
+ if (typeof value !== "string") {
85
+ throw usageError(`--${name} requires a value`);
86
+ }
87
+ return value;
88
+ }
89
+
90
+ /** Every occurrence of a repeatable string flag (e.g. `--header`). */
91
+ export function stringFlags(args: ParsedArgs, name: string): string[] {
92
+ const values = args.flags.get(name) ?? [];
93
+ return values.map((value) => {
94
+ if (typeof value !== "string") {
95
+ throw usageError(`--${name} requires a value`);
96
+ }
97
+ return value;
98
+ });
99
+ }
100
+
101
+ export function requiredStringFlag(args: ParsedArgs, name: string): string {
102
+ const value = stringFlag(args, name);
103
+ if (value === undefined) {
104
+ throw usageError(`--${name} is required`);
105
+ }
106
+ return value;
107
+ }
108
+
109
+ export function booleanFlag(args: ParsedArgs, name: string): boolean {
110
+ const values = args.flags.get(name);
111
+ if (values === undefined) return false;
112
+ const value = values[values.length - 1];
113
+ if (value === undefined) return false;
114
+ if (typeof value === "boolean") return value;
115
+ if (value === "true") return true;
116
+ if (value === "false") return false;
117
+ throw usageError(`--${name} takes no value (got "${value}")`);
118
+ }
119
+
120
+ export function integerFlag(
121
+ args: ParsedArgs,
122
+ name: string,
123
+ ): number | undefined {
124
+ const raw = stringFlag(args, name);
125
+ if (raw === undefined) return undefined;
126
+ if (!/^\d+$/.test(raw)) {
127
+ throw usageError(`--${name} must be a non-negative integer (got "${raw}")`);
128
+ }
129
+ return Number(raw);
130
+ }
131
+
132
+ /**
133
+ * Rejects flags the command does not understand. A silently ignored `--budgt`
134
+ * on a fund-moving command is how someone spends more than they meant to.
135
+ */
136
+ export function assertKnownFlags(
137
+ args: ParsedArgs,
138
+ known: readonly string[],
139
+ ): void {
140
+ const allowed = new Set(known);
141
+ const unknown = [...args.flags.keys()].filter((name) => !allowed.has(name));
142
+ if (unknown.length > 0) {
143
+ throw usageError(
144
+ `unknown flag${unknown.length > 1 ? "s" : ""}: ${unknown
145
+ .map((name) => `--${name}`)
146
+ .join(", ")}`,
147
+ `known flags: ${known.map((name) => `--${name}`).join(", ")}`,
148
+ );
149
+ }
150
+ }
@@ -0,0 +1,86 @@
1
+ import { describeError } from "./errors.js";
2
+ import type { CliReadClient } from "./deps.js";
3
+
4
+ /**
5
+ * Who attests, read off the escrow.
6
+ *
7
+ * Metrik's whole claim is that money moves only for delivery that a NEUTRAL
8
+ * verifier checked. On the live Base Sepolia deployment that neutrality is
9
+ * partial: `getOracleSigners()` returns three addresses, and one of them is the
10
+ * operator of the listings a buyer is being offered. A tool that prints
11
+ * "cryptographically verified" and "tier 2" without saying so is letting the
12
+ * buyer assume something the deployment does not provide.
13
+ *
14
+ * This is DISCLOSURE, not enforcement. The CLI does not refuse such a listing
15
+ * and does not score it — it states the fact and leaves the judgement to the
16
+ * buyer. Fixing the deployment is a deployment decision, not a CLI one.
17
+ */
18
+ export interface AttesterSet {
19
+ /** The signer set, lowercased. `null` when the escrow would not answer. */
20
+ readonly signers: readonly string[] | null;
21
+ /** Why the set could not be read, when applicable. */
22
+ readonly unavailable: string | null;
23
+ }
24
+
25
+ /**
26
+ * Read the signer set, failing SOFT.
27
+ *
28
+ * An unreadable set must never render as "no overlap": "we checked and the
29
+ * operator does not attest" and "we could not check" are different facts, and
30
+ * conflating them would turn an outage into a false assurance. `signers: null`
31
+ * is carried through to every caller and printed as UNKNOWN.
32
+ */
33
+ export async function readAttesterSet(
34
+ read: Pick<CliReadClient, "oracleSigners">,
35
+ ): Promise<AttesterSet> {
36
+ try {
37
+ const signers = await read.oracleSigners();
38
+ return {
39
+ signers: signers.map((signer) => signer.toLowerCase()),
40
+ unavailable: null,
41
+ };
42
+ } catch (error) {
43
+ return { signers: null, unavailable: describeError(error) };
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Is this operator also one of the escrow's attesters?
49
+ *
50
+ * `null` means unknown (the set could not be read) and must be rendered as
51
+ * unknown, never as `false`.
52
+ */
53
+ export function operatorAttestsOwnDelivery(
54
+ set: AttesterSet,
55
+ operator: string,
56
+ ): boolean | null {
57
+ if (set.signers === null) return null;
58
+ return set.signers.includes(operator.toLowerCase());
59
+ }
60
+
61
+ /**
62
+ * One factual stderr line, or `null` when there is nothing to disclose.
63
+ *
64
+ * Deliberately not alarming and deliberately not a verdict: it reports a
65
+ * configuration fact and what that fact means, and stops there.
66
+ */
67
+ export function attesterDisclosure(
68
+ set: AttesterSet,
69
+ operator: string,
70
+ ): string | null {
71
+ const overlap = operatorAttestsOwnDelivery(set, operator);
72
+ if (overlap === null) return attesterSetUnavailableWarning(set);
73
+ if (!overlap) return null;
74
+ const total = set.signers?.length ?? 0;
75
+ return `warning: operator ${operator} is also one of the ${total} oracle signer${total === 1 ? "" : "s"} configured on this escrow. The party being paid is among the parties attesting that delivery happened. Verification here is not independent of the seller.`;
76
+ }
77
+
78
+ /**
79
+ * The one warning that belongs to the ESCROW rather than to any one listing:
80
+ * the signer set could not be read at all. Printed once per command, not once
81
+ * per row, and never as "no overlap".
82
+ */
83
+ export function attesterSetUnavailableWarning(set: AttesterSet): string | null {
84
+ if (set.signers !== null) return null;
85
+ return `warning: the escrow's oracle signer set could not be read (${set.unavailable ?? "no reason given"}) — whether the operator being paid is also one of the parties attesting to its own delivery is UNKNOWN, not "no overlap".`;
86
+ }
package/src/cli/bin.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./run.js";
3
+
4
+ const exitCode = await runCli({ argv: process.argv.slice(2) });
5
+ process.exitCode = exitCode;
@@ -0,0 +1,126 @@
1
+ import { formatUsdc } from "../amounts.js";
2
+ import { assertTestnetRpc, parseBytes32 } from "../config.js";
3
+ import { CliError, describeError, usageError } from "../errors.js";
4
+ import { signClaimMandate } from "../mandate.js";
5
+ import { assertCanPayGas } from "../preflight.js";
6
+ import { writeClientOptions } from "../deps.js";
7
+ import {
8
+ isPolicyEnforced,
9
+ PRIVATE_KEY_ENV,
10
+ resolveCliWallet,
11
+ } from "../wallet.js";
12
+ import type { CommandContext, CommandSpec } from "../context.js";
13
+
14
+ /**
15
+ * The seller's side of the loop.
16
+ *
17
+ * `status` has always computed and printed an `operator claimable` row — a
18
+ * redeemable balance the CLI could see and could not act on. `claimWithProof` is
19
+ * operator-gated on-chain, so this command refuses a wallet that is not the
20
+ * stream's operator BEFORE it broadcasts: an address mismatch is knowable with
21
+ * one read, and letting it become a raw `NotOperator()` revert would cost gas to
22
+ * learn a fact that was already available.
23
+ */
24
+ export const claimCommand: CommandSpec = {
25
+ name: "claim",
26
+ summary: "Operator: redeem what this stream has verifiably earned",
27
+ usage: "metrik claim <streamId>",
28
+ flags: [],
29
+ signs: true,
30
+ async run(context: CommandContext): Promise<void> {
31
+ const { args, config, env, out, deps } = context;
32
+ const [streamIdArg] = args.positionals;
33
+ if (streamIdArg === undefined) {
34
+ throw usageError("metrik claim requires a <streamId>");
35
+ }
36
+ const streamId = parseBytes32(streamIdArg, "<streamId>");
37
+
38
+ const wallet = await resolveCliWallet(env, {
39
+ note: out.note,
40
+ rpcUrl: config.rpcUrl,
41
+ });
42
+ const { account } = wallet;
43
+ if (isPolicyEnforced(wallet)) {
44
+ // Said plainly rather than discovered mid-command. `claim` is the
45
+ // OPERATOR's side, and a Privy broker policy allowlists the BUYER
46
+ // actions only (openStream, close, reclaim, reclaimUnverified, and a
47
+ // bounded USDC approval). That is a property of an operator using a
48
+ // buyer-scoped wallet, not a defect in this command.
49
+ out.note(
50
+ "warning: this wallet's broker policy allowlists buyer actions only, so it will refuse to sign an operator claim. Claim with the operator's own key.",
51
+ );
52
+ }
53
+ const read = deps.createReadClient(config);
54
+ await assertTestnetRpc(config, () => read.getChainId());
55
+ await assertCanPayGas(read, account.address);
56
+
57
+ const stream = await read.getStreamV2(streamId);
58
+ if (stream.operator.toLowerCase() !== account.address.toLowerCase()) {
59
+ throw new CliError(
60
+ `wallet ${account.address} is not the operator of this stream`,
61
+ {
62
+ exitCode: 3,
63
+ hint: `only ${stream.operator} can claim it — the escrow enforces this, so no other wallet can. Set ${PRIVATE_KEY_ENV} to the operator's key. (The buyer's side of this stream is \`metrik reclaim ${streamId}\`.)`,
64
+ },
65
+ );
66
+ }
67
+
68
+ // Accrual lives in the oracle's checkpoint proof, never in the stream tuple.
69
+ // No proof means no claim is possible yet, and saying that is better than
70
+ // paying gas for a `NothingToClaim()` revert.
71
+ let cumulativeAmount: bigint;
72
+ try {
73
+ cumulativeAmount = (await read.checkpointProof(streamId))
74
+ .cumulativeAmount;
75
+ } catch (error) {
76
+ throw new CliError(
77
+ "no checkpoint covers this stream yet, so there is nothing provable to claim",
78
+ {
79
+ exitCode: 4,
80
+ hint: `the oracle publishes roughly every 15-22s and pays only for intervals that verified; ${describeError(
81
+ error,
82
+ )}`,
83
+ cause: error,
84
+ },
85
+ );
86
+ }
87
+
88
+ const claimable = await read.claimableForV2(streamId, cumulativeAmount);
89
+ if (claimable === 0n) {
90
+ throw new CliError("this stream has nothing claimable right now", {
91
+ exitCode: 4,
92
+ hint: `everything checkpointed so far has already been claimed. See where it stands: metrik status ${streamId}`,
93
+ });
94
+ }
95
+
96
+ const write = deps.createWriteClient(
97
+ config,
98
+ account,
99
+ writeClientOptions(wallet),
100
+ );
101
+ const result = await write.claimStream({
102
+ streamId,
103
+ // `null` for a policy-enforced wallet — see `../spend-policy.ts`.
104
+ signedMandate: isPolicyEnforced(wallet)
105
+ ? null
106
+ : await signClaimMandate(account, config.chainId),
107
+ spentSoFarUsdc: 0n,
108
+ nowSeconds: deps.nowSeconds(),
109
+ });
110
+
111
+ out.result({
112
+ streamId,
113
+ txHash: result.txHash,
114
+ operator: stream.operator,
115
+ claimed: claimable,
116
+ verifiedCumulative: cumulativeAmount,
117
+ });
118
+ out.print("Claimed.");
119
+ out.table([
120
+ ["streamId", streamId],
121
+ ["tx", result.txHash],
122
+ ["operator", stream.operator],
123
+ ["claimed", `${formatUsdc(claimable)} (net of protocol fee)`],
124
+ ]);
125
+ },
126
+ };
@@ -0,0 +1,69 @@
1
+ import { assertTestnetRpc, parseBytes32 } from "../config.js";
2
+ import { usageError } from "../errors.js";
3
+ import { signRecoveryMandate } from "../mandate.js";
4
+ import { assertCanPayGas } from "../preflight.js";
5
+ import { writeClientOptions } from "../deps.js";
6
+ import { isPolicyEnforced, resolveCliWallet } from "../wallet.js";
7
+ import type { CommandContext, CommandSpec } from "../context.js";
8
+
9
+ export const closeCommand: CommandSpec = {
10
+ name: "close",
11
+ summary: "Stop the clock on a stream so the unspent budget can be reclaimed",
12
+ usage: "metrik close <streamId>",
13
+ flags: [],
14
+ signs: true,
15
+ async run(context: CommandContext): Promise<void> {
16
+ const { args, config, env, out, deps } = context;
17
+ const [streamIdArg] = args.positionals;
18
+ if (streamIdArg === undefined) {
19
+ throw usageError("metrik close requires a <streamId>");
20
+ }
21
+ const streamId = parseBytes32(streamIdArg, "<streamId>");
22
+
23
+ const wallet = await resolveCliWallet(env, {
24
+ note: out.note,
25
+ rpcUrl: config.rpcUrl,
26
+ });
27
+ const { account } = wallet;
28
+ const read = deps.createReadClient(config);
29
+ await assertTestnetRpc(config, () => read.getChainId());
30
+ await assertCanPayGas(read, account.address);
31
+
32
+ // A policy-enforced wallet signs no mandate of its own: this is a
33
+ // buyer-recovery action, the escrow gates it on the buyer address and the
34
+ // broker gates it again on `stream.buyer == wallet`. A locally self-signed
35
+ // placeholder would add nothing and the broker would refuse to sign it.
36
+ // See `../spend-policy.ts`.
37
+ const signedMandate = isPolicyEnforced(wallet)
38
+ ? null
39
+ : await signRecoveryMandate(account, config.chainId);
40
+ const write = deps.createWriteClient(
41
+ config,
42
+ account,
43
+ writeClientOptions(wallet),
44
+ );
45
+ const result = await write.closeStream({
46
+ streamId,
47
+ signedMandate,
48
+ spentSoFarUsdc: 0n,
49
+ nowSeconds: deps.nowSeconds(),
50
+ });
51
+
52
+ out.result({ streamId, txHash: result.txHash, closed: true });
53
+ out.print("Stream closed.");
54
+ out.table([
55
+ ["streamId", streamId],
56
+ ["tx", result.txHash],
57
+ ]);
58
+ out.print("");
59
+ out.print(
60
+ `Nothing pushes money back. Reclaim what was never earned: metrik reclaim ${streamId}`,
61
+ );
62
+ out.print(
63
+ "That reclaim reverts until the oracle publishes a checkpoint covering the stop time, so it waits and retries for you — leave it running.",
64
+ );
65
+ out.print(
66
+ `Next time: metrik reclaim <streamId> --close-first does this close and that reclaim in one command.`,
67
+ );
68
+ },
69
+ };