How Flash Loans Work

A flash loan is not a loan in the traditional sense. It is an atomic multi-step transaction where borrow and repay happen in the same block:

1
Request
Agent requests $1M USDC from flash loan pool
2
Execute
Agent runs arbitrage / self-liquidation / swap
3
Repay
Agent repays $1M + 0.09% fee in same tx
4
Profit
Agent keeps arb profit minus fee

If step 3 fails (insufficient repayment funds), the EVM reverts the entire transaction to step 0. The agent loses only the gas fee for the failed transaction — typically a few dollars. No capital is at risk beyond gas.

Key insight: Flash loans invert the usual capital requirement for arbitrage. Normally you need to hold the capital to exploit a price difference. With flash loans, you borrow it atomically, exploit the difference, repay, and pocket the spread. Capital requirement drops to zero.

Use Case 1: DEX Price Arbitrage

The classic flash loan use case. If BTC/USDC trades at $95,000 on DEX-A and $95,400 on DEX-B, an agent can:

  1. Flash-borrow $950,000 USDC
  2. Buy 10 BTC on DEX-A at $95,000
  3. Sell 10 BTC on DEX-B at $95,400 (+$4,000 gross)
  4. Repay flash loan + 0.09% fee ($855)
  5. Net profit: ~$3,145 in one transaction
flash_arb.py
import os, httpx, logging
from dataclasses import dataclass

log = logging.getLogger("flash-arb")
PF_KEY  = os.getenv("PURPLE_FLEA_KEY")
PF_BASE = "https://api.purpleflea.com/v1"
HEADERS = {"Authorization": f"Bearer {PF_KEY}", "Content-Type": "application/json"}

FLASH_LOAN_FEE = 0.0009  # 0.09% on Purple Flea flash loans
MIN_PROFIT_USD = 50       # don't execute if net profit < $50

@dataclass
class ArbOpportunity:
    asset: str
    amount: float           # units of asset to trade
    buy_venue: str
    buy_price: float
    sell_venue: str
    sell_price: float

    @property
    def gross_profit(self) -> float:
        return (self.sell_price - self.buy_price) * self.amount

    @property
    def loan_amount_usd(self) -> float:
        return self.buy_price * self.amount

    @property
    def flash_fee_usd(self) -> float:
        return self.loan_amount_usd * FLASH_LOAN_FEE

    @property
    def net_profit(self) -> float:
        return self.gross_profit - self.flash_fee_usd

def find_arb_opportunities(asset: str = "BTC") -> list[ArbOpportunity]:
    "Scan multiple DEX venues for price discrepancies."
    with httpx.Client() as http:
        prices = http.get(
            f"{PF_BASE}/flash-loans/prices",
            params={"asset": asset},
            headers=HEADERS
        ).json()["venues"]

    opportunities = []
    for i, buy_venue in enumerate(prices):
        for sell_venue in prices[i+1:]:
            if sell_venue["ask"] > buy_venue["bid"]:
                # Trade 1 unit to start; profitability calculator will size properly
                opp = ArbOpportunity(
                    asset=asset,
                    amount=1.0,
                    buy_venue=buy_venue["name"], buy_price=buy_venue["ask"],
                    sell_venue=sell_venue["name"], sell_price=sell_venue["bid"],
                )
                if opp.net_profit > MIN_PROFIT_USD:
                    opportunities.append(opp)
    return sorted(opportunities, key=lambda o: o.net_profit, reverse=True)

def execute_flash_arb(opp: ArbOpportunity) -> dict:
    "Submit a flash loan arbitrage transaction to Purple Flea."
    log.info(
        f"Executing arb: buy {opp.asset} on {opp.buy_venue} at ${opp.buy_price:,.2f}, "
        f"sell on {opp.sell_venue} at ${opp.sell_price:,.2f} | "
        f"net profit est. ${opp.net_profit:,.2f}"
    )
    payload = {
        "type": "arbitrage",
        "asset": opp.asset,
        "loan_amount_usd": opp.loan_amount_usd,
        "steps": [
            {"action": "buy",  "venue": opp.buy_venue,  "amount": opp.amount},
            {"action": "sell", "venue": opp.sell_venue, "amount": opp.amount},
        ],
        "max_slippage_bps": 10,   # abort if slippage > 0.10%
        "repay_asset": "USDC",
    }
    with httpx.Client(timeout=30) as http:
        r = http.post(f"{PF_BASE}/flash-loans/execute", json=payload, headers=HEADERS)
    r.raise_for_status()
    result = r.json()
    log.info(f"Arb completed: tx={result['tx_hash']} profit=${result['net_profit_usd']:.2f}")
    return result

Profitability Calculator

Before executing, always run the profitability check. A trade looks profitable on paper but can fail after slippage and fees:

Example: BTC Arb on 10 units

Flash loan amount $950,000 USDC
Gross price spread +$400 per BTC × 10 = $4,000
Flash loan fee (0.09%) -$855
Estimated slippage (0.05% each side) -$950
Gas fee -$4
Net profit $2,191
profitability_check.py
def check_profitability(
    loan_usd: float,
    gross_spread_usd: float,
    slippage_bps: float = 10,    # 0.10% each side
    gas_usd: float = 5,
    flash_fee_bps: float = 9,     # 0.09%
) -> dict:
    flash_fee   = loan_usd * (flash_fee_bps / 10_000)
    slippage    = loan_usd * (slippage_bps / 10_000) * 2  # both legs
    total_costs = flash_fee + slippage + gas_usd
    net_profit  = gross_spread_usd - total_costs
    return {
        "gross_spread_usd": gross_spread_usd,
        "flash_fee_usd": flash_fee,
        "slippage_usd": slippage,
        "gas_usd": gas_usd,
        "net_profit_usd": net_profit,
        "is_profitable": net_profit > 0,
        "minimum_spread_for_profit_usd": total_costs,
        "minimum_spread_pct": total_costs / loan_usd * 100,
    }

# Example: need spread > 0.19% to profit on a $950k loan with 0.10% slippage
result = check_profitability(950_000, 4_000)
print(f"Net profit: ${result['net_profit_usd']:,.2f} | Profitable: {result['is_profitable']}")

Use Case 2: Self-Liquidation to Avoid Penalties

When a borrowing position approaches the liquidation threshold, protocol liquidators step in and charge a liquidation penalty (typically 5–15%). An agent can use a flash loan to repay its own debt before the liquidator does, reclaiming its collateral without paying the penalty.

Example: Agent has $100k ETH as collateral, $70k USDC debt. At a liquidation threshold of 80%, the agent is approaching danger. A third-party liquidator would charge an 8% penalty ($5,600). Instead, the agent flash-borrows $70k USDC, repays its own debt, withdraws its ETH collateral, sells enough ETH to repay the flash loan, and keeps the rest. Net saving: the $5,600 liquidation penalty.

self_liquidation.py
def self_liquidate(position_id: str) -> dict:
    "Use a flash loan to repay own debt and avoid liquidation penalty."
    with httpx.Client() as http:
        pos = http.get(
            f"{PF_BASE}/lending/positions/{position_id}", headers=HEADERS
        ).json()

    debt_usd       = pos["debt_usd"]
    collateral_usd = pos["collateral_usd"]
    health_factor  = pos["health_factor"]

    if health_factor > 1.10:
        raise ValueError(f"Position health {health_factor:.2f} — not at risk, self-liquidation unnecessary")

    log.info(
        f"Self-liquidating position {position_id}: "
        f"debt=${debt_usd:,.0f} collateral=${collateral_usd:,.0f} "
        f"health={health_factor:.2f}"
    )
    payload = {
        "type": "self_liquidation",
        "position_id": position_id,
        "loan_asset": pos["debt_asset"],
        "loan_amount": debt_usd * 1.001,  # borrow slightly more for accrued interest
        "steps": [
            {"action": "repay_debt", "position_id": position_id},
            {"action": "withdraw_collateral", "position_id": position_id},
            {"action": "sell_collateral_to_repay"},  # auto-calculates amount needed
        ],
    }
    with httpx.Client(timeout=30) as http:
        r = http.post(f"{PF_BASE}/flash-loans/execute", json=payload, headers=HEADERS)
    r.raise_for_status()
    result = r.json()
    log.info(f"Self-liquidation complete. Saved: ${result['liquidation_penalty_avoided_usd']:,.2f}")
    return result

Use Case 3: Collateral Swap

An agent holding ETH as collateral in a lending position can swap it to WBTC as collateral in a single atomic transaction — without repaying the loan first. This is useful when the agent’s risk model determines ETH is overexposed and BTC is the preferred collateral, but the position is too large to unwind and re-enter conventionally.

collateral_swap.py
def swap_collateral(
    position_id: str,
    from_asset: str,    # e.g. "ETH"
    to_asset: str,      # e.g. "WBTC"
) -> dict:
    """
    Atomically swap collateral type on a lending position:
    1. Flash-borrow enough to_asset to repay the debt
    2. Withdraw from_asset collateral
    3. Swap from_asset → to_asset
    4. Re-deposit to_asset as new collateral
    5. Reborrow original debt amount
    6. Repay flash loan
    """
    payload = {
        "type": "collateral_swap",
        "position_id": position_id,
        "from_collateral_asset": from_asset,
        "to_collateral_asset": to_asset,
        "max_slippage_bps": 15,
    }
    with httpx.Client(timeout=30) as http:
        r = http.post(f"{PF_BASE}/flash-loans/execute", json=payload, headers=HEADERS)
    r.raise_for_status()
    result = r.json()
    log.info(
        f"Collateral swapped: {from_asset} → {to_asset} | "
        f"new LTV: {result['new_ltv']:.1%}"
    )
    return result

Continuous Arbitrage Scanner

Combine the pieces into a production scanner that runs continuously and executes when profitable opportunities are found:

arb_scanner.py
import time, logging

log = logging.getLogger("arb-scanner")
SCAN_ASSETS   = ["BTC", "ETH", "SOL", "USDC"]
SCAN_INTERVAL = 30   # seconds — arb opportunities close fast

def run_scanner():
    log.info("Flash loan arb scanner starting...")
    total_profit = 0.0
    executions   = 0

    while True:
        for asset in SCAN_ASSETS:
            try:
                opps = find_arb_opportunities(asset)
                if not opps:
                    continue
                best = opps[0]
                log.info(f"Opportunity: {asset} spread=${best.net_profit:.2f}")
                # Verify profitability with slippage estimate before executing
                check = check_profitability(best.loan_amount_usd, best.gross_profit)
                if check["is_profitable"] and check["net_profit_usd"] >= MIN_PROFIT_USD:
                    result = execute_flash_arb(best)
                    total_profit += result["net_profit_usd"]
                    executions   += 1
                    log.info(f"Running total: ${total_profit:,.2f} over {executions} trades")
            except Exception as e:
                log.error(f"Error scanning {asset}: {e}")
        time.sleep(SCAN_INTERVAL)

if __name__ == "__main__":
    run_scanner()

Risk Profile: Why Flash Loans are Unique

ScenarioFlash loan outcomeConventional loan outcome
Arb profitableProfit after feesProfit after fees + interest
Arb fails (price moved)Transaction reverts. Lose only gas (~$5)Lose capital on bad trade
Slippage exceeds spreadTransaction reverts. Lose only gasLose capital
Smart contract bugDepends on bug locationCapital at risk

The one real risk: If the flash loan contract itself has a vulnerability, funds in the pool could be at risk. This is why you should only use flash loans from audited protocols. Purple Flea’s flash loan contracts are audited by Cantina and reports are available at purpleflea.com/security.

Conclusion

Flash loans are the most powerful capital-efficiency tool available to AI agents. They turn any price discrepancy, however briefly it exists, into a riskable opportunity — with the unique property that failed attempts cost only gas. For agents with limited capital, they level the playing field against well-capitalized arbitrageurs by eliminating the working capital requirement entirely.

Start with the arbitrage scanner on a single asset pair, verify the profitability calculator is correctly accounting for slippage, and monitor gas costs. The self-liquidation and collateral swap use cases are higher-value but require active lending positions to use. All three strategies are available via Purple Flea’s flash loan API.

Access Purple Flea Flash Loans

Zero-collateral loans with 0.09% fee, atomic execution, and full Python SDK support.