A production-ready Rust SDK for Odos - the decentralized exchange aggregator that finds optimal token swap routes across 16+ EVM chains. Built with type safety, reliability, and developer experience in mind.
Optimal pricing through advanced routing - Odos analyzes paths across hundreds of DEXes and liquidity sources, splitting trades intelligently to minimize slippage and maximize output. The SDK makes this power accessible in just a few lines of Rust.
Battle-tested for production:
- Smart retry logic with exponential backoff for network resilience
- Structured error codes with clear categorization and trace IDs
- Rate limit detection with
Retry-Aftersupport - Full type safety via Alloy primitives (no string addresses or numeric guessing)
- Connection pooling, configurable timeouts, and graceful degradation
- Comprehensive logging and observability with
tracing
APIs for every use case:
- High-level
SwapBuilder- integrate swaps in minutes - Mid-level
quote→assemble- full control over the flow - Low-level contract bindings - advanced scenarios and direct router access
Add the SDK to your project:
[dependencies]
odos-sdk = "1.0"use odos_sdk::prelude::*;
use alloy_primitives::{Address, U256};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize the client
let client = OdosClient::new()?;
// Define your swap
let usdc: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".parse()?;
let weth: Address = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".parse()?;
let my_address: Address = "0x...".parse()?;
// Build the transaction - that's it!
let tx = client.swap()
.chain(Chain::ethereum())
.from_token(usdc, U256::from(1_000_000)) // 1 USDC (6 decimals)
.to_token(weth)
.slippage(Slippage::percent(0.5)?)
.signer(my_address)
.build_transaction()
.await?;
// Execute with your wallet
// provider.send_transaction(tx.transaction).await?;
Ok(())
}Three concepts, one builder, zero complexity. The SDK handles quote fetching, optimal routing, transaction assembly, and error recovery automatically.
Next steps: Check out GETTING_STARTED.md for a complete walkthrough, or jump to EXAMPLES.md for common patterns.
Supports 16+ EVM chains out of the box:
| Category | Chains |
|---|---|
| Layer 1 | Ethereum |
| Layer 2 | Arbitrum, Optimism, Base, Polygon, zkSync, Scroll, Linea, Mantle, Mode |
| Sidechains | BSC, Avalanche, Fraxtal, Sonic, Unichain |
Chain selection is type-safe and simple:
let chain = Chain::ethereum(); // or arbitrum(), optimism(), etc.
let chain = Chain::from_chain_id(42161)?; // From numeric IDBuilt on the Alloy ecosystem for bulletproof type safety:
use alloy_primitives::{Address, U256};
// Parse addresses at compile time or runtime
let token: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".parse()?;
// Handle amounts with U256 (no floating point errors)
let one_usdc = U256::from(1_000_000); // 6 decimals
// Validated slippage
let slippage = Slippage::percent(0.5)?; // 0.5%
let slippage = Slippage::bps(50)?; // 50 basis pointsStructured errors with clear categorization:
use odos_sdk::{OdosError, error_code::OdosErrorCode};
match client.quote(&request).await {
Ok(quote) => {
println!("Expected output: {}", quote.out_amount());
}
Err(e) => {
// Check error type
if e.is_rate_limit() {
if let Some(retry_after) = e.retry_after() {
println!("Rate limited. Retry after: {:?}", retry_after);
}
}
// Check specific error codes
if let Some(code) = e.error_code() {
if code.is_no_viable_path() {
println!("No routing path found for this pair");
} else if code.is_validation_error() {
println!("Invalid request parameters");
}
}
// Access trace ID for debugging
if let Some(trace_id) = e.trace_id() {
eprintln!("Error trace ID: {}", trace_id);
}
}
}Error codes match the Odos API documentation with type-safe categorization:
- 1XXX: General API errors
- 2XXX: Quote/routing errors (
NoViablePath,AlgoTimeout, etc.) - 3XXX: Internal service errors
- 4XXX: Validation errors (
InvalidChainId,InvalidTokenAmount, etc.) - 5XXX: Internal errors
Configurable retry behavior with exponential backoff:
use std::time::Duration;
use odos_sdk::{OdosClient, RetryConfig};
// Conservative preset - only retry network errors
let client = OdosClient::with_retry_config(RetryConfig::conservative())?;
// Custom retry configuration
let client = OdosClient::with_retry_config(RetryConfig {
max_retries: 5,
initial_backoff_ms: 200,
retry_server_errors: true,
retry_predicate: Some(|err| {
// Custom logic to determine if an error should be retried
err.is_timeout() || (err.is_api_error() && !err.is_validation_error())
}),
})?;Rate limits are detected but not automatically retried - you control the global rate limiting strategy.
The SDK provides three levels of abstraction. Choose based on your needs:
Perfect for most use cases. One builder, automatic flow:
use odos_sdk::prelude::*;
let tx = client.swap()
.chain(Chain::arbitrum())
.from_token(usdc, amount)
.to_token(weth)
.slippage(Slippage::percent(0.5)?)
.signer(my_address)
.recipient(recipient_address) // Optional: send output to different address
.build_transaction()
.await?;
// Or just get a quote first
let quote = client.swap()
.chain(Chain::ethereum())
.from_token(usdc, amount)
.to_token(weth)
.slippage(Slippage::percent(0.5)?)
.signer(my_address)
.quote()
.await?;
println!("Expected output: {}", quote.out_amount());More control over the quote and assembly phases:
use odos_sdk::prelude::*;
// Step 1: Request quote
let quote_request = QuoteRequest::builder()
.chain_id(1)
.input_tokens(vec![(usdc, amount).into()])
.output_tokens(vec![(weth, 1).into()])
.slippage_limit_percent(0.5)
.user_addr(my_address)
.compact(false)
.simple(false)
.referral_code(0)
.disable_rfqs(false)
.build();
let quote = client.quote("e_request).await?;
// Show user the expected output
println!("You will receive approximately: {}", quote.out_amount());
// Step 2: User confirms, assemble transaction
let assembly_request = AssemblyRequest::builder()
.chain(NamedChain::Mainnet)
.router_address(NamedChain::Mainnet.v2_router_address()?)
.signer_address(my_address)
.output_recipient(my_address)
.token_address(usdc)
.token_amount(amount)
.path_id(quote.path_id().to_string())
.build();
let tx = client.assemble(&assembly_request).await?;Direct router contract access for advanced scenarios:
use odos_sdk::{OdosV2Router, OdosChain};
use alloy_chains::NamedChain;
let router_address = NamedChain::Mainnet.v2_router_address()?;
let router = OdosV2Router::new(router_address, provider);
// Call contract methods directly
let result = router.swap(swap_inputs).send().await?;use odos_sdk::{OdosClient, ClientConfig};
use std::time::Duration;
let config = ClientConfig {
timeout: Duration::from_secs(30),
connect_timeout: Duration::from_secs(10),
max_connections: 20,
pool_idle_timeout: Duration::from_secs(90),
..Default::default()
};
let client = OdosClient::with_config(config)?;Choose between public and enterprise endpoints:
use odos_sdk::{ClientConfig, Endpoint, ApiHost, ApiVersion};
// Public API (default)
let config = ClientConfig {
endpoint: Endpoint::public_v2(), // or public_v3()
..Default::default()
};
// Enterprise API with higher rate limits
let config = ClientConfig {
endpoint: Endpoint::enterprise_v2(), // or enterprise_v3()
api_key: Some(ApiKey::new("your-api-key")?),
..Default::default()
};
let client = OdosClient::with_config(config)?;Customize what gets compiled based on your needs:
# Default: V2 + V3 routers
[dependencies]
odos-sdk = "1.0"
# Minimal: Just API types and HTTP client (no contract bindings)
[dependencies]
odos-sdk = { version = "1.0", default-features = false, features = ["minimal"] }
# All contracts: V2 + V3 + Limit Orders
[dependencies]
odos-sdk = { version = "1.0", default-features = false, features = ["contracts"] }
# Custom combination
[dependencies]
odos-sdk = { version = "1.0", default-features = false, features = ["v2", "v3"] }Available features:
minimal- Core API types and HTTP client onlyv2- V2 router contract bindingsv3- V3 router contract bindings (includes v2)limit-orders- Limit order contract bindings (includes v2)contracts- All contract bindings (v2 + v3 + limit-orders)default- V2 + V3 routers
| Resource | Description |
|---|---|
| GETTING_STARTED.md | Complete walkthrough from setup to your first swap |
| EXAMPLES.md | Real-world patterns: error handling, testing, integration |
| API Docs | Complete API reference with inline examples |
| ERROR_HANDLING_GUIDE.md | Deep dive into error types and recovery strategies |
| CHANGELOG.md | Version history and migration guides |
| SECURITY.md | Security best practices and vulnerability reporting |
The SDK detects rate limits but doesn't retry them automatically. Implement your own strategy:
use std::time::Duration;
use tokio::time::sleep;
async fn quote_with_rate_limiting(
client: &OdosClient,
request: &QuoteRequest,
) -> Result<SingleQuoteResponse, OdosError> {
loop {
match client.quote(request).await {
Ok(quote) => return Ok(quote),
Err(e) if e.is_rate_limit() => {
let wait = e.retry_after()
.unwrap_or(Duration::from_secs(5));
eprintln!("Rate limited, waiting {:?}", wait);
sleep(wait).await;
}
Err(e) => return Err(e),
}
}
}For production applications with high request volumes:
- Share a single
OdosClientinstance across your application - Implement a token bucket or leaky bucket algorithm
- Consider using a rate limiting library like
governor - Monitor rate limit errors and adjust your request rate dynamically
The SDK supports multiple router versions with different capabilities:
use odos_sdk::{OdosChain, RouterAvailability};
use alloy_chains::NamedChain;
let chain = NamedChain::Mainnet;
let availability = chain.router_availability()?;
if availability.v3 {
// Use V3 router (unified address across all chains)
let router = chain.v3_router_address()?;
} else if availability.v2 {
// Fall back to V2 router
let router = chain.v2_router_address()?;
}
if availability.limit_order {
// Limit orders are supported
let lo_router = chain.lo_router_address()?;
}V3 router features:
- Deployed at the same address on all supported chains (CREATE2)
- Enhanced gas efficiency
- Improved MEV protection
Check if a chain is supported before attempting operations:
use odos_sdk::OdosChain;
use alloy_chains::NamedChain;
let chain = NamedChain::Mainnet;
// Check general Odos support
if chain.supports_odos() {
println!("Odos is available on this chain");
}
// Check specific router availability
let availability = chain.router_availability()?;
println!("V2: {}, V3: {}, Limit Orders: {}",
availability.v2,
availability.v3,
availability.limit_order
);See EXAMPLES.md for comprehensive examples including:
- Multi-token swaps
- Error recovery and retry strategies
- Integration with wallets (ethers-rs, foundry)
- Gas estimation and optimization
- Testing with mocks
- Cross-chain workflows
- Production deployment patterns
Quick example - handling errors gracefully:
use odos_sdk::prelude::*;
async fn robust_swap(
client: &OdosClient,
from: Address,
to: Address,
amount: U256,
) -> Result<AssemblyResponse, OdosError> {
let result = client.swap()
.chain(Chain::ethereum())
.from_token(from, amount)
.to_token(to)
.slippage(Slippage::percent(0.5)?)
.signer(my_address)
.build_transaction()
.await;
match result {
Ok(tx) => Ok(tx),
Err(e) if e.is_no_viable_path() => {
eprintln!("No routing path available - try increasing slippage or different tokens");
Err(e)
}
Err(e) if e.is_timeout() => {
eprintln!("Request timed out - network or service issue");
Err(e)
}
Err(e) if e.is_rate_limit() => {
eprintln!("Rate limited - implement backoff strategy");
Err(e)
}
Err(e) => {
eprintln!("Unexpected error: {}", e);
if let Some(trace_id) = e.trace_id() {
eprintln!("Trace ID for support: {}", trace_id);
}
Err(e)
}
}
}We welcome contributions! Please see CONTRIBUTING.md for:
- Development setup (Rust 1.90+)
- Code standards and formatting
- Testing requirements
- PR process
- Release workflow
Quick development commands:
# Build
cargo build
# Run tests
cargo test
# Lint (CI enforces zero warnings)
cargo clippy --all-targets --all-features -- -D warnings
# Format
cargo fmt
# Security audit
cargo audit- Issues: GitHub Issues
- Odos Documentation: docs.odos.xyz
- Odos Discord: discord.gg/odos
Security is a top priority. Please report vulnerabilities to security@semiotic.ai. See SECURITY.md for:
- Vulnerability reporting process
- API key security best practices
- Input validation guidelines
- Production deployment checklist
Licensed under the Apache License, Version 2.0. See LICENSE for details.
Built with the excellent Alloy ecosystem for Ethereum interactions.
Ready to integrate? Start with GETTING_STARTED.md or dive into EXAMPLES.md for practical patterns.