Skip to main content

Frequent batch auctions (FBA)

info

Preview. Per-market opt-in via MIP-3; not all markets run FBA.

TL;DR

FBA replaces continuous matching with discrete auction batches every batch_interval_ms. Orders queued within a batch are cleared simultaneously at a single uniform clearing price. This neutralises latency-based MEV: there's no benefit to being one microsecond faster.

Continuous vs batch

PropertyContinuous CLOBFBA
Matching cadenceOn every order arrivalEvery batch_interval_ms
Price discoveryPer-tradePer-batch (one clearing price)
Latency valueHigh (first-to-arrive wins ties at price)Zero within a batch
Surplus from latencyCaptured by HFTReturned to participants via uniform price
Public order visibilityPre-trade (resting book)Pre-batch (visible queue)

Mechanism

batch t: accept orders during [t, t + batch_interval_ms)
batch close t: freeze the queue
compute clearing price p*:
p* = price at which |aggregated buy demand| = |aggregated sell demand|
fill all crossing orders at p*
roll any non-crossing orders into batch t+1 (or cancel per TIF)
batch t+1: open

Clearing rules:

  • All buys with price ≥ p* fill at p*.
  • All sells with price ≤ p* fill at p*.
  • The single clearing price p* maximises total cleared volume (equivalent to walking demand/supply curves to the intersection).

The fill price is uniform across all participants in the batch — no one is filled at a worse price by virtue of arriving later.

When to use FBA

Asset classDefaultWhy
Major perps (BTC, ETH)Continuous CLOBLiquid; latency advantages are small relative to bid-ask
Long-tail listings (MIP-3)Optional FBAThin book; HFT toxicity outweighs liquidity provision
Spot pairsContinuous CLOBConvention
Index / structured productsFBAComposite pricing needs synchronous clearing

Each market's matching mode is in markets_meta as fba_enabled. Markets with FBA on accept fba_submit into the live batch window. See fba_submit for the full action.

Batch interval

Governed per market as period_ms, bounded to [100 ms, 5 s]; read the live value from the operator-lane fba_batch_state read rather than assuming a fixed default — do not derive it from block cadence.

Faster intervals reduce the wait but increase computational cost.

Order shape

{
"type": "fba_submit",
"params": {
"market": 42,
"side": "Bid",
"size": 100000000,
"price": 10050000000
}
}

size / price are raw u64 JSON numbers on the 1e8 plane, not decimal strings. There is no batch_id or cloid field — an fba_submit always joins whichever window is currently open for market; it cannot target a future batch. See fba_submit for the full field table.

Worked example

Batch t has the following orders for asset 42:

buys:
bob: 5 @ 100.10
alice: 3 @ 100.05
carol: 2 @ 100.00

sells:
dave: 3 @ 99.95
eve: 4 @ 100.00
frank: 2 @ 100.05

Walk demand (cumulative size at each price ≥ candidate):

buy-side cumulative at price ≥ p:
100.10: 5
100.05: 5+3 = 8
100.00: 8+2 = 10
99.95: 10 (none here)

Walk supply (cumulative size at price ≤ candidate):

sell-side cumulative at price ≤ p:
99.95: 3
100.00: 3+4 = 7
100.05: 7+2 = 9
100.10: 9 (none here)

Intersection: at p = 100.00, buy-side cumulative = 10, sell-side cumulative = 7. At p = 100.05, buy-side cumulative = 8, sell-side = 9. Intersection between 100.00 and 100.05.

Clearing rule maximises volume:

pmin(buy, sell)
99.95min(10, 3) = 3
100.00min(10, 7) = 7
100.05min(8, 9) = 8
100.10min(5, 9) = 5

Max cleared volume is 8 at p* = 100.05. So:

  • All buys ≥ 100.05 fill: bob (5) + alice (3) = 8 BTC bought at 100.05.
  • All sells ≤ 100.05 fill: dave (3) + eve (4) + frank (2) = 9 BTC offered. Pro-rata: 8/9 = 88.9% of each → dave 2.67, eve 3.56, frank 1.78.
  • Carol (buy 2 @ 100.00) doesn't fill — rolls to t+1 or expires per TIF.

All winners fill at 100.05. Bob doesn't get a worse price for being "earlier" — there's no earlier in FBA.

Fairness in clearing

When supply > demand at p*, the larger side is pro-rata filled — every seller above gets the same fraction. No FIFO, no price priority among the over-supplied side (everyone's already at or better than p*).

This is the FBA fairness property: at the clearing price, no participant gets a better deal than another.

Edge cases

Show edge cases
  • Empty batch. No orders → no clearing event. The next batch starts immediately.
  • Single-sided batch. Only buys (or only sells). No clearing — all orders roll to the next batch (Gtc) or cancel (Ioc).
  • Tie at clearing. When two prices both maximise volume, the protocol picks the price closer to the prior mark (reduces mark-step ambiguity).
  • Market orders in FBA. Submitted as IOC at extreme price; participate in the batch and fill at p* if they cross.
  • Reduce-only in FBA. Checked at batch close, against post-fill state of prior fills within the same batch. Cleared atomically.

Sequence

window opens
bob: fba_submit buy 5 @ 100.10
alice: fba_submit buy 3 @ 100.05
carol: fba_submit buy 2 @ 100.00
dave: fba_submit sell 3 @ 99.95
eve: fba_submit sell 4 @ 100.00
frank: fba_submit sell 2 @ 100.05
window closes; clearing fires — p* = 100.05; 8 BTC clears
next window opens

FBA fills settle into each account's positions and balances like any other fill. Node 0.9.6 records a batch clearing on trades and fills; an older node did not — see every order lane records its fill. There is no /info read that lists past fills for a closed window. Confirm a settlement by diffing clearinghouse_state before and after, and account_state for the balance side.

Querying

The live FBA pool + indicative clearing is exposed on the node /info read path via fba_batch_state.

warning

This read is OPERATOR LANE, not public. The FBA engine is not reachable from the public /exchange yet, so its read does not ship publicly either — it answers with the same error an unknown type gets. See operator lane. The shape below is what a node operator sees today, and what the public API will serve on the day the engine opens.

It takes coin (the market symbol). FBA is a per-market opt-in, so an unregistered market is not a 404 — it returns a 200 with zeroed fields (enabled:false, empty orders, indicative:null).

curl -X POST https://api.testnet.mtf.exchange/info \
-H 'content-type: application/json' \
-d '{"type":"fba_batch_state","coin":"BTC"}'
{
"type": "fba_batch_state",
"data": {
"coin": "BTC",
"enabled": true,
"period_ms": 1000,
"min_lot": "1",
"last_settle": 1735689600000,
"next_settle": 1735689601000,
"order_count": 11,
"bid_count": 5,
"ask_count": 6,
"bid_size": "10",
"ask_size": "9",
"orders": [ /* {oid, owner, side, price, sz, submitted_at} */ ],
"indicative": { "clearing_px": "100.5", "matched_size": "8" }
}
}

Prices and sizes are human decimal strings, tick- and lot-normalized — this is a read, not the raw order-submission plane. next_settle is derived as last_settle + period_ms. The indicative block is the volume-maximising uniform price + matched size the next batch would clear given the current window — computed read-only, not yet settled — and is null when there is no cross (one-sided or empty window). This is what p* would be if the batch closed now, useful for traders deciding whether to add to the batch.

Timestamp keys carry no _ms suffix; only a key naming a DURATION keeps it, which is why period_ms does and last_settle does not. Full rows on the fba_batch_state read above.

Orders carry no stp_group. The chain resolves a self-trade group from committed state, so publishing it would tell every reader which vault an address operates.

indicative is computed on the window after self-trade prevention, the same filter the auction runs, so the price you read is the price settlement uses. orders is the raw parked window and is not filtered: a later arrival can still change which orders the filter drops.

Self-trade prevention

A batch auction applies self-trade prevention before it clears.

Two parked orders are one party when they share an account, or when they share a self-trade group. The chain resolves the group; the stp_group you send is ignored. A metaliquidity vault and the operator that runs it are one party.

The rule is CancelNewest, applied across the whole window. If two orders of one party sit on opposite sides and they cross — the bid price is at or above the ask price — the NEWER order is dropped from that batch. The older order stays. A dropped order drops nothing after it.

Two quotes that do not cross both stay. A party can quote a bid at 99 and an ask at 101 in the same batch, because no clearing price fills both.

A dropped order is not cancelled and it is not held: it leaves the batch with every other unfilled order, and nothing carries to the next window.

The party is judged on BOTH the group stored when the order parked and the group committed state resolves at settlement. That is deliberate: a binding created inside the window is caught by the resolved value, and a binding REVOKED inside the window is caught by the stored one. Neither direction opens a self-cross.

See also

FAQ

Show FAQ

Q: Doesn't FBA give up price discovery? A: No — within a batch the protocol still discovers p* from the participants' orders. The discovery happens at fixed cadence rather than continuously.

Q: Why a 1 s batch instead of 100 ms? A: 100 ms is too tight to neutralise latency in any meaningful sense — even within 100 ms, faster machines can re-submit. 1 s gives enough buffer that physical-network latency dominates within-batch latency, removing the HFT edge.

Q: Can FBA markets co-exist with CLOB markets? A: Yes — each market is FBA or CLOB independently. An account can hold positions in both at the same time.

Q: Does FBA reduce the gas / compute cost of matching? A: Roughly. Continuous matching does O(1) work per arrival; FBA does O(N log N) at batch close. For N orders per batch FBA is comparable, with the advantage of being more predictable per-block cost.