Hybrid swap should default to a price-optimal pool/book split #501
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#501
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
The hybrid pool/book split is chosen by the caller, not solved for.
TradeMarketOrderPanelexposes auseHybridBooktoggle plus a manually typedbookInputHuman, and the pair contract'shybrid_simulationtakespool_input/book_inputas given. Nothing computes the split that actually maximises the taker'sreturn_amount.The practical effect on columbus-5 mainnet is that the limit book receives almost no flow: 12 of the 13 venue trades since 2026-07-19 were pool-only (
book_input = 0). Takers get a worse price than they could, and makers get close to zero fills, which in turn discourages anyone from posting book liquidity at all.Evidence
The one book leg in that window (tx
C9785A0CE66FEF698B671FF078A3D286F51827ECB4353453D47DCD7DFFA83E85, 2026-07-22) used a split that was worse than pool-only: 94.779 vs 95.168 CORAL for 100 EMBER. So even when a user does opt in, the manual control can make their fill worse.Measured today on EMBER/CORAL (
terra1klwuxas6x7p6fjde60kq70t0hu86wvt3fvyr2vgs0nn32fnv0q4qwznwp4) viahybrid_simulation, selling 100 EMBER, sweepingbook_inputwithmax_maker_fills: 10:The curve is single-peaked: routing too little to the book leaves maker liquidity unused, routing too much walks past it into worse resting orders. The optimum is interior, so neither of the two values a user is likely to pick by hand (0, or the full amount) is close to right.
At smaller sizes the gap is larger — for a 10 EMBER sell, book-first returns 9.565380 vs 9.525202 pool-only, +0.422% for the taker.
Suggested change
Default the panel to solving for the split rather than asking for it: bisect (or ternary-search)
book_inputover[0, offer_amount]againsthybrid_simulationand submit the maximiser. The response is monotone enough that a handful of probes converges, and the simulation is already being called for the quote.Keeping the manual
bookInputHumancontrol behind an advanced/expert toggle would preserve the current behaviour for anyone who wants it.Why it matters beyond price improvement
Maker participation is gated on fill probability. As long as book routing is opt-in and hand-sized, resting liquidity is close to unfillable regardless of how competitively it is priced, so the book stays thin, so opting in stays unattractive. Making the split automatic is what breaks that loop — and it matters most at the moment a real token lists, when there is genuine volume to route.
Found while running a market-making bot against the soft-launch pools. Happy to share the sweep script or re-run it on other pairs if useful.
Two weeks of maker-side data from mainnet that bear directly on this, including the first book legs the venue has taken.
On 2026-07-28 I moved a resting ladder on EMBER/CORAL (
terra1klwuxas6x7p6fjde60kq70t0hu86wvt3fvyr2vgs0nn32fnv0q4qwznwp4) inside the pool's 180bps band, so every rung was strictly better than the AMM touch on both sides. Two things followed.The book-crossing route works, and takers who use it gain. Two swaps have crossed the book since:
71C06267E93AE3BBFB5234FF2D8FF2FB77AC2A2EEA42F700DF5AADA741F07C66B0F2EDDB5ACFE2ADE5B6D835F490A033B78DE8730CEBFD76734756161E9CB4BFBoth from
terra1smg8tks5g68u7umhvlcf8d232uru7wecqgkh8z. That same address made five earlier swaps on the pair, all withbook_return_amount = 0-- correctly, because until 07-28 my rungs sat outside the fee band and the pool really was the better price. First trade after the ladder moved inside the band, it crossed. So the router does consult the book and does prefer it when it is genuinely better.But the behaviour is per-caller, and the callers that skip the book pay for it. On the same pair, 2026-07-29 11:22:15Z, tx
A8F9E3F92C18F5EB9ED3D0538E36E07F680B0258BA666DCDF5A6AD039719B1D2fromterra1rf4u54vh3m6qvd3crwrzwjw3s0ysat7a308c0poffered 50 EMBER and returned 47.437778 CORAL withbook_return_amount = 0-- an effective 0.94876. Reconstructing from my own order records, at that point I had bids resting at 0.961439 / 0.956608 / 0.951777 with roughly 28.98 CORAL of aggregate depth, all unfilled and later cancelled on TTL. That is up to ~1.34% better for the first ~30 EMBER of their order, left on the table. A third sender,terra1vadedjw2d9vrpy57hgpg69g78w94dghgrsys84(tx8AE8F7A13B6A0E2416E98C62DF7F99D1B1AB97B5C4281FFBFC7A66DF21ACC46E, 25 EMBER), also took pool-only.So the split is not just theoretically suboptimal -- on this pair it is currently the difference between two callers getting ~1% better fills and three callers not. Which one you get appears to depend on the path, not on the prices.
Two things this adds to the original report:
dex.cl8y.comUI takes by default. If the pool-only callers above are the UI, then every retail swap on the venue is skipping available book liquidity and the fix here is worth roughly a percent per trade to users. If they are a bespoke bot, this is narrower -- but then the UI default is the thing to check, not the thing to assume.Happy to run the
hybrid_simulationsweep again against current depth if that would help size the change.Thanks for the detailed mainnet sweeps and follow-up — very useful.
Follow-up: since you mentioned running your own market-making / taker bots, are those bots calling
GET /api/v1/route/solve(indexer best-execution split), or are they declaringpool_input/book_inputthemselves (direct pairhybrid_simulation/swap, orPOST /route/solvewithhybrid_by_hop)? Knowing which path your bots use would clarify how much of the pool-only fill gap is UI vs integrator defaults.Evaluation summary
Agreed on the goal: default market tickets should submit a price-optimal pool/book split, not a hand-sized one. Preferred approach:
book_inputgrid + coordinate descent). Pattern C stays caller-declared on-chain; the solver fills in the params off-chain.hybrid_simulationin the panel — that would duplicate the indexer solver, add LCD load/latency, and risk quote≠submit drift. Finer continuous search (if needed later) belongs in the indexer only.bookInputHumancan stay behind Advanced / POST override for integrators who want a fixed split.The maker flywheel point stands: as long as common taker paths skip competitive resting depth, books stay thin. Slippage (
min_return/max_spread) remains the execute-time guard; auto-split is advisory like today’s GET path.Frontend routes that do not default to the indexer GET solver
These are the dapp paths that skip
GET /api/v1/route/solvebest-execution (or only hit POST with a caller-fixed split):/trade— market ticket (TradeMarketOrderPanel)Always uses a caller-chosen split via
quoteDirectHybridSwap→POST /route/solvewith fixedhybrid_by_hop(LCDhybrid_simulationfallback). Empty book field defaults to 100% book (pool_input = 0), not an optimized interior split. Never callsgetRouteSolve./— Swap Advanced manual book leg (SwapPage+ Settings hybrid)When hybrid is on, the pair is direct CW20, and the user types a non-empty Book leg amount, quoting/submit use that fixed split (
quoteDirectHybridSwap/ POST) and override the GET solver for that quote./trade— hybrid toggle offPool-only pair
simulateSwap/ pool-only hybrid (book_input = 0); no GET solve./— Swap GET failure / non-solver fallbacksIf
getRouteSolvefails (or assets are outside the CW20 indexer path), Swap falls back to direct pairsimulateSwap/ multihop LCD — pool-oriented, not a fresh hybrid split search.Contrast: default
/Swap (hybrid on, empty manual book, CW20↔CW20) already usesgetRouteSolveand therefore already searches for a return-maximizing split. Empty book on Swap meansbook_input = 0before that GET path (solver may still allocate book); empty book on Trade means full book and never asks the solver — documented asymmetry indocs/limit-orders.md.Likely fix direction (no code yet)
Unify
/trademarket with Swap’s default: quote viaGET /route/solve, submit the solver’s hybrid params, keep manual split as Advanced. That matches your “default to solving” ask without on-chain search or a panel-local optimizer."Unify
/trademarket with Swap’s default: quote viaGET /route/solve, submit the solver’s hybrid params, keep manual split as Advanced." is approvedmentioned in commit
ba891c373fmentioned in merge request !1041
Implemented the approved scope only (leave issue open pending bot-runner path clarification):
MR: !1041 — unify
/tradeMarket with Swap’s default: quote viaGET /route/solve, submit solver hybrid params, keep manual split as Advanced.frontend-dapp/src/utils/cw20RouteSolveQuote.tsdocs/limit-orders.md,AGENTS_HYBRID_QUOTING.md, ADR 0002 amendmentNot in this MR: on-chain split search, client-side ternary over LCD
hybrid_simulation, TradeuseRouteSolveProgresswiring, or closing this issue.mentioned in commit
1ab04d6f58mentioned in commit
66b93245famentioned in commit
8da9f15e32Verification complete (#501) — closed
Verified the approved scope (MR !1041 + follow-up on
main@8da9f15):/tradeMarket defaults to indexerGET /api/v1/route/solve(same best-execution path as Swap), with Advanced manual split viaPOSTand hybrid-off = pool-only.What we did
quoteCw20ViaRouteSolveis used by Swap + Trade market default.hybridParamsWithSubmitCapto GET solver hybrid (matches Swap / #249 gas envelope).fee-discount-quote-245.spec.ts(Advanced →trade-market-hybrid-toggle).make verify-issue-501, Playwrighte2e/trade-market-route-solve-501-tx.spec.ts, and docs/skills crosslinks.indexer/.env(keepsFACTORY_ADDRESS).Verification checklist
simulateSwap(no route/solve)indexerOperations+hybridParamsWithSubmitCapmake verify-issue-501(10/10)trade-market-route-solve-501-tx.spec.ts— 2 passedNote on bot-runner path (comment thread)
Reporter follow-up about maker/taker bots vs GET /route/solve remains an integrator question outside the approved retail UI scope. The venue UI path is now aligned with Swap’s GET default.
How to re-verify
mentioned in issue #533
Thanks for the thorough evaluation and for unifying /trade Market with the GET /route/solve path (!1041). Good outcome for makers and takers alike, and the verification write-up was very helpful.
mentioned in issue #562
marked as related to #562
mentioned in issue #595
mentioned in issue #596
mentioned in issue #615
marked as related to #615
mentioned in issue #690
mentioned in issue #693
mentioned in issue #704
mentioned in issue #707
mentioned in issue #708
mentioned in issue #718