Most people who route orders non-custodially never think about the wiring until a fill fails for no obvious reason. The order looked fine. The balance was there. The price was inside the range. And it still did nothing, or worse, it reverted and burned gas. When that happens the problem is almost never the strategy. It is the plumbing underneath, the part that authorizes and settles the trade without anyone ever holding your keys.
I want to walk through that plumbing, because once you can see it, the failures stop feeling random. A non-custodial router does not move your funds the way a centralized exchange does. There is no internal ledger crediting and debiting an account you funded. Instead there is a chain of permissions and signatures, and each link has to hold for a fill to happen. When something breaks silently, it is usually one specific link, and it is usually the same handful of links.
What actually happens when you route an order
Strip away the interface and a routed order is three separate things stacked on top of each other. First, an approval that says a contract is allowed to move a specific token on your behalf. Second, a signed message that expresses your intent, the thing you want done and the terms you accept. Third, the settlement step where a solver, filler, or the contract itself actually executes against a venue and moves the tokens.
None of these steps takes custody. The approval lives on the token contract. The signature lives off chain until someone submits it. The settlement pulls tokens from your wallet at the moment of execution, using the approval you granted earlier. Your keys never leave your wallet. That is the whole point, and it is also why the failure modes are so unintuitive, because the authorization and the execution are separated in time and can drift out of sync.
On an EVM chain the approval is the classic ERC-20 allowance. You call approve on the token, naming a spender contract and an amount. From then on that spender can call transferFrom up to that amount. A router or its settlement contract is almost always the spender. If that allowance is missing, too small, or pointed at the wrong contract, the fill cannot pull your tokens and it reverts. Simple in theory. In practice this is where a large share of quiet failures live.
The approval traps that eat fills
Here are the allowance problems I see over and over, roughly in order of how often they bite.
- The allowance is for the wrong spender. You approved an old router version, or the aggregator upgraded its settlement contract, and your approval points at the deprecated address. The new contract has no permission, so transferFrom fails. Nothing in the interface warns you clearly, because you technically have an approval, just not the one that matters.
- The allowance is too small after a partial spend. If you approved an exact amount instead of an unlimited one, and part of it already got consumed, the remaining allowance may be under what the next fill needs. The order looks fully funded from your balance, but the spender is capped below the trade size.
- The token needs approval reset to zero first. A few older tokens, USDT being the famous one, refuse a new non-zero approval while an existing non-zero one is still set. You have to approve zero, then approve the new amount. Routers that do not handle this reset will just fail the approve step, and the user sees a stuck flow.
- Fee-on-transfer and rebasing tokens. If a token takes a cut on transfer or changes balances out from under you, the amount that lands at settlement is less than the amount the contract expected to receive. Strict settlement logic reverts on the shortfall. This is a token behavior problem, but it surfaces as a routing failure.
- Permit versus approve confusion. Many modern routers accept a signed permit, an off chain approval, so you skip the separate on chain approve transaction. If the token does not actually support permit, or supports a different permit standard than the router expects, the signed approval is worthless and the fill has no allowance to draw on.
My rule of thumb before routing size through any contract I have not used in a while is boring but it saves real money. Check what the current spender address is, check my live allowance to that exact address, and if I am unsure, revoke everything and re-approve fresh. An approval you forgot about is both a security surface and a silent failure waiting to happen.
Signed intents and the nonce problem
The second link is the signed intent. Instead of you submitting a transaction for every action, you sign a message describing what you want, and a solver or relayer submits it when the terms can be met. This is how a lot of gasless and intent-based routing works, and it is genuinely nice when it works. It also introduces a category of failure that has nothing to do with your balance.
Signed intents carry a nonce, a number that makes each signature usable exactly once and in a defined order. The nonce is what stops someone from replaying your signed order ten times. It is also what breaks routing in ways that feel like ghosts. If you sign two intents in quick succession and they share or misorder a nonce, one of them becomes uninnable. If an intent uses a sequential nonce and an earlier one is still pending, the later one cannot settle until the earlier clears, so it just sits there looking ignored.
Permit-style approvals have their own nonce, separate from your transaction nonce and separate from any intent nonce. That means a single routed action can involve two or three different counters, and any one of them being stale kills the fill. The classic trap is signing a permit, not using it, signing another, and now the first permit is invalid because the permit nonce moved on. The user thinks they have a valid approval signature. They have a dead one.
A short checklist I run when an intent-based fill silently does nothing:
- Is there an earlier signed intent still pending that shares the nonce sequence and is blocking this one.
- Did I re-sign and invalidate a previous permit or order without cancelling it cleanly.
- Has the signature deadline already passed, so solvers correctly refuse to touch it.
- Is the intent even valid on the chain and venue I think it is, or did the router route it somewhere else.
Settlement rules differ per venue, and that is the last trap
Even with a clean approval and a live signature, settlement is where per-venue rules take over. An AMM pool settles atomically in one transaction and either fills at a price inside your slippage bound or reverts. An order book venue may settle against resting liquidity that moved between quote and execution. A cross-chain route settles in two legs with a gap in between where the second leg can fail while the first already happened.
The practical consequences are worth internalizing. Slippage limits that are too tight cause reverts on volatile pairs, which read as failed fills rather than protection working as intended. Deadlines that are too short mean a slow block causes the whole intent to expire unfilled. And on multi-leg cross-chain settlement, the honest question is not whether it usually works but what happens on the leg that does not, because that is where funds can end up parked mid-route until a recovery path runs.
When we built routing across many venues at Blockcircle, the thing that mattered most was never the happy path. It was surfacing which of these links failed, so a stuck order came back as a specific reason rather than a shrug. Wrong spender, stale nonce, expired deadline, slippage revert, half-settled cross-chain leg. Those are five different problems with five different fixes, and treating them as one generic failure is how traders lose confidence in non-custodial routing entirely.
The mental model I keep is that custody never moved, so authorization and settlement are the only things that can break, and they break at named seams. Approvals point somewhere. Signatures carry counters and deadlines. Venues have their own settlement contracts. If you can name the seam, you can usually fix the fill in a minute instead of staring at a balance that was correct the whole time.