How LIQORA works
LIQORA has no backend, no database, no price feed and no API key. It is a handful of static files talking straight to an Ethereum node — mainnet for crypto, Robinhood Chain for tokenized equities. This page is the whole mechanism — the contracts it calls, the arithmetic it does with the answers, and the places where the number it shows you would differ from a real fill.
What "simulate" means here
It means eth_call. That is a real execution of the real EVM, on a real node, against the real
current state of the real pool contracts. The same opcodes run, the same pool code executes, the same
arithmetic happens as in an actual swap. The only difference is that the result is thrown away instead of
written into a block.
So it is not a model, an estimate, or a mock. When LIQORA prices a Uniswap v3 order it calls
QuoterV2, and that contract internally calls the pool's own swap() function and
reads back what came out. It is the same class of read a DEX front-end does to show you a price before you
press Swap.
Nothing moves. No transaction is broadcast, no tokens change hands, no signature is requested, no gas is spent, and nothing on-chain changes. LIQORA contains no swap function and no approval flow. If you act on what it tells you, you go and execute elsewhere — that is where real money is involved, and it is outside this app.
The difference in one table
| Real | Pool reserves and prices · fee tiers · output amount · ticks crossed · gas estimate · the block it was priced at |
| Not real | No broadcast transaction · no balance changes · no signature · no gas spent · nothing written on-chain |
The pipeline
One refresh is four network round trips, whatever the token.
head eth_blockNumber + eth_gasPrice + eth_chainId
→ pins the block every later call is made against
discover 21 calls: symbol / decimals / name, then getPool and getPair
across 4 fee tiers and 3 venues (cached — pool addresses never change)
state slot0 + liquidity + both balanceOf per v3 pool, balances per v2 pair
→ mid prices and pool values
quote every candidate route at your exact size ≈ 11 calls
→ ranked by output; the winner is the route
then, in parallel:
ladder every route at 4 sizes ≈ 44 calls
split top 3 routes × 10 chunk sizes = 30 calls
curve winning route at 10 sizes = 10 calls
That is roughly 160 eth_calls per view, which the Proof panel on the terminal
counts for you. They arrive as four JSON-RPC batches, not 160 requests.
Contracts it talks to
Nothing else. No proxy, no aggregator, no vendor.
Robinhood Chain — chain 4663, an Arbitrum Orbit L2. This is where the tokenized equities live and where their liquidity actually is. Uniswap v3 is officially deployed; there is no v2 or SushiSwap.
UniswapV3Factory 0x1f7d7550b1b028f7571e69a784071f0205fd2efa
QuoterV2 0x33e885ed0ec9bf04ecfb19341582aadcb4c8a9e7
USDG quote asset 0x5fc5360d0400a0fd4f2af552add042d716f1d168
NVDA NVIDIA 0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC
TSLA Tesla 0x322F0929c4625eD5bAd873c95208D54E1c003b2d
SPY SPDR S&P 500 0x117cc2133c37B721F49dE2A7a74833232B3B4C0C
AAPL Apple 0xaF3D76f1834A1d425780943C99Ea8A608f8a93f9
MSTR Strategy Inc. 0xec262a75e413fAfD0dF80480274532C79D42da09
Ethereum mainnet — chain 1, for the crypto markets. Uniswap v3, Uniswap v2 and SushiSwap.
UniswapV3Factory 0x1F98431c8aD98523631AE4a59f267346ea31F984
QuoterV2 0x61fFE014bA17989E743c5F6cB21bF9697530B21e
UniswapV2Factory 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f
UniswapV2Router02 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D
SushiSwapFactory 0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac
SushiSwapRouter 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F
USDC quote asset 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
WETH hop asset 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
Do not assume canonical addresses. The well-known Ethereum Uniswap
addresses have code on Robinhood Chain, but it is not Uniswap — getPool there returns
nothing. The real deployment is at the addresses above. Every one was confirmed by reading it from the
chain, and every token by reading its own symbol().
Calls are encoded by hand — no ethers, no viem, no library at all. Each function selector is the first four bytes of the keccak-256 hash of its signature, computed rather than copied:
getPool(address,address,uint24) 0x1698ee82
getPair(address,address) 0xe6a43905
slot0() 0x3850c7bd
liquidity() 0x1a686502
balanceOf(address) 0x70a08231
quoteExactInputSingle((address,address,uint256,uint24,uint160)) 0xc6a5026a
quoteExactInput(bytes,uint256) 0xcdca1753
getAmountsOut(uint256,address[]) 0xd06ca61f
Finding every pool
A Uniswap v3 pool exists at one address per (token A, token B, fee tier) triple, and the factory will tell you which. LIQORA asks for all four tiers — 0.01%, 0.05%, 0.30% and 1.00% — for three pairings:
- USDC / TOKEN — the direct market
- WETH / TOKEN — the second leg of a routed trade
- USDC / WETH — the first leg, and the dollar anchor for everything else
Then it asks the Uniswap v2 and SushiSwap factories for the same three pairs. A zero address means no
pool, and that tier is dropped. Whatever comes back becomes the candidate set — direct pools, plus every
USDC → WETH → TOKEN path that both legs exist for.
This matters more than it sounds. For long-tail tokens the direct USDC pool is usually a ghost, and the real market is the WETH pair. Pricing only the direct pool would give you a confidently wrong answer.
Token identity is read from the contract too — symbol(), decimals() and
name() — so pasting an unknown address behaves exactly like one of the built-in markets. Old
tokens that return a raw bytes32 symbol instead of a string are handled.
Pool addresses are immutable, so discovery is cached per token for the session. Only their state is re-read on refresh.
Reading pool state
For each v3 pool: slot0() gives the current sqrtPriceX96 and tick,
liquidity() gives the in-range liquidity, and balanceOf on both tokens gives what
the pool actually holds. For each v2 pair, the two balances are enough — its price is its reserve
ratio.
One call is deliberately absent. Uniswap orders a pool's tokens by address, so token0 is simply
whichever of the two addresses sorts lower. That is decided locally instead of asking the chain, which drops
one call per pool.
Turning sqrtPriceX96 into a price
Uniswap v3 stores the square root of the price, scaled by 296, as an integer. Recovering a human price means undoing both the square and the scaling, and correcting for the two tokens' decimals:
// price of token1, denominated in token0
price = (2**192 * 10**dec1) / (sqrtPriceX96**2 * 10**dec0)
This runs in BigInt from end to end. Doing it in floating point loses precision exactly where it
hurts — a token priced at 0.0000037 and a token priced at 78,000 have to come out of the same expression.
An early version of this had the direction backwards. Whether the formula or its reciprocal is correct depends on which of the two addresses sorted lower, and PEPE sorts below USDC. It reported PEPE at $270,647 instead of $0.0000037. The orientation is now derived from the address sort, the same rule the pool itself uses.
Pricing the order
Three different venues need three different calls, and each returns the true output for your exact size — not a mid price with a fee subtracted.
Uniswap v3, one hop
QuoterV2.quoteExactInputSingle walks the tick range your order would actually cross. It returns
the output amount, the price the pool would be left at, how many initialised ticks were crossed,
and a gas estimate. The tick count is the honest measure of how hard the order pushed: crossing one tick is a
rounding error, crossing eighty means you ate through the book.
Uniswap v3, two hops
quoteExactInput takes a packed byte path — token, fee, token, fee, token — and chains the swaps,
feeding the first pool's output into the second. Its return carries an array of ticks crossed, one per hop,
which LIQORA sums.
Uniswap v2 and SushiSwap
getAmountsOut on each router. Constant-product venues have a closed-form answer:
amountOut = (in * 997 * reserveOut) / (reserveIn * 1000 + in * 997)
The 997/1000 is the 0.30% fee. LIQORA could compute this locally from the reserves, but it asks the router instead so that every number on the page has the same provenance: an on-chain call.
Every candidate is quoted at your size in a single batch, then ranked by output. Most fill. Some revert, because the pool cannot supply that much — a revert is a real answer, and those routes are counted at the foot of the venue table rather than hidden.
The reference mid
The hardest number on the page to get right.
Every cost figure is measured against a mid price, so a wrong mid poisons the whole page. The problem is that a token has as many mid prices as it has pools, and abandoned pools keep quoting nonsense forever.
LIQORA collects a mid from every venue that prices the token — v3 pools from sqrtPriceX96, v2
pairs from their reserve ratio, WETH-quoted pools multiplied by the ETH anchor — and then picks between them
with one rule:
Rank candidates only on the quote asset they hold. How much USDC is in this pool, or how much WETH. Never the token side.
Score a pool by its total value and a broken pool can promote itself: it marks its own token holdings at its own wrong price, reports a huge number, and wins. Scoring only the quote side makes that impossible — dollars are dollars regardless of what the pool believes the token is worth.
The ETH/USD anchor is chosen the same way: whichever USDC/WETH venue holds the most USDC. The panel always names the pool it used, so you can check the choice rather than trust it.
This was the second bug. The first version only considered v3 pools for the mid. PEPE's real depth is a Uniswap v2 pair holding thousands of WETH, while its v3 pools are nearly empty — so the mid came from a dead pool. Candidates now span every venue discovered.
Cost vs mid = fee + slippage
The headline cost is what the trade really costs against the reference mid, in basis points:
buy cost = (fillPrice / mid - 1) * 10000
sell cost = (mid / fillPrice - 1) * 10000
That total is then split into the pool's stated fee and whatever is left, which is the price impact your own order caused. The split is the point. A 0.01% tier looks unbeatable on fee alone, but if it holds no liquidity your order crosses dozens of ticks and the slippage dwarfs the saving. Showing 5 bps fee + 47 bps slippage makes that visible immediately.
Past 1,000 bps the display switches to percentages, and past 1,000,000 bps it shows
> 10,000%. Those rows are abandoned pools quoting absurd prices. They are left in the table
on purpose — knowing which venues to avoid is worth as much as knowing which one to use.
Split routing
When one pool cannot absorb the order cheaply, spreading it across several can beat it. LIQORA takes the three deepest venues, quotes each of them at 10%, 20% … 100% of the order — thirty on-chain quotes — and then allocates ten chunks one at a time, each to whichever venue offers the best marginal improvement.
Greedy allocation is optimal here rather than merely convenient. Each venue's output is a concave function of size: every additional chunk into a pool gets a worse rate than the last. For concave curves, taking the best next step each round produces the best whole allocation on that grid.
The result is then charged for what it costs. Three legs are three swaps, so the gross improvement has to clear the extra gas before LIQORA calls it a gain. It frequently does not, and the panel says no gain instead of dressing up a rounding error as alpha.
Allocations resolve to 10% steps, not a continuous optimum. A finer grid would mean more quotes for a difference that gas would eat anyway.
The route quality score
One number, and every point accounted for.
A score is only useful if you can see how it was built, so this one starts at 100 and subtracts named, inspectable penalties. The panel on the terminal prints each line with the figure that produced it.
start 100
Cost vs mid min(55, costBps / 2) up to -55
Depth vs order 50x+ 0 | 20x -4 | 10x -8 | 5x -14 | below -20
Venue breadth 5+ 0 | 4 -3 | 3 -6 | 2 -10 | 1 -15
Two-hop route -4 two swaps, two failure points
Ticks crossed >60 -6 | >25 -3 | else 0
Cost dominates on purpose: it is the part you actually pay. Depth is measured against your order, not in absolute dollars, so a $2M pool scores well for a $10K order and badly for a $1M one. Breadth is there because a single fillable venue is fragile — if it moves before you trade, you have no fallback.
The score never decides anything on its own. It is a summary of numbers that are all on the page already, and every one of them can be checked in the Proof panel.
The impact curve
The winning route is re-quoted on-chain at ten sizes from $1K to $1M and plotted on a log axis. Nothing is
interpolated or fitted — every point is its own eth_call.
The shape is the useful part. A deep market stays flat then bends late; a thin one bends almost immediately. The bend is where that market stops absorbing you cheaply, and it is usually nowhere near where a single quote would suggest. The size ladder answers the neighbouring question: at which size does the winning venue change?
One block, few requests
Two rules keep a view coherent.
Everything is pinned to one block
The first call fetches the block number, and every subsequent call passes that exact block instead of
latest. Without it, a refresh spanning a new block would compare a price from block N against a
quote from block N+1 — a difference that looks like an arbitrage and is really a bug. The block is printed on
the decision panel and in Proof.
Batches, and one endpoint per refresh
Calls go out as JSON-RPC arrays: about 160 calls become four HTTP requests. The endpoint that answers the first call is pinned for the rest of that refresh, because a different node might not have the pinned block yet. Three public endpoints rotate on failure, and you can point it at your own node in Settings.
If a refresh fails, the last good view stays on screen and the QUOTE age counter keeps climbing — amber past 45 seconds, red past 90. Stale data announces itself rather than vanishing or quietly pretending to be fresh.
Verify it yourself
You should not have to take any of this on faith.
The Proof panel on the terminal prints the raw calldata behind the current view with a copy-as-cURL button. Paste one into a terminal and any mainnet node returns the same bytes.
The check that settles it
Read a Uniswap v2 pair's reserves straight from the contract, apply the constant-product formula by hand, and compare against what the router returns for the same input. On the USDC/WETH pair for 25,000 USDC:
reserves 10108951132306 USDC 4050835148640753561913 WETH
computed by hand 9963321513077875279
returned by chain 9963321513077875279 identical, digit for digit
A fabricated number cannot land on that value. It falls out of reserves anyone can read.
A second check
Ask the quoter for an impossible order. A $100 billion buy does not revert and does not return a made-up
figure — it returns roughly 8,900 WETH, because that is genuinely what draining the curve gives you. Correct
arithmetic, useless trade, and the cost column marks it > 10,000%.
What is built on the engine
Everything else on the terminal is the same pipeline, re-aimed.
| Markets board | The whole pipeline run for all twelve markets at your current size, four at a time. Sortable on every column; the star pins to a watchlist kept in your browser. |
| Change % | Real, and honest about its window. Uniswap v3 pools carry their own TWAP oracle, so
LIQORA asks observe() for 24h and falls back through 6h, 2h, 1h and 15m until the pool's ring
buffer can answer. The label always says which window was used, and a WETH-quoted market is combined with
the ETH anchor's own TWAP so the figure is a dollar move. |
| Spread | Distance between the best and worst fill, measured only across venues within 5 percentage points of the best. Include the abandoned pools and every spread reads "> 10,000%" and tells you nothing. With fewer than two usable venues it reports nothing at all. |
| Order simulator | Your slippage tolerance against the live quote: minimum received, cost in dollars, and whether the tolerance is even wide enough for the route to clear. |
| Execution checklist | Five pass/fail checks — depth, cost, breadth, freshness, gas share — each showing the number behind it. |
| Route inspector | Click any venue row for that pool's raw state: address, token order,
sqrtPriceX96, tick, in-range liquidity, both balances. |
| Compare and basket | Two markets side by side, or a whole list of lines priced at once with a total execution cost measured against each market's own mid. |
| Live trades | Real Swap events from the winning pool, decoded and summarised with
buy/sell counts and median size. Public endpoints serve only short recent ranges and refuse
anything older; when an endpoint refuses, the panel says so and stays empty rather than showing an
invented feed. |
| Alerts | A condition on score, cost, price or venue count, checked whenever that market is repriced. Fires as a toast and, with permission, a browser notification. |
| Share | A text snapshot, a deep link, or a PNG route card drawn on a canvas from the live route. |
Limits
Where the number shown would differ from a real fill.
- It does not trade. No execution, no approvals, no custody, no signatures. That is a design decision, not a missing feature, and the roadmap does not secretly end in a router.
- A quote is one block old the moment you read it. Price moves between the quoted block and any transaction you send.
- MEV is not modelled. A sandwich attack on your real transaction would make your fill worse than the quote.
- Fee-on-transfer tokens. The quoter models the pool, not a token that skims a cut on transfer. Those settle under quote.
- v2 and SushiSwap gas is a typical constant (120k single-hop, 190k two-hop) and is marked
≈. Those routers expose no gas figure. Uniswap v3 gas comes from the quoter. - Split allocations resolve to 10% steps, not a continuous optimum.
- Only Uniswap v3, Uniswap v2 and SushiSwap are searched. Curve, Balancer, Uniswap v4 and private market makers are not, so "best venue" means best among those three.
- USDC is assumed to be one dollar. Everything is priced against it.
- Absurd sizes return real but useless numbers rather than refusing.