Rust is the most admired programming language for the tenth consecutive year, and the blockchain ecosystem built on Rust now processes over 200 million transactions daily. If you are building a DeFi backend, trading bot, or wallet service in Rust, you need a way to execute token swaps without managing router contracts, liquidity pools, or DEX protocol upgrades yourself. This guide shows you how to integrate a token swap API into a Rust backend using Axum, reqwest, and a single GET request to swapapi.dev -- a free DEX aggregator covering 46 EVM chains with no API key required.
What You Will Need
Before starting, make sure you have these tools installed:
- Rust 1.75+ with Cargo
-
An Ethereum-compatible wallet address (for the
senderparameter) -
A terminal with
curlfor testing
Add the following dependencies to your Cargo.toml:
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Axum has surpassed Actix Web in developer preference according to recent surveys, while maintaining comparable throughput -- Axum completes 1 million requests in roughly 6 seconds in hello-world benchmarks. Its tight integration with the Tokio ecosystem makes it the natural choice for async HTTP services.
Step 1: Define Response Types with Serde
The swap API returns a JSON envelope with typed fields. Define Rust structs that map directly to the response schema. Every response wraps data in { success, data, error, timestamp }, so your types should mirror that.
#[derive(Debug, Deserialize)]
pub struct SwapEnvelope {
pub success: bool,
pub data: Option<SwapData>,
pub error: Option<SwapError>,
}
The SwapData struct captures the routing result, including the executable transaction object:
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SwapData {
pub status: String,
pub amount_in: Option<String>,
pub expected_amount_out: Option<String>,
pub min_amount_out: Option<String>,
pub price_impact: Option<f64>,
pub tx: Option<SwapTx>,
}
The tx field contains everything needed to submit the swap on-chain -- to, data, value, and gasPrice:
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SwapTx {
pub from: String,
pub to: String,
pub data: String,
pub value: String,
pub gas_price: u64,
}
The error type is straightforward:
#[derive(Debug, Deserialize)]
pub struct SwapError {
pub code: String,
pub message: String,
}
DEX trading volume hit $3.48 trillion in 2025, up 37% year-over-year. That volume flows through smart contract routers that swap APIs abstract away -- your Rust code never touches the routing logic.
Step 2: Build the Swap Client
Create a client function that calls the swap API. The endpoint is a single GET request with query parameters:
GET https://api.swapapi.dev/v1/swap/{chainId}?tokenIn={address}&tokenOut={address}&amount={rawAmount}&sender={address}
pub async fn fetch_swap(
chain_id: u64,
token_in: &str,
token_out: &str,
amount: &str,
sender: &str,
) -> Result<SwapEnvelope, reqwest::Error> {
let url = format!(
"https://api.swapapi.dev/v1/swap/{}?tokenIn={}&tokenOut={}&amount={}&sender={}",
chain_id, token_in, token_out, amount, sender
);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(15))
.build()?;
client.get(&url).send().await?.json().await
}
Set the timeout to 15 seconds. The API documentation notes typical response times of 1-5 seconds, but complex multi-hop routes on congested chains can take longer. The reqwest client handles HTTPS, connection pooling, and async I/O out of the box.
Try It Yourself
Test the endpoint directly before writing more code:
curl "https://api.swapapi.dev/v1/swap/1?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
That swaps 1 ETH for USDC on Ethereum mainnet. The response includes executable calldata in data.tx that you can submit directly to the network.
Step 3: Handle All Three Response Statuses
The API returns three possible statuses, all with HTTP 200. Your Rust code must check data.status explicitly:
| Status | Meaning |
tx Present |
Action |
|---|---|---|---|
Successful |
Full route found | Yes | Execute the transaction |
Partial |
Only part of the amount can be filled | Yes | Execute partial or adjust amount |
NoRoute |
No route exists for this pair | No | Try different pair, amount, or chain |
Here is how to handle all three cases:
pub fn handle_swap_response(envelope: &SwapEnvelope) -> String {
if !envelope.success {
let err = envelope.error.as_ref().unwrap();
return format!("API error: {} - {}", err.code, err.message);
}
let data = envelope.data.as_ref().unwrap();
match data.status.as_str() {
"Successful" => format!("Swap ready: {} out", data.expected_amount_out.as_deref().unwrap_or("0")),
"Partial" => format!("Partial fill: {} out", data.expected_amount_out.as_deref().unwrap_or("0")),
"NoRoute" => "No route found. Try a different pair.".to_string(),
other => format!("Unknown status: {}", other),
}
}
According to Grayscale research, DEXs now account for 7.6% of total crypto trading volume. As that share grows, handling edge cases like partial fills becomes critical for production services.
Step 4: Expose a Swap Endpoint with Axum
Wire the client into an Axum handler. This creates a GET /swap endpoint that proxies requests to the swap API and returns the result:
use axum::{extract::Query, routing::get, Json, Router};
#[derive(Deserialize)]
pub struct SwapQuery {
pub chain_id: u64,
pub token_in: String,
pub token_out: String,
pub amount: String,
pub sender: String,
}
The handler function calls fetch_swap and returns the envelope as JSON:
pub async fn swap_handler(
Query(params): Query<SwapQuery>,
) -> Json<serde_json::Value> {
match fetch_swap(
params.chain_id,
¶ms.token_in,
¶ms.token_out,
¶ms.amount,
¶ms.sender,
).await {
Ok(envelope) => Json(serde_json::to_value(envelope).unwrap()),
Err(e) => Json(serde_json::json!({"error": e.to_string()})),
}
}
Then register the route and start the server:
#[tokio::main]
async fn main() {
let app = Router::new().route("/swap", get(swap_handler));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
This gives you a running Rust service that accepts swap requests on port 3000 and returns executable transaction data. No API keys, no account registration.
Step 5: Add Multi-Chain Support
The API supports 46 EVM chains through a single endpoint -- you just change the chainId path parameter. Here are the most active networks:
| Chain | Chain ID | Native Token | Example Pair |
|---|---|---|---|
| Ethereum | 1 | ETH | ETH/USDC |
| Arbitrum | 42161 | ETH | ETH/USDC |
| Polygon | 137 | POL | POL/USDC |
| Base | 8453 | ETH | ETH/USDC |
| BSC | 56 | BNB | BNB/USDT |
| Optimism | 10 | ETH | ETH/USDC |
| Avalanche | 43114 | AVAX | AVAX/USDC |
Use 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE as tokenIn for the native gas token on any chain. Each chain has its own token contract addresses -- USDC on Ethereum (0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) is a different contract than USDC on Arbitrum (0xaf88d065e77c8cC2239327C5EDb3A432268e5831).
Try It Yourself: Arbitrum Swap
curl "https://api.swapapi.dev/v1/swap/42161?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
Watch for decimal differences across chains. USDC and USDT use 6 decimals on most chains, but on BSC both use 18 decimals. Passing the wrong decimal count means your amount parameter will be off by 12 orders of magnitude. The token addresses from the API documentation include decimal annotations for each chain.
Step 6: Add Safety Checks Before Submitting
The API returns transaction calldata, but you should validate before submitting on-chain. Here is the recommended pre-flight sequence:
-
Check
priceImpact-- reject swaps worse than -5% (i.e.,price_impact < -0.05) -
Verify
status-- only submit when status isSuccessfulor an acceptablePartial -
Simulate with
eth_call-- call the RPC with thetxobject before spending gas -
Estimate gas -- use
eth_estimateGasand add a 20% buffer - Submit within 30 seconds -- calldata includes a deadline, so re-fetch if stale
pub fn is_safe_to_execute(data: &SwapData) -> bool {
let impact_ok = data.price_impact.map_or(false, |p| p > -0.05);
let status_ok = data.status == "Successful";
let has_tx = data.tx.is_some();
impact_ok && status_ok && has_tx
}
For ERC-20 input tokens (not native ETH), the sender must first approve the router contract (data.tx.to) to spend the input amount. The API does not check on-chain balances or allowances -- it always returns a quote if a route exists. The transaction will revert on-chain if the approval or balance is insufficient.
Enterprise Rust adoption has surged 68.75% between 2021 and 2024, with 45% of organizations now making non-trivial use of the language. The memory safety guarantees and zero-cost abstractions that drive that adoption are exactly what you want in a service handling financial transactions.
Step 7: Production Hardening
For a production swap service, add these Rust-idiomatic improvements:
Rate limiting: The API allows approximately 30 requests per minute per IP. Use tower::limit::RateLimitLayer in your Axum middleware stack:
use tower::limit::RateLimitLayer;
use std::time::Duration;
let app = Router::new()
.route("/swap", get(swap_handler))
.layer(RateLimitLayer::new(25, Duration::from_secs(60)));
Retries with backoff: Only retry on UPSTREAM_ERROR (502) and RATE_LIMITED (429). Never retry INVALID_PARAMS or UNSUPPORTED_CHAIN -- those require fixing the request.
Connection pooling: Reuse a single reqwest::Client instance across requests. Create it once at startup and pass it via Axum's State extractor:
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(15))
.pool_max_idle_per_host(10)
.build()
.unwrap();
RPC fallbacks: The API response includes rpcUrls -- an array of up to 5 ranked public RPCs for the target chain. Use them as a fallback list when submitting transactions. If the first RPC fails or times out, try the next.
Frequently Asked Questions
What is a token swap API and why use one in Rust?
A token swap API is a REST service that returns executable DEX swap calldata for a given token pair and amount. Using one in Rust lets you integrate on-chain swaps without managing smart contract ABIs, router upgrades, or liquidity pool math. You send a GET request and receive a ready-to-submit transaction.
Does the token swap API require authentication or an API key?
No. swapapi.dev is completely free with no API keys, no accounts, and no authentication. You can call the endpoint directly from your Rust backend. The only limit is approximately 30 requests per minute per IP address.
How many blockchains does the API support?
The API supports 46 EVM-compatible chains through a single endpoint. You switch networks by changing the chainId path parameter. Supported chains include Ethereum, Arbitrum, Base, Polygon, BSC, Optimism, Avalanche, and 39 others.
Can I use Actix Web instead of Axum?
Yes. The swap client code (reqwest calls and serde types) works with any Rust web framework. Only the handler and router setup differs. The examples in this guide use Axum because it leads in developer adoption as of 2026, but the core integration logic is framework-agnostic.
How do I handle partial fills in production?
When data.status is "Partial", the amountIn and expectedAmountOut fields reflect only the fillable portion. Compare the response amountIn against your original requested amount to detect partial fills. You can execute the partial swap as-is, adjust your requested amount, or try a different token pair.
Get Started
Integrate token swaps into your Rust backend in minutes:
-
API endpoint:
https://api.swapapi.dev/v1/swap/{chainId} - Documentation: swapapi.dev
- OpenAPI spec: swapapi.dev/openapi.json
- 46 EVM chains, free, no API key required
Fetch your first quote right now:
curl "https://api.swapapi.dev/v1/swap/42161?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
The response includes everything you need: the expected output amount, price impact, and a complete transaction object ready to submit on Arbitrum. No contracts to deploy, no infrastructure to manage -- just a GET request and serde_json::from_str.
Top comments (0)