perp-cli 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (313) hide show
  1. package/README.md +152 -13
  2. package/dist/agent-wallet/store.d.ts +3 -2
  3. package/dist/agent-wallet/store.js +2 -2
  4. package/dist/api/public/hyperliquid.js +4 -0
  5. package/dist/api/public/pacifica.js +83 -13
  6. package/dist/api/public/urls.d.ts +2 -0
  7. package/dist/api/public/urls.js +3 -0
  8. package/dist/arb/utils.d.ts +1 -2
  9. package/dist/arb/utils.js +1 -2
  10. package/dist/bot/apex/pulse.js +3 -0
  11. package/dist/bot/apex/radar.js +2 -1
  12. package/dist/bot/engine.d.ts +10 -1
  13. package/dist/bot/engine.js +14 -6
  14. package/dist/bot/indicators.d.ts +7 -0
  15. package/dist/bot/indicators.js +16 -0
  16. package/dist/bot/perp-order.d.ts +4 -4
  17. package/dist/bot/perp-order.js +5 -5
  18. package/dist/bot/quarantine.d.ts +1 -1
  19. package/dist/bot/quarantine.js +1 -1
  20. package/dist/bot/strategies/apex-strategy.js +3 -2
  21. package/dist/bot/strategies/engine-mm.js +6 -5
  22. package/dist/bot/strategies/funding-arb-strategy.js +9 -9
  23. package/dist/bot/strategies/funding-arb-utils.d.ts +15 -2
  24. package/dist/bot/strategies/funding-arb-utils.js +43 -24
  25. package/dist/bot/strategies/funding-auto.js +2 -2
  26. package/dist/bot/strategies/liquidation-mm.d.ts +1 -0
  27. package/dist/bot/strategies/liquidation-mm.js +8 -2
  28. package/dist/bot/strategies/momentum-breakout.d.ts +1 -0
  29. package/dist/bot/strategies/momentum-breakout.js +11 -3
  30. package/dist/bot/strategies/spot-perp-arb-strategy.js +23 -12
  31. package/dist/bot/strategies/spot-perp-executor.d.ts +1 -1
  32. package/dist/bot/strategies/spot-perp-executor.js +1 -1
  33. package/dist/bot/strategy-types.d.ts +2 -1
  34. package/dist/bot/tui/BotDashboard.d.ts +1 -1
  35. package/dist/bot/tui/BotDashboard.js +1 -1
  36. package/dist/cache.js +12 -6
  37. package/dist/cli-program.d.ts +15 -0
  38. package/dist/cli-program.js +125 -5
  39. package/dist/commands/agent-import.d.ts +7 -0
  40. package/dist/commands/agent-import.js +30 -1
  41. package/dist/commands/agent.d.ts +6 -3
  42. package/dist/commands/agent.js +160 -12
  43. package/dist/commands/arb/index.js +4 -4
  44. package/dist/commands/arb-auto.js +21 -17
  45. package/dist/commands/arb.d.ts +12 -0
  46. package/dist/commands/arb.js +61 -143
  47. package/dist/commands/bot.js +1 -1
  48. package/dist/commands/builder.js +1 -1
  49. package/dist/commands/funds.js +113 -3
  50. package/dist/commands/init.d.ts +1 -1
  51. package/dist/commands/init.js +27 -7
  52. package/dist/commands/manage.js +23 -21
  53. package/dist/commands/market.js +168 -36
  54. package/dist/commands/options.d.ts +9 -0
  55. package/dist/commands/options.js +134 -0
  56. package/dist/commands/pacifica-earn.d.ts +5 -0
  57. package/dist/commands/pacifica-earn.js +468 -0
  58. package/dist/commands/pacifica-orders.d.ts +14 -0
  59. package/dist/commands/pacifica-orders.js +142 -0
  60. package/dist/commands/portfolio.js +32 -47
  61. package/dist/commands/rebalance.d.ts +1 -0
  62. package/dist/commands/rebalance.js +15 -0
  63. package/dist/commands/risk.d.ts +11 -2
  64. package/dist/commands/risk.js +39 -44
  65. package/dist/commands/trade.d.ts +1 -0
  66. package/dist/commands/trade.js +63 -58
  67. package/dist/commands/wallet.js +1 -1
  68. package/dist/config.d.ts +2 -2
  69. package/dist/config.js +15 -4
  70. package/dist/constants.js +6 -0
  71. package/dist/dashboard/server.js +1 -1
  72. package/dist/dashboard/ui.js +1 -1
  73. package/dist/errors.d.ts +24 -1
  74. package/dist/errors.js +25 -3
  75. package/dist/exchanges/arcus/adapter.d.ts +7 -7
  76. package/dist/exchanges/arcus/adapter.js +12 -12
  77. package/dist/exchanges/arcus/agent.d.ts +1 -1
  78. package/dist/exchanges/arcus/agent.js +1 -1
  79. package/dist/exchanges/arcus/client.js +1 -1
  80. package/dist/exchanges/arcus/signing.d.ts +2 -3
  81. package/dist/exchanges/arcus/signing.js +1 -1
  82. package/dist/exchanges/arcus/spot-chain.d.ts +1 -1
  83. package/dist/exchanges/arcus/spot-chain.js +1 -1
  84. package/dist/exchanges/arcus/spot-router.d.ts +1 -1
  85. package/dist/exchanges/arcus/spot-router.js +2 -2
  86. package/dist/exchanges/arcus/spot.d.ts +2 -3
  87. package/dist/exchanges/arcus/spot.js +2 -3
  88. package/dist/exchanges/aster-spot.d.ts +3 -3
  89. package/dist/exchanges/aster-spot.js +6 -9
  90. package/dist/exchanges/aster-typed-data.d.ts +1 -1
  91. package/dist/exchanges/aster-typed-data.js +1 -1
  92. package/dist/exchanges/aster.d.ts +9 -4
  93. package/dist/exchanges/aster.js +125 -55
  94. package/dist/exchanges/builder-fee.d.ts +1 -1
  95. package/dist/exchanges/builder-fee.js +13 -2
  96. package/dist/exchanges/canonical-symbols.d.ts +1 -2
  97. package/dist/exchanges/canonical-symbols.js +26 -2
  98. package/dist/exchanges/derive/client.d.ts +37 -0
  99. package/dist/exchanges/derive/client.js +135 -0
  100. package/dist/exchanges/derive/options.d.ts +68 -0
  101. package/dist/exchanges/derive/options.js +360 -0
  102. package/dist/exchanges/extended/adapter.d.ts +82 -0
  103. package/dist/exchanges/extended/adapter.js +179 -0
  104. package/dist/exchanges/extended/client.d.ts +38 -0
  105. package/dist/exchanges/extended/client.js +88 -0
  106. package/dist/exchanges/extended/public.d.ts +130 -0
  107. package/dist/exchanges/extended/public.js +429 -0
  108. package/dist/exchanges/grvt/adapter.d.ts +86 -0
  109. package/dist/exchanges/grvt/adapter.js +206 -0
  110. package/dist/exchanges/grvt/client.d.ts +25 -0
  111. package/dist/exchanges/grvt/client.js +86 -0
  112. package/dist/exchanges/grvt/public.d.ts +151 -0
  113. package/dist/exchanges/grvt/public.js +438 -0
  114. package/dist/exchanges/hyperliquid-spot.d.ts +1 -1
  115. package/dist/exchanges/hyperliquid-spot.js +5 -5
  116. package/dist/exchanges/hyperliquid.d.ts +22 -16
  117. package/dist/exchanges/hyperliquid.js +80 -55
  118. package/dist/exchanges/index.d.ts +1 -1
  119. package/dist/exchanges/index.js +1 -1
  120. package/dist/exchanges/interface.d.ts +97 -8
  121. package/dist/exchanges/interface.js +19 -2
  122. package/dist/exchanges/ioc-outcome.d.ts +1 -1
  123. package/dist/exchanges/ioc-outcome.js +1 -1
  124. package/dist/exchanges/legacy-trading-v2.d.ts +2 -2
  125. package/dist/exchanges/legacy-trading-v2.js +7 -2
  126. package/dist/exchanges/lighter-public.d.ts +2 -2
  127. package/dist/exchanges/lighter-public.js +2 -2
  128. package/dist/exchanges/lighter-signer.d.ts +1 -1
  129. package/dist/exchanges/lighter-signer.js +1 -1
  130. package/dist/exchanges/lighter-spot.d.ts +3 -4
  131. package/dist/exchanges/lighter-spot.js +6 -7
  132. package/dist/exchanges/lighter.d.ts +11 -11
  133. package/dist/exchanges/lighter.js +28 -14
  134. package/dist/exchanges/ondo/adapter.d.ts +5 -6
  135. package/dist/exchanges/ondo/adapter.js +16 -14
  136. package/dist/exchanges/ondo/agent.d.ts +1 -1
  137. package/dist/exchanges/ondo/agent.js +1 -1
  138. package/dist/exchanges/ondo/client.d.ts +1 -1
  139. package/dist/exchanges/ondo/client.js +2 -2
  140. package/dist/exchanges/ondo/public.d.ts +8 -1
  141. package/dist/exchanges/ondo/public.js +13 -2
  142. package/dist/exchanges/ondo/spot.d.ts +2 -2
  143. package/dist/exchanges/ondo/spot.js +3 -3
  144. package/dist/exchanges/options-interface.d.ts +158 -0
  145. package/dist/exchanges/options-interface.js +17 -0
  146. package/dist/exchanges/options-venues.d.ts +12 -0
  147. package/dist/exchanges/options-venues.js +35 -0
  148. package/dist/exchanges/pacifica-atomic.d.ts +211 -0
  149. package/dist/exchanges/pacifica-atomic.js +440 -0
  150. package/dist/exchanges/pacifica-decimal.d.ts +10 -0
  151. package/dist/exchanges/pacifica-decimal.js +51 -0
  152. package/dist/exchanges/pacifica-presigned.d.ts +53 -0
  153. package/dist/exchanges/pacifica-presigned.js +141 -0
  154. package/dist/exchanges/pacifica-print.d.ts +200 -0
  155. package/dist/exchanges/pacifica-print.js +417 -0
  156. package/dist/exchanges/pacifica-spot.d.ts +2 -2
  157. package/dist/exchanges/pacifica-spot.js +3 -3
  158. package/dist/exchanges/pacifica-typed-data.d.ts +1 -1
  159. package/dist/exchanges/pacifica-typed-data.js +1 -1
  160. package/dist/exchanges/pacifica-vaults.d.ts +121 -0
  161. package/dist/exchanges/pacifica-vaults.js +264 -0
  162. package/dist/exchanges/pacifica.d.ts +32 -5
  163. package/dist/exchanges/pacifica.js +114 -48
  164. package/dist/exchanges/precision.d.ts +3 -1
  165. package/dist/exchanges/precision.js +9 -2
  166. package/dist/exchanges/qfex/adapter.d.ts +18 -5
  167. package/dist/exchanges/qfex/adapter.js +32 -12
  168. package/dist/exchanges/qfex/agent.d.ts +1 -1
  169. package/dist/exchanges/qfex/agent.js +1 -1
  170. package/dist/exchanges/qfex/client.d.ts +1 -1
  171. package/dist/exchanges/qfex/client.js +1 -1
  172. package/dist/exchanges/qfex/errors.d.ts +1 -1
  173. package/dist/exchanges/qfex/errors.js +2 -2
  174. package/dist/exchanges/qfex/public.d.ts +3 -3
  175. package/dist/exchanges/qfex/public.js +3 -3
  176. package/dist/exchanges/qfex/signing.d.ts +2 -2
  177. package/dist/exchanges/qfex/signing.js +4 -5
  178. package/dist/exchanges/qfex/ws-transport.d.ts +2 -2
  179. package/dist/exchanges/qfex/ws-transport.js +3 -3
  180. package/dist/exchanges/rate-limiter.d.ts +1 -1
  181. package/dist/exchanges/rate-limiter.js +5 -2
  182. package/dist/exchanges/registry.d.ts +1 -1
  183. package/dist/exchanges/registry.js +1 -1
  184. package/dist/exchanges/risex/adapter.d.ts +148 -0
  185. package/dist/exchanges/risex/adapter.js +796 -0
  186. package/dist/exchanges/risex/agent.d.ts +154 -0
  187. package/dist/exchanges/risex/agent.js +384 -0
  188. package/dist/exchanges/risex/client.d.ts +47 -0
  189. package/dist/exchanges/risex/client.js +174 -0
  190. package/dist/exchanges/risex/nonce.d.ts +43 -0
  191. package/dist/exchanges/risex/nonce.js +212 -0
  192. package/dist/exchanges/risex/public.d.ts +64 -0
  193. package/dist/exchanges/risex/public.js +150 -0
  194. package/dist/exchanges/risex/session.d.ts +71 -0
  195. package/dist/exchanges/risex/session.js +171 -0
  196. package/dist/exchanges/risex/typed-data.d.ts +173 -0
  197. package/dist/exchanges/risex/typed-data.js +240 -0
  198. package/dist/exchanges/risex/units.d.ts +29 -0
  199. package/dist/exchanges/risex/units.js +104 -0
  200. package/dist/exchanges/slippage.d.ts +1 -1
  201. package/dist/exchanges/slippage.js +1 -1
  202. package/dist/exchanges/spot-factory.d.ts +3 -3
  203. package/dist/exchanges/spot-factory.js +7 -7
  204. package/dist/exchanges/spot-interface.d.ts +5 -5
  205. package/dist/exchanges/spot-interface.js +2 -2
  206. package/dist/exchanges/spot-result.d.ts +3 -3
  207. package/dist/exchanges/spot-result.js +3 -3
  208. package/dist/exchanges/trading-v2.d.ts +3 -3
  209. package/dist/exchanges/trading-v2.js +9 -6
  210. package/dist/exchanges/txflow/adapter.d.ts +54 -0
  211. package/dist/exchanges/txflow/adapter.js +118 -0
  212. package/dist/exchanges/txflow/client.d.ts +47 -0
  213. package/dist/exchanges/txflow/client.js +153 -0
  214. package/dist/exchanges/txflow/public.d.ts +76 -0
  215. package/dist/exchanges/txflow/public.js +284 -0
  216. package/dist/exchanges/unknown-symbol.d.ts +29 -0
  217. package/dist/exchanges/unknown-symbol.js +51 -0
  218. package/dist/exchanges/variational/adapter.d.ts +71 -0
  219. package/dist/exchanges/variational/adapter.js +137 -0
  220. package/dist/exchanges/variational/client.d.ts +21 -0
  221. package/dist/exchanges/variational/client.js +66 -0
  222. package/dist/exchanges/variational/public.d.ts +126 -0
  223. package/dist/exchanges/variational/public.js +430 -0
  224. package/dist/exchanges/venue-types.d.ts +6 -7
  225. package/dist/exchanges/venue-types.js +2 -2
  226. package/dist/exchanges/venue.d.ts +51 -8
  227. package/dist/exchanges/venue.js +54 -4
  228. package/dist/exchanges/venues/arcus.d.ts +1 -2
  229. package/dist/exchanges/venues/arcus.js +5 -4
  230. package/dist/exchanges/venues/aster.d.ts +2 -2
  231. package/dist/exchanges/venues/aster.js +6 -4
  232. package/dist/exchanges/venues/create-helpers.d.ts +1 -1
  233. package/dist/exchanges/venues/create-helpers.js +3 -3
  234. package/dist/exchanges/venues/extended.d.ts +21 -0
  235. package/dist/exchanges/venues/extended.js +96 -0
  236. package/dist/exchanges/venues/grvt.d.ts +30 -0
  237. package/dist/exchanges/venues/grvt.js +98 -0
  238. package/dist/exchanges/venues/hyperliquid.d.ts +2 -2
  239. package/dist/exchanges/venues/hyperliquid.js +10 -8
  240. package/dist/exchanges/venues/index.d.ts +7 -2
  241. package/dist/exchanges/venues/index.js +8 -2
  242. package/dist/exchanges/venues/lighter-family.d.ts +7 -9
  243. package/dist/exchanges/venues/lighter-family.js +9 -11
  244. package/dist/exchanges/venues/lighter-rh.d.ts +2 -2
  245. package/dist/exchanges/venues/lighter-rh.js +7 -5
  246. package/dist/exchanges/venues/lighter.d.ts +1 -1
  247. package/dist/exchanges/venues/lighter.js +5 -3
  248. package/dist/exchanges/venues/ondo.d.ts +1 -1
  249. package/dist/exchanges/venues/ondo.js +5 -3
  250. package/dist/exchanges/venues/pacifica.d.ts +2 -2
  251. package/dist/exchanges/venues/pacifica.js +6 -4
  252. package/dist/exchanges/venues/qfex.d.ts +1 -1
  253. package/dist/exchanges/venues/qfex.js +4 -2
  254. package/dist/exchanges/venues/risex-release.d.ts +14 -0
  255. package/dist/exchanges/venues/risex-release.js +14 -0
  256. package/dist/exchanges/venues/risex.d.ts +31 -0
  257. package/dist/exchanges/venues/risex.js +116 -0
  258. package/dist/exchanges/venues/txflow.d.ts +18 -0
  259. package/dist/exchanges/venues/txflow.js +77 -0
  260. package/dist/exchanges/venues/variational.d.ts +21 -0
  261. package/dist/exchanges/venues/variational.js +85 -0
  262. package/dist/execution-log.d.ts +1 -1
  263. package/dist/funding/legacy-public.d.ts +1 -1
  264. package/dist/funding/legacy-public.js +9 -4
  265. package/dist/funding/normalize.d.ts +1 -1
  266. package/dist/funding/normalize.js +1 -1
  267. package/dist/funding/rates.d.ts +44 -4
  268. package/dist/funding/rates.js +56 -12
  269. package/dist/index.js +7 -6
  270. package/dist/jobs.d.ts +1 -0
  271. package/dist/jobs.js +25 -1
  272. package/dist/landing.d.ts +2 -0
  273. package/dist/landing.js +1 -1
  274. package/dist/mcp-pacifica-earn.d.ts +9 -0
  275. package/dist/mcp-pacifica-earn.js +58 -0
  276. package/dist/mcp-server.js +113 -59
  277. package/dist/pacifica/client.d.ts +67 -4
  278. package/dist/pacifica/client.js +170 -8
  279. package/dist/pacifica/rate-limit.d.ts +144 -0
  280. package/dist/pacifica/rate-limit.js +267 -0
  281. package/dist/pacifica/types/order.d.ts +2 -1
  282. package/dist/plan-executor.js +7 -2
  283. package/dist/rebalance.js +1 -1
  284. package/dist/risk.d.ts +2 -1
  285. package/dist/risk.js +2 -4
  286. package/dist/settings.d.ts +32 -3
  287. package/dist/signer/ows-evm.d.ts +1 -1
  288. package/dist/signer/ows-evm.js +1 -1
  289. package/dist/smart-order.d.ts +1 -1
  290. package/dist/smart-order.js +1 -1
  291. package/dist/strategies/funding-arb.d.ts +1 -1
  292. package/dist/strategies/funding-arb.js +1 -1
  293. package/dist/trade-validator.d.ts +1 -1
  294. package/dist/trade-validator.js +9 -4
  295. package/dist/utils.d.ts +12 -3
  296. package/dist/utils.js +11 -3
  297. package/dist/venue-health.d.ts +40 -0
  298. package/dist/venue-health.js +75 -0
  299. package/dist/withdraw-gate.d.ts +1 -1
  300. package/dist/withdraw-gate.js +1 -1
  301. package/dist/ws/index.d.ts +959 -0
  302. package/dist/ws/index.js +4295 -0
  303. package/dist/ws/node.d.ts +975 -0
  304. package/dist/ws/node.js +4366 -0
  305. package/dist/ws/pacifica-instrument.d.ts +8 -0
  306. package/dist/ws/pacifica-instrument.js +8 -0
  307. package/dist/ws-cli/credentials.d.ts +62 -0
  308. package/dist/ws-cli/credentials.js +126 -0
  309. package/package.json +58 -10
  310. package/skills/perp-cli/SKILL.md +62 -9
  311. package/skills/perp-cli/references/agent-operations.md +7 -0
  312. package/skills/perp-cli/references/commands.md +76 -5
  313. package/skills/perp-cli/references/ws-library.md +492 -0
@@ -1,7 +1,8 @@
1
1
  import { getNetworkConfig } from "./constants.js";
2
- import { buildSignedRequest, buildAgentSignedRequest } from "./signing.js";
2
+ import { buildSignedRequest, buildAgentSignedRequest, signWithWallet } from "./signing.js";
3
3
  import { PerpError, extractErrorMessage, orderNotSent, orderOutcomeUnknown, sendFailure } from "../errors.js";
4
4
  import { invalidateCache } from "../cache.js";
5
+ import { PACIFICA_COST_CANCEL, PACIFICA_COST_STANDARD, PACIFICA_MAX_THROTTLE_WAIT_MS, PACIFICA_READ_NEED, PACIFICA_SIGNED_MAX_WAIT_MS, pacificaRateLimiter, } from "./rate-limit.js";
5
6
  /** Per-request timeout of a POST (fetch + body read). */
6
7
  export const PACIFICA_POST_TIMEOUT_MS = 15_000;
7
8
  /**
@@ -14,6 +15,29 @@ export const PACIFICA_POST_TIMEOUT_MS = 15_000;
14
15
  const DEFINITE_4XX_PATHS = new Set(["/positions/tpsl", "/account/leverage"]);
15
16
  /** POST endpoints that only read (signed listings): a failure there is an ordinary read error. */
16
17
  const READ_POST_PATHS = new Set(["/account/subaccount/list", "/agent/list", "/account/api_keys"]);
18
+ /** Cancels cost 0.5 credits (docs: rate-limits "Order cancellation 0.5"), by path and by signed operation type. */
19
+ const CANCEL_POST_PATHS = new Set(["/orders/cancel", "/orders/stop/cancel", "/orders/cancel_all", "/orders/twap/cancel"]);
20
+ const CANCEL_OPERATIONS = new Set(["cancel_order", "cancel_stop_order", "cancel_all_orders", "cancel_twap_order"]);
21
+ /** Signed operation types of the READ_POST_PATHS listings: the read need applies before and after signing. */
22
+ const READ_OPERATIONS = new Set(["list_subaccounts", "list_agent_wallets", "list_api_keys"]);
23
+ /**
24
+ * A signed write needs only its own cost (never the read reserve); `maxWaitMs` = how long it may
25
+ * wait at this point (before signing: the normal bound; after signing: inside expiry_window).
26
+ */
27
+ function writeNeed(cancel, maxWaitMs) {
28
+ const cost = cancel ? PACIFICA_COST_CANCEL : PACIFICA_COST_STANDARD;
29
+ return { cost, reserve: 0, maxWaitMs };
30
+ }
31
+ /**
32
+ * Credits ×10 reserved for one POST /orders/batch. The docs (api/rate-limits "Credit Costs") price
33
+ * a "Standard request/action" at 1 credit and an "Order cancellation" at 0.5 and say nothing
34
+ * batch-specific (batch-order page: no cost); every action is individually signed and processed,
35
+ * so the batch is reserved per ACTION — the conservative reading (over-reserving only throttles
36
+ * earlier; the venue's ratelimit headers on the response settle the real count).
37
+ */
38
+ export function pacificaBatchCost(types) {
39
+ return types.reduce((a, t) => a + (t === "Cancel" || t === "CancelStopOrder" ? PACIFICA_COST_CANCEL : PACIFICA_COST_STANDARD), 0);
40
+ }
17
41
  export class PacificaClient {
18
42
  baseUrl;
19
43
  apiKey;
@@ -24,12 +48,21 @@ export class PacificaClient {
24
48
  * against the agent key. Undefined for master/PK signers.
25
49
  */
26
50
  requestAgentWallet;
51
+ /** Credit-quota state for this base URL + key tier (rate-limit.ts; process-local): unsigned GETs (IP quota) … */
52
+ rate;
53
+ /** … and signed POSTs (may report the account's quota) kept apart. */
54
+ signedRate;
27
55
  /** Attach (or clear) the agent wallet used for subsequent signed requests. */
28
56
  setRequestAgentWallet(agentWallet) {
29
57
  this.requestAgentWallet = agentWallet;
30
58
  }
31
59
  /** Build a signed POST body, attaching agent_wallet when an agent is the active signer. */
32
60
  async signedBody(operationType, payload, account, signMessage) {
61
+ // Throttle BEFORE signing: a wait after signing would eat into the body's expiry_window.
62
+ // A fail-fast RATE_LIMITED here sent nothing (details.sent === false).
63
+ await this.signedRate.awaitCapacity(operationType, READ_OPERATIONS.has(operationType)
64
+ ? PACIFICA_READ_NEED
65
+ : writeNeed(CANCEL_OPERATIONS.has(operationType), PACIFICA_MAX_THROTTLE_WAIT_MS));
33
66
  try {
34
67
  return await (this.requestAgentWallet
35
68
  ? buildAgentSignedRequest(operationType, payload, account, this.requestAgentWallet, signMessage)
@@ -44,6 +77,8 @@ export class PacificaClient {
44
77
  this.baseUrl =
45
78
  config.baseUrl || getNetworkConfig(config.network).restUrl;
46
79
  this.apiKey = config.apiKey;
80
+ this.rate = pacificaRateLimiter(this.baseUrl, this.apiKey, "unsigned");
81
+ this.signedRate = pacificaRateLimiter(this.baseUrl, this.apiKey, "signed");
47
82
  }
48
83
  headers() {
49
84
  const h = { "Content-Type": "application/json" };
@@ -59,9 +94,20 @@ export class PacificaClient {
59
94
  url.searchParams.set(k, v);
60
95
  });
61
96
  }
62
- const res = await fetch(url.toString(), { headers: this.headers() });
97
+ const ticket = await this.rate.acquire(`GET ${path}`, PACIFICA_READ_NEED);
98
+ let res;
99
+ try {
100
+ res = await fetch(url.toString(), { headers: this.headers() });
101
+ }
102
+ finally {
103
+ ticket.release();
104
+ }
105
+ this.rate.observe(res);
63
106
  if (!res.ok) {
64
107
  const text = await res.text();
108
+ // Structural (by status): RATE_LIMITED with retryAfterMs; never retried here.
109
+ if (res.status === 429)
110
+ throw this.rate.rateLimitedError(`GET ${path}`, res, text);
65
111
  throw new Error(`GET ${path} failed (${res.status}): ${text}`);
66
112
  }
67
113
  const json = await res.json();
@@ -77,14 +123,21 @@ export class PacificaClient {
77
123
  * outcome unknown → ORDER_PENDING (sendFailure), never a retryable network error. No connection
78
124
  * at all → not sent. A 4xx is the venue's refusal and keeps the plain `POST <path> failed
79
125
  * (<status>): <body>` error the adapters map (422 business codes) — except on
80
- * DEFINITE_4XX_PATHS, where it is a definite ACTION_REJECTED (429 stays the plain error →
81
- * RATE_LIMITED). A fetch timeout (PACIFICA_POST_TIMEOUT_MS) on a write is ORDER_PENDING too;
126
+ * DEFINITE_4XX_PATHS, where it is a definite ACTION_REJECTED. HTTP 429 → RATE_LIMITED
127
+ * (details.sent === false, retryAfterMs) — never retried, a write is never resubmitted.
128
+ * "429 = not executed" is our reading of the docs ("When credits are exhausted, requests return
129
+ * HTTP 429"), not an explicit Pacifica statement. Before sending, the credit throttle may wait up
130
+ * to PACIFICA_SIGNED_MAX_WAIT_MS (the body is already signed) or fail fast with RATE_LIMITED
131
+ * (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;
82
132
  * on a listing read a timeout is TIMEOUT and any other transport failure EXCHANGE_UNREACHABLE.
83
133
  */
84
- async post(path, body) {
134
+ async post(path, body, needOverride) {
85
135
  const write = !READ_POST_PATHS.has(path);
86
136
  const what = `POST ${path}`;
87
137
  const remediation = `Do NOT resubmit — the request may have been executed. Check open orders, positions and balances first: perp -e pacifica account orders / portfolio`;
138
+ const ticket = await this.signedRate.acquire(what, needOverride !== undefined ? needOverride : write
139
+ ? writeNeed(CANCEL_POST_PATHS.has(path), PACIFICA_SIGNED_MAX_WAIT_MS)
140
+ : { ...PACIFICA_READ_NEED, maxWaitMs: PACIFICA_SIGNED_MAX_WAIT_MS });
88
141
  try {
89
142
  let res;
90
143
  let text;
@@ -95,6 +148,7 @@ export class PacificaClient {
95
148
  body: JSON.stringify(body),
96
149
  signal: AbortSignal.timeout(PACIFICA_POST_TIMEOUT_MS),
97
150
  });
151
+ ticket.settle(res);
98
152
  text = await res.text();
99
153
  }
100
154
  catch (e) {
@@ -110,8 +164,11 @@ export class PacificaClient {
110
164
  if (write && res.status >= 500) {
111
165
  throw orderOutcomeUnknown("pacifica", what, new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`), remediation);
112
166
  }
167
+ // Refused before processing (credits exhausted): nothing executed, never resubmitted here.
168
+ if (res.status === 429)
169
+ throw this.signedRate.rateLimitedError(what, res, text);
113
170
  if (!res.ok) {
114
- if (DEFINITE_4XX_PATHS.has(path) && res.status >= 400 && res.status < 500 && res.status !== 429) {
171
+ if (DEFINITE_4XX_PATHS.has(path) && res.status >= 400 && res.status < 500) {
115
172
  throw new PerpError("ACTION_REJECTED", `Pacifica ${what} refused (HTTP ${res.status}, nothing was changed): ${text.slice(0, 300)}`, {
116
173
  exchange: "pacifica",
117
174
  httpStatus: res.status,
@@ -130,6 +187,7 @@ export class PacificaClient {
130
187
  }
131
188
  }
132
189
  finally {
190
+ ticket.release(); // no-op once settled; a transport failure frees the reservation
133
191
  // Every POST is an account write (order / cancel / TP/SL / leverage / transfer): drop the
134
192
  // shared acct: disk cache so no later read serves the pre-write positions / orders.
135
193
  invalidateCache("acct");
@@ -377,6 +435,70 @@ export class PacificaClient {
377
435
  const body = await this.signedBody("withdraw_from_lake", params, account, signMessage);
378
436
  return this.post("/lake/withdraw", body);
379
437
  }
438
+ // ==================== PRINT (game) + VAULT READS / PRE-SIGNED WRITES ====================
439
+ // Shapes and parsing live in src/exchanges/pacifica-print.ts / pacifica-vaults.ts.
440
+ /**
441
+ * Unauthenticated GET that keeps the whole {success, data, has_more, next_cursor} envelope.
442
+ * Same credit limiter as get() (unsigned IP quota, PACIFICA_READ_NEED: may wait for the window,
443
+ * else RATE_LIMITED sent:false); the response headers are recorded; HTTP 429 → RATE_LIMITED
444
+ * (structural, never retried). Timeout → TIMEOUT, transport failure → EXCHANGE_UNREACHABLE,
445
+ * non-JSON / non-object body → EXCHANGE_ERROR (a read: nothing can have changed).
446
+ */
447
+ async getEnvelope(path, params = {}) {
448
+ const url = new URL(`${this.baseUrl}${path}`);
449
+ for (const [k, v] of Object.entries(params))
450
+ url.searchParams.set(k, v);
451
+ const what = `GET ${path}`;
452
+ const ticket = await this.rate.acquire(what, PACIFICA_READ_NEED);
453
+ let res;
454
+ let text;
455
+ try {
456
+ res = await fetch(url.toString(), { headers: this.headers(), signal: AbortSignal.timeout(PACIFICA_POST_TIMEOUT_MS) });
457
+ ticket.settle(res);
458
+ text = await res.text();
459
+ }
460
+ catch (e) {
461
+ const timedOut = e instanceof Error && (e.name === "TimeoutError" || e.name === "AbortError");
462
+ throw new PerpError(timedOut ? "TIMEOUT" : "EXCHANGE_UNREACHABLE", `Pacifica GET ${path} ${timedOut ? `timed out after ${PACIFICA_POST_TIMEOUT_MS} ms` : `failed: ${extractErrorMessage(e)}`}`, {
463
+ exchange: "pacifica", remediation: "Retry; if it keeps failing check the network and Pacifica's status",
464
+ });
465
+ }
466
+ finally {
467
+ ticket.release(); // no-op once settled; a transport failure frees the reservation
468
+ }
469
+ if (res.status === 429)
470
+ throw this.rate.rateLimitedError(what, res, text);
471
+ if (!res.ok)
472
+ throw new Error(`GET ${path} failed (${res.status}): ${text}`);
473
+ let json;
474
+ try {
475
+ json = JSON.parse(text);
476
+ }
477
+ catch {
478
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica GET ${path} returned a non-JSON body (HTTP ${res.status}): ${text.slice(0, 160)}`, { exchange: "pacifica" });
479
+ }
480
+ if (json === null || typeof json !== "object" || Array.isArray(json)) {
481
+ throw new PerpError("EXCHANGE_ERROR", `Pacifica GET ${path} returned a non-object body: ${text.slice(0, 160)}`, { exchange: "pacifica" });
482
+ }
483
+ return json;
484
+ }
485
+ /**
486
+ * POST a body the caller already built and signed (Print / vault writes sign their own canonical
487
+ * message so `--dry-run` can show the exact body). Same unknown-outcome rules as every write.
488
+ */
489
+ async postPresigned(path, body) {
490
+ return this.post(path, body);
491
+ }
492
+ /**
493
+ * Pre-sign credit wait for a pre-signed write (same rule as signedBody / amend): call BEFORE
494
+ * building + signing, so the post-sign wait in post() (≤ PACIFICA_SIGNED_MAX_WAIT_MS, inside the
495
+ * 5 s expiry_window) finds credits. Cost: the docs' rate-limits table lists no Print / vault
496
+ * entry — "Standard request/action: 1 credit" applies (end_game is not an order cancellation).
497
+ * Fail-fast RATE_LIMITED here sent and signed nothing (details.sent === false).
498
+ */
499
+ async awaitPresignedCapacity(operationType) {
500
+ await this.signedRate.awaitCapacity(operationType, writeNeed(false, PACIFICA_MAX_THROTTLE_WAIT_MS));
501
+ }
380
502
  // ==================== BUILDER CODE ====================
381
503
  async approveBuilderCode(params, account, signMessage) {
382
504
  const body = await this.signedBody("approve_builder_code", params, account, signMessage);
@@ -408,7 +530,47 @@ export class PacificaClient {
408
530
  return this.post("/referral/user/code/claim", body);
409
531
  }
410
532
  // ==================== BATCH ====================
411
- async batchOrders(actions) {
412
- return this.post("/orders/batch", { actions });
533
+ /**
534
+ * POST /orders/batch with the per-action cost (pacificaBatchCost) reserved. `maxWaitMs` caps the
535
+ * post-sign throttle wait (default PACIFICA_SIGNED_MAX_WAIT_MS, never more): a caller with a
536
+ * tighter signature deadline passes what is left of it, so a wait can never send an expired batch
537
+ * — a longer wait fails fast with RATE_LIMITED (sent: false, nothing sent).
538
+ */
539
+ async batchOrders(actions, opts = {}) {
540
+ const maxWaitMs = Math.max(0, Math.min(opts.maxWaitMs !== undefined ? opts.maxWaitMs : PACIFICA_SIGNED_MAX_WAIT_MS, PACIFICA_SIGNED_MAX_WAIT_MS));
541
+ return this.post("/orders/batch", { actions }, { cost: pacificaBatchCost(actions.map(a => a.type)), reserve: 0, maxWaitMs });
542
+ }
543
+ /**
544
+ * Pre-sign throttle for a batch (like signedBody does for single writes): wait — up to
545
+ * PACIFICA_MAX_THROTTLE_WAIT_MS, else RATE_LIMITED with nothing signed or sent — until the
546
+ * batch's whole cost fits, so the post-sign acquire in batchOrders normally finds the credits.
547
+ */
548
+ async awaitBatchCapacity(types) {
549
+ await this.signedRate.awaitCapacity("POST /orders/batch", { cost: pacificaBatchCost(types), reserve: 0, maxWaitMs: PACIFICA_MAX_THROTTLE_WAIT_MS });
550
+ }
551
+ /**
552
+ * One signed batch action body (docs rest-api/orders/batch-order: "Batch orders are not signed
553
+ * as a whole, but rather by its individual actions components"; each action's `data` carries the
554
+ * fields of its standalone endpoint, signed with that endpoint's operation type). Nothing is sent.
555
+ * `header` fixes the signed timestamp / expiry window, so every action of one batch can share
556
+ * one timestamp. agent_wallet is attached when an agent is the active signer. A signing failure
557
+ * → ORDER_REJECTED (orderNotSent).
558
+ */
559
+ async signBatchAction(operationType, payload, account, signMessage, header) {
560
+ const full = { type: operationType, timestamp: header.timestamp, expiry_window: header.expiry_window };
561
+ try {
562
+ const [, signature] = await signWithWallet(full, payload, signMessage);
563
+ return {
564
+ account,
565
+ ...(this.requestAgentWallet ? { agent_wallet: this.requestAgentWallet } : {}),
566
+ signature,
567
+ timestamp: full.timestamp,
568
+ expiry_window: full.expiry_window,
569
+ ...payload,
570
+ };
571
+ }
572
+ catch (e) {
573
+ throw orderNotSent("pacifica", operationType, e);
574
+ }
413
575
  }
414
576
  }
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Pacifica credit rate limit, read from the venue's own response headers.
3
+ *
4
+ * docs.pacifica.fi api-documentation/api/rate-limits (read 2026-10-19): credit-based, rolling
5
+ * 60 s window; 100 credits / 60 s per unidentified IP, 300 with a valid API Config Key (more for
6
+ * higher fee tiers); standard request 1, cancel 0.5, heavy GET 3-12 (1-4 with a key). Exhausted →
7
+ * HTTP 429. "All credit values are multiplied by 10 to support fractional costs":
8
+ *
9
+ * ratelimit: "credits";r=1200;t=32 r = remaining (×10), t = seconds until refresh
10
+ * ratelimit-policy: "credits";q=1250;w=60 q = quota per window (×10), w = window seconds
11
+ *
12
+ * Live mainnet GET /info/prices (2026-10-19, no key): `ratelimit: "credits";r=980;t=22`,
13
+ * `ratelimit-policy: "credits";q=1000;w=60`. Values below stay in those ×10 units.
14
+ *
15
+ * CloudFront caches some GETs (/info: `x-cache: Hit from cloudfront`, `age: 16`): a cache hit
16
+ * replays the headers of whoever filled the cache (live: q=3000, a keyed caller) — not this
17
+ * caller's quota, so a hit carries no information.
18
+ *
19
+ * Scope: one state per REST base URL (mainnet / testnet), credential tier (no key = IP quota;
20
+ * an API Config Key has its own) and request kind (signed POST vs unsigned GET). Process-local: other perp-cli processes (CLI + MCP + bots) on the
21
+ * same IP spend the same quota unseen, so a 429 can still happen — it is then surfaced as
22
+ * RATE_LIMITED, never retried here.
23
+ */
24
+ import { PerpError } from "../errors.js";
25
+ /** One parsed `ratelimit` (+ optional `ratelimit-policy`) observation, in credits ×10. */
26
+ export interface PacificaRateLimitInfo {
27
+ remaining: number;
28
+ resetSec: number;
29
+ /** null = `ratelimit-policy` absent or malformed. */
30
+ quota: number | null;
31
+ windowSec: number | null;
32
+ }
33
+ /**
34
+ * Strict parse of the `ratelimit` / `ratelimit-policy` header values. `ratelimit` absent or
35
+ * malformed (no `"credits"` item, missing / non-integer / duplicate r or t) → null: no info, never
36
+ * a guessed value. A malformed policy only nulls quota / windowSec.
37
+ */
38
+ export declare function parsePacificaRateLimit(ratelimit: string | null, policy: string | null): PacificaRateLimitInfo | null;
39
+ /** `Retry-After` in delta-seconds → ms; absent / HTTP-date / malformed → null. */
40
+ export declare function parseRetryAfterMs(raw: string | null): number | null;
41
+ type HeaderSource = {
42
+ headers?: {
43
+ get(name: string): string | null;
44
+ };
45
+ } | null | undefined;
46
+ /** Parsed rate-limit info of a response this caller's request produced; null = no info. */
47
+ export declare function pacificaRateLimitOf(res: HeaderSource): PacificaRateLimitInfo | null;
48
+ /** Read reserve (credits ×10): the largest documented heavy-GET cost (12 credits) stays unspent after a read. */
49
+ export declare const PACIFICA_READ_RESERVE = 120;
50
+ /** Cost (credits ×10) of a standard request / action (docs: 1 credit). */
51
+ export declare const PACIFICA_COST_STANDARD = 10;
52
+ /** Cost (credits ×10) of an order cancellation (docs: 0.5 credits). */
53
+ export declare const PACIFICA_COST_CANCEL = 5;
54
+ /** Longest pre-send wait before failing fast with RATE_LIMITED. */
55
+ export declare const PACIFICA_MAX_THROTTLE_WAIT_MS = 15000;
56
+ /** Longest wait between signing and sending (signed bodies expire after expiry_window = 5 s). */
57
+ export declare const PACIFICA_SIGNED_MAX_WAIT_MS = 2000;
58
+ export interface RateClock {
59
+ now(): number;
60
+ sleep(ms: number): Promise<void>;
61
+ }
62
+ /** What a request needs before it may be sent. */
63
+ export interface RateNeed {
64
+ /** Credits ×10 this request is assumed to cost (PACIFICA_COST_*). */
65
+ cost: number;
66
+ /**
67
+ * Credits ×10 that must still be left after this request (observed remaining − in flight − cost):
68
+ * reads keep the heavy-GET reserve (PACIFICA_READ_RESERVE); writes 0 — a write needs only its own
69
+ * cost, so an amend's replacement leg is never blocked by the read reserve.
70
+ */
71
+ reserve: number;
72
+ /** Longest pre-send wait; longer → RATE_LIMITED fail-fast (0 = never wait). */
73
+ maxWaitMs: number;
74
+ }
75
+ export declare const PACIFICA_READ_NEED: RateNeed;
76
+ /** A reserved request: settle with its response (or release on a transport failure). Idempotent. */
77
+ export interface RateTicket {
78
+ settle(res: HeaderSource): PacificaRateLimitInfo | null;
79
+ release(): void;
80
+ }
81
+ export declare class PacificaRateLimiter {
82
+ private readonly label;
83
+ private readonly clock;
84
+ private obs;
85
+ /** Credits ×10 reserved by requests sent and not yet settled — not reset by observe(). */
86
+ private inFlight;
87
+ private lastRaw;
88
+ private skew;
89
+ constructor(label: string, clock?: RateClock);
90
+ /** clock.now() made monotonic: a backwards step is absorbed instead of restarting elapsed time. */
91
+ private now;
92
+ /** ms until the observed window refreshes; null = no live observation. */
93
+ private resetWaitMs;
94
+ /** Current state for diagnostics (credits ×10); null = no live observation. */
95
+ snapshot(): {
96
+ remaining: number;
97
+ inFlight: number;
98
+ effectiveRemaining: number;
99
+ resetInMs: number;
100
+ } | null;
101
+ /** ms to wait before a request with `need` may be sent: 0 = now (no info, or enough credits left). */
102
+ waitMs(need: Pick<RateNeed, "cost" | "reserve">): number;
103
+ private ticket;
104
+ /**
105
+ * Pre-send throttle + reservation. Enough credits (or no info) → the cost is reserved
106
+ * synchronously, before any await, so concurrent callers see each other. Otherwise waits for the
107
+ * window to refresh (total ≤ need.maxWaitMs) or fails fast with RATE_LIMITED (sent: false).
108
+ */
109
+ acquire(what: string, need: RateNeed): Promise<RateTicket>;
110
+ /** Like acquire but never waits or refuses (health probe): reserves and records only. */
111
+ track(cost: number): RateTicket;
112
+ /** Wait (no reservation) until `need` would pass — used before signing, so the post-sign acquire finds credits. */
113
+ awaitCapacity(what: string, need: RateNeed): Promise<void>;
114
+ private throttled;
115
+ /**
116
+ * Record a response's headers (any status). No / malformed / cached headers → unchanged. Within
117
+ * the live window an observation with a higher `r` (a late response from before others landed)
118
+ * or a different `q` (another quota: account vs IP) is dropped.
119
+ */
120
+ observe(res: HeaderSource): PacificaRateLimitInfo | null;
121
+ /**
122
+ * HTTP 429 → RATE_LIMITED (structural, by status). retryAfterMs = `Retry-After`, else the
123
+ * ratelimit reset `t`, else absent (clamped to the window). The quota is recorded as exhausted.
124
+ *
125
+ * `sent: false` ("not executed") is OUR reading of the docs — "When credits are exhausted,
126
+ * requests return HTTP 429" — i.e. the request is refused before processing. Pacifica does not
127
+ * state explicitly that a 429'd write was not executed.
128
+ */
129
+ rateLimitedError(what: string, res: HeaderSource & {
130
+ status: number;
131
+ }, body: string): PerpError;
132
+ }
133
+ /** A Pacifica RATE_LIMITED that sent nothing (pre-send throttle or HTTP 429): a definite refusal. */
134
+ export declare function isPacificaRateLimited(e: unknown): boolean;
135
+ /**
136
+ * The process-local limiter for a REST base URL + credential tier (no key = IP quota) + request
137
+ * kind. Signed POSTs are kept apart from unsigned GETs: credits are shared per main account and
138
+ * scale with its fee tier, so signed responses can report an account quota (e.g. q=3000) while
139
+ * keyless GETs report the IP quota (q=1000).
140
+ */
141
+ export declare function pacificaRateLimiter(baseUrl: string, apiKey: string | undefined, kind: "signed" | "unsigned"): PacificaRateLimiter;
142
+ /** Test hook: forget every limiter's state. */
143
+ export declare function resetPacificaRateLimiters(): void;
144
+ export {};