Here is our first public release of Dexalot SDK for Python. It is in alpha testing right now. Fork it, contribute to it and use it to integrate with Dexalot and let us know how we can improve it.
Please Note: The public interface may undergo breaking changes.
dexalot-sdk is a Python library that provides core functionality for interacting with the Dexalot decentralized exchange. It offers a unified client interface for trading operations, cross-chain transfers, and portfolio management across multiple blockchain networks.
- Unified Client: Single
DexalotClientinterface for all Dexalot operations - Modular Architecture: Separate clients for CLOB, Swap, and Transfer operations
- Multi-Chain Support: Works with Dexalot L1 subnet and connected chains
- Caching: TTL-based memory cache utilities for performance optimization
core/client.py: UnifiedDexalotClientinheriting from modular componentscore/base.py: Environment setup, Web3 connections, error handlingcore/clob.py: Central Limit Order Book trading operationscore/swap.py: SimpleSwap RFQ (Request for Quote) functionalitycore/transfer.py: Cross-chain deposits/withdrawals, portfolio management
utils/input_validators.py: Validate SDK method input parameters (amounts, addresses, pairs, etc.)utils/token_normalization.py: Normalize user token symbols andBASE/QUOTEpairs (strip, ASCII uppercase, optional aliases fromdata/token_aliases.json)utils/cache.py: TTL-based caching utilities (MemoryCache,ttl_cached,async_ttl_cached)utils/observability.py: Structured logging and operation trackingutils/result.py: StandardizedResult[T]type for consistent error handlingutils/retry.py: Async retry decorator with exponential backoffutils/rate_limit.py: Token bucket rate limiter for API and RPC callsutils/nonce_manager.py: Async-safe nonce management to prevent transaction race conditionsutils/provider_manager.py: RPC provider failover with health trackingutils/error_sanitizer.py: Error message sanitization to prevent information leakageutils/websocket_manager.py: Persistent WebSocket connection manager with reconnection and heartbeat
User-facing methods accept common symbol variants: surrounding whitespace is ignored, symbols are folded to ASCII uppercase, and a small alias map (for example ETHER → ETH, BITCOIN → BTC) is applied after format validation. Trading pairs are normalized per leg (eth/usdc → ETH/USDC). Examples in this repo use canonical symbols; callers may pass mixed case or aliases interchangeably.
Install from PyPI:
pip install dexalot-sdkFor local development with uv:
uv venv
uv sync --group devOr with pip in editable mode from the repository root:
pip install -e .import asyncio
from dexalot_sdk import DexalotClient
async def main():
client = None
try:
# Initialize client
client = DexalotClient()
result = await client.initialize_client()
if not result.success:
print(f"Initialization failed: {result.error}")
return
# Fetch trading pairs (stores pairs in client.pairs)
pairs_result = await client.get_clob_pairs()
if pairs_result.success:
print(f"Available pairs: {list(client.pairs.keys())}")
else:
print(f"Error: {pairs_result.error}")
finally:
# Always close the client to clean up resources
if client is not None:
await client.close()
# Run the async function
asyncio.run(main())Key Points:
- The SDK is fully async - all methods must be awaited
- Async operational methods return
Result[T]for consistent error handling - Use
asyncio.run()for scripts orawaitin async contexts - Always call
await client.close()when done to clean up resources
See examples/async_basic.py for more examples.
import asyncio
from dexalot_sdk import DexalotClient
async def main():
client = None
try:
client = DexalotClient()
# Initialize client (required before other operations)
init_result = await client.initialize_client()
if not init_result.success:
print(f"Failed to initialize: {init_result.error}")
return
# Get available trading pairs (stores pairs in client.pairs)
pairs_result = await client.get_clob_pairs()
if pairs_result.success:
print(f"Found {len(client.pairs)} trading pairs")
else:
print(f"Error fetching pairs: {pairs_result.error}")
finally:
# Always close the client to clean up resources
if client is not None:
await client.close()
asyncio.run(main())Async operational methods return Result[T] which provides consistent error handling:
result = await client.get_orderbook("AVAX/USDC")
if result.success:
orderbook = result.data
print(f"Bids: {orderbook['bids']}")
print(f"Asks: {orderbook['asks']}")
else:
print(f"Error: {result.error}")
# Handle error appropriatelySee examples/error_handling.py for comprehensive error handling patterns.
web3>=6.0.0: Multi-chain blockchain interactions (AsyncWeb3 for async operations)aiohttp: Async HTTP client for Dexalot API communicationpython-dotenv: Environment variable managementeth-account: Ethereum account management and transaction signingwebsockets: Async WebSocket client for real-time event subscriptions
The published package includes the secrets-vault console command:
secrets-vault keygenRepository maintenance utilities such as scripts/version_manager.py are kept in the repo and are not part of the installed package.
Run tests from the repository root:
make test # Unit tests
make cov # Coverage reportThe SDK includes a built-in 4-level caching system to optimize performance by reducing redundant API calls. Caching is enabled by default with sensible TTL (Time-To-Live) values.
📖 Detailed Documentation: See SDK Caching Guide for comprehensive caching documentation, including advanced usage patterns, use cases, troubleshooting, and performance considerations.
| Level | Data Type | Default TTL | Examples |
|---|---|---|---|
| Static | Rarely changes | 1 hour | Environments, deployments, connected chains |
| Semi-Static | Changes occasionally | 15 minutes | Tokens, trading pairs |
| Balance | User-specific, updates frequently | 10 seconds | Portfolio balances, wallet balances |
| Orderbook | Real-time data | 1 second | Order book snapshots |
import asyncio
from dexalot_sdk import DexalotClient
from pprint import pprint
async def main():
async with DexalotClient() as client:
await client.initialize_client()
# First call fetches from API
result = await client.get_all_portfolio_balances()
if result.success:
pprint(result.data)
# {'ALOT': {'available': 95.5, 'locked': 4.5, 'total': 100.0}, 'AVAX': ...}
# Second call within 10 seconds returns cached result
result = await client.get_all_portfolio_balances() # Cached!
asyncio.run(main())Customize cache behavior during client initialization:
# Disable caching entirely
client = DexalotClient(enable_cache=False)
# Custom TTL values (in seconds)
client = DexalotClient(
enable_cache=True,
cache_ttl_static=7200, # 2 hours for static data
cache_ttl_semi_static=1800, # 30 minutes for semi-static
cache_ttl_balance=5, # 5 seconds for balances
cache_ttl_orderbook=0.5 # 500ms for orderbook
)Manually clear cached data when needed:
# Clear all cache levels
client.invalidate_cache()
# Clear specific cache level
client.invalidate_cache(level="balance") # Options: static, semi_static, balance, orderbookStatic Data (1 hour):
get_environments()get_chains()get_deployment()
Semi-Static Data (15 minutes):
get_tokens()get_clob_pairs()get_swap_pairs(chain_identifier)
Balance Data (10 seconds):
get_portfolio_balance(token, address=None)get_all_portfolio_balances(address=None)get_chain_wallet_balance(chain, token, address=None)get_chain_wallet_balances(chain, address=None)get_all_chain_wallet_balances(address=None)
Orderbook Data (1 second):
get_orderbook(pair)
Note: Write operations (e.g., add_order(), cancel_order(), deposit(), withdraw()) are never cached to ensure data integrity.
Balance data is cached per user address. When address=None, the SDK uses the connected wallet's address:
# Each user gets their own cached balance data
balance1 = await client.get_portfolio_balance("USDC") # Uses connected wallet
balance2 = await client.get_portfolio_balance("USDC", address="0xOtherUser") # Different cache entryExpected reduction in API calls:
- Static data: ~99.9% fewer calls (1 call per hour vs. every request)
- Semi-static data: ~95% fewer calls (1 call per 15 min vs. frequent polling)
- Balance data: Significant reduction for applications polling balances
- Orderbook data: Useful for multi-component applications
See examples/caching_demo.py for complete examples.
The SDK uses a centralized configuration system (DexalotConfig) that supports multiple initialization methods.
| Option | Type | Default | Description |
|---|---|---|---|
parent_env |
str |
"fuji-multi" |
Environment configuration (e.g., production-multi-avax, fuji-multi) |
api_base_url |
str |
Auto-detected | Base URL for Dexalot API (derived from parent_env) |
private_key |
str |
None |
Wallet private key for signing transactions |
enable_cache |
bool |
True |
Enable/disable all caching behavior |
timeouts |
tuple |
(5, 30) |
Connect/Read timeouts for HTTP requests |
log_level |
str |
"INFO" |
Logging verbosity (DEBUG, INFO, WARNING, ERROR) |
log_format |
str |
"console" |
Log output format (console, json) |
connection_pool_limit |
int |
100 |
Total connection pool size across all hosts |
connection_pool_limit_per_host |
int |
30 |
Maximum connections per individual host |
| Option | Type | Default | Description |
|---|---|---|---|
retry_enabled |
bool |
True |
Enable/disable automatic retry |
retry_max_attempts |
int |
3 |
Maximum number of retry attempts |
retry_initial_delay |
float |
1.0 |
Initial delay in seconds before first retry |
retry_max_delay |
float |
10.0 |
Maximum delay in seconds between retries |
retry_exponential_base |
float |
2.0 |
Exponential backoff multiplier |
retry_on_status |
tuple |
(429, 500, 502, 503, 504) |
HTTP status codes that trigger retry |
retry_on_exceptions |
tuple |
(aiohttp.ClientError, asyncio.TimeoutError) |
Exception types that trigger retry |
| Option | Type | Default | Description |
|---|---|---|---|
rate_limit_enabled |
bool |
True |
Enable/disable rate limiting |
rate_limit_requests_per_second |
float |
5.0 |
Maximum API requests per second |
rate_limit_rpc_per_second |
float |
10.0 |
Maximum RPC calls per second |
| Option | Type | Default | Description |
|---|---|---|---|
nonce_manager_enabled |
bool |
True |
Enable/disable nonce manager (prevents race conditions) |
| Option | Type | Default | Description |
|---|---|---|---|
ws_manager_enabled |
bool |
False |
Enable/disable WebSocket Manager (persistent connections) |
ws_ping_interval |
int |
30 |
Seconds between ping messages |
ws_ping_timeout |
int |
10 |
Seconds to wait for pong before reconnecting |
ws_reconnect_initial_delay |
float |
1.0 |
Initial reconnect delay in seconds |
ws_reconnect_max_delay |
float |
60.0 |
Maximum reconnect delay in seconds |
ws_reconnect_exponential_base |
float |
2.0 |
Exponential backoff multiplier |
ws_reconnect_max_attempts |
int |
10 |
Maximum reconnection attempts (0 = infinite) |
ws_time_offset_ms |
int |
0 |
Clock skew compensation added to timestamps in WebSocket auth messages |
Configuration values are resolved in the following order (highest to lowest priority):
- Constructor Arguments: Passed directly to
DexalotClient# 1. Highest Priority client = DexalotClient(parent_env="custom-env")
- Environment Variables: System-level variables
# 2. High Priority export PARENTENV="production-multi-avax"
.envFile: Variables loaded from local.envfile# 3. Medium Priority PARENTENV=fuji-multi
- Defaults: Hardcoded SDK defaults (
fuji-multi)
For complex setups, you can pass a DexalotConfig object directly:
from dexalot_sdk import DexalotClient
from dexalot_sdk.core.config import DexalotConfig
config = DexalotConfig(
parent_env="production-multi-subnet",
timeouts=(10, 60),
enable_cache=False
)
client = DexalotClient(config=config)The SDK includes automatic RPC provider failover to improve reliability when a single RPC endpoint fails. This feature allows you to configure multiple RPC endpoints per chain, with automatic failover to backup providers when the primary provider fails.
- Multiple Providers: Configure multiple RPC endpoints per chain (primary + fallbacks)
- Fail-Fast Strategy: Automatically switches to the next provider when the current one fails
- Health Tracking: Tracks provider health (failure counts, last failure time)
- Automatic Recovery: Failed providers are retried after a cooldown period
- Async-Safe: Concurrent operations are handled safely with asyncio locks; lock-free fast path when the primary provider is healthy
Provider failover is enabled by default. You can configure it via environment variables or DexalotConfig:
| Variable | Description | Default |
|---|---|---|
DEXALOT_PROVIDER_FAILOVER_ENABLED |
Enable/disable failover | true |
DEXALOT_PROVIDER_FAILOVER_COOLDOWN |
Seconds before retrying failed provider | 60 |
DEXALOT_PROVIDER_FAILOVER_MAX_FAILURES |
Max failures before marking provider unhealthy | 3 |
You can override RPC endpoints for specific chains using environment variables. This is useful for:
- Adding backup providers for redundancy
- Using custom RPC endpoints
- Testing with different providers
Two formats are supported:
- Chain ID format (preferred):
DEXALOT_RPC_<CHAIN_ID>=url1,url2,url3 - Native token symbol format:
DEXALOT_RPC_<NATIVE_TOKEN_SYMBOL>=url1,url2,url3
Chain ID takes precedence over native token symbol if both are set. Examples:
# Chain ID format (preferred)
DEXALOT_RPC_43114=https://api.avax.network/ext/bc/C/rpc,https://avalanche.public-rpc.com
DEXALOT_RPC_1=https://eth.llamarpc.com,https://ethereum.public-rpc.com
DEXALOT_RPC_42161=https://arb1.arbitrum.io/rpc
DEXALOT_RPC_432204=https://subnets.avax.network/dexalot/mainnet/rpc
# Native token symbol format (alternative)
DEXALOT_RPC_AVAX=https://api.avax.network/ext/bc/C/rpc,https://avalanche.public-rpc.com
DEXALOT_RPC_ETH=https://eth.llamarpc.com,https://ethereum.public-rpc.com
DEXALOT_RPC_ALOT=https://subnets.avax.network/dexalot/mainnet/rpc-
Provider Initialization: When the client initializes, it loads RPC endpoints from:
- Environment variable overrides (if set)
- API response (from Dexalot API)
- Multiple URLs can be provided (comma-separated)
-
Failover Strategy: When an RPC call fails:
- The failed provider is marked with a failure count
- The SDK automatically tries the next available provider
- If all providers fail, an error is raised
-
Health Tracking: Each provider tracks:
- Failure count (incremented on each failure)
- Last failure time (for cooldown calculation)
- Health status (healthy/unhealthy)
-
Recovery: After the cooldown period, failed providers can be retried. Providers are marked as unhealthy only after exceeding the max failure threshold.
from dexalot_sdk import DexalotClient
from dexalot_sdk.core.config import DexalotConfig
# Configure failover
config = DexalotConfig(
provider_failover_enabled=True,
provider_failover_cooldown=60, # 60 seconds cooldown
provider_failover_max_failures=3, # Mark unhealthy after 3 failures
)
client = DexalotClient(config=config)
await client.connect()
await client.initialize_client()
# RPC calls use failover automatically when the primary provider fails (if enabled)- With
provider_failover_enabled=False, only the primary RPC URL is used (no rotation). - When the API returns a single provider entry, the client uses that URL directly.
- Environment variables can override failover settings as documented above.
By default, plain http:// RPC URLs are rejected at provider setup time with a ValueError. This prevents accidental use of unencrypted connections in production.
| Option | Env Variable | Default | Description |
|---|---|---|---|
allow_insecure_rpc |
DEXALOT_ALLOW_INSECURE_RPC |
false |
Allow plain http:// RPC endpoints |
Security note: Plain
http://RPC connections transmit JSON-RPC calls (including signed transactions) without encryption. In production, always usehttps://endpoints. Only setallow_insecure_rpc=Truefor local development or trusted private networks.
# Allow http:// for local development only
config = DexalotConfig(allow_insecure_rpc=True)
client = DexalotClient(config=config)The SDK includes a comprehensive instrumentation layer to track API operations, performance metrics, and WebSocket events.
- Structured Logging: Logs are output in JSON format (or plain text) with metadata.
- Performance Tracking: Automatically tracks the duration of all core operations (
clob,swap,transfer). - Security: Designed with privacy by default:
- No Arguments: Function arguments and return values are never logged.
- No Payloads: Transaction payloads and private keys are never logged.
- Safe Defaults: Minimal logging in production (
INFO), detailed tracing only inDEBUG.
Control logging behavior using environment variables:
| Variable | Description | Default | Values |
|---|---|---|---|
DEXALOT_LOG_LEVEL |
Logging verbosity | INFO |
DEBUG, INFO, WARNING, ERROR |
DEXALOT_LOG_FORMAT |
Log output format | console |
json, console |
- CLOB: Full coverage of Order Management (
add/cancel/replace), Market Data (orderbook,pairs), and Account Data (open_orders). - Swap: RFQ operation lifecycle including Firm/Soft Quotes and Swap Execution.
- Transfer: Cross-chain Bridge operations (
deposit/withdraw), Portfolio Management (transfer_portfolio), and comprehensive Balance queries. - WebSocket: Connection lifecycle events (
Open/Close/Error) and message traffic (atDEBUGlevel).
{
"timestamp": "2023-10-27T10:00:00Z",
"level": "INFO",
"logger": "dexalot_sdk.core.clob",
"message": "clob completed in 0.123s",
"extra_fields": {
"operation": "clob",
"function": "add_order",
"duration": 0.123,
"status": "success"
}
}See examples/logging_console.py for a demonstration of logging capabilities.
The SDK manages several resources that need proper cleanup:
- HTTP sessions (
aiohttp.ClientSession) - Web3 provider sessions (internal
aiohttpsessions) - WebSocket connections (if WebSocket manager is enabled)
Always call await client.close() when you're done with the client to ensure proper resource cleanup:
async def main():
client = None
try:
client = DexalotClient()
await client.initialize_client()
# Your operations here
result = await client.get_tokens()
if result.success:
print(f"Tokens: {result.data}")
finally:
# Always close the client in a finally block
if client is not None:
await client.close()You can also use the client as an async context manager:
async def main():
async with DexalotClient() as client:
await client.initialize_client()
# Your operations here
result = await client.get_tokens()
if result.success:
print(f"Tokens: {result.data}")
# Client is automatically closed when exiting the contextNote: The close() method:
- Closes all HTTP sessions (SDK's main session and web3 provider sessions)
- Closes WebSocket connections (if enabled)
- Waits for graceful SSL connection shutdown
- Is safe to call multiple times (idempotent)
The SDK is fully async - all methods are async def and must be awaited. This enables concurrent operations and better performance.
For standalone scripts, use asyncio.run():
import asyncio
from dexalot_sdk import DexalotClient
async def main():
client = None
try:
client = DexalotClient()
await client.initialize_client()
# Your async operations here
result = await client.get_tokens()
if result.success:
print(f"Tokens: {result.data}")
finally:
if client is not None:
await client.close()
if __name__ == "__main__":
asyncio.run(main())In async applications (e.g., FastAPI, async web frameworks), use await directly:
from fastapi import FastAPI
from dexalot_sdk import DexalotClient
app = FastAPI()
client = DexalotClient()
@app.on_event("startup")
async def startup():
await client.initialize_client()
@app.on_event("shutdown")
async def shutdown():
await client.close()
@app.get("/tokens")
async def get_tokens():
result = await client.get_tokens()
if result.success:
return result.data
return {"error": result.error}The async architecture enables parallel operations for better performance:
import asyncio
from dexalot_sdk import DexalotClient
async def main():
client = None
try:
client = DexalotClient()
await client.initialize_client()
# Fetch multiple orderbooks in parallel
pairs = ["AVAX/USDC", "ALOT/USDC", "ETH/USDC"]
results = await asyncio.gather(
*[client.get_orderbook(pair) for pair in pairs]
)
for pair, result in zip(pairs, results, strict=True):
if result.success:
print(f"{pair}: {len(result.data['bids'])} bids")
finally:
if client is not None:
await client.close()See examples/async_parallel.py for more parallel operation examples.
The SDK uses a Result[T] pattern for consistent error handling across async operational methods.
Async operational methods return Result[T] with three fields:
success: bool- True if operation succeededdata: T | None- Result data on success, None on errorerror: str | None- Error message on failure, None on success
result = await client.add_order(
pair="AVAX/USDC",
side="BUY",
amount=1.0,
price=25.0,
)
if result.success:
print(f"Order placed: {result.data['tx_hash']}")
print(f"Client order ID: {result.data['client_order_id']}") # save for cancel/replace
else:
print(f"Order failed: {result.error}")
# Handle error (retry, log, notify user, etc.)Input validation errors are returned as Result.fail() with descriptive messages:
# Invalid amount (negative)
result = await client.add_order(
pair="AVAX/USDC",
side="BUY",
amount=-1.0, # Invalid!
price=25.0
)
if not result.success:
# result.error will be: "Invalid amount: must be positive (> 0), got -1.0"
print(f"Validation error: {result.error}")Error messages are automatically sanitized to prevent information leakage:
- File paths are removed
- URLs are removed
- Stack traces are removed
- User-friendly messages are provided
- Always check
result.successbefore accessingresult.data - Handle errors appropriately - log, retry, or notify users
- Use descriptive error messages - the SDK provides clear error messages
- Don't expose internal errors - error sanitization is automatic
async def place_order_safely(client, pair, side, amount, price):
result = await client.add_order(pair, side, amount, price)
if result.success:
return {"status": "success", "tx_hash": result.data["tx_hash"]}
else:
# Log error for debugging
logger.error(f"Order failed: {result.error}")
# Return user-friendly message
return {"status": "error", "message": "Failed to place order. Please try again."}See examples/error_handling.py for comprehensive error handling patterns.
All state-changing operations (placing orders, deposits, withdrawals, etc.) now support a wait_for_receipt parameter that controls whether the SDK waits for blockchain transaction confirmation before returning.
By default, all state-changing operations wait for transaction receipts (wait_for_receipt=True). This ensures:
- Transactions are confirmed on-chain before the method returns
- Transaction failures are detected immediately
- More reliable operation results
# Default behavior: waits for receipt (recommended)
result = await client.add_order("AVAX/USDC", "BUY", 1.0, 25.0)
# Method returns only after transaction is confirmed
# Explicitly wait for receipt
result = await client.add_order(
"AVAX/USDC", "BUY", 1.0, 25.0,
wait_for_receipt=True
)
# Don't wait for receipt (returns immediately after sending)
result = await client.add_order(
"AVAX/USDC", "BUY", 1.0, 25.0,
wait_for_receipt=False
)
# Method returns immediately with transaction hash
# Transaction may still be pendingUse wait_for_receipt=False when:
- Batch operations: Sending many transactions and want to submit them quickly
- Fire-and-forget: You don't need immediate confirmation
- Custom polling: You'll check transaction status yourself
Important: When wait_for_receipt=False, the SDK returns immediately after broadcasting the transaction. You should:
- Check transaction status yourself using the returned
tx_hash - Handle potential transaction failures in your application logic
- Be aware that the transaction may still be pending when the method returns
All state-changing methods support wait_for_receipt:
CLOB Operations:
add_order(pair, side, amount, price, order_type="LIMIT", client_order_id=None, wait_for_receipt=True)add_limit_order_list(orders, wait_for_receipt=True)cancel_order(order_id, wait_for_receipt=True)cancel_order_by_client_id(client_order_id, wait_for_receipt=True)cancel_list_orders(order_ids, wait_for_receipt=True)cancel_list_orders_by_client_id(client_order_ids, wait_for_receipt=True)replace_order(order_id, new_price, new_amount, client_order_id=None, wait_for_receipt=True)cancel_add_list(replacements, wait_for_receipt=True)
Every canonical SDK order has two identifiers, one transaction hash for state-changing actions, and a normalized order shape for reads:
| Field | Source | Description |
|---|---|---|
internal_order_id |
Contract-assigned | Assigned by the TradePairs contract at placement; present in all order data returned by get_open_orders, get_order, and get_order_by_client_id |
client_order_id |
Caller-specified | Provided by the caller at placement (or generated by the SDK when omitted); echoed back in every placement and cancel/replace result |
tx_hash |
Blockchain | Transaction hash for the on-chain action; not an order identifier |
Order reads (get_open_orders, get_order, get_order_by_client_id) return a full canonical order dict with these fields:
internal_order_id,client_order_id,trade_pair_id,pairprice,total_amount,quantity,quantity_filled,total_feetrader_address,side,type1,type2,statusupdate_block,create_block,create_ts,update_ts
Enum-style fields are normalized to human-readable strings such as BUY, SELL, LIMIT, GTC, and FILLED. Block fields are returned as Python integers, not hex strings.
Placement methods — all four placement functions (add_order, add_limit_order_list, replace_order, cancel_add_list) accept an optional client_order_id. When omitted, the SDK generates a random 32-byte value. The result always contains client_order_id (or client_order_ids for batch calls) so you can record the ID for later operations.
Cancel/replace results — cancel and replace methods return typed ID fields so you always know exactly what was cancelled and what was created:
| Method | Returns |
|---|---|
cancel_order |
cancelled_client_order_id, cancelled_internal_order_id |
cancel_order_by_client_id |
cancelled_client_order_id |
cancel_list_orders |
cancelled_internal_order_ids |
cancel_list_orders_by_client_id |
cancelled_client_order_ids |
replace_order |
cancelled_client_order_id, cancelled_internal_order_id, client_order_id (new) |
cancel_add_list |
cancelled_client_order_ids, cancelled_internal_order_ids, client_order_ids (new) |
Routing by identifier type:
get_order()andcancel_order()andreplace_order()accept either type (order_idparameter).get_order_by_client_id()andcancel_order_by_client_id()take aclient_order_idspecifically.cancel_add_list()acceptsorder_idper replacement (either type); also inferspairfrom the existing order when possible.- Never pass
tx_hashto order lookup, cancel, or replace methods.
Transfer Operations:
deposit(token, amount, source_chain, use_layerzero=False, wait_for_receipt=True)withdraw(token, amount, destination_chain, use_layerzero=False, wait_for_receipt=True)add_gas(amount, wait_for_receipt=True)remove_gas(amount, wait_for_receipt=True)transfer_portfolio(token, amount, to_address, wait_for_receipt=True)
Swap Operations:
execute_rfq_swap(quote, wait_for_receipt=True)
# Place multiple orders without waiting for each receipt
orders = [
{"pair": "AVAX/USDC", "side": "BUY", "amount": 1.0, "price": 25.0},
{"pair": "AVAX/USDC", "side": "BUY", "amount": 2.0, "price": 24.0},
{"pair": "AVAX/USDC", "side": "SELL", "amount": 1.0, "price": 26.0},
]
# Submit all orders quickly without waiting
result = await client.add_limit_order_list(orders, wait_for_receipt=False)
if result.success:
tx_hash = result.data["tx_hash"]
# Check status later
# await check_transaction_status(tx_hash)# Submit deposit and continue with other operations
result = await client.deposit("AVAX", 1.0, "Avalanche", wait_for_receipt=False)
if result.success:
tx_hash = result.data # Just the transaction hash
# Continue with other operations
# Monitor deposit status separatelyThe SDK automatically standardizes API response field names to match Python naming conventions (snake_case). This ensures consistent field names regardless of API response format variations.
Orders API:
internal_order_id(fromid)client_order_id(fromclientordid,clientOrderId)trade_pair_id(fromtradePairId, or derived frompairwhen needed)pair,price,quantity,total_amount,quantity_filled,total_feetrader_address,side,type1,type2,statuscreate_block,update_block,create_ts,update_ts
Orders are normalized into one canonical SDK shape regardless of whether the source was the REST API or the contract.
Environments API:
chain_id(fromchainid,chainId)env_type(fromtype,envType)rpc(fromchain_instance)network(fromchain_display_name)
Tokens API:
evm_decimals(fromevmdecimals,evmDecimals,decimals)chain_id(fromchainid,chainId)network(fromchain_display_name)
Pairs API:
base_decimals,quote_decimalsbase_display_decimals,quote_display_decimalsmin_trade_amount,max_trade_amount
RFQ Quotes API:
chainId(fromchainid,chain_id)secureQuote(fromsecurequote,secure_quote)quoteId(fromquoteid,quote_id)- Nested order data:
nonceAndMeta,makerAsset,takerAsset,makerAmount,takerAmount
Deployment API:
env,address,abi(handles variations likeEnv,Address,Abi)
- Consistent interface: Field names are exposed in snake_case in Python consistently.
- Alias handling: Common camelCase and alternate keys from the API are normalized automatically.
All API responses are automatically transformed before being returned, so you can always rely on standardized field names.
The SDK includes several reliability features that work automatically to improve stability and performance.
Automatic retry with exponential backoff for transient failures:
- Default: 3 attempts with exponential backoff (1s, 2s, 4s)
- Retries on: HTTP 429, 500, 502, 503, 504 and network errors
- Configurable: Via
DexalotConfigor environment variables
from dexalot_sdk import DexalotClient
from dexalot_sdk.core.config import DexalotConfig
# Custom retry configuration
config = DexalotConfig(
retry_enabled=True,
retry_max_attempts=5,
retry_initial_delay=2.0, # Start with 2s delay
retry_max_delay=30.0, # Max 30s between retries
retry_exponential_base=2.0
)
client = DexalotClient(config=config)Token bucket rate limiter prevents API throttling:
- Default: 5 requests/second for API, 10 requests/second for RPC
- Automatic: Applied to all HTTP and RPC calls
- Configurable: Via
DexalotConfigor environment variables
config = DexalotConfig(
rate_limit_enabled=True,
rate_limit_requests_per_second=10.0, # 10 API calls/second
rate_limit_rpc_per_second=20.0 # 20 RPC calls/second
)Automatic nonce management prevents transaction race conditions:
- Automatic: Tracks nonces per (chain_id, address) combination
- Thread-safe: Uses async locks for concurrent transactions
- Default-on: No manual nonce bookkeeping for normal use
The nonce manager is enabled by default and works automatically. It:
- Fetches the current nonce from the chain on first use
- Tracks nonces locally for subsequent transactions
- Automatically increments nonces for each transaction
- Prevents race conditions in concurrent scenarios
# Nonce manager works automatically - no configuration needed
# For high-concurrency scenarios, it's already handling nonces correctly
# Multiple transactions can be sent concurrently
tasks = [
client.add_order("AVAX/USDC", "BUY", 1.0, 25.0),
client.add_order("ALOT/USDC", "BUY", 10.0, 0.5),
client.deposit("AVAX", 1.0)
]
results = await asyncio.gather(*tasks)
# Nonce manager ensures correct nonce orderingAutomatic RPC provider failover (see Provider Failover section above).
The SDK includes a persistent WebSocket manager for long-running subscriptions with automatic reconnection and heartbeat.
- Persistent Connections: Single connection for multiple subscriptions
- Multiple Subscriptions: Subscribe to multiple topics with individual callbacks
- Automatic Reconnection: Exponential backoff reconnection on failures
- Heartbeat Monitoring: Ping/pong mechanism to detect dead connections
- Thread-Safe: Safe for concurrent use
from dexalot_sdk import DexalotClient
async def main():
client = None
try:
client = DexalotClient()
await client.initialize_client()
# Enable WebSocket manager
config = client.config
config.ws_manager_enabled = True
# Subscribe to orderbook updates
def on_orderbook_update(message):
print(f"Orderbook update: {message}")
await client.subscribe_to_events(
topic="OrderBook/AVAX/USDC",
callback=on_orderbook_update,
is_private=False
)
# Subscribe to private order updates
def on_order_update(message):
print(f"Order update: {message}")
await client.subscribe_to_events(
topic="Orders",
callback=on_order_update,
is_private=True
)
# Keep connection alive
await asyncio.sleep(60)
# Unsubscribe when done
client.unsubscribe_from_events("OrderBook/AVAX/USDC")
finally:
# Always close the client to clean up WebSocket and HTTP sessions
if client is not None:
await client.close()
asyncio.run(main())from dexalot_sdk.core.config import DexalotConfig
config = DexalotConfig(
ws_manager_enabled=True,
ws_ping_interval=30, # Ping every 30 seconds
ws_ping_timeout=10, # Wait 10s for pong before reconnecting
ws_reconnect_initial_delay=1.0,
ws_reconnect_max_delay=60.0,
ws_reconnect_exponential_base=2.0,
ws_reconnect_max_attempts=10 # 0 = infinite retries
)
client = DexalotClient(config=config)Use subscribe_to_events() with the manager enabled:
async def on_message(message):
print(f"Received: {message}")
# Start the manager on first subscription
await client.subscribe_to_events(
topic="OrderBook/AVAX/USDC",
callback=on_message,
is_private=False
)
# Later, unsubscribe and close the client when done
client.unsubscribe_from_events("OrderBook/AVAX/USDC")See examples/websocket_manager.py for complete examples.
The SDK automatically validates all input parameters before processing operations. This prevents invalid data from reaching the blockchain or API. Validation is implemented in utils/input_validators.py and returns Result[None] for consistent error handling.
Input validation is applied to all critical methods:
- CLOB Operations:
add_order(),cancel_order(),get_orderbook(), etc. - Swap Operations:
execute_rfq_swap(),get_swap_firm_quote(), etc. - Transfer Operations:
deposit(),withdraw(),transfer_portfolio(), etc.
- Amounts: Must be positive, finite numbers (not NaN or infinite)
- Prices: Must be positive, finite numbers
- Addresses: Must be valid Ethereum addresses (0x prefix, 42 chars, hex)
- Pairs: Must be in
TOKEN/TOKENformat - Order IDs: Must be valid prefixed hex, decimal-string internal IDs, bytes32 values, or plain client IDs that fit in bytes32
- Token Symbols: Must be non-empty, alphanumeric strings
Validation errors are returned as Result.fail() with descriptive messages:
# Invalid amount
result = await client.add_order(
pair="AVAX/USDC",
side="BUY",
amount=-1.0, # Invalid: negative amount
price=25.0
)
if not result.success:
# result.error: "Invalid amount: must be positive (> 0), got -1.0"
print(result.error)
# Invalid address
result = await client.get_portfolio_balance(
token="USDC",
address="invalid" # Not a valid Ethereum address
)
if not result.success:
# result.error: "Invalid address: must be a valid Ethereum address (0x prefix, 42 chars, hex)"
print(result.error)| Error | Cause | Solution |
|---|---|---|
| "Invalid amount: must be positive" | Negative or zero amount | Use positive values |
| "Invalid address: must be a valid Ethereum address" | Invalid address format | Use 0x-prefixed hex addresses |
| "Invalid pair: must be in TOKEN/TOKEN format" | Invalid pair format | Use format like "AVAX/USDC" |
| "Invalid order_id: must be hex string or bytes32" | Invalid order ID | Use valid hex string |
Validation happens before any network calls, so invalid inputs fail fast with clear error messages.