perp-cli 0.12.17 → 0.14.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 (361) hide show
  1. package/README.md +247 -27
  2. package/dist/_stderr-filter.js +0 -1
  3. package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
  4. package/dist/agent-wallet/lighter-keystore.js +92 -10
  5. package/dist/agent-wallet/passphrase.d.ts +1 -1
  6. package/dist/agent-wallet/passphrase.js +16 -3
  7. package/dist/agent-wallet/store.d.ts +37 -4
  8. package/dist/agent-wallet/store.js +132 -0
  9. package/dist/agent-wallet/venue-keystore.d.ts +14 -0
  10. package/dist/agent-wallet/venue-keystore.js +121 -0
  11. package/dist/api/public/hyperliquid.js +4 -4
  12. package/dist/api/public/lighter.js +5 -5
  13. package/dist/api/public/pacifica.d.ts +0 -1
  14. package/dist/api/public/pacifica.js +4 -13
  15. package/dist/api/public/urls.d.ts +9 -1
  16. package/dist/api/public/urls.js +35 -13
  17. package/dist/arb/file-lock.d.ts +11 -0
  18. package/dist/arb/file-lock.js +74 -0
  19. package/dist/arb/index.d.ts +2 -2
  20. package/dist/arb/index.js +2 -2
  21. package/dist/arb/pending-entry.d.ts +32 -0
  22. package/dist/arb/pending-entry.js +77 -0
  23. package/dist/arb/sizing.d.ts +6 -13
  24. package/dist/arb/sizing.js +12 -51
  25. package/dist/arb/state.d.ts +45 -12
  26. package/dist/arb/state.js +214 -42
  27. package/dist/arb/utils.d.ts +20 -6
  28. package/dist/arb/utils.js +43 -39
  29. package/dist/bot/conditions.d.ts +4 -4
  30. package/dist/bot/conditions.js +11 -6
  31. package/dist/bot/config.d.ts +17 -1
  32. package/dist/bot/config.js +48 -12
  33. package/dist/bot/engine.d.ts +61 -2
  34. package/dist/bot/engine.js +610 -144
  35. package/dist/bot/fill-size.d.ts +7 -0
  36. package/dist/bot/fill-size.js +18 -0
  37. package/dist/bot/index.d.ts +1 -1
  38. package/dist/bot/index.js +1 -1
  39. package/dist/bot/perp-order.d.ts +99 -0
  40. package/dist/bot/perp-order.js +299 -0
  41. package/dist/bot/presets.d.ts +7 -0
  42. package/dist/bot/presets.js +17 -0
  43. package/dist/bot/quarantine.d.ts +75 -0
  44. package/dist/bot/quarantine.js +190 -0
  45. package/dist/bot/quote-rules.d.ts +23 -0
  46. package/dist/bot/quote-rules.js +69 -0
  47. package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
  48. package/dist/bot/strategies/avellaneda-mm.js +72 -70
  49. package/dist/bot/strategies/basis-arb.js +2 -0
  50. package/dist/bot/strategies/dca-strategy.d.ts +2 -0
  51. package/dist/bot/strategies/dca-strategy.js +5 -0
  52. package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
  53. package/dist/bot/strategies/funding-arb-strategy.js +571 -380
  54. package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
  55. package/dist/bot/strategies/funding-arb-utils.js +159 -195
  56. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  57. package/dist/bot/strategies/funding-arb-v2.js +258 -47
  58. package/dist/bot/strategies/funding-auto.js +215 -194
  59. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
  60. package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
  61. package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
  62. package/dist/bot/strategies/spot-perp-executor.js +319 -120
  63. package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
  64. package/dist/bot/strategies/spot-perp-state.js +70 -0
  65. package/dist/bot/strategy-types.d.ts +6 -0
  66. package/dist/bot/tui/BotDashboard.d.ts +7 -2
  67. package/dist/bot/tui/BotDashboard.js +6 -3
  68. package/dist/bridge-engine.d.ts +120 -19
  69. package/dist/bridge-engine.js +710 -422
  70. package/dist/bridge-signers.d.ts +54 -0
  71. package/dist/bridge-signers.js +161 -0
  72. package/dist/cache.d.ts +4 -1
  73. package/dist/cache.js +11 -0
  74. package/dist/cli-flags.d.ts +50 -0
  75. package/dist/cli-flags.js +104 -0
  76. package/dist/cli-program.d.ts +35 -0
  77. package/dist/cli-program.js +286 -0
  78. package/dist/cli-spec.js +1 -1
  79. package/dist/client-id-tracker.js +11 -2
  80. package/dist/commands/account.d.ts +7 -1
  81. package/dist/commands/account.js +70 -92
  82. package/dist/commands/agent-import.d.ts +114 -0
  83. package/dist/commands/agent-import.js +794 -0
  84. package/dist/commands/agent.d.ts +232 -2
  85. package/dist/commands/agent.js +1858 -955
  86. package/dist/commands/alerts.d.ts +1 -0
  87. package/dist/commands/alerts.js +31 -28
  88. package/dist/commands/arb/index.d.ts +60 -1
  89. package/dist/commands/arb/index.js +381 -384
  90. package/dist/commands/arb-auto.d.ts +132 -4
  91. package/dist/commands/arb-auto.js +1474 -967
  92. package/dist/commands/arb.d.ts +3 -1
  93. package/dist/commands/arb.js +62 -30
  94. package/dist/commands/bot.js +127 -47
  95. package/dist/commands/bridge.d.ts +2 -1
  96. package/dist/commands/bridge.js +339 -192
  97. package/dist/commands/builder.d.ts +55 -0
  98. package/dist/commands/builder.js +387 -0
  99. package/dist/commands/funds-onchain.d.ts +32 -0
  100. package/dist/commands/funds-onchain.js +286 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +939 -245
  103. package/dist/commands/history.js +72 -45
  104. package/dist/commands/init.d.ts +107 -15
  105. package/dist/commands/init.js +1116 -249
  106. package/dist/commands/jobs.d.ts +2 -1
  107. package/dist/commands/jobs.js +77 -15
  108. package/dist/commands/manage.js +136 -91
  109. package/dist/commands/market.d.ts +4 -1
  110. package/dist/commands/market.js +35 -32
  111. package/dist/commands/outcome.d.ts +10 -0
  112. package/dist/commands/outcome.js +396 -0
  113. package/dist/commands/plan.js +52 -12
  114. package/dist/commands/portfolio.d.ts +1 -0
  115. package/dist/commands/portfolio.js +60 -72
  116. package/dist/commands/rebalance.d.ts +2 -1
  117. package/dist/commands/rebalance.js +33 -43
  118. package/dist/commands/risk.js +46 -19
  119. package/dist/commands/settings.d.ts +1 -0
  120. package/dist/commands/settings.js +246 -51
  121. package/dist/commands/trade.d.ts +50 -0
  122. package/dist/commands/trade.js +1943 -695
  123. package/dist/commands/wallet.d.ts +30 -1
  124. package/dist/commands/wallet.js +316 -337
  125. package/dist/config.d.ts +60 -9
  126. package/dist/config.js +252 -82
  127. package/dist/constants.d.ts +29 -4
  128. package/dist/constants.js +62 -20
  129. package/dist/dashboard/server.d.ts +2 -0
  130. package/dist/dashboard/server.js +9 -6
  131. package/dist/dashboard/ui.js +6 -4
  132. package/dist/dashboard/ws-feeds.js +20 -8
  133. package/dist/dex-asset-map.js +5 -3
  134. package/dist/errors.d.ts +122 -0
  135. package/dist/errors.js +194 -0
  136. package/dist/event-stream.js +75 -19
  137. package/dist/evm-master-tx.d.ts +59 -0
  138. package/dist/evm-master-tx.js +154 -0
  139. package/dist/exchanges/adapter-network.d.ts +10 -0
  140. package/dist/exchanges/adapter-network.js +10 -0
  141. package/dist/exchanges/arcus/adapter.d.ts +196 -0
  142. package/dist/exchanges/arcus/adapter.js +1092 -0
  143. package/dist/exchanges/arcus/agent.d.ts +55 -0
  144. package/dist/exchanges/arcus/agent.js +188 -0
  145. package/dist/exchanges/arcus/client.d.ts +63 -0
  146. package/dist/exchanges/arcus/client.js +285 -0
  147. package/dist/exchanges/arcus/public.d.ts +65 -0
  148. package/dist/exchanges/arcus/public.js +138 -0
  149. package/dist/exchanges/arcus/signing.d.ts +176 -0
  150. package/dist/exchanges/arcus/signing.js +288 -0
  151. package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
  152. package/dist/exchanges/arcus/spot-chain.js +241 -0
  153. package/dist/exchanges/arcus/spot-router.d.ts +177 -0
  154. package/dist/exchanges/arcus/spot-router.js +183 -0
  155. package/dist/exchanges/arcus/spot.d.ts +109 -0
  156. package/dist/exchanges/arcus/spot.js +645 -0
  157. package/dist/exchanges/aster-funds.d.ts +127 -0
  158. package/dist/exchanges/aster-funds.js +252 -0
  159. package/dist/exchanges/aster-nonce.d.ts +2 -0
  160. package/dist/exchanges/aster-nonce.js +107 -0
  161. package/dist/exchanges/aster-spot.d.ts +117 -0
  162. package/dist/exchanges/aster-spot.js +686 -0
  163. package/dist/exchanges/aster-typed-data.d.ts +133 -7
  164. package/dist/exchanges/aster-typed-data.js +121 -7
  165. package/dist/exchanges/aster.d.ts +219 -6
  166. package/dist/exchanges/aster.js +1023 -153
  167. package/dist/exchanges/builder-fee.d.ts +254 -0
  168. package/dist/exchanges/builder-fee.js +421 -0
  169. package/dist/exchanges/canonical-symbols.d.ts +32 -0
  170. package/dist/exchanges/canonical-symbols.js +96 -0
  171. package/dist/exchanges/capabilities.d.ts +7 -1
  172. package/dist/exchanges/capabilities.js +7 -0
  173. package/dist/exchanges/hl-signing.d.ts +165 -0
  174. package/dist/exchanges/hl-signing.js +158 -0
  175. package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
  176. package/dist/exchanges/hyperliquid-outcome.js +646 -0
  177. package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
  178. package/dist/exchanges/hyperliquid-spot.js +308 -117
  179. package/dist/exchanges/hyperliquid.d.ts +462 -57
  180. package/dist/exchanges/hyperliquid.js +1575 -449
  181. package/dist/exchanges/index.d.ts +6 -0
  182. package/dist/exchanges/index.js +4 -0
  183. package/dist/exchanges/interface.d.ts +63 -10
  184. package/dist/exchanges/interface.js +18 -3
  185. package/dist/exchanges/ioc-outcome.d.ts +44 -0
  186. package/dist/exchanges/ioc-outcome.js +57 -0
  187. package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
  188. package/dist/exchanges/legacy-trading-v2.js +940 -0
  189. package/dist/exchanges/lighter-public.d.ts +43 -0
  190. package/dist/exchanges/lighter-public.js +276 -0
  191. package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
  192. package/dist/exchanges/lighter-rh-deposit.js +185 -0
  193. package/dist/exchanges/lighter-signer.d.ts +123 -0
  194. package/dist/exchanges/lighter-signer.js +215 -0
  195. package/dist/exchanges/lighter-spot.d.ts +91 -13
  196. package/dist/exchanges/lighter-spot.js +595 -233
  197. package/dist/exchanges/lighter.d.ts +611 -39
  198. package/dist/exchanges/lighter.js +1890 -649
  199. package/dist/exchanges/ondo/adapter.d.ts +166 -0
  200. package/dist/exchanges/ondo/adapter.js +835 -0
  201. package/dist/exchanges/ondo/agent.d.ts +31 -0
  202. package/dist/exchanges/ondo/agent.js +101 -0
  203. package/dist/exchanges/ondo/client.d.ts +60 -0
  204. package/dist/exchanges/ondo/client.js +297 -0
  205. package/dist/exchanges/ondo/funds.d.ts +122 -0
  206. package/dist/exchanges/ondo/funds.js +290 -0
  207. package/dist/exchanges/ondo/public.d.ts +95 -0
  208. package/dist/exchanges/ondo/public.js +214 -0
  209. package/dist/exchanges/ondo/spot.d.ts +127 -0
  210. package/dist/exchanges/ondo/spot.js +392 -0
  211. package/dist/exchanges/outcome-interface.d.ts +140 -0
  212. package/dist/exchanges/outcome-interface.js +16 -0
  213. package/dist/exchanges/pacifica-spot.d.ts +83 -0
  214. package/dist/exchanges/pacifica-spot.js +324 -0
  215. package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
  216. package/dist/exchanges/pacifica-typed-data.js +57 -9
  217. package/dist/exchanges/pacifica.d.ts +288 -4
  218. package/dist/exchanges/pacifica.js +1013 -76
  219. package/dist/exchanges/precision.d.ts +47 -0
  220. package/dist/exchanges/precision.js +209 -0
  221. package/dist/exchanges/qfex/adapter.d.ts +341 -0
  222. package/dist/exchanges/qfex/adapter.js +1383 -0
  223. package/dist/exchanges/qfex/agent.d.ts +58 -0
  224. package/dist/exchanges/qfex/agent.js +188 -0
  225. package/dist/exchanges/qfex/client.d.ts +54 -0
  226. package/dist/exchanges/qfex/client.js +138 -0
  227. package/dist/exchanges/qfex/errors.d.ts +70 -0
  228. package/dist/exchanges/qfex/errors.js +236 -0
  229. package/dist/exchanges/qfex/public.d.ts +122 -0
  230. package/dist/exchanges/qfex/public.js +268 -0
  231. package/dist/exchanges/qfex/signing.d.ts +160 -0
  232. package/dist/exchanges/qfex/signing.js +283 -0
  233. package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
  234. package/dist/exchanges/qfex/ws-transport.js +798 -0
  235. package/dist/exchanges/rate-limiter.d.ts +12 -0
  236. package/dist/exchanges/rate-limiter.js +64 -0
  237. package/dist/exchanges/registry.d.ts +14 -2
  238. package/dist/exchanges/registry.js +20 -32
  239. package/dist/exchanges/slippage.d.ts +37 -0
  240. package/dist/exchanges/slippage.js +70 -0
  241. package/dist/exchanges/spot-factory.d.ts +166 -0
  242. package/dist/exchanges/spot-factory.js +431 -0
  243. package/dist/exchanges/spot-interface.d.ts +98 -4
  244. package/dist/exchanges/spot-interface.js +27 -1
  245. package/dist/exchanges/spot-result.d.ts +79 -0
  246. package/dist/exchanges/spot-result.js +273 -0
  247. package/dist/exchanges/trading-v2.d.ts +117 -0
  248. package/dist/exchanges/trading-v2.js +301 -0
  249. package/dist/exchanges/venue-types.d.ts +231 -0
  250. package/dist/exchanges/venue-types.js +87 -0
  251. package/dist/exchanges/venue.d.ts +211 -0
  252. package/dist/exchanges/venue.js +132 -0
  253. package/dist/exchanges/venues/arcus.d.ts +8 -0
  254. package/dist/exchanges/venues/arcus.js +109 -0
  255. package/dist/exchanges/venues/aster.d.ts +49 -0
  256. package/dist/exchanges/venues/aster.js +176 -0
  257. package/dist/exchanges/venues/create-helpers.d.ts +18 -0
  258. package/dist/exchanges/venues/create-helpers.js +199 -0
  259. package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
  260. package/dist/exchanges/venues/hyperliquid.js +126 -0
  261. package/dist/exchanges/venues/index.d.ts +16 -0
  262. package/dist/exchanges/venues/index.js +26 -0
  263. package/dist/exchanges/venues/lighter-family.d.ts +92 -0
  264. package/dist/exchanges/venues/lighter-family.js +166 -0
  265. package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
  266. package/dist/exchanges/venues/lighter-rh.js +58 -0
  267. package/dist/exchanges/venues/lighter.d.ts +6 -0
  268. package/dist/exchanges/venues/lighter.js +55 -0
  269. package/dist/exchanges/venues/ondo.d.ts +34 -0
  270. package/dist/exchanges/venues/ondo.js +140 -0
  271. package/dist/exchanges/venues/pacifica.d.ts +8 -0
  272. package/dist/exchanges/venues/pacifica.js +95 -0
  273. package/dist/exchanges/venues/qfex.d.ts +28 -0
  274. package/dist/exchanges/venues/qfex.js +132 -0
  275. package/dist/execution-log.js +10 -2
  276. package/dist/funding/legacy-public.d.ts +31 -0
  277. package/dist/funding/legacy-public.js +369 -0
  278. package/dist/funding/normalize.d.ts +28 -16
  279. package/dist/funding/normalize.js +61 -29
  280. package/dist/funding/rates.d.ts +46 -11
  281. package/dist/funding/rates.js +228 -277
  282. package/dist/guardrail/perp-guardrail.js +0 -0
  283. package/dist/index.js +156 -684
  284. package/dist/jobs.d.ts +84 -6
  285. package/dist/jobs.js +239 -41
  286. package/dist/landing.d.ts +7 -2
  287. package/dist/landing.js +19 -3
  288. package/dist/mcp-server.d.ts +14 -4
  289. package/dist/mcp-server.js +820 -402
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/pacifica/client.d.ts +41 -10
  293. package/dist/pacifica/client.js +167 -63
  294. package/dist/pacifica/constants.d.ts +0 -1
  295. package/dist/pacifica/constants.js +0 -5
  296. package/dist/pacifica/deposit.d.ts +30 -3
  297. package/dist/pacifica/deposit.js +119 -2
  298. package/dist/pacifica/faucet.d.ts +29 -0
  299. package/dist/pacifica/faucet.js +100 -0
  300. package/dist/pacifica/sign-tx.d.ts +15 -0
  301. package/dist/pacifica/sign-tx.js +41 -0
  302. package/dist/pacifica/types/account.d.ts +28 -4
  303. package/dist/pacifica/types/lake.d.ts +20 -5
  304. package/dist/pacifica/types/lake.js +4 -0
  305. package/dist/pacifica/types/market.d.ts +5 -0
  306. package/dist/pacifica/types/order.d.ts +4 -1
  307. package/dist/pacifica/types/position.d.ts +9 -1
  308. package/dist/plan-executor.d.ts +10 -5
  309. package/dist/plan-executor.js +165 -54
  310. package/dist/position-history.js +13 -0
  311. package/dist/rebalance.d.ts +26 -2
  312. package/dist/rebalance.js +86 -7
  313. package/dist/relay-swap.d.ts +232 -0
  314. package/dist/relay-swap.js +630 -0
  315. package/dist/risk.js +5 -1
  316. package/dist/secret-env.d.ts +15 -0
  317. package/dist/secret-env.js +36 -0
  318. package/dist/settings.d.ts +169 -6
  319. package/dist/settings.js +219 -38
  320. package/dist/signer/ows-evm.d.ts +13 -0
  321. package/dist/signer/ows-evm.js +22 -0
  322. package/dist/smart-order.d.ts +9 -0
  323. package/dist/smart-order.js +53 -8
  324. package/dist/strategies/dca.d.ts +6 -2
  325. package/dist/strategies/dca.js +66 -26
  326. package/dist/strategies/funding-arb.d.ts +11 -1
  327. package/dist/strategies/funding-arb.js +202 -89
  328. package/dist/strategies/grid.d.ts +31 -0
  329. package/dist/strategies/grid.js +89 -11
  330. package/dist/strategies/pending-stop.d.ts +8 -0
  331. package/dist/strategies/pending-stop.js +12 -0
  332. package/dist/strategies/split-order.d.ts +45 -17
  333. package/dist/strategies/split-order.js +157 -91
  334. package/dist/strategies/trailing-stop.d.ts +2 -1
  335. package/dist/strategies/trailing-stop.js +52 -6
  336. package/dist/strategies/twap.d.ts +6 -1
  337. package/dist/strategies/twap.js +39 -11
  338. package/dist/trade-validator.d.ts +22 -0
  339. package/dist/trade-validator.js +119 -15
  340. package/dist/utils/numeric.d.ts +46 -0
  341. package/dist/utils/numeric.js +79 -0
  342. package/dist/utils.d.ts +19 -0
  343. package/dist/utils.js +96 -19
  344. package/dist/withdraw-gate.d.ts +49 -0
  345. package/dist/withdraw-gate.js +131 -0
  346. package/package.json +15 -6
  347. package/skills/perp-cli/SKILL.md +142 -55
  348. package/skills/perp-cli/references/agent-operations.md +110 -56
  349. package/skills/perp-cli/references/commands.md +266 -71
  350. package/skills/perp-cli/references/strategies.md +34 -31
  351. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  352. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  353. package/skills/perp-cli/scripts/preflight.sh +21 -14
  354. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  355. package/skills/perp-cli/scripts/validate-arb.sh +31 -15
  356. package/vendor/lighter-signer/SHA256SUMS +2 -0
  357. package/vendor/lighter-signer/VERSION +8 -0
  358. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  359. package/vendor/lighter-signer/wasm_exec.js +575 -0
  360. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  361. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dw/perp-cli.svg)](https://www.npmjs.com/package/perp-cli)
5
5
  [![license](https://img.shields.io/npm/l/perp-cli.svg)](https://github.com/hypurrquant/perp-cli/blob/main/LICENSE)
6
6
 
7
- Multi-DEX perpetual futures CLI — **Pacifica** (Solana), **Hyperliquid** (HyperEVM), **Lighter** (Ethereum), **Aster** (BNB Chain).
7
+ Multi-DEX perpetual futures CLI — **Pacifica** (Solana), **Hyperliquid** (HyperEVM), **Lighter** (Ethereum), **Aster** (BNB Chain), **Lighter RH** (Robinhood chain), **Ondo Perps** (off-chain), **Arcus** (Robinhood chain), **QFEX** (off-chain, TradFi perps).
8
8
 
9
9
  ```bash
10
10
  npm install -g perp-cli # global install
@@ -16,7 +16,7 @@ npx -y perp-cli --json portfolio
16
16
 
17
17
  ## Features
18
18
 
19
- - **4 Exchanges** — trade, bridge, arbitrage across Pacifica, Hyperliquid, Lighter, Aster
19
+ - **8 Venues** — trade on Pacifica, Hyperliquid, Lighter, Aster, Lighter RH, Ondo Perps, Arcus, QFEX (bridge / perp-perp arb cover the original four)
20
20
  - **Funding Rate Arb** — perp-perp + spot-perp scan & one-command dual-leg execution
21
21
  - **Portfolio** — single call returns balances, positions, risk level across all exchanges
22
22
  - **Funds** — deposit, withdraw, transfer, multi-provider bridge (cctp/relay/debridge), inter-exchange rebalance — all in one group
@@ -27,15 +27,26 @@ npx -y perp-cli --json portfolio
27
27
  ## Setup
28
28
 
29
29
  ```bash
30
- # Set exchange keys
31
- perp wallet set pac <SOLANA_KEY> # Pacifica
32
- perp wallet set hl <EVM_KEY> # Hyperliquid
33
- perp wallet set lt <EVM_KEY> # Lighter (API key auto-generated)
34
- perp wallet set aster <API_KEY> # Aster (BNB Chain)
35
-
36
- # Or interactive setup wizard
30
+ # Import an exchange key into the encrypted OWS vault and make it the active wallet.
31
+ # The key is read from a hidden prompt or stdin — never argv (shell history), never ~/.perp/.env.
32
+ perp wallet set hl # Hyperliquid (EVM) — hidden prompt
33
+ pbpaste | OWS_PASSPHRASE="$PP" perp wallet set pac --name pac # Pacifica (Solana) — piped
34
+ # The active wallet serves every venue; one EVM key covers HL / Lighter / Aster.
35
+ # Lighter trading also needs: perp wallet agent approve lighter
36
+
37
+ # Or the interactive setup wizard (all 7 venues): picks / creates / imports an OWS
38
+ # vault wallet, network, venues, registers agent wallets (y/N confirmation before
39
+ # any signature — never moves funds), explains + sets the slippage bound, and the
40
+ # default venue. It never writes keys to ~/.perp/.env.
37
41
  perp setup
38
42
 
43
+ # Scriptable (CI / agents) — writes settings only, signs nothing:
44
+ perp --json setup --non-interactive --wallet <ows-wallet> --venues hyperliquid,aster \
45
+ --network testnet --default-venue hyperliquid --slippage-bps 50 --slippage aster=80
46
+ # Add agent registration explicitly (Ondo key id + secret via stdin only):
47
+ OWS_PASSPHRASE=<pp> perp --json setup --non-interactive --wallet <ows-wallet> \
48
+ --venues hyperliquid,arcus --approve-agents --yes --arcus-account-index 0
49
+
39
50
  # Verify
40
51
  perp wallet show
41
52
  ```
@@ -44,6 +55,176 @@ Same EVM key works for both Hyperliquid and Lighter.
44
55
 
45
56
  > **Lighter API Key Index:** Indexes 0–3 are reserved by Lighter's frontend (web/mobile). perp-cli defaults to index `4`. Override with `LIGHTER_API_KEY_INDEX` env var, or use `perp wallet agent approve lighter --api-key-index <n>` for managed agent slots. Valid range: 4–254.
46
57
 
58
+ ### Lighter RH, Ondo Perps, Arcus
59
+
60
+ These venues have no `wallet set` private-key path — register an agent / API key instead:
61
+
62
+ ```bash
63
+ perp wallet agent approve lighter-rh # Lighter RH L2 slot key (USDG collateral, Robinhood chain)
64
+ printf '%s\n%s\n' "$KEY_ID" "$SECRET" | perp wallet agent approve ondo # Ondo HMAC key id + secret, stdin only
65
+ perp wallet agent approve arcus --master <ows-wallet> # Arcus Ed25519 API key (OWS wallet agent-arcus-<master>)
66
+ perp wallet agent verify [lighter-rh|ondo|arcus] # no venue = verify every configured venue
67
+ ```
68
+
69
+ ### No master wallet on this computer — import a key created on the exchange website
70
+
71
+ Create the agent / API key on the venue's website, then import it. The master wallet never has to be
72
+ on this machine; it is identified only by its public address. The key is read from a hidden prompt or
73
+ stdin (never argv) and **checked against the venue before anything is stored** — a refused check stores
74
+ nothing; a venue that cannot be checked without the master is reported as `unverified`, never as verified.
75
+
76
+ ```bash
77
+ perp -n testnet wallet agent import hyperliquid # API wallet key; master read from HL (userRole)
78
+ pbpaste | perp wallet agent import aster --master-address 0xYOU # signer key; signed /fapi/v3/balance check (works on Aster testnet too)
79
+ perp wallet agent import pacifica --master-address <SOL account> # agent key; UNVERIFIED (Pacifica lists bindings only master-signed)
80
+ perp wallet agent import lighter --account-index 123 --api-key-index 5 # slot key; auth token accepted for that slot
81
+ perp wallet agent import arcus --master-address 0xOWNER # Ed25519 key; public key listed ACTIVE for the owner
82
+ printf '%s\n%s\n' "$KEY_ID" "$SECRET" | perp wallet agent import ondo
83
+ printf '%s\n%s\n' "$PUBLIC_KEY" "$SECRET" | perp wallet agent import qfex --account-id <uuid> # QFEX: import only (keys are created on the website, 2FA)
84
+ perp wallet agent list # SOURCE column: approved | imported
85
+
86
+ # Setup without any master wallet: one stdin line per --import-agents venue, in order (ondo: 2 lines)
87
+ printf '%s\n%s\n' "$HL_AGENT_KEY" "$LT_KEY" | perp --json setup --non-interactive \
88
+ --venues hyperliquid,lighter --import-agents hyperliquid,lighter --yes \
89
+ --lighter-account-index 123 --lighter-api-key-index 5
90
+ ```
91
+
92
+ Trading works with the imported key. **Deposits, withdrawals, transfers, bridge, referral set and agent
93
+ approvals need the master**: without it they fail with `NO_SIGNER_AVAILABLE` ("do it on the exchange
94
+ website, or add the master with `perp wallet import`"). The CLI cannot revoke or rotate an imported key —
95
+ revoke it on the website; `perp wallet agent revoke <venue> <name> --local-only` removes the local copy,
96
+ and `wallet agent import <venue> --agent-name <name> --replace` swaps in a new website-created key.
97
+ `perp setup` offers the same choice per venue: approve with this computer's master wallet, or import a
98
+ website-created key (Step 1 can skip the master wallet).
99
+
100
+ Secrets are stored once (see `docs/SSOT_RULES.md` Rule #3): Lighter RH → `~/.perp/lighter-rh-agents/`, Ondo → `~/.perp/ondo-agents/<keyId>.json`, Arcus → OWS wallet `agent-arcus-<master>`; imported EVM / Solana / Ed25519 keys → OWS wallet `agent-{hl,aster,pac,arcus}-import-<name>` (empty passphrase, like approved agent wallets), imported Lighter keys → the network-tagged Lighter keystore. A missing wallet/key now fails with `KEY_NOT_FOUND` / `NO_SIGNER_AVAILABLE` instead of silently falling back to read-only.
101
+
102
+ ## Venues
103
+
104
+ | Venue | `-e` id (aliases) | Trading | Notes |
105
+ |-------|-------------------|---------|-------|
106
+ | Pacifica | `pacifica` (`pac`) | yes | Solana |
107
+ | Hyperliquid | `hyperliquid` (`hl`) | yes | HyperEVM; HIP-3 dexes via `--dex` |
108
+ | Lighter | `lighter` (`lt`) | yes | Ethereum L2 |
109
+ | Aster | `aster` (`ast`) | yes | BNB Chain; funding interval is per symbol |
110
+ | Lighter RH | `lighter-rh` (`lrh`, `rh`) | yes | Lighter engine on the Robinhood chain, USDG collateral |
111
+ | Ondo Perps | `ondo` (`ondo-perps`) | yes | Off-chain engine; limit TIF GTC (default) / IOC / ALO; restricted: US, PA |
112
+ | QFEX | `qfex` (`qf`) | yes (see **QFEX** below) | Custodial, off-chain engine; equities / indices / commodities, cross margin, USDC; limit TIF GTC (default) / IOC / FOK / ALO; testnet = QFEX UAT; restricted: US, GB, ES and others |
113
+ | Arcus | `arcus` | yes | Robinhood chain, USDG collateral; limit TIF GTC (default, sent as GTT) / IOC / FOK / ALO; restricted: US, CA, GB; **access whitelist** — unlisted addresses are rejected (request access at arcus.xyz) |
114
+
115
+ - **Time in force:** `trade limit` defaults to `GTC` on every venue (common names `GTC | IOC | FOK | ALO`; a TIF the venue does not offer → `CAPABILITY_UNSUPPORTED`). Lighter post-only (`ALO`) is disabled until verified on the venue.
116
+ - **TP/SL on an entry:** `trade market|buy|sell|limit ... --tp <px> --sl <px>` means the same on every venue — *protect this entry's verified fill*. The direction is checked before anything is sent (long: tp > reference > sl; short: the reverse; reference = mark for market entries, the limit price for limit entries). Ondo attaches the legs to the order; Arcus sends an entry+TP/SL batch; Hyperliquid / Lighter / Aster verify the entry fill, then place reduce-only TP/SL trigger orders sized to it (only market or `--tif IOC` entries — a resting entry → `CAPABILITY_UNSUPPORTED`); Pacifica verifies the fill, then sets position TP/SL (refused if the symbol already has a position or TP/SL — never overwritten). Entry outcome unknown → `ORDER_PENDING` and **no** TP/SL (the position may be unprotected); entry filled but a leg failed → `ORDER_PARTIAL` (not retryable) with the entry result and which legs were placed — do not re-enter, finish with `trade tpsl`.
117
+ - **TP/SL on a position:** `trade tpsl <SYM> <long|short> --tp <px> --sl <px> [--tp-limit <px>] [--size <n>]` — the argument is the **position's** side (buy/sell no longer accepted), checked against the live position; legs are reduce-only and sized to the live position (or `--size`).
118
+ - **Stop orders are reduce-only by default** on every venue (`trade stop`); pass `--no-reduce-only` to let a stop open or flip a position (refused on Ondo and QFEX, whose stops are always reduce-only). Lighter / Arcus market triggers execute as a limit at trigger ± the slippage bound (`--slippage-bps` or settings) and may stay unfilled in a fast move; other venues use native market triggers (`--slippage-bps` → `CAPABILITY_UNSUPPORTED`).
119
+ - **Order edit:** `trade edit` keeps the order's TIF. Pacifica: post-only (ALO) orders are edited natively; GTC / TOB orders placed by perp-cli (TIF recorded in the client order id) are cancelled and replaced with the remaining open size; orders whose TIF is unknown → `CAPABILITY_UNSUPPORTED` (pass `--tif`).
120
+ - **Slippage bound:** every market-type order (entry, close, spot) is bounded by one value: `--slippage-bps <n>` → `settings.slippageBps.<venue>` → `settings.defaultSlippageBps` (shipped default 50, shown in `perp settings show`; change with `perp settings set defaultSlippageBps <n>`). Values outside 1..venue max (Arcus 1000) are rejected before sending.
121
+ - **Restricted regions:** creating a signing adapter for a venue with restricted regions (Ondo, Arcus, QFEX) prints a one-time stderr notice; it never blocks — check the venue terms yourself.
122
+ - **Aggregate views** (`portfolio`, `account positions/orders`, `risk`, `history`, `wallet agent verify`, MCP `portfolio`) cover every account venue with configured credentials.
123
+
124
+ ### What works — mainnet verification (2026-10-13)
125
+
126
+ Every mark below comes from a real-money mainnet run, not from code reading — at the venue minimum, with the per-venue cap raised to about $25–30 for multi-slice / multi-level checks and about $110 for the Hyperliquid TWAP. Details, evidence and the open issues: [`docs/MAINNET-VERIFICATION-STATUS.md`](docs/MAINNET-VERIFICATION-STATUS.md).
127
+
128
+ ✅ verified live · ⛔ not supported (refused before anything is sent) · ❌ implemented but not run on mainnet yet · — not offered by the venue
129
+
130
+ | Feature | HL | HIP-3 `xyz` | Pacifica | Lighter | Lighter RH | Aster | Ondo | Arcus | QFEX |
131
+ |---|---|---|---|---|---|---|---|---|---|
132
+ | Market / limit (GTC, ALO, IOC), cancel, edit, close, reduce, flatten, status | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
133
+ | Entry `--tp/--sl`, `trade tpsl`, `trade stop` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
134
+ | Stop-limit | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⛔ | ✅ | ⛔ |
135
+ | Resting reduce-only limit | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ⛔ | ✅ | ⛔ (IOC only) |
136
+ | Client order id (place / status / cancel) | ✅ | ✅ | ✅³ (UUID) | ✅ (numeric) | ✅ (numeric) | ✅ | ✅ | ✅ | generated ids only |
137
+ | Isolated margin (set, read back with a position, revert) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ (venue rejected with `-4050`, condition unknown) | ⛔ | ✅ | ⛔ |
138
+ | `trade scale-in` / `scale-tp` | ✅ | ✅ (position ≥ $20.5) | ✅ | ✅ | ✅ | ✅ | scale-in ✅ / scale-tp ⛔ — use partial `trade tpsl --size` ✅ | ✅ | scale-in ✅ / scale-tp ⛔ |
139
+ | `trade split` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
140
+ | `trade multi` (legs on several venues) | ✅ | ✅ (global `--dex xyz`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
141
+ | Native TWAP (`trade twap`) | ✅ | ❌ (HL minimum $100) | ✅ master-signed / ⛔ agent-signed | —² | —² | —² | —² | —² | —² |
142
+ | Client-side TWAP (`trade twap` on other venues, `strategy twap`, `--background`) | — | — | — | ✅ | ✅ | ✅ | ✅ | ✅ | ⛔ (automation gate) |
143
+ | `account margin` with a position | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
144
+ | Spot (`trade spot-*`) | ✅ | — | ✅ | ✅ | ✅ | ❌ (needs USDT) | ✅ | ❌ (needs an Arcus-issued key) | — |
145
+ | Deposit via perp-cli | ✅ | (HL) | ✅ | ✅ | ✅ | ✅ | ✅ | — (web app) | — (address only) |
146
+ | Withdraw | ❌ | (HL) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
147
+
148
+ ² perp-cli has no native TWAP for this venue: `trade twap` runs the client-side TWAP (slices) instead. Whether the venue's API offers one is not checked.
149
+ ³ Verified in the session; no saved log file.
150
+
151
+ Automation, verified live (bounded runs at minimum size, stopped and flattened afterwards):
152
+
153
+ | Area | Verified |
154
+ |---|---|
155
+ | Arbitrage | `arb exec` / `status` / `close` on Aster–Arcus, Lighter–Arcus; `arb auto` (Lighter–HL); spot-perp `arb exec <sym> spot:hl pacifica` and `arb close`; `strategy funding-arb`, `quick-arb`, `funding-auto` (Pacifica–HL); `strategy delta-neutral` (HL spot + Pacifica perp) |
156
+ | Bots | `strategy grid` / `dca` / `trailing-stop` / `quick-grid` / `quick-dca` / `preset` (Aster); all 12 engine strategies via `strategy start <config>` (Aster); `strategy apex` (Aster) |
157
+ | Execution helpers | `strategy twap` (Aster), `trade twap --background` (Arcus), `strategy plan execute` (9 of 10 action types — `cancel_order` not run), `perp background stop` (graceful: orders cancelled, positions listed) |
158
+ | Hyperliquid HIP-4 outcomes | GTC place / cancel, IOC buy, `outcome sell --all` / `--size` |
159
+
160
+ Not run on mainnet yet: withdrawals and `funds transfer` / `rebalance execute` / `bridge exchange` (run last), subaccounts, builder fees, `strategy claude-agent` (needs an Anthropic key), alerts / dashboard.
161
+
162
+ ### QFEX
163
+
164
+ QFEX is custodial: there is no wallet signature. The only credential is an **API key created on the QFEX website** (Developer Settings, 2FA), imported with `perp wallet agent import qfex` (public key + secret via hidden prompt / stdin; stored encrypted in `~/.perp/qfex-agents/`). The import resolves the **one account UUID** the key trades (primary, or `--account-id <uuid>`), shows it and stores nothing until you confirm it (piped: pass `--account-id` equal to it). Create the trading key **without** the "Deposit and withdraw" permission.
165
+
166
+ ```bash
167
+ perp -n testnet wallet agent import qfex # QFEX UAT (api.qfex.io); y/N on the resolved account
168
+ perp -e qfex market list # public: /refdata + market-data snapshot
169
+ perp -e qfex trade limit AAPL buy 320 0.01 # GTC: "accepted" after the ACK; "open" once listed (trade status)
170
+ perp -e qfex trade market AAPL buy 0.01 # IOC LIMIT at mark ± slippage (never the venue's unbounded MARKET)
171
+ perp -e qfex trade status c:<id> --symbol AAPL # by the generated client order id
172
+ perp funds deposit qfex # prints the USDC / Arbitrum One deposit address; you send the funds
173
+ ```
174
+
175
+ Verified on QFEX production (2026-10-13): `trade close` / `close-all` / `flatten` / `reduce` (sent as reduce-only **IOC**), `trade stop`, `trade tpsl` (both legs), market entry with `--sl`, `trade edit` (native modify — a new order id, the client id is kept, so `c:<id>` keeps working), cancel / status by `c:<id>`.
176
+
177
+ What perp-cli **still refuses on QFEX** (uniform `CAPABILITY_UNSUPPORTED`, before anything is sent):
178
+
179
+ | Refused | Why | Do instead |
180
+ |---|---|---|
181
+ | Resting reduce-only orders (`trade limit --reduce-only` with GTC / ALO, `trade scale-tp`) | Live: a resting reduce-only order **stays open after the position goes flat**; how the venue treats it if it later matches is unverified | Reduce-only IOC (`trade close` / `reduce`), or `trade stop` / `trade tpsl` |
182
+ | Stop-limit (`--tp-limit`, `trade stop` with a limit price) | The venue's stop orders convert to market orders when triggered | Stop-market |
183
+ | Stops that open or flip a position (`trade stop --no-reduce-only`) | QFEX stop orders only reduce a position | Reduce-only stops |
184
+ | `trade edit` of a reduce-only order | A reduce-only order is sized against the live position when placed; a modify would skip that check | `trade cancel`, then place again |
185
+ | `--client-id` | Client ids are not unique on the venue; perp-cli only trusts ids it generates | Omit it (the generated id is reported) |
186
+ | Markets not quoted in USD (`SKHYNIX-KRW`, …) | Sizing, risk limits and notionals assume USD | QFEX web app |
187
+ | Unattended automation on QFEX: bots, plans, TWAP, DCA, grid, funding arb, trailing stop, background jobs; QFEX in `arb scan` rankings | Automation is gated until verified (`QFEX_AUTOMATION_VERIFIED`); the funding rate's unit is unverified and equity funding accrues only part of the week (Q4) | Trade QFEX by hand; view funding only: `perp arb scan --rates --include-unverified` |
188
+
189
+ Known caveat: cancelling a stop / TP right after its position was closed can answer "unexpected error" while the venue removes the stop itself. QFEX keeps no record of stop orders, so perp-cli cannot prove the cancel and reports `ORDER_PARTIAL` (pending) — check `perp -e qfex account orders`; if it lists nothing, the stop is gone.
190
+
191
+ Order outcomes are never guessed: an IOC / market order is final only on a venue terminal frame (nothing filled → `ORDER_REJECTED`, `reason: iocNoFill`); anything unclear after the order was sent is `ORDER_PENDING` with the `c:<id>` to check — never resubmit. perp-cli never enables QFEX cancel-on-disconnect, but **another connection of the same user that enabled it (e.g. the official QFEX CLI) cancels all of the account's orders when that connection drops**.
192
+
193
+ **Withdrawals** are authorized by the API key alone, so they are off until you set a local gate in an interactive terminal (no `--yes`, refused under `--json` / pipes / MCP): `perp settings set withdraw.qfex <address[+address…]>,<maxPerTx>,<maxPerDay>`, then `perp funds withdraw qfex <amount> --to <allowlisted address>` (`--dry-run` shows the plan and the gate verdict). The gate guards against mistakes and misdirected automation — it is **not** a defence against key theft; that defence is a trading key without the withdraw permission. QFEX itself documents that withdrawals need 2FA on the web app, so the API may refuse. Subaccounts: `perp -e qfex funds transfer <amount> primary --from <account uuid>`.
194
+
195
+ ### Spot trading
196
+
197
+ Spot support is declared per venue (`VenueDefinition.spot`) and every spot command, portfolio view and bot goes through one factory. Symbols are `BASE/QUOTE`; a bare base (`ETH`) uses the venue's first quote asset.
198
+
199
+ | Venue | Account model | Quote | Limit / book / cancel | Spot↔perp transfer |
200
+ |-------|---------------|-------|-----------------------|--------------------|
201
+ | Hyperliquid | runtime (unified accounts share USDC with perp; standard accounts are separate) | USDC | yes | yes (no-op on unified accounts) |
202
+ | Lighter / Lighter RH | separate | USDC / USDG | yes | yes |
203
+ | Aster | separate (`sapi` spot account) | USDT, USD1 (bare base → USDT) | yes | yes |
204
+ | Pacifica | **unified** — spot shares the perp account | USDC | yes | no (not needed) |
205
+ | Ondo Perps | separate (spot wallet ↔ margin) | USDC | yes | yes |
206
+ | Arcus | **wallet** — on-chain swaps from your master EVM wallet (router, Permit2) | USDG | no (market swaps only) | no |
207
+
208
+ ```bash
209
+ perp --json -e <EX> trade spot-buy <SYM> <SIZE> [--slippage-bps <n>] [--yes]
210
+ perp --json -e <EX> trade spot-sell <SYM> <SIZE> [--slippage-bps <n>] [--yes]
211
+ perp --json -e <EX> trade spot-limit <SYM> <buy|sell> <PRICE> <SIZE> [--tif GTC|IOC|ALO]
212
+ perp --json -e <EX> trade spot-cancel <SYM> <ORDER_ID>
213
+ perp --json -e <EX> trade spot-orders [SYM]
214
+ perp --json -e <EX> trade spot-transfer <to-spot|to-perp> <AMOUNT> # moves the venue's first quote asset
215
+ perp --json -e <EX> trade spot-balance
216
+ ```
217
+
218
+ - **Unsupported operations fail loudly** with `CAPABILITY_UNSUPPORTED` (e.g. `spot-limit` / `spot-cancel` / `spot-orders` / `spot-transfer` on Arcus).
219
+ - **`spot-transfer` on a shared-balance account** (Pacifica, Hyperliquid unified) succeeds as a no-op with `execution.method: "sharedAccount"` (nothing needs to move); a real move reports `execution.method: "transfer"`.
220
+ - **Slippage:** same rule as perps — `--slippage-bps <n>` → `settings.slippageBps.<venue>` → `settings.defaultSlippageBps` (shipped 50).
221
+ - **Pacifica:** spot buys spend the shared USDC perp margin and reduce perp available margin (a warning is printed before the first buy). Pacifica `getMarkets()` (perp) no longer lists the `SOL-USDC` spot market.
222
+ - **Aster:** an agent signer needs spot permission — `perp wallet agent rotate aster --can-spot` (or `perp wallet agent approve aster --can-spot`); otherwise spot orders fail with `POLICY_DENIED`.
223
+ - **Arcus:** swaps go through `router.spot.arcus.xyz` and settle on the Robinhood chain from the master EVM wallet (`--ows` / active OWS wallet; none → `NO_SIGNER_AVAILABLE`). Spot needs only the master wallet — no Arcus perp agent key; perp orders / account reads without one fail with `AGENT_NOT_REGISTERED` (`perp wallet agent approve arcus`). Listing Arcus spot markets is slow (~80 s, one router `/price` per token) — use `spot-buy` / `spot-sell` directly. Tokens without EIP-2612 permit need a one-time on-chain `approve(Permit2)` that costs Robinhood Chain ETH gas — pass `--yes` to allow it. Results show `status`, `finality`, `receivedToken` and a remediation; a `pending` result means the swap was submitted — **do not resubmit**.
224
+ - **Results:** spot orders print the normalized result (status, finality, filled size, average price, received token, remediation). `spot-buy` / `spot-sell` add an `execution` record on every venue (method, slippage bps + source, protection price and reference where the venue path computes one). `spot-cancel` returns one `SpotOrderResult` shape on every venue (a Lighter cancel is `pending` until `spot-orders` confirms it); `spot-orders` lists the same columns on every venue (Hyperliquid included).
225
+ - **Portfolio / landing:** spot value is added on top of perp equity except balances marked `shared` (Pacifica USDC, Hyperliquid unified USDC). Tokens without a spot price are excluded from totals with a stderr warning (never valued at 0).
226
+ - **Bots and arb** (`funding-arb`, `spot-perp-arb`, `arb exec spot:<venue>`, `arb close`, `arb scan --mode spot-perp`) only use spot venues with a separate (or runtime) spot balance **and** a spot↔perp transfer: hyperliquid, lighter, lighter-rh, aster, ondo. Pacifica (shared margin) and Arcus (on-chain swaps) are refused with `CAPABILITY_UNSUPPORTED`; trade them manually with `trade spot-*`.
227
+
47
228
  ## Command Groups
48
229
 
49
230
  | Group | Description |
@@ -51,22 +232,35 @@ Same EVM key works for both Hyperliquid and Lighter.
51
232
  | `market` | Prices, orderbook, funding, klines, HIP-3 dexes |
52
233
  | `account` | Balance, positions, orders, margin |
53
234
  | `trade` | Market/limit/stop orders, close, scale, split execution |
235
+ | `outcome` | Hyperliquid Outcome (HIP-4) — binary/range contracts, quoted per outcome (outcomeMeta `quoteToken`, USDC today), no leverage |
54
236
  | `arb` | Funding rate arb — scan, exec, close, monitor (perp-perp & spot-perp) |
55
237
  | `strategy` | 19 bot algorithms (grid, dca, twap, APEX, REFLECT, presets) + nested scripted plans |
56
238
  | `funds` | Deposit, withdraw, transfer, cross-chain bridge (multi-provider), inter-exchange rebalance |
57
239
  | `risk` | Risk limits, liquidation distance, guardrails |
58
- | `wallet` | Multi-wallet management & on-chain balances |
240
+ | `wallet` | Multi-wallet management, agent wallets (`wallet agent ...`), builder fees (`wallet builder ...`, off by default), margin mode / subaccount / API keys (`wallet manage ...`), on-chain balances |
59
241
  | `history` | Execution log, PnL, performance breakdown |
60
- | `manage` | Margin mode, subaccount, API keys, builder |
61
- | `portfolio` | Cross-exchange unified overview |
62
- | `dashboard` | Live web dashboard |
63
- | `settings` | CLI settings (referrals, defaults) |
242
+ | `portfolio` | Cross-exchange unified overview (replaces former `account balance` / `status` / `dashboard`) |
243
+ | `health` | Adapter health check: ping every venue's REST endpoint (connectivity + latency) |
244
+ | `settings` | CLI settings (referrals, defaults, fees, builder fees, deBridge affiliate) |
64
245
  | `backtest` | Strategy backtesting |
65
246
  | `background` | Background process supervisor (tmux sessions for strategies, alerts, etc.) |
66
- | `alerts` | Telegram funding rate alerts with background daemon |
67
- | `agent` | Schema introspection, capabilities, health check |
68
- | `setup` | Interactive setup wizard (alias: `init`) |
69
- | `status` | Unified dashboard: balances, positions, arb opps |
247
+ | `alerts` | Funding rate alerts (Telegram / Discord) with background daemon |
248
+ | `setup` | Setup wizard: OWS wallet, venues, agent wallets, slippage, default venue (alias: `init`; `--non-interactive` for scripts) |
249
+
250
+ ### Builder fees (off by default)
251
+
252
+ perp-cli attaches **no** builder fee unless you configure one and the venue approval is confirmed:
253
+
254
+ ```bash
255
+ perp settings set builder.hyperliquid 0xBuilder…,1 # <address|code>,<feeBps> — strict: format, venue cap; asks y/N (or --yes)
256
+ perp wallet builder status # per venue: supported? configured? approved? attached?
257
+ perp --dry-run wallet builder approve hyperliquid # plan only
258
+ perp wallet builder approve hyperliquid # master-signed, asks y/N (or --yes)
259
+ perp --no-builder trade market BTC buy 0.1 # skip it for one command
260
+ perp settings unset builder.hyperliquid # off again
261
+ ```
262
+
263
+ Supported: hyperliquid (≤ 10 bps, not on HIP-3 dexes), pacifica (≤ 10 bps max, not on spot), aster (≤ 10 bps, mainnet), ondo (≤ 10 bps, no approval step), qfex (`perp settings set builder.qfex <uuid>` — a **fee share**: your fills are attributed to the builder code and you pay the normal fee, nothing extra; the code rides on the trading connection; mainnet only). Lighter / Lighter RH / Arcus: unsupported. If the approval is missing or cannot be read, the order is sent **without** the fee and the result's `execution.notes` says why; `execution.builderFee` shows what was attached.
70
264
 
71
265
  ## Core Commands
72
266
 
@@ -91,16 +285,29 @@ perp --json -e <EX> trade close <SYM>
91
285
  perp --json -e <EX> trade flatten # close ALL positions on exchange
92
286
  perp --json -e <EX> trade reduce <SYM> 50 # reduce position by 50%
93
287
  perp --json -e <EX> trade cancel <SYM> # cancel by symbol (or orderId)
94
- perp --json -e <EX> trade tpsl <SYM> long # set take-profit / stop-loss
288
+ perp --json -e <EX> trade buy <SYM> <SIZE> --tp <PX> --sl <PX> # entry protected by TP/SL
289
+ perp --json -e <EX> trade tpsl <SYM> long --tp <PX> --sl <PX> # TP/SL on an open position (long|short)
290
+ perp --json -e <EX> trade stop <SYM> sell <SIZE> <PX> # reduce-only stop (default)
95
291
  perp --json -e <EX> trade leverage <SYM> <N>
96
292
 
97
293
  # Account
98
- perp --json -e <EX> account balance
294
+ perp --json portfolio -e <EX> # balance + spot + positions (account balance was renamed in v0.12)
99
295
  perp --json -e <EX> account positions
100
296
  perp --json -e <EX> account pnl # realized + unrealized + funding
101
297
  perp --json -e <EX> account funding # personal funding payment history
102
298
  perp --json -e <EX> account settings # per-market leverage & margin mode
103
299
 
300
+ # Outcome markets (Hyperliquid HIP-4 — fully-collateralized binary contracts, quote token per outcome (USDC today), min 10 quote tokens)
301
+ perp --json outcome list # active markets + Yes/No mid prices
302
+ perp --json outcome view <outcome> # symmetric Yes/No book + underlying gap + expiry
303
+ perp --json outcome book <outcome> <side> # one-side orderbook (e.g. '1 yes' or '1 0')
304
+ perp --json outcome positions # open outcome holdings
305
+ perp --json outcome orders # open outcome orders
306
+ perp --json outcome buy <outcome> <side> <usd> --dry-run # validate before submit
307
+ perp --json outcome buy <outcome> <side> <usd> # market buy, notional in the outcome's quote token
308
+ perp --json outcome sell <outcome> <side> <usd> --limit <px> --tif gtc
309
+ perp --json outcome cancel <outcome> <side> <oid>
310
+
104
311
  # Funding rate arbitrage
105
312
  perp --json arb scan --min 5 # perp-perp opportunities
106
313
  perp --json arb scan --mode spot-perp # spot+perp opportunities
@@ -117,9 +324,19 @@ perp --json arb history # past arb trade performanc
117
324
  # Funds (deposit, withdraw, transfer, bridge, rebalance)
118
325
  perp --json funds deposit hyperliquid 100
119
326
  perp --json funds withdraw pacifica 50
327
+ perp --json funds deposit lighter-rh 10 # USDG on Robinhood Chain (approve + deposit, mainnet)
328
+ perp --json -e lighter-rh funds withdraw lighter 10 # USDG → Robinhood Chain
329
+ perp --json --dry-run funds withdraw ondo 10 --to <ADDRESS> # plan only, nothing sent
330
+ perp --json funds deposit aster 8 # USDT → Aster vault on BNB Chain (--chain arbitrum / --asset USDC; mainnet)
331
+ perp --json funds withdraw aster 8 --chain bsc # USDT → your own address (agent needs --can-withdraw --ip-whitelist)
332
+ perp --json funds deposit ondo 12 --probe 2 # USDC transfer to your Ondo funding address (Arbitrum; --chain ethereum)
333
+ perp --json funds address-book add ondo <MASTER_ADDR> # SIWE challenge → Ondo withdrawal address book
334
+ perp --json funds withdraw arcus 10 # to the Arcus account owner
120
335
  perp --json funds transfer 100 <ADDRESS> # HL internal transfer (instant)
121
336
  perp --json funds bridge quote --from solana --to arbitrum --amount 100
122
337
  perp --json funds bridge send --from solana --to arbitrum --amount 100 # auto-cheapest provider
338
+ perp --json --dry-run funds bridge send --from robinhood --to solana --from-asset USDG --to-asset SOL --amount 15 # Relay quote only
339
+ perp --json funds bridge send --from robinhood --to bsc --from-asset USDG --to-asset USDT --amount 15 # Relay swap, OWS master signs
123
340
  perp --json funds bridge exchange --from pacifica --to hyperliquid --amount 100
124
341
  perp --json funds rebalance check # balances across exchanges
125
342
  perp --json funds rebalance plan # compute optimal moves
@@ -146,6 +363,9 @@ perp --json strategy plan execute <FILE> --dry-run # dry-run scripted plan
146
363
  perp background list # list running jobs
147
364
  perp background stop <ID> # stop a job
148
365
  perp background logs <ID> -f # follow logs
366
+ # Jobs inherit -n, --dry-run, -e, --ows, -w, --no-agent, --dex and never receive keys via argv/env:
367
+ # they read the OWS vault / agent keystores / ~/.perp/.env themselves. -k, --passphrase, --ows-key or a
368
+ # shell-exported key / OWS_PASSPHRASE is refused — register an agent (perp wallet agent approve <venue>).
149
369
 
150
370
  ```
151
371
 
@@ -185,6 +405,8 @@ perp-cli includes a full-featured MCP server (18 tools, 3 resources, 2 prompts)
185
405
 
186
406
  **No API keys required for market data** — explore prices, orderbooks, funding rates, and arb opportunities without any setup. Add keys only when you want to trade.
187
407
 
408
+ **Network:** set `PERP_NETWORK=testnet` in the server env to serve testnet (validated; unset = mainnet); every result reports `meta.network`. `trade_execute` / `trade_close` place real orders through the same local risk gate as `perp trade` (no override over MCP), refuse non-reduce-only orders on quarantined symbols, and are written to the execution log.
409
+
188
410
  ```json
189
411
  {
190
412
  "mcpServers": {
@@ -196,14 +418,11 @@ perp-cli includes a full-featured MCP server (18 tools, 3 resources, 2 prompts)
196
418
  }
197
419
  ```
198
420
 
199
- Optional: add keys for trading and account data:
421
+ For trading and account data, run `perp setup` first: the MCP server reads the same OWS vault wallet and agent keystores as the CLI. Never put private keys in the MCP config. Select the network with `PERP_NETWORK` (`mainnet` | `testnet`; unset = mainnet, invalid values stop the server):
200
422
  ```json
201
423
  {
202
424
  "env": {
203
- "PACIFICA_PRIVATE_KEY": "your-solana-key",
204
- "HYPERLIQUID_PRIVATE_KEY": "your-evm-key",
205
- "LIGHTER_PRIVATE_KEY": "your-evm-key",
206
- "ASTER_API_KEY": "your-api-key"
425
+ "PERP_NETWORK": "testnet"
207
426
  }
208
427
  }
209
428
  ```
@@ -276,8 +495,9 @@ perp --json --ndjson -e hl market list
276
495
  perp --json -e hl trade check BTC buy 0.01
277
496
  perp --json --dry-run -e hl trade market BTC buy 0.01
278
497
 
279
- # Idempotent orders with client ID
280
- perp --json -e hl trade market BTC buy 0.01 --client-id my-unique-id
498
+ # Idempotent orders with client ID (Hyperliquid: a 128-bit cloid, 0x + 32 hex; --auto-id generates one)
499
+ perp --json -e hl trade market BTC buy 0.01 --client-id 0x1234567890abcdef1234567890abcdef
500
+ perp --json -e hl trade status c:0x1234567890abcdef1234567890abcdef
281
501
  ```
282
502
 
283
503
  All responses are auto-sanitized (control chars stripped, prompt injection patterns blocked).
@@ -5,7 +5,6 @@
5
5
  if (process.argv.includes("--json") || process.argv.includes("--ndjson")) {
6
6
  const _suppressPatterns = [
7
7
  /'--chain evm' is deprecated/,
8
- /\[hyperliquid\] Failed to load asset map/,
9
8
  /\[lighter\] Account index not available/,
10
9
  /Failed to initialize SymbolConversion/,
11
10
  /HyperliquidAPIError/,
@@ -32,3 +32,14 @@ export declare function deleteLighterKey(accountIndex: number, slot: number): bo
32
32
  * never reads from keystore back to env.
33
33
  */
34
34
  export declare function migrateFromEnvIfPresent(): void;
35
+ export type LighterFamilyVenue = "lighter" | "lighter-rh";
36
+ type KeystoreNetwork = "mainnet" | "testnet";
37
+ /** Absolute (network-tagged) keystore path for a Lighter-family L2 key. */
38
+ export declare function lighterFamilyKeystorePath(venue: LighterFamilyVenue, network: KeystoreNetwork, accountIndex: number, slot: number): string;
39
+ /** Encrypt + persist a Lighter-family L2 private key (hex) at its network-tagged path. Returns the absolute path. */
40
+ export declare function saveLighterFamilyKey(venue: LighterFamilyVenue, network: KeystoreNetwork, accountIndex: number, slot: number, privateKeyHex: string, passphrase: string): string;
41
+ /** Read + decrypt a Lighter-family L2 private key (hex, no 0x) for `network`. Throws on missing/corrupt/wrong passphrase. */
42
+ export declare function loadLighterFamilyKey(venue: LighterFamilyVenue, network: KeystoreNetwork, accountIndex: number, slot: number, passphrase: string): string;
43
+ /** Idempotent delete of the `network` key (after migrating an untagged file). Returns true if a file was removed. */
44
+ export declare function deleteLighterFamilyKey(venue: LighterFamilyVenue, network: KeystoreNetwork, accountIndex: number, slot: number): boolean;
45
+ export {};
@@ -14,10 +14,11 @@
14
14
  * No fallbacks (SSOT Rule #2): missing files / wrong passphrase throw with
15
15
  * remediation pointing the user at `perp wallet agent approve lighter --rotate`.
16
16
  */
17
- import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, chmodSync, } from "fs";
17
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, renameSync, chmodSync, } from "fs";
18
18
  import { resolve } from "path";
19
19
  import { randomBytes, scryptSync, createCipheriv, createDecipheriv, } from "node:crypto";
20
20
  import { PerpError } from "../errors.js";
21
+ import { loadVenueSecret, saveVenueSecret, venueKeystorePath, writeKeystoreAtomic } from "./venue-keystore.js";
21
22
  const KDF_PARAMS = { dklen: 32, n: 65536, p: 1, r: 8 };
22
23
  const SCRYPT_OPTS = { N: KDF_PARAMS.n, r: KDF_PARAMS.r, p: KDF_PARAMS.p, maxmem: 128 * 1024 * 1024 };
23
24
  const REMEDIATION = "perp wallet agent approve lighter --rotate";
@@ -52,8 +53,11 @@ function stripHexPrefix(hex) {
52
53
  * Returns the absolute path of the written file.
53
54
  */
54
55
  export function saveLighterKey(accountIndex, slot, privateKeyHex, passphrase) {
55
- const dir = ensureDir();
56
- const path = resolve(dir, `${accountIndex}-${slot}.json`);
56
+ ensureDir();
57
+ return writeLighterKeyFile(lighterKeystorePath(accountIndex, slot), accountIndex, slot, privateKeyHex, passphrase);
58
+ }
59
+ function writeLighterKeyFile(path, accountIndex, slot, privateKeyHex, passphrase) {
60
+ ensureDir();
57
61
  const cleanHex = stripHexPrefix(privateKeyHex).toLowerCase();
58
62
  if (!/^[0-9a-f]+$/.test(cleanHex) || cleanHex.length === 0) {
59
63
  throw new PerpError("INVALID_PARAMS", "Lighter L2 private key must be hex", {
@@ -89,12 +93,7 @@ export function saveLighterKey(accountIndex, slot, privateKeyHex, passphrase) {
89
93
  },
90
94
  createdAt: new Date().toISOString(),
91
95
  };
92
- writeFileSync(path, JSON.stringify(file, null, 2), { mode: 0o600 });
93
- // Re-tighten in case writeFileSync overwrote an existing file with a looser umask
94
- try {
95
- chmodSync(path, 0o600);
96
- }
97
- catch { /* non-fatal */ }
96
+ writeKeystoreAtomic(path, JSON.stringify(file, null, 2));
98
97
  return path;
99
98
  }
100
99
  /**
@@ -111,7 +110,9 @@ export function loadLighterKey(accountIndex, slot, passphrase) {
111
110
  // before continuing. This is NOT a runtime fallback — it's a single-pass
112
111
  // upgrade path that runs at most once per install.
113
112
  migrateFromEnvIfPresent();
114
- const path = lighterKeystorePath(accountIndex, slot);
113
+ return readLighterKeyFile(lighterKeystorePath(accountIndex, slot), passphrase);
114
+ }
115
+ function readLighterKeyFile(path, passphrase) {
115
116
  if (!existsSync(path)) {
116
117
  throw new PerpError("KEY_NOT_FOUND", `LIGHTER_KEYSTORE_NOT_FOUND: ${path}`, { remediation: REMEDIATION });
117
118
  }
@@ -230,3 +231,84 @@ function clearEnvLegacyKeys() {
230
231
  }
231
232
  writeFileSync(ENV_FILE, out.join("\n"), { mode: 0o600 });
232
233
  }
234
+ const LIGHTER_RH_AGENTS_DIR = "lighter-rh-agents";
235
+ const LIGHTER_RH_REMEDIATION = "perp wallet agent approve lighter-rh --rotate";
236
+ function rhKeyName(accountIndex, slot) {
237
+ if (!Number.isInteger(accountIndex) || accountIndex < 0 || !Number.isInteger(slot) || slot < 0) {
238
+ throw new PerpError("INVALID_PARAMS", `invalid Lighter RH keystore address: account ${accountIndex}, slot ${slot}`, {
239
+ remediation: LIGHTER_RH_REMEDIATION,
240
+ });
241
+ }
242
+ return `${accountIndex}-${slot}`;
243
+ }
244
+ /** Network-tagged keystore file name (`<network>-<accountIndex>-<slot>`). Account indexes are per network. */
245
+ function taggedName(network, accountIndex, slot) {
246
+ return `${network}-${rhKeyName(accountIndex, slot)}`;
247
+ }
248
+ /** Absolute (network-tagged) keystore path for a Lighter-family L2 key. */
249
+ export function lighterFamilyKeystorePath(venue, network, accountIndex, slot) {
250
+ switch (venue) {
251
+ case "lighter": return resolve(lighterAgentsDir(), `${taggedName(network, accountIndex, slot)}.json`);
252
+ case "lighter-rh": return venueKeystorePath(LIGHTER_RH_AGENTS_DIR, taggedName(network, accountIndex, slot));
253
+ }
254
+ }
255
+ /** Pre-tagging path `<accountIndex>-<slot>` (no network). */
256
+ function legacyFamilyKeystorePath(venue, accountIndex, slot) {
257
+ switch (venue) {
258
+ case "lighter": return lighterKeystorePath(accountIndex, slot);
259
+ case "lighter-rh": return venueKeystorePath(LIGHTER_RH_AGENTS_DIR, rhKeyName(accountIndex, slot));
260
+ }
261
+ }
262
+ /**
263
+ * One-time move of a pre-tagging `<accountIndex>-<slot>` file to the tagged path. Called only
264
+ * with the network of a network-TAGGED agent record (untagged records are refused upstream by
265
+ * agentNetworkOf → re-register), and approve refuses to reuse another network's account+slot,
266
+ * so the untagged file belongs to that record's network.
267
+ */
268
+ function migrateUntaggedKeystore(venue, network, accountIndex, slot) {
269
+ const tagged = lighterFamilyKeystorePath(venue, network, accountIndex, slot);
270
+ if (existsSync(tagged))
271
+ return;
272
+ const legacy = legacyFamilyKeystorePath(venue, accountIndex, slot);
273
+ if (!existsSync(legacy))
274
+ return;
275
+ renameSync(legacy, tagged);
276
+ process.stderr.write(`[${venue}] keystore ${accountIndex}-${slot} tagged as ${network} (moved to ${tagged})\n`);
277
+ }
278
+ /** Encrypt + persist a Lighter-family L2 private key (hex) at its network-tagged path. Returns the absolute path. */
279
+ export function saveLighterFamilyKey(venue, network, accountIndex, slot, privateKeyHex, passphrase) {
280
+ switch (venue) {
281
+ case "lighter":
282
+ return writeLighterKeyFile(lighterFamilyKeystorePath(venue, network, accountIndex, slot), accountIndex, slot, privateKeyHex, passphrase);
283
+ case "lighter-rh": {
284
+ const cleanHex = stripHexPrefix(privateKeyHex).toLowerCase();
285
+ if (!/^[0-9a-f]+$/.test(cleanHex)) {
286
+ throw new PerpError("INVALID_PARAMS", "Lighter RH L2 private key must be hex", {
287
+ remediation: LIGHTER_RH_REMEDIATION,
288
+ });
289
+ }
290
+ return saveVenueSecret(LIGHTER_RH_AGENTS_DIR, taggedName(network, accountIndex, slot), cleanHex, passphrase);
291
+ }
292
+ }
293
+ }
294
+ /** Read + decrypt a Lighter-family L2 private key (hex, no 0x) for `network`. Throws on missing/corrupt/wrong passphrase. */
295
+ export function loadLighterFamilyKey(venue, network, accountIndex, slot, passphrase) {
296
+ if (venue === "lighter")
297
+ migrateFromEnvIfPresent();
298
+ migrateUntaggedKeystore(venue, network, accountIndex, slot);
299
+ switch (venue) {
300
+ case "lighter":
301
+ return readLighterKeyFile(lighterFamilyKeystorePath(venue, network, accountIndex, slot), passphrase);
302
+ case "lighter-rh":
303
+ return loadVenueSecret(LIGHTER_RH_AGENTS_DIR, taggedName(network, accountIndex, slot), passphrase, LIGHTER_RH_REMEDIATION);
304
+ }
305
+ }
306
+ /** Idempotent delete of the `network` key (after migrating an untagged file). Returns true if a file was removed. */
307
+ export function deleteLighterFamilyKey(venue, network, accountIndex, slot) {
308
+ migrateUntaggedKeystore(venue, network, accountIndex, slot);
309
+ const path = lighterFamilyKeystorePath(venue, network, accountIndex, slot);
310
+ if (!existsSync(path))
311
+ return false;
312
+ unlinkSync(path);
313
+ return true;
314
+ }
@@ -2,7 +2,7 @@
2
2
  * Resolve master passphrase from (in precedence order):
3
3
  * 1. opts.flag (--passphrase)
4
4
  * 2. process.env.OWS_PASSPHRASE
5
- * 3. stdin (only if process.stdin.isTTY === false)
5
+ * 3. stdin (only when stdin is not a TTY — Node leaves isTTY undefined for pipes/files)
6
6
  *
7
7
  * Returns null if no path provides one AND stdin IS TTY (caller may prompt).
8
8
  * Throws PASSPHRASE_REQUIRED with remediation if no path provides one AND
@@ -1,10 +1,11 @@
1
+ import { fstatSync } from "node:fs";
1
2
  import { PerpError } from "../errors.js";
2
3
  const REMEDIATION = "Provide passphrase via --passphrase flag, OWS_PASSPHRASE env var, or stdin pipe (echo $PP | perp ...)";
3
4
  /**
4
5
  * Resolve master passphrase from (in precedence order):
5
6
  * 1. opts.flag (--passphrase)
6
7
  * 2. process.env.OWS_PASSPHRASE
7
- * 3. stdin (only if process.stdin.isTTY === false)
8
+ * 3. stdin (only when stdin is not a TTY — Node leaves isTTY undefined for pipes/files)
8
9
  *
9
10
  * Returns null if no path provides one AND stdin IS TTY (caller may prompt).
10
11
  * Throws PASSPHRASE_REQUIRED with remediation if no path provides one AND
@@ -20,8 +21,16 @@ export async function resolvePassphrase(opts) {
20
21
  if ("OWS_PASSPHRASE" in process.env) {
21
22
  return process.env["OWS_PASSPHRASE"] ?? "";
22
23
  }
23
- // 3. stdin pipe — only when stdin is not a TTY
24
- if (process.stdin.isTTY === false) {
24
+ // 3. stdin pipe — only when stdin is not a TTY. Node sets isTTY only for a TTY;
25
+ // a pipe or redirected file leaves it undefined, so test `!== true`.
26
+ if (process.stdin.isTTY !== true) {
27
+ // Read only a real pipe or redirected file. Anything else (/dev/null, a socket,
28
+ // an inherited never-closing stream) has no passphrase to give — reading it could block forever.
29
+ if (!stdinIsPipeOrFile()) {
30
+ throw new PerpError("PASSPHRASE_REQUIRED", "No passphrase provided and stdin is not a terminal or a pipe", {
31
+ remediation: REMEDIATION,
32
+ });
33
+ }
25
34
  const piped = await readStdin();
26
35
  if (piped !== null) {
27
36
  return piped;
@@ -34,6 +43,10 @@ export async function resolvePassphrase(opts) {
34
43
  // stdin IS TTY — caller decides whether to prompt interactively
35
44
  return null;
36
45
  }
46
+ function stdinIsPipeOrFile() {
47
+ const st = fstatSync(0);
48
+ return st.isFIFO() || st.isFile();
49
+ }
37
50
  /**
38
51
  * Read all bytes from stdin until EOF, trim trailing newlines.
39
52
  * Returns null if stdin produced zero bytes.