@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
package/src/cli/deps.ts CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  type StreamV2,
9
9
  type StreamProofTransactionResult,
10
10
  } from "@absol-labs/sdk";
11
- import { erc20Abi, getAbiItem, http, type LocalAccount } from "viem";
11
+ import { erc20Abi, getAbiItem, http, type Transport } from "viem";
12
12
 
13
13
  import {
14
14
  discoverServicesDetailed,
@@ -30,8 +30,15 @@ import {
30
30
  type ReclaimAuthorizedStreamInput,
31
31
  type ReclaimVerifiedStreamResult,
32
32
  } from "../sdk/client.js";
33
+ import type { SignedSpendMandate } from "../mandates/mandate.js";
34
+ import type {
35
+ PayX402WithMandateInput,
36
+ PayX402WithMandateResult,
37
+ } from "../x402-buyer/pay.js";
38
+ import type { RefundClient } from "@absol-labs/sdk/x402";
33
39
 
34
40
  import type { CliConfig } from "./config.js";
41
+ import type { CliSignerAccount } from "./wallet.js";
35
42
  import { RPC_RETRY_COUNT, RPC_TIMEOUT_MS, withReadTimeout } from "./timeout.js";
36
43
 
37
44
  /**
@@ -149,17 +156,39 @@ export interface OpenedStream {
149
156
  readonly blockNumber: bigint;
150
157
  }
151
158
 
159
+ /**
160
+ * A fund-moving call, with the mandate made EXPLICITLY optional.
161
+ *
162
+ * `null` is not "no authorization" — it is "this wallet's ceiling is enforced
163
+ * by a broker policy the client cannot reach or raise, so a locally self-signed
164
+ * mandate would be the agent attesting to its own limits on top of a stronger
165
+ * limit". The CLI substitutes the real ceiling instead; see
166
+ * `./spend-policy.ts`, which also explains why re-adding the mandate here would
167
+ * be a regression rather than a tidy-up.
168
+ *
169
+ * A key-holding wallet (private key, CDP) always passes a real mandate, and its
170
+ * path through `VerifiedStreamAgentClient` is unchanged.
171
+ */
172
+ type MandateOptional<T extends { readonly signedMandate: SignedSpendMandate }> =
173
+ Omit<T, "signedMandate"> & {
174
+ readonly signedMandate: SignedSpendMandate | null;
175
+ };
176
+
177
+ export type CliOpenStreamInput = MandateOptional<OpenVerifiedStreamInput>;
178
+ export type CliStreamActionInput =
179
+ MandateOptional<MandateAuthorizedStreamActionInput>;
180
+ export type CliReclaimStreamInput =
181
+ MandateOptional<ReclaimAuthorizedStreamInput>;
182
+
152
183
  export interface CliWriteClient {
153
184
  /** Build + best-effort simulate the hire WITHOUT broadcasting (`--dry-run`). */
154
185
  buildHireTx(input: HireComputeInput): Promise<HireTxPreview>;
155
- openVerifiedStream(
156
- input: OpenVerifiedStreamInput,
157
- ): Promise<HireComputeResult>;
186
+ openVerifiedStream(input: CliOpenStreamInput): Promise<HireComputeResult>;
158
187
  closeStream(
159
- input: MandateAuthorizedStreamActionInput,
188
+ input: CliStreamActionInput,
160
189
  ): Promise<StreamProofTransactionResult>;
161
190
  reclaimStream(
162
- input: ReclaimAuthorizedStreamInput,
191
+ input: CliReclaimStreamInput,
163
192
  ): Promise<ReclaimVerifiedStreamResult>;
164
193
  /**
165
194
  * The no-proof escape hatch. Separate from {@link reclaimStream} because it is
@@ -167,14 +196,53 @@ export interface CliWriteClient {
167
196
  * — see `VerifiedStreamAgentClient.reclaimUnverifiedStream`.
168
197
  */
169
198
  reclaimUnverifiedStream(
170
- input: MandateAuthorizedStreamActionInput,
199
+ input: CliStreamActionInput,
171
200
  ): Promise<StreamProofTransactionResult>;
172
201
  /** Operator-only: redeem the verified-earned balance for a stream. */
173
202
  claimStream(
174
- input: MandateAuthorizedStreamActionInput,
203
+ input: CliStreamActionInput,
175
204
  ): Promise<StreamProofTransactionResult>;
176
205
  }
177
206
 
207
+ /**
208
+ * Per-wallet write-path overrides.
209
+ *
210
+ * `transport` exists for exactly one reason: a Privy wallet has no local
211
+ * `signTransaction`. Its EIP-1193 provider performs `eth_sendTransaction`
212
+ * itself, so the provider must BE the wallet client's transport rather than the
213
+ * plain `http(--rpc-url)` every local signer uses. Left unset — which is every
214
+ * private-key and CDP run — the transport is unchanged, byte for byte.
215
+ */
216
+ export interface CreateWriteClientOptions {
217
+ readonly transport?: Transport;
218
+ }
219
+
220
+ /**
221
+ * The buyer side of x402 (`metrik pay` / `metrik refund`). Behind the injectable
222
+ * surface like every other side effect, and loaded lazily by default so a CLI
223
+ * run that never pays over x402 never imports `@x402/*`.
224
+ */
225
+ export interface CliX402 {
226
+ /** Mandate-gated `payWithX402` (see `../x402-buyer/pay.ts`). */
227
+ readonly pay: (
228
+ input: PayX402WithMandateInput,
229
+ ) => Promise<PayX402WithMandateResult>;
230
+ /**
231
+ * The SDK's `refundClient`, bound to ONE stream and verifying every relay
232
+ * "success" against the chain this CLI is configured for.
233
+ */
234
+ readonly refundClient: (
235
+ config: CliConfig,
236
+ target: {
237
+ readonly refundUrl: string;
238
+ readonly streamId: `0x${string}`;
239
+ readonly opener?: `0x${string}`;
240
+ readonly maxAttempts?: number;
241
+ readonly maxDelayMs?: number;
242
+ },
243
+ ) => Promise<RefundClient>;
244
+ }
245
+
178
246
  export interface CliDependencies {
179
247
  readonly createReadClient: (config: CliConfig) => CliReadClient;
180
248
  /**
@@ -185,7 +253,8 @@ export interface CliDependencies {
185
253
  */
186
254
  readonly createWriteClient: (
187
255
  config: CliConfig,
188
- account: LocalAccount | null,
256
+ account: CliSignerAccount | null,
257
+ options?: CreateWriteClientOptions,
189
258
  ) => CliWriteClient;
190
259
  readonly discover: (
191
260
  options: DiscoverServicesOptions,
@@ -198,6 +267,8 @@ export interface CliDependencies {
198
267
  readonly createInvokeStreamReader: (config: CliConfig) => InvokeStreamReader;
199
268
  readonly nowSeconds: () => number;
200
269
  readonly sleep: (ms: number) => Promise<void>;
270
+ /** Optional so an embedder's existing dependency object keeps compiling. */
271
+ readonly x402?: CliX402;
201
272
  }
202
273
 
203
274
  /**
@@ -218,21 +289,38 @@ const STREAM_OPENED_EVENT = getAbiItem({
218
289
  * with nothing on screen. Bounding it here bounds every read and every
219
290
  * simulation the SDK performs, in one place.
220
291
  */
221
- function rpcTransport(config: CliConfig) {
292
+ export function rpcTransport(config: CliConfig) {
222
293
  return http(config.rpcUrl, {
223
294
  timeout: RPC_TIMEOUT_MS,
224
295
  retryCount: RPC_RETRY_COUNT,
225
296
  });
226
297
  }
227
298
 
299
+ /**
300
+ * An `--escrow` the registry does not record was declared with
301
+ * `--escrow-generation`: the only case the SDK may skip its code-hash pin.
302
+ */
303
+ function unregisteredEscrowOptions(config: CliConfig): {
304
+ readonly allowUnregisteredEscrow?: true;
305
+ readonly escrowGeneration?: "v2" | "v3";
306
+ } {
307
+ return config.unregisteredEscrowGeneration === undefined
308
+ ? {}
309
+ : {
310
+ allowUnregisteredEscrow: true,
311
+ escrowGeneration: config.unregisteredEscrowGeneration,
312
+ };
313
+ }
314
+
228
315
  /** Shared `MetrikClient` construction: one escrow, declared as V2 on both keys. */
229
316
  function createMetrikClient(
230
317
  config: CliConfig,
231
- account?: LocalAccount,
318
+ account?: CliSignerAccount,
319
+ transport?: Transport,
232
320
  ): MetrikClient {
233
321
  return new MetrikClient({
234
322
  chain: config.chain,
235
- transport: rpcTransport(config),
323
+ transport: transport ?? rpcTransport(config),
236
324
  escrow: config.escrow,
237
325
  // Declared on BOTH keys: without `escrowV2` the SDK routes reads and
238
326
  // settlement through the legacy V1 ABI, which reverts against
@@ -242,6 +330,7 @@ function createMetrikClient(
242
330
  usdc: config.usdc,
243
331
  oracleBaseUrl: config.oracleBaseUrl,
244
332
  settlementTarget: { chainId: config.chainId, escrow: config.escrow },
333
+ ...unregisteredEscrowOptions(config),
245
334
  ...(account === undefined ? {} : { account }),
246
335
  });
247
336
  }
@@ -365,15 +454,19 @@ export const defaultCliDependencies: CliDependencies = {
365
454
  },
366
455
  };
367
456
  },
368
- createWriteClient(config, account) {
457
+ createWriteClient(config, account, options) {
458
+ // A Privy wallet's provider IS the transport (see CreateWriteClientOptions);
459
+ // every other wallet keeps the plain bounded HTTP transport.
460
+ const transport = options?.transport ?? rpcTransport(config);
369
461
  const clientConfig = {
370
462
  chain: config.chain,
371
- transport: rpcTransport(config),
463
+ transport,
372
464
  escrow: config.escrow,
373
465
  escrowV2: config.escrow,
374
466
  usdc: config.usdc,
375
467
  oracleBaseUrl: config.oracleBaseUrl,
376
468
  settlementTarget: { chainId: config.chainId, escrow: config.escrow },
469
+ ...unregisteredEscrowOptions(config),
377
470
  ...(account === null ? {} : { account }),
378
471
  };
379
472
  // The mandate-gated agent client owns every fund-moving call; the raw SDK
@@ -382,26 +475,61 @@ export const defaultCliDependencies: CliDependencies = {
382
475
  const agentClient = new VerifiedStreamAgentClient(clientConfig);
383
476
  const metrik = createMetrikClient(
384
477
  config,
385
- ...(account === null ? [] : ([account] as const)),
478
+ account === null ? undefined : account,
479
+ transport,
386
480
  );
387
481
  return {
388
482
  async buildHireTx(input) {
389
483
  return await metrik.buildHireTx(input);
390
484
  },
391
- async openVerifiedStream(input) {
392
- return await agentClient.openVerifiedStream(input);
485
+ async openVerifiedStream({ signedMandate, ...input }) {
486
+ // No mandate means a policy-enforced wallet: the ceiling was already
487
+ // checked against the broker's own `/v1/config` before we got here, and
488
+ // the broker checks it AGAIN server-side on the transaction it signs.
489
+ // Going straight at the SDK client is the same path the SDK-driven
490
+ // (non-CLI) flow has always used.
491
+ if (signedMandate === null) {
492
+ return await metrik.hireVerifiedService({
493
+ operator: input.operator,
494
+ serviceRef: input.serviceRef,
495
+ budgetUsdc: input.budgetUsdc,
496
+ ratePerSecond: input.ratePerSecondUsdc,
497
+ maxDurationSeconds: input.maxDurationSeconds,
498
+ });
499
+ }
500
+ return await agentClient.openVerifiedStream({
501
+ ...input,
502
+ signedMandate,
503
+ });
393
504
  },
394
- async closeStream(input) {
395
- return await agentClient.closeStream(input);
505
+ async closeStream({ signedMandate, ...input }) {
506
+ if (signedMandate === null) return await metrik.close(input.streamId);
507
+ return await agentClient.closeStream({ ...input, signedMandate });
396
508
  },
397
- async reclaimStream(input) {
398
- return await agentClient.reclaimStream(input);
509
+ async reclaimStream({ signedMandate, ...input }) {
510
+ if (signedMandate === null) {
511
+ const closeResult = input.closeFirst
512
+ ? await metrik.close(input.streamId)
513
+ : null;
514
+ return {
515
+ closeResult,
516
+ reclaimResult: await metrik.reclaim(input.streamId),
517
+ };
518
+ }
519
+ return await agentClient.reclaimStream({ ...input, signedMandate });
399
520
  },
400
- async reclaimUnverifiedStream(input) {
401
- return await agentClient.reclaimUnverifiedStream(input);
521
+ async reclaimUnverifiedStream({ signedMandate, ...input }) {
522
+ if (signedMandate === null) {
523
+ return await metrik.reclaimUnverified(input.streamId);
524
+ }
525
+ return await agentClient.reclaimUnverifiedStream({
526
+ ...input,
527
+ signedMandate,
528
+ });
402
529
  },
403
- async claimStream(input) {
404
- return await agentClient.claimStream(input);
530
+ async claimStream({ signedMandate, ...input }) {
531
+ if (signedMandate === null) return await metrik.claim(input.streamId);
532
+ return await agentClient.claimStream({ ...input, signedMandate });
405
533
  },
406
534
  };
407
535
  },
@@ -429,9 +557,45 @@ export const defaultCliDependencies: CliDependencies = {
429
557
  nowSeconds() {
430
558
  return Math.floor(Date.now() / 1000);
431
559
  },
560
+ x402: {
561
+ async pay(input) {
562
+ const { payX402WithMandate } = await import("../x402-buyer/pay.js");
563
+ return await payX402WithMandate(input);
564
+ },
565
+ async refundClient(config, target) {
566
+ const { refundClient } = await import("@absol-labs/sdk/x402");
567
+ return refundClient(target.refundUrl, {
568
+ streamId: target.streamId,
569
+ ...(target.opener === undefined ? {} : { opener: target.opener }),
570
+ // Every relay "success" is checked against the receipt on THIS chain.
571
+ verifyWith: createMetrikClient(config),
572
+ expectedChainId: config.chainId,
573
+ ...(target.maxAttempts === undefined
574
+ ? {}
575
+ : { maxAttempts: target.maxAttempts }),
576
+ ...(target.maxDelayMs === undefined
577
+ ? {}
578
+ : { maxDelayMs: target.maxDelayMs }),
579
+ });
580
+ },
581
+ },
432
582
  async sleep(ms) {
433
583
  await new Promise((resolve) => setTimeout(resolve, ms));
434
584
  },
435
585
  };
436
586
 
587
+ /**
588
+ * The write-client options a resolved wallet implies.
589
+ *
590
+ * Kept next to {@link CreateWriteClientOptions} so every fund-moving command
591
+ * spells the transport decision the same way, and so a wallet that needs a
592
+ * provider transport cannot be broadcast through a plain RPC by a command that
593
+ * simply forgot to pass it.
594
+ */
595
+ export function writeClientOptions(
596
+ wallet: { readonly transport?: Transport } | null,
597
+ ): CreateWriteClientOptions {
598
+ return wallet?.transport === undefined ? {} : { transport: wallet.transport };
599
+ }
600
+
437
601
  export type { DiscoverResult, ServiceListing };
package/src/cli/errors.ts CHANGED
@@ -76,6 +76,28 @@ function namesTheRevert(line: string): boolean {
76
76
  );
77
77
  }
78
78
 
79
+ /**
80
+ * Links that state the OUTCOME rather than open a sentence the next link
81
+ * completes — so the trailing "…and it reverted" carries nothing.
82
+ *
83
+ * `metrik status <a stream id that does not exist>` used to print
84
+ *
85
+ * stream 0x…: does not exist on the escrow: The contract function
86
+ * "getStream" reverted.
87
+ *
88
+ * The first link is the whole answer; the tail is viem restating that a revert
89
+ * is how the escrow says so. Contrast "reclaim simulation failed", which is a
90
+ * PREFIX — dropping the boilerplate after it would leave a failure with no
91
+ * stated reason at all, which is why the one-boilerplate rule exists.
92
+ */
93
+ const STATES_THE_OUTCOME: readonly RegExp[] = [
94
+ /\bdoes not exist on the escrow\b/,
95
+ ];
96
+
97
+ function statesTheOutcome(line: string): boolean {
98
+ return STATES_THE_OUTCOME.some((pattern) => pattern.test(line));
99
+ }
100
+
79
101
  function isBoilerplate(line: string): boolean {
80
102
  return VIEM_BOILERPLATE.some((pattern) => pattern.test(line));
81
103
  }
@@ -108,7 +130,8 @@ export function stripRedundantBoilerplate(
108
130
  chain: readonly string[],
109
131
  ): readonly string[] {
110
132
  const decoded = chain.some(
111
- (line) => namesTheRevert(line) && !isBoilerplate(line),
133
+ (line) =>
134
+ (namesTheRevert(line) || statesTheOutcome(line)) && !isBoilerplate(line),
112
135
  );
113
136
  let keptBoilerplate = false;
114
137
  const kept = chain.filter((line, index) => {
@@ -1,5 +1,7 @@
1
1
  import type { StreamV2 } from "@absol-labs/sdk";
2
2
 
3
+ import { escapeHatchReadyAtFormula } from "../escrow-generation.js";
4
+
3
5
  import { formatDuration } from "./amounts.js";
4
6
  import type { CliReadClient } from "./deps.js";
5
7
 
@@ -17,9 +19,11 @@ import type { CliReadClient } from "./deps.js";
17
19
  * — and `escapeGraceSeconds` is a public immutable on the escrow, so readiness
18
20
  * is fully computable before a single wei of gas is spent.
19
21
  *
20
- * Note which timestamp the escrow uses: `expiresAt`, NEVER `closedAt`. Closing a
21
- * stream early does not bring the escape hatch forward, and the CLI must not
22
- * imply that it does.
22
+ * Note which timestamp the escrow uses, which depends on its GENERATION (the
23
+ * SDK registry's `escrow.generation`; see `../escrow-generation.ts`): V2 uses
24
+ * `expiresAt`, NEVER `closedAt` — closing early does not bring the hatch
25
+ * forward, and the CLI must not imply that it does. V3 opens it at
26
+ * `min(expiresAt, closedAt) + escapeGraceSeconds`.
23
27
  */
24
28
  export interface EscapeHatchWindow {
25
29
  /** `escapeGraceSeconds` as read from the escrow. */
@@ -32,11 +36,21 @@ export interface EscapeHatchWindow {
32
36
  }
33
37
 
34
38
  export function escapeHatchWindow(
35
- stream: Pick<StreamV2, "expiresAt">,
39
+ stream: Pick<StreamV2, "expiresAt"> & Partial<Pick<StreamV2, "closedAt">>,
36
40
  graceSeconds: number,
37
41
  nowSeconds: number,
42
+ generation = "v2",
38
43
  ): EscapeHatchWindow {
39
- const readyAt = stream.expiresAt + graceSeconds;
44
+ const readyAt = Number(
45
+ escapeHatchReadyAtFormula(
46
+ generation,
47
+ {
48
+ expiresAt: BigInt(stream.expiresAt),
49
+ closedAt: BigInt(stream.closedAt ?? 0),
50
+ },
51
+ BigInt(graceSeconds),
52
+ ),
53
+ );
40
54
  const secondsRemaining = readyAt - nowSeconds;
41
55
  return {
42
56
  graceSeconds,
@@ -18,6 +18,30 @@ export async function resolveListing(
18
18
  serviceRef: `0x${string}`,
19
19
  options: { readonly required: boolean },
20
20
  ): Promise<ServiceListing | null> {
21
+ return (await resolveListingRecord(context, serviceRef, options)).listing;
22
+ }
23
+
24
+ /**
25
+ * What {@link resolveListing} found, including the verified signed record of a
26
+ * matching row even when that row is not usable as a listing (no signed
27
+ * invocation URL) — `open` still has to read the operator-signed rate band off
28
+ * it — and whether the registry was unreachable (the only case in which a
29
+ * signed band may exist and be unknown).
30
+ */
31
+ export interface ListingRecordResolution {
32
+ readonly listing: ServiceListing | null;
33
+ readonly record: {
34
+ readonly serviceRef: string;
35
+ readonly signed: ServiceListing["signed"];
36
+ } | null;
37
+ readonly registryUnreachable: boolean;
38
+ }
39
+
40
+ export async function resolveListingRecord(
41
+ context: CommandContext,
42
+ serviceRef: `0x${string}`,
43
+ options: { readonly required: boolean },
44
+ ): Promise<ListingRecordResolution> {
21
45
  const { config, out, deps } = context;
22
46
  let services;
23
47
  try {
@@ -32,7 +56,7 @@ export async function resolveListing(
32
56
  out.note(
33
57
  `warning: the registry at ${config.registryUrl} is unreachable; continuing without a verified listing.`,
34
58
  );
35
- return null;
59
+ return { listing: null, record: null, registryUnreachable: true };
36
60
  }
37
61
  throw new CliError(
38
62
  `the service registry at ${config.registryUrl} could not be read`,
@@ -45,7 +69,9 @@ export async function resolveListing(
45
69
  (service) => service.serviceRef.toLowerCase() === wanted,
46
70
  );
47
71
  if (match === undefined) {
48
- if (!options.required) return null;
72
+ if (!options.required) {
73
+ return { listing: null, record: null, registryUnreachable: false };
74
+ }
49
75
  throw new CliError(
50
76
  `no verified listing in the registry matches serviceRef ${serviceRef}`,
51
77
  {
@@ -54,8 +80,11 @@ export async function resolveListing(
54
80
  },
55
81
  );
56
82
  }
83
+ const record = { serviceRef: match.serviceRef, signed: match.signed };
57
84
  if (match.accessUrl === null) {
58
- if (!options.required) return null;
85
+ if (!options.required) {
86
+ return { listing: null, record, registryUnreachable: false };
87
+ }
59
88
  throw new CliError(
60
89
  `listing ${serviceRef} carries no signed invocation URL`,
61
90
  {
@@ -66,12 +95,16 @@ export async function resolveListing(
66
95
  }
67
96
 
68
97
  return {
69
- serviceRef: match.serviceRef,
70
- operator: match.operator,
71
- publicUrl: match.publicUrl,
72
- access: match.access,
73
- accessUrl: match.accessUrl,
74
- signed: match.signed,
75
- category: match.category,
98
+ listing: {
99
+ serviceRef: match.serviceRef,
100
+ operator: match.operator,
101
+ publicUrl: match.publicUrl,
102
+ access: match.access,
103
+ accessUrl: match.accessUrl,
104
+ signed: match.signed,
105
+ category: match.category,
106
+ },
107
+ record,
108
+ registryUnreachable: false,
76
109
  };
77
110
  }
@@ -1,14 +1,10 @@
1
+ import type { MetrikNetwork } from "@absol-labs/sdk";
2
+
3
+ import { gasFundingHint, usdcFundingHint } from "../network.js";
1
4
  import { formatUsdc } from "./amounts.js";
2
5
  import { CliError } from "./errors.js";
3
6
  import type { CliReadClient } from "./deps.js";
4
7
 
5
- /** Where a Base Sepolia test wallet gets its gas. */
6
- export const BASE_SEPOLIA_FAUCET_URL =
7
- "https://www.alchemy.com/faucets/base-sepolia";
8
-
9
- /** Where a Base Sepolia test wallet gets the USDC it escrows. */
10
- export const CIRCLE_USDC_FAUCET_URL = "https://faucet.circle.com";
11
-
12
8
  /**
13
9
  * Refuse to broadcast from a wallet that cannot pay for gas.
14
10
  *
@@ -16,20 +12,29 @@ export const CIRCLE_USDC_FAUCET_URL = "https://faucet.circle.com";
16
12
  * phrasing — `USDC approve transaction failed: Execution reverted with reason:
17
13
  * gas required exceeds allowance (0)` — which reads like a USDC allowance
18
14
  * problem and sends the reader looking at approvals. The actual fact is "this
19
- * address holds no ETH", and it is knowable with one read before anything is
15
+ * address holds no gas", and it is knowable with one read before anything is
20
16
  * signed.
21
17
  *
18
+ * The gas token is read from the network's registry metadata, never assumed to
19
+ * be ETH. Where gas IS the settlement token (`gasToken.isSettlementToken`),
20
+ * there is only one balance: the USDC balance is the gas balance, so that is
21
+ * the one read.
22
+ *
22
23
  * Deliberately only a ZERO check, not an estimate: predicting the gas a hire
23
- * will cost means simulating it, and a wallet with a little ETH that still runs
24
+ * will cost means simulating it, and a wallet with a little gas that still runs
24
25
  * short should get the real revert rather than a guess from this CLI.
25
26
  */
26
27
  export async function assertCanPayGas(
27
28
  read: CliReadClient,
28
29
  address: `0x${string}`,
30
+ network: MetrikNetwork,
29
31
  ): Promise<void> {
32
+ const gasIsUsdc = network.gasToken.isSettlementToken;
30
33
  let balance: bigint;
31
34
  try {
32
- balance = await read.nativeBalance(address);
35
+ balance = gasIsUsdc
36
+ ? await read.usdcBalance(address)
37
+ : await read.nativeBalance(address);
33
38
  } catch {
34
39
  // A balance read is a courtesy, not a gate. If the RPC will not answer it,
35
40
  // let the command proceed and fail with the real error.
@@ -38,10 +43,12 @@ export async function assertCanPayGas(
38
43
  if (balance > 0n) return;
39
44
 
40
45
  throw new CliError(
41
- `wallet ${address} has 0 ETH on Base Sepolia, so it cannot pay for gas`,
46
+ `wallet ${address} has 0 ${network.gasToken.symbol} on ${network.name}, so it cannot pay for gas`,
42
47
  {
43
48
  exitCode: 3,
44
- hint: `fund it at ${BASE_SEPOLIA_FAUCET_URL}, then re-run. (Gas is ETH; the stream itself is escrowed in USDC, which is a separate balance.)`,
49
+ hint: gasIsUsdc
50
+ ? `${gasFundingHint(network)}, then re-run. (On ${network.name} gas is paid in USDC, the same balance a stream is escrowed from.)`
51
+ : `${gasFundingHint(network)}, then re-run. (Gas is ${network.gasToken.symbol}; the stream itself is escrowed in USDC, which is a separate balance.)`,
45
52
  },
46
53
  );
47
54
  }
@@ -49,12 +56,16 @@ export async function assertCanPayGas(
49
56
  /**
50
57
  * Refuse to broadcast a hire the wallet cannot fund.
51
58
  *
52
- * The sibling of `assertCanPayGas`, for the OTHER balance. Gas is ETH and the
53
- * deposit is USDC: they are separate balances on separate tokens, so a wallet
54
- * that passes the gas check can still hold no USDC at all. Without this the
55
- * `open` path sends the USDC approval FIRST — which succeeds, because approving
56
- * an amount you do not hold is legal — and then reverts inside `transferFrom`,
57
- * having already spent gas to learn something that was one `balanceOf` away.
59
+ * The sibling of `assertCanPayGas`, for the OTHER balance. On a network whose
60
+ * gas token is not USDC, gas and deposit are separate balances on separate
61
+ * tokens, so a wallet that passes the gas check can still hold no USDC at all.
62
+ * Without this the `open` path sends the USDC approval FIRST — which succeeds,
63
+ * because approving an amount you do not hold is legal — and then reverts
64
+ * inside `transferFrom`, having already spent gas to learn something that was
65
+ * one `balanceOf` away.
66
+ *
67
+ * Where gas IS USDC there is one balance, and it must cover the deposit AND
68
+ * leave something for gas, so the check is strict (`balance > budget`).
58
69
  *
59
70
  * Deliberately NOT an allowance check. The CLI's own open path grants the
60
71
  * bounded approval it needs (`approveTxHash` in the result), so a missing
@@ -66,7 +77,9 @@ export async function assertCanFundStream(
66
77
  read: CliReadClient,
67
78
  address: `0x${string}`,
68
79
  budgetUsdc: bigint,
80
+ network: MetrikNetwork,
69
81
  ): Promise<void> {
82
+ const gasIsUsdc = network.gasToken.isSettlementToken;
70
83
  let balance: bigint;
71
84
  try {
72
85
  balance = await read.usdcBalance(address);
@@ -76,13 +89,15 @@ export async function assertCanFundStream(
76
89
  // the real error surface.
77
90
  return;
78
91
  }
79
- if (balance >= budgetUsdc) return;
92
+ if (gasIsUsdc ? balance > budgetUsdc : balance >= budgetUsdc) return;
80
93
 
81
94
  throw new CliError(
82
- `wallet ${address} holds ${formatUsdc(balance)} but --budget is ${formatUsdc(budgetUsdc)}`,
95
+ `wallet ${address} holds ${formatUsdc(balance)} but --budget is ${formatUsdc(budgetUsdc)}${gasIsUsdc ? " (plus gas, which is also USDC here)" : ""}`,
83
96
  {
84
97
  exitCode: 3,
85
- hint: `the deposit is escrowed in USDC — fund the wallet at ${CIRCLE_USDC_FAUCET_URL} (Base Sepolia), then re-run. (Gas is a separate ETH balance.)`,
98
+ hint: gasIsUsdc
99
+ ? `the deposit and the gas are both USDC on ${network.name} — ${usdcFundingHint(network)}, then re-run.`
100
+ : `the deposit is escrowed in USDC — ${usdcFundingHint(network)}, then re-run. (Gas is a separate ${network.gasToken.symbol} balance.)`,
86
101
  },
87
102
  );
88
103
  }