@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.
Files changed (331) hide show
  1. package/README.md +106 -30
  2. package/dist/capability/invocation-capability.d.ts +20 -8
  3. package/dist/capability/invocation-capability.d.ts.map +1 -1
  4. package/dist/capability/invocation-capability.js.map +1 -1
  5. package/dist/capability/probe-capability.d.ts +225 -0
  6. package/dist/capability/probe-capability.d.ts.map +1 -0
  7. package/dist/capability/probe-capability.js +248 -0
  8. package/dist/capability/probe-capability.js.map +1 -0
  9. package/dist/cli/args.d.ts +11 -0
  10. package/dist/cli/args.d.ts.map +1 -1
  11. package/dist/cli/args.js +23 -0
  12. package/dist/cli/args.js.map +1 -1
  13. package/dist/cli/commands/claim.d.ts.map +1 -1
  14. package/dist/cli/commands/claim.js +21 -5
  15. package/dist/cli/commands/claim.js.map +1 -1
  16. package/dist/cli/commands/close.d.ts.map +1 -1
  17. package/dist/cli/commands/close.js +51 -7
  18. package/dist/cli/commands/close.js.map +1 -1
  19. package/dist/cli/commands/invoke.d.ts +2 -0
  20. package/dist/cli/commands/invoke.d.ts.map +1 -1
  21. package/dist/cli/commands/invoke.js +3 -2
  22. package/dist/cli/commands/invoke.js.map +1 -1
  23. package/dist/cli/commands/mcp.d.ts.map +1 -1
  24. package/dist/cli/commands/mcp.js +15 -2
  25. package/dist/cli/commands/mcp.js.map +1 -1
  26. package/dist/cli/commands/open.d.ts.map +1 -1
  27. package/dist/cli/commands/open.js +121 -15
  28. package/dist/cli/commands/open.js.map +1 -1
  29. package/dist/cli/commands/pay.d.ts +19 -0
  30. package/dist/cli/commands/pay.d.ts.map +1 -0
  31. package/dist/cli/commands/pay.js +355 -0
  32. package/dist/cli/commands/pay.js.map +1 -0
  33. package/dist/cli/commands/reclaim-unverified.d.ts.map +1 -1
  34. package/dist/cli/commands/reclaim-unverified.js +15 -6
  35. package/dist/cli/commands/reclaim-unverified.js.map +1 -1
  36. package/dist/cli/commands/reclaim.d.ts +23 -2
  37. package/dist/cli/commands/reclaim.d.ts.map +1 -1
  38. package/dist/cli/commands/reclaim.js +200 -21
  39. package/dist/cli/commands/reclaim.js.map +1 -1
  40. package/dist/cli/commands/refund.d.ts +13 -0
  41. package/dist/cli/commands/refund.d.ts.map +1 -0
  42. package/dist/cli/commands/refund.js +224 -0
  43. package/dist/cli/commands/refund.js.map +1 -0
  44. package/dist/cli/commands/status.d.ts.map +1 -1
  45. package/dist/cli/commands/status.js +10 -1
  46. package/dist/cli/commands/status.js.map +1 -1
  47. package/dist/cli/commands/streams.js +9 -13
  48. package/dist/cli/commands/streams.js.map +1 -1
  49. package/dist/cli/commands/watch.d.ts.map +1 -1
  50. package/dist/cli/commands/watch.js +2 -1
  51. package/dist/cli/commands/watch.js.map +1 -1
  52. package/dist/cli/config.d.ts +44 -26
  53. package/dist/cli/config.d.ts.map +1 -1
  54. package/dist/cli/config.js +88 -38
  55. package/dist/cli/config.js.map +1 -1
  56. package/dist/cli/deps.d.ts +86 -7
  57. package/dist/cli/deps.d.ts.map +1 -1
  58. package/dist/cli/deps.js +102 -16
  59. package/dist/cli/deps.js.map +1 -1
  60. package/dist/cli/errors.d.ts.map +1 -1
  61. package/dist/cli/errors.js +21 -1
  62. package/dist/cli/errors.js.map +1 -1
  63. package/dist/cli/escape-hatch.d.ts +6 -4
  64. package/dist/cli/escape-hatch.d.ts.map +1 -1
  65. package/dist/cli/escape-hatch.js +6 -2
  66. package/dist/cli/escape-hatch.js.map +1 -1
  67. package/dist/cli/listings.d.ts +18 -0
  68. package/dist/cli/listings.d.ts.map +1 -1
  69. package/dist/cli/listings.js +22 -12
  70. package/dist/cli/listings.js.map +1 -1
  71. package/dist/cli/preflight.d.ts +20 -14
  72. package/dist/cli/preflight.d.ts.map +1 -1
  73. package/dist/cli/preflight.js +34 -20
  74. package/dist/cli/preflight.js.map +1 -1
  75. package/dist/cli/privy.d.ts +83 -0
  76. package/dist/cli/privy.d.ts.map +1 -0
  77. package/dist/cli/privy.js +232 -0
  78. package/dist/cli/privy.js.map +1 -0
  79. package/dist/cli/reclaimability.d.ts +82 -0
  80. package/dist/cli/reclaimability.d.ts.map +1 -0
  81. package/dist/cli/reclaimability.js +80 -0
  82. package/dist/cli/reclaimability.js.map +1 -0
  83. package/dist/cli/run.d.ts +2 -1
  84. package/dist/cli/run.d.ts.map +1 -1
  85. package/dist/cli/run.js +40 -11
  86. package/dist/cli/run.js.map +1 -1
  87. package/dist/cli/scan-range.d.ts +2 -2
  88. package/dist/cli/scan-range.js +2 -2
  89. package/dist/cli/spend-policy.d.ts +60 -0
  90. package/dist/cli/spend-policy.d.ts.map +1 -0
  91. package/dist/cli/spend-policy.js +65 -0
  92. package/dist/cli/spend-policy.js.map +1 -0
  93. package/dist/cli/status.d.ts +3 -1
  94. package/dist/cli/status.d.ts.map +1 -1
  95. package/dist/cli/status.js +15 -3
  96. package/dist/cli/status.js.map +1 -1
  97. package/dist/cli/wallet.d.ts +109 -40
  98. package/dist/cli/wallet.d.ts.map +1 -1
  99. package/dist/cli/wallet.js +109 -44
  100. package/dist/cli/wallet.js.map +1 -1
  101. package/dist/discovery/rate-band.d.ts +74 -0
  102. package/dist/discovery/rate-band.d.ts.map +1 -0
  103. package/dist/discovery/rate-band.js +114 -0
  104. package/dist/discovery/rate-band.js.map +1 -0
  105. package/dist/escrow-generation.d.ts +81 -0
  106. package/dist/escrow-generation.d.ts.map +1 -0
  107. package/dist/escrow-generation.js +76 -0
  108. package/dist/escrow-generation.js.map +1 -0
  109. package/dist/frameworks/agentkit.d.ts +8 -4
  110. package/dist/frameworks/agentkit.d.ts.map +1 -1
  111. package/dist/frameworks/agentkit.js +27 -10
  112. package/dist/frameworks/agentkit.js.map +1 -1
  113. package/dist/frameworks/eliza.d.ts +3 -3
  114. package/dist/frameworks/eliza.d.ts.map +1 -1
  115. package/dist/frameworks/eliza.js +15 -6
  116. package/dist/frameworks/eliza.js.map +1 -1
  117. package/dist/frameworks/langchain.d.ts.map +1 -1
  118. package/dist/frameworks/langchain.js +8 -0
  119. package/dist/frameworks/langchain.js.map +1 -1
  120. package/dist/gateway/caller-auth-gateway.d.ts +91 -4
  121. package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
  122. package/dist/gateway/caller-auth-gateway.js +184 -7
  123. package/dist/gateway/caller-auth-gateway.js.map +1 -1
  124. package/dist/gateway/http-server.d.ts +121 -1
  125. package/dist/gateway/http-server.d.ts.map +1 -1
  126. package/dist/gateway/http-server.js +474 -5
  127. package/dist/gateway/http-server.js.map +1 -1
  128. package/dist/gateway/probe-auth.d.ts +112 -0
  129. package/dist/gateway/probe-auth.d.ts.map +1 -0
  130. package/dist/gateway/probe-auth.js +127 -0
  131. package/dist/gateway/probe-auth.js.map +1 -0
  132. package/dist/gateway/signed-rate-bounds.d.ts +51 -0
  133. package/dist/gateway/signed-rate-bounds.d.ts.map +1 -0
  134. package/dist/gateway/signed-rate-bounds.js +94 -0
  135. package/dist/gateway/signed-rate-bounds.js.map +1 -0
  136. package/dist/gateway/x402-opener-mode.d.ts +215 -0
  137. package/dist/gateway/x402-opener-mode.d.ts.map +1 -0
  138. package/dist/gateway/x402-opener-mode.js +639 -0
  139. package/dist/gateway/x402-opener-mode.js.map +1 -0
  140. package/dist/index.d.ts +11 -5
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +15 -5
  143. package/dist/index.js.map +1 -1
  144. package/dist/mandates/env.d.ts +3 -2
  145. package/dist/mandates/env.d.ts.map +1 -1
  146. package/dist/mandates/env.js +3 -2
  147. package/dist/mandates/env.js.map +1 -1
  148. package/dist/mcp/server.d.ts +100 -0
  149. package/dist/mcp/server.d.ts.map +1 -1
  150. package/dist/mcp/server.js +719 -39
  151. package/dist/mcp/server.js.map +1 -1
  152. package/dist/mcp/service-invoker.d.ts +97 -0
  153. package/dist/mcp/service-invoker.d.ts.map +1 -0
  154. package/dist/mcp/service-invoker.js +167 -0
  155. package/dist/mcp/service-invoker.js.map +1 -0
  156. package/dist/mcp/x402-tools.d.ts +104 -0
  157. package/dist/mcp/x402-tools.d.ts.map +1 -0
  158. package/dist/mcp/x402-tools.js +434 -0
  159. package/dist/mcp/x402-tools.js.map +1 -0
  160. package/dist/network.d.ts +119 -0
  161. package/dist/network.d.ts.map +1 -0
  162. package/dist/network.js +199 -0
  163. package/dist/network.js.map +1 -0
  164. package/dist/sdk/invoke.d.ts +23 -4
  165. package/dist/sdk/invoke.d.ts.map +1 -1
  166. package/dist/sdk/invoke.js +71 -10
  167. package/dist/sdk/invoke.js.map +1 -1
  168. package/dist/wallet/autonomous-wallet-broker.d.ts +3 -0
  169. package/dist/wallet/autonomous-wallet-broker.d.ts.map +1 -1
  170. package/dist/wallet/autonomous-wallet-broker.js +27 -23
  171. package/dist/wallet/autonomous-wallet-broker.js.map +1 -1
  172. package/dist/wallet/autonomous-wallet-protocol.d.ts +8 -2
  173. package/dist/wallet/autonomous-wallet-protocol.d.ts.map +1 -1
  174. package/dist/wallet/autonomous-wallet-protocol.js +9 -2
  175. package/dist/wallet/autonomous-wallet-protocol.js.map +1 -1
  176. package/dist/wallet/autonomous-wallet.d.ts.map +1 -1
  177. package/dist/wallet/autonomous-wallet.js +3 -2
  178. package/dist/wallet/autonomous-wallet.js.map +1 -1
  179. package/dist/wallet/broker-escrows.d.ts +222 -0
  180. package/dist/wallet/broker-escrows.d.ts.map +1 -0
  181. package/dist/wallet/broker-escrows.js +36 -0
  182. package/dist/wallet/broker-escrows.js.map +1 -0
  183. package/dist/wallet/lifecycle.d.ts +21 -5
  184. package/dist/wallet/lifecycle.d.ts.map +1 -1
  185. package/dist/wallet/lifecycle.js +32 -12
  186. package/dist/wallet/lifecycle.js.map +1 -1
  187. package/dist/wallet/privy-broker-server.d.ts.map +1 -1
  188. package/dist/wallet/privy-broker-server.js +31 -26
  189. package/dist/wallet/privy-broker-server.js.map +1 -1
  190. package/dist/wallet/privy-session-broker.d.ts +14 -5
  191. package/dist/wallet/privy-session-broker.d.ts.map +1 -1
  192. package/dist/wallet/privy-session-broker.js +29 -17
  193. package/dist/wallet/privy-session-broker.js.map +1 -1
  194. package/dist/wallet/privy-session-provider.d.ts +3 -1
  195. package/dist/wallet/privy-session-provider.d.ts.map +1 -1
  196. package/dist/wallet/privy-session-provider.js +5 -3
  197. package/dist/wallet/privy-session-provider.js.map +1 -1
  198. package/dist/wallet/provider.d.ts.map +1 -1
  199. package/dist/wallet/provider.js +15 -8
  200. package/dist/wallet/provider.js.map +1 -1
  201. package/dist/x402/facilitator.d.ts +42 -17
  202. package/dist/x402/facilitator.d.ts.map +1 -1
  203. package/dist/x402/facilitator.js +49 -33
  204. package/dist/x402/facilitator.js.map +1 -1
  205. package/dist/x402-buyer/guarded-fetch.d.ts +51 -0
  206. package/dist/x402-buyer/guarded-fetch.d.ts.map +1 -0
  207. package/dist/x402-buyer/guarded-fetch.js +147 -0
  208. package/dist/x402-buyer/guarded-fetch.js.map +1 -0
  209. package/dist/x402-buyer/index.d.ts +10 -0
  210. package/dist/x402-buyer/index.d.ts.map +1 -0
  211. package/dist/x402-buyer/index.js +10 -0
  212. package/dist/x402-buyer/index.js.map +1 -0
  213. package/dist/x402-buyer/pay.d.ts +135 -0
  214. package/dist/x402-buyer/pay.d.ts.map +1 -0
  215. package/dist/x402-buyer/pay.js +336 -0
  216. package/dist/x402-buyer/pay.js.map +1 -0
  217. package/dist/x402-opener/abi.d.ts +684 -0
  218. package/dist/x402-opener/abi.d.ts.map +1 -0
  219. package/dist/x402-opener/abi.js +880 -0
  220. package/dist/x402-opener/abi.js.map +1 -0
  221. package/dist/x402-opener/facilitator-entry.d.ts +3 -0
  222. package/dist/x402-opener/facilitator-entry.d.ts.map +1 -0
  223. package/dist/x402-opener/facilitator-entry.js +37 -0
  224. package/dist/x402-opener/facilitator-entry.js.map +1 -0
  225. package/dist/x402-opener/facilitator-server.d.ts +180 -0
  226. package/dist/x402-opener/facilitator-server.d.ts.map +1 -0
  227. package/dist/x402-opener/facilitator-server.js +696 -0
  228. package/dist/x402-opener/facilitator-server.js.map +1 -0
  229. package/dist/x402-opener/index.d.ts +14 -0
  230. package/dist/x402-opener/index.d.ts.map +1 -0
  231. package/dist/x402-opener/index.js +14 -0
  232. package/dist/x402-opener/index.js.map +1 -0
  233. package/dist/x402-opener/listing-resolver.d.ts +55 -0
  234. package/dist/x402-opener/listing-resolver.d.ts.map +1 -0
  235. package/dist/x402-opener/listing-resolver.js +125 -0
  236. package/dist/x402-opener/listing-resolver.js.map +1 -0
  237. package/dist/x402-opener/refund-relay.d.ts +240 -0
  238. package/dist/x402-opener/refund-relay.d.ts.map +1 -0
  239. package/dist/x402-opener/refund-relay.js +1240 -0
  240. package/dist/x402-opener/refund-relay.js.map +1 -0
  241. package/dist/x402-opener/relayer-queue.d.ts +14 -0
  242. package/dist/x402-opener/relayer-queue.d.ts.map +1 -0
  243. package/dist/x402-opener/relayer-queue.js +18 -0
  244. package/dist/x402-opener/relayer-queue.js.map +1 -0
  245. package/dist/x402-opener/scheme.d.ts +217 -0
  246. package/dist/x402-opener/scheme.d.ts.map +1 -0
  247. package/dist/x402-opener/scheme.js +749 -0
  248. package/dist/x402-opener/scheme.js.map +1 -0
  249. package/dist/x402-opener/terms.d.ts +105 -0
  250. package/dist/x402-opener/terms.d.ts.map +1 -0
  251. package/dist/x402-opener/terms.js +212 -0
  252. package/dist/x402-opener/terms.js.map +1 -0
  253. package/dist/zktls/reclaim.d.ts +2 -2
  254. package/docs/agent-layer.md +13 -8
  255. package/docs/autonomous-privy-wallet.md +188 -0
  256. package/docs/cli.md +342 -53
  257. package/docs/langchain.md +5 -0
  258. package/docs/mcp-hosted.md +4 -3
  259. package/docs/mcp-reference.md +245 -0
  260. package/docs/probe-capability.md +191 -0
  261. package/docs/quickstart.md +54 -5
  262. package/docs/threat-model.md +24 -3
  263. package/docs/x402.md +681 -0
  264. package/package.json +19 -6
  265. package/src/capability/invocation-capability.ts +17 -1
  266. package/src/capability/probe-capability.ts +355 -0
  267. package/src/cli/args.ts +29 -0
  268. package/src/cli/commands/claim.ts +31 -5
  269. package/src/cli/commands/close.ts +56 -7
  270. package/src/cli/commands/invoke.ts +6 -2
  271. package/src/cli/commands/mcp.ts +18 -2
  272. package/src/cli/commands/open.ts +151 -14
  273. package/src/cli/commands/pay.ts +442 -0
  274. package/src/cli/commands/reclaim-unverified.ts +24 -6
  275. package/src/cli/commands/reclaim.ts +274 -19
  276. package/src/cli/commands/refund.ts +277 -0
  277. package/src/cli/commands/status.ts +17 -1
  278. package/src/cli/commands/streams.ts +11 -15
  279. package/src/cli/commands/watch.ts +7 -1
  280. package/src/cli/config.ts +139 -52
  281. package/src/cli/deps.ts +189 -25
  282. package/src/cli/errors.ts +24 -1
  283. package/src/cli/escape-hatch.ts +19 -5
  284. package/src/cli/listings.ts +43 -10
  285. package/src/cli/preflight.ts +36 -21
  286. package/src/cli/privy.ts +398 -0
  287. package/src/cli/reclaimability.ts +154 -0
  288. package/src/cli/run.ts +54 -11
  289. package/src/cli/scan-range.ts +2 -2
  290. package/src/cli/spend-policy.ts +139 -0
  291. package/src/cli/status.ts +14 -1
  292. package/src/cli/wallet.ts +189 -58
  293. package/src/discovery/rate-band.ts +172 -0
  294. package/src/escrow-generation.ts +135 -0
  295. package/src/frameworks/agentkit.ts +45 -11
  296. package/src/frameworks/eliza.ts +27 -5
  297. package/src/frameworks/langchain.ts +14 -0
  298. package/src/gateway/caller-auth-gateway.ts +336 -13
  299. package/src/gateway/http-server.ts +690 -7
  300. package/src/gateway/probe-auth.ts +191 -0
  301. package/src/gateway/signed-rate-bounds.ts +164 -0
  302. package/src/gateway/x402-opener-mode.ts +974 -0
  303. package/src/index.ts +112 -14
  304. package/src/mandates/env.ts +3 -2
  305. package/src/mcp/server.ts +973 -44
  306. package/src/mcp/service-invoker.ts +285 -0
  307. package/src/mcp/x402-tools.ts +621 -0
  308. package/src/network.ts +302 -0
  309. package/src/sdk/invoke.ts +96 -17
  310. package/src/wallet/autonomous-wallet-broker.ts +46 -20
  311. package/src/wallet/autonomous-wallet-protocol.ts +11 -2
  312. package/src/wallet/autonomous-wallet.ts +4 -2
  313. package/src/wallet/broker-escrows.ts +66 -0
  314. package/src/wallet/lifecycle.ts +53 -18
  315. package/src/wallet/privy-broker-server.ts +35 -25
  316. package/src/wallet/privy-session-broker.ts +58 -25
  317. package/src/wallet/privy-session-provider.ts +7 -3
  318. package/src/wallet/provider.ts +20 -15
  319. package/src/x402/facilitator.ts +85 -46
  320. package/src/x402-buyer/guarded-fetch.ts +213 -0
  321. package/src/x402-buyer/index.ts +9 -0
  322. package/src/x402-buyer/pay.ts +517 -0
  323. package/src/x402-opener/abi.ts +881 -0
  324. package/src/x402-opener/facilitator-entry.ts +45 -0
  325. package/src/x402-opener/facilitator-server.ts +1014 -0
  326. package/src/x402-opener/index.ts +13 -0
  327. package/src/x402-opener/listing-resolver.ts +202 -0
  328. package/src/x402-opener/refund-relay.ts +1743 -0
  329. package/src/x402-opener/relayer-queue.ts +18 -0
  330. package/src/x402-opener/scheme.ts +1133 -0
  331. package/src/x402-opener/terms.ts +300 -0
@@ -0,0 +1,277 @@
1
+ import type {
2
+ RefundClient,
3
+ RefundRelayResult,
4
+ RefundStatus,
5
+ X402PayerSigner,
6
+ } from "@absol-labs/sdk/x402";
7
+
8
+ import { verifyMandateOwnerSignature } from "../../mandates/mandate.js";
9
+ import { booleanFlag, integerFlag, stringFlag } from "../args.js";
10
+ import { formatUsdc } from "../amounts.js";
11
+ import { assertTestnetRpc, parseBytes32 } from "../config.js";
12
+ import type { CliX402 } from "../deps.js";
13
+ import { CliError, configError, usageError } from "../errors.js";
14
+ import { signRecoveryMandate } from "../mandate.js";
15
+ import { isPrivyWalletSource, resolveCliWallet } from "../wallet.js";
16
+ import type { CommandContext, CommandSpec } from "../context.js";
17
+
18
+ /** Environment fallback for `--refund-url`. */
19
+ export const REFUND_URL_ENV = "METRIK_AGENT_REFUND_URL";
20
+
21
+ const DEFAULT_ATTEMPTS = 5;
22
+ const DEFAULT_MAX_WAIT_SECONDS = 60;
23
+
24
+ export const refundCommand: CommandSpec = {
25
+ name: "refund",
26
+ summary:
27
+ "Recover the unearned part of an x402 payment through the gasless refund relay",
28
+ usage:
29
+ "metrik refund <streamId> --refund-url <url> [--close] [--reclaim] [--attempts 5] [--max-wait 60]",
30
+ flags: ["refund-url", "close", "reclaim", "attempts", "max-wait"],
31
+ // `--close` signs the payer's EIP-712 Close (off-chain); the RELAY broadcasts
32
+ // and pays the gas. `--reclaim` needs no wallet at all: `opener.reclaim` is
33
+ // permissionless and always pays `payerOf(streamId)`.
34
+ signs: false,
35
+ requiresKey: false,
36
+ async run(context: CommandContext): Promise<void> {
37
+ const { args, config, env, out, deps } = context;
38
+ if (deps.x402 === undefined) {
39
+ throw new CliError(
40
+ "this CLI build has no x402 dependency wired (CliDependencies.x402)",
41
+ { exitCode: 4 },
42
+ );
43
+ }
44
+ const x402: CliX402 = deps.x402;
45
+ const [streamIdArg] = args.positionals;
46
+ if (streamIdArg === undefined) {
47
+ throw usageError("metrik refund requires a <streamId>");
48
+ }
49
+ const streamId = parseBytes32(streamIdArg, "<streamId>");
50
+ const refundUrl = resolveRefundUrl(
51
+ stringFlag(args, "refund-url") ?? env[REFUND_URL_ENV],
52
+ streamId,
53
+ );
54
+ const doClose = booleanFlag(args, "close");
55
+ const doReclaim = booleanFlag(args, "reclaim");
56
+ const attempts = integerFlag(args, "attempts") ?? DEFAULT_ATTEMPTS;
57
+ const maxWait = integerFlag(args, "max-wait") ?? DEFAULT_MAX_WAIT_SECONDS;
58
+ if (attempts < 1) throw usageError("--attempts must be at least 1");
59
+
60
+ const read = deps.createReadClient(config);
61
+ await assertTestnetRpc(config, () => read.getChainId());
62
+ // Bind the relay to the stream AS THE CHAIN SEES IT: the opener comes from
63
+ // `factory.isOpener(buyer)` on-chain, never from the relay's answer.
64
+ const stream = await read.getStreamV2(streamId);
65
+ const opener = stream.opener ?? null;
66
+ if (opener === null) {
67
+ throw configError(
68
+ `stream ${streamId} was not opened by an x402 opener, so the refund relay does not serve it`,
69
+ `the buyer is ${stream.buyer}, an ordinary wallet: use \`metrik close ${streamId}\` and \`metrik reclaim ${streamId}\` with that wallet.`,
70
+ );
71
+ }
72
+
73
+ const refunds = await x402.refundClient(config, {
74
+ refundUrl,
75
+ streamId,
76
+ opener,
77
+ maxAttempts: attempts,
78
+ maxDelayMs: maxWait * 1_000,
79
+ });
80
+
81
+ let closeResult: RefundRelayResult | null = null;
82
+ if (doClose) {
83
+ const wallet = await resolveCliWallet(env, {
84
+ purpose: "capability",
85
+ note: out.note,
86
+ rpcUrl: config.rpcUrl,
87
+ });
88
+ if (isPrivyWalletSource(wallet.source)) {
89
+ throw configError(
90
+ "metrik refund --close cannot sign with a Privy wallet",
91
+ "the payer signs the opener's EIP-712 Close, which the Privy broker's typed-data policy does not allow. Use the key that paid.",
92
+ );
93
+ }
94
+ // Buyer recovery: authorized on the mandate OWNER's signature alone, like
95
+ // `metrik close` — never on caps or expiry, so nothing can strand the
96
+ // payer's own money. The SDK then refuses a signer that is not
97
+ // `payerOf(streamId)` before anything is signed for the relay.
98
+ const mandate = await signRecoveryMandate(wallet.account, config.chainId);
99
+ const decision = await verifyMandateOwnerSignature(mandate);
100
+ if (!decision.allowed) {
101
+ throw new CliError(`mandate denied: ${decision.reason}`, {
102
+ exitCode: 3,
103
+ });
104
+ }
105
+ closeResult = await relay(() =>
106
+ refunds.close({
107
+ signer: wallet.account as unknown as X402PayerSigner,
108
+ }),
109
+ );
110
+ out.note(
111
+ `close relayed in ${closeResult.transaction}${closeResult.verified ? " (receipt verified on-chain)" : ""}`,
112
+ );
113
+ }
114
+
115
+ let reclaimResult: RefundRelayResult | null = null;
116
+ if (doReclaim) {
117
+ reclaimResult = await relay(() => refunds.reclaim());
118
+ }
119
+
120
+ const status = await relay(() => refunds.status());
121
+ out.result({
122
+ streamId,
123
+ opener,
124
+ payer: status.payer,
125
+ status: status.status,
126
+ reclaimed: status.reclaimed,
127
+ reclaimable: status.reclaimable,
128
+ next: status.next,
129
+ close:
130
+ closeResult === null
131
+ ? null
132
+ : {
133
+ transaction: closeResult.transaction,
134
+ verified: closeResult.verified,
135
+ },
136
+ reclaim:
137
+ reclaimResult === null
138
+ ? null
139
+ : {
140
+ transaction: reclaimResult.transaction,
141
+ amount: reclaimResult.amount,
142
+ verified: reclaimResult.verified,
143
+ },
144
+ refundIsAutomatic: false,
145
+ });
146
+ out.print(`x402 stream ${streamId}`);
147
+ out.table(statusRows(status, closeResult, reclaimResult));
148
+ out.print("");
149
+ out.print(nextStep(status, streamId, refundUrl));
150
+ },
151
+ };
152
+
153
+ /**
154
+ * Accepts the relay's full stream URL, a `{streamId}` template, or the relay's
155
+ * base origin (`https://relay.example` -> `https://relay.example/streams/<id>`).
156
+ */
157
+ export function resolveRefundUrl(
158
+ raw: string | undefined,
159
+ streamId: `0x${string}`,
160
+ ): string {
161
+ if (raw === undefined || raw.trim().length === 0) {
162
+ throw usageError(
163
+ `metrik refund needs the seller's refund relay: pass --refund-url (printed by \`metrik pay\`) or set ${REFUND_URL_ENV}`,
164
+ );
165
+ }
166
+ const value = raw.trim();
167
+ if (value.includes("{streamId}")) return value;
168
+ let url: URL;
169
+ try {
170
+ url = new URL(value);
171
+ } catch {
172
+ throw usageError(
173
+ `--refund-url must be an absolute http(s) URL (got "${value}")`,
174
+ );
175
+ }
176
+ if (/\/streams\/0x[0-9a-fA-F]{64}\/?$/.test(url.pathname)) return value;
177
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/streams/${streamId}`;
178
+ return url.toString();
179
+ }
180
+
181
+ async function relay<T>(task: () => Promise<T>): Promise<T> {
182
+ try {
183
+ return await task();
184
+ } catch (error) {
185
+ throw explainRefundFailure(error);
186
+ }
187
+ }
188
+
189
+ export function explainRefundFailure(error: unknown): unknown {
190
+ if (!(error instanceof Error)) return error;
191
+ switch (error.name) {
192
+ case "RefundNotReadyError": {
193
+ const retryAt = (error as { retryAt?: number | null }).retryAt;
194
+ return new CliError(error.message, {
195
+ exitCode: 3,
196
+ hint:
197
+ retryAt === undefined || retryAt === null
198
+ ? "the relay is not ready to reclaim yet (the settlement window is open). Re-run later with --reclaim."
199
+ : `the relay can reclaim from ${new Date(retryAt * 1000).toISOString()}. Re-run then with --reclaim (or raise --max-wait to wait here).`,
200
+ cause: error,
201
+ });
202
+ }
203
+ case "RefundRelayError":
204
+ return new CliError(error.message, {
205
+ exitCode: 3,
206
+ hint: `relay refusal${(error as { code?: string }).code === undefined ? "" : ` (${(error as { code?: string }).code})`}. The payer can always recover on-chain without the relay: \`metrik close\` (as the payer, needs gas) and \`metrik reclaim\` (anyone).`,
207
+ cause: error,
208
+ });
209
+ case "NotStreamPayerError":
210
+ return new CliError(error.message, {
211
+ exitCode: 3,
212
+ hint: "only the address that paid (payerOf(streamId)) can sign the close. Nothing was signed.",
213
+ cause: error,
214
+ });
215
+ default:
216
+ return error;
217
+ }
218
+ }
219
+
220
+ function statusRows(
221
+ status: RefundStatus,
222
+ closeResult: RefundRelayResult | null,
223
+ reclaimResult: RefundRelayResult | null,
224
+ ): (readonly [string, string])[] {
225
+ const rows: (readonly [string, string])[] = [
226
+ [
227
+ "status",
228
+ status.reclaimed ? `${status.status} (reclaimed)` : status.status,
229
+ ],
230
+ ["payer", status.payer],
231
+ ["opener", status.opener],
232
+ [
233
+ "reclaimable",
234
+ status.reclaimable === null ? "-" : formatUsdc(status.reclaimable),
235
+ ],
236
+ ["next", status.next.action],
237
+ ];
238
+ if (closeResult !== null) {
239
+ rows.push([
240
+ "close tx",
241
+ `${closeResult.transaction}${closeResult.verified ? " (verified)" : " (relay-reported)"}`,
242
+ ]);
243
+ }
244
+ if (reclaimResult !== null) {
245
+ rows.push([
246
+ "reclaim tx",
247
+ `${reclaimResult.transaction}${reclaimResult.verified ? " (verified)" : " (relay-reported)"}`,
248
+ ]);
249
+ if (reclaimResult.amount !== null) {
250
+ rows.push(["refunded to payer", formatUsdc(reclaimResult.amount)]);
251
+ }
252
+ }
253
+ return rows;
254
+ }
255
+
256
+ function nextStep(
257
+ status: RefundStatus,
258
+ streamId: `0x${string}`,
259
+ refundUrl: string,
260
+ ): string {
261
+ if (status.reclaimed) {
262
+ return "Reclaimed: the unearned remainder was returned to the payer.";
263
+ }
264
+ const base = `metrik refund ${streamId} --refund-url ${refundUrl}`;
265
+ switch (status.next.action) {
266
+ case "close":
267
+ return `Refunds are NOT automatic. The stream is still running: stop it with ${base} --close --reclaim`;
268
+ case "reclaim":
269
+ return `Refunds are NOT automatic. Reclaim the unearned remainder with ${base} --reclaim`;
270
+ case "wait":
271
+ return `Refunds are NOT automatic, and the settlement window is still open${status.next.retryAt === undefined ? "" : ` until ${new Date(status.next.retryAt * 1000).toISOString()}`}. Then run ${base} --reclaim`;
272
+ default:
273
+ return `Refunds are NOT automatic. The relay says: ${status.next.message ?? status.next.action}.`;
274
+ }
275
+ }
276
+
277
+ export type { RefundClient };
@@ -7,6 +7,7 @@ import {
7
7
  streamReportRows,
8
8
  } from "../status.js";
9
9
  import type { CommandContext, CommandSpec } from "../context.js";
10
+ import { escrowGenerationFor } from "../../escrow-generation.js";
10
11
 
11
12
  export const statusCommand: CommandSpec = {
12
13
  name: "status",
@@ -28,7 +29,12 @@ export const statusCommand: CommandSpec = {
28
29
  // diverge from index 5 onward, so a wrong-chain read does not revert — it
29
30
  // silently decodes something else into fields with the right names.
30
31
  await assertTestnetRpc(config, () => read.getChainId());
31
- const report = await readStreamReport(read, streamId, deps.nowSeconds());
32
+ const report = await readStreamReport(
33
+ read,
34
+ streamId,
35
+ deps.nowSeconds(),
36
+ escrowGenerationFor(config.chainId, config.escrow),
37
+ );
32
38
 
33
39
  out.result(streamReportJson(report));
34
40
  // Who attests, before the numbers. Printed on stderr so it survives `--json`
@@ -40,6 +46,16 @@ export const statusCommand: CommandSpec = {
40
46
  if (disclosure !== null) out.note(disclosure);
41
47
  out.print(`Stream ${streamId}`);
42
48
  out.table(streamReportRows(report));
49
+ const opener = report.stream.opener ?? null;
50
+ if (opener !== null) {
51
+ out.print("");
52
+ out.print(
53
+ `Bought over x402: the escrow accepts close/reclaim only from the opener ${opener}. ` +
54
+ `\`metrik close ${streamId}\` (signed by the payer ${report.stream.payer}) routes through it; ` +
55
+ `\`metrik reclaim ${streamId}\` sends the permissionless opener.reclaim, and the refund always goes to the payer. ` +
56
+ "A payer with no gas can use the seller's relay instead: `metrik refund <streamId> --refund-url <url> --close --reclaim`. Nothing refunds automatically.",
57
+ );
58
+ }
43
59
  if (report.settlement.pending) {
44
60
  out.print("");
45
61
  out.print(
@@ -1,11 +1,11 @@
1
- import { BASE_SEPOLIA_ESCROW_V2 } from "@absol-labs/sdk";
2
1
  import { getAddress, isAddress } from "viem";
3
2
 
4
3
  import { integerFlag, stringFlag } from "../args.js";
5
4
  import { formatUsdc } from "../amounts.js";
6
5
  import {
7
- BASE_SEPOLIA_ESCROW_V2_DEPLOY_BLOCK,
8
6
  assertTestnetRpc,
7
+ escrowDeployBlock,
8
+ type CliConfig,
9
9
  } from "../config.js";
10
10
  import { CliError, configError, usageError } from "../errors.js";
11
11
  import { escrowScanRange } from "../scan-range.js";
@@ -32,7 +32,7 @@ const SCAN_CONCURRENCY = 6;
32
32
  /**
33
33
  * Attempts per window before it is reported as unscanned. A public RPC answers
34
34
  * a burst of `eth_getLogs` with an occasional 429 — measured against
35
- * `sepolia.base.org`, 2 of 212 windows on a full-history scan — and a transient
35
+ * the network's public RPC, 2 of 212 windows on a full-history scan — and a transient
36
36
  * rate-limit is NOT a window that cannot be scanned. Declaring it one would
37
37
  * make the "this scan is incomplete" warning routine, and a warning that fires
38
38
  * on every healthy run is a warning nobody reads on the run that matters.
@@ -71,7 +71,7 @@ export const streamsCommand: CommandSpec = {
71
71
  // else into fields with the right names.
72
72
  await assertTestnetRpc(config, () => read.getChainId());
73
73
 
74
- const anchor = resolveAnchorBlock(args, config.escrow);
74
+ const anchor = resolveAnchorBlock(args, config);
75
75
  const head = await read.blockNumber();
76
76
  const range = escrowScanRange(head, anchor.block);
77
77
 
@@ -234,7 +234,7 @@ async function resolveScanAddress(
234
234
  if (wallet === null) {
235
235
  throw configError(
236
236
  "metrik streams needs a wallet to know whose streams to list",
237
- `pass --address 0x… to list any address's streams without one, or set ${PRIVATE_KEY_ENV} (or the CDP variables).`,
237
+ `pass --address 0x… to list any address's streams without one, or set ${PRIVATE_KEY_ENV} (or the CDP or Privy variables).`,
238
238
  );
239
239
  }
240
240
  return wallet.account.address;
@@ -251,25 +251,21 @@ async function resolveScanAddress(
251
251
  */
252
252
  function resolveAnchorBlock(
253
253
  args: CommandContext["args"],
254
- escrow: `0x${string}`,
254
+ config: CliConfig,
255
255
  ): { readonly block: bigint; readonly canary: bigint | null } {
256
256
  const override = integerFlag(args, "from-block");
257
- const isKnownEscrow =
258
- escrow.toLowerCase() === BASE_SEPOLIA_ESCROW_V2.toLowerCase();
257
+ const deployBlock = escrowDeployBlock(config);
259
258
 
260
259
  if (override !== undefined) {
261
260
  return { block: BigInt(override), canary: null };
262
261
  }
263
- if (!isKnownEscrow) {
262
+ if (deployBlock === undefined) {
264
263
  throw configError(
265
- `no scan anchor is known for escrow ${escrow}`,
266
- "the scan is anchored at the escrow's deploy block, and this CLI only ships that block for its default Base Sepolia escrow. Pass --from-block <deploy block> for a different escrow.",
264
+ `no scan anchor is known for escrow ${config.escrow}`,
265
+ `the scan is anchored at the escrow's deploy block, and the network registry only records it for ${config.network.name}'s registered escrow (${config.network.escrow.address}). Pass --from-block <deploy block> for a different escrow.`,
267
266
  );
268
267
  }
269
- return {
270
- block: BASE_SEPOLIA_ESCROW_V2_DEPLOY_BLOCK,
271
- canary: BASE_SEPOLIA_ESCROW_V2_DEPLOY_BLOCK,
272
- };
268
+ return { block: deployBlock, canary: deployBlock };
273
269
  }
274
270
 
275
271
  /**
@@ -7,6 +7,7 @@ import {
7
7
  streamReportRows,
8
8
  } from "../status.js";
9
9
  import type { CommandContext, CommandSpec } from "../context.js";
10
+ import { escrowGenerationFor } from "../../escrow-generation.js";
10
11
 
11
12
  const DEFAULT_INTERVAL_SECONDS = 15;
12
13
 
@@ -43,7 +44,12 @@ export const watchCommand: CommandSpec = {
43
44
 
44
45
  let report;
45
46
  try {
46
- report = await readStreamReport(read, streamId, deps.nowSeconds());
47
+ report = await readStreamReport(
48
+ read,
49
+ streamId,
50
+ deps.nowSeconds(),
51
+ escrowGenerationFor(config.chainId, config.escrow),
52
+ );
47
53
  } catch (error) {
48
54
  // A transient RPC blip must not end a long watch.
49
55
  out.note(`read failed, retrying: ${describeError(error)}`);
package/src/cli/config.ts CHANGED
@@ -1,76 +1,97 @@
1
- import {
2
- BASE_SEPOLIA_ESCROW_V2,
3
- BASE_SEPOLIA_USDC,
4
- DEFAULT_ORACLE_BASE_URL,
5
- DEFAULT_PUBLIC_REGISTRY_URL,
6
- } from "@absol-labs/sdk";
7
- import { isAddress, getAddress } from "viem";
8
- import { baseSepolia } from "viem/chains";
1
+ import type { MetrikNetwork } from "@absol-labs/sdk";
2
+ import { isAddress, getAddress, type Chain } from "viem";
9
3
 
4
+ import {
5
+ allowedNetworks,
6
+ describeAllowedNetworks,
7
+ registeredDeployment,
8
+ METRIK_NETWORK_ENV,
9
+ resolveAgentNetwork,
10
+ UnsupportedNetworkError,
11
+ } from "../network.js";
10
12
  import { CliError, configError, usageError } from "./errors.js";
11
13
  import { stringFlag, integerFlag, type ParsedArgs } from "./args.js";
12
14
 
13
15
  /**
14
- * TESTNET ONLY — the single allowlist this CLI will settle against.
16
+ * TESTNET ONLY — the CLI settles only on registered networks the SDK registry
17
+ * marks `testnet: true`.
15
18
  *
16
19
  * Metrik's escrow moves real USDC the moment it is pointed at a mainnet chain,
17
20
  * and this CLI signs with a raw private key read from the environment. So the
18
- * chain is an allowlist, not a default: any other chain id is refused outright,
19
- * with no override flag. There is deliberately no `--allow-mainnet` — an escape
20
- * hatch that re-enables real funds would defeat the guarantee, and a mainnet
21
- * deployment is not something a flag can conjure (there is none).
21
+ * chain is an allowlist, not a default: a chain the registry does not know, or
22
+ * one it knows as a mainnet, is refused outright, with no override flag. There
23
+ * is deliberately no `--allow-mainnet` — an escape hatch that re-enables real
24
+ * funds would defeat the guarantee.
22
25
  *
23
- * The allowlist is enforced TWICE: once against the configured/declared chain
24
- * id, and again against the chain id the RPC endpoint actually reports before
25
- * any fund-moving command signs anything (see `assertTestnetRpc`). The second
26
- * check is what stops `--chain-id 84532 --rpc-url <a mainnet RPC>`.
26
+ * The allowlist is enforced TWICE: once against the configured/declared network,
27
+ * and again against the chain id the RPC endpoint actually reports before any
28
+ * fund-moving command signs anything (see `assertTestnetRpc`). The second check
29
+ * is what stops a testnet `--chain-id` paired with a mainnet `--rpc-url`.
27
30
  */
28
- export const TESTNET_CHAIN_IDS: readonly number[] = [baseSepolia.id];
31
+ export const CLI_NETWORK_POLICY = { testnetOnly: true } as const;
32
+
33
+ /** Chain ids of every registered testnet this CLI will settle against. */
34
+ export function testnetChainIds(): number[] {
35
+ return allowedNetworks(CLI_NETWORK_POLICY).map((network) => network.chain.id);
36
+ }
29
37
 
30
38
  /**
31
- * The block `BASE_SEPOLIA_ESCROW_V2` was deployed at — the anchor of every
32
- * escrow log scan, because the contract has no history before it.
33
- *
34
- * Verified against Base Sepolia on 2026-09-17: `eth_getLogs` for this single
35
- * block returns 6 escrow logs (the deployment's own events). History is
36
- * immutable, so that is a stable known answer forever, which is what lets the
37
- * same block serve as the log-retention canary in `metrik streams`.
38
- *
39
- * It is tied to that ONE address. A run pointed at a different `--escrow` has
40
- * no anchor and must be given `--from-block`, rather than silently scanning
41
- * from a block that means nothing for it.
39
+ * @deprecated Snapshot of {@link testnetChainIds} at import time. A network
40
+ * registered later is not in it; call `testnetChainIds()` instead.
42
41
  */
43
- export const BASE_SEPOLIA_ESCROW_V2_DEPLOY_BLOCK = 45_892_018n;
42
+ export const TESTNET_CHAIN_IDS: readonly number[] = testnetChainIds();
44
43
 
45
44
  export interface CliConfig {
46
- readonly chain: typeof baseSepolia;
45
+ /** The registered network this run settles on (from the SDK registry). */
46
+ readonly network: MetrikNetwork;
47
+ readonly chain: Chain;
47
48
  readonly chainId: number;
48
49
  readonly rpcUrl: string;
49
50
  readonly escrow: `0x${string}`;
50
51
  readonly usdc: `0x${string}`;
51
52
  readonly oracleBaseUrl: string;
52
53
  readonly registryUrl: string;
54
+ /**
55
+ * Set ONLY for an `--escrow` the registry does not record, from the explicit
56
+ * `--escrow-generation`: the SDK then opts out of its code-hash pin
57
+ * (`allowUnregisteredEscrow`). A registered escrow's generation comes from
58
+ * the registry.
59
+ */
60
+ readonly unregisteredEscrowGeneration?: "v2" | "v3";
61
+ }
62
+
63
+ /**
64
+ * The block the configured escrow was deployed at — the anchor of every escrow
65
+ * log scan — or `undefined` when the escrow is not the network's registered
66
+ * escrow (a run pointed at a different `--escrow` has no anchor and must be
67
+ * given `--from-block`, rather than silently scanning from a block that means
68
+ * nothing for it).
69
+ */
70
+ export function escrowDeployBlock(config: CliConfig): bigint | undefined {
71
+ return registeredDeployment(config.network, config.escrow)?.escrow
72
+ .deployBlock;
53
73
  }
54
74
 
55
75
  /**
56
- * Resolves settlement configuration from flags, then env, then the baked-in
57
- * Base Sepolia defaults — so `metrik discover` works with nothing configured at
76
+ * Resolves settlement configuration from flags, then env, then the network's
77
+ * registry defaults — so `metrik discover` works with nothing configured at
58
78
  * all, while every value stays overridable.
79
+ *
80
+ * The network is selected by `--network` / `METRIK_NETWORK` (registry id,
81
+ * chain id or CAIP-2 id) or by `--chain-id` / `METRIK_AGENT_CHAIN_ID`; giving
82
+ * both is allowed only when they agree.
59
83
  */
60
84
  export function resolveConfig(
61
85
  args: ParsedArgs,
62
86
  env: NodeJS.ProcessEnv,
63
87
  ): CliConfig {
64
- const chainId =
65
- integerFlag(args, "chain-id") ??
66
- parseEnvChainId(env.METRIK_AGENT_CHAIN_ID) ??
67
- baseSepolia.id;
68
- assertTestnetChainId(chainId, "the configured chain id");
88
+ const network = resolveCliNetwork(args, env);
89
+ const chainId = network.chain.id;
69
90
 
70
91
  const rpcUrl =
71
92
  stringFlag(args, "rpc-url") ??
72
93
  env.METRIK_AGENT_RPC_URL ??
73
- baseSepolia.rpcUrls.default.http[0];
94
+ network.rpcUrls[0];
74
95
  if (rpcUrl === undefined || !isHttpUrl(rpcUrl)) {
75
96
  throw configError(
76
97
  `RPC URL must be an http(s) URL (got "${rpcUrl ?? "undefined"}")`,
@@ -78,35 +99,101 @@ export function resolveConfig(
78
99
  );
79
100
  }
80
101
 
102
+ const escrow = resolveAddress(
103
+ stringFlag(args, "escrow") ?? env.METRIK_AGENT_ESCROW,
104
+ network.escrow.address,
105
+ "escrow",
106
+ );
107
+ const declared = (
108
+ stringFlag(args, "escrow-generation") ?? env.METRIK_AGENT_ESCROW_GENERATION
109
+ )
110
+ ?.trim()
111
+ .toLowerCase();
112
+ if (declared !== undefined && declared !== "v2" && declared !== "v3") {
113
+ throw configError(
114
+ `--escrow-generation must be "v2" or "v3" (got "${declared}")`,
115
+ );
116
+ }
117
+ const deployment = registeredDeployment(network, escrow);
118
+ if (deployment === undefined && declared === undefined) {
119
+ throw configError(
120
+ `escrow ${escrow} is not a registered deployment of ${network.name}`,
121
+ `the SDK pins registered escrows by code hash; for your own deployment pass --escrow-generation v2|v3 (it then only checks the interface, which any contract can fake). Registered: ${[network.escrow, ...network.legacy.map((d) => d.escrow)].map((e) => `${e.address} (${e.generation})`).join(", ")}.`,
122
+ );
123
+ }
124
+ if (
125
+ deployment !== undefined &&
126
+ declared !== undefined &&
127
+ declared !== deployment.escrow.generation
128
+ ) {
129
+ throw configError(
130
+ `--escrow-generation ${declared} contradicts the registry: ${escrow} is the ${deployment.escrow.generation} escrow`,
131
+ );
132
+ }
133
+
81
134
  return {
82
- chain: baseSepolia,
135
+ network,
136
+ chain: network.chain,
83
137
  chainId,
84
138
  rpcUrl,
85
- escrow: resolveAddress(
86
- stringFlag(args, "escrow") ?? env.METRIK_AGENT_ESCROW,
87
- BASE_SEPOLIA_ESCROW_V2,
88
- "escrow",
89
- ),
139
+ escrow,
140
+ ...(deployment === undefined
141
+ ? { unregisteredEscrowGeneration: declared as "v2" | "v3" }
142
+ : {}),
90
143
  usdc: resolveAddress(
91
144
  stringFlag(args, "usdc") ?? env.METRIK_AGENT_USDC,
92
- BASE_SEPOLIA_USDC,
145
+ network.usdc.address,
93
146
  "usdc",
94
147
  ),
95
148
  oracleBaseUrl: resolveHttpUrl(
96
149
  stringFlag(args, "oracle-url") ?? env.METRIK_AGENT_ORACLE_URL,
97
- DEFAULT_ORACLE_BASE_URL,
150
+ network.oracleBaseUrl,
98
151
  "oracle-url",
99
152
  ),
100
153
  registryUrl: resolveHttpUrl(
101
154
  stringFlag(args, "registry-url") ?? env.METRIK_AGENT_REGISTRY_URL,
102
- DEFAULT_PUBLIC_REGISTRY_URL,
155
+ network.publicRegistryUrl ?? `${network.oracleBaseUrl}/listings`,
103
156
  "registry-url",
104
157
  ),
105
158
  };
106
159
  }
107
160
 
161
+ function resolveCliNetwork(
162
+ args: ParsedArgs,
163
+ env: NodeJS.ProcessEnv,
164
+ ): MetrikNetwork {
165
+ const chainId =
166
+ integerFlag(args, "chain-id") ?? parseEnvChainId(env.METRIK_AGENT_CHAIN_ID);
167
+ const selector = stringFlag(args, "network") ?? env[METRIK_NETWORK_ENV];
168
+ if (chainId !== undefined)
169
+ assertTestnetChainId(chainId, "the configured chain id");
170
+ let network: MetrikNetwork;
171
+ try {
172
+ network = resolveAgentNetwork(
173
+ selector !== undefined && selector.trim().length > 0 ? selector : chainId,
174
+ CLI_NETWORK_POLICY,
175
+ );
176
+ } catch (error) {
177
+ if (error instanceof UnsupportedNetworkError) {
178
+ throw configError(
179
+ `refusing to run against network "${error.selector}": it is not a supported testnet`,
180
+ `${error.message}. There is no override flag.`,
181
+ );
182
+ }
183
+ throw error;
184
+ }
185
+ if (chainId !== undefined && chainId !== network.chain.id) {
186
+ throw configError(
187
+ `--chain-id ${chainId} does not match --network ${network.id} (chain ${network.chain.id})`,
188
+ "give one of them, or make them agree.",
189
+ );
190
+ }
191
+ return network;
192
+ }
108
193
  /** Flags every command accepts for settlement configuration. */
109
194
  export const CONFIG_FLAGS: readonly string[] = [
195
+ "network",
196
+ "escrow-generation",
110
197
  "chain-id",
111
198
  "rpc-url",
112
199
  "escrow",
@@ -119,12 +206,12 @@ export const CONFIG_FLAGS: readonly string[] = [
119
206
  export const GLOBAL_FLAGS: readonly string[] = ["json", "help"];
120
207
 
121
208
  export function assertTestnetChainId(chainId: number, source: string): void {
122
- if (TESTNET_CHAIN_IDS.includes(chainId)) return;
209
+ if (testnetChainIds().includes(chainId)) return;
123
210
  throw configError(
124
211
  `refusing to run against chain ${chainId}: ${source} is not a supported testnet`,
125
- `this CLI signs with a raw private key and settles real value, so it is testnet-only. Supported: ${TESTNET_CHAIN_IDS.join(
126
- ", ",
127
- )} (Base Sepolia). There is no override flag.`,
212
+ `this CLI signs with a raw private key and settles real value, so it is testnet-only. Supported (registered testnets): ${describeAllowedNetworks(
213
+ CLI_NETWORK_POLICY,
214
+ )}. There is no override flag.`,
128
215
  );
129
216
  }
130
217