Burst

Burst orders sweep the book through small IOC waves with a price ladder and hard slippage cap. Full reference: architecture, cloid scheme, resume, edge cases.

A burst order is a smarter market order. Instead of sending one big IOC limit and accepting whatever the book offers up to a wide slippage cap, it sweeps the book through a series of small IOC limit "children" — each priced just slightly above the current best — with a hard cap on total slippage from the initial best.

The goal: get the immediate-execution behaviour of a Market order, but with bounded slippage that aborts the operation if the market runs away, and with adaptive sizing that takes advantage of book replenishment between waves.

When to Use

  • You want immediate execution but the book on this market is thin or moves fast.
  • You suspect a market order would sweep through bad liquidity.
  • You want a hard slippage ceiling: if the cap is breached, the burst aborts and reports a partial fill rather than burning through worse and worse prices.

When Not to Use

  • The book is deep and a Market order would fill at one level. Burst's loop overhead is unnecessary.
  • You're a maker — Burst is always a taker. Use Limit ALO or TWAP Limit ALO.
  • You want to stage by time. Use TWAP.
  • You want to distribute by price range. Use Scale.

Architecture

Burst is client-side. Hyperliquid has no burst order type. Borsa runs the loop in your browser, kicks it off when you submit, and persists progress to localStorage so a page reload resumes where it left off.

[BurstParams]
     ↓
[start] — subscribe L2 book, snapshot initial_best, write state
     ↓
[runLoop]
     ↓
  ┌──[ check abort: cancelled? cap? timeout? children exhausted? zero-fill streak? ]
  │       ↓ no
  │   [ read current_best from L2 book cache ]
  │   [ build wave: ladder prices + adaptive size + clamp to remaining ]
  │   [ batch POST as one signed `order` action ]
  │   [ parse statuses, update children fills + filled qty + avg price ]
  │   [ adjust prev-wave stats for next sizing decision ]
  │   [ sleep delay_ms ]
  └─── loop
     ↓
[finish] — toast result, unsubscribe book

Wire Format

Each wave is one signed order action with N children, where N is the configured wave size (Batch size, default 100, auto-clamped so each sub-order is at least $10):

{
  type: "order",
  orders: [
    {
      a: <asset>,
      b: <isBuy>,
      p: <ladder_price_i>,
      s: <adaptive_size_i>,
      r: <reduceOnly>,
      t: { limit: { tif: "Ioc" } },
      c: <deriveChildCloid(burstId, child_idx)>
    },
    ... wave_size copies, child_idx increasing
  ],
  grouping: "na"
}

Every child is tif: "Ioc" so nothing rests. Each carries a deterministic cloid (see below). The grouping is "na" because the children are independent — Hyperliquid does not see them as one logical order.

Cloid Scheme

Each child's cloid is derived deterministically from the burst's UUID and the child's index:

data = utf8(`${burstId}:${childIdx}`)
hash = SHA-256(data)
cloid = "0x" + hash.slice(0, 16).map(hex).join("")

16 bytes, 32 hex chars, 0x-prefixed — Hyperliquid's standard cloid shape.

Why deterministic: if your browser crashes mid-burst and you reload, the executor can recompute all possible cloids for any child_idx, fetch userFillsByTime since startedAt, and reconcile fills against children by matching oid (recorded at send time) — without persisting a random list of cloids. The deterministic scheme is the canonical reference; the persisted state is a cache for performance.

Price Ladder

The limit price for child i is the current best, shifted by a per-index step:

step_i = slippageCap × ((i + consecutiveZeroWaves) / (childCountMax − 1))^0.7
target = currentBest × (1 + sign × step_i)
sign = +1 for buy, −1 for sell

The ^0.7 exponent makes the curve concave: early children pay near-zero step (so they get hit by the inside of the book first), later children pay closer to the cap. The consecutiveZeroWaves term widens the step faster after each wave that didn't fill anything, so a stalled burst escalates rather than spinning at the same uncompetitive price.

Cap is enforced separately: if currentBest itself has moved past initialBest × (1 + slippageCap) for a buy (or below × (1 − slippageCap) for a sell), the burst aborts immediately, regardless of where the ladder would have placed the next child.

Batch Sizing

Each batch is sized by a deterministic split, not adaptive heuristics. Two constraints govern per-child size: a hard $10 minimum (Hyperliquid's enforced floor) and a smart book-aware maximum (so individual sub-orders don't sweep deep into thin liquidity).

The naive split

If the smart max doesn't bind, Borsa splits the remaining qty evenly across as many sub-orders as $10 minimum allows, capped by your batch size:

remainingNotional = remaining_qty × currentBest
naiveCount = min(batchSize, floor(remainingNotional / $10))
naivePerChild = remaining_qty / naiveCount

Examples with batchSize = 100:

Remaining notionalnaiveCountPer-child notional
$9
0 — dust, batch skipped
n/a (burst completes)
$33
3
$11
$25
2
$12.50
$1,000
100
$10
$10,000
100
$100
$100,000
100
$1,000

The last child takes any leftover from formatSize truncation, so the sum of children matches remaining exactly (no dust left behind).

The smart max

For each iteration, Borsa walks the L2 book on the taker side and sums available qty within a fraction of your slippage cap:

slipFraction = max(0.00005, slippageCap / 10)
capPx_buy   = currentBest × (1 + slipFraction)
capPx_sell  = currentBest × (1 − slipFraction)
maxPerChildQty = sum(level.sz for level in book up to capPx)

The /10 factor means each individual sub-order is allowed to walk up to one-tenth of your overall slippage budget. Ten sub-orders' worth of book depth fits within the cap before the burst would even need to widen via waveOffset.

When the max binds

If naivePerChild > maxPerChildQty, each child is capped at the smart max and the remainder spills into the next batch (the loop continues firing batches over time until parent fills or another abort condition triggers):

if naivePerChild > maxPerChildQty:
  perChildQty = maxPerChildQty
  effectiveBatch = min(batchSize, ceil(remaining / maxPerChildQty))
else:
  perChildQty = naivePerChild
  effectiveBatch = naiveCount

Worked example — deep book

Parent: 21.32 HYPE @ $58 → $1,237 notional. batchSize = 100, slippageCap = 50 bps.

Book has 13,000+ HYPE within 5 bps of inside ask. maxPerChildQty = 13,000 — way more than needed.

naivePerChild = 21.32 / 100 = 0.213 HYPE (≈ $12.4 per child, above $10 floor). naivePerChild (0.213) < maxPerChildQty (13,000) → naive wins. Fire 100 × 0.213 HYPE in one batch. Done.

Worked example — thin book

Parent: $100,000 of an illiquid asset. batchSize = 100, slippageCap = 50 bps.

Only $20,000 worth of qty within 5 bps. maxPerChildQty = $20,000 / price.

naivePerChild = $100k / 100 = $1,000 per child — but only $200 worth of qty available per child within smart-max depth.

naivePerChild > maxPerChildQty → cap binds. Each child = $200. effectiveBatch = min(100, ceil($100k/$200)) = 100. Fire 100 × $200 = $20,000 this batch. $80,000 spills to the next batch (after a delayMs pause), where the burst re-reads the book, re-evaluates maxPerChildQty, and continues.

This way Burst paces itself through illiquid markets without paying disproportionate slippage per sub-order, while still firing as fast as the book can absorb.

Abort Conditions

The runLoop checks these every iteration in this order:

ConditionStatusReason
User pressed Cancel
cancelled
cancelled
Elapsed > timeoutMs (excl. rate-limit waits)
aborted
timeout
filledQty ≥ parentQty
completed
filled
Rate-limit backoff streak ≥ 30
aborted
rate_limited
consecutiveZeroWaves ≥ maxZeroWaves
aborted
zero_fill_streak
slippage_actual > slippageCap
aborted
slippage_cap

abortReason is persisted with the final state and reported in the closing toast.

Rate-limit handling

Hyperliquid enforces per-account request rate limits tied to cumulative volume traded. If a batch is rejected for rate-limit reasons ("Too many cumulative requests…"), Burst:

  1. Does not count the failed batch toward any progress counter.
  2. Backs off exponentially (500ms → 8s).
  3. Pauses the timeout clock so the burst doesn't expire during a rate-limit wait.
  4. Aborts only after ~30 consecutive rate-limited batches (~4 min of waiting), with reason rate_limited.

Resume on Refresh

Because the burst state is persisted to localStorage, a page reload during an in-flight burst doesn't lose the order. On app mount:

  1. Read all persisted bursts with status running.
  2. For each, call getUserFillsByTime(user, startedAt - 1s).
  3. For each child with a known oid (recorded when the wave POST returned), sum matching fills.
  4. Update filledQty, avgFillPrice per the reconciled totals.
  5. If reconciliation shows the parent is satisfied, mark completed. Else re-attach the executor loop and continue firing children for the remainder.

Children whose POST response we lost (browser killed mid-wave, before the response landed) stay with oid = null and can't be reconciled by fill matching. The loop will simply fire fresh children to make up the missing qty — IOC bounds the worst case per child so any overfill is small.

Smart Slippage Cap

The slippage cap field you see in the UI is your ceiling, not the value Burst actually enforces. Default-on, Borsa walks the live L2 book at submit and clamps the effective cap to market_impact + 5 bps, where market_impact is the VWAP a Market order of the same size would pay versus the inside best.

effective_cap = min(user_cap, market_impact + buffer)
buffer = 5 bps (fixed)

Why: if user_cap > market_impact + buffer, Burst would be strictly worse than Market for that config — same worst-case slippage, slower. The clamp makes Burst always at-or-better than Market on worst-case price; the size and ladder logic make it usually better on average price too.

Live recompute. As you change Size, the panel reads the current L2 book and re-displays the effective cap. The hint underneath the input shows:

Market impact 18 bps · Effective 23 bps

If the book is unavailable (WS disconnect, sparse market), the user value is used as-is — no clamp.

At submit. The cap is recomputed once more from the freshest book, so what shows in the UI is a preview; the value sent to the executor is whatever the book says at click time.

Buffer reasoning. 5 bps gives the executor a tiny margin to react to micro-replenish between waves without bouncing off the cap on the first zero-fill. Larger buffers make Burst more permissive vs Market; smaller buffers risk premature aborts. 5 bps is the default and is not configurable from the UI (visible in Advanced for reference only).

Inputs

InputDefaultRangeNotes
Side
—
buy/sell
Long/Short.
Size
—
> 0
Parent qty in base asset.
Slippage cap
0.5%
0.05–5%
Your ceiling. Effective cap = min(this, impact + 5bps). See Smart Slippage Cap.
Batch size
100
1–100
Sub-orders fired together per API request. Higher = fewer RTTs but slight overfill risk if not clamped.
Delay (ms)
50
0–1000
Pause between batches. Below network RTT (~50ms) has diminishing returns.
Max empty batches
8
1–50
Abort after N consecutive batches with zero fills.
Timeout (s)
5
1–120
Hard wall-clock ceiling. Pauses during rate-limit waits.
Smart buffer
5 bps
fixed
Slack added on top of market-impact estimate. Not configurable.
Reduce Only
off
—
Applies to every sub-order.

There is no max-splits cap. Burst runs until the parent qty is filled, you cancel, the slippage cap is breached, or one of the safety conditions above fires.

The defaults are tuned for liquid perps. For thin markets, raise the slippage ceiling (smart clamp protects against overpaying).

Live UI

While a burst is running, it appears as a row in Open Orders with:

  • Type column → Burst
  • Size column → remaining (parentQty − filledQty)
  • Price column → running average fill price
  • Filled column → percentage of parent qty filled so far
  • Cancel button → dispatches burstExecutor.cancel(burstId)

Cancel is non-blocking: it sets a flag the loop checks each iteration. Any wave already in flight completes (the IOC nature means it fills or doesn't, no retry). A brief spinner shows while the cancel propagates; the row vanishes when the burst transitions to terminal status.

Toasts

On completion, a single toast summarises:

Burst <status>: <filled>/<total> <SYMBOL> @ <avg>  ·  slip <Xbps>  ·  <children>/<max> children · <elapsed>s [(reason)]
  • completed → success toast
  • cancelled → neutral toast
  • aborted → error toast with reason

slip is (avgFillPrice − initialBest) / initialBest × 10000 (positive = paid up for buys).

Fees

Every child is IOC, every fill is taker. Same fee class as Market. The advantage Burst gives you isn't fee — it's average fill price closer to the inside of the book, less far-side sweeping when the book is thin.

Edge Cases & Failures

  • Book vanishes briefly. Zero-fill wave. The ladder widens, the next wave tries again. After maxZeroWaves consecutive misses, abort.
  • Wallet not connected. Borsa rejects the start before any wave fires.
  • Outcome market. Burst is not currently supported on HIP-4 outcomes. The order type toggle is hidden when an outcome is active.
  • Rate limit. Hyperliquid's /exchange POST has a 1200 weight/minute limit. With a 50ms delay that is up to 20 POSTs/sec, far above the limit if held indefinitely. Real bursts complete in 1–5s, so this is rarely hit; if it is, you'll see a rejection toast and the run aborts.
  • Overfill protection. Wave total is clamped to remainingQty before send. The last wave may have 1 or 2 children instead of waveSize. Inside a single batch, the exchange does not guarantee atomic "either all fill or none" — but because all IOC children combined sum to ≤ remaining, the worst case is that fewer fill, not more.
  • Slippage check is vs initial_best, not vs avg fill. The cap is on how far the market has moved since you started, not on what you've already paid. This is intentional: if the inside moves away, that's information you want to act on; if you've already paid 0.5% on early children but the inside hasn't moved, you're still inside cap.

Concrete Example

You want to buy 1 ETH. Current mid 2,000, inside ask 2,000.10. Example settings (not the panel defaults):

parentQty = 1
slippageCap = 0.003  (0.3%)
childCountMax = 20
waveSize = 3
delayMs = 50
probeSize = max(1 × 0.05, 1 / 20) = 0.05
initial_best = 2000.10

Wave 0 (children 0, 1, 2):

step_0 = 0.003 × (0/19)^0.7 = 0
step_1 = 0.003 × (1/19)^0.7 ≈ 0.000371
step_2 = 0.003 × (2/19)^0.7 ≈ 0.000603

p_0 = 2000.10 × 1.0     = 2000.10
p_1 = 2000.10 × 1.000371 = 2000.84
p_2 = 2000.10 × 1.000603 = 2001.31

size_i = probe = 0.05  for all three

POST 3 IOC limits in one signed action. Suppose all fill:

  • filled_qty = 0.15
  • avg_fill ≈ 2000.75
  • consecutive_zero_waves = 0
  • prev wave fill ratio = 100% → next base = 0.05 × 1.5 = 0.075

Wave 1 (children 3, 4, 5):

step_3 ≈ 0.000805
step_4 ≈ 0.000994
step_5 ≈ 0.001175
p_3 = 2000.10 × 1.000805 ≈ 2001.71
p_4 ≈ 2002.09
p_5 ≈ 2002.45
sizes = 0.075 each
wave_total = 0.225
remaining = 1 − 0.15 = 0.85
wave_total ≤ remaining → ok

Continue waves until either filled_qty ≥ 1, currentBest deviates > 0.3% from 2000.10 (= 2006.10), 20 children fire, 5s elapses, or 8 consecutive zero-fill waves.

In practice on a liquid book, all 1 ETH fills in 3–6 batches over ~250ms with average slip 5–15 bps — much tighter than the 5% slippage default of a Market order would allow.

  • Market — single-shot taker. Same intent, simpler, no slippage smarts.
  • TWAP — time-based slicing.
  • Scale — price-range distribution.
  • Trading Fundamentals — mark vs oracle vs last; fee classes.
Burst · Docs | Borsa