I Built a Trading Bot That Was Never Allowed to Trade. Then I Gave It Hands.
What I wanted my agent to do, and why...
I spend most of my time building trading tools. My main one is a Python assistant that watches Binance Futures, scores every setup out of 100, and tells me BUY, SELL or WAIT with the full reasoning behind it. EMA cross, VWAP position, volume, breakout confirmation, ATR, nearby levels, candlestick patterns. Each criterion is worth its points or nothing.
It has one deliberate limitation. It is not allowed to place orders.
That was not an oversight. I wrote it that way because I did not trust myself with a bot that could act while I was asleep, and I did not want to hand a raw API key with trading permissions to code I was still changing every week.
So my bot has been sitting there for months, being right, being wrong, and never being able to do anything about either.
BinanceAgentOS changed the calculation. Instead of embedding an API key with trading rights into my own code, the agent goes through an official gateway where I set the permissions and can revoke them at any time. So I gave myself one job: connect my analysis engine to Agent OS, let it place its own trades in a sandboxed setup, and find out what breaks.
Plenty broke. That is the interesting part.
How I did it
Step 1: Look before touching anything 🧐
Before writing a single line, I connected Agent OS to Claude and asked it one question: what am I actually allowed to do?
👉 wallet.getApiKeyPermission
The answer was the first thing worth writing down:
{"enableReading": true,"enableSpotAndMarginTrading": true,"enableFutures": true,"enableWithdrawals": false,"enableInternalTransfer": false,"permitsUniversalTransfer": true,"ipRestrict": false}
Withdrawals are off. Good, that is the boundary everyone checks first.
But permitsUniversalTransfer is true. My agent cannot take money out of Binance, but it can move funds between my own accounts. That is a very different threat model from "it can only trade what I fund it with", and it is exactly why the dedicated sub-account matters rather than being an optional best practice.
I did not read that anywhere. I found it by calling the endpoint and looking at the response.
Step 2: Build a layer that does not care which market it hits.
I wanted both Spot and USDT-M Futures, switchable while running. So my scoring engine emits a venue-neutral order, and a router decides where it lands:
router(.)use(Venue.SPOT)
router(.)place(intent)
router(.)toggle() now USDT-M Futures
router(.)place(intent) same intent, different market
The two markets are less similar than the docs suggest. Spot has no leverage, no reduce-only, and its exits are separate STOP_LOSS_LIMIT orders that sell the asset back. Futures needs leverage set before the entry, and exits with STOP_MARKET and closePosition. One adapter each, and everything above them stops caring.
The router also refuses to switch venues while a position is still open on the active one. Walking away from an unattended futures position to go trade spot is a very fast way to lose money.
Step 3: Guardrails, because the sub-account is only the first layer.
Binance's own answer to a compromised agent is the sub-account boundary. That protects the rest of your funds. It does not protect the money inside the sub-account. So I added a second layer before anything reaches the exchange: notional cap, leverage cap, orders per hour, symbol whitelist, mandatory stop loss on every opening order, and a kill switch that halts both venues at once.
Dry run is the default. Reads pass through, every write is refused and logged. I did the entire build in that mode.
What actually happened
The flagship AI feature returned nothing
analysis.getTokenAiReport → {''code'':''000000" ''success'':true,''data'':null}
Same for $BTC. Same for $BNB. Success, no data. It may be a rollout in progress, it may be regional. Either way, if you build a workflow that depends on the token AI report today, check the payload rather than the status field.
A serialisation bug that costs you ten minutes
Requesting several tickers in one call fails:
spot.ticker24hr symbols=["BTCUSDT", "BNBUSDT"]
→ -1100 Illegal characters found in parameter 'symbols'
The space after the comma is the problem. One symbol per call works fine. Small thing, but it is the kind of thing you only learn by hitting it.
There is no API key to paste, and I built my config around one anyway
This was my biggest wrong assumption. I designed a config field called auth_token, because that is how every exchange integration I have ever written works.
#AgentOS does not work that way. Public market data needs no authentication at all, so my bot could read prices, tick sizes and order books before I configured anything. Account access and trading go through an authorisation flow from a compatible client, where you assign a sub-account and set permissions you can revoke later. The whole point is that you never store an exchange key locally.
I rewrote the transport with three modes instead: no auth for market data, OAuth for the account, and a manual bearer token for edge cases. The config went from "paste your key here" to "choose what this agent is allowed to reach".
That inversion is the real design difference, and I only understood it by getting it wrong first.
The SDK renamed the function under me
MCP SDK 1.x
streamablehttp_client(url, headers=..., auth=...)
MCP SDK 2.x
streamable_http_client(url, http_client=create_mcp_http_client(auth=...))
Different name, different signature. If you copy a snippet from a tutorial written a few months ago, it fails on a fresh install. My transport now checks for both.
The leverage discovery that actually mattered
My dashboard now has a leverage slider from 1 to 150. Testing it is where I learned the most useful thing of the whole build.
Leverage does not change your risk while your stop loss holds. It changes your margin and your liquidation distance. In isolated margin, roughly:
liquidation_distance ≈ (1 / leverage) - maintenance_margin_rate
At 150x, that is about 0.67% before maintenance margin. My typical stop sits around 0.5% from entry. Those two numbers are close enough that a wick, funding, or fees can flip the order between them, and if liquidation comes first, the exchange closes me out before my own protection ever fires. I lose the margin instead of the 10 USDT I planned to lose.
So the executor now refuses the order outright when estimated liquidation precedes the stop, and warns when they are within 1.5x of each other. Nothing is sent, not even the leverage setting. I did not plan to build that. The slider forced the question.
Would I keep using it?
What I can say from the build itself:
The thing I like is that the boundary is where it should be. My guardrails are mine, @binance's permissions are Binance's, and the two do not have to trust each other. Compared to embedding a trading-enabled API key in my own repository, this is a genuine improvement, and I say that as someone who was not willing to do the key version at all.
What I would change: the permissions summary should surface permitsUniversalTransfer more loudly, because "withdrawals disabled" reads as safer than it is. And I would like the AI report endpoint to return an explicit reason rather than a null payload with a success code.
The honest summary: my bot spent months being an opinion. It took about a day to turn it into an action with Binance Agent OS, and most of that day was spent building the reasons for it to say no.
I want to reiterate that I'm not a dev, but I took the time to study the code, and Claude was my best companion. I built all of this thanks to Claude and Binance.
Everything above ran against my own account in dry-run mode before a single real order. This bot is simply an analysis tool that can be integrated with your existing tools, it's not a magic cure or a money-making machine. This is for informational purposes only, please always do your own research first. NFA
@Yi He $BNB