perp-cli 0.14.3 → 0.15.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 (311) hide show
  1. package/README.md +152 -13
  2. package/dist/agent-wallet/store.d.ts +3 -2
  3. package/dist/agent-wallet/store.js +2 -2
  4. package/dist/api/public/hyperliquid.js +4 -0
  5. package/dist/api/public/pacifica.js +83 -13
  6. package/dist/api/public/urls.d.ts +2 -0
  7. package/dist/api/public/urls.js +3 -0
  8. package/dist/arb/utils.d.ts +1 -2
  9. package/dist/arb/utils.js +1 -2
  10. package/dist/bot/apex/pulse.js +3 -0
  11. package/dist/bot/apex/radar.js +2 -1
  12. package/dist/bot/engine.d.ts +10 -1
  13. package/dist/bot/engine.js +14 -6
  14. package/dist/bot/indicators.d.ts +7 -0
  15. package/dist/bot/indicators.js +16 -0
  16. package/dist/bot/perp-order.d.ts +4 -4
  17. package/dist/bot/perp-order.js +5 -5
  18. package/dist/bot/quarantine.d.ts +1 -1
  19. package/dist/bot/quarantine.js +1 -1
  20. package/dist/bot/strategies/apex-strategy.js +3 -2
  21. package/dist/bot/strategies/engine-mm.js +6 -5
  22. package/dist/bot/strategies/funding-arb-strategy.js +2 -2
  23. package/dist/bot/strategies/funding-arb-utils.d.ts +1 -1
  24. package/dist/bot/strategies/funding-arb-utils.js +5 -5
  25. package/dist/bot/strategies/funding-auto.js +2 -2
  26. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  27. package/dist/bot/strategies/liquidation-mm.js +8 -2
  28. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  29. package/dist/bot/strategies/momentum-breakout.js +11 -3
  30. package/dist/bot/strategies/spot-perp-arb-strategy.js +2 -2
  31. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  32. package/dist/bot/strategies/spot-perp-executor.js +1 -1
  33. package/dist/bot/strategy-types.d.ts +2 -1
  34. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  35. package/dist/bot/tui/BotDashboard.js +1 -1
  36. package/dist/cache.js +12 -6
  37. package/dist/cli-program.d.ts +15 -0
  38. package/dist/cli-program.js +125 -5
  39. package/dist/commands/agent-import.d.ts +7 -0
  40. package/dist/commands/agent-import.js +30 -1
  41. package/dist/commands/agent.d.ts +6 -3
  42. package/dist/commands/agent.js +160 -12
  43. package/dist/commands/arb/index.js +4 -4
  44. package/dist/commands/arb-auto.js +21 -17
  45. package/dist/commands/arb.d.ts +12 -0
  46. package/dist/commands/arb.js +61 -143
  47. package/dist/commands/bot.js +1 -1
  48. package/dist/commands/builder.js +1 -1
  49. package/dist/commands/funds.js +113 -3
  50. package/dist/commands/init.d.ts +1 -1
  51. package/dist/commands/init.js +27 -7
  52. package/dist/commands/manage.js +23 -21
  53. package/dist/commands/market.js +167 -35
  54. package/dist/commands/options.d.ts +9 -0
  55. package/dist/commands/options.js +134 -0
  56. package/dist/commands/pacifica-earn.d.ts +5 -0
  57. package/dist/commands/pacifica-earn.js +468 -0
  58. package/dist/commands/pacifica-orders.d.ts +14 -0
  59. package/dist/commands/pacifica-orders.js +142 -0
  60. package/dist/commands/portfolio.js +32 -47
  61. package/dist/commands/rebalance.d.ts +1 -0
  62. package/dist/commands/rebalance.js +15 -0
  63. package/dist/commands/risk.d.ts +11 -2
  64. package/dist/commands/risk.js +39 -44
  65. package/dist/commands/trade.d.ts +1 -0
  66. package/dist/commands/trade.js +63 -58
  67. package/dist/commands/wallet.js +1 -1
  68. package/dist/config.d.ts +2 -2
  69. package/dist/config.js +15 -4
  70. package/dist/constants.js +6 -0
  71. package/dist/dashboard/server.js +1 -1
  72. package/dist/errors.d.ts +24 -1
  73. package/dist/errors.js +25 -3
  74. package/dist/exchanges/arcus/adapter.d.ts +7 -7
  75. package/dist/exchanges/arcus/adapter.js +12 -12
  76. package/dist/exchanges/arcus/agent.d.ts +1 -1
  77. package/dist/exchanges/arcus/agent.js +1 -1
  78. package/dist/exchanges/arcus/client.js +1 -1
  79. package/dist/exchanges/arcus/signing.d.ts +2 -3
  80. package/dist/exchanges/arcus/signing.js +1 -1
  81. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  82. package/dist/exchanges/arcus/spot-chain.js +1 -1
  83. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  84. package/dist/exchanges/arcus/spot-router.js +2 -2
  85. package/dist/exchanges/arcus/spot.d.ts +2 -3
  86. package/dist/exchanges/arcus/spot.js +2 -3
  87. package/dist/exchanges/aster-spot.d.ts +3 -3
  88. package/dist/exchanges/aster-spot.js +6 -9
  89. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  90. package/dist/exchanges/aster-typed-data.js +1 -1
  91. package/dist/exchanges/aster.d.ts +4 -4
  92. package/dist/exchanges/aster.js +31 -13
  93. package/dist/exchanges/builder-fee.d.ts +1 -1
  94. package/dist/exchanges/builder-fee.js +13 -2
  95. package/dist/exchanges/canonical-symbols.d.ts +1 -2
  96. package/dist/exchanges/canonical-symbols.js +26 -2
  97. package/dist/exchanges/derive/client.d.ts +37 -0
  98. package/dist/exchanges/derive/client.js +135 -0
  99. package/dist/exchanges/derive/options.d.ts +68 -0
  100. package/dist/exchanges/derive/options.js +360 -0
  101. package/dist/exchanges/extended/adapter.d.ts +82 -0
  102. package/dist/exchanges/extended/adapter.js +179 -0
  103. package/dist/exchanges/extended/client.d.ts +38 -0
  104. package/dist/exchanges/extended/client.js +88 -0
  105. package/dist/exchanges/extended/public.d.ts +130 -0
  106. package/dist/exchanges/extended/public.js +429 -0
  107. package/dist/exchanges/grvt/adapter.d.ts +86 -0
  108. package/dist/exchanges/grvt/adapter.js +206 -0
  109. package/dist/exchanges/grvt/client.d.ts +25 -0
  110. package/dist/exchanges/grvt/client.js +86 -0
  111. package/dist/exchanges/grvt/public.d.ts +151 -0
  112. package/dist/exchanges/grvt/public.js +438 -0
  113. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  114. package/dist/exchanges/hyperliquid-spot.js +5 -5
  115. package/dist/exchanges/hyperliquid.d.ts +22 -12
  116. package/dist/exchanges/hyperliquid.js +80 -34
  117. package/dist/exchanges/index.d.ts +1 -1
  118. package/dist/exchanges/index.js +1 -1
  119. package/dist/exchanges/interface.d.ts +94 -6
  120. package/dist/exchanges/interface.js +16 -0
  121. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  122. package/dist/exchanges/ioc-outcome.js +1 -1
  123. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  124. package/dist/exchanges/legacy-trading-v2.js +7 -2
  125. package/dist/exchanges/lighter-public.d.ts +2 -2
  126. package/dist/exchanges/lighter-public.js +2 -2
  127. package/dist/exchanges/lighter-signer.d.ts +1 -1
  128. package/dist/exchanges/lighter-signer.js +1 -1
  129. package/dist/exchanges/lighter-spot.d.ts +3 -4
  130. package/dist/exchanges/lighter-spot.js +6 -7
  131. package/dist/exchanges/lighter.d.ts +11 -11
  132. package/dist/exchanges/lighter.js +28 -14
  133. package/dist/exchanges/ondo/adapter.d.ts +5 -6
  134. package/dist/exchanges/ondo/adapter.js +16 -14
  135. package/dist/exchanges/ondo/agent.d.ts +1 -1
  136. package/dist/exchanges/ondo/agent.js +1 -1
  137. package/dist/exchanges/ondo/client.d.ts +1 -1
  138. package/dist/exchanges/ondo/client.js +2 -2
  139. package/dist/exchanges/ondo/public.d.ts +8 -1
  140. package/dist/exchanges/ondo/public.js +13 -2
  141. package/dist/exchanges/ondo/spot.d.ts +2 -2
  142. package/dist/exchanges/ondo/spot.js +3 -3
  143. package/dist/exchanges/options-interface.d.ts +158 -0
  144. package/dist/exchanges/options-interface.js +17 -0
  145. package/dist/exchanges/options-venues.d.ts +12 -0
  146. package/dist/exchanges/options-venues.js +35 -0
  147. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  148. package/dist/exchanges/pacifica-atomic.js +440 -0
  149. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  150. package/dist/exchanges/pacifica-decimal.js +51 -0
  151. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  152. package/dist/exchanges/pacifica-presigned.js +141 -0
  153. package/dist/exchanges/pacifica-print.d.ts +200 -0
  154. package/dist/exchanges/pacifica-print.js +417 -0
  155. package/dist/exchanges/pacifica-spot.d.ts +2 -2
  156. package/dist/exchanges/pacifica-spot.js +3 -3
  157. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  158. package/dist/exchanges/pacifica-typed-data.js +1 -1
  159. package/dist/exchanges/pacifica-vaults.d.ts +121 -0
  160. package/dist/exchanges/pacifica-vaults.js +264 -0
  161. package/dist/exchanges/pacifica.d.ts +32 -5
  162. package/dist/exchanges/pacifica.js +114 -48
  163. package/dist/exchanges/precision.d.ts +3 -1
  164. package/dist/exchanges/precision.js +9 -2
  165. package/dist/exchanges/qfex/adapter.d.ts +18 -5
  166. package/dist/exchanges/qfex/adapter.js +32 -12
  167. package/dist/exchanges/qfex/agent.d.ts +1 -1
  168. package/dist/exchanges/qfex/agent.js +1 -1
  169. package/dist/exchanges/qfex/client.d.ts +1 -1
  170. package/dist/exchanges/qfex/client.js +1 -1
  171. package/dist/exchanges/qfex/errors.d.ts +1 -1
  172. package/dist/exchanges/qfex/errors.js +2 -2
  173. package/dist/exchanges/qfex/public.d.ts +3 -3
  174. package/dist/exchanges/qfex/public.js +3 -3
  175. package/dist/exchanges/qfex/signing.d.ts +2 -2
  176. package/dist/exchanges/qfex/signing.js +4 -5
  177. package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
  178. package/dist/exchanges/qfex/ws-transport.js +3 -3
  179. package/dist/exchanges/rate-limiter.d.ts +1 -1
  180. package/dist/exchanges/rate-limiter.js +5 -2
  181. package/dist/exchanges/registry.d.ts +1 -1
  182. package/dist/exchanges/registry.js +1 -1
  183. package/dist/exchanges/risex/adapter.d.ts +148 -0
  184. package/dist/exchanges/risex/adapter.js +796 -0
  185. package/dist/exchanges/risex/agent.d.ts +154 -0
  186. package/dist/exchanges/risex/agent.js +384 -0
  187. package/dist/exchanges/risex/client.d.ts +47 -0
  188. package/dist/exchanges/risex/client.js +174 -0
  189. package/dist/exchanges/risex/nonce.d.ts +43 -0
  190. package/dist/exchanges/risex/nonce.js +212 -0
  191. package/dist/exchanges/risex/public.d.ts +64 -0
  192. package/dist/exchanges/risex/public.js +150 -0
  193. package/dist/exchanges/risex/session.d.ts +71 -0
  194. package/dist/exchanges/risex/session.js +171 -0
  195. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  196. package/dist/exchanges/risex/typed-data.js +240 -0
  197. package/dist/exchanges/risex/units.d.ts +29 -0
  198. package/dist/exchanges/risex/units.js +104 -0
  199. package/dist/exchanges/slippage.d.ts +1 -1
  200. package/dist/exchanges/slippage.js +1 -1
  201. package/dist/exchanges/spot-factory.d.ts +3 -3
  202. package/dist/exchanges/spot-factory.js +7 -7
  203. package/dist/exchanges/spot-interface.d.ts +5 -5
  204. package/dist/exchanges/spot-interface.js +2 -2
  205. package/dist/exchanges/spot-result.d.ts +3 -3
  206. package/dist/exchanges/spot-result.js +3 -3
  207. package/dist/exchanges/trading-v2.d.ts +3 -3
  208. package/dist/exchanges/trading-v2.js +9 -6
  209. package/dist/exchanges/txflow/adapter.d.ts +54 -0
  210. package/dist/exchanges/txflow/adapter.js +118 -0
  211. package/dist/exchanges/txflow/client.d.ts +47 -0
  212. package/dist/exchanges/txflow/client.js +153 -0
  213. package/dist/exchanges/txflow/public.d.ts +76 -0
  214. package/dist/exchanges/txflow/public.js +284 -0
  215. package/dist/exchanges/unknown-symbol.d.ts +29 -0
  216. package/dist/exchanges/unknown-symbol.js +51 -0
  217. package/dist/exchanges/variational/adapter.d.ts +71 -0
  218. package/dist/exchanges/variational/adapter.js +137 -0
  219. package/dist/exchanges/variational/client.d.ts +21 -0
  220. package/dist/exchanges/variational/client.js +66 -0
  221. package/dist/exchanges/variational/public.d.ts +126 -0
  222. package/dist/exchanges/variational/public.js +430 -0
  223. package/dist/exchanges/venue-types.d.ts +6 -7
  224. package/dist/exchanges/venue-types.js +2 -2
  225. package/dist/exchanges/venue.d.ts +51 -8
  226. package/dist/exchanges/venue.js +54 -4
  227. package/dist/exchanges/venues/arcus.d.ts +1 -2
  228. package/dist/exchanges/venues/arcus.js +5 -4
  229. package/dist/exchanges/venues/aster.d.ts +2 -2
  230. package/dist/exchanges/venues/aster.js +6 -4
  231. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  232. package/dist/exchanges/venues/create-helpers.js +3 -3
  233. package/dist/exchanges/venues/extended.d.ts +21 -0
  234. package/dist/exchanges/venues/extended.js +96 -0
  235. package/dist/exchanges/venues/grvt.d.ts +30 -0
  236. package/dist/exchanges/venues/grvt.js +98 -0
  237. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  238. package/dist/exchanges/venues/hyperliquid.js +10 -8
  239. package/dist/exchanges/venues/index.d.ts +7 -2
  240. package/dist/exchanges/venues/index.js +8 -2
  241. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  242. package/dist/exchanges/venues/lighter-family.js +9 -11
  243. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  244. package/dist/exchanges/venues/lighter-rh.js +7 -5
  245. package/dist/exchanges/venues/lighter.d.ts +1 -1
  246. package/dist/exchanges/venues/lighter.js +5 -3
  247. package/dist/exchanges/venues/ondo.d.ts +1 -1
  248. package/dist/exchanges/venues/ondo.js +5 -3
  249. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  250. package/dist/exchanges/venues/pacifica.js +6 -4
  251. package/dist/exchanges/venues/qfex.d.ts +1 -1
  252. package/dist/exchanges/venues/qfex.js +4 -2
  253. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  254. package/dist/exchanges/venues/risex-release.js +14 -0
  255. package/dist/exchanges/venues/risex.d.ts +31 -0
  256. package/dist/exchanges/venues/risex.js +116 -0
  257. package/dist/exchanges/venues/txflow.d.ts +18 -0
  258. package/dist/exchanges/venues/txflow.js +77 -0
  259. package/dist/exchanges/venues/variational.d.ts +21 -0
  260. package/dist/exchanges/venues/variational.js +85 -0
  261. package/dist/execution-log.d.ts +1 -1
  262. package/dist/funding/legacy-public.d.ts +1 -1
  263. package/dist/funding/legacy-public.js +9 -4
  264. package/dist/funding/normalize.d.ts +1 -1
  265. package/dist/funding/normalize.js +1 -1
  266. package/dist/funding/rates.d.ts +44 -4
  267. package/dist/funding/rates.js +56 -12
  268. package/dist/index.js +7 -6
  269. package/dist/jobs.d.ts +1 -0
  270. package/dist/jobs.js +25 -1
  271. package/dist/landing.d.ts +2 -0
  272. package/dist/landing.js +1 -1
  273. package/dist/mcp-pacifica-earn.d.ts +9 -0
  274. package/dist/mcp-pacifica-earn.js +58 -0
  275. package/dist/mcp-server.js +113 -59
  276. package/dist/pacifica/client.d.ts +67 -4
  277. package/dist/pacifica/client.js +170 -8
  278. package/dist/pacifica/rate-limit.d.ts +144 -0
  279. package/dist/pacifica/rate-limit.js +267 -0
  280. package/dist/pacifica/types/order.d.ts +2 -1
  281. package/dist/plan-executor.js +7 -2
  282. package/dist/rebalance.js +1 -1
  283. package/dist/risk.d.ts +2 -1
  284. package/dist/risk.js +2 -4
  285. package/dist/settings.d.ts +32 -3
  286. package/dist/signer/ows-evm.d.ts +1 -1
  287. package/dist/signer/ows-evm.js +1 -1
  288. package/dist/smart-order.d.ts +1 -1
  289. package/dist/smart-order.js +1 -1
  290. package/dist/strategies/funding-arb.d.ts +1 -1
  291. package/dist/strategies/funding-arb.js +1 -1
  292. package/dist/trade-validator.js +2 -2
  293. package/dist/utils.d.ts +12 -3
  294. package/dist/utils.js +11 -3
  295. package/dist/venue-health.d.ts +40 -0
  296. package/dist/venue-health.js +75 -0
  297. package/dist/withdraw-gate.d.ts +1 -1
  298. package/dist/withdraw-gate.js +1 -1
  299. package/dist/ws/index.d.ts +959 -0
  300. package/dist/ws/index.js +4295 -0
  301. package/dist/ws/node.d.ts +975 -0
  302. package/dist/ws/node.js +4366 -0
  303. package/dist/ws/pacifica-instrument.d.ts +8 -0
  304. package/dist/ws/pacifica-instrument.js +8 -0
  305. package/dist/ws-cli/credentials.d.ts +62 -0
  306. package/dist/ws-cli/credentials.js +126 -0
  307. package/package.json +58 -10
  308. package/skills/perp-cli/SKILL.md +62 -9
  309. package/skills/perp-cli/references/agent-operations.md +7 -0
  310. package/skills/perp-cli/references/commands.md +76 -5
  311. package/skills/perp-cli/references/ws-library.md +492 -0
@@ -6,13 +6,15 @@ import { Command } from "commander";
6
6
  import { cliCredentialOptsFrom, hasCredentials, resolveCredentials } from "./config.js";
7
7
  import { PacificaAdapter } from "./exchanges/pacifica.js";
8
8
  import { HyperliquidAdapter } from "./exchanges/hyperliquid.js";
9
- import { getVenue, listVenues, resolveVenueId, READ_ONLY_CREDENTIALS } from "./exchanges/venue.js";
9
+ import { getVenue, listVenues, resolveVenueId, roleUnsupported, tryResolveVenueId, READ_ONLY_CREDENTIALS } from "./exchanges/venue.js";
10
10
  import { registerAdapterNetwork } from "./exchanges/adapter-network.js";
11
11
  import "./exchanges/venues/index.js";
12
12
  import { PerpError } from "./errors.js";
13
13
  import { registerMarketCommands } from "./commands/market.js";
14
14
  import { registerAccountCommands } from "./commands/account.js";
15
15
  import { registerOutcomeCommands } from "./commands/outcome.js";
16
+ import { registerOptionsCommands } from "./commands/options.js";
17
+ import { registerPacificaEarnCommands } from "./commands/pacifica-earn.js";
16
18
  import { registerTradeCommands } from "./commands/trade.js";
17
19
  // manage commands now register under `wallet manage ...` via registerWalletCommands.
18
20
  // stream commands removed — WS feeds still used by dashboard/event-stream internally
@@ -86,6 +88,11 @@ export function landingData(program, version) {
86
88
  return {
87
89
  name: program.name(),
88
90
  version,
91
+ // Registry facts only (no reads): an "unofficial" venue is read through an undocumented API.
92
+ venues: listVenues().map(v => ({
93
+ id: v.id, displayName: v.displayName, roles: [...v.roles], apiStatus: v.apiStatus.kind,
94
+ note: v.apiStatus.kind === "unofficial" ? v.apiStatus.note : null,
95
+ })),
89
96
  usage: `${program.name()} [options] <command> [args] (add --json for one JSON document on stdout)`,
90
97
  help: `${program.name()} <command> --help`,
91
98
  commands: program.commands
@@ -131,9 +138,9 @@ export function buildProgram(opts) {
131
138
  });
132
139
  let _adapter = null;
133
140
  /**
134
- * One adapter per `${venueId}:${network}:${wallet}:${hlDex}` (design §3 L1 cache key).
141
+ * One adapter per `${venueId}:${network}:${wallet}:${hlDex}`.
135
142
  * Every adapter is built by the venue definition's create(ctx) — the single
136
- * creation path shared with the MCP server (design §10.2).
143
+ * creation path shared with the MCP server.
137
144
  */
138
145
  const _adapters = new Map();
139
146
  function cliCredentialOpts() {
@@ -147,7 +154,7 @@ export function buildProgram(opts) {
147
154
  const wallet = opts.ows ? `ows:${opts.ows}` : (opts.wallet ?? "default");
148
155
  return `${venue}:${network}:${wallet}:${hlDex ?? "-"}`;
149
156
  }
150
- /** restrictedRegions notice: stderr, once per venue per process (design §9.1 #2 — warn, never block). */
157
+ /** restrictedRegions notice: stderr, once per venue per process (warn, never block). */
151
158
  const _restrictedWarned = new Set();
152
159
  function warnRestrictedRegions(def) {
153
160
  if (def.restrictedRegions.length === 0 || _restrictedWarned.has(def.id))
@@ -167,6 +174,10 @@ export function buildProgram(opts) {
167
174
  remediation: `${def.id} is market-data only — use the funding/market public commands (e.g. perp arb scan)`,
168
175
  });
169
176
  }
177
+ // A market-data-only venue (TxFlow: unofficial read-only API) never gets a credentialed adapter:
178
+ // every account / trading / funds / bot path refuses here, before any credential or request.
179
+ if (!publicOnly && !def.roles.includes("account"))
180
+ throw roleUnsupported(def, "account");
170
181
  const network = currentNetwork();
171
182
  const hlDex = def.id === "hyperliquid" ? dex : null;
172
183
  const key = publicOnly ? `${def.id}:${network}:public:${hlDex ?? "-"}` : adapterCacheKey(def.id, network, hlDex);
@@ -246,6 +257,8 @@ export function buildProgram(opts) {
246
257
  registerMarketCommands(program, getPublicAdapter, isJson, getPublicAdapterForExchange, getAdapter);
247
258
  registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
248
259
  registerOutcomeCommands(program, getAdapterForExchange, isJson);
260
+ registerOptionsCommands(program, isJson);
261
+ registerPacificaEarnCommands(program, getAdapterForExchange, isJson);
249
262
  registerTradeCommands(program, getAdapter, isJson, isDryRunFlag, getAdapterForExchange);
250
263
  // manage tree wired inside registerWalletCommands below.
251
264
  // stream commands removed
@@ -273,7 +286,15 @@ export function buildProgram(opts) {
273
286
  // registerFundsCommands above.
274
287
  // Background processes & strategies
275
288
  registerBackgroundCommands(program, isJson, getAdapterForExchange);
276
- registerStrategyCommands(program, getAdapter, getAdapterForExchange, isJson);
289
+ // Every strategy command (bots, plans, TWAP / DCA / grid, `strategy run`, `--exchanges` legs,
290
+ // config-file venues) builds its adapters through these: a venue without the trading role
291
+ // (TxFlow, release-gated RISEx) is refused here, before any key is opened — also under --dry-run.
292
+ const strategyVenueGate = (venue) => {
293
+ const def = getVenue(venue);
294
+ if (!def.roles.includes("trading"))
295
+ throw roleUnsupported(def, "automation");
296
+ };
297
+ registerStrategyCommands(program, async () => { strategyVenueGate(getExchange()); return getAdapter(); }, async (exchange) => { strategyVenueGate(exchange); return getAdapterForExchange(exchange); }, isJson);
277
298
  // Agent-friendly commands
278
299
  registerRiskCommands(program, getAdapterForExchange, isJson);
279
300
  registerHistoryCommands(program, isJson, getAdapterForExchange);
@@ -286,7 +307,12 @@ export function buildProgram(opts) {
286
307
  registerPortfolioCommand(program, getAdapterForExchange, isJson, version, getHLAdapterForDex);
287
308
  registerInitCommand(program);
288
309
  registerAlertCommands(program, isJson);
310
+ installNestedHelp(program);
311
+ installMissingSubcommandError(program);
289
312
  // Switch shared API URLs if --network testnet is used
313
+ // A venue without the trading role (market-data-only TxFlow, release-gated RISEx) refuses every
314
+ // trading command before anything else — also under --dry-run, whose plans skip the adapter.
315
+ program.hook("preAction", (_root, actionCommand) => assertCommandVenueRole(actionCommand, program.opts().exchange));
290
316
  // Validates -n and enforces default-deny --dry-run before any action runs.
291
317
  installDryRunGuard(program);
292
318
  // Secrets on argv (review L10): -k is rejected outright; --passphrase still works but warns loudly.
@@ -301,3 +327,97 @@ export function buildProgram(opts) {
301
327
  program.hook("preAction", () => setQuarantineNetwork(networkOf(program)));
302
328
  return { program, getAdapterForExchange, isJson };
303
329
  }
330
+ /** `perp trade <leaf>` commands that only read (order status, fills, P&L tracking, spot reads). */
331
+ const TRADE_READ_COMMANDS = new Set(["status", "fills", "pnl-track", "spot-orders", "spot-balance"]);
332
+ /**
333
+ * The trading-role gate at command resolution: `perp -e <venue> trade <write>`, every
334
+ * `perp trade multi <venue>:…` leg and `perp wallet agent approve|import|revoke|rotate <venue>`
335
+ * (revoke --force / --local-only excepted: local cleanup only) and every `strategy … --exchanges` leg refuse with roleUnsupported() (CAPABILITY_UNSUPPORTED) when the venue has no trading role. Runs
336
+ * before the dry-run guard and before any action, so a dry-run plan can never present a gated
337
+ * venue as tradable. Unknown venue names are left to the command's own UNKNOWN_VENUE handling.
338
+ */
339
+ export function assertCommandVenueRole(actionCommand, exchangeOpt) {
340
+ const path = commandPath(actionCommand).split(" ");
341
+ const leaf = path[path.length - 1];
342
+ let venueArgs;
343
+ let feature;
344
+ if (leaf === "multi" && (path[0] === "trade" || path.length === 1)) {
345
+ venueArgs = actionCommand.args.map(leg => leg.split(":")[0]);
346
+ feature = "trading";
347
+ }
348
+ else if (path[0] === "strategy" && typeof actionCommand.opts().exchanges === "string") {
349
+ // `--exchanges` legs (strategy run / funding-arb / quick-arb / delta-neutral, incl. their defaults):
350
+ // refused before the first leg's adapter is built. The -e venue is gated by the strategy adapter factory.
351
+ venueArgs = String(actionCommand.opts().exchanges).split(",").map(v => v.trim()).filter(v => v !== "");
352
+ feature = "automation";
353
+ }
354
+ else if (path[0] === "trade" && path.length === 2 && !TRADE_READ_COMMANDS.has(leaf)) {
355
+ venueArgs = [exchangeOpt];
356
+ feature = "trading";
357
+ }
358
+ else if (path[0] === "wallet" && path[1] === "agent" && ["approve", "import", "revoke", "rotate"].includes(path[2] ?? "")) {
359
+ // `revoke --force` / `--local-only` only clears local records (nothing signed or sent): allowed.
360
+ const o = actionCommand.opts();
361
+ if (path[2] === "revoke" && (o.force === true || o.localOnly === true))
362
+ return;
363
+ venueArgs = actionCommand.args.slice(0, 1);
364
+ feature = path[2] === "import" ? "agentImport" : `agent ${path[2]}`;
365
+ }
366
+ else {
367
+ return;
368
+ }
369
+ for (const v of venueArgs) {
370
+ const id = tryResolveVenueId(v);
371
+ if (id !== null && !getVenue(id).roles.includes("trading"))
372
+ throw roleUnsupported(getVenue(id), feature);
373
+ }
374
+ }
375
+ /** `perp a b c` path of a command (root name excluded). */
376
+ function commandPath(cmd) {
377
+ const names = [];
378
+ for (let c = cmd; c !== null && c.parent !== null; c = c.parent)
379
+ names.unshift(c.name());
380
+ return names.join(" ");
381
+ }
382
+ /**
383
+ * `perp help <group> <leaf> ...` shows the help of the deepest named command. Commander's built-in
384
+ * root help command resolves only its first operand, so `perp help market list` printed the
385
+ * `market` group help. Groups keep their own built-in `help` subcommand (`perp market help list`).
386
+ */
387
+ function installNestedHelp(program) {
388
+ program.helpCommand(false);
389
+ program
390
+ .command("help [command...]")
391
+ .description("display help for command (any depth: perp help trade limit)")
392
+ .action((names) => {
393
+ let cmd = program;
394
+ for (const name of names) {
395
+ const sub = cmd.commands.find(c => c.name() === name || c.aliases().includes(name));
396
+ if (sub === undefined) {
397
+ const where = cmd === program ? "perp" : `perp ${commandPath(cmd)}`;
398
+ throw new PerpError("INVALID_PARAMS", `Unknown command '${name}' under '${where}'`, {
399
+ remediation: `Available: ${cmd.commands.map(c => c.name()).join(", ")} — see: ${where} --help`,
400
+ });
401
+ }
402
+ cmd = sub;
403
+ }
404
+ cmd.help();
405
+ });
406
+ }
407
+ /**
408
+ * A command group run without a subcommand (`perp history`): Commander prints the group's help as
409
+ * an error. In --json mode that reached stdout as a CLI_ERROR whose message was the usage text;
410
+ * it is now one INVALID_PARAMS document naming the subcommands, with remediation (exit 1).
411
+ * Human mode keeps the usage listing on stderr.
412
+ */
413
+ function installMissingSubcommandError(program) {
414
+ program.on("beforeAllHelp", (ctx) => {
415
+ if (!ctx.error || program.opts().json !== true)
416
+ return;
417
+ const path = `perp ${commandPath(ctx.command)}`.trim();
418
+ const subs = ctx.command.commands.map(c => c.name()).filter(n => n !== "help");
419
+ throw new PerpError("INVALID_PARAMS", `'${path}' needs a subcommand: ${subs.join(", ")}`, {
420
+ remediation: `Run one of them, e.g. ${path} ${subs[0]} — list them with: ${path} --help`,
421
+ });
422
+ });
423
+ }
@@ -90,6 +90,13 @@ export declare function importSecretLineCount(venue: VenueId): number;
90
90
  export declare function importSecretFromLines(venue: VenueId, lines: string[]): ImportSecret;
91
91
  /** Read the secret: hidden TTY prompt, else piped stdin (ondo: 2 lines key id + secret; others: 1 line). */
92
92
  export declare function readImportSecret(venue: VenueId): Promise<ImportSecret>;
93
+ /**
94
+ * Venues whose keys cannot be imported. TxFlow: market-data-only (unofficial read-only API).
95
+ * RISEx: perp-cli trades only with a signer it registered itself and then restricted ON-CHAIN to
96
+ * the trading permission; a signer created in the RISEx app keeps every permission (including
97
+ * deposit / withdraw), so it is never stored here.
98
+ */
99
+ export declare function assertImportSupported(venue: VenueId, network: Network): void;
93
100
  /** Venues whose import cannot be checked against the venue (stored as "unverified"). */
94
101
  export declare const UNVERIFIED_IMPORT_VENUES: readonly VenueId[];
95
102
  /**
@@ -24,7 +24,8 @@
24
24
  import chalk from "chalk";
25
25
  import { PerpError, classifyError } from "../errors.js";
26
26
  import { acquireLock, releaseLock, getAgent, setAgent, deleteAgent, listAgents, agentNetworkOf, agentSourceOf, getVenueAgent, setVenueAgent, deleteVenueAgent, } from "../agent-wallet/store.js";
27
- import { getVenue, networkConfig } from "../exchanges/venue.js";
27
+ import { getVenue, networkConfig, roleUnsupported } from "../exchanges/venue.js";
28
+ import { unsupported } from "../exchanges/trading-v2.js";
28
29
  import { LIGHTER_RH_RESERVED_KEY_INDEXES } from "../exchanges/venues/lighter-family.js";
29
30
  import { loadOws } from "../signer/ows-loader.js";
30
31
  import { defaultAgentName, isLighterFamilyVenue, parseExpiresIn, promptHidden, qfexWithdrawKeyWarning } from "./agent.js";
@@ -67,6 +68,11 @@ export function importSecretLabel(venue) {
67
68
  case "arcus": return "Arcus API key private key (Ed25519)";
68
69
  case "ondo": return "Ondo API secret";
69
70
  case "qfex": return "QFEX API secret";
71
+ case "risex": return "RISEx signer private key";
72
+ case "txflow": return "TxFlow key (not supported)";
73
+ case "variational": return "Variational key (not supported)";
74
+ case "grvt": return "GRVT key (not supported)";
75
+ case "extended": return "Extended key (not supported)";
70
76
  }
71
77
  }
72
78
  /**
@@ -154,6 +160,20 @@ function redacted(err, secrets, venue) {
154
160
  details.remediation = scrub(e.remediation, forms);
155
161
  return new PerpError(e.code, scrub(e.message, forms), details);
156
162
  }
163
+ /**
164
+ * Venues whose keys cannot be imported. TxFlow: market-data-only (unofficial read-only API).
165
+ * RISEx: perp-cli trades only with a signer it registered itself and then restricted ON-CHAIN to
166
+ * the trading permission; a signer created in the RISEx app keeps every permission (including
167
+ * deposit / withdraw), so it is never stored here.
168
+ */
169
+ export function assertImportSupported(venue, network) {
170
+ const def = getVenue(venue);
171
+ if (!def.roles.includes("trading"))
172
+ throw roleUnsupported(def, "agentImport");
173
+ if (venue === "risex") {
174
+ throw unsupported("risex", "agentImport", "perp-cli only uses RISEx signers it registered and restricted on-chain to trading-only permissions; an app-created signer keeps deposit / withdraw permissions", `Register a restricted signer with your master wallet: perp${network === "testnet" ? " -n testnet" : ""} wallet agent approve risex --master <ows-wallet>`);
175
+ }
176
+ }
157
177
  /** Venues whose import cannot be checked against the venue (stored as "unverified"). */
158
178
  export const UNVERIFIED_IMPORT_VENUES = ["pacifica"];
159
179
  /**
@@ -722,6 +742,8 @@ async function importQfex(p, agentName, s, replacing) {
722
742
  * appears in a thrown error.
723
743
  */
724
744
  export async function importVenueAgent(p) {
745
+ // Refused before the secret is even inspected (nothing read, stored or sent).
746
+ assertImportSupported(p.venue, p.network);
725
747
  const secrets = secretsOf(p.secret);
726
748
  if (secrets.some(x => x.trim().length === 0))
727
749
  throw new PerpError("INVALID_PARAMS", "Empty key — nothing was imported");
@@ -751,6 +773,13 @@ export async function importVenueAgent(p) {
751
773
  case "lighter":
752
774
  case "lighter-rh":
753
775
  return await importLighter(p, p.venue, agentName, key, replacing);
776
+ case "risex":
777
+ case "txflow":
778
+ case "variational":
779
+ case "grvt":
780
+ case "extended":
781
+ // Unreachable: assertImportSupported() refused these before the lock.
782
+ throw new PerpError("CAPABILITY_UNSUPPORTED", `${p.venue} keys cannot be imported`);
754
783
  }
755
784
  }
756
785
  catch (err) {
@@ -1,5 +1,5 @@
1
1
  import { Command, Option } from "commander";
2
- import { loadSettings, type OndoAgentMeta, type ArcusAgentMeta, type QfexAgentMeta } from "../settings.js";
2
+ import { loadSettings, type OndoAgentMeta, type ArcusAgentMeta, type QfexAgentMeta, type RisexAgentMeta } from "../settings.js";
3
3
  import "../exchanges/venues/index.js";
4
4
  import type { Network, VenueId } from "../exchanges/venue-types.js";
5
5
  import { type LighterVenueId } from "../exchanges/venues/lighter-family.js";
@@ -27,7 +27,7 @@ export interface VerifyResult {
27
27
  /** Extract the agent list from a Pacifica /agent/list envelope; throws on an unrecognized shape. */
28
28
  export declare function parsePacAgentList(json: unknown): unknown[];
29
29
  /**
30
- * Confirmation of the account a QFEX key was resolved to (design §14.12 #9). In a terminal: a
30
+ * Confirmation of the account a QFEX key was resolved to. In a terminal: a
31
31
  * y/N prompt showing the UUID. Without one (piped stdin, --json): only an explicit --account-id
32
32
  * that equals the resolved account counts — the resolved UUID is printed on stderr either way.
33
33
  */
@@ -94,6 +94,9 @@ export type ApproveAgentOutcome = {
94
94
  } | {
95
95
  venue: "arcus";
96
96
  meta: ArcusAgentMeta;
97
+ } | {
98
+ venue: "risex";
99
+ meta: RisexAgentMeta;
97
100
  } | {
98
101
  venue: LighterVenueId;
99
102
  agentName: string;
@@ -131,7 +134,7 @@ export declare function qfexWithdrawKeyWarning(meta: Pick<QfexAgentMeta, "agentN
131
134
  /** Venues `wallet agent rotate` (and `approve --rotate`) can rotate. */
132
135
  type RotatableVenue = "aster" | "hyperliquid" | "pacifica" | LighterVenueId;
133
136
  type RotatedAgentOutcome = Exclude<ApproveAgentOutcome, {
134
- venue: "ondo" | "arcus";
137
+ venue: "ondo" | "arcus" | "risex";
135
138
  }>;
136
139
  export interface RotateAgentParams {
137
140
  exchange: RotatableVenue;
@@ -9,7 +9,8 @@ import chalk from "chalk";
9
9
  import bs58 from "bs58";
10
10
  import { loadSettings } from "../settings.js";
11
11
  import { getAgent, setAgent, deleteAgent, listAgents, acquireLock, releaseLock, getVenueAgent, getVenueAgentForNetwork, setVenueAgent, deleteVenueAgent, listVenueAgents, agentNetworkOf, recordedAgentNetworkOf, assertAgentNetwork, getAgentForNetwork, agentSourceOf, } from "../agent-wallet/store.js";
12
- import { getVenue, networkConfig, resolveVenueId } from "../exchanges/venue.js";
12
+ import { getVenue, networkConfig, resolveVenueId, roleUnsupported } from "../exchanges/venue.js";
13
+ import { RISEX_APP_URL } from "../exchanges/venues/risex.js";
13
14
  import { listConfiguredAccountVenues } from "../config.js";
14
15
  import "../exchanges/venues/index.js";
15
16
  import { LIGHTER_RH_RESERVED_KEY_INDEXES } from "../exchanges/venues/lighter-family.js";
@@ -114,7 +115,7 @@ function readPipedStdin() {
114
115
  });
115
116
  }
116
117
  /**
117
- * Ondo API key id + secret, from stdin only (design §10.4 — never flags).
118
+ * Ondo API key id + secret, from stdin only (never flags).
118
119
  * TTY: key id prompt, then hidden secret prompt. Piped: line 1 = key id, line 2 = secret.
119
120
  */
120
121
  /** Lighter ChangePubKey settlement window when confirming a slot (docs example: 30 s, 1 s apart; kept under the 60 req/min budget). */
@@ -445,7 +446,7 @@ async function verifyQfex(network, opts) {
445
446
  };
446
447
  }
447
448
  /**
448
- * Confirmation of the account a QFEX key was resolved to (design §14.12 #9). In a terminal: a
449
+ * Confirmation of the account a QFEX key was resolved to. In a terminal: a
449
450
  * y/N prompt showing the UUID. Without one (piped stdin, --json): only an explicit --account-id
450
451
  * that equals the resolved account counts — the resolved UUID is printed on stderr either way.
451
452
  */
@@ -471,6 +472,40 @@ export function confirmImportAccount(explicitAccountId, json) {
471
472
  }
472
473
  };
473
474
  }
475
+ /** RISEx: session-key status on the venue and the on-chain permission bitmap (read-only). */
476
+ async function verifyRisex(network, opts) {
477
+ const meta = opts.agentName !== undefined ? getVenueAgent("risex", opts.agentName) : getVenueAgentForNetwork("risex", network);
478
+ // Read-only release (trading role gated): approve / revoke are refused, so point to the RISEx app.
479
+ const tradingGated = !getVenue("risex").roles.includes("trading");
480
+ if (!meta) {
481
+ throw new PerpError("AGENT_NOT_REGISTERED", `No RISEx signer registered for ${network}`, {
482
+ remediation: tradingGated
483
+ ? `RISEx is read-only in this perp-cli release (trading in a later release): manage signers in the RISEx app (${RISEX_APP_URL})`
484
+ : `perp${network === "testnet" ? " -n testnet" : ""} wallet agent approve risex --master <ows-wallet>`,
485
+ });
486
+ }
487
+ const { verifyRisexAgent } = await import("../exchanges/risex/agent.js");
488
+ const v = await verifyRisexAgent(meta);
489
+ const warnings = [];
490
+ if (!v.active)
491
+ warnings.push(`RISEx signer ${meta.signerAddress} status is ${String(v.status)} (1 = Active)`);
492
+ if (v.expired)
493
+ warnings.push(`RISEx signer ${meta.signerAddress} expired at ${meta.expiresAt}`);
494
+ if (v.active && (!v.tradingOnly || meta.permissions.tradingOnly !== true)) {
495
+ // An active signer that can do more than trade is a failure, never ok:true.
496
+ throw new PerpError("POLICY_DENIED", `RISEx signer ${meta.signerAddress} ("${meta.agentName}") is NOT restricted to trading-only (on-chain ${JSON.stringify(v.permissions)}, bitmap ${String(v.bitmap)}; recorded tradingOnly=${String(meta.permissions.tradingOnly)})${v.reason !== null ? ` — ${v.reason}` : ""}`, {
497
+ exchange: "risex", signer: meta.signerAddress, permissions: v.permissions, bitmap: v.bitmap,
498
+ remediation: tradingGated
499
+ ? `Revoke it in the RISEx app (${RISEX_APP_URL}); then remove the local record: perp${network === "testnet" ? " -n testnet" : ""} wallet agent revoke risex ${meta.agentName} --force`
500
+ : `Revoke it: perp${network === "testnet" ? " -n testnet" : ""} wallet agent revoke risex ${meta.agentName}, then approve a new one`,
501
+ });
502
+ }
503
+ return {
504
+ registered: v.active, count: 1,
505
+ items: [{ agentName: meta.agentName, signer: meta.signerAddress, account: meta.accountAddress, network: meta.network, status: v.status, permissions: v.permissions, tradingOnly: v.tradingOnly, expiresAt: meta.expiresAt }],
506
+ warnings: warnings.length > 0 ? warnings : undefined,
507
+ };
508
+ }
474
509
  /** Dispatch a verify to the venue-specific implementation. */
475
510
  async function verifyVenue(ex, network, opts) {
476
511
  if (ex === "aster")
@@ -487,8 +522,10 @@ async function verifyVenue(ex, network, opts) {
487
522
  return verifyArcus(network, opts);
488
523
  if (ex === "qfex")
489
524
  return verifyQfex(network, opts);
525
+ if (ex === "risex")
526
+ return verifyRisex(network, opts);
490
527
  throw new PerpError("NOT_IMPLEMENTED", `${ex} has no agents to verify`, {
491
- remediation: "perp wallet agent verify <aster|hyperliquid|pacifica|lighter|lighter-rh|ondo|arcus|qfex> [agentName]",
528
+ remediation: "perp wallet agent verify <aster|hyperliquid|pacifica|lighter|lighter-rh|ondo|arcus|qfex|risex> [agentName]",
492
529
  });
493
530
  }
494
531
  /**
@@ -509,6 +546,9 @@ export function agentApproveUnsupportedReason(venue, network, op) {
509
546
  }
510
547
  /** Throws the uniform CAPABILITY_UNSUPPORTED error when agentApproveUnsupportedReason() is non-null. */
511
548
  export function assertAgentApproveSupported(venue, network, op) {
549
+ const def = getVenue(venue);
550
+ if (!def.roles.includes("trading"))
551
+ throw roleUnsupported(def, `agent ${op}`);
512
552
  if (venue === "qfex") {
513
553
  // Programmatic key creation needs a website session (SIWE + MFA) — not a CLI path.
514
554
  throw unsupported("qfex", "agentApprove", "API keys are created (and revoked) on the QFEX website, behind 2FA", `Create the key in QFEX Developer Settings, then import it: perp${network === "testnet" ? " -n testnet" : ""} wallet agent import qfex`);
@@ -540,6 +580,11 @@ export function defaultAgentName(venue, network) {
540
580
  case "ondo": return `perp-cli-ondo${suffix}`;
541
581
  case "arcus": return `perp-cli-arcus${suffix}`;
542
582
  case "qfex": return `perp-cli-qfex${suffix}`;
583
+ case "risex": return `perp-cli-risex${suffix}`;
584
+ case "txflow": return `perp-cli-txflow${suffix}`;
585
+ case "variational": return `perp-cli-variational${suffix}`;
586
+ case "grvt": return `perp-cli-grvt${suffix}`;
587
+ case "extended": return `perp-cli-extended${suffix}`;
543
588
  }
544
589
  }
545
590
  /**
@@ -622,7 +667,7 @@ function requirePassphrase(passphrase) {
622
667
  export async function approveVenueAgent(p) {
623
668
  assertAgentApproveSupported(p.venue, p.network, "approve");
624
669
  const agentName = p.agentName ?? defaultAgentName(p.venue, p.network);
625
- // Ondo: HMAC key entered on stdin only (design §10.4)
670
+ // Ondo: HMAC key entered on stdin only
626
671
  if (p.venue === "ondo") {
627
672
  assertVenueAgentNameFree("ondo", agentName, p.network);
628
673
  if (getVenueAgent("ondo", agentName) && !p.rotate) {
@@ -636,7 +681,7 @@ export async function approveVenueAgent(p) {
636
681
  setVenueAgent("ondo", meta);
637
682
  return { venue: "ondo", meta };
638
683
  }
639
- // Arcus: OWS Ed25519 agent + EIP-712 createApiKey (design §10.4)
684
+ // Arcus: OWS Ed25519 agent + EIP-712 createApiKey
640
685
  if (p.venue === "arcus") {
641
686
  const masterName = requireMaster(p.master);
642
687
  assertVenueAgentNameFree("arcus", agentName, p.network);
@@ -660,6 +705,53 @@ export async function approveVenueAgent(p) {
660
705
  releaseLock("arcus");
661
706
  }
662
707
  }
708
+ // RISEx: master-signed RegisterSigner, then an on-chain restriction to trading-only.
709
+ if (p.venue === "risex") {
710
+ const masterName = requireMaster(p.master);
711
+ assertVenueAgentNameFree("risex", agentName, p.network);
712
+ if (p.canWithdraw || p.canSpot) {
713
+ throw new PerpError("INVALID_PARAMS", "RISEx signers registered by perp-cli are always restricted to trading only (no fund movement, no spot)", {
714
+ remediation: "Drop --can-withdraw / --can-spot",
715
+ });
716
+ }
717
+ const expiresAt = parseExpiresInParam(p.expiresIn);
718
+ const { approveRisexAgent, revokeRisexAgent, risexAgentWalletName, risexRotatePreflight, risexSignerStatus } = await import("../exchanges/risex/agent.js");
719
+ acquireLock("risex");
720
+ try {
721
+ const existing = getVenueAgent("risex", agentName);
722
+ if (existing !== null) {
723
+ if (!p.rotate) {
724
+ throw new PerpError("INVALID_PARAMS", `RISEx signer "${agentName}" already exists`, {
725
+ remediation: `Replace it (revoke the old signer, register a fresh one): perp${p.network === "testnet" ? " -n testnet" : ""} wallet agent approve risex --rotate — or choose another --agent-name`,
726
+ });
727
+ }
728
+ // Rotate: the new signer's checks (pins, maintenance, master gas) pass first, then a
729
+ // venue-confirmed revoke of the old signer, then drop its key, then approve.
730
+ await risexRotatePreflight(masterName, p.passphrase, p.network);
731
+ // A kept record whose signer never registered (or is already revoked) has nothing to
732
+ // revoke on the venue: it is only forgotten locally. An Active one is revoked first.
733
+ const status = await risexSignerStatus(existing);
734
+ if (status === 1)
735
+ await revokeRisexAgent({ meta: existing, masterOwsWallet: masterName, passphrase: p.passphrase });
736
+ try {
737
+ loadOws().deleteWallet(existing.agentWalletName);
738
+ }
739
+ catch { /* already gone */ }
740
+ deleteVenueAgent("risex", agentName);
741
+ }
742
+ else if (p.rotate && loadOws().listWallets().some((w) => w.name === risexAgentWalletName(masterName, p.network))) {
743
+ throw new PerpError("INVALID_PARAMS", `OWS wallet "${risexAgentWalletName(masterName, p.network)}" exists but no RISEx signer record uses it`, {
744
+ remediation: `If no RISEx signer uses its key, delete it (ows wallet delete ${risexAgentWalletName(masterName, p.network)}) and re-run the approve`,
745
+ });
746
+ }
747
+ const meta = await approveRisexAgent({ agentName, masterOwsWallet: masterName, passphrase: p.passphrase, network: p.network, expiresAt });
748
+ setVenueAgent("risex", meta);
749
+ return { venue: "risex", meta };
750
+ }
751
+ finally {
752
+ releaseLock("risex");
753
+ }
754
+ }
663
755
  const expiresAt = parseExpiresInParam(p.expiresIn);
664
756
  const masterName = requireMaster(p.master);
665
757
  const expiresAtIso = expiresAt.toISOString();
@@ -993,6 +1085,16 @@ export function printApproveOutcome(outcome) {
993
1085
  console.log(` Valid Until: ${chalk.cyan(outcome.meta.validUntil)}`);
994
1086
  console.log();
995
1087
  return;
1088
+ case "risex":
1089
+ console.log(chalk.green.bold("\n RISEx signer registered and restricted to trading-only!\n"));
1090
+ console.log(` Name: ${chalk.cyan(outcome.meta.agentName)}`);
1091
+ console.log(` Signer: ${chalk.cyan(outcome.meta.signerAddress)}`);
1092
+ console.log(` Account: ${chalk.cyan(outcome.meta.accountAddress)}`);
1093
+ console.log(` Expires: ${chalk.cyan(outcome.meta.expiresAt)}`);
1094
+ console.log(` Permissions: ${chalk.cyan("trading only")} ${chalk.gray(`(on-chain; disabled ${outcome.meta.permissions.disabled.join(", ")}; verified ${outcome.meta.permissions.verifiedAt})`)}`);
1095
+ console.log(chalk.gray(" Deposits / withdrawals: RISEx app only (perp-cli never moves RISEx funds)."));
1096
+ console.log();
1097
+ return;
996
1098
  case "lighter":
997
1099
  case "lighter-rh":
998
1100
  console.log(chalk.green.bold(`\n ${getVenue(outcome.venue).displayName} Agent approved successfully!\n`));
@@ -1043,7 +1145,7 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
1043
1145
  // ── agent approve <exchange> ── register an agent wallet ──────────────────
1044
1146
  agent
1045
1147
  .command("approve <exchange>")
1046
- .description("Register an agent wallet / API key for an exchange (aster, hyperliquid, pacifica, lighter, lighter-rh, ondo, arcus)")
1148
+ .description("Register an agent wallet / API key for an exchange (aster, hyperliquid, pacifica, lighter, lighter-rh, ondo, arcus, risex — risex: a trading-only session signer, restricted on-chain)")
1047
1149
  .option("--master <name>", "Master OWS wallet name")
1048
1150
  .option("--agent-name <name>", "Agent name (default: perp-cli-aster)")
1049
1151
  .option("--expires-in <duration>", "Expiry: 30d, 90d, 180d, 1y, or ISO-8601 datetime (default: 90d)")
@@ -1087,7 +1189,8 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
1087
1189
  assertAgentApproveSupported(venue, network, "approve");
1088
1190
  let master = opts.master ?? null;
1089
1191
  let agentName = opts.agentName ?? null;
1090
- let expiresIn = opts.expiresIn ?? "90d";
1192
+ // RISEx signers default to 30 days (owner decision; the venue maximum is not documented).
1193
+ let expiresIn = opts.expiresIn ?? (venue === "risex" ? "30d" : "90d");
1091
1194
  let canPerp = opts.canPerp ?? true;
1092
1195
  let canSpot = opts.canSpot ?? false;
1093
1196
  let canWithdraw = opts.canWithdraw ?? false;
@@ -1218,6 +1321,9 @@ Examples:
1218
1321
  }
1219
1322
  const network = networkOf(command);
1220
1323
  const venue = resolveVenueId(exchange);
1324
+ // TxFlow (market data only) / RISEx (perp-cli-registered, restricted signers only): refused before any stdin read.
1325
+ const { assertImportSupported } = await import("./agent-import.js");
1326
+ assertImportSupported(venue, network);
1221
1327
  const int = (flag, raw) => {
1222
1328
  if (raw === undefined)
1223
1329
  return null;
@@ -1296,14 +1402,14 @@ Examples:
1296
1402
  for (const { exchange: ex, meta } of listVenueAgents()) {
1297
1403
  if (venueFilter !== null && venueFilter !== ex)
1298
1404
  continue;
1299
- const expiresAt = meta.kind === "arcus" ? meta.validUntil : null;
1405
+ const expiresAt = meta.kind === "arcus" ? meta.validUntil : meta.kind === "risex" ? meta.expiresAt : null;
1300
1406
  const status = expiresAt !== null && expiresAt !== "" && new Date(expiresAt).getTime() < now ? "expired" : "active";
1301
1407
  rows.push({
1302
1408
  name: meta.agentName,
1303
1409
  exchange: ex,
1304
1410
  network: meta.network,
1305
1411
  source: agentSourceOf(ex, meta),
1306
- evmAddress: meta.kind === "arcus" ? meta.ownerAddress : "",
1412
+ evmAddress: meta.kind === "arcus" ? meta.ownerAddress : meta.kind === "risex" ? meta.signerAddress : "",
1307
1413
  // Ondo keys carry no expiry; shown as "—".
1308
1414
  expiresAt: expiresAt ?? "",
1309
1415
  status,
@@ -1373,9 +1479,11 @@ Examples:
1373
1479
  network = networkOf(command);
1374
1480
  // An imported key is only ever removed locally, so it needs no venue support for revoke
1375
1481
  // (e.g. an imported Aster testnet key).
1376
- const rec = exchange === "ondo" || exchange === "arcus" || exchange === "qfex" ? getVenueAgent(exchange, agentName) : getAgent(exchange, agentName);
1482
+ const rec = exchange === "ondo" || exchange === "arcus" || exchange === "qfex" || exchange === "risex" ? getVenueAgent(exchange, agentName) : getAgent(exchange, agentName);
1377
1483
  // QFEX keys only exist as imports: a missing record is "already removed", never an approve-capability error.
1378
- if (exchange !== "qfex" && (rec === null || agentSourceOf(exchange, rec) !== "imported"))
1484
+ // A release-gated venue (RISEx in 0.15.0) still lets --force / --local-only clear local state: nothing is signed or sent.
1485
+ const localCleanupOfGated = localOnly && getVenue(exchange).gatedRoles !== null;
1486
+ if (exchange !== "qfex" && !localCleanupOfGated && (rec === null || agentSourceOf(exchange, rec) !== "imported"))
1379
1487
  assertAgentApproveSupported(exchange, network, "revoke");
1380
1488
  }
1381
1489
  catch (e) {
@@ -1425,6 +1533,46 @@ Examples:
1425
1533
  console.log(chalk.green(`Agent "${agentName}" on qfex removed locally.`) + chalk.yellow(`\n ${note}`));
1426
1534
  return;
1427
1535
  }
1536
+ // ── RISEx: RevokeSigner signed by the master wallet (gasless REST); then the key is deleted ──
1537
+ if (exchange === "risex") {
1538
+ const risexMeta = getVenueAgent("risex", agentName);
1539
+ if (!risexMeta) {
1540
+ if (useJson)
1541
+ printJson({ ok: true, data: { alreadyRevoked: true } });
1542
+ else
1543
+ console.log(`Agent "${agentName}" on risex not found locally — already revoked.`);
1544
+ return;
1545
+ }
1546
+ if (risexMeta.network !== network) {
1547
+ throw new PerpError("AGENT_NOT_REGISTERED", `RISEx signer "${agentName}" is registered on ${risexMeta.network}, not ${network}`, {
1548
+ remediation: risexMeta.network === "mainnet" ? "Run without -n testnet to revoke it" : "Run with -n testnet to revoke it",
1549
+ });
1550
+ }
1551
+ let txHash = null;
1552
+ if (!localOnly) {
1553
+ const masterName = opts.master ?? loadSettings().owsActiveWallet;
1554
+ if (!masterName) {
1555
+ throw new PerpError("INVALID_PARAMS", "Master wallet name is required to revoke a RISEx signer", {
1556
+ remediation: "Use --master <name>, or --force to clear local state only (then revoke it in the RISEx app)",
1557
+ });
1558
+ }
1559
+ const passphrase = await resolvePassphrase({ flag: passphraseFlag });
1560
+ const { revokeRisexAgent } = await import("../exchanges/risex/agent.js");
1561
+ txHash = (await revokeRisexAgent({ meta: risexMeta, masterOwsWallet: masterName, passphrase })).txHash;
1562
+ }
1563
+ try {
1564
+ loadOws().deleteWallet(risexMeta.agentWalletName);
1565
+ }
1566
+ catch { /* already gone */ }
1567
+ deleteVenueAgent("risex", agentName);
1568
+ if (useJson)
1569
+ printJson(jsonOk({ revoked: !localOnly, local: localOnly, txHash }));
1570
+ else
1571
+ console.log(localOnly
1572
+ ? chalk.yellow(`Agent "${agentName}" on risex removed locally only — revoke signer ${risexMeta.signerAddress} in the RISEx app.`)
1573
+ : chalk.green(`Agent "${agentName}" on risex revoked successfully.`));
1574
+ return;
1575
+ }
1428
1576
  // ── Arcus: EIP-712 RevokeApiKey signed by the master wallet ──
1429
1577
  if (exchange === "arcus") {
1430
1578
  const arcusMeta = getVenueAgent("arcus", agentName);
@@ -420,7 +420,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
420
420
  };
421
421
  // Collect spot balances per exchange
422
422
  const spotBalancesByExchange = new Map();
423
- // Bot/arb spot scope (§12.4 #3): configured venues with a separate spot balance + transfer.
423
+ // Bot/arb spot scope: configured venues with a separate spot balance + transfer.
424
424
  const configuredVenues = new Set(await listConfiguredAccountVenues());
425
425
  for (const def of listBotSpotVenues()) {
426
426
  if (!configuredVenues.has(def.id))
@@ -802,7 +802,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
802
802
  await dropStaleStateEntry(sym, opts.yes === true, dryRun);
803
803
  return;
804
804
  }
805
- // Global quarantine (design §12.3): an unknown / unhedged outcome was recorded for
805
+ // Global quarantine: an unknown / unhedged outcome was recorded for
806
806
  // this symbol. A human may still close it, but only knowingly (--force).
807
807
  const quarantined = quarantineEntryFor(sym, (m) => console.error(chalk.red(m)));
808
808
  if (quarantined !== null) {
@@ -846,7 +846,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
846
846
  console.log(chalk.cyan(` [DRY-RUN] Would close spot-perp ${sym}: sell spot on ${spotExch} + buy back the perp short on ${perpExch}`));
847
847
  return;
848
848
  }
849
- // One implementation for spot-perp close (fill-checked legs, §12.4 #5): arb-auto's handler.
849
+ // One implementation for spot-perp close (fill-checked legs): arb-auto's handler.
850
850
  if (handleSpotClose === undefined)
851
851
  throw new Error("spot-perp close handler is not registered (registerArbAutoCommands not called)");
852
852
  await handleSpotClose(sym, { spotExch, perpExch }, getAdapterForExchange, isJson);
@@ -982,7 +982,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
982
982
  // Close the legs sequentially (long, then short), reduce-only and verified, each
983
983
  // sized from the venue's own position size. No retries: a definite failure of the
984
984
  // long close changes nothing; an unknown outcome or a leg left open after the
985
- // other closed quarantines the symbol (design §12.3).
985
+ // other closed quarantines the symbol.
986
986
  const results = [];
987
987
  const closeRes = await closePerpPairVerified({
988
988
  source: "arb-close", sym,