perf(token-prices): price each token from a router solve, in one pass - #401
perf(token-prices): price each token from a router solve, in one pass#401tamaralipows wants to merge 3 commits into
Conversation
Pricing had its own path search: a DFS from the gas token, its own ranking rule, its own cap on how many candidates to simulate. That is a routing algorithm, and a worse one than the router next to it — every wrong price it produced was a ranking rule answering a question the router already answers by construction. Each token is now priced from two solves: gas_token -> token for the simulation amount, then token -> gas_token for what came back. The price is the mean of the two, held as an exact fraction. Both solves run Bellman-Ford with gas-aware scoring off, which is what makes this non-circular: gas-aware scoring converts a route's gas into output-token terms and so needs the prices being computed. Nothing here reads derived data, so token prices now require no other computation and survive a spot-price failure instead of cascading with it. A gas price is no longer needed either, so the computation has no market-data failure mode left; the test that drove one is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Baseline to beat
For scale, on ethereum BSC and Base are the binding constraints, not ethereum. The bar is no slowdown at all, so this branch as written — two BF solves per token — is not mergeable on cost. Path to meeting it
The reverse leg does not batch: it is many sources into one destination, each starting from its own Two variants to build and measure against the table above:
🤖 Generated with Claude Code |
SPFA fills the best amount at every node and find_single_route read one index out of it, so pricing N tokens ran N relaxations for a result one already held. find_routes_from_source returns the whole set. The buy side is now one pass per block regardless of how many tokens are priced. The sell side stays one solve per token: it is many sources into one destination, each starting from its own buy output, and slippage makes the relaxation amount-dependent, so those cannot share a pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Solving the sell leg as well measured 5.960s per computation against main's 0.283s on BSC, and stopped keeping up with the feed: 29 runs in 190s where main managed 379. Those solves are many sources into one destination, each starting from its own buy output, and slippage makes the relaxation amount-dependent, so they cannot share the buy side's single pass. One per token does not fit in a block. Pricing from the buy route alone measures 0.057s, five times faster than main, and keeps up at 408 runs in 190s. The price no longer carries what selling the token back would cost: about a fee for an ordinary token, more for one with a transfer tax or a thin exit. Nothing reads the sell side or the spread, so no consumer breaks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follows Markus's point on #398: pricing should not be a second routing algorithm. #398 is closed — it extended the bespoke path search this PR deletes, and this PR fixes the same BSC mispricing.
What it replaces
TokenGasPriceComputationhad its own path search — a DFS from the gas token, its own ranking rule, its own cap on candidates to simulate. Every mispricing it produced was that ranking rule getting a question wrong that a router answers by construction.How it works
One
BellmanFordAlgorithmrelaxation from the gas token. SPFA already fills the best amount at every node, so a single pass yields the buy route to every token it reaches —find_routes_from_sourcereturns them all wherefind_single_routeread one index and discarded the rest. A token's price isbuy_out / simulation_amount.The router runs with
gas_aware(false). That is what keeps it non-circular: gas-aware scoring converts a route's gas into output-token terms, which needs the prices being computed. Bellman-Ford is the algorithm that can do this — MostLiquid cannot, since it normalizes pool depth through the same prices.This also fixes the BSC mispricing on its own. The router ranks by output: buying USDT with 1 BNB, the lopsided pool returns 0.046 and the real pool returns ~565, so the bad pool is never selected.
Measured
Live BSC Tycho,
--min-tvl 10, 2 hops, ~190 s per run. Times are mean per computation.Local run on a public RPC, so compare the rows to each other, not to prod's 179 ms p50.
Limitation: the price is what you pay to buy, not what you would realise selling
Selling back is not solved. Those solves are many sources into one destination, each starting from its own buy output, and slippage makes the relaxation amount-dependent, so they cannot share the buy side's single pass — one per token is the 5.960 s row above.
So the price omits what leaving the token would cost: roughly one fee for an ordinary token, more for one with a transfer tax or a thin sell side. Concretely:
water_fill,bellman_ford) carries that error on a quantity that is already a small fraction of output.most_liquid) shifts edge ranking slightly between tokens with different exit costs.Nothing reads the sell side or the spread (#398 removed the last consumer of spread), so no computation breaks.
If the sell side turns out to be needed, the affordable shape is to solve it only on the per-block path, where just the tokens whose routes touched a changed pool are re-priced, rather than in a full sweep. Unmeasured.
Other consequences
spot_price_failure_does_not_stop_token_pricespins that.pool_depthsis no longer read here.🤖 Generated with Claude Code