← All posts

Advanced Market Making Strategies for AI Agents


Market making is one of the most sophisticated and profitable activities available to AI agents on financial platforms. Unlike directional trading — where you bet prices go up or down — market making earns income from the bid-ask spread by simultaneously quoting prices on both sides of the order book. Done well, it generates consistent fee income while remaining largely market-neutral. Done poorly, it bleeds inventory against adverse price moves.

This guide covers the full stack: the mathematics behind optimal quote placement, a production-grade Python implementation using the Avellaneda-Stoikov model, multi-level liquidity provision, fee optimization, and hard risk limits. By the end, you will have a working MarketMakerAgent class ready to plug into the Purple Flea Trading API.

0.01%
Typical maker rebate per fill
Fills per day for a busy MM bot
A-S optimal spread half-width
5%
Max inventory limit (safe default)

1. Market Making Mechanics

The Bid-Ask Spread

A market maker continuously posts a bid (the price they will buy at) and an ask (the price they will sell at). The difference between the two is the spread. Every time a taker crosses your quote, you earn half the spread. Simple example:

Basic spread income
Mid price = 100.00 USDC
Bid = 99.95 USDC (-0.05%)
Ask = 100.05 USDC (+0.05%)
Spread = 0.10 USDC = 10 bps
Income per round-trip = 0.10 USDC (buy low, sell high)

The goal is to quote tight enough to attract order flow (volume) while wide enough to cover adverse selection and earn a profit margin.

Inventory Management

Inventory risk is the central problem of market making. When a sell order hits your bid, you acquire inventory (long position). If price then falls, that inventory loses value faster than the spread income can compensate. Conversely, selling inventory via your ask when prices are rising costs you opportunity.

Three inventory management approaches:

  • Fixed quotes: Always quote symmetrically around mid. Simple, but accumulates directional risk during trending markets.
  • Quote skewing: Adjust bid and ask asymmetrically to incentivize the offsetting side of inventory. Long too much? Skew ask lower to attract sellers.
  • Reservation price (Avellaneda-Stoikov): Derive a mathematically optimal mid price that accounts for current inventory, risk aversion, and remaining trading horizon.

Adverse Selection Risk

Adverse selection occurs when the counterparty has more information than you. An informed trader buys your ask just before a price surge, leaving you short at a bad level. Signals of adverse selection in your fills:

  • One-sided fill pressure (e.g., only your asks getting hit)
  • Price moving against you immediately after each fill
  • Fill rate spikes coinciding with news or volume events
  • PnL negative despite tight spreads
Adverse Selection Warning

If 70%+ of your fills are moving against you within 5 seconds of execution, widen your spread or pause quoting. You are likely being picked off by informed order flow.

2. The Avellaneda-Stoikov Model

Published in 2008 by Marco Avellaneda and Sasha Stoikov, this stochastic optimal control model remains the gold standard for market making in continuous-time settings. It solves for bid and ask prices that maximize expected utility over a finite trading horizon, balancing spread income against inventory risk.

Key Assumptions

  • Mid price follows a Brownian motion: dS = σ dW
  • Arrival of buy and sell orders follows independent Poisson processes
  • Order arrival intensity decreases exponentially as quotes move away from mid
  • Agent has CARA (constant absolute risk aversion) utility function

Reservation Price

The reservation price r is the agent's subjective mid price, adjusted for inventory exposure:

Reservation price (A-S model)
r = s - q * γ * σ² * (T - t)

Where:
s = current mid price
q = current inventory (signed, + = long, - = short)
γ = risk aversion parameter (typically 0.001 to 0.1)
σ = price volatility (std dev per unit time)
T - t = remaining time in trading session

When you are long (q > 0), the reservation price falls below mid — making your ask more competitive to offload inventory. When short, it rises above mid, making your bid more competitive to cover.

Optimal Spread Formula

The optimal half-spread δ determines how far each quote sits from the reservation price:

Optimal half-spread
δ = (γ * σ² * (T - t)) / 2 + (1/γ) * ln(1 + γ/k)

Where:
k = order arrival rate decay parameter
γ = risk aversion
σ = volatility
T - t = time remaining

Full spread = 2δ = γσ²(T-t) + (2/γ)ln(1 + γ/k)
Simplified Rule of Thumb

When the math is too complex for real-time use: optimal spread ≈ 2σ√(γ). For σ = 0.1% per second and γ = 0.01, that gives ~2 bps per side. Always add a floor equal to your taker fee to ensure you never quote yourself to a loss.

Quote Placement

Final bid and ask
bid = r - δ
ask = r + δ

Equivalently:
bid = s - q*γ*σ²*(T-t) - δ
ask = s - q*γ*σ²*(T-t) + δ

3. Inventory Risk Management

Quote Skewing

Quote skewing translates inventory imbalance into asymmetric pricing pressure. Instead of quoting symmetrically around mid, you lean prices to naturally attract trades that reduce your position:

Skewed quotes (linear model)
inventory_ratio = q / max_inventory (-1.0 to +1.0)
skew = inventory_ratio * base_spread * skew_factor

bid = mid - half_spread + skew
ask = mid + half_spread + skew

When long (inventory_ratio > 0): skew > 0 raises both quotes
→ ask moves closer to mid (more competitive sell)
→ bid moves away from mid (less competitive buy)

Inventory Targets and Rebalancing

Set a target inventory (typically zero for a neutral market maker) and a tolerance band. Outside the band, take more aggressive action:

  • Band 0-33%: Normal quote skewing, no extra action
  • Band 33-66%: Widen the side adding to inventory, tighten the reducing side
  • Band 66-100%: Cancel quotes on the adding side entirely, only show reducing quotes
  • Beyond 100% (limit breach): Emergency market order to reduce, then circuit-break quoting for N seconds

Volatility-Adjusted Spread

Spreads should widen during volatile periods — not only does adverse selection risk increase, but the probability of a large inventory loss rises. A simple volatility scaler:

Volatility-adjusted spread
vol_30s = rolling_std(prices, window=30s)
vol_baseline = historical_median_vol
vol_ratio = vol_30s / vol_baseline
adjusted_spread = base_spread * max(1.0, vol_ratio ** 0.5)

4. Python Implementation

The following MarketMakerAgent class implements the full A-S model with inventory skewing, volatility adjustment, and circuit breakers. It connects to the Purple Flea Trading API for live quote placement.

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

# Purple Flea Trading API base URL
PF_API = "https://purpleflea.com/api/trading"

@dataclass
class MarketMakerConfig:
    api_key: str                        # pf_live_XXXXX format
    symbol: str = "BTC-USDC"
    base_spread_bps: float = 8.0       # 8 basis points (0.08%)
    risk_aversion: float = 0.01        # gamma (γ) parameter
    max_inventory: float = 0.5         # max BTC position
    skew_factor: float = 0.5          # aggressiveness of skew
    order_size: float = 0.01          # BTC per quote level
    vol_window_seconds: int = 60       # rolling vol window
    session_hours: float = 8.0        # trading session length
    circuit_break_loss: float = 50.0   # USD loss to trigger break
    max_loss_session: float = 200.0   # max session loss (USD)


@dataclass
class Quote:
    bid: float
    ask: float
    bid_size: float
    ask_size: float
    spread_bps: float
    reservation_price: float


class MarketMakerAgent:
    """
    Avellaneda-Stoikov market making agent for Purple Flea.
    Manages optimal quote placement, inventory skewing,
    volatility adjustment, and risk circuit breakers.
    """

    def __init__(self, config: MarketMakerConfig):
        self.cfg = config
        self.inventory: float = 0.0      # current signed position
        self.cash_pnl: float = 0.0       # realized P&L in USDC
        self.session_start = time.time()
        self.active_orders: dict = {}
        self.price_history: deque = deque(maxlen=500)
        self.fill_history: list = []
        self.circuit_broken: bool = False
        self.circuit_break_until: float = 0.0
        self.client = httpx.AsyncClient(
            base_url=PF_API,
            headers={"Authorization": f"Bearer {config.api_key}"},
            timeout=5.0
        )

    # ─────────────────────────────────────────────────
    # Core A-S model calculations
    # ─────────────────────────────────────────────────

    def rolling_volatility(self) -> float:
        """Estimate short-term price volatility from recent ticks."""
        if len(self.price_history) < 10:
            return 0.001  # default 0.1% baseline
        prices = list(self.price_history)
        returns = [
            (prices[i] - prices[i-1]) / prices[i-1]
            for i in range(1, len(prices))
        ]
        mean_r = sum(returns) / len(returns)
        variance = sum((r - mean_r) ** 2 for r in returns) / len(returns)
        return math.sqrt(variance)

    def reservation_price(self, mid: float) -> float:
        """
        A-S reservation price:
          r = s - q * γ * σ² * (T - t)
        """
        sigma = self.rolling_volatility()
        elapsed = time.time() - self.session_start
        remaining = max(0.001, self.cfg.session_hours * 3600 - elapsed)
        adjustment = (
            self.inventory
            * self.cfg.risk_aversion
            * (sigma ** 2)
            * remaining
        )
        return mid - adjustment

    def optimal_spread(self, sigma: float, k: float = 1.5) -> float:
        """
        A-S optimal half-spread:
          δ = (γσ²(T-t))/2 + (1/γ)ln(1 + γ/k)
        Returns full spread in price units (not bps).
        """
        elapsed = time.time() - self.session_start
        remaining = max(0.001, self.cfg.session_hours * 3600 - elapsed)
        gamma = self.cfg.risk_aversion

        inventory_term = (gamma * sigma**2 * remaining) / 2
        arrival_term = (1 / gamma) * math.log(1 + gamma / k)
        half_spread = inventory_term + arrival_term

        # Never quote tighter than base_spread_bps minimum
        mid_proxy = self.price_history[-1] if self.price_history else 100.0
        min_half = mid_proxy * (self.cfg.base_spread_bps / 10000) / 2
        return max(half_spread, min_half)

    def inventory_skew(self, mid: float) -> float:
        """
        Compute skew offset based on inventory ratio.
        Returns signed offset applied to both bid and ask equally.
        """
        if self.cfg.max_inventory == 0:
            return 0.0
        ratio = self.inventory / self.cfg.max_inventory  # -1 to +1
        half_spread = mid * (self.cfg.base_spread_bps / 10000) / 2
        return ratio * half_spread * self.cfg.skew_factor

    def update_quotes(self, mid: float) -> Optional[Quote]:
        """
        Compute optimal bid/ask given current mid price.
        Returns None if circuit breaker is active.
        """
        if self._circuit_breaker_active():
            return None

        self.price_history.append(mid)
        sigma = self.rolling_volatility()
        r = self.reservation_price(mid)
        half_spread = self.optimal_spread(sigma)
        skew = self.inventory_skew(mid)

        # Vol scaler: widen during high volatility
        baseline_vol = 0.001
        vol_ratio = sigma / baseline_vol
        vol_scaler = max(1.0, vol_ratio ** 0.5)
        half_spread *= vol_scaler

        bid = r - half_spread + skew
        ask = r + half_spread + skew
        spread_bps = ((ask - bid) / mid) * 10000

        return Quote(
            bid=round(bid, 4),
            ask=round(ask, 4),
            bid_size=self._compute_size("bid"),
            ask_size=self._compute_size("ask"),
            spread_bps=round(spread_bps, 2),
            reservation_price=round(r, 4)
        )

    def manage_inventory(self) -> str:
        """
        Assess current inventory state and return recommended action.
        Returns one of: 'normal', 'skew_sell', 'skew_buy',
        'cancel_bids', 'cancel_asks', 'emergency_reduce'.
        """
        if self.cfg.max_inventory == 0:
            return "normal"

        ratio = abs(self.inventory) / self.cfg.max_inventory

        if ratio < 0.33:
            return "normal"
        elif ratio < 0.66:
            return "skew_sell" if self.inventory > 0 else "skew_buy"
        elif ratio < 1.0:
            return "cancel_bids" if self.inventory > 0 else "cancel_asks"
        else:
            return "emergency_reduce"

    # ─────────────────────────────────────────────────
    # Purple Flea API integration
    # ─────────────────────────────────────────────────

    async def place_quotes(self, quote: Quote) -> dict:
        """Submit bid and ask limit orders to the trading API."""
        results = {}
        for side, price, size in [
            ("buy", quote.bid, quote.bid_size),
            ("sell", quote.ask, quote.ask_size)
        ]:
            resp = await self.client.post("/orders", json={
                "symbol": self.cfg.symbol,
                "side": side,
                "type": "limit",
                "price": price,
                "size": size,
                "post_only": True   # ensures maker rebate
            })
            data = resp.json()
            if data.get("order_id"):
                self.active_orders[data["order_id"]] = {
                    "side": side, "price": price, "size": size
                }
            results[side] = data
        return results

    async def cancel_all_quotes(self):
        """Cancel all active maker orders."""
        for oid in list(self.active_orders.keys()):
            try:
                await self.client.delete(f"/orders/{oid}")
            except Exception:
                pass
        self.active_orders.clear()

    def record_fill(self, side: str, price: float, size: float):
        """Update inventory and PnL on a fill event."""
        if side == "buy":
            self.inventory += size
            self.cash_pnl -= price * size
        else:
            self.inventory -= size
            self.cash_pnl += price * size
        self.fill_history.append({
            "time": time.time(), "side": side,
            "price": price, "size": size
        })
        self._check_risk_limits()

    def _compute_size(self, side: str) -> float:
        """Reduce size on the side that adds to inventory."""
        state = self.manage_inventory()
        if side == "bid" and state in ("cancel_bids", "emergency_reduce"):
            return 0.0
        if side == "ask" and state in ("cancel_asks", "emergency_reduce"):
            return 0.0
        return self.cfg.order_size

    def _circuit_breaker_active(self) -> bool:
        if self.circuit_broken and time.time() < self.circuit_break_until:
            return True
        self.circuit_broken = False
        return False

    def _check_risk_limits(self):
        """Trigger circuit breaker if loss limits are exceeded."""
        recent_loss = self._recent_loss_usd()
        if recent_loss > self.cfg.circuit_break_loss:
            self.circuit_broken = True
            self.circuit_break_until = time.time() + 60  # 60s pause
            print(f"[CIRCUIT BREAK] Loss ${recent_loss:.2f} — pausing 60s")
        if self.cash_pnl < -self.cfg.max_loss_session:
            self.circuit_broken = True
            self.circuit_break_until = time.time() + 86400  # 24h halt
            print(f"[SESSION HALT] Max daily loss reached")

    def _recent_loss_usd(self, window_seconds: int = 300) -> float:
        """Compute realized loss over last N seconds of fills."""
        cutoff = time.time() - window_seconds
        recent = [f for f in self.fill_history if f["time"] > cutoff]
        pnl = 0.0
        for f in recent:
            if f["side"] == "buy":
                pnl -= f["price"] * f["size"]
            else:
                pnl += f["price"] * f["size"]
        return max(0.0, -pnl)


# ────────────────── Main event loop ──────────────────
async def main():
    config = MarketMakerConfig(
        api_key="pf_live_YOUR_KEY_HERE",
        symbol="BTC-USDC",
        base_spread_bps=8.0,
        risk_aversion=0.01,
        max_inventory=0.5
    )
    agent = MarketMakerAgent(config)
    client = httpx.AsyncClient(base_url=PF_API)

    while True:
        try:
            # 1. Fetch current mid price
            r = await client.get(f"/ticker/{config.symbol}")
            mid = r.json()["mid"]

            # 2. Compute new quotes
            quote = agent.update_quotes(mid)
            if quote is None:
                print("Circuit breaker active — skipping")
                await asyncio.sleep(5)
                continue

            # 3. Cancel stale quotes, place fresh ones
            await agent.cancel_all_quotes()
            result = await agent.place_quotes(quote)
            print(f"Quoted bid={quote.bid} ask={quote.ask} "
                  f"spread={quote.spread_bps:.1f}bps inv={agent.inventory:.4f}")

            # 4. Wait before next refresh
            await asyncio.sleep(2)

        except Exception as e:
            print(f"Error: {e}")
            await asyncio.sleep(5)


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

5. Multi-Level Order Book Strategy

A single bid/ask pair is simple to implement but leaves volume on the table. Professional market makers run layered liquidity: multiple price levels with progressively larger sizes at wider spreads. This serves two purposes: (1) it captures more of the order flow across different urgency levels, and (2) it provides depth that institutional takers need for larger fills.

Layer Design Principles

  • Level 1 (tight): Smallest size, tightest spread — captures price-sensitive flow
  • Level 2 (mid): Medium size, moderate spread — the workhorse layer
  • Level 3 (wide): Largest size, widest spread — backstop for large trades, higher margin
  • Level 4+ (deep): Optional. Reserve for extreme dislocation events only
Level Spread from Mid Size (BTC) Role Fill Rate
1±5 bps0.005Price discovery, flow captureHigh
2±12 bps0.02Core volume layerMedium
3±25 bps0.05Deep liquidity, adverse selection bufferLow
4±60 bps0.1Extreme events backstopVery low
def build_order_book_layers(
    mid: float,
    inventory: float,
    max_inventory: float,
    levels: list[dict]
) -> list[dict]:
    """
    Build multi-level order book quotes.
    levels = [{"spread_bps": 5, "size": 0.005}, ...]
    Returns list of {side, price, size} dicts.
    """
    orders = []
    inv_ratio = inventory / max_inventory  # -1 to +1

    for level in levels:
        half = mid * (level["spread_bps"] / 10000) / 2
        skew = inv_ratio * half * 0.5

        bid_price = mid - half + skew
        ask_price = mid + half + skew

        # Reduce bid size when long, reduce ask size when short
        bid_size = level["size"] * max(0.1, 1 - inv_ratio)
        ask_size = level["size"] * max(0.1, 1 + inv_ratio)

        orders.extend([
            {"side": "buy",  "price": round(bid_price, 4), "size": round(bid_size, 5)},
            {"side": "sell", "price": round(ask_price, 4), "size": round(ask_size, 5)},
        ])

    return orders

# Example usage
layers = [
    {"spread_bps": 5,  "size": 0.005},
    {"spread_bps": 12, "size": 0.02},
    {"spread_bps": 25, "size": 0.05},
    {"spread_bps": 60, "size": 0.10},
]
book = build_order_book_layers(mid=65000.0, inventory=0.15, max_inventory=0.5, levels=layers)
Tip: Refresh Cadence by Layer

Tight levels (1-2) should refresh every 1-2 seconds as the mid moves. Wide levels (3-4) can refresh every 15-30 seconds — they are rarely at risk of immediate crossing and excessive cancels waste API quota.

6. Fee Optimization: Maker Rebates vs Taker Fees

Fee structure can make or break a market making operation. On most exchanges, maker orders (limit orders that rest in the book) earn a rebate, while taker orders (market orders or limit orders that cross) pay a fee. The spread income equation must account for this:

True spread income per round-trip
gross = ask_fill_price - bid_fill_price
fees = -maker_rebate_bid - maker_rebate_ask (negative = income)
net = gross + |maker_rebate_bid| + |maker_rebate_ask|

Example (8 bps spread, 1 bps rebate each side):
net = 8 bps + 1 bps + 1 bps = 10 bps effective spread income

post_only Orders

Always set post_only: true on maker orders. This guarantees the order will never accidentally cross the book and become a taker, which would flip your fee from a rebate (income) to a cost (expense). If the order would have crossed, it gets cancelled instead — a small annoyance, but far better than paying a taker fee on every missed update.

Volume Tier Benefits

Most exchanges, including Purple Flea, offer tiered fee schedules based on 30-day trading volume. A market maker churning significant volume quickly reaches elite tiers:

Tier 30d Volume Maker Rebate Taker Fee
Standard< $100K0.00%0.05%
Silver$100K – $1M+0.01%0.04%
Gold$1M – $10M+0.02%0.03%
Platinum> $10M+0.03%0.02%

Optimal Minimum Spread

The minimum economically rational spread must cover adverse selection costs plus guarantee a non-negative contribution after fees — even if your rebate barely covers transaction costs:

Minimum viable spread
min_spread = 2 * adverse_selection_cost_bps + overhead_bps
where overhead_bps accounts for: API costs, infra, risk capital carry

Typical: min_spread ≥ 3-5 bps on liquid BTC markets
Wider markets (altcoins): 20-100 bps minimum to be competitive

7. Risk Limits and Circuit Breakers

A market making bot left unchecked can accumulate catastrophic losses in minutes during adverse conditions. Every production market maker must implement multiple layers of hard stops.

Inventory Limits

  • Soft limit (50% of max): Begin aggressive skewing. No manual intervention needed.
  • Hard limit (80% of max): Cancel quotes on the inventory-adding side. Only show the reducing side.
  • Emergency limit (100% of max): Cancel all quotes. Send a market order to reduce inventory by 50%. Alert operator.

Loss-Based Circuit Breakers

class RiskManager:
    def __init__(self,
        max_inventory: float,       # in base asset
        loss_per_5min: float,       # USD, triggers 60s pause
        max_daily_loss: float,      # USD, triggers 24h halt
        max_spread_bps: float = 200 # safety cap on spread width
    ):
        self.limits = {
            "max_inventory": max_inventory,
            "loss_5min": loss_per_5min,
            "daily_loss": max_daily_loss,
            "max_spread": max_spread_bps
        }
        self.fills: list = []
        self.daily_realized: float = 0.0
        self.paused_until: float = 0.0
        self.halted: bool = False

    def check(self, inventory: float, mid: float) -> str:
        """
        Returns: 'ok' | 'pause' | 'halt' | 'reduce_inventory'
        """
        if self.halted:
            return "halt"
        if time.time() < self.paused_until:
            return "pause"
        if abs(inventory) >= self.limits["max_inventory"]:
            return "reduce_inventory"

        loss_5m = self._recent_loss(300)
        if loss_5m > self.limits["loss_5min"]:
            self.paused_until = time.time() + 60
            return "pause"
        if self.daily_realized < -self.limits["daily_loss"]:
            self.halted = True
            return "halt"

        return "ok"

    def _recent_loss(self, window: int) -> float:
        cutoff = time.time() - window
        recent = [f for f in self.fills if f["t"] > cutoff]
        pnl = sum(
            f["price"] * f["size"] * (1 if f["side"] == "sell" else -1)
            for f in recent
        )
        return max(0.0, -pnl)

Spread Safety Cap

During extreme volatility or model errors, the A-S formula can produce nonsensically wide spreads. Always cap the maximum spread to a sane value:

Spread sanity cap
spread = min(computed_spread, mid * 0.02) # never wider than 200 bps spread = max(spread, mid * 0.0003) # never tighter than 3 bps

Stale Price Detection

If the feed stops updating — due to exchange outages, WebSocket disconnects, or network issues — you may be quoting at a stale mid price. Detect this and halt:

MAX_PRICE_AGE_SECONDS = 5

def is_price_stale(last_price_time: float) -> bool:
    return (time.time() - last_price_time) > MAX_PRICE_AGE_SECONDS

8. Integration with Purple Flea Trading API

Purple Flea's trading API is REST-based with WebSocket feeds for real-time market data. Here is the full integration pattern for a production market maker agent:

import asyncio
import json
import websockets
import httpx

PF_WS  = "wss://purpleflea.com/ws/trading"
PF_API = "https://purpleflea.com/api/trading"

async def run_market_maker(api_key: str, symbol: str = "BTC-USDC"):
    config = MarketMakerConfig(
        api_key=api_key,
        symbol=symbol,
        base_spread_bps=8.0,
        risk_aversion=0.01,
        max_inventory=0.5,
        circuit_break_loss=50.0,
        max_loss_session=200.0
    )
    agent = MarketMakerAgent(config)
    risk  = RiskManager(
        max_inventory=0.5,
        loss_per_5min=25.0,
        max_daily_loss=200.0
    )
    last_price_time = 0.0
    mid = None

    async with websockets.connect(
        f"{PF_WS}?token={api_key}"
    ) as ws:
        # Subscribe to ticker stream
        await ws.send(json.dumps({
            "action": "subscribe",
            "channel": "ticker",
            "symbol": symbol
        }))

        # Also subscribe to fills channel to update inventory
        await ws.send(json.dumps({
            "action": "subscribe",
            "channel": "fills"
        }))

        async def quote_loop():
            while True:
                if mid is None or is_price_stale(last_price_time):
                    await agent.cancel_all_quotes()
                    await asyncio.sleep(1)
                    continue

                status = risk.check(agent.inventory, mid)
                if status == "halt":
                    await agent.cancel_all_quotes()
                    print("[HALT] Daily loss limit reached")
                    return
                elif status in ("pause", "reduce_inventory"):
                    await agent.cancel_all_quotes()
                    await asyncio.sleep(2)
                    continue

                quote = agent.update_quotes(mid)
                if quote:
                    await agent.cancel_all_quotes()
                    await agent.place_quotes(quote)
                    print(f"bid={quote.bid} ask={quote.ask} "
                          f"r={quote.reservation_price} "
                          f"spread={quote.spread_bps:.1f}bps")

                await asyncio.sleep(2)

        async def feed_loop():
            nonlocal mid, last_price_time
            async for msg in ws:
                data = json.loads(msg)
                if data.get("channel") == "ticker":
                    mid = data["mid"]
                    last_price_time = time.time()
                elif data.get("channel") == "fills":
                    fill = data["fill"]
                    agent.record_fill(
                        fill["side"], fill["price"], fill["size"]
                    )

        await asyncio.gather(feed_loop(), quote_loop())

Agent Registration

Before placing quotes, your agent must be registered with Purple Flea. Get your API key from the API Keys page, then authenticate via the Trading API docs. New agents can claim free starting capital via the Agent Faucet to bootstrap their market making operation.

Start Market Making on Purple Flea

Register your agent, get an API key, and start earning maker rebates on every fill. New agents get free capital via the faucet.

Register Agent View API Docs

Performance Benchmarks and Tuning

Key Performance Metrics

Metric Formula Target Range
Fill ratefills / quotes placed20-60%
Spread earned per fillnet_pnl / total_volume3-15 bps
Inventory turnovervolume / avg_inventory> 5x per session
Adverse selection ratiolosing fills / total fills< 40%
Sharpe ratiopnl_mean / pnl_std (daily)> 2.0

Parameter Tuning Guide

  • Low fill rate: Spread too wide. Reduce base_spread_bps or lower risk_aversion.
  • High adverse selection: Spread too tight relative to information. Increase risk_aversion or add volatility filter.
  • Inventory accumulating: Skew not aggressive enough. Increase skew_factor.
  • Negative PnL despite fills: Classic adverse selection or fees not covered. Increase minimum spread floor.
Backtesting First

Never deploy parameter changes live without first running a backtest on historical tick data. The Purple Flea API provides historical order book snapshots via the /history/orderbook endpoint. A good backtest covers at least 7 days including a high-volatility period.


Summary

Advanced market making for AI agents combines mathematical rigor with robust software engineering. The Avellaneda-Stoikov framework gives you a principled approach to quote placement that adapts to inventory and volatility in real time. Layered order books maximize fill opportunities. Hard risk limits and circuit breakers protect capital during model failures or adverse market conditions.

The key principles to remember:

  • Always use post_only orders to guarantee maker rebates
  • Skew quotes aggressively before inventory reaches hard limits
  • Widen spreads during high-volatility periods, not after losses
  • Implement multiple circuit-breaker layers: per-fill, per-5-min, and daily
  • Measure adverse selection ratio — if it's too high, widen your spread

Ready to deploy? Grab a free API key at purpleflea.com/register, claim your starting capital via the faucet, and start providing liquidity today.