perp-cli 0.14.2 → 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 (313) 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 +9 -9
  23. package/dist/bot/strategies/funding-arb-utils.d.ts +15 -2
  24. package/dist/bot/strategies/funding-arb-utils.js +43 -24
  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 +23 -12
  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 +168 -36
  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/dashboard/ui.js +1 -1
  73. package/dist/errors.d.ts +24 -1
  74. package/dist/errors.js +25 -3
  75. package/dist/exchanges/arcus/adapter.d.ts +7 -7
  76. package/dist/exchanges/arcus/adapter.js +12 -12
  77. package/dist/exchanges/arcus/agent.d.ts +1 -1
  78. package/dist/exchanges/arcus/agent.js +1 -1
  79. package/dist/exchanges/arcus/client.js +1 -1
  80. package/dist/exchanges/arcus/signing.d.ts +2 -3
  81. package/dist/exchanges/arcus/signing.js +1 -1
  82. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  83. package/dist/exchanges/arcus/spot-chain.js +1 -1
  84. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  85. package/dist/exchanges/arcus/spot-router.js +2 -2
  86. package/dist/exchanges/arcus/spot.d.ts +2 -3
  87. package/dist/exchanges/arcus/spot.js +2 -3
  88. package/dist/exchanges/aster-spot.d.ts +3 -3
  89. package/dist/exchanges/aster-spot.js +6 -9
  90. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  91. package/dist/exchanges/aster-typed-data.js +1 -1
  92. package/dist/exchanges/aster.d.ts +9 -4
  93. package/dist/exchanges/aster.js +125 -55
  94. package/dist/exchanges/builder-fee.d.ts +1 -1
  95. package/dist/exchanges/builder-fee.js +13 -2
  96. package/dist/exchanges/canonical-symbols.d.ts +1 -2
  97. package/dist/exchanges/canonical-symbols.js +26 -2
  98. package/dist/exchanges/derive/client.d.ts +37 -0
  99. package/dist/exchanges/derive/client.js +135 -0
  100. package/dist/exchanges/derive/options.d.ts +68 -0
  101. package/dist/exchanges/derive/options.js +360 -0
  102. package/dist/exchanges/extended/adapter.d.ts +82 -0
  103. package/dist/exchanges/extended/adapter.js +179 -0
  104. package/dist/exchanges/extended/client.d.ts +38 -0
  105. package/dist/exchanges/extended/client.js +88 -0
  106. package/dist/exchanges/extended/public.d.ts +130 -0
  107. package/dist/exchanges/extended/public.js +429 -0
  108. package/dist/exchanges/grvt/adapter.d.ts +86 -0
  109. package/dist/exchanges/grvt/adapter.js +206 -0
  110. package/dist/exchanges/grvt/client.d.ts +25 -0
  111. package/dist/exchanges/grvt/client.js +86 -0
  112. package/dist/exchanges/grvt/public.d.ts +151 -0
  113. package/dist/exchanges/grvt/public.js +438 -0
  114. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  115. package/dist/exchanges/hyperliquid-spot.js +5 -5
  116. package/dist/exchanges/hyperliquid.d.ts +22 -16
  117. package/dist/exchanges/hyperliquid.js +80 -55
  118. package/dist/exchanges/index.d.ts +1 -1
  119. package/dist/exchanges/index.js +1 -1
  120. package/dist/exchanges/interface.d.ts +97 -8
  121. package/dist/exchanges/interface.js +19 -2
  122. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  123. package/dist/exchanges/ioc-outcome.js +1 -1
  124. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  125. package/dist/exchanges/legacy-trading-v2.js +7 -2
  126. package/dist/exchanges/lighter-public.d.ts +2 -2
  127. package/dist/exchanges/lighter-public.js +2 -2
  128. package/dist/exchanges/lighter-signer.d.ts +1 -1
  129. package/dist/exchanges/lighter-signer.js +1 -1
  130. package/dist/exchanges/lighter-spot.d.ts +3 -4
  131. package/dist/exchanges/lighter-spot.js +6 -7
  132. package/dist/exchanges/lighter.d.ts +11 -11
  133. package/dist/exchanges/lighter.js +28 -14
  134. package/dist/exchanges/ondo/adapter.d.ts +5 -6
  135. package/dist/exchanges/ondo/adapter.js +16 -14
  136. package/dist/exchanges/ondo/agent.d.ts +1 -1
  137. package/dist/exchanges/ondo/agent.js +1 -1
  138. package/dist/exchanges/ondo/client.d.ts +1 -1
  139. package/dist/exchanges/ondo/client.js +2 -2
  140. package/dist/exchanges/ondo/public.d.ts +8 -1
  141. package/dist/exchanges/ondo/public.js +13 -2
  142. package/dist/exchanges/ondo/spot.d.ts +2 -2
  143. package/dist/exchanges/ondo/spot.js +3 -3
  144. package/dist/exchanges/options-interface.d.ts +158 -0
  145. package/dist/exchanges/options-interface.js +17 -0
  146. package/dist/exchanges/options-venues.d.ts +12 -0
  147. package/dist/exchanges/options-venues.js +35 -0
  148. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  149. package/dist/exchanges/pacifica-atomic.js +440 -0
  150. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  151. package/dist/exchanges/pacifica-decimal.js +51 -0
  152. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  153. package/dist/exchanges/pacifica-presigned.js +141 -0
  154. package/dist/exchanges/pacifica-print.d.ts +200 -0
  155. package/dist/exchanges/pacifica-print.js +417 -0
  156. package/dist/exchanges/pacifica-spot.d.ts +2 -2
  157. package/dist/exchanges/pacifica-spot.js +3 -3
  158. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  159. package/dist/exchanges/pacifica-typed-data.js +1 -1
  160. package/dist/exchanges/pacifica-vaults.d.ts +121 -0
  161. package/dist/exchanges/pacifica-vaults.js +264 -0
  162. package/dist/exchanges/pacifica.d.ts +32 -5
  163. package/dist/exchanges/pacifica.js +114 -48
  164. package/dist/exchanges/precision.d.ts +3 -1
  165. package/dist/exchanges/precision.js +9 -2
  166. package/dist/exchanges/qfex/adapter.d.ts +18 -5
  167. package/dist/exchanges/qfex/adapter.js +32 -12
  168. package/dist/exchanges/qfex/agent.d.ts +1 -1
  169. package/dist/exchanges/qfex/agent.js +1 -1
  170. package/dist/exchanges/qfex/client.d.ts +1 -1
  171. package/dist/exchanges/qfex/client.js +1 -1
  172. package/dist/exchanges/qfex/errors.d.ts +1 -1
  173. package/dist/exchanges/qfex/errors.js +2 -2
  174. package/dist/exchanges/qfex/public.d.ts +3 -3
  175. package/dist/exchanges/qfex/public.js +3 -3
  176. package/dist/exchanges/qfex/signing.d.ts +2 -2
  177. package/dist/exchanges/qfex/signing.js +4 -5
  178. package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
  179. package/dist/exchanges/qfex/ws-transport.js +3 -3
  180. package/dist/exchanges/rate-limiter.d.ts +1 -1
  181. package/dist/exchanges/rate-limiter.js +5 -2
  182. package/dist/exchanges/registry.d.ts +1 -1
  183. package/dist/exchanges/registry.js +1 -1
  184. package/dist/exchanges/risex/adapter.d.ts +148 -0
  185. package/dist/exchanges/risex/adapter.js +796 -0
  186. package/dist/exchanges/risex/agent.d.ts +154 -0
  187. package/dist/exchanges/risex/agent.js +384 -0
  188. package/dist/exchanges/risex/client.d.ts +47 -0
  189. package/dist/exchanges/risex/client.js +174 -0
  190. package/dist/exchanges/risex/nonce.d.ts +43 -0
  191. package/dist/exchanges/risex/nonce.js +212 -0
  192. package/dist/exchanges/risex/public.d.ts +64 -0
  193. package/dist/exchanges/risex/public.js +150 -0
  194. package/dist/exchanges/risex/session.d.ts +71 -0
  195. package/dist/exchanges/risex/session.js +171 -0
  196. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  197. package/dist/exchanges/risex/typed-data.js +240 -0
  198. package/dist/exchanges/risex/units.d.ts +29 -0
  199. package/dist/exchanges/risex/units.js +104 -0
  200. package/dist/exchanges/slippage.d.ts +1 -1
  201. package/dist/exchanges/slippage.js +1 -1
  202. package/dist/exchanges/spot-factory.d.ts +3 -3
  203. package/dist/exchanges/spot-factory.js +7 -7
  204. package/dist/exchanges/spot-interface.d.ts +5 -5
  205. package/dist/exchanges/spot-interface.js +2 -2
  206. package/dist/exchanges/spot-result.d.ts +3 -3
  207. package/dist/exchanges/spot-result.js +3 -3
  208. package/dist/exchanges/trading-v2.d.ts +3 -3
  209. package/dist/exchanges/trading-v2.js +9 -6
  210. package/dist/exchanges/txflow/adapter.d.ts +54 -0
  211. package/dist/exchanges/txflow/adapter.js +118 -0
  212. package/dist/exchanges/txflow/client.d.ts +47 -0
  213. package/dist/exchanges/txflow/client.js +153 -0
  214. package/dist/exchanges/txflow/public.d.ts +76 -0
  215. package/dist/exchanges/txflow/public.js +284 -0
  216. package/dist/exchanges/unknown-symbol.d.ts +29 -0
  217. package/dist/exchanges/unknown-symbol.js +51 -0
  218. package/dist/exchanges/variational/adapter.d.ts +71 -0
  219. package/dist/exchanges/variational/adapter.js +137 -0
  220. package/dist/exchanges/variational/client.d.ts +21 -0
  221. package/dist/exchanges/variational/client.js +66 -0
  222. package/dist/exchanges/variational/public.d.ts +126 -0
  223. package/dist/exchanges/variational/public.js +430 -0
  224. package/dist/exchanges/venue-types.d.ts +6 -7
  225. package/dist/exchanges/venue-types.js +2 -2
  226. package/dist/exchanges/venue.d.ts +51 -8
  227. package/dist/exchanges/venue.js +54 -4
  228. package/dist/exchanges/venues/arcus.d.ts +1 -2
  229. package/dist/exchanges/venues/arcus.js +5 -4
  230. package/dist/exchanges/venues/aster.d.ts +2 -2
  231. package/dist/exchanges/venues/aster.js +6 -4
  232. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  233. package/dist/exchanges/venues/create-helpers.js +3 -3
  234. package/dist/exchanges/venues/extended.d.ts +21 -0
  235. package/dist/exchanges/venues/extended.js +96 -0
  236. package/dist/exchanges/venues/grvt.d.ts +30 -0
  237. package/dist/exchanges/venues/grvt.js +98 -0
  238. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  239. package/dist/exchanges/venues/hyperliquid.js +10 -8
  240. package/dist/exchanges/venues/index.d.ts +7 -2
  241. package/dist/exchanges/venues/index.js +8 -2
  242. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  243. package/dist/exchanges/venues/lighter-family.js +9 -11
  244. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  245. package/dist/exchanges/venues/lighter-rh.js +7 -5
  246. package/dist/exchanges/venues/lighter.d.ts +1 -1
  247. package/dist/exchanges/venues/lighter.js +5 -3
  248. package/dist/exchanges/venues/ondo.d.ts +1 -1
  249. package/dist/exchanges/venues/ondo.js +5 -3
  250. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  251. package/dist/exchanges/venues/pacifica.js +6 -4
  252. package/dist/exchanges/venues/qfex.d.ts +1 -1
  253. package/dist/exchanges/venues/qfex.js +4 -2
  254. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  255. package/dist/exchanges/venues/risex-release.js +14 -0
  256. package/dist/exchanges/venues/risex.d.ts +31 -0
  257. package/dist/exchanges/venues/risex.js +116 -0
  258. package/dist/exchanges/venues/txflow.d.ts +18 -0
  259. package/dist/exchanges/venues/txflow.js +77 -0
  260. package/dist/exchanges/venues/variational.d.ts +21 -0
  261. package/dist/exchanges/venues/variational.js +85 -0
  262. package/dist/execution-log.d.ts +1 -1
  263. package/dist/funding/legacy-public.d.ts +1 -1
  264. package/dist/funding/legacy-public.js +9 -4
  265. package/dist/funding/normalize.d.ts +1 -1
  266. package/dist/funding/normalize.js +1 -1
  267. package/dist/funding/rates.d.ts +44 -4
  268. package/dist/funding/rates.js +56 -12
  269. package/dist/index.js +7 -6
  270. package/dist/jobs.d.ts +1 -0
  271. package/dist/jobs.js +25 -1
  272. package/dist/landing.d.ts +2 -0
  273. package/dist/landing.js +1 -1
  274. package/dist/mcp-pacifica-earn.d.ts +9 -0
  275. package/dist/mcp-pacifica-earn.js +58 -0
  276. package/dist/mcp-server.js +113 -59
  277. package/dist/pacifica/client.d.ts +67 -4
  278. package/dist/pacifica/client.js +170 -8
  279. package/dist/pacifica/rate-limit.d.ts +144 -0
  280. package/dist/pacifica/rate-limit.js +267 -0
  281. package/dist/pacifica/types/order.d.ts +2 -1
  282. package/dist/plan-executor.js +7 -2
  283. package/dist/rebalance.js +1 -1
  284. package/dist/risk.d.ts +2 -1
  285. package/dist/risk.js +2 -4
  286. package/dist/settings.d.ts +32 -3
  287. package/dist/signer/ows-evm.d.ts +1 -1
  288. package/dist/signer/ows-evm.js +1 -1
  289. package/dist/smart-order.d.ts +1 -1
  290. package/dist/smart-order.js +1 -1
  291. package/dist/strategies/funding-arb.d.ts +1 -1
  292. package/dist/strategies/funding-arb.js +1 -1
  293. package/dist/trade-validator.d.ts +1 -1
  294. package/dist/trade-validator.js +9 -4
  295. package/dist/utils.d.ts +12 -3
  296. package/dist/utils.js +11 -3
  297. package/dist/venue-health.d.ts +40 -0
  298. package/dist/venue-health.js +75 -0
  299. package/dist/withdraw-gate.d.ts +1 -1
  300. package/dist/withdraw-gate.js +1 -1
  301. package/dist/ws/index.d.ts +959 -0
  302. package/dist/ws/index.js +4295 -0
  303. package/dist/ws/node.d.ts +975 -0
  304. package/dist/ws/node.js +4366 -0
  305. package/dist/ws/pacifica-instrument.d.ts +8 -0
  306. package/dist/ws/pacifica-instrument.js +8 -0
  307. package/dist/ws-cli/credentials.d.ts +62 -0
  308. package/dist/ws-cli/credentials.js +126 -0
  309. package/package.json +58 -10
  310. package/skills/perp-cli/SKILL.md +62 -9
  311. package/skills/perp-cli/references/agent-operations.md +7 -0
  312. package/skills/perp-cli/references/commands.md +76 -5
  313. package/skills/perp-cli/references/ws-library.md +492 -0
@@ -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,
@@ -20,7 +20,7 @@ import { loadArbState, addPosition as persistAddPosition, removePosition as pers
20
20
  import { handleRates, handleBasisScan, handleDexScan, handleCompare, handleFundingHistory, handleFundingPositions, fetchAllPrices, printGapTable, printTrackSummary, formatGapPrice, } from "./arb.js";
21
21
  // ── Fee-Adjusted Net Spread Calculation ──
22
22
  import { getTakerFee } from "../constants.js";
23
- import { assertFundingArbVenue, getVenue, listVenuesWithRole, networkConfig, resolveVenueId, tryResolveVenueId } from "../exchanges/venue.js";
23
+ import { assertFundingArbVenue, getVenue, listVenuesWithRole, networkConfig, resolveVenueId, roleUnsupported, tryResolveVenueId } from "../exchanges/venue.js";
24
24
  import { sharedApiNetwork } from "../api/public/urls.js";
25
25
  import { findSpotQuoteBalance, readSpotQuoteAvailableFresh, requireBotSpotVenue, requireSpotAdapter, spotNetworkOf, spotPairSymbol, spotSharesPerpMargin, sweepSpotQuoteToPerp, transferQuoteToPerp, transferQuoteToSpot } from "../exchanges/spot-factory.js";
26
26
  import { fillSizeString, readSpotBalancesFresh, sizeDecimalsOf, spotLotStepOf, spotMarketFill } from "../exchanges/spot-result.js";
@@ -821,7 +821,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
821
821
  if (stop.requested())
822
822
  break; // stopping: no new close sequence
823
823
  const pos = openPositions[i];
824
- // Quarantined (design §12.3): hands off until a human clears it.
824
+ // Quarantined: hands off until a human clears it.
825
825
  if (isQuarantined(pos.symbol, (m) => console.log(chalk.red(m))))
826
826
  continue;
827
827
  // Per-position isolation: one position's failing exit check (e.g. an
@@ -877,7 +877,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
877
877
  try {
878
878
  const longAdapter = await getAdapterForExchange(pos.longExchange);
879
879
  const shortAdapter = await getAdapterForExchange(pos.shortExchange);
880
- // Sequential, reduce-only, verified (design §12.3): long first, then
880
+ // Sequential, reduce-only, verified: long first, then
881
881
  // short. A definite long-close failure changes nothing → retried next
882
882
  // cycle, quarantined after MAX_PAIR_CLOSE_FAILURES in a row. An unknown
883
883
  // outcome, a partial close or an unhedged leg quarantines (no retry).
@@ -1114,7 +1114,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1114
1114
  skip(snap.symbol, "already open");
1115
1115
  continue;
1116
1116
  }
1117
- // Global quarantine (design §12.3): unknown / unhedged outcome on this symbol somewhere.
1117
+ // Global quarantine: unknown / unhedged outcome on this symbol somewhere.
1118
1118
  if (isQuarantined(snap.symbol, (m) => console.log(chalk.red(m)))) {
1119
1119
  skip(snap.symbol, "quarantined");
1120
1120
  continue;
@@ -1234,7 +1234,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1234
1234
  try {
1235
1235
  const longAdapter = await getAdapterForExchange(longExchange);
1236
1236
  const shortAdapter = await getAdapterForExchange(shortExchange);
1237
- // Sequential verified legs (design §12.3): long, then short sized to the
1237
+ // Sequential verified legs: long, then short sized to the
1238
1238
  // long's verified fill; reduce-only rollback / trim; unknown → quarantine.
1239
1239
  legsStarted = true;
1240
1240
  const res = await openPerpPairVerified({
@@ -1445,7 +1445,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1445
1445
  .option("--duration <minutes>", "Track duration in minutes (with --track)", "10")
1446
1446
  .option("--beep", "Beep on large gaps (with --gaps)")
1447
1447
  .option("--check-liquidity", "Check orderbook depth in live mode (slower)")
1448
- .option("--include-unverified", "With --rates / --compare: also show venues whose funding rate is unverified (qfex) — display only, never ranked for trading")
1448
+ .option("--include-unverified", "With --rates / --compare: also show venues whose funding rate is unverified (qfex; txflow — unofficial API; variational; grvt — indicative rate; extended) — display only, never ranked for trading")
1449
1449
  .action(async (opts) => {
1450
1450
  // ── Price gap modes ──
1451
1451
  if (opts.gaps || opts.track || opts.alert) {
@@ -1857,6 +1857,8 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1857
1857
  for (const ex of [longExch, shortExch]) {
1858
1858
  const venue = getVenue(ex);
1859
1859
  if (!venue.roles.includes("trading")) {
1860
+ if (venue.gatedRoles !== null)
1861
+ throw roleUnsupported(venue, "trading");
1860
1862
  throw unsupported(venue.displayName, "trading", "no trading API — it cannot be an arb leg", `Use a trading venue: ${listVenuesWithRole("trading").map(v => v.id).join(", ")}`);
1861
1863
  }
1862
1864
  }
@@ -1867,7 +1869,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1867
1869
  console.log(chalk.red(" Long and short exchange must be different."));
1868
1870
  return;
1869
1871
  }
1870
- // Global quarantine (design §12.3): an unknown / unhedged outcome on this symbol
1872
+ // Global quarantine: an unknown / unhedged outcome on this symbol
1871
1873
  // anywhere blocks new trades until a human clears it.
1872
1874
  const quarantined = quarantineEntryFor(sym, (m) => console.error(m));
1873
1875
  if (quarantined !== null) {
@@ -1875,7 +1877,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1875
1877
  remediation: `${quarantined.remediation} — then: perp strategy quarantine-clear ${sym}`,
1876
1878
  });
1877
1879
  }
1878
- // A venue whose funding rate is unverified is never a funding-arb leg (design §14.12 #6).
1880
+ // A venue whose funding rate is unverified is never a funding-arb leg.
1879
1881
  assertFundingArbVenue(longExch);
1880
1882
  assertFundingArbVenue(shortExch);
1881
1883
  const longAdapter = await getAdapterForExchange(longExch);
@@ -1991,7 +1993,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
1991
1993
  }
1992
1994
  if (!isJson())
1993
1995
  console.log(chalk.gray(` Executing legs sequentially (long, then short sized to the verified long fill)...\n`));
1994
- // 5. Sequential verified legs (design §12.3). The second leg is sized to the
1996
+ // 5. Sequential verified legs. The second leg is sized to the
1995
1997
  // first leg's VERIFIED fill, any shortfall is trimmed off the long
1996
1998
  // (reduce-only, verified — always, --reconcile is implied), a definite short
1997
1999
  // failure rolls the long back reduce-only, and an unknown outcome (incl. a
@@ -2070,12 +2072,14 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2070
2072
  // Resolve exchange aliases
2071
2073
  spotExch = resolveVenueId(spotExch);
2072
2074
  perpExch = resolveVenueId(perpExch);
2073
- // Validate spot exchange: bot/arb spot scope (§12.4 #3) — throws CAPABILITY_UNSUPPORTED
2075
+ // Validate spot exchange: bot/arb spot scope — throws CAPABILITY_UNSUPPORTED
2074
2076
  // for venues without spot or outside the scope (pacifica shared margin, arcus swaps).
2075
2077
  const spotDef = requireBotSpotVenue(spotExch.replace(/-spot$/, ""));
2076
2078
  spotExch = spotDef.id;
2077
2079
  perpExch = tryResolveVenueId(perpExch) ?? perpExch;
2078
2080
  if (!getVenue(perpExch).roles.includes("trading")) {
2081
+ if (getVenue(perpExch).gatedRoles !== null)
2082
+ throw roleUnsupported(getVenue(perpExch), "trading");
2079
2083
  throw unsupported(perpExch, "trading", "no trading API — it cannot be the perp leg", `Use a trading venue: ${listVenuesWithRole("trading").map(v => v.id).join(", ")}`);
2080
2084
  }
2081
2085
  assertFundingArbVenue(perpExch);
@@ -2093,7 +2097,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2093
2097
  return;
2094
2098
  console.log(chalk.red(` ${msg}`));
2095
2099
  };
2096
- // Global quarantine (design §12.3): checked before anything is read or moved.
2100
+ // Global quarantine: checked before anything is read or moved.
2097
2101
  const quarantined = quarantineEntryFor(sym, (m) => console.error(m));
2098
2102
  if (quarantined !== null) {
2099
2103
  throw new PerpError("RISK_VIOLATION", `${sym} is quarantined (${quarantined.strategy}: ${quarantined.reason})`, {
@@ -2273,7 +2277,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2273
2277
  // 7. Execute both legs. Same exchange shares a nonce → sequential; else parallel.
2274
2278
  if (!isJson())
2275
2279
  console.log(chalk.gray(` Executing ${sameVenue ? "sequentially" : "both legs simultaneously"}...\n`));
2276
- // The perp leg goes through placePerpVerified (design §12.3): a fill is the measured
2280
+ // The perp leg goes through placePerpVerified: a fill is the measured
2277
2281
  // position delta; an unknown outcome is ORDER_PENDING (quarantine, never roll back).
2278
2282
  let spotResult;
2279
2283
  let perpResult;
@@ -2523,7 +2527,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2523
2527
  remediation: `Pass both: perp arb close ${sym} --spot-exch <spot venue> --perp-exch <perp venue>`,
2524
2528
  });
2525
2529
  }
2526
- // Load adapters (bot/arb spot scope — §12.4 #3)
2530
+ // Load adapters (bot/arb spot scope)
2527
2531
  const spotDef = requireBotSpotVenue(spotExch.replace(/-spot$/, ""));
2528
2532
  const spotAdapter = await requireSpotAdapter(spotDef.id, await getAdapter(spotDef.id));
2529
2533
  const perpAdapter = await getAdapter(perpExch);
@@ -2551,12 +2555,12 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
2551
2555
  // Read before anything moves — a failure aborts the close.
2552
2556
  const preCloseQuote = await readSpotQuoteAvailableFresh(spotAdapter);
2553
2557
  // Same exchange → sequential (nonce collision), different → parallel.
2554
- // The spot leg's REAL fill decides the outcome (§12.4 #5): only a confirmed spot sale
2558
+ // The spot leg's REAL fill decides the outcome: only a confirmed spot sale
2555
2559
  // counts as closed; legs are re-matched so no naked short / unhedged spot is left.
2556
2560
  const sameVenue = spotDef.id === (tryResolveVenueId(perpExch) ?? perpExch);
2557
2561
  const closeDec = sizeDecimalsOf(closeSize);
2558
- // The perp buy-back and every re-match order go through placePerpVerified (design
2559
- // §12.3): only a measured position change counts; an unknown outcome stops the close.
2562
+ // The perp buy-back and every re-match order go through placePerpVerified:
2563
+ // only a measured position change counts; an unknown outcome stops the close.
2560
2564
  let spotResult;
2561
2565
  let perpResult;
2562
2566
  if (sameVenue) {
@@ -3073,7 +3077,7 @@ export function registerArbAutoCommands(program, getAdapterForExchange, isJson,
3073
3077
  (p.long.dex === pos.shortDex && p.short.dex === pos.longDex)));
3074
3078
  const currentSpread = current?.annualSpread ?? 0;
3075
3079
  if (currentSpread <= closeSpread || !current) {
3076
- // Quarantined (design §12.3): hands off until a human clears it.
3080
+ // Quarantined: hands off until a human clears it.
3077
3081
  if (isQuarantined(pos.underlying, (m) => console.error(chalk.red(m))))
3078
3082
  continue;
3079
3083
  const reason = !current ? "pair disappeared" : `spread ${currentSpread.toFixed(1)}% <= ${closeSpread}%`;
@@ -29,6 +29,18 @@ export declare function handleRates(isJson: () => boolean, opts: {
29
29
  top?: string;
30
30
  includeUnverified?: boolean;
31
31
  }): Promise<void>;
32
+ /**
33
+ * Cross-venue price basis — a PRICE comparison, independent of funding.
34
+ *
35
+ * Venue scope: every venue with the "trading" role (both legs must be executable), whether or not
36
+ * its funding rate is verified; market-data-only venues (TxFlow, unofficial API) are out. Prices
37
+ * come from each venue's market directory and are grouped by `canonical` symbol with the funding
38
+ * scan's guards (fetchCanonicalPriceGroups): unclassified / ambiguous tickers have no canonical and
39
+ * never pair, and a venue whose mark deviates more than MAX_PRICE_DEVIATION_PCT from the group
40
+ * median is dropped. The chosen cheap / rich pair must itself agree within MAX_PRICE_DEVIATION_PCT:
41
+ * a wider gap is a different asset under the same ticker (BB on Lighter 8.50 vs Aster 0.0091).
42
+ * A venue that fails is reported on stderr and is absent from the scan, never silently empty.
43
+ */
32
44
  export declare function handleBasisScan(isJson: () => boolean, opts: {
33
45
  minBasis: string;
34
46
  symbol?: string;