Repository navigation
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.
|
Reminder: Please consider backward compatibility when modifying the API specification.
Caused by: |
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.
|
@claude review |
|
Claude finished @tilacog's task in 3m 42s —— View job PR ReviewReviewed the autopilot-svm native-price API, the orderbook forwarder, and the auction-cut filtering. The change is well-structured, well-documented, and well-tested. One issue worth addressing, posted inline:
Everything else looks good — the negative-cache / |
|
Claude finished @tilacog's task in 2m 51s —— View job PR Review
I re-reviewed the full diff (autopilot-svm native-price API, the orderbook forwarder, the What I checked and liked
On the earlier finding (client/server timeout race): the previous review noted that LGTM. 👍 |
…-327-forward-native-prices-from-the-autopilot-to-the-orderbook # Conflicts: # crates/solana-orderbook/src/infra/api/routes/quote/mod.rs
| match tokio::time::timeout(budget, prices.price(mint)) | ||
| .await | ||
| .unwrap_or(Ok(None)) | ||
| { | ||
| Ok(Some(price)) => Json(NativePrice { price }).into_response(), | ||
| Ok(None) => (StatusCode::NOT_FOUND, "No liquidity").into_response(), | ||
| Err(err) if err.is::<RateLimited>() => { | ||
| (StatusCode::TOO_MANY_REQUESTS, "Rate limited").into_response() | ||
| } | ||
| Err(err) => { | ||
| tracing::warn!(?err, %mint, "native price lookup failed"); | ||
| (StatusCode::NOT_FOUND, "No liquidity").into_response() | ||
| } | ||
| } |
There was a problem hiding this comment.
A budget timeout answers 404 without a log line, and since the lookup is dropped, the source warn never fires either. The EVM route logs every failure with elapsed. Maybe a warn! here?
There was a problem hiding this comment.
CI validates four specs (pull-request.yaml:321-328) and this one is not among them. It passes swagger-cli validate locally, so adding it to the job should be enough.
| inner | ||
| .single_lookups | ||
| .shared_or_else(token, move |&token| { | ||
| async move { | ||
| fetcher | ||
| .lookup(HashSet::from([token])) | ||
| .await | ||
| .map(|mut prices| prices.remove(&token)) | ||
| .map_err(Arc::new) | ||
| } | ||
| .boxed() | ||
| }) | ||
| .await |
There was a problem hiding this comment.
This PR helped me to realize we have a gap in the price estimation logic. #5112 rewrites fetch_into_cache to ask the sources by stage, like the EVM competition.
What stays is the tail. A token CoinGecko does not list costs its miss plus the driver stage, and on timeout, it drops the lookup with nothing cached, so every quote on that token pays it again. EVM caches the timeout. The spawn from 66d85fd would let the second quote hit the cache.
There was a problem hiding this comment.
Spawn re-applied in c097e66, with the test.
A lookup past `timeout_ms` answered 404 silently: the handler swallowed the timeout, and dropping the lookup cancelled the source request before its own warning could fire. The EVM route warns on every failed lookup with the elapsed time. Treat the timeout as a failed lookup so the same warning covers it, and log the budget and elapsed time beside the error.
The spec joined the repo without joining the openapi job, which validates and lints the other four. It passes both locally.
A quote whose `timeout_ms` ran out dropped the shared lookup future, and with it the in-flight source request: nothing was cached, so the next quote on the token asked the sources again and ran out of time the same way. A token CoinGecko does not list always costs its miss plus a driver probe, more than the orderbook's budget covers, so such tokens never entered the cache. The EVM estimator bounds each source request by the budget instead, so its fetch finishes and its verdict is cached. Spawn the lookup so a waiter giving up leaves it running and its verdict lands in the cache for the next quote.
…native-prices-from-the-autopilot-to-the-orderbook
Description
The Solana autopilot serves its native prices over HTTP. The Solana orderbook reads them and refuses to quote a token nobody prices, and the autopilot drops orders on such tokens from the auction.
Motivation
The orderbook had no native prices, so
/quoteanswered any pair with the best raw driver amount. An order on a token no estimator prices was accepted and then sat in every auction until it expired, because its solutions score nothing.This is the EVM setup: the orderbook fetches both native prices from the autopilot through the
Forwarderestimator and answersNoLiquiditywhen one is missing, and the autopilot filters orders without both prices out of each auction. The forwarder is the orderbook's only price source for now.Builds on #5054 (merged), which forwards the prices to drivers.
Changes
autopilot-svm
api-address(default0.0.0.0:12088, the EVM autopilot's port) withGET /native_price/{mint}. 200 carries{"price": "<lamports per 10^9 atoms>"}from theNativePricescache, the unit the autopilot already sends drivers. 404 for an unpriced token or a failed lookup, 429 when every source failed and one of them was rate limited, 400 for a mint that does not parse. An optionaltimeout_msbounds the lookup, with a 250ms floor. Past it the route answers 404. Spec inopenapi.yml.NativePrices::price(mint)looks one token up without touching the set the refresher maintains, so a request cannot shrink that set to one token. Concurrent lookups of one token share one fetch. A CoinGecko 429 fails the lookup asRateLimitedand leaves the token uncached, so the next lookup asks again.Filteredevent undermissing_native_price.solana-orderbook
infra/prices.rs, pointed at the new required[quoting] autopilotURL. 404 maps toNoLiquidity, 429 toRateLimited, anything else toInternal. It forwardsX-REQUEST-IDand sendsquoting.timeoutastimeout_ms./quotefetches both prices alongside the driver quotes. A missing or rate limited price answers 404NoLiquidity. A transport error answers 500. The prices themselves stay unused until BE-366 nets costs out of quotes.openapi.ymldescribes the new 404 case.Deployment configuration: the orderbook's
[quoting] autopilotpoints at the autopilot's base URL, and the autopilot'sapi-addressport is exposed to it.How to test
cargo nextest run -p autopilot-svm -p solana-orderbook. The route tests inautopilot-svm/src/infra/api.rscover 200, 400, 404, 429 and thetimeout_mscut against a seeded cache and mock sources.quote_without_a_native_price_is_no_liquidityinsolana-orderbook/tests/api.rsruns the quote route with drivers answering and the autopilot returning 404.Rollout: the orderbook refuses to start without
[quoting] autopilot, so set it before upgrading. The autopilot change is additive, but its port must be reachable from the orderbook.Related Issues
Linear: BE-327