Skip to content

perf(token-prices): price each token from a router solve, in one pass - #401

Draft
tamaralipows wants to merge 3 commits into
mainfrom
tnl/bf-token-pricing
Draft

perf(token-prices): price each token from a router solve, in one pass#401
tamaralipows wants to merge 3 commits into
mainfrom
tnl/bf-token-pricing

Conversation

@tamaralipows

@tamaralipows tamaralipows commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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

TokenGasPriceComputation had 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 BellmanFordAlgorithm relaxation 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_source returns them all where find_single_route read one index and discarded the rest. A token's price is buy_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.

option per computation vs main keeps up with blocks? fixes BSC USDT? price includes selling back?
main today (DFS + spread) 0.283 s baseline yes — 379 runs in 190 s no yes
#398 (median of deepest paths) unmeasured ~same expected yes yes yes
this PR (buy route only) 0.057 s 5x faster yes — 408 runs in 190 s yes no
buy + sell 5.960 s 21x slower no — only 29 runs in 190 s yes yes

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:

  • Gas converted into token terms (water_fill, bellman_ford) carries that error on a quantity that is already a small fraction of output.
  • Depth normalization (most_liquid) shifts edge ranking slightly between tokens with different exit costs.
  • hindsight's USD figures read slightly optimistic by the same amount.
  • A token that can be bought but not sold now prices normally. Nothing flags it. The old round trip would not have caught the BSC pool either — through one pool, slippage cancels and the trip stays symmetric — so this is not the protection that mattered there.

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

  • Token prices declare no requirements. They no longer wait on spot prices, and no longer fail when spot prices fail — spot_price_failure_does_not_stop_token_prices pins that.
  • No gas price is read, so the computation has no market-data failure mode left. The test that drove one via a market without a gas price is deleted along with its fixture.
  • pool_depths is no longer read here.

🤖 Generated with Claude Code

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>
@tamaralipows

Copy link
Copy Markdown
Contributor Author

Baseline to beat

derived_computation_duration_seconds{computation="token_prices"} from prod, last hour. The share is rate(duration_sum) — the fraction of wall-clock one process spends inside this computation.

chain p50 p95 share of wall-clock slowdown until it fills the block
ethereum 758 ms 990 ms 7.6% 13x
bsc 179 ms 385 ms 50.1% 2.0x
base 81 ms 238 ms 38.6% 2.6x
arbitrum 8.5 ms 48 ms 1.5% 65x
polygon 5.7 ms 23 ms 0.3% 290x
unichain 0.6 ms 1.8 ms 0.01% 11000x

For scale, on ethereum pool_depths is 99 ms and spot_prices 7.5 ms, so token pricing is already the expensive one by an order of magnitude.

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

run_spfa already fills the best amount at every node and find_single_route reads one index (bellman_ford.rs:270, :96-99). Exposing that array turns the forward leg into one pass per block for all tokens.

The reverse leg does not batch: it is many sources into one destination, each starting from its own buy_out, and slippage makes the relaxation amount-dependent, so one pass cannot serve all of them.

Two variants to build and measure against the table above:

  1. Forward-only — one pass per block, no round trip. Certainly faster than today. Gives a gross buy price; loses the sell side, so no mid price and no spread.
  2. All-destinations forward + N reverse solves — removes half this branch's cost. Unmeasured whether the total beats today.

🤖 Generated with Claude Code

tamaralipows and others added 2 commits August 4, 2026 13:54
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>
@tamaralipows tamaralipows changed the title feat(token-prices): derive token prices by solving with the router perf(token-prices): price each token from a router solve, in one pass Aug 4, 2026
@tamaralipows
tamaralipows marked this pull request as draft August 6, 2026 14:10
@tamaralipows tamaralipows self-assigned this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant