perp-cli 0.12.17 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. package/README.md +247 -27
  2. package/dist/_stderr-filter.js +0 -1
  3. package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
  4. package/dist/agent-wallet/lighter-keystore.js +92 -10
  5. package/dist/agent-wallet/passphrase.d.ts +1 -1
  6. package/dist/agent-wallet/passphrase.js +16 -3
  7. package/dist/agent-wallet/store.d.ts +37 -4
  8. package/dist/agent-wallet/store.js +132 -0
  9. package/dist/agent-wallet/venue-keystore.d.ts +14 -0
  10. package/dist/agent-wallet/venue-keystore.js +121 -0
  11. package/dist/api/public/hyperliquid.js +4 -4
  12. package/dist/api/public/lighter.js +5 -5
  13. package/dist/api/public/pacifica.d.ts +0 -1
  14. package/dist/api/public/pacifica.js +4 -13
  15. package/dist/api/public/urls.d.ts +9 -1
  16. package/dist/api/public/urls.js +35 -13
  17. package/dist/arb/file-lock.d.ts +11 -0
  18. package/dist/arb/file-lock.js +74 -0
  19. package/dist/arb/index.d.ts +2 -2
  20. package/dist/arb/index.js +2 -2
  21. package/dist/arb/pending-entry.d.ts +32 -0
  22. package/dist/arb/pending-entry.js +77 -0
  23. package/dist/arb/sizing.d.ts +6 -13
  24. package/dist/arb/sizing.js +12 -51
  25. package/dist/arb/state.d.ts +45 -12
  26. package/dist/arb/state.js +214 -42
  27. package/dist/arb/utils.d.ts +20 -6
  28. package/dist/arb/utils.js +43 -39
  29. package/dist/bot/conditions.d.ts +4 -4
  30. package/dist/bot/conditions.js +11 -6
  31. package/dist/bot/config.d.ts +17 -1
  32. package/dist/bot/config.js +48 -12
  33. package/dist/bot/engine.d.ts +61 -2
  34. package/dist/bot/engine.js +610 -144
  35. package/dist/bot/fill-size.d.ts +7 -0
  36. package/dist/bot/fill-size.js +18 -0
  37. package/dist/bot/index.d.ts +1 -1
  38. package/dist/bot/index.js +1 -1
  39. package/dist/bot/perp-order.d.ts +99 -0
  40. package/dist/bot/perp-order.js +299 -0
  41. package/dist/bot/presets.d.ts +7 -0
  42. package/dist/bot/presets.js +17 -0
  43. package/dist/bot/quarantine.d.ts +75 -0
  44. package/dist/bot/quarantine.js +190 -0
  45. package/dist/bot/quote-rules.d.ts +23 -0
  46. package/dist/bot/quote-rules.js +69 -0
  47. package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
  48. package/dist/bot/strategies/avellaneda-mm.js +72 -70
  49. package/dist/bot/strategies/basis-arb.js +2 -0
  50. package/dist/bot/strategies/dca-strategy.d.ts +2 -0
  51. package/dist/bot/strategies/dca-strategy.js +5 -0
  52. package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
  53. package/dist/bot/strategies/funding-arb-strategy.js +571 -380
  54. package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
  55. package/dist/bot/strategies/funding-arb-utils.js +159 -195
  56. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  57. package/dist/bot/strategies/funding-arb-v2.js +258 -47
  58. package/dist/bot/strategies/funding-auto.js +215 -194
  59. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
  60. package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
  61. package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
  62. package/dist/bot/strategies/spot-perp-executor.js +319 -120
  63. package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
  64. package/dist/bot/strategies/spot-perp-state.js +70 -0
  65. package/dist/bot/strategy-types.d.ts +6 -0
  66. package/dist/bot/tui/BotDashboard.d.ts +7 -2
  67. package/dist/bot/tui/BotDashboard.js +6 -3
  68. package/dist/bridge-engine.d.ts +120 -19
  69. package/dist/bridge-engine.js +710 -422
  70. package/dist/bridge-signers.d.ts +54 -0
  71. package/dist/bridge-signers.js +161 -0
  72. package/dist/cache.d.ts +4 -1
  73. package/dist/cache.js +11 -0
  74. package/dist/cli-flags.d.ts +50 -0
  75. package/dist/cli-flags.js +104 -0
  76. package/dist/cli-program.d.ts +35 -0
  77. package/dist/cli-program.js +286 -0
  78. package/dist/cli-spec.js +1 -1
  79. package/dist/client-id-tracker.js +11 -2
  80. package/dist/commands/account.d.ts +7 -1
  81. package/dist/commands/account.js +70 -92
  82. package/dist/commands/agent-import.d.ts +114 -0
  83. package/dist/commands/agent-import.js +794 -0
  84. package/dist/commands/agent.d.ts +232 -2
  85. package/dist/commands/agent.js +1858 -955
  86. package/dist/commands/alerts.d.ts +1 -0
  87. package/dist/commands/alerts.js +31 -28
  88. package/dist/commands/arb/index.d.ts +60 -1
  89. package/dist/commands/arb/index.js +381 -384
  90. package/dist/commands/arb-auto.d.ts +132 -4
  91. package/dist/commands/arb-auto.js +1474 -967
  92. package/dist/commands/arb.d.ts +3 -1
  93. package/dist/commands/arb.js +62 -30
  94. package/dist/commands/bot.js +127 -47
  95. package/dist/commands/bridge.d.ts +2 -1
  96. package/dist/commands/bridge.js +339 -192
  97. package/dist/commands/builder.d.ts +55 -0
  98. package/dist/commands/builder.js +387 -0
  99. package/dist/commands/funds-onchain.d.ts +32 -0
  100. package/dist/commands/funds-onchain.js +286 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +939 -245
  103. package/dist/commands/history.js +72 -45
  104. package/dist/commands/init.d.ts +107 -15
  105. package/dist/commands/init.js +1116 -249
  106. package/dist/commands/jobs.d.ts +2 -1
  107. package/dist/commands/jobs.js +77 -15
  108. package/dist/commands/manage.js +136 -91
  109. package/dist/commands/market.d.ts +4 -1
  110. package/dist/commands/market.js +35 -32
  111. package/dist/commands/outcome.d.ts +10 -0
  112. package/dist/commands/outcome.js +396 -0
  113. package/dist/commands/plan.js +52 -12
  114. package/dist/commands/portfolio.d.ts +1 -0
  115. package/dist/commands/portfolio.js +60 -72
  116. package/dist/commands/rebalance.d.ts +2 -1
  117. package/dist/commands/rebalance.js +33 -43
  118. package/dist/commands/risk.js +46 -19
  119. package/dist/commands/settings.d.ts +1 -0
  120. package/dist/commands/settings.js +246 -51
  121. package/dist/commands/trade.d.ts +50 -0
  122. package/dist/commands/trade.js +1943 -695
  123. package/dist/commands/wallet.d.ts +30 -1
  124. package/dist/commands/wallet.js +316 -337
  125. package/dist/config.d.ts +60 -9
  126. package/dist/config.js +252 -82
  127. package/dist/constants.d.ts +29 -4
  128. package/dist/constants.js +62 -20
  129. package/dist/dashboard/server.d.ts +2 -0
  130. package/dist/dashboard/server.js +9 -6
  131. package/dist/dashboard/ui.js +6 -4
  132. package/dist/dashboard/ws-feeds.js +20 -8
  133. package/dist/dex-asset-map.js +5 -3
  134. package/dist/errors.d.ts +122 -0
  135. package/dist/errors.js +194 -0
  136. package/dist/event-stream.js +75 -19
  137. package/dist/evm-master-tx.d.ts +59 -0
  138. package/dist/evm-master-tx.js +154 -0
  139. package/dist/exchanges/adapter-network.d.ts +10 -0
  140. package/dist/exchanges/adapter-network.js +10 -0
  141. package/dist/exchanges/arcus/adapter.d.ts +196 -0
  142. package/dist/exchanges/arcus/adapter.js +1092 -0
  143. package/dist/exchanges/arcus/agent.d.ts +55 -0
  144. package/dist/exchanges/arcus/agent.js +188 -0
  145. package/dist/exchanges/arcus/client.d.ts +63 -0
  146. package/dist/exchanges/arcus/client.js +285 -0
  147. package/dist/exchanges/arcus/public.d.ts +65 -0
  148. package/dist/exchanges/arcus/public.js +138 -0
  149. package/dist/exchanges/arcus/signing.d.ts +176 -0
  150. package/dist/exchanges/arcus/signing.js +288 -0
  151. package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
  152. package/dist/exchanges/arcus/spot-chain.js +241 -0
  153. package/dist/exchanges/arcus/spot-router.d.ts +177 -0
  154. package/dist/exchanges/arcus/spot-router.js +183 -0
  155. package/dist/exchanges/arcus/spot.d.ts +109 -0
  156. package/dist/exchanges/arcus/spot.js +645 -0
  157. package/dist/exchanges/aster-funds.d.ts +127 -0
  158. package/dist/exchanges/aster-funds.js +252 -0
  159. package/dist/exchanges/aster-nonce.d.ts +2 -0
  160. package/dist/exchanges/aster-nonce.js +107 -0
  161. package/dist/exchanges/aster-spot.d.ts +117 -0
  162. package/dist/exchanges/aster-spot.js +686 -0
  163. package/dist/exchanges/aster-typed-data.d.ts +133 -7
  164. package/dist/exchanges/aster-typed-data.js +121 -7
  165. package/dist/exchanges/aster.d.ts +219 -6
  166. package/dist/exchanges/aster.js +1023 -153
  167. package/dist/exchanges/builder-fee.d.ts +254 -0
  168. package/dist/exchanges/builder-fee.js +421 -0
  169. package/dist/exchanges/canonical-symbols.d.ts +32 -0
  170. package/dist/exchanges/canonical-symbols.js +96 -0
  171. package/dist/exchanges/capabilities.d.ts +7 -1
  172. package/dist/exchanges/capabilities.js +7 -0
  173. package/dist/exchanges/hl-signing.d.ts +165 -0
  174. package/dist/exchanges/hl-signing.js +158 -0
  175. package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
  176. package/dist/exchanges/hyperliquid-outcome.js +646 -0
  177. package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
  178. package/dist/exchanges/hyperliquid-spot.js +308 -117
  179. package/dist/exchanges/hyperliquid.d.ts +462 -57
  180. package/dist/exchanges/hyperliquid.js +1575 -449
  181. package/dist/exchanges/index.d.ts +6 -0
  182. package/dist/exchanges/index.js +4 -0
  183. package/dist/exchanges/interface.d.ts +63 -10
  184. package/dist/exchanges/interface.js +18 -3
  185. package/dist/exchanges/ioc-outcome.d.ts +44 -0
  186. package/dist/exchanges/ioc-outcome.js +57 -0
  187. package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
  188. package/dist/exchanges/legacy-trading-v2.js +940 -0
  189. package/dist/exchanges/lighter-public.d.ts +43 -0
  190. package/dist/exchanges/lighter-public.js +276 -0
  191. package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
  192. package/dist/exchanges/lighter-rh-deposit.js +185 -0
  193. package/dist/exchanges/lighter-signer.d.ts +123 -0
  194. package/dist/exchanges/lighter-signer.js +215 -0
  195. package/dist/exchanges/lighter-spot.d.ts +91 -13
  196. package/dist/exchanges/lighter-spot.js +595 -233
  197. package/dist/exchanges/lighter.d.ts +611 -39
  198. package/dist/exchanges/lighter.js +1890 -649
  199. package/dist/exchanges/ondo/adapter.d.ts +166 -0
  200. package/dist/exchanges/ondo/adapter.js +835 -0
  201. package/dist/exchanges/ondo/agent.d.ts +31 -0
  202. package/dist/exchanges/ondo/agent.js +101 -0
  203. package/dist/exchanges/ondo/client.d.ts +60 -0
  204. package/dist/exchanges/ondo/client.js +297 -0
  205. package/dist/exchanges/ondo/funds.d.ts +122 -0
  206. package/dist/exchanges/ondo/funds.js +290 -0
  207. package/dist/exchanges/ondo/public.d.ts +95 -0
  208. package/dist/exchanges/ondo/public.js +214 -0
  209. package/dist/exchanges/ondo/spot.d.ts +127 -0
  210. package/dist/exchanges/ondo/spot.js +392 -0
  211. package/dist/exchanges/outcome-interface.d.ts +140 -0
  212. package/dist/exchanges/outcome-interface.js +16 -0
  213. package/dist/exchanges/pacifica-spot.d.ts +83 -0
  214. package/dist/exchanges/pacifica-spot.js +324 -0
  215. package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
  216. package/dist/exchanges/pacifica-typed-data.js +57 -9
  217. package/dist/exchanges/pacifica.d.ts +288 -4
  218. package/dist/exchanges/pacifica.js +1013 -76
  219. package/dist/exchanges/precision.d.ts +47 -0
  220. package/dist/exchanges/precision.js +209 -0
  221. package/dist/exchanges/qfex/adapter.d.ts +341 -0
  222. package/dist/exchanges/qfex/adapter.js +1383 -0
  223. package/dist/exchanges/qfex/agent.d.ts +58 -0
  224. package/dist/exchanges/qfex/agent.js +188 -0
  225. package/dist/exchanges/qfex/client.d.ts +54 -0
  226. package/dist/exchanges/qfex/client.js +138 -0
  227. package/dist/exchanges/qfex/errors.d.ts +70 -0
  228. package/dist/exchanges/qfex/errors.js +236 -0
  229. package/dist/exchanges/qfex/public.d.ts +122 -0
  230. package/dist/exchanges/qfex/public.js +268 -0
  231. package/dist/exchanges/qfex/signing.d.ts +160 -0
  232. package/dist/exchanges/qfex/signing.js +283 -0
  233. package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
  234. package/dist/exchanges/qfex/ws-transport.js +798 -0
  235. package/dist/exchanges/rate-limiter.d.ts +12 -0
  236. package/dist/exchanges/rate-limiter.js +64 -0
  237. package/dist/exchanges/registry.d.ts +14 -2
  238. package/dist/exchanges/registry.js +20 -32
  239. package/dist/exchanges/slippage.d.ts +37 -0
  240. package/dist/exchanges/slippage.js +70 -0
  241. package/dist/exchanges/spot-factory.d.ts +166 -0
  242. package/dist/exchanges/spot-factory.js +431 -0
  243. package/dist/exchanges/spot-interface.d.ts +98 -4
  244. package/dist/exchanges/spot-interface.js +27 -1
  245. package/dist/exchanges/spot-result.d.ts +79 -0
  246. package/dist/exchanges/spot-result.js +273 -0
  247. package/dist/exchanges/trading-v2.d.ts +117 -0
  248. package/dist/exchanges/trading-v2.js +301 -0
  249. package/dist/exchanges/venue-types.d.ts +231 -0
  250. package/dist/exchanges/venue-types.js +87 -0
  251. package/dist/exchanges/venue.d.ts +211 -0
  252. package/dist/exchanges/venue.js +132 -0
  253. package/dist/exchanges/venues/arcus.d.ts +8 -0
  254. package/dist/exchanges/venues/arcus.js +109 -0
  255. package/dist/exchanges/venues/aster.d.ts +49 -0
  256. package/dist/exchanges/venues/aster.js +176 -0
  257. package/dist/exchanges/venues/create-helpers.d.ts +18 -0
  258. package/dist/exchanges/venues/create-helpers.js +199 -0
  259. package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
  260. package/dist/exchanges/venues/hyperliquid.js +126 -0
  261. package/dist/exchanges/venues/index.d.ts +16 -0
  262. package/dist/exchanges/venues/index.js +26 -0
  263. package/dist/exchanges/venues/lighter-family.d.ts +92 -0
  264. package/dist/exchanges/venues/lighter-family.js +166 -0
  265. package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
  266. package/dist/exchanges/venues/lighter-rh.js +58 -0
  267. package/dist/exchanges/venues/lighter.d.ts +6 -0
  268. package/dist/exchanges/venues/lighter.js +55 -0
  269. package/dist/exchanges/venues/ondo.d.ts +34 -0
  270. package/dist/exchanges/venues/ondo.js +140 -0
  271. package/dist/exchanges/venues/pacifica.d.ts +8 -0
  272. package/dist/exchanges/venues/pacifica.js +95 -0
  273. package/dist/exchanges/venues/qfex.d.ts +28 -0
  274. package/dist/exchanges/venues/qfex.js +132 -0
  275. package/dist/execution-log.js +10 -2
  276. package/dist/funding/legacy-public.d.ts +31 -0
  277. package/dist/funding/legacy-public.js +369 -0
  278. package/dist/funding/normalize.d.ts +28 -16
  279. package/dist/funding/normalize.js +61 -29
  280. package/dist/funding/rates.d.ts +46 -11
  281. package/dist/funding/rates.js +228 -277
  282. package/dist/guardrail/perp-guardrail.js +0 -0
  283. package/dist/index.js +156 -684
  284. package/dist/jobs.d.ts +84 -6
  285. package/dist/jobs.js +239 -41
  286. package/dist/landing.d.ts +7 -2
  287. package/dist/landing.js +19 -3
  288. package/dist/mcp-server.d.ts +14 -4
  289. package/dist/mcp-server.js +820 -402
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/pacifica/client.d.ts +41 -10
  293. package/dist/pacifica/client.js +167 -63
  294. package/dist/pacifica/constants.d.ts +0 -1
  295. package/dist/pacifica/constants.js +0 -5
  296. package/dist/pacifica/deposit.d.ts +30 -3
  297. package/dist/pacifica/deposit.js +119 -2
  298. package/dist/pacifica/faucet.d.ts +29 -0
  299. package/dist/pacifica/faucet.js +100 -0
  300. package/dist/pacifica/sign-tx.d.ts +15 -0
  301. package/dist/pacifica/sign-tx.js +41 -0
  302. package/dist/pacifica/types/account.d.ts +28 -4
  303. package/dist/pacifica/types/lake.d.ts +20 -5
  304. package/dist/pacifica/types/lake.js +4 -0
  305. package/dist/pacifica/types/market.d.ts +5 -0
  306. package/dist/pacifica/types/order.d.ts +4 -1
  307. package/dist/pacifica/types/position.d.ts +9 -1
  308. package/dist/plan-executor.d.ts +10 -5
  309. package/dist/plan-executor.js +165 -54
  310. package/dist/position-history.js +13 -0
  311. package/dist/rebalance.d.ts +26 -2
  312. package/dist/rebalance.js +86 -7
  313. package/dist/relay-swap.d.ts +232 -0
  314. package/dist/relay-swap.js +630 -0
  315. package/dist/risk.js +5 -1
  316. package/dist/secret-env.d.ts +15 -0
  317. package/dist/secret-env.js +36 -0
  318. package/dist/settings.d.ts +169 -6
  319. package/dist/settings.js +219 -38
  320. package/dist/signer/ows-evm.d.ts +13 -0
  321. package/dist/signer/ows-evm.js +22 -0
  322. package/dist/smart-order.d.ts +9 -0
  323. package/dist/smart-order.js +53 -8
  324. package/dist/strategies/dca.d.ts +6 -2
  325. package/dist/strategies/dca.js +66 -26
  326. package/dist/strategies/funding-arb.d.ts +11 -1
  327. package/dist/strategies/funding-arb.js +202 -89
  328. package/dist/strategies/grid.d.ts +31 -0
  329. package/dist/strategies/grid.js +89 -11
  330. package/dist/strategies/pending-stop.d.ts +8 -0
  331. package/dist/strategies/pending-stop.js +12 -0
  332. package/dist/strategies/split-order.d.ts +45 -17
  333. package/dist/strategies/split-order.js +157 -91
  334. package/dist/strategies/trailing-stop.d.ts +2 -1
  335. package/dist/strategies/trailing-stop.js +52 -6
  336. package/dist/strategies/twap.d.ts +6 -1
  337. package/dist/strategies/twap.js +39 -11
  338. package/dist/trade-validator.d.ts +22 -0
  339. package/dist/trade-validator.js +119 -15
  340. package/dist/utils/numeric.d.ts +46 -0
  341. package/dist/utils/numeric.js +79 -0
  342. package/dist/utils.d.ts +19 -0
  343. package/dist/utils.js +96 -19
  344. package/dist/withdraw-gate.d.ts +49 -0
  345. package/dist/withdraw-gate.js +131 -0
  346. package/package.json +15 -6
  347. package/skills/perp-cli/SKILL.md +142 -55
  348. package/skills/perp-cli/references/agent-operations.md +110 -56
  349. package/skills/perp-cli/references/commands.md +266 -71
  350. package/skills/perp-cli/references/strategies.md +34 -31
  351. package/skills/perp-cli/scripts/arb-monitor.sh +10 -5
  352. package/skills/perp-cli/scripts/funding-analysis.sh +14 -9
  353. package/skills/perp-cli/scripts/preflight.sh +21 -14
  354. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  355. package/skills/perp-cli/scripts/validate-arb.sh +31 -15
  356. package/vendor/lighter-signer/SHA256SUMS +2 -0
  357. package/vendor/lighter-signer/VERSION +8 -0
  358. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  359. package/vendor/lighter-signer/wasm_exec.js +575 -0
  360. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  361. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Builder fees — one uniform, default-OFF mechanism for every venue.
3
+ *
4
+ * A builder fee is an extra per-order fee the venue pays to a third-party "builder"
5
+ * (Hyperliquid builder codes, Pacifica builder program, Aster builder, Ondo builder
6
+ * codes). It is charged to the USER, so perp-cli never attaches one unless:
7
+ * 1. `settings.builder.<venue>` is set explicitly by the user (absent = off — there
8
+ * is no shipped default and no env var), and
9
+ * 2. the venue approval the fee needs is CONFIRMED (venue status read, cached per
10
+ * process for BUILDER_APPROVAL_TTL_MS; a builder-related order rejection drops the
11
+ * cached read). Approval-free venues (Ondo) are confirmed by (1) alone.
12
+ * A builder-related order rejection is returned to the caller with remediation
13
+ * (--no-builder / re-approve) and is NEVER retried — re-sending is a new order.
14
+ * An order whose approval is missing / unknown is sent WITHOUT the fee and says so in
15
+ * `execution.notes` (and once on stderr) — the fee is the builder's, never a reason to
16
+ * fail or silently change the user's order. `--no-builder` disables it per command.
17
+ *
18
+ * Per-venue facts (official docs, checked 2026-10-02):
19
+ * - hyperliquid https://hyperliquid.gitbook.io/hyperliquid-docs/trading/builder-codes
20
+ * ApproveBuilderFee signed by the MAIN wallet (an agent cannot sign it); order action
21
+ * `builder: { b: <address>, f: <tenths of a bp> }`; max 0.1% perps / 1% spot; status
22
+ * info `{ type: "maxBuilderFee", user, builder }`. HIP-3 dexes are not covered by the
23
+ * docs → never attached there.
24
+ * - pacifica https://pacifica.gitbook.io/docs/programs/builder-program.md
25
+ * approve_builder_code { builder_code, max_fee_rate } signed by the account; per-order
26
+ * `builder_code` on create_market / create / stop/create / positions/tpsl. TWAP
27
+ * (create_twap_order) does not document builder_code → never sent there.
28
+ * The builder sets its own fee_rate; the user's approved max_fee_rate caps it, so the
29
+ * code is attached only while the approved max_fee_rate ≤ the configured feeBps (the
30
+ * user's consent). The venue documents no ceiling → perp-cli caps the consent at 10
31
+ * bps. Spot rejects it (err 108).
32
+ * - aster https://asterdex.github.io/aster-api-website/asterCode/integration-flow/
33
+ * + asterdex/api-docs demo/aster-code.py: approveBuilder { builder, maxFeeRate ≤ 0.001 }
34
+ * master-signed; orders carry `builder` + `feeRate` (decimal) signed by the API wallet;
35
+ * list GET /fapi/v3/builder, revoke DELETE /fapi/v3/builder (DelBuilder). The testnet
36
+ * API documents no approveBuilder → testnet off.
37
+ * - ondo https://docs.ondoperps.xyz/api-reference/integration_guide (+ rest-spec)
38
+ * no user approval; per-order `builderCode: { code, feeRateBpsFractional }` on
39
+ * POST /v1/perps/orders and POST /v1/perps/stop_order (stops do NOT inherit it); cap
40
+ * 10 bps; sandbox uses the same API.
41
+ * - qfex https://docs.qfex.com/qfex/builder-code
42
+ * fee SHARE, not an extra fee: the trader pays the normal venue fee and the venue credits
43
+ * part of it to the builder (feeKind "share", feeBps null). No user approval. The code
44
+ * (UUID) rides on the Trade WebSocket auth (`params.builder_code`) for the life of the
45
+ * connection — never per order; an unknown code is not an error, so attribution is not
46
+ * confirmable from the trader side. Prod codes mean nothing on UAT → testnet off.
47
+ * - lighter / lighter-rh: unsupported — see UNSUPPORTED below.
48
+ * - arcus: unsupported — see UNSUPPORTED below.
49
+ */
50
+ import { PerpError, classifyError, isDefiniteOrderFailure, isTransportFailure, isUnknownOutcomeServerError } from "../errors.js";
51
+ import { isVenueId } from "./venue-types.js";
52
+ export const BUILDER_FEE_CAPABILITIES = {
53
+ hyperliquid: {
54
+ approval: "master", identity: "address", perpCapBps: 10, spotCapBps: 100, hip3: false, testnet: true,
55
+ feeStepBps: 0.1, feeKind: "exact", docs: "https://hyperliquid.gitbook.io/hyperliquid-docs/trading/builder-codes",
56
+ },
57
+ pacifica: {
58
+ approval: "master", identity: "code", perpCapBps: 10, spotCapBps: null, hip3: null, testnet: true,
59
+ feeStepBps: 0.01, feeKind: "max", docs: "https://pacifica.gitbook.io/docs/programs/builder-program.md",
60
+ },
61
+ aster: {
62
+ approval: "master", identity: "address", perpCapBps: 10, spotCapBps: null, hip3: null, testnet: false,
63
+ feeStepBps: 0.01, feeKind: "exact", docs: "https://asterdex.github.io/aster-api-website/asterCode/integration-flow/",
64
+ },
65
+ ondo: {
66
+ approval: "none", identity: "code", perpCapBps: 10, spotCapBps: null, hip3: null, testnet: true,
67
+ feeStepBps: 0.01, feeKind: "exact", docs: "https://docs.ondoperps.xyz/api-reference/integration_guide",
68
+ },
69
+ qfex: {
70
+ approval: "none", identity: "code", perpCapBps: null, spotCapBps: null, hip3: null, testnet: false,
71
+ feeStepBps: null, feeKind: "share", docs: "https://docs.qfex.com/qfex/builder-code",
72
+ },
73
+ lighter: null,
74
+ "lighter-rh": null,
75
+ arcus: null,
76
+ };
77
+ /** Why a venue has no builder-fee support (shown in CAPABILITY_UNSUPPORTED). */
78
+ export const BUILDER_FEE_UNSUPPORTED = {
79
+ lighter: "not yet implemented in perp-cli — the official lighter-go signer can sign ApproveIntegrator and the per-order integrator fields, but perp-cli sends them as 0 (https://apidocs.lighter.xyz/docs/partner-integration)",
80
+ "lighter-rh": "not yet implemented in perp-cli — Lighter RH uses the same official lighter-go signer; partner-attribution fields are sent as 0 (https://apidocs.rh.lighter.xyz/docs/partner-attribution.md)",
81
+ arcus: "Arcus has no perps builder fee; its integrator fee applies to the spot router only and needs a partner key from Arcus (https://docs.arcus.xyz/concepts/spot-router)",
82
+ };
83
+ export function builderFeeUnsupported(venue) {
84
+ return new PerpError("CAPABILITY_UNSUPPORTED", `${venue} does not support builderFee — ${BUILDER_FEE_UNSUPPORTED[venue] ?? "no builder-fee mechanism"}`, {
85
+ venue, feature: "builderFee", reason: BUILDER_FEE_UNSUPPORTED[venue] ?? "no builder-fee mechanism",
86
+ remediation: `Builder fees are available on: ${builderFeeVenues().join(", ")}`,
87
+ });
88
+ }
89
+ export function builderFeeVenues() {
90
+ return Object.keys(BUILDER_FEE_CAPABILITIES).filter(v => BUILDER_FEE_CAPABILITIES[v] !== null);
91
+ }
92
+ /** Capability of a venue, or CAPABILITY_UNSUPPORTED. */
93
+ export function requireBuilderCapability(venue) {
94
+ const cap = BUILDER_FEE_CAPABILITIES[venue];
95
+ if (cap === null)
96
+ throw builderFeeUnsupported(venue);
97
+ return cap;
98
+ }
99
+ /** The configured user-paid fee of an "exact" / "max" venue. A "share" target has none. */
100
+ export function requireFeeBps(t) {
101
+ if (t.feeBps === null)
102
+ throw new PerpError("INVALID_PARAMS", `${t.venue} builder attribution carries no fee (fee share)`);
103
+ return t.feeBps;
104
+ }
105
+ const ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
106
+ /** Pacifica: "alphanumeric, max 16 characters" (builder-program docs). */
107
+ const PACIFICA_CODE_RE = /^[A-Za-z0-9]{1,16}$/;
108
+ /** Ondo documents no format; refuse whitespace / control characters and absurd lengths. */
109
+ const ONDO_CODE_RE = /^[A-Za-z0-9_-]{1,64}$/;
110
+ /** QFEX builder codes are UUIDs (docs builder-code / authenticate). */
111
+ const QFEX_CODE_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
112
+ /**
113
+ * Validate one settings.builder.<venue> entry. Throws `invalid(problem)` — never clamps
114
+ * or rewrites a value. Returns the normalized target.
115
+ */
116
+ export function parseBuilderEntry(venue, raw, invalid) {
117
+ if (!isVenueId(venue))
118
+ throw invalid(`builder: unknown venue "${venue}"`);
119
+ const cap = BUILDER_FEE_CAPABILITIES[venue];
120
+ if (cap === null)
121
+ throw invalid(`builder.${venue}: ${venue} does not support builder fees — ${BUILDER_FEE_UNSUPPORTED[venue] ?? ""}`);
122
+ if (cap.feeKind === "share") {
123
+ // Fee share: the code only. A feeBps key is refused — there is no user-paid fee to configure.
124
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
125
+ throw invalid(`builder.${venue} must be { code: <uuid> } (fee share — no feeBps)`);
126
+ }
127
+ const s = raw;
128
+ for (const k of Object.keys(s)) {
129
+ if (k !== "code")
130
+ throw invalid(`builder.${venue}: unknown key "${k}" (expected code only — ${venue} builder codes share the venue fee, the user pays nothing extra)`);
131
+ }
132
+ if (typeof s.code !== "string" || !QFEX_CODE_RE.test(s.code)) {
133
+ throw invalid(`builder.${venue}.code must be a UUID (got ${JSON.stringify(s.code)})`);
134
+ }
135
+ return { venue, identity: s.code.toLowerCase(), feeBps: null };
136
+ }
137
+ if (cap.perpCapBps === null || cap.feeStepBps === null) {
138
+ throw invalid(`builder.${venue}: capability declares no fee cap / increment`);
139
+ }
140
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
141
+ throw invalid(`builder.${venue} must be { ${cap.identity}: <${cap.identity}>, feeBps: <number> }`);
142
+ }
143
+ const e = raw;
144
+ for (const k of Object.keys(e)) {
145
+ if (k !== cap.identity && k !== "feeBps")
146
+ throw invalid(`builder.${venue}: unknown key "${k}" (expected ${cap.identity}, feeBps)`);
147
+ }
148
+ const id = e[cap.identity];
149
+ let identity;
150
+ if (cap.identity === "address") {
151
+ if (typeof id !== "string" || !ADDRESS_RE.test(id))
152
+ throw invalid(`builder.${venue}.address must be a 0x-prefixed 20-byte EVM address (got ${JSON.stringify(id)})`);
153
+ identity = id.toLowerCase();
154
+ }
155
+ else {
156
+ const re = venue === "pacifica" ? PACIFICA_CODE_RE : ONDO_CODE_RE;
157
+ if (typeof id !== "string" || !re.test(id)) {
158
+ throw invalid(`builder.${venue}.code must match ${re.source} (got ${JSON.stringify(id)})`);
159
+ }
160
+ identity = id;
161
+ }
162
+ const fee = e.feeBps;
163
+ if (typeof fee !== "number" || !Number.isFinite(fee) || fee <= 0) {
164
+ throw invalid(`builder.${venue}.feeBps must be a number of basis points > 0 (got ${JSON.stringify(fee)})`);
165
+ }
166
+ if (fee > cap.perpCapBps) {
167
+ throw invalid(`builder.${venue}.feeBps ${fee} exceeds the ${venue} cap of ${cap.perpCapBps} bps`);
168
+ }
169
+ const steps = fee / cap.feeStepBps;
170
+ if (Math.abs(steps - Math.round(steps)) > 1e-9) {
171
+ throw invalid(`builder.${venue}.feeBps ${fee} is not a multiple of the venue's ${cap.feeStepBps} bps increment`);
172
+ }
173
+ return { venue, identity, feeBps: fee };
174
+ }
175
+ /** Validate the whole settings.builder object (absent / null = nothing configured). */
176
+ export function parseBuilderSettings(raw, invalid) {
177
+ const out = {};
178
+ if (raw === undefined || raw === null)
179
+ return out;
180
+ if (typeof raw !== "object" || Array.isArray(raw))
181
+ throw invalid("builder must be an object of { venue: { address|code, feeBps } }");
182
+ for (const [venue, entry] of Object.entries(raw)) {
183
+ const t = parseBuilderEntry(venue, entry, invalid);
184
+ const cap = BUILDER_FEE_CAPABILITIES[t.venue];
185
+ const stored = entry;
186
+ out[venue] = cap.feeKind === "share"
187
+ ? { code: stored.code }
188
+ : cap.identity === "address"
189
+ ? { address: stored.address, feeBps: t.feeBps }
190
+ : { code: stored.code, feeBps: t.feeBps };
191
+ }
192
+ return out;
193
+ }
194
+ /** "2.5 bps", or "fee share (no extra fee)" for a share target. */
195
+ export function feeText(t) {
196
+ return t.feeBps === null ? "fee share (no extra fee)" : `${t.feeBps} bps`;
197
+ }
198
+ /** The configured target for a venue, or null (off). */
199
+ export function builderTargetOf(builder, venue) {
200
+ const entry = builder[venue];
201
+ if (entry === undefined)
202
+ return null;
203
+ return parseBuilderEntry(venue, entry, p => new PerpError("INVALID_PARAMS", p));
204
+ }
205
+ export const NO_BUILDER = { applied: null, note: null };
206
+ /** How long a venue approval read is reused before it is read again. */
207
+ export const BUILDER_APPROVAL_TTL_MS = 5 * 60_000;
208
+ /** Process-wide approval cache: `${venue}:${network}:${account}:${identity}` → status read + read time. */
209
+ const _approvalCache = new Map();
210
+ const _warned = new Set();
211
+ /** Forget cached approval reads and stderr warnings (after approve / revoke; test hook). */
212
+ export function resetBuilderApprovalCache() {
213
+ _approvalCache.clear();
214
+ _warned.clear();
215
+ }
216
+ /** Drop the cached approval reads of one venue + network (the next order re-reads the venue). */
217
+ export function invalidateBuilderApproval(venue, network) {
218
+ const prefix = `${venue}:${network}:`;
219
+ for (const k of [..._approvalCache.keys()])
220
+ if (k.startsWith(prefix))
221
+ _approvalCache.delete(k);
222
+ }
223
+ /**
224
+ * Per-adapter builder-fee gate. Built only when settings.builder.<venue> is set
225
+ * (resolveBuilderGate) — an unconfigured venue has no gate and no notes at all.
226
+ */
227
+ export class BuilderFeeGate {
228
+ target;
229
+ network;
230
+ skipReason;
231
+ readApproval;
232
+ accountOf;
233
+ constructor(target, network,
234
+ /** Static reason the configured fee is never attached by this adapter (--no-builder, testnet). */
235
+ skipReason,
236
+ /** Reads the venue approval for the active account; null when the venue has no approval step. */
237
+ readApproval,
238
+ /** Account the approval belongs to (master address) — part of the cache key. */
239
+ accountOf) {
240
+ this.target = target;
241
+ this.network = network;
242
+ this.skipReason = skipReason;
243
+ this.readApproval = readApproval;
244
+ this.accountOf = accountOf;
245
+ }
246
+ get capability() {
247
+ return requireBuilderCapability(this.target.venue);
248
+ }
249
+ /** Venue approval for the configured builder (cached for BUILDER_APPROVAL_TTL_MS). Failures → "unknown". */
250
+ async approval() {
251
+ if (this.readApproval === null)
252
+ return { status: "approved", maxFeeBps: null };
253
+ const key = `${this.target.venue}:${this.network}:${this.accountOf().toLowerCase()}:${this.target.identity}`;
254
+ const cached = _approvalCache.get(key);
255
+ if (cached !== undefined && Date.now() - cached.at < BUILDER_APPROVAL_TTL_MS)
256
+ return cached.read;
257
+ const read = this.readApproval(this.target).then(a => a,
258
+ // Recorded, not hidden: the decision carries the reason and the order goes out without the fee.
259
+ (e) => ({ status: "unknown", reason: e instanceof Error ? e.message : String(e) }));
260
+ _approvalCache.set(key, { read, at: Date.now() });
261
+ return read;
262
+ }
263
+ /**
264
+ * Whether the next perp order carries the fee. `hip3` = the order is on a Hyperliquid
265
+ * HIP-3 dex. Never throws: a fee that cannot be attached becomes a note.
266
+ */
267
+ async decide(opts) {
268
+ const t = this.target;
269
+ const cap = this.capability;
270
+ const skip = (why) => this.warnOnce({ applied: null, note: `builder fee not attached: ${why}` });
271
+ if (this.skipReason !== null)
272
+ return { applied: null, note: `builder fee not attached: ${this.skipReason}` };
273
+ if (opts.hip3 && cap.hip3 === false)
274
+ return skip("HIP-3 dex orders do not carry builder fees (not covered by the venue docs)");
275
+ const a = await this.approval();
276
+ if (a.status === "unknown") {
277
+ return skip(`${t.venue} approval for ${t.identity} could not be confirmed (${a.reason}) — check: perp${this.network === "testnet" ? " -n testnet" : ""} wallet builder status ${t.venue}`);
278
+ }
279
+ if (a.status === "notApproved") {
280
+ const have = a.maxFeeBps === null ? "none" : `${a.maxFeeBps} bps`;
281
+ return skip(`${t.venue} approval for ${t.identity} missing (approved max: ${have}, configured: ${feeText(t)}) — approve with: perp${this.network === "testnet" ? " -n testnet" : ""} wallet builder approve ${t.venue}`);
282
+ }
283
+ if (a.status === "aboveConsent") {
284
+ return skip(`${t.venue} approved max for ${t.identity} is ${a.maxFeeBps} bps, above the configured ${feeText(t)} — the builder could charge more than you configured; re-approve exactly ${feeText(t)}: perp${this.network === "testnet" ? " -n testnet" : ""} wallet builder approve ${t.venue}`);
285
+ }
286
+ return { applied: { venue: t.venue, identity: t.identity, feeBps: t.feeBps, feeKind: cap.feeKind, approvedMaxBps: a.maxFeeBps }, note: null };
287
+ }
288
+ warnOnce(d) {
289
+ const key = `${this.target.venue}:${this.network}:${d.note}`;
290
+ if (d.note !== null && !_warned.has(key)) {
291
+ _warned.add(key);
292
+ process.stderr.write(`[warn] ${d.note}\n`);
293
+ }
294
+ return d;
295
+ }
296
+ }
297
+ /**
298
+ * The gate for an adapter being created, or null when settings.builder.<venue> is not
299
+ * set (default) or the context is read-only. The single place builder settings are
300
+ * resolved (adapters never read settings).
301
+ */
302
+ export function resolveBuilderGate(venue, ctx, builder, readApproval, accountOf) {
303
+ if (ctx.readOnly)
304
+ return null;
305
+ const target = builderTargetOf(builder, venue);
306
+ if (target === null)
307
+ return null;
308
+ const cap = requireBuilderCapability(venue);
309
+ let skipReason = null;
310
+ if (ctx.noBuilder)
311
+ skipReason = "disabled by --no-builder";
312
+ else if (ctx.network === "testnet" && !cap.testnet)
313
+ skipReason = `${venue} testnet has no builder-fee flow`;
314
+ return new BuilderFeeGate(target, ctx.network, skipReason, cap.approval === "none" ? null : readApproval, accountOf);
315
+ }
316
+ /** Gate held by an adapter (public `builderFee` field), or null. */
317
+ export function builderGateOf(adapter) {
318
+ const g = adapter?.builderFee;
319
+ return g instanceof BuilderFeeGate ? g : null;
320
+ }
321
+ /** Decision for an adapter's next perp order (no gate = not configured = NO_BUILDER). */
322
+ export async function builderDecisionOf(adapter, opts) {
323
+ const g = builderGateOf(adapter);
324
+ return g === null ? NO_BUILDER : g.decide(opts);
325
+ }
326
+ // ── Unit conversions (one place) ──
327
+ /** Fee rate as a decimal fraction string: 2.5 bps → "0.00025". */
328
+ export function bpsToRate(bps) {
329
+ const s = (bps / 10_000).toFixed(10).replace(/0+$/, "").replace(/\.$/, "");
330
+ return s === "" ? "0" : s;
331
+ }
332
+ /** Decimal fraction (string | number) → bps, or null when unreadable. */
333
+ export function rateToBps(rate) {
334
+ const n = typeof rate === "number" ? rate : typeof rate === "string" && rate.trim() !== "" ? Number(rate) : NaN;
335
+ return Number.isFinite(n) && n >= 0 ? Math.round(n * 10_000 * 1e6) / 1e6 : null;
336
+ }
337
+ /** Hyperliquid `f` (tenths of a basis point). */
338
+ export function hlFeeTenthsBps(bps) {
339
+ return Math.round(bps * 10);
340
+ }
341
+ /** Hyperliquid ApproveBuilderFee maxFeeRate: a percent string ("0.01%" = 1 bp). */
342
+ export function hlMaxFeeRatePercent(bps) {
343
+ return `${bpsToRate(bps * 100)}%`;
344
+ }
345
+ /** feeKind "exact": approval verdict from the venue's approved ceiling (bps; null = no approval on record). */
346
+ export function approvalFromMax(maxFeeBps, feeBps) {
347
+ // 1e-9 absorbs decimal-string → float conversion; never rounds a smaller approval up.
348
+ if (maxFeeBps !== null && maxFeeBps + 1e-9 >= feeBps)
349
+ return { status: "approved", maxFeeBps };
350
+ return { status: "notApproved", maxFeeBps };
351
+ }
352
+ /**
353
+ * feeKind "max" (Pacifica): the builder picks its own rate up to the approved ceiling, so the
354
+ * configured feeBps is the user's consent and the approved ceiling must not exceed it.
355
+ * null = no approval on record.
356
+ */
357
+ export function approvalFromCeiling(maxFeeBps, feeBps) {
358
+ if (maxFeeBps === null)
359
+ return { status: "notApproved", maxFeeBps: null };
360
+ if (maxFeeBps - 1e-9 <= feeBps)
361
+ return { status: "approved", maxFeeBps };
362
+ return { status: "aboveConsent", maxFeeBps };
363
+ }
364
+ /**
365
+ * An order the venue rejected for a builder-related reason (the error text names the builder):
366
+ * the cached approval of that venue is dropped (the next order re-reads it) and the rejection is
367
+ * returned with remediation. The order is NOT retried — re-sending is a new order, so that stays
368
+ * the user's explicit choice. Any other error (or no gate) is returned unchanged.
369
+ */
370
+ export function builderRejectionOf(adapter, err) {
371
+ const g = builderGateOf(adapter);
372
+ if (g === null)
373
+ return err;
374
+ const msg = err instanceof Error ? err.message : String(err);
375
+ if (!/builder/i.test(msg))
376
+ return err;
377
+ // Only a DEFINITE venue rejection is re-described: an unknown outcome (ORDER_PENDING /
378
+ // ORDER_PARTIAL, a transport failure, a 5xx, any non-definite PerpError) keeps its own
379
+ // "do NOT resubmit" remediation — "re-send it without the fee" could double the order.
380
+ if (!isDefiniteBuilderRejection(err))
381
+ return err;
382
+ const venue = g.target.venue;
383
+ invalidateBuilderApproval(venue, g.network);
384
+ const n = g.network === "testnet" ? " -n testnet" : "";
385
+ const remediation = `The venue rejected the order over the builder fee; it was NOT retried. Re-send it without the fee (--no-builder), or re-approve: perp${n} wallet builder approve ${venue} (check: perp${n} wallet builder status ${venue})`;
386
+ const e = classifyError(err, venue);
387
+ return new PerpError(e.code, e.message, { ...(e.details ?? {}), exchange: venue, remediation });
388
+ }
389
+ /**
390
+ * true when `err` proves the order was refused (nothing placed): a PerpError with a definite code
391
+ * (isDefiniteOrderFailure), or a venue 4xx answer (details.httpStatus 4xx, not 429) whose code is
392
+ * not an unknown-outcome code; a plain Error that is neither a transport failure nor a 5xx
393
+ * unknown-outcome message (adapter-level venue refusal text).
394
+ */
395
+ function isDefiniteBuilderRejection(err) {
396
+ if (err instanceof PerpError) {
397
+ const code = err.structured.code;
398
+ if (code === "ORDER_PENDING" || code === "ORDER_PARTIAL")
399
+ return false;
400
+ if (isDefiniteOrderFailure(err))
401
+ return true;
402
+ const http = err.structured.details?.httpStatus;
403
+ return typeof http === "number" && http >= 400 && http < 500 && http !== 429;
404
+ }
405
+ return !isTransportFailure(err) && !isUnknownOutcomeServerError(err);
406
+ }
407
+ /**
408
+ * Builder decision for a planned perp order on `symbol` (dry-run / preview): the same
409
+ * cached gate decision a live order would use. Hyperliquid resolves the asset id so a
410
+ * HIP-3 market reports "not attached".
411
+ */
412
+ export async function builderPlanFor(adapter, symbol) {
413
+ if (builderGateOf(adapter) === null)
414
+ return NO_BUILDER;
415
+ const hl = adapter;
416
+ if (hl.name === "hyperliquid" && typeof hl.builderDecision === "function" && typeof hl.getAssetIndex === "function") {
417
+ const idx = symbol === null ? null : await hl.getAssetIndex.call(adapter, symbol.toUpperCase());
418
+ return hl.builderDecision.call(adapter, idx);
419
+ }
420
+ return builderDecisionOf(adapter, { hip3: false });
421
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Cross-venue canonical symbols — the single mapping source used to decide
3
+ * that two markets on different venues are the same underlying
4
+ * (design §4 L4, §10.9).
5
+ *
6
+ * Rules by asset class:
7
+ * - crypto: ticker identity after the adapter strips its quote suffix, plus
8
+ * DENOMINATION_ALIASES (kPEPE ≡ 1000PEPE, UBTC ≡ BTC ...).
9
+ * - equity / etf: exchange-listed ticker identity (TSLA on Ondo ≡ TSLA on
10
+ * Arcus ≡ TSLA on Lighter RH ≡ xyz:TSLA on HIP-3). Adapters must
11
+ * pass the plain ticker (e.g. strip Ondo's "-USD.P").
12
+ * - index / commodity / fx / preIpo: EXPLICIT table only. Same-looking names
13
+ * are frequently different products (US500 vs USA500, USOIL vs CL),
14
+ * so an unmapped market returns null and is excluded from matching.
15
+ */
16
+ import type { AssetClass, VenueId } from "./venue-types.js";
17
+ /** Denomination / wrapper aliases → canonical crypto ticker. */
18
+ export declare const DENOMINATION_ALIASES: Readonly<Record<string, string>>;
19
+ /**
20
+ * Explicit mapping for index / commodity / fx / preIpo markets.
21
+ * Key: `${venue}|${symbol}` (symbol as the adapter reports it; for HIP-3 use `hl:<dex>` as venue).
22
+ */
23
+ export declare const EXPLICIT_CANONICAL: Readonly<Record<string, string>>;
24
+ /** Pairs verified to be DIFFERENT products despite similar names. Never match. */
25
+ export declare const NEVER_MATCH: ReadonlySet<string>;
26
+ export declare function isNeverMatch(a: string, b: string): boolean;
27
+ /**
28
+ * Canonical key for a market, or null when it must not be matched across venues.
29
+ * @param venueKey venue id, or `hl:<dex>` for HIP-3 dex markets
30
+ * @param symbol plain symbol as reported by the adapter (no quote suffix, no dex prefix)
31
+ */
32
+ export declare function canonicalFor(venueKey: VenueId | `hl:${string}`, symbol: string, assetClass: AssetClass): string | null;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Cross-venue canonical symbols — the single mapping source used to decide
3
+ * that two markets on different venues are the same underlying
4
+ * (design §4 L4, §10.9).
5
+ *
6
+ * Rules by asset class:
7
+ * - crypto: ticker identity after the adapter strips its quote suffix, plus
8
+ * DENOMINATION_ALIASES (kPEPE ≡ 1000PEPE, UBTC ≡ BTC ...).
9
+ * - equity / etf: exchange-listed ticker identity (TSLA on Ondo ≡ TSLA on
10
+ * Arcus ≡ TSLA on Lighter RH ≡ xyz:TSLA on HIP-3). Adapters must
11
+ * pass the plain ticker (e.g. strip Ondo's "-USD.P").
12
+ * - index / commodity / fx / preIpo: EXPLICIT table only. Same-looking names
13
+ * are frequently different products (US500 vs USA500, USOIL vs CL),
14
+ * so an unmapped market returns null and is excluded from matching.
15
+ */
16
+ /** Denomination / wrapper aliases → canonical crypto ticker. */
17
+ export const DENOMINATION_ALIASES = {
18
+ KPEPE: "1000PEPE",
19
+ "1000PEPE": "1000PEPE",
20
+ KSHIB: "1000SHIB",
21
+ KBONK: "1000BONK",
22
+ KFLOKI: "1000FLOKI",
23
+ KLUNC: "1000LUNC",
24
+ KNEIRO: "1000NEIRO",
25
+ KDOGS: "1000DOGS",
26
+ // Hyperliquid spot wrapper tokens (was SPOT_PERP_TOKEN_MAP)
27
+ UBTC: "BTC",
28
+ UETH: "ETH",
29
+ USOL: "SOL",
30
+ UFART: "FARTCOIN",
31
+ UPUMP: "PUMP",
32
+ LINK0: "LINK",
33
+ AVAX0: "AVAX",
34
+ AAVE0: "AAVE",
35
+ };
36
+ /**
37
+ * Explicit mapping for index / commodity / fx / preIpo markets.
38
+ * Key: `${venue}|${symbol}` (symbol as the adapter reports it; for HIP-3 use `hl:<dex>` as venue).
39
+ */
40
+ export const EXPLICIT_CANONICAL = {
41
+ // Gold / silver spot-referenced perps
42
+ "ondo|XAU": "GOLD",
43
+ "ondo|XAG": "SILVER",
44
+ "lighter-rh|XAU": "GOLD",
45
+ "lighter-rh|XAG": "SILVER",
46
+ "hl:xyz|GOLD": "GOLD",
47
+ "hl:xyz|SILVER": "SILVER",
48
+ "qfex|GOLD": "GOLD",
49
+ "qfex|SILVER": "SILVER",
50
+ // Crude oil (WTI)
51
+ "ondo|WTI": "CRUDE_OIL_WTI",
52
+ "hl:xyz|CL": "CRUDE_OIL_WTI",
53
+ "hl:flx|OIL": "CRUDE_OIL_WTI",
54
+ // QFEX CL blends expiring futures; roll-week gaps are caught by the cross-venue price filter.
55
+ "qfex|CL": "CRUDE_OIL_WTI",
56
+ // US equity indices
57
+ "ondo|US500": "SPX",
58
+ "ondo|US100": "NDX",
59
+ "qfex|US500": "SPX",
60
+ "qfex|US100": "NDX",
61
+ // FX
62
+ "ondo|USDJPY": "USDJPY",
63
+ };
64
+ /** Pairs verified to be DIFFERENT products despite similar names. Never match. */
65
+ export const NEVER_MATCH = new Set([
66
+ "USAR:US500", "USAR:USA500", "US500:USA500",
67
+ "SEMI:SEMIS",
68
+ "USOIL:CL", "USOIL:OIL",
69
+ "GOLDJM:GOLD", "GLDMINE:GOLD",
70
+ "SILVERJM:SILVER",
71
+ "URNM:URANIUM",
72
+ "NUCLEAR:ENERGY", "NUCLEAR:USENERGY",
73
+ ]);
74
+ export function isNeverMatch(a, b) {
75
+ return NEVER_MATCH.has(`${a}:${b}`) || NEVER_MATCH.has(`${b}:${a}`);
76
+ }
77
+ /**
78
+ * Canonical key for a market, or null when it must not be matched across venues.
79
+ * @param venueKey venue id, or `hl:<dex>` for HIP-3 dex markets
80
+ * @param symbol plain symbol as reported by the adapter (no quote suffix, no dex prefix)
81
+ */
82
+ export function canonicalFor(venueKey, symbol, assetClass) {
83
+ const up = symbol.toUpperCase();
84
+ switch (assetClass) {
85
+ case "crypto":
86
+ return DENOMINATION_ALIASES[up] ?? up; // ticker identity is the rule for crypto; the alias table overrides it
87
+ case "equity":
88
+ case "etf":
89
+ return up;
90
+ case "index":
91
+ case "commodity":
92
+ case "fx":
93
+ case "preIpo":
94
+ return EXPLICIT_CANONICAL[`${venueKey}|${up}`] ?? null;
95
+ }
96
+ }
@@ -56,7 +56,8 @@ export interface EvmAddressCapable {
56
56
  readonly address: string;
57
57
  }
58
58
  export interface QueryOrderCapable {
59
- queryOrder(orderId: number): Promise<unknown>;
59
+ /** Order id (u64) or, on Hyperliquid, a client order id (cloid) string. */
60
+ queryOrder(orderId: number | string): Promise<unknown>;
60
61
  }
61
62
  export interface UsdTransferCapable {
62
63
  usdTransfer(amount: number, destination: string): Promise<unknown>;
@@ -72,3 +73,8 @@ export declare function hasLighterAccount(adapter: ExchangeAdapter): adapter is
72
73
  export declare function hasEvmAddress(adapter: ExchangeAdapter): adapter is ExchangeAdapter & EvmAddressCapable;
73
74
  export declare function isQueryOrderCapable(adapter: ExchangeAdapter): adapter is ExchangeAdapter & QueryOrderCapable;
74
75
  export declare function isUsdTransferCapable(adapter: ExchangeAdapter): adapter is ExchangeAdapter & UsdTransferCapable;
76
+ /**
77
+ * true when the venue's kline endpoint needs account credentials (Ondo GET /v1/perps/candles):
78
+ * `market kline` then uses the credentialed adapter instead of the keyless public one.
79
+ */
80
+ export declare function klinesNeedAuth(adapter: ExchangeAdapter): boolean;
@@ -42,3 +42,10 @@ export function isQueryOrderCapable(adapter) {
42
42
  export function isUsdTransferCapable(adapter) {
43
43
  return typeof adapter.usdTransfer === "function";
44
44
  }
45
+ /**
46
+ * true when the venue's kline endpoint needs account credentials (Ondo GET /v1/perps/candles):
47
+ * `market kline` then uses the credentialed adapter instead of the keyless public one.
48
+ */
49
+ export function klinesNeedAuth(adapter) {
50
+ return adapter.klinesRequireAuth === true;
51
+ }