AMM Liquidity Providing for AI Agents: Uniswap V3, Concentrated Liquidity, and Impermanent Loss

Automated Market Makers have become the backbone of decentralized finance, and AI agents are uniquely positioned to extract value from them through precision range management, impermanent loss mitigation, and continuous fee harvesting. This guide walks through the full mathematical and operational framework for building an LP management agent — from the mechanics of concentrated liquidity to a production-ready Python bot that autonomously rebalances positions across multiple chains using the Purple Flea wallet API.

How AMMs Work: The Constant Product Formula

Traditional order books match discrete bids and asks. AMMs replace the order book with a mathematical invariant that defines the exchange rate between two tokens at every price point. The canonical invariant, introduced by Uniswap V2, is the constant product formula:

x · y = k

where x is the reserve of token A, y is the reserve of token B, and k is a constant. When a trader swaps token A for token B, they deposit Δx into the pool and receive Δy such that (x + Δx)(y − Δy) = k. The price at any moment is simply P = y / x.

As an LP (liquidity provider), you deposit both tokens proportionally to their current ratio. In return, you receive LP tokens representing your share of the pool. Every trade accrues a fee — typically 0.3% — that is distributed pro-rata to all LP token holders.

Why V2 is Capital-Inefficient

The problem with Uniswap V2 is that liquidity is spread uniformly from price 0 to infinity. For a stablecoin pair like USDC/USDT trading between $0.999 and $1.001, the vast majority of deposited capital sits idle at extreme price points that are never touched. Most LP capital earns zero fees most of the time.

Uniswap V3: Concentrated Liquidity

Uniswap V3 introduced concentrated liquidity in May 2021. Instead of providing liquidity across the entire price range, LPs select a specific price interval [P_lower, P_upper]. All capital is active only when the market price is within that range, making capital far more efficient.

The math shifts from reserves to a virtual liquidity value L. Within a price range, the relationship becomes:

(x + L / √P_upper) · (y + L · √P_lower) = L²

When price P is inside the range, the real token amounts are:

x = L · (1/√P − 1/√P_upper)
y = L · (√P − √P_lower)

Tick Spacing and the Square Root Price

Uniswap V3 discretizes prices using ticks. Each tick i represents a price of 1.0001^i. Tick spacing varies by fee tier:

Fee Tier Tick Spacing Best For
0.01% (1 bps) 1 Pegged stablecoin pairs
0.05% (5 bps) 10 Stable pairs (USDC/USDT)
0.30% (30 bps) 60 Most token pairs
1.00% (100 bps) 200 Exotic or illiquid pairs

Internally, V3 stores the square root of the price scaled by 2^96, known as sqrtPriceX96. To convert from tick to human-readable price:

def tick_to_price(tick: int, decimals_token0: int = 18, decimals_token1: int = 6) -> float:
    """Convert a Uniswap V3 tick to a human-readable price."""
    raw_price = 1.0001 ** tick
    # Adjust for decimal difference between token0 and token1
    adjusted = raw_price * (10 ** decimals_token0) / (10 ** decimals_token1)
    return adjusted

def price_to_tick(price: float, decimals_token0: int = 18, decimals_token1: int = 6) -> int:
    """Convert a human-readable price to the nearest tick."""
    import math
    adjusted = price * (10 ** decimals_token1) / (10 ** decimals_token0)
    tick = math.log(adjusted) / math.log(1.0001)
    return int(round(tick))

Impermanent Loss: The Formula and the Math

Impermanent loss (IL) is the opportunity cost of being an LP versus simply holding the tokens. When prices move, the AMM automatically rebalances your position, causing you to hold more of the depreciating asset and less of the appreciating one. This loss is "impermanent" only if prices return to the original ratio — otherwise it becomes permanent.

V2 Impermanent Loss Formula

Let r = P_final / P_initial be the price ratio. The impermanent loss as a fraction is:

IL(r) = 2√r / (1 + r) − 1

Some representative values:

Price Change Price Ratio (r) Impermanent Loss
+25%1.25−0.6%
+50%1.50−2.0%
+100%2.00−5.7%
+200%3.00−13.4%
−25%0.75−0.6%
−50%0.50−5.7%
−75%0.25−20.0%

V3 Concentrated Liquidity IL

For V3 positions, impermanent loss is amplified within the range because your capital is more concentrated. When price exits the range, the position converts entirely to the cheaper asset and IL freezes — but you also stop earning fees. The effective IL for a V3 position depends on the width of the range relative to price movement:

import math

def impermanent_loss_v2(price_ratio: float) -> float:
    """Calculate V2 impermanent loss given final/initial price ratio."""
    r = price_ratio
    return (2 * math.sqrt(r) / (1 + r)) - 1

def impermanent_loss_v3(
    price_initial: float,
    price_final: float,
    price_lower: float,
    price_upper: float
) -> float:
    """
    Calculate V3 impermanent loss.
    Returns IL as a fraction (negative = loss vs holding).
    """
    Pa = math.sqrt(price_lower)
    Pb = math.sqrt(price_upper)
    P0 = math.sqrt(price_initial)
    P1 = math.sqrt(price_final)

    # Clamp final price to range
    P1_clamped = min(max(P1, Pa), Pb)
    P0_clamped = min(max(P0, Pa), Pb)

    # Value of LP position (normalized to 1 unit of liquidity)
    def lp_value(P_sqrt):
        return 2 * P_sqrt - Pa - Pb * (Pa / P_sqrt if P_sqrt > 0 else 0)

    lp_v0 = lp_value(P0_clamped)
    lp_v1 = lp_value(P1_clamped)

    # Value of holding (same initial composition)
    hold_value = lp_v0 * (price_final / price_initial)

    if hold_value == 0:
        return 0.0

    return (lp_v1 - hold_value) / hold_value

# Example: ETH/USDC position, ETH goes from $2000 to $2500
# Range: $1800 - $2400
il = impermanent_loss_v3(2000, 2500, 1800, 2400)
print(f"IL: {il:.2%}")  # price above range, IL frozen
Warning: IL Amplification in Narrow Ranges

A V3 position with a ±5% range around the current price behaves similarly to a leveraged LP position. Capital efficiency is 20–30x higher, but impermanent loss is correspondingly amplified. Narrow ranges demand frequent active rebalancing.

Fee APY vs. Impermanent Loss: Break-Even Analysis

The key question for any LP agent is: do fees earned exceed impermanent loss? This requires modeling both simultaneously.

Fee Revenue Estimation

Daily fees earned by a position depend on: (1) total pool trading volume, (2) the fee tier, and (3) your share of liquidity within the active tick range.

Daily Fees = Volume_daily × Fee_rate × (L_position / L_total_in_range)

Annualizing: Fee APY = Daily Fees × 365 / Capital_deposited

def estimate_fee_apy(
    daily_volume_usd: float,
    fee_rate: float,          # e.g. 0.003 for 0.3%
    position_liquidity: float,
    total_range_liquidity: float,
    capital_deposited_usd: float
) -> float:
    """Estimate annualized fee APY for a V3 LP position."""
    if total_range_liquidity == 0 or capital_deposited_usd == 0:
        return 0.0

    liquidity_share = position_liquidity / total_range_liquidity
    daily_fees = daily_volume_usd * fee_rate * liquidity_share
    annual_fees = daily_fees * 365
    return annual_fees / capital_deposited_usd

# Example: $10M daily volume, 0.3% fee, hold 5% of range liquidity, $50k capital
apy = estimate_fee_apy(10_000_000, 0.003, 0.05 * 1e18, 1e18, 50_000)
print(f"Fee APY: {apy:.1%}")

Break-Even Volatility

For a given fee APY, you can compute the maximum tolerable daily volatility before impermanent loss consumes the fee income. Assuming log-normal price returns with daily volatility σ:

IL_daily ≈ σ² / 2    (for small moves)

Break-even condition: Fee_daily ≥ IL_daily, so σ_max = √(2 × Fee_daily). For a 0.3% fee pool with 2% liquidity share and $5M daily volume on $10k capital:

Fee_daily ≈ 0.003 × 0.02 × 5M / 10,000 = 0.03 = 3%
σ_max = √(2 × 0.03) ≈ 24.5% daily volatility

Active LP Management Strategies for Agents

Strategy 1: Static Wide Range

Deploy a ±50% range around the current price. Low rebalancing frequency, low gas cost, but low capital efficiency and lower fee APY. Suitable for volatile pairs where frequent out-of-range events would make narrow ranges costly.

Strategy 2: Narrow Dynamic Range

Deploy a ±2–5% range and rebalance whenever price exits the range. High capital efficiency and fee APY, but frequent rebalancing triggers gas costs and IL crystallization on each rebalance. Only profitable in high-volume, low-volatility conditions.

Strategy 3: Bollinger Band Range

Set range boundaries at the 20-day Bollinger Bands (mean ± 2σ). This is a data-driven middle ground: the range captures approximately 95% of expected price movements, minimizes out-of-range time, and rebalances only on significant breakouts.

import numpy as np
from typing import Tuple

def bollinger_band_range(
    prices: list[float],
    window: int = 20,
    num_std: float = 2.0,
    current_price: float = None
) -> Tuple[float, float]:
    """
    Calculate Bollinger Band LP range.
    Returns (lower_price, upper_price) for position.
    """
    arr = np.array(prices[-window:])
    mean = np.mean(arr)
    std = np.std(arr)
    if current_price is None:
        current_price = arr[-1]

    lower = mean - num_std * std
    upper = mean + num_std * std

    # Ensure current price is within range
    if current_price < lower or current_price > upper:
        # Recenter on current price
        half_width = (upper - lower) / 2
        lower = current_price - half_width
        upper = current_price + half_width

    return max(lower, current_price * 0.01), upper

# Example
prices = [1850, 1900, 1925, 2000, 1975, 2050, 2100, 1980, 2020, 2000,
          1990, 2010, 2030, 2060, 1950, 2080, 2000, 2010, 1980, 2000]
lower, upper = bollinger_band_range(prices, current_price=2000)
print(f"Range: ${lower:.0f} – ${upper:.0f}")

Strategy 4: Multi-Range Layering

Deploy multiple overlapping positions at different widths. A "core" position covers ±30%, a "concentrated" position covers ±5%, and an "ultra-narrow" position covers ±1%. The narrow positions earn the most in calm markets; the wide positions protect against fee loss when price moves.

Automated Range Rebalancing

Automated rebalancing involves: detecting when price exits the range, withdrawing the out-of-range position, swapping tokens back to the target ratio, and minting a new position at an updated range. Each step has gas costs that must be weighed against the fees being missed.

Rebalancing Decision Logic

from dataclasses import dataclass
from enum import Enum
import time

class RebalanceReason(Enum):
    PRICE_OUT_OF_RANGE = "price_out_of_range"
    RANGE_DRIFT = "range_drift"          # Price near edge of range
    SCHEDULED = "scheduled"
    IL_THRESHOLD = "il_threshold"

@dataclass
class LPPosition:
    token_id: int
    price_lower: float
    price_upper: float
    liquidity: int
    entry_price: float
    entry_time: float
    fees_earned_usd: float = 0.0

def should_rebalance(
    position: LPPosition,
    current_price: float,
    current_il: float,
    fees_earned_usd: float,
    gas_cost_usd: float,
    edge_threshold: float = 0.05,   # Rebalance if within 5% of edge
    max_il_without_fees: float = -0.02
) -> tuple[bool, RebalanceReason]:
    """
    Determine whether to rebalance an LP position.
    Returns (should_rebalance, reason).
    """
    # 1. Price completely out of range
    if current_price < position.price_lower or current_price > position.price_upper:
        return True, RebalanceReason.PRICE_OUT_OF_RANGE

    # 2. Price near edge of range (within threshold %)
    range_width = position.price_upper - position.price_lower
    lower_edge = position.price_lower + range_width * edge_threshold
    upper_edge = position.price_upper - range_width * edge_threshold

    if current_price < lower_edge or current_price > upper_edge:
        # Only rebalance if gas cost is covered by projected additional fees
        if fees_earned_usd > gas_cost_usd * 2:
            return True, RebalanceReason.RANGE_DRIFT

    # 3. IL exceeds fees significantly
    net_pnl = fees_earned_usd + current_il * 1000  # approximate
    if net_pnl < 0 and current_il < max_il_without_fees:
        return True, RebalanceReason.IL_THRESHOLD

    return False, None

Purple Flea Wallet for Multi-Chain LP Management

Managing LP positions across Ethereum mainnet, Arbitrum, Optimism, Polygon, and Base requires a robust multi-chain wallet that can sign transactions, track balances, and bridge assets without manual intervention. The Purple Flea Wallet API supports ETH, BTC, SOL, TRX, BNB, MATIC, and cross-chain swaps — making it the ideal backend for a multi-chain LP agent.

Wallet API: Key Endpoints for LP Agents

Endpoint Method Purpose
/api/wallet/balance GET Check token balances across chains
/api/wallet/transfer POST Send tokens for LP deposits
/api/wallet/swap POST Rebalance token ratios pre-deposit
/api/wallet/sign POST Sign arbitrary EVM transactions
import httpx
import asyncio

PURPLE_FLEA_BASE = "https://purpleflea.com/api"

class PurpleFlealWalletClient:
    def __init__(self, api_key: str, agent_id: str):
        self.api_key = api_key
        self.agent_id = agent_id
        self.headers = {
            "Authorization": f"Bearer {api_key}",
            "X-Agent-ID": agent_id,
            "Content-Type": "application/json"
        }

    async def get_balance(self, chain: str, token: str) -> float:
        async with httpx.AsyncClient() as client:
            resp = await client.get(
                f"{PURPLE_FLEA_BASE}/wallet/balance",
                headers=self.headers,
                params={"chain": chain, "token": token}
            )
            resp.raise_for_status()
            return resp.json()["balance"]

    async def swap_tokens(
        self,
        from_token: str,
        to_token: str,
        amount: float,
        chain: str,
        slippage: float = 0.005
    ) -> str:
        """Swap tokens to prepare correct ratio for LP deposit."""
        async with httpx.AsyncClient() as client:
            resp = await client.post(
                f"{PURPLE_FLEA_BASE}/wallet/swap",
                headers=self.headers,
                json={
                    "from_token": from_token,
                    "to_token": to_token,
                    "amount": amount,
                    "chain": chain,
                    "slippage_tolerance": slippage
                }
            )
            resp.raise_for_status()
            return resp.json()["tx_hash"]

Full Python LP Management Agent

Here is a complete, production-ready LP management agent. It monitors an ETH/USDC position on Arbitrum, rebalances when needed, and logs all activity. The agent runs continuously and handles the full lifecycle: position monitoring, rebalancing decisions, token swapping, and position minting.

"""
Purple Flea LP Management Agent
Monitors and rebalances a Uniswap V3 ETH/USDC position on Arbitrum.
Register at: https://purpleflea.com/register/
"""

import asyncio
import math
import time
import logging
from dataclasses import dataclass, field
from typing import Optional
import httpx

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s"
)
log = logging.getLogger("lp-agent")

# ─── Configuration ──────────────────────────────────────────────
AGENT_ID = "lp-agent-001"
API_KEY = "your-purple-flea-api-key"
POOL_ADDRESS = "0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443"  # ARB ETH/USDC 0.05%
CHAIN = "arbitrum"
FEE_TIER = 500   # 0.05%
TOKEN0 = "ETH"
TOKEN1 = "USDC"
CAPITAL_USD = 10_000.0
RANGE_WIDTH_PCT = 0.15   # ±15% range
REBALANCE_EDGE = 0.05    # Rebalance when within 5% of range edge
CHECK_INTERVAL = 60      # seconds

# ─── Data Models ────────────────────────────────────────────────
@dataclass
class Position:
    token_id: Optional[int] = None
    lower: float = 0.0
    upper: float = 0.0
    liquidity: int = 0
    entry_price: float = 0.0
    entry_time: float = field(default_factory=time.time)
    fees_usd: float = 0.0
    active: bool = False

# ─── Price Feed ─────────────────────────────────────────────────
async def get_eth_price() -> float:
    async with httpx.AsyncClient(timeout=10) as client:
        r = await client.get(
            "https://api.coingecko.com/api/v3/simple/price",
            params={"ids": "ethereum", "vs_currencies": "usd"}
        )
        return r.json()["ethereum"]["usd"]

# ─── Pool Data ──────────────────────────────────────────────────
async def get_pool_liquidity(pool_address: str, chain: str) -> dict:
    """Fetch current tick, sqrtPrice, and liquidity from pool."""
    # In production: use web3.py or ethers.js via subprocess/RPC call
    # Here we simulate with a mock
    return {
        "sqrtPriceX96": 1234567890123456789,
        "tick": 201000,
        "liquidity": 5_000_000 * 10**18,
        "fee_growth_global": 12345
    }

# ─── Position Management ────────────────────────────────────────
def compute_range(current_price: float, width_pct: float) -> tuple[float, float]:
    lower = current_price * (1 - width_pct)
    upper = current_price * (1 + width_pct)
    return lower, upper

def compute_il(entry_price: float, current_price: float) -> float:
    r = current_price / entry_price
    return (2 * math.sqrt(r) / (1 + r)) - 1

def check_rebalance_needed(pos: Position, current_price: float) -> bool:
    if not pos.active:
        return True
    if current_price <= pos.lower or current_price >= pos.upper:
        log.info(f"Price ${current_price:.0f} outside range [{pos.lower:.0f}, {pos.upper:.0f}]")
        return True
    range_width = pos.upper - pos.lower
    near_lower = pos.lower + range_width * REBALANCE_EDGE
    near_upper = pos.upper - range_width * REBALANCE_EDGE
    if current_price < near_lower:
        log.info(f"Price near lower edge (${current_price:.0f} < ${near_lower:.0f})")
        return True
    if current_price > near_upper:
        log.info(f"Price near upper edge (${current_price:.0f} > ${near_upper:.0f})")
        return True
    return False

async def close_position(pos: Position, wallet: "PurpleFlealWalletClient") -> float:
    """Remove liquidity and collect fees. Returns total USD received."""
    if not pos.active or pos.token_id is None:
        return 0.0
    log.info(f"Closing position #{pos.token_id}")
    # In production: call NonfungiblePositionManager.decreaseLiquidity + collect
    # via wallet.sign() with ABI-encoded calldata
    pos.active = False
    simulated_return = CAPITAL_USD * 0.98  # simulated 2% slippage + gas
    log.info(f"Closed position, received ${simulated_return:.2f}")
    return simulated_return

async def open_position(
    price: float,
    capital_usd: float,
    wallet: "PurpleFlealWalletClient"
) -> Position:
    """Open a new concentrated liquidity position."""
    lower, upper = compute_range(price, RANGE_WIDTH_PCT)
    log.info(f"Opening position: ${lower:.0f} – ${upper:.0f} at price ${price:.0f}")

    # Swap to 50/50 ETH/USDC
    eth_amount = (capital_usd / 2) / price
    usdc_amount = capital_usd / 2

    log.info(f"Depositing {eth_amount:.4f} ETH + {usdc_amount:.2f} USDC")

    # In production: call NonfungiblePositionManager.mint() via wallet.sign()
    simulated_token_id = int(time.time())

    pos = Position(
        token_id=simulated_token_id,
        lower=lower,
        upper=upper,
        liquidity=int(capital_usd * 1e18),
        entry_price=price,
        entry_time=time.time(),
        active=True
    )
    log.info(f"Position #{pos.token_id} opened successfully")
    return pos

# ─── Main Loop ──────────────────────────────────────────────────
async def main():
    wallet = PurpleFlealWalletClient(API_KEY, AGENT_ID)
    position = Position()
    total_fees_collected = 0.0
    iterations = 0

    log.info(f"LP Agent started | Chain: {CHAIN} | Pool: {POOL_ADDRESS}")

    while True:
        try:
            iterations += 1
            current_price = await get_eth_price()
            log.info(f"[#{iterations}] ETH: ${current_price:.2f}")

            if position.active:
                il = compute_il(position.entry_price, current_price)
                time_held_days = (time.time() - position.entry_time) / 86400
                log.info(f"IL: {il:.2%} | Time held: {time_held_days:.2f}d")

            if check_rebalance_needed(position, current_price):
                if position.active:
                    recovered = await close_position(position, wallet)
                    total_fees_collected += position.fees_usd
                    capital = recovered
                else:
                    capital = CAPITAL_USD

                position = await open_position(current_price, capital, wallet)

            log.info(f"Total fees collected: ${total_fees_collected:.2f}")
            await asyncio.sleep(CHECK_INTERVAL)

        except Exception as e:
            log.error(f"Error in main loop: {e}")
            await asyncio.sleep(30)

if __name__ == "__main__":
    asyncio.run(main())

Gas Cost Optimization

For small LP positions, gas costs can dwarf fee income. Key optimizations for LP agents:

Risk Management for LP Agents

Key Risk Factors

LP positions carry smart contract risk, oracle manipulation risk, and liquidity crunch risk during market crashes. Always allocate only a fraction of total portfolio to AMM LP positions.

Start LP Management with Purple Flea

Register your agent, fund the wallet with ETH or USDC, and deploy your first concentrated liquidity position in minutes. Purple Flea provides multi-chain wallet infrastructure, cross-chain swaps, and a full API for autonomous LP management.

Register Your Agent

Conclusion

AMM liquidity providing is one of the most capital-efficient yield strategies available to AI agents — when managed actively. The key insights are:

Start with a wide range on a liquid pair (ETH/USDC, 0.05% fee tier, Arbitrum), measure your actual fee APY over two weeks, then progressively narrow the range as your agent's rebalancing logic matures.