perp-cli 0.14.3 → 0.16.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 (363) hide show
  1. package/README.ja.md +243 -0
  2. package/README.ko.md +243 -0
  3. package/README.md +138 -405
  4. package/README.zh.md +243 -0
  5. package/dist/agent-wallet/store.d.ts +3 -2
  6. package/dist/agent-wallet/store.js +2 -2
  7. package/dist/api/public/hyperliquid.js +4 -0
  8. package/dist/api/public/lighter.d.ts +5 -0
  9. package/dist/api/public/lighter.js +5 -0
  10. package/dist/api/public/pacifica.js +85 -15
  11. package/dist/api/public/urls.d.ts +2 -0
  12. package/dist/api/public/urls.js +3 -0
  13. package/dist/arb/sizing.js +4 -2
  14. package/dist/arb/state.d.ts +6 -0
  15. package/dist/arb/utils.d.ts +1 -2
  16. package/dist/arb/utils.js +1 -2
  17. package/dist/balance-by-address.d.ts +39 -0
  18. package/dist/balance-by-address.js +44 -0
  19. package/dist/bot/apex/pulse.js +3 -0
  20. package/dist/bot/apex/radar.js +2 -1
  21. package/dist/bot/engine.d.ts +10 -1
  22. package/dist/bot/engine.js +14 -6
  23. package/dist/bot/indicators.d.ts +7 -0
  24. package/dist/bot/indicators.js +16 -0
  25. package/dist/bot/perp-order.d.ts +4 -4
  26. package/dist/bot/perp-order.js +5 -5
  27. package/dist/bot/quarantine.d.ts +1 -1
  28. package/dist/bot/quarantine.js +1 -1
  29. package/dist/bot/quote-rules.js +1 -1
  30. package/dist/bot/strategies/apex-strategy.js +3 -2
  31. package/dist/bot/strategies/engine-mm.js +6 -5
  32. package/dist/bot/strategies/funding-arb-strategy.js +12 -5
  33. package/dist/bot/strategies/funding-arb-utils.d.ts +15 -3
  34. package/dist/bot/strategies/funding-arb-utils.js +53 -20
  35. package/dist/bot/strategies/funding-auto.js +2 -2
  36. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  37. package/dist/bot/strategies/liquidation-mm.js +8 -2
  38. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  39. package/dist/bot/strategies/momentum-breakout.js +11 -3
  40. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +1 -1
  41. package/dist/bot/strategies/spot-perp-arb-strategy.js +6 -4
  42. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  43. package/dist/bot/strategies/spot-perp-executor.js +3 -3
  44. package/dist/bot/strategy-types.d.ts +2 -1
  45. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  46. package/dist/bot/tui/BotDashboard.js +1 -1
  47. package/dist/cache.js +12 -6
  48. package/dist/cli-program.d.ts +15 -0
  49. package/dist/cli-program.js +126 -6
  50. package/dist/commands/account.js +168 -24
  51. package/dist/commands/agent-import.d.ts +7 -0
  52. package/dist/commands/agent-import.js +30 -1
  53. package/dist/commands/agent.d.ts +6 -3
  54. package/dist/commands/agent.js +160 -12
  55. package/dist/commands/arb/index.js +23 -81
  56. package/dist/commands/arb-auto.d.ts +24 -0
  57. package/dist/commands/arb-auto.js +175 -97
  58. package/dist/commands/arb.d.ts +20 -0
  59. package/dist/commands/arb.js +107 -163
  60. package/dist/commands/backtest.js +23 -21
  61. package/dist/commands/bot.js +1 -1
  62. package/dist/commands/builder.js +1 -1
  63. package/dist/commands/funds.js +113 -3
  64. package/dist/commands/init.d.ts +1 -1
  65. package/dist/commands/init.js +27 -7
  66. package/dist/commands/manage.js +23 -21
  67. package/dist/commands/market.d.ts +48 -0
  68. package/dist/commands/market.js +395 -100
  69. package/dist/commands/options.d.ts +14 -0
  70. package/dist/commands/options.js +151 -0
  71. package/dist/commands/pacifica-earn.d.ts +5 -0
  72. package/dist/commands/pacifica-earn.js +455 -0
  73. package/dist/commands/pacifica-orders.d.ts +14 -0
  74. package/dist/commands/pacifica-orders.js +142 -0
  75. package/dist/commands/portfolio.d.ts +16 -0
  76. package/dist/commands/portfolio.js +83 -55
  77. package/dist/commands/rebalance.d.ts +1 -0
  78. package/dist/commands/rebalance.js +15 -0
  79. package/dist/commands/risk.d.ts +11 -2
  80. package/dist/commands/risk.js +39 -44
  81. package/dist/commands/trade.d.ts +1 -0
  82. package/dist/commands/trade.js +67 -62
  83. package/dist/commands/wallet.js +1 -1
  84. package/dist/config.d.ts +2 -2
  85. package/dist/config.js +15 -4
  86. package/dist/constants.js +6 -0
  87. package/dist/cross-chain-margin.d.ts +3 -1
  88. package/dist/cross-chain-margin.js +4 -2
  89. package/dist/dashboard/server.js +3 -3
  90. package/dist/dashboard/ui.js +9 -6
  91. package/dist/errors.d.ts +29 -1
  92. package/dist/errors.js +35 -3
  93. package/dist/exchanges/address.d.ts +8 -0
  94. package/dist/exchanges/address.js +47 -0
  95. package/dist/exchanges/arcus/adapter.d.ts +16 -12
  96. package/dist/exchanges/arcus/adapter.js +45 -18
  97. package/dist/exchanges/arcus/agent.d.ts +1 -1
  98. package/dist/exchanges/arcus/agent.js +1 -1
  99. package/dist/exchanges/arcus/client.js +1 -1
  100. package/dist/exchanges/arcus/public.js +3 -2
  101. package/dist/exchanges/arcus/signing.d.ts +2 -3
  102. package/dist/exchanges/arcus/signing.js +1 -1
  103. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  104. package/dist/exchanges/arcus/spot-chain.js +1 -1
  105. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  106. package/dist/exchanges/arcus/spot-router.js +2 -2
  107. package/dist/exchanges/arcus/spot.d.ts +2 -3
  108. package/dist/exchanges/arcus/spot.js +2 -3
  109. package/dist/exchanges/aster-spot.d.ts +3 -3
  110. package/dist/exchanges/aster-spot.js +6 -9
  111. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  112. package/dist/exchanges/aster-typed-data.js +1 -1
  113. package/dist/exchanges/aster.d.ts +30 -10
  114. package/dist/exchanges/aster.js +153 -53
  115. package/dist/exchanges/builder-fee.d.ts +1 -1
  116. package/dist/exchanges/builder-fee.js +13 -2
  117. package/dist/exchanges/canonical-symbols.d.ts +8 -2
  118. package/dist/exchanges/canonical-symbols.js +46 -2
  119. package/dist/exchanges/capabilities.d.ts +2 -0
  120. package/dist/exchanges/capabilities.js +2 -1
  121. package/dist/exchanges/derive/client.d.ts +37 -0
  122. package/dist/exchanges/derive/client.js +135 -0
  123. package/dist/exchanges/derive/options.d.ts +73 -0
  124. package/dist/exchanges/derive/options.js +379 -0
  125. package/dist/exchanges/extended/adapter.d.ts +96 -0
  126. package/dist/exchanges/extended/adapter.js +241 -0
  127. package/dist/exchanges/extended/client.d.ts +38 -0
  128. package/dist/exchanges/extended/client.js +88 -0
  129. package/dist/exchanges/extended/public.d.ts +133 -0
  130. package/dist/exchanges/extended/public.js +436 -0
  131. package/dist/exchanges/grvt/adapter.d.ts +100 -0
  132. package/dist/exchanges/grvt/adapter.js +283 -0
  133. package/dist/exchanges/grvt/client.d.ts +25 -0
  134. package/dist/exchanges/grvt/client.js +86 -0
  135. package/dist/exchanges/grvt/public.d.ts +151 -0
  136. package/dist/exchanges/grvt/public.js +438 -0
  137. package/dist/exchanges/hyperliquid-outcome.d.ts +2 -0
  138. package/dist/exchanges/hyperliquid-outcome.js +29 -1
  139. package/dist/exchanges/hyperliquid-spot.d.ts +9 -1
  140. package/dist/exchanges/hyperliquid-spot.js +24 -16
  141. package/dist/exchanges/hyperliquid.d.ts +199 -22
  142. package/dist/exchanges/hyperliquid.js +550 -202
  143. package/dist/exchanges/index.d.ts +1 -1
  144. package/dist/exchanges/index.js +1 -1
  145. package/dist/exchanges/interface.d.ts +157 -13
  146. package/dist/exchanges/interface.js +47 -0
  147. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  148. package/dist/exchanges/ioc-outcome.js +1 -1
  149. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  150. package/dist/exchanges/legacy-trading-v2.js +7 -2
  151. package/dist/exchanges/lighter-public.d.ts +67 -12
  152. package/dist/exchanges/lighter-public.js +250 -82
  153. package/dist/exchanges/lighter-signer.d.ts +1 -1
  154. package/dist/exchanges/lighter-signer.js +1 -1
  155. package/dist/exchanges/lighter-spot.d.ts +10 -5
  156. package/dist/exchanges/lighter-spot.js +105 -97
  157. package/dist/exchanges/lighter.d.ts +80 -21
  158. package/dist/exchanges/lighter.js +312 -105
  159. package/dist/exchanges/ondo/adapter.d.ts +38 -13
  160. package/dist/exchanges/ondo/adapter.js +86 -27
  161. package/dist/exchanges/ondo/agent.d.ts +1 -1
  162. package/dist/exchanges/ondo/agent.js +1 -1
  163. package/dist/exchanges/ondo/client.d.ts +1 -1
  164. package/dist/exchanges/ondo/client.js +2 -2
  165. package/dist/exchanges/ondo/public.d.ts +35 -3
  166. package/dist/exchanges/ondo/public.js +75 -32
  167. package/dist/exchanges/ondo/spot.d.ts +2 -2
  168. package/dist/exchanges/ondo/spot.js +3 -3
  169. package/dist/exchanges/options-interface.d.ts +158 -0
  170. package/dist/exchanges/options-interface.js +17 -0
  171. package/dist/exchanges/options-venues.d.ts +12 -0
  172. package/dist/exchanges/options-venues.js +35 -0
  173. package/dist/exchanges/outcome-interface.d.ts +4 -0
  174. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  175. package/dist/exchanges/pacifica-atomic.js +440 -0
  176. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  177. package/dist/exchanges/pacifica-decimal.js +51 -0
  178. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  179. package/dist/exchanges/pacifica-presigned.js +141 -0
  180. package/dist/exchanges/pacifica-print.d.ts +200 -0
  181. package/dist/exchanges/pacifica-print.js +417 -0
  182. package/dist/exchanges/pacifica-spot.d.ts +8 -2
  183. package/dist/exchanges/pacifica-spot.js +26 -18
  184. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  185. package/dist/exchanges/pacifica-typed-data.js +1 -1
  186. package/dist/exchanges/pacifica-vaults.d.ts +130 -0
  187. package/dist/exchanges/pacifica-vaults.js +277 -0
  188. package/dist/exchanges/pacifica.d.ts +108 -10
  189. package/dist/exchanges/pacifica.js +428 -171
  190. package/dist/exchanges/precision.d.ts +3 -1
  191. package/dist/exchanges/precision.js +9 -2
  192. package/dist/exchanges/public-balances.d.ts +25 -0
  193. package/dist/exchanges/public-balances.js +158 -0
  194. package/dist/exchanges/public-positions.d.ts +43 -0
  195. package/dist/exchanges/public-positions.js +154 -0
  196. package/dist/exchanges/qfex/adapter.d.ts +69 -13
  197. package/dist/exchanges/qfex/adapter.js +102 -17
  198. package/dist/exchanges/qfex/agent.d.ts +1 -1
  199. package/dist/exchanges/qfex/agent.js +1 -1
  200. package/dist/exchanges/qfex/client.d.ts +1 -1
  201. package/dist/exchanges/qfex/client.js +1 -1
  202. package/dist/exchanges/qfex/errors.d.ts +1 -1
  203. package/dist/exchanges/qfex/errors.js +2 -2
  204. package/dist/exchanges/qfex/public.d.ts +65 -12
  205. package/dist/exchanges/qfex/public.js +131 -25
  206. package/dist/exchanges/qfex/signing.d.ts +2 -2
  207. package/dist/exchanges/qfex/signing.js +4 -5
  208. package/dist/exchanges/qfex/ws-transport.d.ts +3 -3
  209. package/dist/exchanges/qfex/ws-transport.js +3 -3
  210. package/dist/exchanges/rate-limiter.d.ts +1 -1
  211. package/dist/exchanges/rate-limiter.js +5 -2
  212. package/dist/exchanges/registry.d.ts +1 -1
  213. package/dist/exchanges/registry.js +1 -1
  214. package/dist/exchanges/risex/adapter.d.ts +160 -0
  215. package/dist/exchanges/risex/adapter.js +842 -0
  216. package/dist/exchanges/risex/agent.d.ts +154 -0
  217. package/dist/exchanges/risex/agent.js +384 -0
  218. package/dist/exchanges/risex/client.d.ts +47 -0
  219. package/dist/exchanges/risex/client.js +174 -0
  220. package/dist/exchanges/risex/nonce.d.ts +43 -0
  221. package/dist/exchanges/risex/nonce.js +212 -0
  222. package/dist/exchanges/risex/public.d.ts +64 -0
  223. package/dist/exchanges/risex/public.js +150 -0
  224. package/dist/exchanges/risex/session.d.ts +71 -0
  225. package/dist/exchanges/risex/session.js +171 -0
  226. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  227. package/dist/exchanges/risex/typed-data.js +240 -0
  228. package/dist/exchanges/risex/units.d.ts +29 -0
  229. package/dist/exchanges/risex/units.js +104 -0
  230. package/dist/exchanges/slippage.d.ts +1 -1
  231. package/dist/exchanges/slippage.js +1 -1
  232. package/dist/exchanges/spot-factory.d.ts +3 -3
  233. package/dist/exchanges/spot-factory.js +8 -8
  234. package/dist/exchanges/spot-interface.d.ts +5 -5
  235. package/dist/exchanges/spot-interface.js +2 -2
  236. package/dist/exchanges/spot-result.d.ts +3 -3
  237. package/dist/exchanges/spot-result.js +3 -3
  238. package/dist/exchanges/trading-v2.d.ts +3 -3
  239. package/dist/exchanges/trading-v2.js +9 -6
  240. package/dist/exchanges/txflow/adapter.d.ts +61 -0
  241. package/dist/exchanges/txflow/adapter.js +150 -0
  242. package/dist/exchanges/txflow/client.d.ts +47 -0
  243. package/dist/exchanges/txflow/client.js +155 -0
  244. package/dist/exchanges/txflow/public.d.ts +87 -0
  245. package/dist/exchanges/txflow/public.js +355 -0
  246. package/dist/exchanges/unknown-symbol.d.ts +34 -0
  247. package/dist/exchanges/unknown-symbol.js +56 -0
  248. package/dist/exchanges/variational/adapter.d.ts +67 -0
  249. package/dist/exchanges/variational/adapter.js +137 -0
  250. package/dist/exchanges/variational/client.d.ts +21 -0
  251. package/dist/exchanges/variational/client.js +66 -0
  252. package/dist/exchanges/variational/public.d.ts +126 -0
  253. package/dist/exchanges/variational/public.js +430 -0
  254. package/dist/exchanges/venue-types.d.ts +52 -10
  255. package/dist/exchanges/venue-types.js +82 -11
  256. package/dist/exchanges/venue.d.ts +150 -11
  257. package/dist/exchanges/venue.js +126 -4
  258. package/dist/exchanges/venues/arcus.d.ts +1 -2
  259. package/dist/exchanges/venues/arcus.js +5 -4
  260. package/dist/exchanges/venues/aster.d.ts +2 -2
  261. package/dist/exchanges/venues/aster.js +6 -4
  262. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  263. package/dist/exchanges/venues/create-helpers.js +3 -3
  264. package/dist/exchanges/venues/extended.d.ts +21 -0
  265. package/dist/exchanges/venues/extended.js +96 -0
  266. package/dist/exchanges/venues/grvt.d.ts +30 -0
  267. package/dist/exchanges/venues/grvt.js +98 -0
  268. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  269. package/dist/exchanges/venues/hyperliquid.js +19 -8
  270. package/dist/exchanges/venues/index.d.ts +7 -2
  271. package/dist/exchanges/venues/index.js +8 -2
  272. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  273. package/dist/exchanges/venues/lighter-family.js +9 -11
  274. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  275. package/dist/exchanges/venues/lighter-rh.js +20 -8
  276. package/dist/exchanges/venues/lighter.d.ts +1 -1
  277. package/dist/exchanges/venues/lighter.js +18 -6
  278. package/dist/exchanges/venues/ondo.d.ts +1 -1
  279. package/dist/exchanges/venues/ondo.js +5 -3
  280. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  281. package/dist/exchanges/venues/pacifica.js +15 -4
  282. package/dist/exchanges/venues/qfex.d.ts +1 -1
  283. package/dist/exchanges/venues/qfex.js +4 -2
  284. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  285. package/dist/exchanges/venues/risex-release.js +14 -0
  286. package/dist/exchanges/venues/risex.d.ts +31 -0
  287. package/dist/exchanges/venues/risex.js +131 -0
  288. package/dist/exchanges/venues/txflow.d.ts +18 -0
  289. package/dist/exchanges/venues/txflow.js +77 -0
  290. package/dist/exchanges/venues/variational.d.ts +21 -0
  291. package/dist/exchanges/venues/variational.js +85 -0
  292. package/dist/execution-log.d.ts +1 -1
  293. package/dist/funding/legacy-public.d.ts +6 -4
  294. package/dist/funding/legacy-public.js +42 -41
  295. package/dist/funding/normalize.d.ts +1 -1
  296. package/dist/funding/normalize.js +1 -1
  297. package/dist/funding/rates.d.ts +95 -6
  298. package/dist/funding/rates.js +99 -19
  299. package/dist/index.js +7 -6
  300. package/dist/jobs.d.ts +1 -0
  301. package/dist/jobs.js +25 -1
  302. package/dist/landing.d.ts +2 -0
  303. package/dist/landing.js +1 -1
  304. package/dist/liquidity.d.ts +3 -1
  305. package/dist/liquidity.js +8 -4
  306. package/dist/mcp-pacifica-earn.d.ts +9 -0
  307. package/dist/mcp-pacifica-earn.js +64 -0
  308. package/dist/mcp-server.js +211 -68
  309. package/dist/pacifica/client.d.ts +69 -5
  310. package/dist/pacifica/client.js +181 -8
  311. package/dist/pacifica/rate-limit.d.ts +144 -0
  312. package/dist/pacifica/rate-limit.js +267 -0
  313. package/dist/pacifica/types/order.d.ts +2 -1
  314. package/dist/plan-executor.js +7 -2
  315. package/dist/positions-by-address.d.ts +34 -0
  316. package/dist/positions-by-address.js +46 -0
  317. package/dist/rebalance.js +1 -1
  318. package/dist/risk.d.ts +2 -1
  319. package/dist/risk.js +2 -4
  320. package/dist/settings.d.ts +32 -3
  321. package/dist/signer/ows-evm.d.ts +1 -1
  322. package/dist/signer/ows-evm.js +1 -1
  323. package/dist/smart-order.d.ts +1 -1
  324. package/dist/smart-order.js +1 -1
  325. package/dist/strategies/funding-arb.d.ts +1 -1
  326. package/dist/strategies/funding-arb.js +36 -63
  327. package/dist/trade-validator.js +2 -2
  328. package/dist/utils.d.ts +22 -3
  329. package/dist/utils.js +11 -3
  330. package/dist/venue-health.d.ts +40 -0
  331. package/dist/venue-health.js +75 -0
  332. package/dist/withdraw-gate.d.ts +1 -1
  333. package/dist/withdraw-gate.js +1 -1
  334. package/dist/ws/arcus-market.d.ts +9 -0
  335. package/dist/ws/arcus-market.js +9 -0
  336. package/dist/ws/aster-market.d.ts +33 -0
  337. package/dist/ws/aster-market.js +30 -0
  338. package/dist/ws/index.d.ts +980 -0
  339. package/dist/ws/index.js +4460 -0
  340. package/dist/ws/lighter-market.d.ts +14 -0
  341. package/dist/ws/lighter-market.js +13 -0
  342. package/dist/ws/node.d.ts +996 -0
  343. package/dist/ws/node.js +4554 -0
  344. package/dist/ws/pacifica-instrument.d.ts +8 -0
  345. package/dist/ws/pacifica-instrument.js +8 -0
  346. package/dist/ws/unclassified.d.ts +16 -0
  347. package/dist/ws/unclassified.js +43 -0
  348. package/dist/ws-cli/credentials.d.ts +62 -0
  349. package/dist/ws-cli/credentials.js +126 -0
  350. package/package.json +71 -10
  351. package/skills/perp-cli/SKILL.md +153 -36
  352. package/skills/perp-cli/package.json +15 -2
  353. package/skills/perp-cli/references/agent-operations.md +22 -12
  354. package/skills/perp-cli/references/commands.md +143 -17
  355. package/skills/perp-cli/references/strategies.md +38 -3
  356. package/skills/perp-cli/references/ws-library.md +523 -0
  357. package/skills/perp-cli/scripts/arb-monitor.sh +13 -10
  358. package/skills/perp-cli/scripts/funding-analysis.sh +57 -29
  359. package/skills/perp-cli/scripts/preflight.sh +27 -19
  360. package/skills/perp-cli/scripts/spot-perp-scan.sh +58 -38
  361. package/skills/perp-cli/scripts/validate-arb.sh +56 -39
  362. package/dist/cli-spec.d.ts +0 -46
  363. package/dist/cli-spec.js +0 -81
@@ -23,8 +23,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
23
23
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
24
24
  import { z } from "zod";
25
25
  import { hasCredentials, listConfiguredAccountVenues, resolveCredentials, NO_CLI_CREDENTIAL_OPTS } from "./config.js";
26
- import { getVenue, listVenues, listVenuesWithRole, networkConfig, READ_ONLY_CREDENTIALS, requireCapabilities } from "./exchanges/venue.js";
26
+ import { getVenue, listPublicBalanceVenues, listPublicPositionVenues, listVenues, listVenuesWithRole, networkConfig, READ_ONLY_CREDENTIALS, requireCapabilities, roleUnsupported } from "./exchanges/venue.js";
27
27
  import "./exchanges/venues/index.js";
28
+ import { venueMarketRow } from "./exchanges/venue-types.js";
29
+ import { lighterFundingStatsWarning } from "./exchanges/lighter-public.js";
28
30
  import { newVenueClientOrderId, placeAndConfirm, validateLegacyTif, validateOrderRequest } from "./exchanges/trading-v2.js";
29
31
  import { closePerpVerified, isOutcomeUnknown } from "./bot/perp-order.js";
30
32
  import { quarantineSymbol } from "./bot/quarantine.js";
@@ -32,8 +34,11 @@ import { fillSizeString, sizeDecimalsOf } from "./exchanges/spot-result.js";
32
34
  import { tradingV2Of } from "./exchanges/legacy-trading-v2.js";
33
35
  import { PerpError, classifyError, extractErrorMessage, isDefiniteOrderFailure, orderOutcomeUnknown, parsePositiveDecimal } from "./errors.js";
34
36
  import { invalidateCache } from "./cache.js";
35
- import { pingPacifica, pingHyperliquid, pingLighter, } from "./shared-api.js";
36
- import { fetchAllFundingRates } from "./funding-rates.js";
37
+ import { probeVenueHealth } from "./venue-health.js";
38
+ import { getOptionsVenue, listOptionsVenueIds } from "./exchanges/options-venues.js";
39
+ import { fetchAllFundingRates, isRankedComparison } from "./funding-rates.js";
40
+ import { readPositionsByAddress } from "./positions-by-address.js";
41
+ import { readBalanceByAddress, readBalanceByAddressAll } from "./balance-by-address.js";
37
42
  import { loadArbState } from "./arb/state.js";
38
43
  import { validateTrade } from "./trade-validator.js";
39
44
  import { orderResultJson, symbolMatch } from "./utils.js";
@@ -41,6 +46,7 @@ import { logMcpExecution, mcpPreTradeGate, mcpReduceOnlyGate, resolveMcpNetwork
41
46
  import { setSharedApiNetwork } from "./api/public/urls.js";
42
47
  import { logClientId } from "./client-id-tracker.js";
43
48
  import { loadSettings } from "./settings.js";
49
+ import { registerPacificaEarnMcpTools } from "./mcp-pacifica-earn.js";
44
50
  /**
45
51
  * The one network this server serves: PERP_NETWORK, validated like `-n` (an invalid value
46
52
  * makes the server refuse to start). Unset = mainnet, the documented default. Reported in
@@ -52,8 +58,8 @@ setQuarantineNetwork(MCP_NETWORK);
52
58
  setSharedApiNetwork(MCP_NETWORK);
53
59
  // ── Adapter cache & factory ──
54
60
  //
55
- // Same creation path and signer tiers as the CLI (agent → OWS master → PK;
56
- // design §9.1 #1, §10.2). The MCP server has no CLI flags, so credentials come
61
+ // Same creation path and signer tiers as the CLI (agent → OWS master → PK).
62
+ // The MCP server has no CLI flags, so credentials come
57
63
  // from the registered agent, settings.owsActiveWallet, and (legacy venues) env.
58
64
  const adapters = new Map();
59
65
  /** The network each adapter was created on (TradingV2 wrappers must use the same one). */
@@ -71,6 +77,9 @@ async function getOrCreateAdapter(exchange) {
71
77
  const cached = adapters.get(def.id);
72
78
  if (cached)
73
79
  return cached;
80
+ // Market-data-only venue (TxFlow: unofficial read-only API): every account / trading tool refuses here.
81
+ if (!def.roles.includes("account"))
82
+ throw roleUnsupported(def, "account");
74
83
  const create = requireCreate(def);
75
84
  if (!(await hasCredentials(def.id, NO_CLI_CREDENTIAL_OPTS))) {
76
85
  throw new PerpError("NO_SIGNER_AVAILABLE", `No credentials configured for ${def.id}`, {
@@ -118,9 +127,58 @@ async function tryGetAdapter(exchange) {
118
127
  throw e;
119
128
  }
120
129
  }
121
- /** Fetch markets using the venue's public data path (no key needed). */
130
+ /**
131
+ * Fetch markets using the venue's public data path (no key needed). Unofficial-API rows carry
132
+ * venueApi; rows of a venue whose funding is unverified (QFEX, GRVT, TxFlow) carry fundingVerified false.
133
+ */
122
134
  async function fetchMarketsPublic(exchange) {
123
- return getVenue(exchange).publicData.fetchMarkets(MCP_NETWORK);
135
+ return labelMarketRows(exchange, await getVenue(exchange).publicData.fetchMarkets(MCP_NETWORK));
136
+ }
137
+ /**
138
+ * meta.warnings when a venue's rows carry fundingRate null for a known reason (Lighter: the
139
+ * settled-rate snapshot could not be read) — the reason reaches the agent, not only stderr.
140
+ */
141
+ function fundingWarningMeta(exchange, rows) {
142
+ const id = getVenue(exchange).id;
143
+ if (id !== "lighter" && id !== "lighter-rh")
144
+ return {};
145
+ // Tied to the rows served: only when they actually carry a null rate (cached rows that have
146
+ // rates never get a stale warning; null rows always say why when the reason is known).
147
+ if (!rows.some(r => r.fundingRate === null))
148
+ return {};
149
+ const w = lighterFundingStatsWarning(id, MCP_NETWORK);
150
+ return w === null ? {} : { warnings: [w] };
151
+ }
152
+ /**
153
+ * Keyless get_markets rows: the public directory's VenueMarket rows in the CLI market-row shape
154
+ * (venueMarketRow — the mapper the VenueMarket CLI adapters use), the VenueMarket fields kept
155
+ * (venueSymbol, canonical, assetClass, fundingRateBasis, tickSize, …) and the MCP labels added.
156
+ */
157
+ async function fetchMarketRowsPublic(exchange) {
158
+ const markets = await getVenue(exchange).publicData.fetchMarkets(MCP_NETWORK);
159
+ return labelMarketRows(exchange, markets.map(m => ({ ...m, ...venueMarketRow(m) })));
160
+ }
161
+ /**
162
+ * The one row label for market rows of any source (public data path or a credentialed adapter):
163
+ * every row carries fundingVerified (true or false — as `perp market list`), unofficial-API rows venueApi.
164
+ */
165
+ function labelMarketRows(exchange, rows) {
166
+ const def = getVenue(exchange);
167
+ const labelled = rows.map(r => ({ ...r, fundingVerified: def.fundingVerified }));
168
+ return def.apiStatus.kind === "unofficial" ? labelled.map(r => ({ ...r, venueApi: "unofficial" })) : labelled;
169
+ }
170
+ /**
171
+ * meta fields every market-data tool adds for its venue: apiStatus, the note for an unofficial API,
172
+ * and fundingVerified false (with a note) when the venue's funding rate is unverified.
173
+ */
174
+ function apiMeta(exchange) {
175
+ const def = getVenue(exchange);
176
+ if (def.apiStatus.kind === "unofficial") {
177
+ return { apiStatus: "unofficial", apiNote: `${def.displayName}: UNOFFICIAL API — ${def.apiStatus.note}`, fundingVerified: def.fundingVerified };
178
+ }
179
+ return def.fundingVerified
180
+ ? { apiStatus: "official" }
181
+ : { apiStatus: "official", fundingVerified: false, fundingNote: `${def.displayName}: funding rate UNVERIFIED (not confirmed against settled payments) — display only, never a basis for trading` };
124
182
  }
125
183
  /** One book level from a venue payload; a level without a string price / size is a payload error (never "0"). */
126
184
  function bookLevel(venue, symbol, price, size) {
@@ -219,6 +277,8 @@ export const MCP_WITHDRAW_VENUES = ["pacifica", "hyperliquid", "lighter", "aster
219
277
  const _require = createRequire(import.meta.url);
220
278
  const _pkg = _require("../package.json");
221
279
  const server = new McpServer({ name: "perp-cli", version: _pkg.version }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
280
+ // Pacifica Print + vaults: read-only tools (src/mcp-pacifica-earn.ts).
281
+ registerPacificaEarnMcpTools(server, MCP_NETWORK, ok, errOf);
222
282
  // ============================================================
223
283
  // Market Data tools (read-only, no private key needed)
224
284
  // ============================================================
@@ -227,11 +287,14 @@ server.tool("get_markets", `Get all available perpetual futures markets on an ex
227
287
  // Try adapter first (richer data), fall back to public API (no key needed)
228
288
  const adapter = await tryGetAdapter(exchange);
229
289
  if (adapter) {
230
- const markets = await adapter.getMarkets();
231
- return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length }) }] };
232
- }
233
- const markets = await fetchMarketsPublic(exchange);
234
- return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length, source: "public" }) }] };
290
+ const markets = labelMarketRows(exchange, await adapter.getMarkets());
291
+ return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length, ...apiMeta(exchange), ...fundingWarningMeta(exchange, markets) }) }] };
292
+ }
293
+ // No credentials: the venue's public directory — price-only on GRVT (no 184-request
294
+ // margin_rules fan-out per call) — in the CLI market-row shape (venueMarketRow), with the
295
+ // same market set as `perp market list` (the venue's shared eligibility / listing rule).
296
+ const markets = await fetchMarketRowsPublic(exchange);
297
+ return { content: [{ type: "text", text: ok(markets, { exchange, count: markets.length, source: "public", ...apiMeta(exchange), ...fundingWarningMeta(exchange, markets) }) }] };
235
298
  }
236
299
  catch (e) {
237
300
  return { content: [{ type: "text", text: errOf(e, { exchange }) }], isError: true };
@@ -246,24 +309,25 @@ server.tool("get_orderbook", `Get the order book (bids and asks) for a symbol on
246
309
  const adapter = await tryGetAdapter(exchange);
247
310
  if (adapter) {
248
311
  const book = await adapter.getOrderbook(symbol);
249
- return { content: [{ type: "text", text: ok(book, { exchange, symbol }) }] };
312
+ return { content: [{ type: "text", text: ok(book, { exchange, symbol, ...apiMeta(exchange) }) }] };
250
313
  }
251
314
  const book = await fetchOrderbookPublic(exchange, symbol);
252
- return { content: [{ type: "text", text: ok(book, { exchange, symbol, source: "public" }) }] };
315
+ return { content: [{ type: "text", text: ok(book, { exchange, symbol, source: "public", ...apiMeta(exchange) }) }] };
253
316
  }
254
317
  catch (e) {
255
318
  return { content: [{ type: "text", text: errOf(e, { exchange, symbol }) }], isError: true };
256
319
  }
257
320
  });
258
- server.tool("get_funding_rates", "Compare funding rates across every market-data venue. Returns rates per symbol with spread analysis", {
321
+ server.tool("get_funding_rates", "Compare funding rates across every market-data venue whose funding rate is verified. Returns rates per symbol with spread analysis. includeUnverified also returns venues whose rate is unverified (QFEX; TxFlow — unofficial API; Variational — a predicted rate not checked against settled payments; GRVT — indicative rate; Extended — not checked against settled payments), each row tagged fundingVerified / apiStatus — display only, never a basis for trading (mirrors the CLI `arb scan --include-unverified`).", {
259
322
  symbols: z
260
323
  .array(z.string())
261
324
  .optional()
262
325
  .describe("Filter to specific symbols (e.g. ['BTC','ETH']). Omit for all available"),
263
326
  minSpread: z.number().optional().describe("Minimum annualized spread % to include (default: 0)"),
264
- }, async ({ symbols, minSpread }) => {
327
+ includeUnverified: z.boolean().optional().describe("Also include venues whose funding rate is unverified (default false); rows carry fundingVerified:false / apiStatus"),
328
+ }, async ({ symbols, minSpread, includeUnverified }) => {
265
329
  try {
266
- const snapshot = await fetchAllFundingRates({ symbols, minSpread });
330
+ const snapshot = await fetchAllFundingRates({ symbols, minSpread, includeUnverified: includeUnverified === true });
267
331
  return {
268
332
  content: [{
269
333
  type: "text",
@@ -275,7 +339,7 @@ server.tool("get_funding_rates", "Compare funding rates across every market-data
275
339
  return { content: [{ type: "text", text: errOf(e) }], isError: true };
276
340
  }
277
341
  });
278
- server.tool("get_prices", "Get cross-exchange prices for symbols. Fetches mark prices from every market-data venue for comparison", {
342
+ server.tool("get_prices", "Get cross-exchange prices for symbols. Fetches mark prices from every market-data venue for comparison; a venue appears in a row only when it lists that symbol (e.g. QFEX lists no BTC). Each row's unofficialApiVenues names the venues in it read through an undocumented API (TxFlow) — like get_markets' venueApi tag.", {
279
343
  symbols: z
280
344
  .array(z.string())
281
345
  .optional()
@@ -295,6 +359,10 @@ server.tool("get_prices", "Get cross-exchange prices for symbols. Fetches mark p
295
359
  return;
296
360
  }
297
361
  for (const m of r.value) {
362
+ // An ambiguous ticker (SPX6900 vs the S&P 500) names different instruments per venue:
363
+ // never grouped by symbol across venues.
364
+ if (m.assetClass === "unclassified")
365
+ continue;
298
366
  const sym = m.symbol.toUpperCase();
299
367
  if (filter && !filter.has(sym))
300
368
  continue;
@@ -303,8 +371,13 @@ server.tool("get_prices", "Get cross-exchange prices for symbols. Fetches mark p
303
371
  priceMap.get(sym)[ex] = Number(m.markPrice);
304
372
  }
305
373
  });
306
- const data = Array.from(priceMap.entries()).map(([symbol, prices]) => ({ symbol, prices }));
307
- return { content: [{ type: "text", text: ok(data, { count: data.length, errors }) }] };
374
+ const unofficial = listVenuesWithRole("marketData").filter(v => v.apiStatus.kind === "unofficial").map(v => v.id);
375
+ const data = Array.from(priceMap.entries()).map(([symbol, prices]) => ({
376
+ symbol,
377
+ prices,
378
+ unofficialApiVenues: Object.keys(prices).filter(ex => unofficial.includes(ex)),
379
+ }));
380
+ return { content: [{ type: "text", text: ok(data, { count: data.length, errors, unofficialApiVenues: unofficial }) }] };
308
381
  }
309
382
  catch (e) {
310
383
  return { content: [{ type: "text", text: errOf(e) }], isError: true };
@@ -362,6 +435,44 @@ server.tool("get_outcome_book", "Get the orderbook for one outcome side. Public
362
435
  }
363
436
  });
364
437
  // ============================================================
438
+ // Options market data (read-only, keyless; venues: src/exchanges/options-venues.ts)
439
+ // ============================================================
440
+ const OPTIONS_VENUE_ARG = z.string().describe(`Options venue: ${listOptionsVenueIds().join(", ")}`);
441
+ server.tool("get_option_expiries", "List the active option expiries of an underlying on an options venue (Derive): per expiry the UTC date (YYYY-MM-DD), expiry ms, strike and instrument counts, soonest first. Public read — no API key needed.", { venue: OPTIONS_VENUE_ARG, underlying: z.string().describe("Underlying, e.g. ETH, BTC") }, async ({ venue, underlying }) => {
442
+ try {
443
+ const data = await (await getOptionsVenue(venue, MCP_NETWORK)).listExpiries(underlying);
444
+ return { content: [{ type: "text", text: ok(data, { venue, count: data.expiries.length }) }] };
445
+ }
446
+ catch (e) {
447
+ return { content: [{ type: "text", text: errOf(e, { venue, underlying }) }], isError: true };
448
+ }
449
+ });
450
+ server.tool("get_option_chain", "Option chain for one expiry (Derive): rows per strike with call and put quotes — bid / ask (price + size; null = empty side), mark, index, forward, IV (mark / bid / ask; null = not computable), greeks (delta, gamma, theta, vega, rho), open interest, 24h volume — plus the ATM strike. Decimals are strings. One venue request per expiry. Public read.", {
451
+ venue: OPTIONS_VENUE_ARG,
452
+ underlying: z.string().describe("Underlying, e.g. ETH, BTC"),
453
+ expiry: z.string().optional().describe("Expiry date YYYY-MM-DD (UTC); omit for the nearest active expiry (result expirySelection: \"nearest\")"),
454
+ strikes: z.number().int().min(1).optional().describe("Only the N strikes closest to the ATM reference (forward, else index); omit for all strikes"),
455
+ }, async ({ venue, underlying, expiry, strikes }) => {
456
+ try {
457
+ const chain = await (await getOptionsVenue(venue, MCP_NETWORK)).getChain({
458
+ underlying, expiry: expiry === undefined ? null : expiry, strikesAroundAtm: strikes === undefined ? null : strikes,
459
+ });
460
+ return { content: [{ type: "text", text: ok(chain, { venue, expiry: chain.expiry, rows: chain.rows.length }) }] };
461
+ }
462
+ catch (e) {
463
+ return { content: [{ type: "text", text: errOf(e, { venue, underlying }) }], isError: true };
464
+ }
465
+ });
466
+ server.tool("get_option_quote", "Quote one option instrument (Derive name, e.g. ETH-20261225-3500-P): bid / ask (price + size), mark, index, forward, IV, greeks, open interest, 24h stats. Unknown instrument → SYMBOL_NOT_FOUND. Public read.", { venue: OPTIONS_VENUE_ARG, instrument: z.string().describe("Venue instrument name, e.g. ETH-20261225-3500-P") }, async ({ venue, instrument }) => {
467
+ try {
468
+ const q = await (await getOptionsVenue(venue, MCP_NETWORK)).getQuote(instrument);
469
+ return { content: [{ type: "text", text: ok(q, { venue, instrument: q.instrument }) }] };
470
+ }
471
+ catch (e) {
472
+ return { content: [{ type: "text", text: errOf(e, { venue, instrument }) }], isError: true };
473
+ }
474
+ });
475
+ // ============================================================
365
476
  // Account tools (need private key)
366
477
  // ============================================================
367
478
  server.tool("get_outcome_positions", "Get the active account's open outcome positions (size, entry notional, mark price, unrealized PnL). Requires a configured Hyperliquid wallet or agent.", {}, async () => {
@@ -400,6 +511,49 @@ server.tool("get_positions", `Get all open positions on an exchange, including s
400
511
  return { content: [{ type: "text", text: errOf(e, { exchange }) }], isError: true };
401
512
  }
402
513
  });
514
+ server.tool("get_positions_by_address", `Open positions of ANY account by its public address — keyless, read-only (public venue data; nothing is signed, no key is needed). Venues: ${listPublicPositionVenues().map(v => `${v.id} (${v.publicData.addressKind === "solana" ? "base58 Solana address" : "0x address"})`).join(", ")}. Hyperliquid reads the native dex and every HIP-3 dex (rows carry dex: null | "<dex>"; a dex that fails is listed in meta.errors / meta.warnings — its rows are missing, not absent); Lighter reads every account of the address (rows carry accountIndex; accountIndex narrows). Other venues: CAPABILITY_UNSUPPORTED; a wrong address format: INVALID_PARAMS. Same rows as \`perp -e <venue> account positions --address <addr>\`.`, {
515
+ exchange: z.string().describe(`Exchange: ${listPublicPositionVenues().map(v => v.id).join(", ")} (aliases accepted)`),
516
+ address: z.string().describe("Public account address (0x… for hyperliquid / lighter / lighter-rh; base58 Solana for pacifica)"),
517
+ symbol: z.string().optional().describe("Only this market (plain asset, e.g. BTC)"),
518
+ accountIndex: z.number().int().nonnegative().optional().describe("Lighter only: one account (sub-account) of the address"),
519
+ }, async ({ exchange, address, symbol, accountIndex }) => {
520
+ try {
521
+ const r = await readPositionsByAddress(exchange, MCP_NETWORK, address, { symbol, accountIndex });
522
+ return { content: [{ type: "text", text: ok(r.positions, {
523
+ exchange: r.venue, address, source: "public", count: r.positions.length,
524
+ ...(r.warnings.length > 0 ? { warnings: r.warnings, errors: r.errors } : {}),
525
+ ...(r.notes.length > 0 ? { notes: r.notes } : {}),
526
+ }) }] };
527
+ }
528
+ catch (e) {
529
+ return { content: [{ type: "text", text: errOf(e, { exchange }) }], isError: true };
530
+ }
531
+ });
532
+ server.tool("get_balance_by_address", `Balances of ANY account by its public address — keyless, read-only (public venue data; nothing is signed, no key is needed). Venues: ${listPublicBalanceVenues().map(v => `${v.id} (${v.publicData.addressKind === "solana" ? "base58 Solana address" : "0x address"})`).join(", ")}; omit exchange to read every one of them (a venue that fails, address format included, is listed in data.errors). data: { balance (equity / available / marginUsed / unrealizedPnl + venue extras; null on Lighter), spot (token balances; null where the venue does not publish them), accounts (Lighter: one row per account of the address; accountIndex narrows), dexes + accountMode (Hyperliquid: HIP-3 dexes with a non-zero account value; a dex that fails is in meta.errors / meta.warnings — unknown, not zero) }. Other venues: CAPABILITY_UNSUPPORTED; a wrong address format: INVALID_PARAMS. Same data as \`perp [-e <venue>] account balance --address <addr>\`.`, {
533
+ exchange: z.string().optional().describe(`Exchange: ${listPublicBalanceVenues().map(v => v.id).join(", ")} (aliases accepted); omit for every one`),
534
+ address: z.string().describe("Public account address (0x… for hyperliquid / lighter / lighter-rh / risex; base58 Solana for pacifica)"),
535
+ accountIndex: z.number().int().nonnegative().optional().describe("Lighter only: one account (sub-account) of the address"),
536
+ }, async ({ exchange, address, accountIndex }) => {
537
+ try {
538
+ if (exchange === undefined) {
539
+ const all = await readBalanceByAddressAll(MCP_NETWORK, address, { accountIndex });
540
+ return { content: [{ type: "text", text: ok({
541
+ venues: all.venues,
542
+ ...(Object.keys(all.errors).length > 0 ? { errors: all.errors } : {}),
543
+ ...(Object.keys(all.notes).length > 0 ? { notes: all.notes } : {}),
544
+ }, { address, source: "public", ...(all.warnings.length > 0 ? { warnings: all.warnings } : {}) }) }] };
545
+ }
546
+ const r = await readBalanceByAddress(exchange, MCP_NETWORK, address, { accountIndex });
547
+ return { content: [{ type: "text", text: ok({ exchange: r.venue, address, ...r.data }, {
548
+ exchange: r.venue, address, source: "public",
549
+ ...(r.warnings.length > 0 ? { warnings: r.warnings, errors: r.errors } : {}),
550
+ ...(r.notes.length > 0 ? { notes: r.notes } : {}),
551
+ }) }] };
552
+ }
553
+ catch (e) {
554
+ return { content: [{ type: "text", text: errOf(e, { exchange }) }], isError: true };
555
+ }
556
+ });
403
557
  server.tool("get_open_orders", `Get all open/pending orders on an exchange. ${HL_SCOPE_NOTE}`, { exchange: z.string().describe(`Exchange name: ${listVenues().map(v => v.id).join(", ")} (aliases accepted)`) }, async ({ exchange }) => {
404
558
  try {
405
559
  const adapter = await getOrCreateAdapter(exchange);
@@ -433,6 +587,9 @@ server.tool("portfolio", "Cross-exchange portfolio summary: balances, positions,
433
587
  };
434
588
  }
435
589
  }));
590
+ // Totals are null (unknown) when no account balance was read — no configured credentials, or
591
+ // every venue failed — never 0 (QA 0.15.0: totalEquity 0 without credentials).
592
+ const read = snapshots.filter(s => s.balance !== null);
436
593
  let totalEquity = 0;
437
594
  let totalAvailable = 0;
438
595
  let totalMarginUsed = 0;
@@ -468,16 +625,21 @@ server.tool("portfolio", "Cross-exchange portfolio summary: balances, positions,
468
625
  pct: totalEquity > 0 ? (Number(s.balance.equity) / totalEquity) * 100 : 0,
469
626
  }))
470
627
  .sort((a, b) => b.pct - a.pct);
628
+ const known = read.length > 0;
471
629
  const summary = {
472
- totalEquity,
473
- totalAvailable,
474
- totalMarginUsed,
475
- totalUnrealizedPnl,
630
+ /** "noAccount" = no venue has credentials; "unread" = credentials exist but no balance was read. */
631
+ accountState: EXCHANGES.length === 0 ? "noAccount" : known ? "ok" : "unread",
632
+ /** Venues whose balances the totals cover. */
633
+ totalsCover: read.map(s => s.exchange),
634
+ totalEquity: known ? totalEquity : null,
635
+ totalAvailable: known ? totalAvailable : null,
636
+ totalMarginUsed: known ? totalMarginUsed : null,
637
+ totalUnrealizedPnl: known ? totalUnrealizedPnl : null,
476
638
  totalPositions,
477
639
  totalOpenOrders,
478
640
  exchanges: snapshots,
479
641
  positions: allPositions,
480
- riskMetrics: { marginUtilization, largestPosition, exchangeConcentration },
642
+ riskMetrics: { marginUtilization: known ? marginUtilization : null, largestPosition, exchangeConcentration },
481
643
  };
482
644
  return { content: [{ type: "text", text: ok(summary) }] };
483
645
  });
@@ -1121,10 +1283,14 @@ server.tool("arb_scan", "Scan for funding rate arbitrage opportunities across ex
1121
1283
  symbols,
1122
1284
  minSpread: minSpread ?? 5,
1123
1285
  });
1124
- const opportunities = snapshot.symbols.map(s => ({
1286
+ const opportunities = snapshot.symbols.filter(isRankedComparison).map(s => ({
1125
1287
  symbol: s.symbol,
1126
1288
  maxSpreadAnnual: `${s.maxSpreadAnnual.toFixed(2)}%`,
1127
1289
  strategy: `Long ${s.longExchange} / Short ${s.shortExchange}`,
1290
+ // false = a leg is on a read-only venue (RISEx in this release): data only, not tradable here.
1291
+ executable: s.executable,
1292
+ readOnlyLegs: s.readOnlyLegs,
1293
+ unverifiedLegs: s.unverifiedLegs,
1128
1294
  estHourlyIncomePerK: `$${s.estHourlyIncomeUsd.toFixed(4)}`,
1129
1295
  rates: s.rates.map(r => ({
1130
1296
  exchange: r.exchange,
@@ -1149,44 +1315,17 @@ server.tool("arb_scan", "Scan for funding rate arbitrage opportunities across ex
1149
1315
  });
1150
1316
  server.tool("health_check", "Ping every registered venue on the server's network and return connectivity status and latency. Pacifica / Hyperliquid / Lighter / Aster / QFEX: REST ping (status = HTTP status, 0 = no response); every other venue: its public market-data read (status null). A failure carries its error message.", {}, async () => {
1151
1317
  try {
1152
- const timed = async (probe, fn) => {
1153
- const start = Date.now();
1154
- try {
1155
- const r = await fn();
1156
- return { ok: r.ok, latencyMs: Date.now() - start, status: r.status, error: r.ok ? null : `HTTP ${r.status}`, probe };
1157
- }
1158
- catch (e) {
1159
- return { ok: false, latencyMs: Date.now() - start, status: probe === "ping" ? 0 : null, error: extractErrorMessage(e), probe };
1160
- }
1161
- };
1162
- const restPing = (url) => timed("ping", async () => {
1163
- const res = await fetch(url);
1164
- return { ok: res.ok, status: res.status };
1165
- });
1166
- // Shared-api helpers report a transport failure as status 0 (no message).
1167
- const helperPing = (fn) => async () => {
1168
- const r = await fn();
1169
- return { ...r, error: r.ok ? null : r.status === 0 ? "no response" : `HTTP ${r.status}`, probe: "ping" };
1170
- };
1171
- const pings = {
1172
- pacifica: helperPing(pingPacifica),
1173
- hyperliquid: helperPing(pingHyperliquid),
1174
- lighter: helperPing(pingLighter),
1175
- aster: () => restPing(`${networkConfig(getVenue("aster"), MCP_NETWORK).restUrl}/fapi/v1/time`),
1176
- // QFEX: public GET /refdata for one instrument on the server's network.
1177
- qfex: () => restPing(`${networkConfig(getVenue("qfex"), MCP_NETWORK).restUrl}/refdata?ticker=AAPL-USD`),
1178
- };
1179
- const venues = listVenues();
1180
- const probes = await Promise.all(venues.map(v => {
1181
- const ping = pings[v.id];
1182
- return ping !== undefined
1183
- ? ping()
1184
- : timed("markets", async () => { await v.publicData.fetchMarkets(MCP_NETWORK); return { ok: true, status: null }; });
1185
- }));
1318
+ const health = await probeVenueHealth(MCP_NETWORK);
1186
1319
  const result = {};
1187
- venues.forEach((v, i) => { result[v.id] = { ...probes[i], statusText: probes[i].ok ? "healthy" : "unreachable" }; });
1188
- result.covered = venues.map(v => v.id);
1189
- result.allHealthy = probes.every(p => p.ok);
1320
+ for (const { venue, ...p } of health.probes)
1321
+ result[venue] = { ...p, statusText: p.ok ? "healthy" : "unreachable" };
1322
+ result.covered = health.probes.map(p => p.venue);
1323
+ // A venue without an environment on the server's network (TxFlow has no testnet) is not probed.
1324
+ result.skipped = health.skipped;
1325
+ // Same two verdicts as `perp health --json`: healthy = every OFFICIAL-API venue is up (an
1326
+ // unofficial API being down does not make perp-cli unhealthy); allHealthy includes them.
1327
+ result.healthy = health.probes.filter(p => p.apiStatus === "official").every(p => p.ok);
1328
+ result.allHealthy = health.allHealthy;
1190
1329
  return { content: [{ type: "text", text: ok(result) }] };
1191
1330
  }
1192
1331
  catch (e) {
@@ -1446,7 +1585,7 @@ function mcpOrderRequest(adapter, p) {
1446
1585
  };
1447
1586
  }
1448
1587
  server.tool("trade_preview", `Preview a trade WITHOUT executing. Runs trade_execute's pre-send checks read-only (size parse, order validation incl. TIF, quarantine, local risk limits — never forced) and reports any refusal as valid:false with its code + remediation; also returns the estimated fill price from the book, notional, margin impact, the builder fee the order would carry, and the existing position. Venue trading fees are NOT estimated. ALWAYS call this before trade_execute and show the result to the user for confirmation. ${HL_SCOPE_NOTE}`, {
1449
- exchange: z.string().describe(`Exchange: ${listVenues().filter(v => v.create !== null).map(v => v.id).join(", ")} (aliases accepted)`),
1588
+ exchange: z.string().describe(`Exchange: ${listVenues().filter(v => v.roles.includes("trading")).map(v => v.id).join(", ")} (aliases accepted)`),
1450
1589
  symbol: z.string().describe("Trading symbol (e.g., BTC, ETH, SOL)"),
1451
1590
  side: z.enum(["buy", "sell"]).describe("Order side"),
1452
1591
  size: z.string().describe("Order size (base currency units, e.g., '0.1' for 0.1 BTC)"),
@@ -1456,6 +1595,8 @@ server.tool("trade_preview", `Preview a trade WITHOUT executing. Runs trade_exec
1456
1595
  reduceOnly: z.boolean().default(false).describe("Reduce-only order (close position only) — checked like trade_execute (quarantine applies only to non-reduce-only orders)"),
1457
1596
  }, async ({ exchange, symbol, side, size, orderType, price, tif, reduceOnly }) => {
1458
1597
  try {
1598
+ // No trading role (TxFlow, release-gated RISEx) → CAPABILITY_UNSUPPORTED before any credential.
1599
+ requireCapabilities(getVenue(exchange));
1459
1600
  const adapter = await getOrCreateAdapter(exchange);
1460
1601
  const sym = symbol.toUpperCase();
1461
1602
  const isLimit = orderType === "limit";
@@ -1570,9 +1711,9 @@ server.tool("trade_preview", `Preview a trade WITHOUT executing. Runs trade_exec
1570
1711
  });
1571
1712
  /**
1572
1713
  * Place one order through TradingV2 (native on ondo/arcus, legacy wrapper on the four
1573
- * pre-existing venues) and return OrderResult + execution (design §13.7). Pre-send
1714
+ * pre-existing venues) and return OrderResult + execution. Pre-send
1574
1715
  * validation failures stay definite; anything thrown after the request may have reached
1575
- * the venue becomes ORDER_PENDING ("do NOT resubmit", design §12.3).
1716
+ * the venue becomes ORDER_PENDING ("do NOT resubmit").
1576
1717
  */
1577
1718
  async function placeViaV2(adapter, req, what, remediation) {
1578
1719
  const def = getVenue(adapter.name);
@@ -1589,7 +1730,7 @@ async function placeViaV2(adapter, req, what, remediation) {
1589
1730
  }
1590
1731
  }
1591
1732
  server.tool("trade_execute", `Execute a REAL trade on the server's network (meta.network; PERP_NETWORK, default mainnet). IMPORTANT: Always call trade_preview first and get explicit user confirmation before calling this tool. Refused with RISK_VIOLATION when the order breaches the local risk limits (perp risk set; there is no override over MCP) or when the symbol is quarantined (only reduce-only orders pass). Every order is written to the execution log. Supports market and limit orders. Returns the uniform OrderResult (status, finality, filledSize, execution: slippage / protection price / TIF sent) with the raw venue response under \`legacy\`, plus top-level filledSize / requestedSize / partial. Outcomes: an unknown outcome (transport failure after send, or a non-final venue ack) → ok:false, code ORDER_PENDING, executed:false, pending:true and a "do NOT resubmit" remediation — the symbol is quarantined for opening orders until a human clears it; a final rejection / cancel with nothing filled → ORDER_REJECTED, executed:false; a cancel after a partial fill → executed:true, partial:true with the real filledSize. ${HL_SCOPE_NOTE}`, {
1592
- exchange: z.string().describe(`Exchange: ${listVenues().filter(v => v.create !== null).map(v => v.id).join(", ")} (aliases accepted)`),
1733
+ exchange: z.string().describe(`Exchange: ${listVenues().filter(v => v.roles.includes("trading")).map(v => v.id).join(", ")} (aliases accepted)`),
1593
1734
  symbol: z.string().describe("Trading symbol (e.g., BTC, ETH, SOL)"),
1594
1735
  side: z.enum(["buy", "sell"]).describe("Order side"),
1595
1736
  size: z.string().describe("Order size (base currency units)"),
@@ -1602,6 +1743,7 @@ server.tool("trade_execute", `Execute a REAL trade on the server's network (meta
1602
1743
  try {
1603
1744
  // One parsed size for the risk gate and the order ("1,000" / "abc" refused before any read or send).
1604
1745
  const size = parsePositiveDecimal(rawSize, "size");
1746
+ requireCapabilities(getVenue(exchange)); // trading role first, before any credential
1605
1747
  const adapter = await getOrCreateAdapter(exchange);
1606
1748
  const def = getVenue(adapter.name);
1607
1749
  const caps = requireCapabilities(def);
@@ -1722,10 +1864,11 @@ server.tool("trade_execute", `Execute a REAL trade on the server's network (meta
1722
1864
  }
1723
1865
  });
1724
1866
  server.tool("trade_close", `Close an existing position. IMPORTANT: Call trade_preview first with the opposite side to show impact, then get user confirmation. Verified close: \`closedSize\` is the measured / venue-final fill (one residual market close is attempted); a residual still open → ORDER_PARTIAL error (symbol quarantined). Includes the uniform OrderResult of the reduce-only market order. ${HL_SCOPE_NOTE}`, {
1725
- exchange: z.string().describe(`Exchange: ${listVenues().filter(v => v.create !== null).map(v => v.id).join(", ")} (aliases accepted)`),
1867
+ exchange: z.string().describe(`Exchange: ${listVenues().filter(v => v.roles.includes("trading")).map(v => v.id).join(", ")} (aliases accepted)`),
1726
1868
  symbol: z.string().describe("Symbol to close (e.g., BTC, ETH)"),
1727
1869
  }, async ({ exchange, symbol }) => {
1728
1870
  try {
1871
+ requireCapabilities(getVenue(exchange)); // trading role first, before any credential
1729
1872
  const adapter = await getOrCreateAdapter(exchange);
1730
1873
  const def = getVenue(adapter.name);
1731
1874
  const caps = requireCapabilities(def);
@@ -1,13 +1,23 @@
1
1
  import { type Network } from "./constants.js";
2
- import type { MarketInfo, PriceInfo, Orderbook, Trade, Kline, KlineInterval, AggLevel, AccountInfo, AccountSettings, Position, OrderInfo, MarketOrderParams, LimitOrderParams, StopOrderParams, EditOrderParams, CancelOrderParams, CancelAllOrdersParams, TWAPParams, CancelTWAPParams, TPSLParams, UpdateLeverageParams, WithdrawParams, TransferFundsParams, BatchAction, CreateLakeParams, LakeDepositParams, LakeWithdrawParams } from "./types/index.js";
2
+ import { requireSolanaAddress } from "../exchanges/address.js";
3
+ import type { MarketInfo, PriceInfo, Orderbook, Trade, Kline, KlineInterval, AggLevel, AccountInfo, AccountSettings, Position, OrderInfo, MarketOrderParams, LimitOrderParams, StopOrderParams, EditOrderParams, CancelOrderParams, CancelAllOrdersParams, TWAPParams, CancelTWAPParams, TPSLParams, UpdateLeverageParams, WithdrawParams, TransferFundsParams, BatchAction, BatchActionType, CreateLakeParams, LakeDepositParams, LakeWithdrawParams } from "./types/index.js";
3
4
  type SignMessageFn = (message: Uint8Array) => Promise<Uint8Array>;
4
5
  /** Per-request timeout of a POST (fetch + body read). */
5
6
  export declare const PACIFICA_POST_TIMEOUT_MS = 15000;
7
+ /**
8
+ * Credits ×10 reserved for one POST /orders/batch. The docs (api/rate-limits "Credit Costs") price
9
+ * a "Standard request/action" at 1 credit and an "Order cancellation" at 0.5 and say nothing
10
+ * batch-specific (batch-order page: no cost); every action is individually signed and processed,
11
+ * so the batch is reserved per ACTION — the conservative reading (over-reserving only throttles
12
+ * earlier; the venue's ratelimit headers on the response settle the real count).
13
+ */
14
+ export declare function pacificaBatchCost(types: readonly BatchActionType[]): number;
6
15
  export interface PacificaClientConfig {
7
16
  network?: Network;
8
17
  baseUrl?: string;
9
18
  apiKey?: string;
10
19
  }
20
+ export { requireSolanaAddress };
11
21
  export declare class PacificaClient {
12
22
  private baseUrl;
13
23
  private apiKey?;
@@ -18,6 +28,10 @@ export declare class PacificaClient {
18
28
  * against the agent key. Undefined for master/PK signers.
19
29
  */
20
30
  private requestAgentWallet?;
31
+ /** Credit-quota state for this base URL + key tier (rate-limit.ts; process-local): unsigned GETs (IP quota) … */
32
+ private readonly rate;
33
+ /** … and signed POSTs (may report the account's quota) kept apart. */
34
+ private readonly signedRate;
21
35
  /** Attach (or clear) the agent wallet used for subsequent signed requests. */
22
36
  setRequestAgentWallet(agentWallet?: string): void;
23
37
  /** Build a signed POST body, attaching agent_wallet when an agent is the active signer. */
@@ -31,8 +45,12 @@ export declare class PacificaClient {
31
45
  * outcome unknown → ORDER_PENDING (sendFailure), never a retryable network error. No connection
32
46
  * at all → not sent. A 4xx is the venue's refusal and keeps the plain `POST <path> failed
33
47
  * (<status>): <body>` error the adapters map (422 business codes) — except on
34
- * DEFINITE_4XX_PATHS, where it is a definite ACTION_REJECTED (429 stays the plain error →
35
- * RATE_LIMITED). A fetch timeout (PACIFICA_POST_TIMEOUT_MS) on a write is ORDER_PENDING too;
48
+ * DEFINITE_4XX_PATHS, where it is a definite ACTION_REJECTED. HTTP 429 → RATE_LIMITED
49
+ * (details.sent === false, retryAfterMs) — never retried, a write is never resubmitted.
50
+ * "429 = not executed" is our reading of the docs ("When credits are exhausted, requests return
51
+ * HTTP 429"), not an explicit Pacifica statement. Before sending, the credit throttle may wait up
52
+ * to PACIFICA_SIGNED_MAX_WAIT_MS (the body is already signed) or fail fast with RATE_LIMITED
53
+ * (nothing sent). A write needs only its own cost, never the read reserve. A fetch timeout (PACIFICA_POST_TIMEOUT_MS) on a write is ORDER_PENDING too;
36
54
  * on a listing read a timeout is TIMEOUT and any other transport failure EXCHANGE_UNREACHABLE.
37
55
  */
38
56
  private post;
@@ -119,6 +137,27 @@ export declare class PacificaClient {
119
137
  createLake(params: CreateLakeParams, account: string, signMessage: SignMessageFn): Promise<unknown>;
120
138
  depositToLake(params: LakeDepositParams, account: string, signMessage: SignMessageFn): Promise<unknown>;
121
139
  withdrawFromLake(params: LakeWithdrawParams, account: string, signMessage: SignMessageFn): Promise<unknown>;
140
+ /**
141
+ * Unauthenticated GET that keeps the whole {success, data, has_more, next_cursor} envelope.
142
+ * Same credit limiter as get() (unsigned IP quota, PACIFICA_READ_NEED: may wait for the window,
143
+ * else RATE_LIMITED sent:false); the response headers are recorded; HTTP 429 → RATE_LIMITED
144
+ * (structural, never retried). Timeout → TIMEOUT, transport failure → EXCHANGE_UNREACHABLE,
145
+ * non-JSON / non-object body → EXCHANGE_ERROR (a read: nothing can have changed).
146
+ */
147
+ getEnvelope(path: string, params?: Record<string, string>): Promise<Record<string, unknown>>;
148
+ /**
149
+ * POST a body the caller already built and signed (Print / vault writes sign their own canonical
150
+ * message so `--dry-run` can show the exact body). Same unknown-outcome rules as every write.
151
+ */
152
+ postPresigned(path: "/game/deposit" | "/game/end" | "/lake/deposit" | "/lake/withdraw", body: Record<string, unknown>): Promise<unknown>;
153
+ /**
154
+ * Pre-sign credit wait for a pre-signed write (same rule as signedBody / amend): call BEFORE
155
+ * building + signing, so the post-sign wait in post() (≤ PACIFICA_SIGNED_MAX_WAIT_MS, inside the
156
+ * 5 s expiry_window) finds credits. Cost: the docs' rate-limits table lists no Print / vault
157
+ * entry — "Standard request/action: 1 credit" applies (end_game is not an order cancellation).
158
+ * Fail-fast RATE_LIMITED here sent and signed nothing (details.sent === false).
159
+ */
160
+ awaitPresignedCapacity(operationType: string): Promise<void>;
122
161
  approveBuilderCode(params: {
123
162
  builder_code: string;
124
163
  max_fee_rate: string;
@@ -137,6 +176,31 @@ export declare class PacificaClient {
137
176
  claimReferralCode(params: {
138
177
  code: string;
139
178
  }, account: string, signMessage: SignMessageFn): Promise<unknown>;
140
- batchOrders(actions: BatchAction[]): Promise<unknown>;
179
+ /**
180
+ * POST /orders/batch with the per-action cost (pacificaBatchCost) reserved. `maxWaitMs` caps the
181
+ * post-sign throttle wait (default PACIFICA_SIGNED_MAX_WAIT_MS, never more): a caller with a
182
+ * tighter signature deadline passes what is left of it, so a wait can never send an expired batch
183
+ * — a longer wait fails fast with RATE_LIMITED (sent: false, nothing sent).
184
+ */
185
+ batchOrders(actions: BatchAction[], opts?: {
186
+ maxWaitMs?: number;
187
+ }): Promise<unknown>;
188
+ /**
189
+ * Pre-sign throttle for a batch (like signedBody does for single writes): wait — up to
190
+ * PACIFICA_MAX_THROTTLE_WAIT_MS, else RATE_LIMITED with nothing signed or sent — until the
191
+ * batch's whole cost fits, so the post-sign acquire in batchOrders normally finds the credits.
192
+ */
193
+ awaitBatchCapacity(types: readonly BatchActionType[]): Promise<void>;
194
+ /**
195
+ * One signed batch action body (docs rest-api/orders/batch-order: "Batch orders are not signed
196
+ * as a whole, but rather by its individual actions components"; each action's `data` carries the
197
+ * fields of its standalone endpoint, signed with that endpoint's operation type). Nothing is sent.
198
+ * `header` fixes the signed timestamp / expiry window, so every action of one batch can share
199
+ * one timestamp. agent_wallet is attached when an agent is the active signer. A signing failure
200
+ * → ORDER_REJECTED (orderNotSent).
201
+ */
202
+ signBatchAction(operationType: string, payload: Record<string, unknown>, account: string, signMessage: SignMessageFn, header: {
203
+ timestamp: number;
204
+ expiry_window: number;
205
+ }): Promise<Record<string, unknown>>;
141
206
  }
142
- export {};