← All writing

Systems6 min read

One quote, every bridge

How BRDG asks every bridge that can serve a route, ranks quotes by what you actually receive, and the bugs that shaped its sanity checks and transfer tracking.

From Print.World · Aug 2025 – Present

At Print.World I've been building BRDG. The pitch is short: one request asks every bridge that can serve your route, ranks the answers by what you'll actually receive, and you sign the winner in your own wallet. It covers around 35 bridges across 73 chains.

35Bridges quoted on every request
73Chains reachable
10Normalized transfer statuses
300Routing tests with no infrastructure

Getting a quote from one bridge is easy. Getting honest, comparable quotes from 35 of them, and then tracking the transfer afterwards, is where the engineering lives.

What a bridge doesThe two chains never talk to each other directly. The bridge is the service in the middle that takes your deposit on one and delivers on the other.

Ranking by what you receive

The first decision was what to rank by. Dollars seem natural, but converting to dollars means depending on a price feed. If that feed goes down, every quote becomes unrankable at once, which is a bad failure mode for something whose whole job is ranking.

So quotes are ranked in the route's own token. For a fixed input, the best quote is the highest net output. For a fixed output ("I need exactly this much on the other side"), it's the lowest net input. No external price needed.

"Net" is doing a lot of work there. Bridges disagree about whether fees are already subtracted from the amount they report. So each quote marks which fees it has already accounted for, and the ranking subtracts only what's left. Without that, you double-count fees for some bridges and miss them for others, and the ranking quietly favours whoever reports most optimistically.

Quotes that expire too soon to realistically sign and send are dropped. A great price you can't use isn't a great price.

From one request to a ranked tableEvery quote is normalized to what you actually receive, checked against the others, and only then ranked.

Guarding the ranking

Aggregating 35 integrations means trusting 35 different interpretations of "amount". Token decimals vary from 6 to 18 and beyond, so a unit mix-up isn't off by a little: it's off by a factor of 10^2 to 10^12, and a ranking that sorts by "best output" will put the most wrong quote at the top.

So I designed the ranking to distrust outliers from the start, with two independent guards:

  • Relative to the crowd. Any quote more than 10x away from the median of the other quotes for the same route is excluded. On realistic transfer sizes, real price differences between bridges are measured in basis points, not multiples, so the threshold costs nothing for honest quotes. Tiny transfers are the exception, where a fixed gas fee can be a large share of the amount.
  • Relative to itself. A median needs other quotes, and some routes only have one bridge. So there's a second rule that needs no comparison: if a quote's output looks like its input echoed back in different units, it's excluded.
Two independent guards before rankingOne guard compares a quote to its peers; the other works even when a route has only one bridge.

A shared quote board

Many users look at the same popular routes. Polling every bridge separately for each viewer would be wasteful and would hit rate limits fast. Instead, each route and amount is refreshed once per interval and pushed to everyone watching. A hundred viewers cost the same as one.

Ranking without flicker

Live quotes wobble. If the top two bridges are within a hair of each other, the table would swap them on every refresh, which looks broken. So a bridge only overtakes the row above it if it beats it by a margin.

That rule has a subtle problem. "Beats by a margin" isn't a consistent ordering: A can be roughly equal to B, and B roughly equal to C, while A clearly beats C. Passing a comparator like that to Array.sort leaves the resulting order implementation-defined by the spec, and in practice the order can change between refreshes.

So I don't sort with it. Starting from the previous order, I look for adjacent pairs that are out of order by more than the margin and swap them one at a time until none are left. It's stable, predictable, and it only moves rows that genuinely should move.

A cache key that includes who you are

Quotes are cached. The obvious key is route and amount. But a bridge's quote often includes ready-to-sign calldata, and that calldata bakes in the sender and recipient addresses. With a key that ignored them, one user could be served calldata that sends their funds to another user's recipient.

So the cache key includes sender and recipient. It costs some hit rate. That's fine.

Tracking the transfer

Once you've signed, a bridge transfer can take seconds or much longer, and statuses come from many different APIs. Ours normalises them into ten statuses, split into terminal ones and ones we keep polling.

Transfer lifecycle, simplifiedTen normalized statuses, split into ones we keep polling and ones that are final. Not-found becomes UNKNOWN, never FAILED.

A few rules I designed in deliberately:

  • a transfer that repeatedly comes back "not found" becomes UNKNOWN, never FAILED; we don't tell someone their money is gone because an API was slow to index
  • a transfer filled with the wrong token, or less than expected, becomes PARTIAL, not success
  • we record the broadcast before sending it, and a sweep flags any record still missing a transaction hash, so nothing falls through the gap between "we sent it" and "we saw it"

Keeping the engine testable

The routing engine has no infrastructure dependencies. It talks to the outside world through five small injected interfaces, and lint rules enforce that it can't import anything else. That's what lets around 300 routing tests run with no Redis, no network, and no bridges.

Failure policy is decided per useThe same Redis outage means different things to different callers, so each one chooses how to fail.

Redis failure policy is decided per use, not globally. The routing state cache fails closed, because routing on stale state is worse than not routing. Rate limits fail open, because a limiter outage shouldn't stop users. And locks are released only by their holder, so a slow worker can't free a lock someone else now owns.

Docs that can't drift

The public docs at docs.brdg.now are generated from the live OpenAPI spec, so they can't fall out of date with the API. There's also a TypeScript SDK on GitHub.

What I took from it

Most of the interesting decisions here were about what to do when something is wrong or unknown: a price feed that's down, a bridge reporting nonsense, a status API that hasn't caught up. I keep relearning the lesson from the perps aggregator: pick the honest status, not the optimistic one.

Next on my list is making those failure policies visible on the board itself, so a user can see not just which bridge won, but which ones were excluded and why.