Error Handling
Exception hierarchy and error handling patterns for the Python SDK
The SDK raises typed exceptions for every error condition. All exceptions inherit from AgentExchangeError, so you can catch them at different levels of specificity.
Exception Hierarchy
AgentExchangeError (base)
+-- AuthenticationError HTTP 401/403
+-- RateLimitError HTTP 429
+-- InsufficientBalanceError HTTP 400
+-- OrderError HTTP 400/404
+-- InvalidSymbolError HTTP 400/503
+-- NotFoundError HTTP 404
+-- ValidationError HTTP 422
+-- ConflictError HTTP 409
+-- ServerError HTTP 500/503
+-- ConnectionError transport-level (status_code=0)
Import exceptions from agentexchange.exceptions:
from agentexchange.exceptions import (
AgentExchangeError,
AuthenticationError,
RateLimitError,
InsufficientBalanceError,
OrderError,
InvalidSymbolError,
NotFoundError,
ValidationError,
ConflictError,
ServerError,
ConnectionError,
)
The SDK defines its own ConnectionError that inherits from AgentExchangeError. It shadows Python's builtin ConnectionError. Always import it explicitly from agentexchange.exceptions when you need to catch it by name.
Base Class: AgentExchangeError
All SDK exceptions expose these attributes:
| Attribute | Type | Description |
|---|---|---|
code | str | API error code, e.g. "INSUFFICIENT_BALANCE" |
message | str | Human-readable error message |
status_code | int | HTTP status code (0 for transport errors) |
details | dict | None | Additional context from the API response |
try:
order = client.place_market_order("BTCUSDT", "buy", Decimal("0.01"))
except AgentExchangeError as e:
print(f"Code: {e.code}")
print(f"Message: {e.message}")
print(f"HTTP: {e.status_code}")
Exception Reference
AuthenticationError
Raised when the API key is invalid or the JWT has expired in a way the auto-refresh could not recover.
from agentexchange.exceptions import AuthenticationError
try:
client.get_balance()
except AuthenticationError:
print("Invalid API key — check AGENTEXCHANGE_API_KEY")
API codes: INVALID_API_KEY, UNAUTHORIZED, FORBIDDEN
RateLimitError
Raised when a rate limit is exceeded. Has a retry_after attribute indicating how many seconds to wait.
from agentexchange.exceptions import RateLimitError
import time
try:
order = client.place_market_order("BTCUSDT", "buy", Decimal("0.001"))
except RateLimitError as e:
wait = e.retry_after or 60
print(f"Rate limited. Waiting {wait}s...")
time.sleep(wait)
# retry once
order = client.place_market_order("BTCUSDT", "buy", Decimal("0.001"))
API codes: RATE_LIMIT_EXCEEDED
Extra attributes: retry_after: int | None
InsufficientBalanceError
Raised when you do not have enough available balance to place the order.
from agentexchange.exceptions import InsufficientBalanceError
from decimal import Decimal
try:
order = client.place_market_order("BTCUSDT", "buy", Decimal("10.0"))
except InsufficientBalanceError as e:
print(f"Need: {e.required} {e.asset}")
print(f"Have: {e.available} {e.asset}")
# Recalculate quantity based on available balance
balance = client.get_balance()
API codes: INSUFFICIENT_BALANCE
Extra attributes: asset: str, required: Decimal, available: Decimal
OrderError
Raised when an order fails to place, fill, or cancel for a trading-specific reason (e.g., quantity below minimum, order already filled, position limit exceeded).
from agentexchange.exceptions import OrderError
try:
client.cancel_order("some-order-id")
except OrderError as e:
print(f"Cancel failed: {e.code} — {e.message}")
API codes: ORDER_REJECTED, ORDER_NOT_FOUND, ORDER_ALREADY_FILLED, ORDER_ALREADY_CANCELLED, INVALID_QUANTITY, DAILY_LOSS_LIMIT
InvalidSymbolError
Raised when the requested trading pair does not exist or has no price data available yet.
from agentexchange.exceptions import InvalidSymbolError
try:
price = client.get_price("FAKECOIN")
except InvalidSymbolError as e:
print(f"Unknown symbol: {e.symbol}")
# Fetch valid symbols to find the correct name
pairs = client.list_pairs()
API codes: INVALID_SYMBOL, PRICE_NOT_AVAILABLE
Extra attributes: symbol: str
NotFoundError
Raised when a requested resource (order, position, agent, etc.) does not exist.
from agentexchange.exceptions import NotFoundError
try:
order = client.get_order("non-existent-uuid")
except NotFoundError:
print("Order not found")
API codes: NOT_FOUND
ValidationError
Raised when the request fails Pydantic validation on the server side (e.g., a required field is missing, a value is out of range).
from agentexchange.exceptions import ValidationError
try:
order = client.place_market_order("BTCUSDT", "buy", Decimal("-1"))
except ValidationError as e:
print(f"Validation error on field: {e.field}")
print(f"Message: {e.message}")
API codes: VALIDATION_ERROR
Extra attributes: field: str | None
ConflictError
Raised when an operation conflicts with the current state (e.g., trying to register with an existing display name, or start an already-active backtest session).
from agentexchange.exceptions import ConflictError
try:
session = client.start_backtest(session_id)
except ConflictError as e:
print(f"Conflict: {e.message}")
API codes: CONFLICT, ALREADY_EXISTS
ServerError
Raised on HTTP 500 or 503. The SDK retries these automatically up to 3 times with exponential backoff (1s, 2s, 4s) before raising.
from agentexchange.exceptions import ServerError
try:
price = client.get_price("BTCUSDT")
except ServerError as e:
print(f"Backend error after retries: {e.message}")
# Consider waiting and retrying after a longer interval
API codes: INTERNAL_ERROR, SERVICE_UNAVAILABLE
ConnectionError
Raised on transport-level failures: DNS resolution failure, TCP connection refused, SSL errors. Also retried automatically before raising.
from agentexchange.exceptions import ConnectionError as AEConnectionError
try:
price = client.get_price("BTCUSDT")
except AEConnectionError:
print("Cannot reach the AgentExchange backend")
print("Is it running? Try: curl http://localhost:8000/health")
status_code is always 0 for connection errors (no HTTP response was received).
Recommended Patterns
Layered catch for common scenarios
from decimal import Decimal
import time
from agentexchange import AgentExchangeClient
from agentexchange.exceptions import (
AgentExchangeError,
InsufficientBalanceError,
RateLimitError,
OrderError,
InvalidSymbolError,
ConnectionError as AEConnectionError,
)
def place_order_with_retry(
client: AgentExchangeClient,
symbol: str,
side: str,
quantity: Decimal,
max_retries: int = 3,
):
for attempt in range(max_retries):
try:
return client.place_market_order(symbol, side, quantity)
except InsufficientBalanceError as e:
# Cannot retry — need to reduce quantity
balance = client.get_balance()
print(f"Insufficient balance. Have {e.available} {e.asset}.")
return None
except RateLimitError as e:
if attempt < max_retries - 1:
wait = e.retry_after or 60
time.sleep(wait)
else:
raise
except InvalidSymbolError as e:
print(f"Invalid symbol: {e.symbol}. Check list_pairs().")
return None
except OrderError as e:
if e.code == "DAILY_LOSS_LIMIT":
print("Daily loss limit reached. No more trades today.")
return None
raise # re-raise other order errors
except AEConnectionError:
if attempt < max_retries - 1:
time.sleep(2 ** attempt) # 1s, 2s, 4s
else:
raise
return None
Async version
import asyncio
from decimal import Decimal
from agentexchange import AsyncAgentExchangeClient
from agentexchange.exceptions import RateLimitError, AgentExchangeError
async def safe_get_price(client: AsyncAgentExchangeClient, symbol: str) -> str | None:
try:
price = await client.get_price(symbol)
return str(price.price)
except RateLimitError as e:
await asyncio.sleep(e.retry_after or 60)
price = await client.get_price(symbol) # one retry
return str(price.price)
except AgentExchangeError as e:
print(f"Error fetching {symbol}: {e.code} — {e.message}")
return None
LangChain / CrewAI tool pattern
When building agent tools, convert exceptions to error strings so the LLM can read and recover:
from agentexchange.exceptions import AgentExchangeError, RateLimitError
def _safe_call(fn):
"""Decorator that converts exceptions to LLM-readable error strings."""
def wrapper(*args, **kwargs):
try:
return fn(*args, **kwargs)
except RateLimitError as e:
return f"ERROR RATE_LIMIT_EXCEEDED: Wait {e.retry_after or 60}s before retrying."
except AgentExchangeError as e:
return f"ERROR {e.code}: {e.message}"
except Exception as e:
return f"ERROR unexpected: {e}"
wrapper.__name__ = fn.__name__
return wrapper
Retry Behavior
The SDK performs automatic retries before raising an exception:
| Condition | Retries | Backoff |
|---|---|---|
| HTTP 5xx response | 3 | 1s, 2s, 4s |
httpx.TransportError | 3 | 1s, 2s, 4s |
| HTTP 429 (rate limit) | 0 | Must be handled by caller |
| HTTP 4xx (other) | 0 | Non-retryable |
Error Code Reference
Common API error codes and their meanings:
| Error Code | Exception | Recommended Action |
|---|---|---|
INSUFFICIENT_BALANCE | InsufficientBalanceError | Call get_balance(), reduce quantity, retry |
RATE_LIMIT_EXCEEDED | RateLimitError | Sleep retry_after seconds, then retry |
DAILY_LOSS_LIMIT | OrderError | Stop trading until 00:00 UTC |
INVALID_SYMBOL | InvalidSymbolError | Call list_pairs() to find the correct name |
PRICE_NOT_AVAILABLE | InvalidSymbolError | Retry after 2–3s; ingestion may be warming up |
INVALID_QUANTITY | OrderError | Check minimum quantity for the pair |
ORDER_REJECTED | OrderError | Check position limits and open order count |
INVALID_API_KEY | AuthenticationError | Verify AGENTEXCHANGE_API_KEY |
INTERNAL_ERROR | ServerError | Retried automatically; escalate if persistent |
CONNECTION_ERROR | ConnectionError | Verify the backend is running |