preflight-mcp
preflight-mcp
A pre-trade guard for AI agents that have been given trading authority.
An MCP server that answers one question before an order is placed: should this
order be placed at all? It returns CLEARED, REFUSED, or UNREADABLE, with
the numbers that produced the answer.
Built for the Binance Agent OS Mini Hackathon — Track A, Trading Workflows.
CLEARED An ordinary trade
"Buy a small amount of BTC."
✓ 0 bps slippage, 0 bps spread, quote 1263ms old
REFUSED Size the book cannot carry
"Go long BTC with everything."
✗ the visible book holds 5.31328 of the 500 this order wants — the rest
has no price at all, and a market order would find whatever is behind it.
REFUSED A misplaced decimal
"Set a limit buy at 900000." (the model meant 90000)
✗ the limit price 900000 sits 1032.3% outside the 24h range of
77620.01–79485 — far enough out that a misplaced decimal is the
likeliest explanation.
REFUSED A trade that is really a position
"Buy BTC." — with 500 USDT free, and no instruction about size.
✗ this order is 39.4% of the 500 free balance, over the 25% ceiling —
a position decision rather than a trade, and not one to take unattended.
UNREADABLE The data never arrived
"Buy BTC." — with the market endpoint down.
✗ could not establish bid, ask, order book for BTCUSDT — this is not a
judgement about the order, it is the absence of one. A failed read is
not permission.
1 of 7 orders cleared. The other 6 would all have been accepted by the venue.That last line is the whole argument. None of those six orders would have been rejected. They are well-formed, affordable, and the exchange takes them.
Why this exists
Agent OS gives an agent spot, margin, convert and futures authority through one MCP connection. That is the right thing to build, and it moves the dangerous part somewhere new: the model now decides when and how much, and models are bad at exactly the parts of that which are arithmetic rather than judgement.
An LLM does not know that the quote it is reasoning from is forty seconds old.
It does not walk an order book to find out what its own order does to the price
it gets. It cannot tell a null from a 0, so a market-data request that
quietly failed reads as a market that is quietly fine.
The venue will not save you here. A venue rejects malformed orders — wrong lot size, insufficient balance. It accepts, without comment, an order that walks the book 300 bps, an order priced off stale data, and an order that puts 40% of an account into one position. Those are the expensive ones, and nothing was checking them.
So: a guard that runs before execution, holds no credentials, and cannot move funds even if it is wrong.
The rule that matters most
A failed read is not permission.
UNREADABLE is a distinct outcome from REFUSED because they call for
different responses. REFUSED means the order is wrong — reprice it, resize it,
pick another moment. UNREADABLE means we never established anything: the order
may be perfect, and we cannot say. An agent told "refused" goes looking for a
better price. An agent told "unreadable" should stop and retry the read.
Most guards collapse the two, or worse, treat a failed fetch as an empty result
and pass it through as though the market were fine. This one is built around not
doing that. Anything it could not read comes back null and is named.
Install
git clone https://github.com/tinoxbt/preflight-mcp.git
cd preflight-mcp
npm install
npm run buildAdd it to Claude Code:
claude mcp add preflight -- node /absolute/path/to/preflight-mcp/dist/server.jsOr in claude_desktop_config.json / any MCP client:
{
"mcpServers": {
"preflight": {
"command": "node",
"args": ["/absolute/path/to/preflight-mcp/dist/server.js"]
}
}
}No API key, no account, no OAuth. It reads public market data only.
Use it with Binance MCP
The point is to pair it with the server that actually executes:
claude mcp add binance-mcp-server --transport http https://agent.binance.com/mcp/agentic
claude mcp add preflight -- node /absolute/path/to/preflight-mcp/dist/server.jsThen give the agent one instruction:
Before placing any order through the Binance MCP server, call
preflight_tradewith the same symbol, side and quantity. If the verdict is notCLEARED, do not place the order — report the finding instead.
The division of labour is deliberate. Binance MCP holds the OAuth session and places orders. preflight-mcp holds nothing and places nothing. A bug in the guard cannot cost you money; it can only cost you a trade.
Tools
Tool | What it does |
| The gate. Symbol, side, quantity, optional limit price and balance → |
| The facts a decision rests on, with their age stated. Unread fields are |
| Every active threshold and the environment variable that changes it. A guard nobody can inspect is a guard nobody can argue with. |
The rules
Rule | Default | Env | Why |
| 5 s |
| A quote older than this is history, not a price. |
| 50 bps |
| Crossing it costs more than the trade is likely to make. |
| 100 bps |
| What the order does to its own price, walked across the live book. |
| — | — | The order is larger than the visible book. Absence, not expense — reported separately. |
| 25% of free balance |
| Past this it is a position decision. Skipped entirely unless a balance is supplied. |
| 20% outside 24h range |
| A fat-finger catch for limit orders, judged against the pair's own day. |
| from the venue | — | The venue's own filters, checked before it has to reject the order. |
Defaults lean towards refusing, because the two errors do not cost the same. A refusal costs a round trip and a message. A bad fill costs money and does not come back.
UNREADABLE is not in the table because it is not a rule. It is what happens
when the facts cannot be established, and it always refuses.
Design notes
Slippage is measured, not assumed. walkBook consumes real levels and
returns the average fill price across all of them. The top of book is not the
price a 500 BTC order gets.
Both sides of the book. A buy walks the asks, a sell walks the bids. A guard that checked one side would clear a sell into a hollow bid stack.
Position size is skipped, not guessed. Omit quoteBalance and that rule
does not run. Inventing a balance would be the exact failure this project is
about.
Every refusal cites its number. There is a test asserting it. A refusal an operator cannot check is an obstacle rather than a safeguard.
Symbol filters are cached for an hour — successes only. A failed lookup is never cached as "no filters", which would turn one bad minute into an hour of orders checked against nothing.
Tests
npm testTwelve tests over the pure rules, each one a thing that actually goes wrong when an agent trades unattended: stale quotes, hollow books, wrong side, fat fingers, dust, absent data. They run against fixtures rather than a venue, so they are deterministic and need no network.
npm run demo # the scenarios above, against the live venueLimitations, stated plainly
Spot only. Futures and margin have their own failure modes — leverage, liquidation distance, funding — and this build does not model them. It should.
The visible book is not the whole book. Iceberg and hidden liquidity mean
book_too_thincan be pessimistic. That is the safe direction, but it is a real limitation, not a feature.It cannot enforce anything. It is advice an agent must be instructed to ask for. A guard that could enforce would need to sit in the execution path and hold credentials, which is the trade-off this deliberately does not make.
Public data host by default.
api.binance.comreturns HTTP 451 from many hosts; the default is Binance's public market-data mirror (data-api.binance.vision), same endpoints and numbers. Override withBINANCE_API_BASE.
Provenance
The rules are not invented for a hackathon. They are ported from a Rust mint sniper the author has been running against live money, where the same principle was learned the expensive way — an unreadable value read as zero cost 229,074 gas across six wallets in a single run. That codebase's preflight module refuses to arm on a value it could not establish, for the same reason this one refuses to clear a trade.
MIT.