CCXT/Router beta

What the router decides for you

You say what you hold, what you want, and how much — from=SOL, to=BTC, amountIn=100. Everything else is the router's job. It works out which market that conversion lives in, which side of it you are on, which venue or venues out of roughly 60 exchanges streamed live over WebSocket give you the best price for that size, how much to send to each, and whether going through an intermediary asset beats trading the pair directly. It answers with the venues, the sizes, the expected average price, the fees, and how much worse that price is than the best price available anywhere. The computation takes about 0.3 ms, so the answer is as current as the last book update it received.

There is no side parameter, on purpose. Direction follows from the two assets: from=USDT&to=BTC is a buy of BTC/USDT, from=BTC&to=USDT is a sell of the same market. That is the step callers most often get backwards, so the router derives it rather than asking.

It does not place the orders. Nothing here touches your funds, your accounts, or your exchange credentials. It is a venue-and-price recommendation; you execute it. The last section spells out exactly where the line is.

Top of book is the wrong price

The best bid and best ask on your screen are the price for the next infinitesimal unit. They are not the price for your order, and the gap between the two grows with your size. A router that ranks venues on top of book is answering a question nobody asked.

So this one does not. For every venue quoting the pair, it walks that venue's book down to your actual size, computes the volume-weighted average you would really pay, and adds that venue's taker fee to every level before anything is compared. Only then are the venues ranked.

A worked example

Buying 5 BTC, two venues, both charging 10 bps taker. The numbers are illustrative; the mechanics are exactly what the router does.

VenueLevelAskSize there
Alpha1104,7800.4 BTC
Alpha2104,9401.1 BTC
Alpha3105,0803.5 BTC
Beta1104,8053.5 BTC
Beta2104,8806.0 BTC

Alpha has the better ask — 104,780 against Beta's 104,805, about 2.4 bps cheaper. Anyone ranking on top of book sends the order to Alpha. But Alpha only has 0.4 BTC at that price; the other 4.6 BTC comes off levels that are worse than anything Beta is showing.

Alpha alone,  5 BTC    VWAP 105,025.20    cost 525,126.00 USDT
Beta alone,   5 BTC    VWAP 104,827.50    cost 524,137.50 USDT    -18.9 bps
Both, merged           VWAP 104,819.50    cost 524,097.50 USDT    -19.6 bps

The venue with the best headline price is the more expensive fill by 988 USDT — about 19 bps. That entire gap is depth. It has nothing to do with fees, and no amount of clever splitting recovers it. Choosing the right venue for the size does.

Fees are folded into each level before the books meet, not netted off the total afterwards. That matters when venues charge differently: a venue 2 bps better on price and 5 bps worse on fees is the worse venue, and the comparison should say so at every level rather than at the end. It is also what makes the merged split below cost-minimal rather than a heuristic that usually works — once every level carries its true cost, taking them cheapest-first is provably the cheapest fill available across those books.

Price impact, and what to do about it

Every hop of every route reports two numbers that answer "how much did size cost me here":

  • referencePrice — the best fee-adjusted price on any fresh venue the router is permitted to use, for an infinitesimally small order. The frictionless price. In the example above, Alpha's 104,780 plus its fee.
  • impactBps — how much worse your actual size executed than that. The merged 5 BTC fill above lands 3.8 bps above the reference. Putting all 5 on Alpha because it had the best ask would have been 23 bps.

Positive always means worse, on both sides. Buying, positive means you paid above the benchmark; selling, positive means you received below it. There is no sign to flip depending on which way you are going.

The benchmark is deliberately the best price anywhere, not the chosen venue's own top of book. Measuring against the venue you ended up on would report zero impact for an order that demonstrably paid more than the market's best price, simply because the cheapest venue was too thin to use — a number that only ever measures a venue against itself. Fees and the staleness adjustment sit in both halves of the comparison and cancel, so what remains is purely the cost of consuming depth.

On a bridged route the same comparison runs end to end: the reference rate chains each hop's frictionless price and impactBps measures the realised rate against it, so both hops' depth costs land in one number rather than being left for you to combine.

Acting on it

Impact is only meaningful next to your expected move. Ten basis points is nothing on a position you expect to hold for a week and fatal on a two-day carry trade. Once you have compared it to your edge, there are four responses and they are not interchangeable:

ResponseWhat it changesWhen it is the right call
Trade smaller Fewer levels consumed, so a lower average per unit. Halving the size often more than halves the impact, because the levels you drop are the worst ones. The impact came from clearing several levels rather than from a wide spread.
Split across venues Fills the cheapest levels wherever they sit instead of only where you started. Measured gain: 0.19–2.2 bps. Several venues quote the pair and none of them is deep enough alone.
Wait Books refill. Nothing in the router models how fast — that judgement is yours. Impact dwarfs your edge and the trade is not urgent.
Accept it Nothing. You pay the impact and get the fill now. Impact is small relative to the move you are expecting.

Splitting will not rescue a large impact number. The measured gain from splitting is 0.19–2.2 bps. If impactBps reads 30, splitting recovers a rounding error of it. A big impact is telling you the order is large relative to the visible book, and the honest answers to that are a smaller order, more time, or a different venue — not a cleverer allocation.

When splitting helps, and when it does not

By default the router puts the whole order on one venue: best_single, the venue that fills your full size most cheaply after fees. Splitting is something you ask for, in one of two shapes.

  • split_optimal — every fresh venue's book merged into one price-ordered ladder and walked. Because each level was fee-adjusted before the merge, walking it cheapest-first is the lowest-cost fill available across those books at that instant.
  • split_capped with maxVenues (3 by default) — the same walk, then restricted to the venues carrying the most size and re-solved. Three venues capture about 95% of the unconstrained gain.

The honest size of the prize is 0.19–2.2 bps, depending on how the books happen to line up. On a 500,000 USDT order that is roughly 10 to 110 USDT. Set that against the 19 bps that venue selection was worth in the example above — nearly ten times the entire splitting range. Splitting is the last few basis points after the venue choice has done the real work, and it should be sized in your head accordingly.

What splitting costs you does not appear in any of those numbers:

  • Three venues means three orders, placed at slightly different moments, each with its own chance to miss. The router quotes them all against one instant; you cannot execute them at one instant.
  • You need balance on every venue in the route before you start. A three-way split you cannot fund is not a route, it is a plan to move money.
  • Small legs can fall below a venue's minimum order size or be too fiddly to bother with. minLegNotional is the control: set it to the smallest leg worth placing and anything under it is dropped and the rest re-solved.
  • More fills to reconcile, on more venues, at more prices.

On single-hop routes the response reports what the split actually beat the best single venue by, in basis points. Read that before you decide the extra orders are worth placing. When it comes back at 0.3, they usually are not.

Bridged routes: why SOL→BTC can come back as two trades

If no SOL/BTC book is being streamed, the conversion still exists — through an intermediary. The router builds every candidate it can: the direct market, plus one two-hop route per bridge asset that has both legs listed (USDT, USDC, BTC and ETH by default). Then it solves all of them and takes the best. It does not assume USDT is deepest and stop looking.

That distinction earns its keep. Live, USDC→TRY routes through ETH and beats the obvious USDT path by about 9 bps. A thin direct pair can likewise lose to going around it. Every candidate comes back with its output and its score, so "why this market?" is auditable in the same way "why this venue?" is.

The extra hop is charged for

A longer path must beat a shorter one by more than a hop penalty — 5 bps per extra hop by default — before it is allowed to win. A second order is a second chance for the price to move between fills, and that risk is in no order book. A bridge that wins by a hair is not actually the better trade. If you want the paths ranked purely on rate, hopPenaltyBps=0 does that, and you own the consequence.

What you are taking on

  • Two orders, in sequence. Between them you are holding the bridge asset and exposed to it.
  • The second hop is priced against its book as of the same instant as the first, but you will place it only after the first one fills.
  • Fees are reported per hop, in that hop's own currency, and there is no cross-hop total. Adding a USDT fee to a BTC fee produces a meaningless number, so the router declines to produce one.
  • Hops are solved sequentially, not jointly — the first hop's split is chosen without knowing what the second would have preferred. This is a good route, not a provably optimal one, and it is described that way in the response.

It will not strand you in the bridge asset

If the first hop can consume everything you offered but the second cannot absorb what it produces, the router cuts the first hop back to what the second will actually take. Without that it would recommend selling 10 SOL to buy 0.01 BTC and leaving the other 99 USDT parked in the bridge — while reporting a complete fill, because only the side you pinned was ever measured. Instead you get a proposal for the 0.1 SOL the route can genuinely complete, a fill ratio of 0.01, and a partial-fill warning whose percentage matches the numbers beside it.

One limitation to know before you ask: a bridged route can only be requested by what you spend (amountIn), not by what you want to receive (amountOut). Solving backwards from the destination across a bridge is a different traversal, and the router refuses it rather than approximating. The refusal is reported distinctly, so you know to retry with amountIn rather than concluding the two assets are unreachable.

Partial fills: read the fill ratio before the rate

fillRatio is the fraction of what you asked for that the route actually covers. A route that covers 62% of your size still reports an effective rate, and that rate is entirely real — for the 62%.

requested        5 BTC
fillRatio        0.62      -> the route covers 3.1 BTC
effectiveRate    ...       -> the average rate for those 3.1 BTC, not for 5
unfilledAmount   1.9 BTC   -> and this part would cost MORE than the rate above

The trap is arithmetic, not wording. The portion that filled is the cheap part of the book by construction; the levels that would have covered the remaining 1.9 BTC sit beyond everything already priced in. Multiplying the effective rate by your full size therefore understates the cost of the trade — always, and in the direction that flatters it. That is why the response carries an explicit partial-fill warning with the percentage in it, and why the rate is the second thing to look at rather than the first.

Two different situations both surface as a fill ratio below 1: a single market that simply ran out of depth, and a bridged route that was trimmed back to what its second hop could absorb. In both cases the rate you are looking at prices only what the route can actually complete.

If a partial fill is worse than no trade — you are closing a position, or the leftover leaves you unhedged — ask for requireFullFill=true and you get a clean refusal instead of a partial quote.

"Not fully fillable" can mean the router cannot see far enough, not that the liquidity is absent. Some venues stream capped depth — Kraken 10 levels, Gate 50 — so a large order can exhaust the cached book while real depth sits below it. On those venues, treat a partial fill on a large order as "unknown past this point" rather than "the market ends here", and check the venue directly before concluding your size is unfillable.

When the router refuses to quote

A response with no route and a stated reason is not the service failing. It is the service declining to hand you a price it does not believe. This matters more than it sounds: a quote built on a book that stopped updating four minutes ago is indistinguishable, on screen, from a good one. Books older than the freshness window — 5 seconds by default — are therefore excluded from ranking outright rather than quoted with a caveat.

ReasonWhat actually happened
no_marketNeither direction of the pair is streamed, and no bridge asset has both legs. There is nothing to route through.
all_books_staleVenues quote this pair, but every one of their books is older than the freshness window. Common on the illiquid tail. Retry, or widen the window deliberately.
no_liquidityFresh books, but nothing resting on the side you need to trade against.
insufficient_depthYou asked for all-or-nothing and the visible depth could not cover the size. Ask again without it to see how far it would have got.
no_venues_matched_filterYour own venue filter left nothing standing. Widen it.
exact_out_multi_hop_unsupportedYou asked by the amount you want to receive, on a conversion that needs a bridge. Ask by the amount you want to spend.

You do not configure any of this, and that is deliberate. Two things happen automatically. Books past the freshness cutoff are excluded outright. Books inside it are marked down as they age — the markdown grows with the square root of the book's age, which is how far a price is expected to have drifted since it was captured, so a four-second-old quote has to be genuinely better to win rather than merely look better.

Deciding whether a price is still real is the router's job. You cannot see the update rates that judgment is made against, and a millisecond threshold would be the wrong shape for the question anyway. What you do get is transparency: every response echoes the cutoff and the markdown that were applied, and every venue carries the age of the book it was priced from.

A refusal comes back as a normal, successful response with an empty route and a reason — not as an error. Anything you build on top of this should read the reason, not the status code.

What this is not

It is not an exchange, a broker, or a custodian. Precisely:

  • It never places an order. It holds no trading credentials and exposes no endpoint that could submit one — everything it serves is read-only.
  • It never holds or moves your funds. There is no account, no balance, no deposit address, no custody of any kind at any point.
  • It does not know what you own. It reads public order books. It does not see your balances, your positions, or your open orders.

What you get is a recommendation: trade these venues, in these sizes, expect roughly this average price, and here is how much worse than the best available price that is. Placing the orders is yours — and so is everything that happens between the quote and the fill.

Consequences worth being explicit about

  • The quote is a snapshot. It is computed from the last book updates received and is already history by the time you act on it. For anything bigger than a click-to-trade, use the streaming route, which recomputes and pushes a fresh answer whenever any market the route depends on moves — including every leg of every candidate path, so a bridged route does not miss half the price changes that would alter its answer.
  • The fees are the venues' public taker rates. If you trade on a better tier, your real cost is lower than quoted. Every price is walked as a taker; nothing here is a maker-fee estimate.
  • Moving funds between venues is not modelled. A three-venue split assumes you are already funded on all three. Transfer fees, withdrawal limits and confirmation times appear nowhere in the numbers.
  • Exchange minimums, lot sizes and precision are not checked. A leg the router allocates can be smaller than the venue will accept. minLegNotional is the control you have, and setting it is your call.
  • It is an expected price, not a firm quote. Nothing here is executable against, and nobody is on the other side of it.