Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
d914f6b
Autopilot Native Price API
squadgazzz Dec 17, 2025
4699fb5
Fix port
squadgazzz Dec 17, 2025
e106870
Better errors
squadgazzz Dec 17, 2025
9a7c718
Fix
squadgazzz Dec 17, 2025
285a3e5
Ignore dual autopilot test
squadgazzz Dec 17, 2025
d6cfb57
Fixed test with a proxy
squadgazzz Dec 17, 2025
273faae
fix
squadgazzz Dec 17, 2025
5db5fc0
Init API server before metrics
squadgazzz Dec 17, 2025
25a0a24
OpenAPI
squadgazzz Dec 17, 2025
785582e
Include error reason
squadgazzz Dec 17, 2025
45f99ab
Forward the timeout
squadgazzz Dec 17, 2025
b6089cd
Migrate the config
squadgazzz Dec 18, 2025
c0ce7b3
Merge branch 'main' into autopilot/native-price-api
squadgazzz Dec 18, 2025
5ac6341
Formatting
squadgazzz Dec 18, 2025
065a5aa
Fix and docs
squadgazzz Dec 18, 2025
ab91606
Fix
squadgazzz Dec 18, 2025
9b571d3
fix
squadgazzz Dec 18, 2025
c2fc05b
Formatting
squadgazzz Dec 18, 2025
ebfeafb
Fix
squadgazzz Dec 18, 2025
ef3a1a2
Fix
squadgazzz Dec 18, 2025
34202be
Review comments
squadgazzz Dec 19, 2025
3b4a1f9
Review comments
squadgazzz Dec 19, 2025
0477a38
Logs
squadgazzz Dec 19, 2025
87f29ad
Better parser
squadgazzz Dec 19, 2025
fc11acd
Proxy rework
squadgazzz Dec 19, 2025
bd79595
e2e config rework
squadgazzz Dec 19, 2025
556cc2b
Update proxy
squadgazzz Dec 19, 2025
449abcb
Doc
squadgazzz Dec 19, 2025
48414f2
Refactor
squadgazzz Dec 19, 2025
9721a90
Allowed timeout range
squadgazzz Dec 22, 2025
114c398
Timer
squadgazzz Dec 22, 2025
687fb30
Refactor
squadgazzz Dec 22, 2025
b7914fc
Doc
squadgazzz Dec 22, 2025
487f081
Body ownership
squadgazzz Dec 22, 2025
2a0e063
Merge branch 'main' into autopilot/native-price-api
squadgazzz Dec 22, 2025
2243fdf
Small refactor
squadgazzz Dec 23, 2025
3ed714c
Nit
squadgazzz Dec 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion crates/autopilot/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ path = "src/main.rs"
[dependencies]
alloy = { workspace = true, features = ["rand", "provider-debug-api", "provider-trace-api"] }
app-data = { workspace = true }
axum = { workspace = true }
bytes-hex = { workspace = true } # may get marked as unused but it's used with serde
anyhow = { workspace = true }
async-trait = { workspace = true }
Expand All @@ -31,7 +32,8 @@ database = { workspace = true }
derive_more = { workspace = true }
ethrpc = { workspace = true }
futures = { workspace = true }
observe = { workspace = true }
hyper = { workspace = true }
observe = { workspace = true, features = ["axum-tracing"] }
const-hex = { workspace = true }
hex-literal = { workspace = true }
humantime = { workspace = true }
Expand All @@ -58,6 +60,8 @@ sqlx = { workspace = true }
strum = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal", "sync", "time"] }
tower = { workspace = true }
tower-http = { workspace = true, features = ["trace"] }
tracing = { workspace = true }
url = { workspace = true }
web3 = { workspace = true }
Expand Down
94 changes: 94 additions & 0 deletions crates/autopilot/openapi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
openapi: 3.0.3
info:
title: Autopilot Native Price API
description: |
Internal API for retrieving native token prices from the autopilot service.
This API is intended to be used by orderbook instances to query the shared
native price cache maintained by autopilot.
version: 0.0.1
paths:
/native_price/{token}:
get:
operationId: getNativePrice
summary: Get the native price for a token
description: |
Returns the price of the specified token denominated in the native token
(e.g., ETH on Ethereum mainnet, xDAI on Gnosis Chain).
parameters:
- in: path
name: token
description: The address of the token to get the price for.
schema:
$ref: "#/components/schemas/Address"
required: true
- in: query
name: timeout_ms
description: |
Optional timeout in milliseconds for the price estimation request.
If not provided, uses the default timeout configured for autopilot.
Values below 250ms are automatically clamped to the minimum (250ms).
Values exceeding the configured maximum are clamped to the maximum.
schema:
type: integer
format: int64
minimum: 250
example: 5000
required: false
responses:
"200":
description: Native price retrieved successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/NativeTokenPrice"
"400":
description: |
Bad request. Possible causes:
- Unsupported token
content:
text/plain:
schema:
type: string
example: "Unsupported token"
"404":
description: No liquidity available for this token.
content:
text/plain:
schema:
type: string
example: "No liquidity"
"429":
description: Rate limited by upstream price estimator.
content:
text/plain:
schema:
type: string
example: "Rate limited"
"500":
description: Internal server error.
content:
text/plain:
schema:
type: string
example: "Internal error"
components:
schemas:
Address:
description: |
An Ethereum address encoded as a hex with `0x` prefix.
type: string
example: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
NativeTokenPrice:
description: |
The price of a token denominated in the native token (e.g., ETH).
type: object
properties:
price:
type: number
format: double
description: |
The price of the token in terms of the native token. For example, if
1 USDC = 0.0005 ETH, the price would be 0.0005.
example: 0.0005
required:
- price
6 changes: 6 additions & 0 deletions crates/autopilot/src/arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ pub struct Arguments {
#[clap(long, env, default_value = "0.0.0.0:9589")]
pub metrics_address: SocketAddr,

/// Address to bind the HTTP API server
#[clap(long, env, default_value = "0.0.0.0:12088")]
pub api_address: SocketAddr,

/// Url of the Postgres database. By default connects to locally running
/// postgres.
#[clap(long, env, default_value = "postgresql://")]
Expand Down Expand Up @@ -370,6 +374,7 @@ impl std::fmt::Display for Arguments {
ethflow_contracts,
ethflow_indexing_start,
metrics_address,
api_address,
skip_event_sync,
allowed_tokens,
unsupported_tokens,
Expand Down Expand Up @@ -417,6 +422,7 @@ impl std::fmt::Display for Arguments {
writeln!(f, "ethflow_contracts: {ethflow_contracts:?}")?;
writeln!(f, "ethflow_indexing_start: {ethflow_indexing_start:?}")?;
writeln!(f, "metrics_address: {metrics_address}")?;
writeln!(f, "api_address: {api_address}")?;
display_secret_option(f, "db_write_url", Some(&db_write_url))?;
writeln!(f, "skip_event_sync: {skip_event_sync}")?;
writeln!(f, "allowed_tokens: {allowed_tokens:?}")?;
Expand Down
133 changes: 133 additions & 0 deletions crates/autopilot/src/infra/api.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use {
alloy::primitives::Address,
axum::{
Router,
extract::{Path, Query, State as AxumState},
http::StatusCode,
response::{IntoResponse, Json, Response},
routing::get,
},
model::quote::NativeTokenPrice,
observe::distributed_tracing::tracing_axum::{make_span, record_trace_id},
serde::Deserialize,
shared::price_estimation::{PriceEstimationError, native::NativePriceEstimating},
std::{
net::SocketAddr,
ops::RangeInclusive,
sync::Arc,
time::{Duration, Instant},
},
tokio::sync::oneshot,
};

/// Minimum allowed timeout for price estimation requests.
/// Values below this are not useful as they don't give estimators enough time.
const MIN_TIMEOUT: Duration = Duration::from_millis(250);

#[derive(Clone)]
struct State {
estimator: Arc<dyn NativePriceEstimating>,
allowed_timeout: RangeInclusive<Duration>,
}

#[derive(Debug, Deserialize)]
struct NativePriceQuery {
/// Optional timeout in milliseconds for the price estimation request.
/// If not provided, uses the default timeout configured for autopilot.
/// Values below 250ms are automatically clamped to the minimum (250ms).
/// Values exceeding the configured maximum are clamped to the maximum.
#[serde(default)]
timeout_ms: Option<u64>,
}

pub async fn serve(
addr: SocketAddr,
estimator: Arc<dyn NativePriceEstimating>,
max_timeout: Duration,
shutdown: oneshot::Receiver<()>,
) -> Result<(), hyper::Error> {
let state = State {
estimator,
allowed_timeout: MIN_TIMEOUT..=max_timeout,
};

let app = Router::new()
.route("/native_price/:token", get(get_native_price))
.with_state(state)
.layer(
tower::ServiceBuilder::new()
.layer(tower_http::trace::TraceLayer::new_for_http().make_span_with(make_span))
.map_request(record_trace_id),
);

let server = axum::Server::bind(&addr).serve(app.into_make_service());
tracing::info!(?addr, "serving HTTP API");

server
.with_graceful_shutdown(async {
shutdown.await.ok();
})
.await
}

async fn get_native_price(
Path(token): Path<Address>,
Query(query): Query<NativePriceQuery>,
AxumState(state): AxumState<State>,
) -> Response {
let timeout = query
.timeout_ms
.map(Duration::from_millis)
.unwrap_or(*state.allowed_timeout.end())
.clamp(*state.allowed_timeout.start(), *state.allowed_timeout.end());

let start = Instant::now();
match state.estimator.estimate_native_price(token, timeout).await {
Ok(price) => {
let elapsed = start.elapsed();
tracing::debug!(
?token,
?timeout,
?elapsed,
?price,
"estimated native token price"
);
Json(NativeTokenPrice { price }).into_response()
}
Err(err) => {
let elapsed = start.elapsed();
tracing::warn!(
?err,
?token,
?timeout,
?elapsed,
"failed to estimate native token price"
);
error_to_response(err)
}
}
}

fn error_to_response(err: PriceEstimationError) -> Response {
match err {
PriceEstimationError::NoLiquidity | PriceEstimationError::EstimatorInternal(_) => {
(StatusCode::NOT_FOUND, "No liquidity").into_response()
}
PriceEstimationError::UnsupportedToken { token: _, reason } => (
StatusCode::BAD_REQUEST,
format!("Unsupported token, reason: {reason}"),
)
.into_response(),
PriceEstimationError::RateLimited => {
(StatusCode::TOO_MANY_REQUESTS, "Rate limited").into_response()
}
PriceEstimationError::UnsupportedOrderType(reason) => (
StatusCode::BAD_REQUEST,
format!("Unsupported order type, reason: {reason}"),
)
.into_response(),
PriceEstimationError::ProtocolInternal(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response()
}
}
}
1 change: 1 addition & 0 deletions crates/autopilot/src/infra/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod api;
pub mod blockchain;
pub mod persistence;
pub mod shadow;
Expand Down
12 changes: 12 additions & 0 deletions crates/autopilot/src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,15 @@ pub async fn run(args: Arguments, shutdown_controller: ShutdownController) {

let liveness = Arc::new(Liveness::new(args.max_auction_age));
let startup = Arc::new(Some(AtomicBool::new(false)));

let (api_shutdown_sender, api_shutdown_receiver) = tokio::sync::oneshot::channel();
let api_task = tokio::spawn(infra::api::serve(
args.api_address,
native_price_estimator.clone(),
args.price_estimation.quote_timeout,
api_shutdown_receiver,
));

observe::metrics::serve_metrics(
liveness.clone(),
args.metrics_address,
Expand Down Expand Up @@ -699,6 +708,9 @@ pub async fn run(args: Arguments, shutdown_controller: ShutdownController) {
competition_updates_sender,
);
run.run_forever(shutdown_controller).await;

api_shutdown_sender.send(()).ok();
api_task.await.ok();
}

async fn shadow_mode(args: Arguments) -> ! {
Expand Down
1 change: 1 addition & 0 deletions crates/e2e/src/setup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod deploy;
#[macro_use]
pub mod onchain_components;
pub mod fee;
pub mod proxy;
mod services;
mod solver;

Expand Down
Loading
Loading