On 2025-12-09 Binance silently switched STOP_MARKET, TAKE_PROFIT_MARKET, and other conditional order types on USDⓈ-M Futures to a new "Algo Order" endpoint set. From that minute on, every bot still POSTing to /fapi/v1/order for a stop order started getting back:
{"code":-4120,"msg":"Order type not supported for this endpoint. Please use the Algo Order API endpoints instead."}
Six months later (as of 2026-05), ccxt/ccxt ships raw fapiPrivate* bindings for the new endpoints in the abstract layer, but the high-level createOrder() REST helper still routes conditional types to the old /fapi/v1/order path — i.e. client.create_order(type="STOP_MARKET", ...) is still expected to fail with -4120. Several other major libraries took weeks to react. This repo is the framework-agnostic recipe — raw HTTP examples that work without any SDK, plus pointers to upstream issues for the SDKs that have or haven't been patched, plus a vendorable algo_wrapper.py drop-in module for projects that don't carry ccxt.
What this guide is not: trading strategy advice, SL/TP placement rules, position-management patterns, or anything about what to trade. It's pure API-integration plumbing — the mechanical "how do I keep my bot from crashing on -4120" recipe.
You're hitting -4120 and using one of these:
| Library | Status (as of 2026-05) | Where to look |
|---|---|---|
ccxt/ccxt (Python · JS · PHP) |
fapiPrivate* bindings exist · createOrder() REST helper still routes to old endpoint |
#26861 (closed) · #27486 (closed · OP posted workaround) · #27474 |
freqtrade/freqtrade |
✅ fixed Dec 2025 · upgrade | #12610 (closed · +5 reactions · 17 comments) |
nautechsystems/nautilus_trader |
✅ fixed Dec 2025 · upgrade to ≥1.222 | #3287 (closed · +3 reactions · 27 comments) |
JKorf/Binance.Net (.NET) |
✅ fixed v11.x · upgrade | #1542 |
tiagosiebler/binance (Node.js) |
✅ library fixed Dec 2025 · use submitNewAlgoOrder / cancelAlgoOrder / cancelAllAlgoOpenOrders |
#609 (open · docs example update pending · see mr-smit's shape) |
oliver-zehentleitner/unicorn-binance-rest-api |
✅ fixed | #93 |
QuantConnect/Lean.Brokerages.Binance |
✅ fixed | #61 |
| Raw HTTP / custom HMAC code | n/a · this guide is for you | — |
If your library is below this list, search its issue tracker for -4120 first — chances are good someone already filed it.
Binance's 2025-11-06 changelog entry announced — about five weeks before the cutover — that conditional order types on USDⓈ-M Futures move to a new endpoint set:
Affected order types (must go to /fapi/v1/algoOrder now):
STOP_MARKET,STOP(stop-limit)TAKE_PROFIT_MARKET,TAKE_PROFITTRAILING_STOP_MARKET
Unaffected (still go to /fapi/v1/order):
LIMIT,MARKETLIMIT_MAKER- Post-only / time-in-force variants of the above
The three new endpoints:
| Action | Method · Path | Notes |
|---|---|---|
| Place algo order | POST /fapi/v1/algoOrder |
response field renamed: orderId → algoId |
| List open algo orders | GET /fapi/v1/openAlgoOrders |
separate from /fapi/v1/openOrders — you now have to poll two lists |
| Cancel algo order | DELETE /fapi/v1/algoOrder |
by symbol + algoId |
The param rename trap: the new endpoint expects triggerPrice, not stopPrice. Passing the old name does not raise — the order is accepted but the trigger never fires. Verified 2026-05 against live Binance Futures. Always use triggerPrice.
This is the canonical pure-Python wrapper using only stdlib. Translate to your language as needed — the HMAC + query-string signing is the same shape Binance has used for years.
# binance_algo.py — no third-party deps
import hashlib, hmac, json, time, urllib.parse, urllib.request
FAPI = "https://fapi.binance.com"
def _signed_request(method, path, params, api_key, api_secret):
params = dict(params)
params["timestamp"] = int(time.time() * 1000)
params["recvWindow"] = 5000
query = urllib.parse.urlencode(params)
sig = hmac.new(api_secret.encode(), query.encode(), hashlib.sha256).hexdigest()
url = f"{FAPI}{path}?{query}&signature={sig}"
req = urllib.request.Request(url, method=method, headers={"X-MBX-APIKEY": api_key})
with urllib.request.urlopen(req) as r:
return json.loads(r.read())
def create_stop_market(symbol, side, qty, trigger, pos_side, api_key, api_secret, coid=None):
"""Place a STOP_MARKET conditional order via the new algo endpoint.
side: "BUY" or "SELL"
pos_side: "LONG", "SHORT", or "BOTH" (one-way mode)
trigger: trigger price (string or number); MUST be `triggerPrice` not `stopPrice`
coid: optional client algo id (max 36 chars) for idempotency
"""
p = {
"symbol": symbol,
"side": side.upper(),
"type": "STOP_MARKET",
"algoType": "CONDITIONAL",
"triggerPrice": str(trigger),
"quantity": str(qty),
"positionSide": pos_side.upper(),
"workingType": "CONTRACT_PRICE",
}
if coid:
p["clientAlgoId"] = coid[:36]
return _signed_request("POST", "/fapi/v1/algoOrder", p, api_key, api_secret)
def list_open_algo_orders(symbol, api_key, api_secret):
"""List open conditional orders for a symbol. Use `None` symbol to list all."""
params = {"symbol": symbol} if symbol else {}
res = _signed_request("GET", "/fapi/v1/openAlgoOrders", params, api_key, api_secret)
return res.get("orders", []) if isinstance(res, dict) else (res or [])
def cancel_algo_order(symbol, algo_id, api_key, api_secret):
"""Cancel by algoId returned from create_stop_market."""
return _signed_request(
"DELETE", "/fapi/v1/algoOrder",
{"symbol": symbol, "algoId": algo_id},
api_key, api_secret,
)Sample call:
order = create_stop_market(
symbol="BTCUSDT",
side="SELL", # SELL to stop-loss a LONG
qty="0.002",
trigger="60000",
pos_side="LONG", # hedge-mode; use "BOTH" for one-way mode
api_key=API_KEY,
api_secret=API_SECRET,
coid="bot-sl-12345",
)
print(order["algoId"]) # save this — you need it to cancel laterIf you want a vendorable file rather than copy-pasting the snippet above, this repo ships algo_wrapper.py — a ~250-line, zero-dep, stdlib-only Python module with:
create_stop_market(...)andcreate_take_profit_market(...)list_open_algo_orders(symbol=None)for cross-symbol zombie sweepscancel_algo_order(symbol, algo_id)- A typed
BinanceAlgoErrorthat parses Binance'scode/msgso you can branch onexc.code == -1111(precision),-4045(max-stop-order limit),-4061(positionSide mismatch), etc. - Mainnet / testnet base-URL switch via
base_url=FAPI_TESTNET
Vendor the file:
curl -O https://raw.githubusercontent.com/MankhongGarden/binance-futures-algo-endpoint-migration/main/algo_wrapper.pyQuick usage:
from algo_wrapper import create_stop_market, BinanceAlgoError
try:
res = create_stop_market(
symbol="BTCUSDT",
side="SELL", # closes a LONG position
quantity="0.002", # string -> avoid float precision drift
trigger_price="62000",
position_side="LONG", # "BOTH" if you're in one-way mode
client_algo_id="my-bot-sl-42",
reduce_only=True,
api_key=API_KEY,
api_secret=API_SECRET,
)
algo_id = res["algoId"] # store this — it's your handle for cancel/list
except BinanceAlgoError as exc:
if exc.code == -1111:
# "Precision is over the maximum defined for this asset"
# round qty/price via ccxt's amount_to_precision / price_to_precision
...
elif exc.code == -4045:
# "Reach max stop order limit" — Binance enforces a 10-algo-orders-
# per-symbol cap. Sweep zombies before retry.
...
raiseTestnet smoke test (round-trip list → place far-away STOP_MARKET → cancel):
$env:BINANCE_API_KEY = "..." # PowerShell; bash: export BINANCE_API_KEY=...
$env:BINANCE_API_SECRET = "..."
python examples/dry_run.py --testnet --placeThe test places a STOP_MARKET 30% above spot (cannot trigger), confirms it via list, then cancels it. Cost: zero on testnet — but start on testnet.
CCXT exposes Binance's raw private endpoints as methods even when there's no unified wrapper. As of 2026-05-29, in ccxt/ccxt master:
- The raw endpoint bindings are present in the abstract layer (
ts/src/abstract/binance.ts→fapiPrivatePostAlgoOrder,fapiPrivateGetOpenAlgoOrders,fapiPrivateGetAlgoOrder). You can call them directly viaclient.fapi_private_post_algo_order(params). - The
pro/binance.tsWebSocket path auto-routes conditional orders toalgoOrder.place/algoOrder.cancelwhenmarket.linear && market.swap && isConditional. - The REST
createOrder()high-level helper still routes tofapiPrivatePostOrderfor conditional types — i.e.client.create_order(type="STOP_MARKET", ...)on Python ccxt is still expected to fail with-4120. (Verify against the ccxt version you're on; this status will eventually flip.)
The OP of ccxt/ccxt#27486 documented this idiom — re-stating here so the recipe is findable without diving into a closed issue:
import ccxt.async_support as ccxt
exchange = ccxt.binanceusdm({"apiKey": API_KEY, "secret": API_SECRET})
# Place STOP_MARKET via algo endpoint
order = await exchange.fapiPrivatePostAlgoOrder({
"symbol": "BTCUSDT",
"side": "SELL",
"type": "STOP_MARKET",
"algoType": "CONDITIONAL",
"triggerPrice": "60000", # NOT stopPrice
"quantity": "0.002",
"positionSide": "LONG",
"workingType": "CONTRACT_PRICE",
})
algo_id = order["algoId"]
# List open algo orders
open_algo = await exchange.fapiPrivateGetOpenAlgoOrders({"symbol": "BTCUSDT"})
# Cancel by algoId
await exchange.fapiPrivateDeleteAlgoOrder({
"symbol": "BTCUSDT",
"algoId": algo_id,
})Note: fapiPrivatePostAlgoOrder, fapiPrivateGetOpenAlgoOrders, and fapiPrivateDeleteAlgoOrder are auto-generated method names from CCXT's Binance API descriptor — they exist even without changelog mention.
While migrating, you'll likely also hit:
{"code":-1111,"msg":"Precision is over the maximum defined for this asset."}
This is unrelated to -4120 but tends to surface in the same migration window because:
- The new algo endpoint enforces
triggerPriceprecision per market'spricePrecision, not per the looser "any-float" tolerance the old endpoint had. - If your bot was using a homemade rounder (
round(price, 2)) instead of CCXT'sprice_to_precision, the old endpoint silently accepted slightly-off values; the new endpoint rejects them.
The natural temptation is to hand-roll a rounder:
qty = math.floor(qty * 10**decimals) / 10**decimalsIEEE-754 bites: 0.018 * 1000 = 17.999999999999996, so math.floor lands at 17 and you ship "0.017". Or on BTCUSDT where the actual filter ladder is 1e-5 (despite what the metadata's nominal precision.amount displays), the same code lands on 0.01799 and Binance rejects with -1111.
Fix: use CCXT's price_to_precision(symbol, price) for triggerPrice, or fetch the market's filters[].tickSize from /fapi/v1/exchangeInfo and round to that step. Do not rely on the old loose precision behaviour — Binance has been tightening this across endpoints for two years.
Binance enforces 10 algo orders per symbol by default. Easy to hit if:
- Zombie SLs from crashed bot restarts accumulate.
- Multiple engines on the same account (hedge mode) share a
(symbol, side)position bucket and each places its own SL/TP. - A trailing-stop loop cancels-then-replaces too aggressively.
The sweep idiom: at engine boot, list all open algos for the symbol, compute the set of algoIds your ledger knows about, and cancel everything else. Snapshot algos before ledger (so a concurrent place_sl in flight isn't killed), and skip algos created in the last 60 seconds (so a race between "Binance accepted" and "ledger row inserted" doesn't kill a fresh SL).
That sweep pattern is out of scope for this guide — it's bot-specific state management, not API plumbing. The API plumbing is what this guide covers.
Mapping from old /fapi/v1/order STOP_MARKET payload to new /fapi/v1/algoOrder:
Old (/fapi/v1/order) |
New (/fapi/v1/algoOrder) |
Notes |
|---|---|---|
type=STOP_MARKET |
type=STOP_MARKET + algoType=CONDITIONAL |
new endpoint requires algoType |
stopPrice=60000 |
triggerPrice=60000 |
rename · old name silently fails on new endpoint |
closePosition=true |
not supported · use quantity + reduceOnly=true |
algo endpoint doesn't honour closePosition |
workingType=CONTRACT_PRICE |
workingType=CONTRACT_PRICE |
unchanged |
positionSide=LONG |
positionSide=LONG |
unchanged |
clientOrderId=... |
clientAlgoId=... |
rename · 36 char max |
(response) orderId |
algoId |
rename · used for fetch/cancel |
(response) status |
status |
values include NEW, TRIGGERED, EXPIRED, CANCELED |
- You're on a ccxt version where
client.create_order(type="STOP_MARKET", ...)already routes correctly. Use ccxt's high-level API. Re-verify after every ccxt upgrade — the moment ccxt fixescreateOrder, this module becomes obsolete for that code path. - You're using the WebSocket Pro API (
ccxt.pro.binance). It already routes conditional orders viaalgoOrder.place/algoOrder.cancel. - You're on coin-margined Futures (
/dapi/v1/...). Different base URL, different endpoint paths. This module is USDⓈ-M only. - You're on Spot. Spot conditional orders weren't part of the 2025-12-09 migration.
- You need OCO (one-cancels-other) bracketing. New algo endpoint shape is different; not covered here.
It was in the official changelog about five weeks before the cutover — but the entry was a one-liner buried under several other 2025-11-06 items. Most bot teams found out at incident time, which is why nine major libraries filed -4120 bugs within the same 48-hour window.
The takeaway is the obvious one: subscribe to the developers.binance.com changelog RSS, and treat any line item containing "endpoint", "deprecated", or "Algo" as a P1 review.
If you want the full diagnostic thread (or want to add your own data point):
- ccxt/ccxt#26861 — TS Market via Binance · closed
- ccxt/ccxt#27486 — Python · workaround in OP body · closed
- ccxt/ccxt#27474 — same symptom · 中文
- freqtrade/freqtrade#12610 — fix path · 17 comments · closed
- nautechsystems/nautilus_trader#3287 — fix path · 27 comments · closed
- JKorf/Binance.Net#1542 — .NET fix path
- tiagosiebler/binance#609 — Node.js · library fixed (commit
17c8f6f) · issue stays open for docs/example refresh - oliver-zehentleitner/unicorn-binance-rest-api#93
- QuantConnect/Lean.Brokerages.Binance#61
This guide is free. If it saved you the half-day I spent debugging when my bot ate -4120 at 3am:
- ⭐ Star this repo so other searchers find it (GitHub ranks by stars)
- 💛 GitHub Sponsors — sustains weekend OSS writeups like this
- Hit an edge case not covered? Open an issue — I'd rather add a section than have you debug alone
MIT — see LICENSE. Use anything here however you want.
The code samples are also MIT and have no external dependencies beyond Python 3.7+ stdlib (for the raw HTTP path) or CCXT 4.x (for the fapiPrivate* path).