3 ใน 4 ของ call บน HyperEVM และ 1 ใน 3 บน BNB ยิงไปหา pool ที่ไม่มีอยู่จริงหรือไม่มีสภาพคล่อง — SDK สร้าง model ให้ทุกคู่ token × ทุก route โดยไม่เคยเช็คว่า pool ถูก deploy และไม่จำผลไว้ข้ามการ quote
Three quarters of the multicall inner calls on HyperEVM (one third on BNB) are spent on pools that were never created or have zero liquidity. The SDK instantiates a model for every token pair on every route without checking pool existence, and rebuilds everything from scratch on each quote. Fixing that takes HyperEVM from 26 requests per quote to 2 with byte-identical prices. Cutting correspondent tokens or routes blindly, the tempting fix, measurably worsens prices.
Every getQuote() does exactly one round of network I/O: it registers models, collects each model's state calls, packs them into Multicall2.tryAggregate batches, fires all batches at once, then computes routes purely in memory. So RPC cost is decided by two things only: how many models get registered and how they are batched. Split on/off, trade size, and gas price change nothing on the wire (verified).
pairs = generateAllTokenPairs(src, dest) HyperEVM 11 pairs (4 corr tokens) · BNB 28 pairs (8 corr tokens) models = Σ routes × pairs the route "supports" every V2/V3 route claims EVERY pair → 289 / 914 models calls = Σ models × state calls V2: 1 · V3: 4 · Hybra V4: 5 · Solidly: 4 · Curve: 3 + n requests = ceil(calls / maxCallsPerFetch) + 1 (+1 = a separate eth_call for dest.decimals())
UniswapV2Model.getTradablePairs() returns the pair unconditionally and the V3 models compute the pool address off-chain, so nothing ever checks that the pool was deployed. A new DexManager is created per quote, so the same non-existent pools are queried again on every quote, forever.0x) or responded with zero reserves / liquidity.| pair | calls | live | wasted | waste % |
|---|
Every pair on HyperEVM lands at 72–77 % waste; every BNB pair among the correspondent tokens resolves to the same 28-pair matrix and the same 36 %.
| family | models | calls | ok | dead (0 liq.) | empty (no contract) |
|---|
On HyperEVM, 350 of the 572 tick-querier calls (the heaviest call type, ~500 B each) go to pools that do not exist; tick data is 91 % of the 327 KB payload. Five routes never have a usable pool for any pair tested and cost 231 calls (19 %) per quote: hybra-v4-100, laminar-v3-10000, ultrasolid-v3-10000, prjx-v3-10000, hybra-v3-7500.
maxCallsPerFetch50 (today) → 200 cuts requests 3.6× at the same latency. A single 1,300-call batch reverts on the public node. Sequential batches at 50 take 7.7 s, which is why the SDK fires everything in parallel today.
bsc-dataseed serves many small batches faster than a few big ones (100 → 1.06 s, 500 → 1.6–1.9 s, 1,000 → 4.5 s). The batch size must be a per-provider profile, not a per-chain constant.
Cutting intermediate tokens does slash RPC cost, but it removes exactly the hops that large trades need. Measured with the real getQuote():
| configuration | requests | 1 unit | 100 units | 1,000 units |
|---|---|---|---|---|
| HyperEVM · 4 corr tokens (today) | 26 | 79.397 USDT0 | 7,938.07 | 79,309.14 |
| HyperEVM · 2 corr tokens (WHYPE, USDT0) | 10 | 79.388 (−0.01 %) | 7,930.21 (−0.10 %) | 78,508.06 (−1.0 %) |
| BNB · 8 corr tokens (today) | 6 | 726.32 USDT | 72,604.76 | 722,941.59 |
| BNB · 4 corr tokens | 3 | 726.34 | 72,604.04 | 721,588.18 (−0.19 %) |
| HyperEVM · drop the 5 dead routes | 21 | identical | identical | identical |
| HyperEVM · batch 200 | 8 | identical | identical | identical |
decimals() × 112 and factory.getFee() × 56 — both immutable. Velodrome/Hybra V4 fee() × 58, Curve A/fee/totalSupply.dest.decimals() is a separate eth_call outside the multicall: +1 request per quote on a 100 req/min budget.Promise.all with no concurrency cap, retry or back-off — this is what trips the public limiter (429).getReserves() exists only for the Uniswap V2/V3 families (35 model files omit it, so the liquidity validator would throw for Curve/Solidly/DODO/Balancer); dagre, @xyflow/react are unused runtime dependencies that pull React into every install; the bundle (80 KB gzip) ships all 7 chains' route tables and 55 ABIs to every consumer.| # | change | HyperEVM req | BNB req | price risk | effort |
|---|---|---|---|---|---|
| R1 | Pool registry: per-chain JSON of (route, token0, token1) → pool | null generated offline by the profiler and shipped with the SDK, plus an in-memory negative cache learned at runtime | 26 → 8 | 6 → 3 | none | M |
| R2 | Batch profile per provider: HyperEVM 200, BNB 100–250, adaptive halving on revert / 429 | 8 → 3 | – | none | S |
| R3 | Fold decimals() into the multicall; cache decimals in the registry | −1 | −1 | none | S |
| R4 | Static-data cache with TTL (Solidly decimals/fee, Velodrome fee, Curve A/fee) | – | −226 calls | none | S |
| R5 | Retire the 5 dead HyperEVM routes; add a route-health job that flags routes with 0 live pools | −231 calls | −84 calls | none (measured) | S |
| R6 | Adaptive tick window from trade size (halves payload for typical trades) | bytes −50 % | bytes −50 % | small · verify vs QuoterV2 | M |
| R7 | Reusable DexManager + block-scoped state cache (re-fetch only when the head advances) | repeat → 1–2 | repeat → 1–2 | none | M |
| R8 | Concurrency limiter + retry/back-off in multicallBatches | fewer 429s | – | none | S |
| R9 | Lightweight build: per-chain entry points, drop unused deps, delete *.optimized.ts | 80 KB gz → ~25–30 | – | none | M |
| R10 | Fix the getReserves() contract; document volumns semantics (last share is the remainder) | – | – | none | S |
| pair | today | after | Alchemy CU / quote | quotes / month, Alchemy free tier |
|---|---|---|---|---|
| HyperEVM 1 HYPE → USDT0 | 26 req · 1,216 calls · 327 KB | 2 req · ~320 calls · ~215 KB | 676 → 52 | 44 k → 577 k |
| BNB 1 BNB → USDT | 6 req · 2,053 calls · 489 KB | 2–3 req · ~1,300 calls · ~390 KB | 156 → 52–78 | 192 k → 385–577 k |
Do not: cut correspondent tokens (§5), disable split (no RPC saving), or replace Multicall with per-pool calls. Suggested order: R5 + R9(deps) + R10 as 1.1.1 → R1 + R2 + R3 as 1.2.0 → R4/R6/R8 → R7 + per-chain builds as 1.3.0.
After optimisation a quote needs 2–3 eth_call batches of ≤ 200–500 calls. Choose on batch tolerance, requests/second and HyperEVM support; archive/trace/WebSocket do not matter here.
| HyperEVM (999) | free tier | paid from | note |
|---|---|---|---|
| rpc.hyperliquid.xyz/evm (official) | ~100 req/min per IP, no signup | – | ≈4 quotes/min today → ≈50 after R1–R3; accepts 500-call batches, rejects ~1,300 |
| Alchemy | 30 M CU/month permanent, ~300 rps | $5 PAYG | best free tier; CU model rewards fewer, larger batches |
| dRPC | public/aggregated, 40–250 rps | ~$6 / 1 M req | good for testing, latency varies |
| Chainstack | 3 M req/month, 25 rps | $49/mo | also HyperCore |
| QuickNode | 1-month trial | $49/mo | lowest latency in third-party comparisons |
| HypeRPC (Imperator) | 10 k req/mo, 5 rps | $99/mo | Hyperliquid-only |
| BNB Chain (56) | free tier | paid from | note |
|---|---|---|---|
| bsc-dataseed*.binance.org (official) | 10 k req / 5 min, no eth_getLogs | – | prefers 100–250-call batches; fine for the optimised SDK |
| NodeReal MegaNode | free tier | paid tiers | BNB Chain's partner, lowest p95 on BSC in public benchmarks — production pick |
| Ankr | 500 M CU/day public | paid | generous, slower |
| Alchemy | 300 M CU/month | $5+ | same CU maths as above |
| PublicNode / 1RPC / dRPC public | free | – | fallbacks for the UI |
benchmark/rpc-profile.ts rebuilds exactly what MoroBestRate._fetch() registers, executes the same batches itself, and attributes every inner call to route / model / selector, classifying each model as ok, dead (0 liquidity), empty (no contract), or failed (revert). Dead is only detectable for the Uniswap V2/V3 families (see §6).benchmark/quote-quality.ts runs the real getQuote() at 1 / 100 / 1,000 units under different configurations and compares amountOut, requests and time.benchmark/profile.sh --chain hyperevm --src HYPE --dest USDT0 --live-only
benchmark/profile.sh --chain bsc --batch 100 --corr 4 --json out.json
npx ts-node -T -O '{"module":"commonjs"}' benchmark/quote-quality.ts --chain hyperevm --drop hybra-v3-7500
Full write-up with every table: INVESTIGATE.md on branch feat/optimize at korrio/moro-sdk-optimized. Production SDK on npm is unchanged: @moromoro/moro-sdk.
Each optimisation round is measured by benchmark/ab-test.ts: the published npm SDK (A) and the candidate build (B) run the identical workload (3 pairs × 1 / 100 / 1,000 units per chain) with every eth_call pinned to the same block, interleaved with alternating order, N repetitions, medians reported. The gate is on price: B fails if its amountOut is worse than A's by more than 5 bps on any case. Cost improvements are reported, never assumed.
| metric | definition | pass condition for a round |
|---|---|---|
| Δ price (bps) | (B − A) / A × 10,000 at the same block | ≥ −5 on every case (0 expected for R1–R5) |
| requests | JSON-RPC requests per quote | ↓ (the headline number for rate limits and billing) |
| inner calls · empty/failed | calls inside multicall batches · those returning 0x or reverting | waste → 0 |
| bytes · median ms | response size · wall time over N runs | not worse; latency judged on a private node only |
Protocol, metric definitions, per-round template, and pros/cons of R1–R10: benchmark/AB-TESTING.md in the optimize repo. Results of each round are committed under benchmark/results/ and summarised as Report #2, #3, … on this site.