● Strategy

NFT Market Making for AI Agents: Floor Price Bots and Collection Analytics

NFT markets are among the most inefficient in crypto. Bid-ask spreads of 10-30% are routine. Rarity mispricing is ubiquitous. Floor prices are set by the least sophisticated seller. For a well-configured AI agent with reliable collection analytics, market making in the NFT space can capture these inefficiencies systematically — provided the agent avoids the two main pitfalls: wash trading and illiquidity traps.

This guide covers the full stack: floor price discovery mechanics, rarity-adjusted fair value models, spread optimization, wash trading detection, multi-chain custody via Purple Flea's wallet, and a production Python market making bot.


NFT Market Making Fundamentals

Traditional market making involves simultaneously quoting a bid and an ask price, profiting from the spread. NFT market making is structurally different in three key ways:

Despite these differences, the market making logic is similar: buy below fair value, sell above fair value, manage inventory, and turn spreads into profit.

The NFT Market Making Loop

  1. Compute fair value for each token in a collection (rarity-adjusted)
  2. Place collection offers below fair value (bid side)
  3. When a bid is filled (you buy an NFT), list it above fair value (ask side)
  4. Continuously update bids/asks as floor price and rarity premia shift
  5. Track inventory, PnL, and exit positions that age beyond your time horizon

Floor Price Bots

The floor price of an NFT collection is the lowest listing price across all tokens. It serves as the baseline for valuation — but it is a noisy and manipulable signal.

Why Floor Price Alone is Insufficient

Floor price captures only the cheapest token, which is almost always a common trait token. It ignores:

Floor Price Smoothing

A robust floor price for market making should be computed as a time-weighted median of recent sales, not instantaneous listings:

Smoothed Floor Price
Floor_smooth = EWMA(recent_sales, span=24h) × (1 − listing_discount)

Where listing_discount accounts for the fact that listings overshoot fair value. A 5-10% discount is typical for high-volume collections. In practice, use sales data, not listings, as the primary signal.

Floor Price Bot Architecture

A production floor price bot should:

Rarity-Adjusted Pricing

Rarity scoring is the core of NFT fair value estimation. The two dominant approaches are:

1. Trait Rarity Score (Sum of Inverse Frequencies)

For each trait t in a token, compute the rarity score as the sum of the inverse frequency of each trait across the collection:

Trait Rarity Score
RS(token) = Σt (1 / P(trait_t)) = Σt (N / count(trait_t))

Where N = total supply and count(trait_t) = number of tokens with that trait. A token with an extremely rare trait (1/10000) contributes 10000 to its RS; common traits (5000/10000) contribute only 2.

2. Information Entropy Scoring

A more principled approach treats rarity as information content:

Entropy Rarity Score
IS(token) = −Σt P(trait_t) × log2(P(trait_t))

Higher information content = rarer token. This approach is less sensitive to outlier traits that dominate the sum in Method 1.

Rarity-to-Price Mapping

Having computed rarity scores, the critical step is mapping them to price premiums. This is empirical — you need sales history to calibrate:

Rarity Percentile Rarity Tier Typical Price Premium (vs Floor) Liquidity
0–10% (most rare) Legendary 5x–20x floor Very Low
10–25% Rare 2x–5x floor Low
25–50% Uncommon 1.2x–2x floor Medium
50–85% Common 0.9x–1.2x floor High
85–100% (most common) Floor 0.8x–1.0x floor Highest

Market making agents should focus on the Common and Uncommon tiers (25–85th percentile). Rare and Legendary tokens have insufficient liquidity for reliable market making — they require holding inventory for weeks or months, which introduces unacceptable capital risk.

Bid-Ask Spread Optimization

The optimal spread for NFT market making depends on three factors: holding time, volatility, and transaction costs.

The Avellaneda-Stoikov Framework (Adapted)

The Avellaneda-Stoikov model, originally for stock market making, adapts well to NFTs. The optimal bid and ask around fair value s are:

Optimal Bid and Ask
bid* = s − (γ·σ²·T/2) − (1/γ)·ln(1 + γ/k)
ask* = s + (γ·σ²·T/2) + (1/γ)·ln(1 + γ/k)

Where:

In practice for NFTs, this simplifies to: set your bid at (fair_value − expected_volatility − tx_cost) and your ask at (fair_value + target_spread + tx_cost).

Practical Spread Guidelines

Market Condition Recommended Spread Rationale
Stable floor (low vol) 8–12% Low adverse selection; tighter spread captures volume
Rising floor (bull) 12–18% Risk that bids get lifted, inventory goes up in value
Falling floor (bear) 20–35% High adverse selection; protect against holding losers
High volume / trending 10–15% Fast turnover compensates for tighter margin
Low volume / dead Pause bids Illiquidity trap risk exceeds spread income

Wash Trading Detection

NFT wash trading is widespread. It inflates apparent volume, distorts floor price calculations, and can trap market makers who use volume as a signal. Detecting it is non-trivial but possible.

Wash Trade Signatures

Wash Trade Filter Implementation

def is_wash_trade(sale: dict, wallet_graph: dict) -> bool:
    """
    Heuristic wash trade detector.
    Returns True if the sale appears to be a wash trade.
    """
    buyer = sale["buyer_address"]
    seller = sale["seller_address"]
    price = sale["price_eth"]
    smoothed_floor = sale["collection_floor_smooth"]

    # Heuristic 1: Same wallet or known related wallets
    if buyer == seller:
        return True

    buyer_cluster = wallet_graph.get(buyer, {buyer})
    seller_cluster = wallet_graph.get(seller, {seller})
    if buyer_cluster & seller_cluster:  # overlapping clusters
        return True

    # Heuristic 2: Price outlier — more than 5x smoothed floor
    if smoothed_floor > 0 and price > smoothed_floor * 5:
        return True

    # Heuristic 3: Round-trip — check if buyer previously sold same token
    token_id = sale["token_id"]
    prior_sales = sale.get("token_sale_history", [])
    for prior in prior_sales[-3:]:  # check last 3 sales
        if prior["seller"] == buyer and prior["buyer"] == seller:
            return True  # clear round-trip

    return False

Collection Analytics

Before committing capital to market making a collection, a thorough analytics scan should determine if the collection is worth trading.

Key Metrics to Evaluate

Metric Good Signal Red Flag
Daily sales volume > 50 sales/day < 5 sales/day
Unique buyers (7d) > 100 wallets < 20 wallets
Wash trade ratio < 15% of volume > 40% of volume
Floor price trend (7d) Stable or rising Down >20% in 7d
Listed supply % < 5% listed > 15% listed
Holder concentration Top 10 hold < 20% Top 10 hold > 50%
Royalty enforcement Royalties paid on >80% Blur pool bypasses OK

Collection Scoring Model

Automate collection selection with a weighted scoring model. Each metric gets a score of 0-10 and a weight based on its predictive power for maker profitability:

Collection Score
Score = 0.30×Volume + 0.25×Trend + 0.20×(1−WashRatio) + 0.15×Diversity + 0.10×Liquidity

Collections scoring above 7.0 are candidates for active market making. Below 5.0 are avoided entirely.

Purple Flea Wallet for NFT Custody

NFT market making requires holding inventory across multiple chains simultaneously. Purple Flea's multi-chain wallet provides programmatic custody for:

Key wallet features for NFT market makers:

Python Market Making Bot

"""
Purple Flea NFT Market Making Agent
Implements: rarity scoring, wash trade filtering, spread optimization,
            multi-chain custody via Purple Flea Wallet API
Author: Purple Flea Research Team | 2026
"""

import asyncio
import aiohttp
import logging
import math
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Set

API_BASE = "https://api.purpleflea.com"
API_KEY = "your_api_key_here"
AGENT_ID = "your_agent_id"

# Market making parameters
MIN_SPREAD_PCT = 0.08           # 8% minimum spread
MAX_SPREAD_PCT = 0.35           # 35% maximum spread
MAX_INVENTORY_TOKENS = 15       # max tokens held at once
MAX_POSITION_ETH = 5.0          # max ETH per collection
FLOOR_EMA_PERIODS = 20          # number of sales for floor EMA
TARGET_RARITY_PERCENTILE = (25, 85)  # only trade common/uncommon

@dataclass
class NFTToken:
    collection: str
    token_id: int
    chain: str
    rarity_score: float
    rarity_percentile: float    # 0 = rarest, 100 = most common
    fair_value_eth: float
    purchase_price_eth: float
    listed_price_eth: float = 0.0
    held_hours: float = 0.0

@dataclass
class CollectionState:
    name: str
    chain: str
    contract: str
    floor_smooth: float = 0.0   # smoothed floor in ETH
    floor_raw: float = 0.0      # raw cheapest listing
    volume_24h: float = 0.0
    wash_ratio: float = 0.0
    score: float = 0.0
    traits: Dict[str, Dict[str, int]] = field(default_factory=dict)
    total_supply: int = 0
    recent_sales: List[dict] = field(default_factory=list)
    bid_placed: bool = False
    current_bid: float = 0.0

class NFTMarketMaker:
    def __init__(self, capital_eth: float, collections: List[dict]):
        self.capital_eth = capital_eth
        self.target_collections = collections
        self.inventory: List[NFTToken] = []
        self.collection_states: Dict[str, CollectionState] = {}
        self.session: Optional[aiohttp.ClientSession] = None
        self.wallet_graph: Dict[str, Set[str]] = defaultdict(set)
        self.logger = logging.getLogger("NFTMaker")
        self.total_pnl_eth: float = 0.0

    async def start(self):
        self.session = aiohttp.ClientSession(
            headers={"X-API-Key": API_KEY, "X-Agent-ID": AGENT_ID}
        )
        self.logger.info(f"NFT Market Maker starting. Capital: {self.capital_eth} ETH")
        await self._run_loop()

    def _compute_rarity_score(self, token_traits: Dict[str, str],
                               collection: CollectionState) -> float:
        """
        Trait Rarity Score: sum of (1/frequency) for each trait.
        Normalized by collection supply.
        """
        score = 0.0
        for trait_type, trait_value in token_traits.items():
            trait_counts = collection.traits.get(trait_type, {})
            count = trait_counts.get(trait_value, 1)
            score += collection.total_supply / count
        return score

    def _score_to_percentile(self, score: float,
                               all_scores: List[float]) -> float:
        """Convert raw rarity score to percentile (0=rarest, 100=most common)."""
        n = len(all_scores)
        rank = sum(1 for s in all_scores if s > score)
        return (1 - rank / n) * 100

    def _rarity_premium(self, percentile: float) -> float:
        """
        Map rarity percentile to price premium multiplier over floor.
        Calibrated from empirical NFT sales data.
        """
        if percentile <= 10:
            return 8.0    # legendary tier
        elif percentile <= 25:
            return 3.0    # rare tier
        elif percentile <= 50:
            return 1.5    # uncommon tier
        elif percentile <= 85:
            return 1.05   # common tier
        else:
            return 0.90   # floor tier (at slight discount)

    def _compute_spread(self, state: CollectionState) -> float:
        """
        Dynamic spread based on floor volatility and volume.
        Higher volatility and lower volume = wider spread.
        """
        # Estimate volatility from recent sales vs floor
        if len(state.recent_sales) < 5:
            return MAX_SPREAD_PCT

        prices = [s["price"] for s in state.recent_sales[-10:]]
        mean_p = sum(prices) / len(prices)
        variance = sum((p - mean_p) ** 2 for p in prices) / len(prices)
        vol = math.sqrt(variance) / mean_p if mean_p > 0 else 0.3

        # Volume score: 0-1, higher volume → tighter spread OK
        vol_score = min(state.volume_24h / 20.0, 1.0)  # normalize to 20 ETH/day

        # Blend: spread = base + vol_penalty - volume_discount
        spread = MIN_SPREAD_PCT + (2 * vol) - (0.05 * vol_score)
        return min(max(spread, MIN_SPREAD_PCT), MAX_SPREAD_PCT)

    def _is_wash_trade(self, sale: dict) -> bool:
        """Heuristic wash trade filter."""
        buyer = sale.get("buyer", "")
        seller = sale.get("seller", "")
        price = sale.get("price", 0)
        floor = sale.get("collection_floor", price)

        if buyer == seller:
            return True
        if floor > 0 and price > floor * 5:
            return True

        buyer_cluster = self.wallet_graph.get(buyer, {buyer})
        seller_cluster = self.wallet_graph.get(seller, {seller})
        if buyer_cluster & seller_cluster:
            return True

        return False

    def _compute_floor_smooth(self, sales: List[dict]) -> float:
        """EWMA of recent clean sales prices — the true floor estimate."""
        clean_sales = [s for s in sales if not self._is_wash_trade(s)]
        if not clean_sales:
            return 0.0

        prices = [s["price"] for s in clean_sales[-FLOOR_EMA_PERIODS:]]
        alpha = 2 / (len(prices) + 1)
        ema = prices[0]
        for p in prices[1:]:
            ema = alpha * p + (1 - alpha) * ema
        return ema

    async def _fetch_collection_state(self, col: dict) -> CollectionState:
        """Fetch current collection data: floor, sales, traits."""
        async with self.session.get(
            f"{API_BASE}/v1/wallet/nft/collection",
            params={"contract": col["contract"], "chain": col["chain"]}
        ) as r:
            data = (await r.json())

        state = CollectionState(
            name=col["name"],
            chain=col["chain"],
            contract=col["contract"],
            floor_raw=data["floor_price_eth"],
            volume_24h=data["volume_24h_eth"],
            traits=data.get("traits", {}),
            total_supply=data.get("total_supply", 10000),
            recent_sales=data.get("recent_sales", [])
        )
        state.floor_smooth = self._compute_floor_smooth(state.recent_sales)
        return state

    async def _place_collection_bid(self, state: CollectionState, bid_price: float):
        """Place a collection-wide offer via Purple Flea Wallet API."""
        self.logger.info(
            f"Placing bid: {state.name} @ {bid_price:.4f} ETH"
        )
        async with self.session.post(
            f"{API_BASE}/v1/wallet/nft/offer",
            json={
                "contract": state.contract,
                "chain": state.chain,
                "price_eth": bid_price,
                "offer_type": "collection",
                "expiry_hours": 4
            }
        ) as r:
            result = await r.json()
            state.bid_placed = result.get("success", False)
            state.current_bid = bid_price

    async def _list_inventory_token(self, token: NFTToken, ask_price: float):
        """List a held token for sale at the computed ask price."""
        self.logger.info(
            f"Listing #{token.token_id}: {token.collection} @ {ask_price:.4f} ETH"
        )
        async with self.session.post(
            f"{API_BASE}/v1/wallet/nft/list",
            json={
                "contract": token.collection,
                "chain": token.chain,
                "token_id": token.token_id,
                "price_eth": ask_price,
                "marketplace": "blur"    # best liquidity in 2026
            }
        ) as r:
            token.listed_price_eth = ask_price

    async def _process_collection(self, col: dict):
        """Full market making cycle for one collection."""
        state = await self._fetch_collection_state(col)
        key = state.contract

        # Collection eligibility check
        if state.floor_smooth <= 0 or state.volume_24h < 1.0:
            self.logger.info(f"Skipping {state.name}: low volume")
            return

        # Update collection state cache
        self.collection_states[key] = state

        # Compute spread
        spread = self._compute_spread(state)

        # Target: buy at floor_smooth * (1 - spread/2)
        #         sell at floor_smooth * (1 + spread/2)
        bid_price = state.floor_smooth * (1 - spread / 2)
        ask_base = state.floor_smooth * (1 + spread / 2)

        # Count current inventory for this collection
        col_inventory = [t for t in self.inventory
                        if t.collection == state.contract]
        total_eth_exposure = sum(t.purchase_price_eth for t in col_inventory)

        # Place bids if under inventory limits
        can_buy = (len(self.inventory) < MAX_INVENTORY_TOKENS and
                   total_eth_exposure < MAX_POSITION_ETH)

        if can_buy and not state.bid_placed:
            await self._place_collection_bid(state, bid_price)

        # Update asks for held inventory
        for token in col_inventory:
            premium = self._rarity_premium(token.rarity_percentile)
            ask = ask_base * premium
            if abs(ask - token.listed_price_eth) / (token.listed_price_eth or 1) > 0.02:
                await self._list_inventory_token(token, ask)

    async def _run_loop(self):
        """Main market making loop — runs continuously."""
        while True:
            try:
                for col in self.target_collections:
                    await self._process_collection(col)
                    await asyncio.sleep(2)  # rate limiting between collections

                self.logger.info(
                    f"Cycle complete. Inventory: {len(self.inventory)} tokens | "
                    f"Total PnL: {self.total_pnl_eth:+.4f} ETH"
                )
            except Exception as e:
                self.logger.error(f"Loop error: {e}")

            await asyncio.sleep(300)  # 5-minute refresh cycle


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)

    collections = [
        {"name": "Pudgy Penguins", "contract": "0xbd3531...", "chain": "ethereum"},
        {"name": "DeGods", "contract": "6XxjKYFb...", "chain": "solana"},
        {"name": "Milady", "contract": "0x5af0d9...", "chain": "ethereum"},
    ]

    agent = NFTMarketMaker(capital_eth=10.0, collections=collections)
    asyncio.run(agent.start())

Risk Management

Inventory Risk

Holding NFT inventory is fundamentally a directional bet on the collection's floor price. Unlike fungible tokens, you cannot hedge individual NFT positions efficiently. Risk controls:

Gas Cost Modeling

On Ethereum, gas costs are a significant expense for NFT market makers. A single buy + list cycle can cost $30-100 in gas during congested periods. Model this explicitly:

Minimum Viable Spread (Gas-Adjusted)
min_spread = (2 × gas_cost_usd) / (fair_value_usd) + platform_fee

If gas is $50 and the NFT is worth $500, the gas overhead alone requires a minimum 20% round-trip spread. On L2s (Arbitrum, Base, Polygon), gas drops to <$1, enabling much tighter spreads and lower floor-price thresholds.

Getting Started

  1. Register your agent at /register and enable wallet access
  2. Fund your wallet via Purple Flea Wallet API with ETH on Ethereum and/or L2s
  3. Select 2-3 target collections using the analytics scoring framework above
  4. Deploy the market maker with conservative parameters (8-12% spread, max 5 tokens inventory) to start
  5. Monitor via logs — track fills, PnL per collection, and wash trade detection rates
  6. Scale gradually — add collections and increase inventory limits as you build confidence in the system

Start Small: Begin with 1-2 ETH allocated and max 5 tokens inventory. NFT market making has a learning curve around wash trade detection and spread calibration. Validate your rarity scoring against actual market sales before scaling capital.

Summary

NFT market making is an alpha-rich but operationally complex strategy. The edge comes from three sources: rarity mispricing (most sellers don't compute fair value rigorously), spread capture (10-30% round-trip spreads are common), and information advantages from systematic wash trade filtering and collection analytics.

The Python bot above implements the full stack: rarity scoring, dynamic spread computation, wash trade filtering, collection offer placement, and inventory listing via Purple Flea's multi-chain wallet. Start with the conservative parameters on high-liquidity collections, let the bot run for 2-3 cycles, and tune the spread and inventory limits based on actual fill rates and PnL.

NFT markets in 2026 are increasingly bot-dominated on the bid side — which means well-engineered agents have a structural advantage over human market makers who cannot monitor 24/7 or react to floor moves within seconds.