DEV Community

Moon Soon
Moon Soon

Posted on • Originally published at swapapi.dev

How to Add Token Swaps to Your Rust Backend

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 sender parameter)
  • A terminal with curl for 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"
Enter fullscreen mode Exit fullscreen mode

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>,
}
Enter fullscreen mode Exit fullscreen mode

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>,
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

The error type is straightforward:

#[derive(Debug, Deserialize)]
pub struct SwapError {
    pub code: String,
    pub message: String,
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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),
    }
}
Enter fullscreen mode Exit fullscreen mode

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,
}
Enter fullscreen mode Exit fullscreen mode

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,
        &params.token_in,
        &params.token_out,
        &params.amount,
        &params.sender,
    ).await {
        Ok(envelope) => Json(serde_json::to_value(envelope).unwrap()),
        Err(e) => Json(serde_json::json!({"error": e.to_string()})),
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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:

  1. Check priceImpact -- reject swaps worse than -5% (i.e., price_impact < -0.05)
  2. Verify status -- only submit when status is Successful or an acceptable Partial
  3. Simulate with eth_call -- call the RPC with the tx object before spending gas
  4. Estimate gas -- use eth_estimateGas and add a 20% buffer
  5. 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
}
Enter fullscreen mode Exit fullscreen mode

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)));
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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:

Fetch your first quote right now:

curl "https://api.swapapi.dev/v1/swap/42161?tokenIn=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&tokenOut=0xaf88d065e77c8cC2239327C5EDb3A432268e5831&amount=1000000000000000000&sender=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
Enter fullscreen mode Exit fullscreen mode

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)