The three Betfair Exchange order types — Limit, LimitOnClose, and MarketOnClose — what each one does, when to use each, and how to construct them correctly in a Java API client.
The Betfair Exchange supports three order types. Most developers only use Limit orders — the standard back and lay with a specific price. The other two, LimitOnClose and MarketOnClose, participate in the BSP (Betfair Starting Price) pool and have different semantics. Understanding all three, and the time-of-use restrictions on each, prevents order placement errors and enables strategies that use BSP for execution.
A Limit order specifies a price, a size, and a side. It is matched when a counterparty places an opposing order at the same or better price.
LimitOrder limitOrder = new LimitOrder()
.withSize(10.0) // stake in GBP
.withPrice(2.5) // odds
.withPersistenceType(PersistenceType.LAPSE); // cancel at off if unmatched
PersistenceType determines what happens to unmatched remainder at the start of the race:
| Value | Behaviour |
|---|---|
LAPSE |
Unmatched portion is cancelled at the off |
PERSIST |
Unmatched portion remains as a limit order in-play |
MARKET_ON_CLOSE |
Unmatched portion moves to the BSP pool |
LAPSE is the right default for pre-race strategies — you do not want positions accidentally entering in-play.
PlaceInstruction instruction = new PlaceInstruction()
.withOrderType(OrderType.LIMIT)
.withSide(Side.BACK)
.withLimitOrder(limitOrder);
PlaceExecutionReport report = exchange.placeOrders(
marketId, List.of(instruction), null, null, null);
A LimitOnClose order specifies a price limit and participates in the BSP pool — but only if the BSP is at or better than your limit. It is the way to say “back this runner at BSP, but only if BSP is 3.0 or better.”
LimitOnCloseOrder loc = new LimitOnCloseOrder()
.withLiability(10.0) // maximum stake (back) or liability (lay)
.withPrice(3.0); // minimum acceptable BSP for a back; maximum for a lay
PlaceInstruction instruction = new PlaceInstruction()
.withOrderType(OrderType.LIMIT_ON_CLOSE)
.withSide(Side.BACK)
.withLimitOnCloseOrder(loc);
For a back LimitOnClose, price is the minimum BSP — if BSP is 2.8, the bet is not placed. For a lay LimitOnClose, price is the maximum BSP you are willing to lay at.
Key restriction: LimitOnClose orders can only be placed before the market goes in-play. Once in-play has started, attempting to place one returns BET_LAPSED_OFFSIDE or MARKET_NOT_OPEN_FOR_BSP_BETTING.
A MarketOnClose order participates in the BSP pool with no price limit — you accept whatever BSP the algorithm calculates.
MarketOnCloseOrder moc = new MarketOnCloseOrder()
.withLiability(10.0); // stake (back) or liability (lay)
PlaceInstruction instruction = new PlaceInstruction()
.withOrderType(OrderType.MARKET_ON_CLOSE)
.withSide(Side.BACK)
.withMarketOnCloseOrder(moc);
MarketOnClose is the simplest BSP bet. It guarantees execution (assuming there is a valid BSP), but at whatever price the algorithm determines.
Only Limit orders can be cancelled before the off. LimitOnClose and MarketOnClose orders cannot be cancelled after submission. This is a hard exchange rule — verify order type before submitting BSP bets.
// Check before cancelling
if (order.getOrderType() == OrderType.LIMIT) {
exchange.cancelOrders(marketId,
List.of(new CancelInstruction().withBetId(order.getBetId())),
null);
} else {
log.warn("Cannot cancel BSP order {} after submission", order.getBetId());
}
The PlaceExecutionReport status is the first check:
PlaceExecutionReport report = exchange.placeOrders(marketId, instructions, null, null, null);
if (report.getStatus() != ExecutionReportStatus.SUCCESS) {
throw new OrderPlacementException("Batch failed: " + report.getErrorCode());
}
for (PlaceInstructionReport instr : report.getInstructionReports()) {
if (instr.getStatus() != InstructionReportStatus.SUCCESS) {
log.error("Order failed: {} — {}",
instr.getErrorCode(), instr.getInstruction().getSide());
} else {
log.info("Order placed: {} size matched: {}",
instr.getBetId(), instr.getSizeMatched());
}
}
Common InstructionReportErrorCode values:
BET_ACTION_ERROR — general placement failureINVALID_ODDS — price outside valid Betfair price ladderINSUFFICIENT_FUNDS — not enough balanceMARKET_NOT_OPEN_FOR_BETTING — market is suspendedBELOW_MIN_BET — stake below the £2 minimumBetfair prices are constrained to the official Betfair price ladder — not every decimal value is valid. The ladder has irregular spacing:
1.01–2.00: increments of 0.01
2.0–3.0: increments of 0.02
3.0–4.0: increments of 0.05
4.0–6.0: increments of 0.1
6.0–10.0: increments of 0.2
10.0–20.0: increments of 0.5
20.0–30.0: increments of 1.0
30.0–50.0: increments of 2.0
50.0–100: increments of 5.0
100–1000: increments of 10.0
Round any price to the nearest valid tick before placing an order:
public static double roundToTick(double price) {
if (price < 2.0) return Math.round(price * 100.0) / 100.0;
if (price < 3.0) return Math.round(price * 50.0) / 50.0;
if (price < 4.0) return Math.round(price * 20.0) / 20.0;
if (price < 6.0) return Math.round(price * 10.0) / 10.0;
if (price < 10.0) return Math.round(price * 5.0) / 5.0;
if (price < 20.0) return Math.round(price * 2.0) / 2.0;
if (price < 30.0) return Math.round(price);
if (price < 50.0) return Math.round(price / 2.0) * 2.0;
if (price < 100.0) return Math.round(price / 5.0) * 5.0;
return Math.round(price / 10.0) * 10.0;
}
An invalid price returns INVALID_ODDS — a common early mistake.
If you’re building a Betfair order management system in Java and want a review, get in touch.