fix(perps): order 0: insufficient margin to place order - #9694
Conversation
Market buys are sent to HyperLiquid as limit orders priced at market * (1 + slippage), and the exchange charges initial margin against that submitted price. getMaxAllowedAmount sized the max off the market price with only a 0.5% buffer, so a max-size market buy asked for ~3% more margin than the account had and was rejected with 'order 0: insufficient margin to place order'. Apply the slippage haircut to the max for market buys, with optional orderType/isBuy/maxSlippageBps params so sells and limit orders are unaffected.
Automated fix-bug run — TAT-3344
Worker reportTAT-3344 — [Core] order 0: insufficient margin to place orderBranch: SummaryMax-size ("Max" / 100% slider) market buy orders were structurally guaranteed to be Root cause
Arithmetic for a max market buy at default slippage: At the 1000 bps slippage setting it is ~1.09 × spendableBalance. Confirmed the client is the caller of the broken calc: Aggravating factor (not fixed here, see Not addressed): the main-DEX order path has no Changes
The new params default to a market buy at default slippage — the conservative case — so Test results
Not addressed (follow-ups worth filing)These are separate contributors to the same error message; each needs its own change and was
|
Sizes a max market buy through getMaxAllowedAmount against live account state and submits that exact amount, so the max-sizing path is covered by a real order rather than a fixed notional. mode=prefix recomputes the pre-fix formula from the same inputs as a counterfactual. Testnet-guarded with deterministic cleanup.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0ed32d9. Configure here.
The live reproduction harness is task-local validation tooling, not package source. Kept out of the shipped diff; the two commits above retain it in branch history.
HyperLiquid reserves initial margin for a resting order against the price that order is submitted at, not the market price its size was derived from. A max-size limit order resting above the market price - typically a sell - therefore reserved more margin than the account had and the venue refused it with "order 0: insufficient margin to place order". getMaxAllowedAmount now takes optional orderType and limitPrice and scales the maximum by limitPrice / marketPrice when the order rests above the market. Orders at or below the market price, and market orders, are unchanged. Reproduced live on HyperLiquid testnet before the change and verified accepted after it.
…er-0-insufficient-marg # Conflicts: # packages/perps-controller/CHANGELOG.md # packages/perps-controller/tests/src/utils/orderCalculations.test.ts
…er-0-insufficient-marg # Conflicts: # packages/perps-controller/CHANGELOG.md

Summary
A max-size ("Max" / 100% slider) limit order resting above the market price — in
practice a sell — was refused by HyperLiquid with
order 0: insufficient margin to place order.getMaxAllowedAmountderived the maximum from the market (mid) price, but HyperLiquidreserves initial margin for a resting order against the price that order is submitted
at. An order resting above mid therefore reserves more margin than a mid-priced notional
budgets for, and the venue refuses it.
Root cause
packages/perps-controller/src/utils/orderCalculations.ts—getMaxAllowedAmountcomputed
theoreticalMax = spendableBalance * leveragefrom the market price only.calculateFinalPositionSize:size = usdAmount / currentPrice) and submits it at the limit price.size * limitPrice / leverage. With size budgeted at mid,that is
spendableBalance * (limitPrice / mid)— above the balance whenever the orderrests above mid.
Measured live on testnet:
spendableBalance = 771.21, mid64227, max2301→ size0.03584resting at+5%(67438.35) → venue reserves 805.51 against 771.21available → refused.
A resting buy sits below mid and reserves less, so it cannot trip this. A market
order is charged at the fill price, not the padded price it is submitted with, so it is
unaffected — see Validation for the evidence.
Changes
src/utils/orderCalculations.tsgetMaxAllowedAmountscales the maximum bylimitPrice / marketPricewhen a limit order rests above the market price. New optionalorderTypeandlimitPriceparams; the post-rounding margin check uses the same ratio.tests/src/utils/orderCalculations.test.tsCHANGELOG.mdBoth new params are optional and default to the previous behaviour, so existing callers are
unaffected until they pass a limit price.
Validation
Live reproduction, then verification — HyperLiquid testnet
The bug was reproduced live before claiming a fix, using a task-local recipe that sizes
the order through
getMaxAllowedAmountfrom live account state and submits it as a realorder. Same recipe, same market, same parameters, before and after:
getMaxAllowedAmount0.035840.03366order 0: Insufficient margin to place order. asset=3The red run asserted the exact venue error by code and that no position or resting order was
left behind; the green run asserted a live resting order. Both ended with deterministic
cleanup back to flat.
Harness: mm-harness 0.24.0 (source
4d0dd235be723334eb22fc045dd356eda10797f8), perps recipelibrary
421fd1576616b66a5f7dda3cacc27ba3abd9fabb. The reproduction harness is task-localvalidation tooling and is deliberately not part of this diff.
Why the market-order path is not changed
Four attempts to refuse a max-size market buy — all accepted, so no fix is warranted
there:
For a marketable order the venue charges margin at the fill/mark price and caps the fill
rather than refusing.
Checks
tests/src/utils/orderCalculations.test.tsyarn workspace @metamask/perps-controller test(suite + coverage thresholds)yarn build(monorepo)yarn lint:tsc(monorepo typecheck)yarn eslint,oxfmt --checkon changed filesyarn changelog:validate,yarn constraints,yarn lint:teamsNot addressed (follow-ups worth filing)
margin were reusable. Not reproducible on the market path (see above), but untested for
resting orders.
placeOrderfor the main DEX — the venue rejection isstill the first and only check.
#errorMappingsdoes not map"insufficient margin to place order", soPERPS_ERROR_CODES.INSUFFICIENT_MARGINis never produced — no user-facingtranslation and no downsize/retry path.
hip3Enabled: false,availableHip3Dexs: 0.Note
Low Risk
Localized change to max-order sizing math with optional params and regression tests; no auth or payment paths touched.
Overview
Fixes max-size limit orders (e.g. 100% slider) that HyperLiquid rejected with
order 0: insufficient margin to place orderwhen the order rests above mid—common on sells.getMaxAllowedAmountnow takes optionalorderTypeandlimitPrice. For limit orders withlimitPrice > marketPrice, the max USD notional is divided bylimitPrice / marketPriceso reserved margin at the submitted price stays within spendable balance; the post-rounding margin check uses the same ratio. Market orders and limits at or below mid behave as before when callers omit the new params.Adds
getMaxAllowedAmountunit tests (including TAT-3344 regression) and an Unreleased → Fixed changelog entry.Reviewed by Cursor Bugbot for commit dc62582. Bugbot is set up for automated code reviews on this repo. Configure here.