Repository navigation
solana-orderbook: net the execution cost out of quotes - #5121
Conversation
The autopilot already prices every auction token in lamports per 10^9 atoms for scoring, but never sent the prices on. The driver's `/solve` request now carries them, and the driver passes them to engines as `tokens[mint].referencePrice`, keyed under wSOL for native SOL buys. Both inbound sides default the field, so payloads without it parse.
Without price estimators the autopilot prices every token at the denominator as a scoring stand-in. Those placeholders now stay with the autopilot instead of reaching engines that would convert lamport costs into token atoms with them. The baseline solver engine also drops its `tokens` field: it does not use reference prices.
…native-reference-prices-to-drivers-and-solvers
"Lamports one atom is worth, scaled by 10^9" left readers working out what the number is. It is a rate, and naming it directly reads as one.
The Solana orderbook has no native prices of its own, so it will read
them from the autopilot like the EVM orderbook does. The autopilot now
binds an HTTP API (default 0.0.0.0:12088, the EVM autopilot's port)
serving GET /native_price/{mint} from the price cache: 200 with the
lamports per 10^9 atoms as a decimal string, 404 for a mint no estimator
prices, 429 while every source is rate limited.
A single-mint lookup fetches inline like a cut's but leaves the
refresher's maintained set alone: a quote for one token must not shrink
the set to that token and let the auction tokens expire.
The Solana orderbook had no native prices: /quote answered with the best raw driver amount for any pair, and an order on a token nobody prices was accepted and then sat unscored in every auction until it expired. The orderbook now reads prices from the autopilot's native price route, mirroring the EVM forwarder, and /quote fetches the sell and buy token prices concurrently with the driver quotes, failing with NoLiquidity when either is missing. Native SOL is forwarded under the System Program key the request carries, which the autopilot prices at the denominator like wSOL. The autopilot URL is a new required quoting config, since the forwarder is the orderbook's only price source.
An order on a token no estimator prices went out in every auction until it expired, since its solutions score nothing. The cut now keeps only orders with both native prices and records the rest with a filtered event under `missing_native_price`, mirroring the EVM autopilot.
The EVM autopilot ships an openapi.yml for /native_price/{token}, the
route the orderbook forwards to. Mirror it for the Solana autopilot so
the wire is reviewable as a spec: a per-token object with price, the
same route and status codes, priced in lamports per 10^9 atoms like the
/solve tokens[mint].price.
The EVM forwarder sends its quote timeout as timeout_ms and the EVM autopilot bounds the estimate by it, raised to a 250ms floor. Do the same here: the orderbook sends its quote timeout, and a lookup that outlives it answers 404, as EVM answers an estimator that ran out of time. Without the parameter the sources' own timeouts bound the lookup, so there is no ceiling to clamp to.
The EVM autopilot answers 404 when an estimator fails (CoinGecko's EstimatorInternal included) and keeps 429 for a spent quota. The Solana autopilot answered 500, so a CoinGecko outage failed every quote on an uncached token with 500 and an error log in the orderbook, where EVM answers NoLiquidity. Answer 404 like EVM; the route has no other 500.
Quotes now reach the price sources through the autopilot, and each concurrent quote on an uncached token fetched it on its own: N quotes spent N CoinGecko requests and N driver probes. A spent quota then fails the cut's own lookup, which skips the whole cycle. The EVM native price cache shares in-flight requests per token through request-sharing; do the same for the single-token lookups the orderbook drives. The shared future needs a cloneable error, so the lookup returns Arc<anyhow::Error>.
The quote gate's comments said an unpriced order would sit unscored in every auction, but the autopilot's cut now drops such orders, so they never enter one. Also drop the orderbook price error variant docs that repeated their #[error] strings.
A quote whose `timeout_ms` ran out dropped the shared lookup future, and with it the in-flight source request: its answer was discarded, nothing was cached, and the next quote on the token asked the source again. The EVM autopilot lets the estimator finish and caches what it returns. Spawn the lookup so a waiter giving up leaves it running and its verdict lands in the cache.
The orderbook built the native price URL by hand to survive a base configured without a trailing slash. The same config already appends the slash to the driver URLs at load time and serde-ext has the single-URL helper, so apply it to the autopilot URL and resolve the route with `Url::join` like the Quoter does.
…native-prices-from-the-autopilot-to-the-orderbook
A 429 cached nothing, so every later quote on an uncached token asked CoinGecko again while its quota was already spent. The shared lookup future dies as soon as its waiter answers, so sequential quotes did not coalesce either. The EVM autopilot wraps CoinGecko in a rate limiter whose back-off refuses requests locally after a 429. Record when the back-off ends on a 429 and fail every lookup as rate limited until then without asking a source, so the cut keeps its skip-the-cycle semantics and the orderbook keeps seeing 429.
Eight comments justified a floor, a port, a probe size, or an error mapping with "the EVM side does this". A reader of the Solana code should get the reason itself, so each comment now states it, and the ones whose code already says it are gone.
Test docs that repeated the test name, fn docs that repeated the signature, and a handler doc that repeated the openapi spec are gone. The `RateLimited` doc claimed the orderbook tells a spent quota from an unpriced token, but its quote route answers both as `NoLiquidity`; it now names what the type is for, the back-off. The orderbook client's module and struct docs said the same thing, so the struct doc is gone.
`rate_limited_sources_are_left_alone_during_the_back_off` asserts the first lookup's `RateLimited` verdict before asserting the back-off, so `rate_limited_sources_fail_the_lookup_as_rate_limited` pinned nothing on its own.
The two prices were discarded with `_`, which reads as if they were fetched only to be ignored. Nothing consumes them until the execution cost is netted out of the quote, which needs the driver's network fee and the account rent first. Name them and leave the TODO where that work starts.
…native-prices-from-the-autopilot-to-the-orderbook
The single lookup was spawned so a quote whose budget ran out could not cancel the source request. The EVM autopilot does not do that: its request sharing holds a weak handle, so a dropped waiter drops the fetch, and the budget is passed into the estimator instead. Match it; the shared in-flight future still keeps concurrent waiters on one request.
A 429 paused every source for ten seconds, which also stalled the refresher, so the auction's prices stopped refreshing until it lifted. The EVM autopilot has no such pause on its native sources: a 429 is answered as rate limited, the verdict is not cached, and the next lookup asks again. Do the same; concurrent lookups of one token still share one request, and the route still answers 429.
…-327-forward-native-prices-from-the-autopilot-to-the-orderbook # Conflicts: # crates/solana-orderbook/src/infra/api/routes/quote/mod.rs
|
Reminder: Please consider backward compatibility when modifying the API specification.
Caused by: |
|
Claude finished @squadgazzz's task in 3m 36s —— View job Review: net the execution cost out of quotes
I read What I verified
Minor, non-blocking observation (no change expected) In practice a buy order is only rejected with |
…6-net-quote-costs # Conflicts: # crates/autopilot-svm/src/infra/api.rs # crates/autopilot-svm/src/infra/prices.rs # crates/solana-orderbook/src/infra/api/routes/quote/mod.rs
tilacog
left a comment
There was a problem hiding this comment.
LGTM!
This was way simpler than I could imagine!
Description
A quote is net of the settlement's execution cost, the way the fill is: the driver takes the cost out of the fill (#5115), so a quote that ignores it promises more than the fill delivers.
The cost is the buy token account's rent, when the settlement must create it, plus the
networkFeethe driver reports for the quoted route (#5101). A driver that reports no fee is charged rent only. The cost comes off the side the trader does not fix: a sell order delivers less buy token, a buy order pulls more sell token, converted at the autopilot's native price (#5068) and rounded up against the trader. Drivers are ranked on the amounts after the cut, so a cheaper route can beat a larger raw answer. A quote the cost swallows answers 400SellAmountDoesNotCoverFee.Stacked on #5068.
Changes
networkFeesreports the winning driver's fee instead of a fixed placeholderSellAmountDoesNotCoverFeeHow to test
New and updated unit and integration tests.
Related issues
BE-366