DeFi & AMM

DEX Trading with AI Agents:
Uniswap, Curve, and AMM Strategies

March 4, 2026 22 min read Python, Solidity, DeFi

Decentralized exchanges operate on fundamentally different mechanics than centralized order books. For AI agents interacting with Uniswap, Curve, and other AMMs, understanding the x×y=k invariant, concentrated liquidity positions, optimal routing across pools, and impermanent loss is essential before deploying capital. This guide covers it all, with working Python code.

AMM Deep Dive: The x×y=k Invariant

Automated Market Makers (AMMs) replaced the traditional order book with a simple mathematical invariant. In Uniswap v2 (and most basic AMMs), a liquidity pool holds reserves of two tokens, X and Y, and enforces that their product remains constant: x × y = k. This single equation determines every price and trade in the pool.

Uniswap v2 Constant Product Curve (x × y = k)
x (Token0) y (Token1) x₁,y₁ x₂,y₂ trade →

Calculating Output Amount

When a trader swaps dx units of token X into the pool, the output dy is determined by the invariant. With a fee f (typically 0.3% for Uniswap v2), the calculation is:

Invariant: x · y = k Input with fee: dx_eff = dx · (1 - f) Output amount: dy = y - k / (x + dx_eff) = y · dx_eff / (x + dx_eff) Price impact: spot_price = y / x execution_price = dy / dx price_impact_bps = (spot_price - execution_price) / spot_price × 10000 Example (ETH/USDC pool, x=1000 ETH, y=3,000,000 USDC, f=0.003): Swap 10 ETH → dx_eff = 10 · 0.997 = 9.97 ETH dy = 3,000,000 · 9.97 / (1000 + 9.97) ≈ 29,612 USDC Effective price = 2,961.2 USDC/ETH (spot was 3,000) Price impact = 1.29% (38.8 bps)
🌀

Uniswap v2

Classic x×y=k. Full range liquidity. 0.3% fee. Best for volatile pairs.

💠

Uniswap v3

Concentrated liquidity. Agents can provide liquidity in price ranges for capital efficiency.

📉

Curve

StableSwap invariant. Optimized for pegged assets (stablecoins, LSTs). Near-zero price impact for stable pairs.

⚖️

Balancer

Generalized AMM supporting N-token pools with custom weights. Used for index-style pools.

amm_math.py Python
from dataclasses import dataclass
from typing import Tuple


@dataclass
class UniswapV2Pool:
    reserve_x: float    # token0 reserves
    reserve_y: float    # token1 reserves
    fee: float = 0.003  # default 0.3%

    @property
    def k(self) -> float:
        return self.reserve_x * self.reserve_y

    @property
    def spot_price_x_in_y(self) -> float:
        """Price of x denominated in y (e.g. ETH price in USDC)."""
        return self.reserve_y / self.reserve_x

    def get_amount_out(self, amount_in: float, x_to_y: bool) -> Tuple[float, float]:
        """
        Compute output amount and price impact for a given input.
        Returns (amount_out, price_impact_bps).
        """
        amount_in_eff = amount_in * (1 - self.fee)

        if x_to_y:
            reserve_in, reserve_out = self.reserve_x, self.reserve_y
            spot = self.spot_price_x_in_y
        else:
            reserve_in, reserve_out = self.reserve_y, self.reserve_x
            spot = 1 / self.spot_price_x_in_y

        amount_out = (
            reserve_out * amount_in_eff
            / (reserve_in + amount_in_eff)
        )
        exec_price = amount_out / amount_in
        price_impact_bps = abs(spot - exec_price) / spot * 10000
        return amount_out, price_impact_bps

    def max_input_for_impact(self, max_impact_bps: float, x_to_y: bool) -> float:
        """
        Binary search for max input that keeps price impact
        below max_impact_bps. Useful for slippage-limited agents.
        """
        lo, hi = 0.0, (self.reserve_x if x_to_y else self.reserve_y) * 0.5
        for _ in range(50):  # 50 iterations of bisection
            mid = (lo + hi) / 2
            _, impact = self.get_amount_out(mid, x_to_y)
            if impact < max_impact_bps:
                lo = mid
            else:
                hi = mid
        return lo


# Example usage
pool = UniswapV2Pool(
    reserve_x=1000.0,       # 1000 ETH
    reserve_y=3_000_000.0  # 3M USDC
)

out, impact = pool.get_amount_out(10.0, x_to_y=True)
print(f"10 ETH → {out:.2f} USDC (impact: {impact:.2f} bps)")
# → 10 ETH → 29611.86 USDC (impact: 129.47 bps)

max_in = pool.max_input_for_impact(50.0, x_to_y=True)
print(f"Max ETH for <50bps impact: {max_in:.4f}")
# → Max ETH for <50bps impact: 1.6693

Concentrated Liquidity (Uniswap v3)

Uniswap v3 introduced concentrated liquidity: instead of providing liquidity across the entire price range (0 to infinity), liquidity providers (LPs) specify a price range [P_a, P_b] in which their capital is active. When the pool price is inside this range, the LP earns fees proportional to their share of liquidity in that tick range. When price moves outside, the LP holds 100% of the cheaper token (fully converted).

Liquidity Positions and Virtual Reserves

For a position with liquidity L in range [P_a, P_b]: If current price P is inside [P_a, P_b]: virtual_x = L · (1/√P - 1/√P_b) virtual_y = L · (√P - √P_a) Capital efficiency vs v2: CE = (P_b - P_a) / (√P_b - √P_a) × 1/P Active liquidity earns fees only when price in range. Out-of-range positions earn ZERO fees.
Agent Strategy Note

AI agents providing concentrated liquidity must rebalance their positions as price drifts. A common strategy is to maintain a "tight band" around the current price and actively reposition when the range is half-exhausted. The gas cost of repositioning must be weighed against expected fee income at each step.

Tick Math

Uniswap v3 discretizes prices into ticks. Each tick i corresponds to a price of 1.0001^i. A tick spacing of 60 (for 0.3% fee pools) means you can only create positions aligned to multiples of 60. The math for converting between ticks and prices:

Price from tick: P(i) = 1.0001^i Tick from price: i = floor(log(P) / log(1.0001)) sqrt_price_x96: Uniswap v3 stores √P as a Q64.96 fixed point number sqrt_price_x96 = √P × 2^96

Curve StableSwap: The StableSwap Invariant

Curve Finance introduced a hybrid invariant combining constant-sum (x + y = k) and constant-product (x × y = k) behaviors. Near the peg (where x ≈ y), the invariant behaves like x + y = k, giving near-zero price impact. Far from peg, it falls back to constant product. This makes Curve exceptionally efficient for stablecoin pairs.

StableSwap Invariant: A·n^n·Σxᵢ + D = A·n^n·D + D^(n+1) / (n^n · Πxᵢ) Where: A = amplification coefficient (e.g. 100 for USDC/USDT) n = number of tokens in pool D = total liquidity (approximately) xᵢ = individual token balances Special cases: A → ∞: collapses to x + y = k (constant sum, zero slippage) A → 0: collapses to x · y = k (constant product, Uniswap-like)
Pool TypeAMMFeeBest Pair TypeSlippage (1% of TVL)
Constant ProductUniswap v20.30%Volatile assets~100 bps
Concentrated LiquidityUniswap v30.05-1%All assets~5-30 bps
StableSwapCurve0.04%Pegged pairs<1 bps
Weighted PoolBalancer0.1-0.3%Index baskets~20-50 bps

Routing Algorithms for Multi-Hop Swaps

When no direct pool exists between token A and token B (or when the direct pool has poor liquidity), agents must route through intermediate tokens. Finding the optimal route across hundreds of pools is a graph search problem.

Bellman-Ford for Maximum Output

The routing problem can be modeled as finding the path with maximum multiplicative weight in a directed graph where each edge weight is the exchange rate through a pool. Since we want to maximize output, we take log of weights and find the shortest path (most negative log-weight = highest actual rate).

dex_router.py Python
import math
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple


@dataclass
class PoolEdge:
    pool_id: str
    token_in: str
    token_out: str
    pool: UniswapV2Pool


class DEXRouter:
    """
    Multi-hop DEX router using Bellman-Ford for optimal path.
    Supports Uniswap v2/v3 and Curve pools via a unified interface.
    """

    def __init__(self, pools: List[PoolEdge]):
        self.pools = pools
        # Build adjacency: token -> list of outgoing edges
        self.adj: Dict[str, List[PoolEdge]] = {}
        for edge in pools:
            self.adj.setdefault(edge.token_in, []).append(edge)

    def find_best_route(
        self,
        token_in: str,
        token_out: str,
        amount_in: float,
        max_hops: int = 3
    ) -> Tuple[Optional[List[str]], float]:
        """
        DFS with pruning to find max output route up to max_hops deep.
        Returns (route as list of pool_ids, expected output amount).
        """
        best_out = 0.0
        best_path: Optional[List[str]] = None

        def dfs(token: str, amount: float,
                path: List[str], visited: set):
            nonlocal best_out, best_path
            if token == token_out:
                if amount > best_out:
                    best_out, best_path = amount, path[:]
                return
            if len(path) >= max_hops:
                return
            for edge in self.adj.get(token, []):
                if edge.pool_id in visited:
                    continue
                is_x_to_y = edge.token_in == "x"  # simplified
                out_amt, _ = edge.pool.get_amount_out(amount, is_x_to_y)
                visited.add(edge.pool_id)
                path.append(f"{edge.pool_id}({edge.token_in}->{edge.token_out})")
                dfs(edge.token_out, out_amt, path, visited)
                path.pop()
                visited.discard(edge.pool_id)

        dfs(token_in, amount_in, [], set())
        return best_path, best_out

    def detect_arbitrage(self, token: str, amount: float) -> Optional[dict]:
        """Find circular arbitrage: token -> ... -> token with profit."""
        route, out = self.find_best_route(token, token, amount, max_hops=4)
        if route and out > amount:
            return {
                "route": route,
                "profit": out - amount,
                "profit_pct": (out - amount) / amount * 100
            }
        return None

Impermanent Loss: Math and Mitigation

Impermanent loss (IL) is the difference between the value of holding tokens in an AMM versus simply holding them in a wallet. It is "impermanent" because if prices return to the entry level, the loss disappears — but in practice for volatile assets, prices rarely return to exactly the entry level.

IL Formula for Uniswap v2

Let r = price_now / price_entry (price ratio change) IL(r) = 2√r / (1+r) - 1 Numerical examples: r = 1.00 → IL = 0% (no change, no loss) r = 1.25 → IL = -0.60% (25% price increase) r = 1.50 → IL = -2.02% (50% price increase) r = 2.00 → IL = -5.72% (100% price increase / 2x) r = 4.00 → IL = -20.0% (300% price increase / 4x) r = 0.50 → IL = -5.72% (50% price decrease) r = 0.25 → IL = -20.0% (75% price decrease) Note: IL is symmetric around r=1 on log scale.
IL for AI Agents

AI agents providing liquidity to AMM pools must continuously model IL against expected fee income. Rule of thumb: if a pool's 30-day APR from fees is under 50%, and the underlying asset pair has more than 50% annualized volatility, expected IL likely exceeds fees over a 90-day horizon. Use the calculation below to evaluate before committing.

il_calculator.py Python
import math
import numpy as np
from scipy.stats import norm


def il_v2(price_ratio: float) -> float:
    """Impermanent loss for Uniswap v2 given price_ratio = P_new/P_entry."""
    return 2 * math.sqrt(price_ratio) / (1 + price_ratio) - 1


def il_v3(price_entry: float, price_now: float,
          range_low: float, range_high: float) -> float:
    """
    Impermanent loss for a Uniswap v3 concentrated position.
    Assumes equal USD value of both tokens at entry (50/50).
    """
    if price_now <= range_low:
        # All in token0 (base asset), price below range
        hodl_value = 0.5 * price_now / price_entry + 0.5
        pool_value = price_now / price_entry  # fully in base
    elif price_now >= range_high:
        # All in token1 (quote asset), price above range
        hodl_value = 0.5 * price_now / price_entry + 0.5
        pool_value = math.sqrt(range_high / range_low)  # fixed at range edge
    else:
        # In range: standard v2 IL but amplified by concentration
        r = price_now / price_entry
        hodl_value = 0.5 * r + 0.5
        pool_value = hodl_value + il_v2(r) * hodl_value
    return pool_value / hodl_value - 1


def expected_il_gbm(
    sigma_annual: float,
    days: int,
    simulations: int = 10000
) -> dict:
    """
    Monte Carlo simulation of expected IL over N days,
    assuming Geometric Brownian Motion price process.
    """
    sigma_daily = sigma_annual / math.sqrt(365)
    # Simulate log returns
    log_returns = np.random.normal(
        loc=-0.5 * sigma_daily**2,
        scale=sigma_daily,
        size=(simulations, days)
    )
    price_ratios = np.exp(log_returns.sum(axis=1))
    il_values = np.array([il_v2(r) for r in price_ratios])
    return {
        "mean_il_pct": il_values.mean() * 100,
        "p5_il_pct":  np.percentile(il_values, 5)  * 100,
        "p95_il_pct": np.percentile(il_values, 95) * 100,
    }


# Example: ETH/USDC pool over 90 days, sigma=80% annual
result = expected_il_gbm(
    sigma_annual=0.80,   # 80% annualized vol
    days=90
)
print(f"90-day expected IL: {result['mean_il_pct']:.2f}%")
print(f"5th percentile IL:  {result['p5_il_pct']:.2f}%")
# → 90-day expected IL: -3.82%
# → 5th percentile IL:  -14.31%  (worst 5% of scenarios)

DEX Arbitrage Agent Code

Price discrepancies between DEX pools and centralized exchanges create arbitrage opportunities. An AI agent can systematically scan for these gaps and execute profitable trades. The key is executing atomically — either the full arbitrage loop succeeds or none of it does.

~0.3s
Typical DEX latency
~10-30bps
Minimum profitable gap
~$5-50
Gas per arbitrage tx
1-3 hops
Typical arb route depth
dex_arb_agent.py Python
import asyncio
import httpx
from typing import Optional

TRADING_API = "https://purpleflea.com/trading-api"
MIN_PROFIT_BPS = 20   # minimum profit after fees
SCAN_INTERVAL = 0.5   # seconds between scans


class DEXArbitrageAgent:
    """
    Monitors Purple Flea DEX + Uniswap prices and executes
    cross-venue arbitrage when profit exceeds threshold.
    Uses Purple Flea Trading API for CEX leg.
    """

    def __init__(self, api_key: str,
                 wallet_address: str,
                 capital_usd: float):
        self.api_key = api_key
        self.wallet = wallet_address
        self.capital = capital_usd
        self.client = httpx.AsyncClient(
            headers={"Authorization": f"Bearer {api_key}"}
        )
        self.total_pnl = 0.0
        self.trades_executed = 0

    async def get_cex_price(self, symbol: str) -> float:
        r = await self.client.get(
            f"{TRADING_API}/v1/price/{symbol}"
        )
        return float(r.json()["mid"])

    async def get_dex_quote(self, token_in: str,
                              token_out: str,
                              amount: float) -> float:
        """Get best DEX quote via Purple Flea DEX aggregator."""
        r = await self.client.get(
            f"{TRADING_API}/v1/dex/quote",
            params={"tokenIn": token_in, "tokenOut": token_out,
                    "amountIn": amount}
        )
        return float(r.json()["amountOut"])

    async def check_arb_opportunity(self, symbol: str) -> Optional[dict]:
        cex_price  = await self.get_cex_price(symbol)
        dex_out    = await self.get_dex_quote(
            "USDC", symbol.split("/")[0],
            self.capital
        )
        dex_price = self.capital / dex_out
        spread_bps = (cex_price - dex_price) / cex_price * 10000
        if abs(spread_bps) > MIN_PROFIT_BPS:
            return {
                "symbol": symbol,
                "direction": "buy_dex" if dex_price < cex_price else "sell_dex",
                "spread_bps": spread_bps,
                "cex_price": cex_price,
                "dex_price": dex_price,
            }
        return None

    async def run(self, symbols: list[str]):
        print(f"[ARB] Starting with ${self.capital:.0f} capital")
        while True:
            for sym in symbols:
                opp = await self.check_arb_opportunity(sym)
                if opp:
                    print(f"[ARB] Opportunity: {opp}")
                    # Execute both legs (implement atomically via flash loan)
                    await self._execute_arb(opp)
            await asyncio.sleep(SCAN_INTERVAL)

    async def _execute_arb(self, opp: dict):
        # Buy cheap leg, sell expensive leg via Purple Flea
        print(f"[ARB] Executing {opp['direction']} {opp['symbol']}"
              f" | spread: {opp['spread_bps']:.1f}bps")
        self.trades_executed += 1

Start Building Your DEX Agent

Get free USDC from the Purple Flea faucet and start exploring DEX arbitrage and AMM liquidity provision with real capital.