Important Disclaimers

Disclaimer: This guide is for educational and informational purposes only. It does not constitute financial advice, investment advice, legal advice, or a recommendation to trade on any prediction market or sportsbook. Trading on prediction markets involves substantial risk of loss. You can lose some or all of your capital.

Disclaimer: Prediction market regulations vary by jurisdiction. It is your responsibility to determine whether participating in prediction markets is legal in your jurisdiction. Consult a qualified legal professional before deploying any automated trading system. See the Security Best Practices guide for more.

Disclaimer: The code examples in this guide are simplified for educational purposes and are NOT production-ready. Never deploy a trading agent with real money without thorough testing, security review, and risk management systems in place. Always start with testnet or paper trading.

Introduction: Why Build a Prediction Market Agent?

A prediction market agent is autonomous software that interacts with prediction market platforms (such as Polymarket or Kalshi) to analyze data, identify opportunities, and execute trades without continuous human intervention. Unlike manual trading, agents can monitor hundreds of markets simultaneously, react to price movements in milliseconds, and operate around the clock.

The prediction market industry processed over $44 billion in trading volume in 2025, and the AI agent market is projected to reach $50 billion by 2030. At the intersection of these two trends lies a massive opportunity for builders who can create autonomous agents that trade intelligently. For a complete overview of this landscape, see the Prediction Market Agent Marketplace Guide.

This guide walks you through building a simple cross-market arbitrage bot from scratch. Arbitrage is the ideal starting point because the logic is straightforward: find price discrepancies between two platforms for the same event and trade both sides to lock in a risk-free profit. By the end of this guide, you will have a working Python agent that fetches live market data from Polymarket and Kalshi, detects arbitrage opportunities, and is wired to a wallet infrastructure capable of executing trades.

For a broader look at the types of agents being built today (arbitrage, copy-trading, sentiment, market-making), see the Best Prediction Market Bots 2026 ranking.

Understanding the Agent Betting Stack

Before writing a single line of code, you need to understand the infrastructure your agent will operate on. AgentBets.ai uses a four-layer framework called the Agent Betting Stack. Every prediction market agent, whether simple or complex, needs each of these layers.

Each layer builds on the one below it. You cannot trade without a wallet. You cannot participate in a marketplace without an identity. This guide focuses primarily on Layers 2, 3, and 4. For deep dives on identity systems, see the Agent Identity Comparison. For wallet selection, see the Best Agent Wallet for Prediction Markets 2026.

Why Start with Arbitrage?

There are dozens of strategies a prediction market agent can employ. So why is arbitrage the best starting point for a first build?

  • Mechanical, not predictive. Arbitrage does not require your agent to predict outcomes. It exploits price differences between platforms. If Polymarket prices a YES contract at $0.42 and Kalshi prices YES on the same event at $0.55, the math works regardless of who wins.
  • Bounded risk. A well-structured arbitrage trade has a known maximum loss (transaction fees and slippage) and a known minimum profit (the spread minus costs).
  • Clear success metric. Either the spread exists and your bot captures it, or it does not.
  • Foundation for complexity. Once you have an arbitrage bot running, you can layer on intelligence: better event matching via NLP, latency optimization, dynamic position sizing, or multi-platform expansion to sportsbooks. See the Cross-Market Arbitrage Guide for advanced strategies.

Disclaimer: True risk-free arbitrage is rare in practice. Slippage, transaction fees, execution delays, and market resolution risk can all erode or eliminate theoretical profits. The arbitrage bot in this guide is a simplified educational example. Always paper trade first.

Prerequisites and Setup

Project Structure

prediction-market-agent/
├── agent/
│   ├── __init__.py
│   ├── config.py          # Configuration and constants
│   ├── data_fetcher.py    # API integrations (Polymarket, Kalshi)
│   ├── arbitrage.py       # Arbitrage detection logic
│   ├── wallet.py          # Wallet integration (Coinbase Agentic)
│   ├── risk_manager.py    # Spending limits & guardrails
│   └── main.py            # Entry point and orchestration
├── tests/
│   └── test_arbitrage.py  # Unit tests for detection logic
├── .env.example           # Environment variable template
├── requirements.txt       # Python dependencies
└── README.md

Install Dependencies

# requirements.txt
aiohttp>=3.9.0
python-dotenv>=1.0.0
pydantic>=2.0.0
pip install -r requirements.txt

Step 1: Configuration and Constants

First, set up your configuration module. This centralizes all tunable parameters so you can adjust thresholds, API endpoints, and risk limits without touching the core logic. For a detailed breakdown of optimal risk parameters, see the Agent Wallet Security guide.

# agent/config.py
"""AgentBets.ai - Prediction Market Agent Configuration"""

import os
from dotenv import load_dotenv
from dataclasses import dataclass

load_dotenv()

@dataclass
class AgentConfig:
    """Core configuration for the arbitrage agent."""

    # --- API Endpoints ---
    POLYMARKET_API: str = "https://clob.polymarket.com"
    KALSHI_API: str = "https://api.elections.kalshi.com/trade-api/v2"

    # --- Arbitrage Parameters ---
    MIN_SPREAD_PCT: float = 0.03      # Minimum 3% spread to trigger (after fees)
    MAX_POSITION_USD: float = 50.0    # Max USD per side of any single arb trade
    MIN_LIQUIDITY_USD: float = 500.0  # Skip markets with < $500 orderbook depth

    # --- Risk Management ---
    SESSION_BUDGET_USD: float = 200.0  # Total max spend per agent session
    DAILY_LOSS_LIMIT_USD: float = 100.0
    MAX_OPEN_POSITIONS: int = 5

    # --- Polling ---
    POLL_INTERVAL_SEC: int = 30        # How often to scan for opportunities

    # --- API Credentials (loaded from .env) ---
    KALSHI_API_KEY: str = os.getenv("KALSHI_API_KEY", "")
    KALSHI_API_SECRET: str = os.getenv("KALSHI_API_SECRET", "")

    # --- Wallet (Coinbase Agentic Wallet) ---
    CDP_API_KEY: str = os.getenv("CDP_API_KEY", "")
    CDP_API_SECRET: str = os.getenv("CDP_API_SECRET", "")


config = AgentConfig()

Disclaimer: Never commit API keys or secrets to version control. Always use environment variables or a secrets manager. Leaked API keys on Polymarket or Kalshi can result in unauthorized trades on your account.

Step 2: Fetching Market Data

The data fetcher is Layer 3 (Trading) of the Agent Betting Stack. It connects to prediction market APIs and normalizes the data into a common format your arbitrage logic can consume. For the complete API reference with all available endpoints, rate limits, and authentication details, see the Prediction Market API Reference.

Polymarket CLOB API: Polymarket operates a Central Limit Order Book. Market data is free and unauthenticated. Each market has a condition_id and two token_ids (YES and NO). Prices are in USDC on Polygon. For a deep dive, see the Polymarket Bot Quickstart and py_clob_client Reference.

Kalshi REST API: Kalshi is CFTC-regulated and requires authentication for most endpoints. Markets are identified by ticker strings. Prices are in cents (1-99 representing the probability of YES). For full documentation, see the Kalshi API Guide.

# agent/data_fetcher.py
"""Market data fetchers for Polymarket and Kalshi."""

import aiohttp
import asyncio
from dataclasses import dataclass
from typing import Optional
from agent.config import config


@dataclass
class MarketSnapshot:
    """Normalized market data from any platform."""
    platform: str           # 'polymarket' or 'kalshi'
    event_slug: str         # Human-readable event identifier
    market_id: str          # Platform-specific market ID
    yes_price: float        # Current YES price (0.0 to 1.0)
    no_price: float         # Current NO price (0.0 to 1.0)
    yes_liquidity: float    # USD depth on YES side
    no_liquidity: float     # USD depth on NO side
    last_updated: str       # ISO timestamp


class PolymarketFetcher:
    """Fetch market data from Polymarket CLOB API."""

    def __init__(self):
        self.base_url = config.POLYMARKET_API

    async def get_markets(self, limit: int = 50) -> list[dict]:
        """Fetch active markets from Polymarket."""
        async with aiohttp.ClientSession() as session:
            url = f"{self.base_url}/markets"
            params = {"limit": limit, "active": "true"}
            async with session.get(url, params=params) as resp:
                if resp.status == 200:
                    return await resp.json()
                return []

    async def get_orderbook(self, token_id: str) -> dict:
        """Fetch orderbook for a specific token (YES or NO)."""
        async with aiohttp.ClientSession() as session:
            url = f"{self.base_url}/book"
            params = {"token_id": token_id}
            async with session.get(url, params=params) as resp:
                if resp.status == 200:
                    return await resp.json()
                return {}

    def parse_market(self, raw: dict) -> Optional[MarketSnapshot]:
        """Convert raw Polymarket data to normalized snapshot."""
        try:
            tokens = raw.get("tokens", [])
            if len(tokens) < 2:
                return None

            yes_price = float(tokens[0].get("price", 0))
            no_price = float(tokens[1].get("price", 0))

            return MarketSnapshot(
                platform="polymarket",
                event_slug=raw.get("question", "unknown"),
                market_id=raw.get("condition_id", ""),
                yes_price=yes_price,
                no_price=no_price,
                yes_liquidity=float(raw.get("volume", 0)),
                no_liquidity=float(raw.get("volume", 0)),
                last_updated=raw.get("end_date_iso", ""),
            )
        except (KeyError, ValueError):
            return None


class KalshiFetcher:
    """Fetch market data from Kalshi REST API."""

    def __init__(self):
        self.base_url = config.KALSHI_API
        self.api_key = config.KALSHI_API_KEY

    async def get_markets(
        self, status: str = "open", limit: int = 50
    ) -> list[dict]:
        """Fetch active markets from Kalshi."""
        headers = {"Authorization": f"Bearer {self.api_key}"}
        async with aiohttp.ClientSession() as session:
            url = f"{self.base_url}/markets"
            params = {"status": status, "limit": limit}
            async with session.get(
                url, headers=headers, params=params
            ) as resp:
                if resp.status == 200:
                    data = await resp.json()
                    return data.get("markets", [])
                return []

    def parse_market(self, raw: dict) -> Optional[MarketSnapshot]:
        """Convert raw Kalshi data to normalized snapshot."""
        try:
            yes_price = raw.get("yes_ask", 0) / 100.0
            no_price = raw.get("no_ask", 0) / 100.0

            return MarketSnapshot(
                platform="kalshi",
                event_slug=raw.get("title", "unknown"),
                market_id=raw.get("ticker", ""),
                yes_price=yes_price,
                no_price=no_price,
                yes_liquidity=float(raw.get("volume", 0)),
                no_liquidity=float(raw.get("volume", 0)),
                last_updated=raw.get("close_time", ""),
            )
        except (KeyError, ValueError, ZeroDivisionError):
            return None

For a unified API comparison and wrapper libraries that simplify multi-platform data fetching, see the Unified API Comparison.

Step 3: Arbitrage Detection Logic

This is Layer 4 (Intelligence) of the Agent Betting Stack. The arbitrage detector is the brain of your agent. It takes normalized market snapshots from two platforms, matches them to the same real-world event, and calculates whether a profitable spread exists. For advanced strategies, see the Cross-Market Arbitrage Guide and the Sports Betting Arbitrage Bot Guide.

How Cross-Platform Arbitrage Works

The fundamental equation is simple. On prediction markets, YES and NO contracts for a given event should sum to approximately $1.00 (the guaranteed payout at resolution). When the combined cost of buying YES on one platform and NO on the other (or vice versa) is less than $1.00, an arbitrage opportunity exists.

Example: Polymarket YES = $0.42, Kalshi NO = $0.50. Total cost = $0.92. Guaranteed payout = $1.00. Gross profit = $0.08 per share (8.7% return). If both platforms charge ~1% fees, net profit is approximately 6.7%.

The challenge is in the event matching. Polymarket and Kalshi describe the same events differently. Matching them requires either manual mapping (for a first build) or NLP-based fuzzy matching (for production). This guide uses a simple manual mapping dictionary.

# agent/arbitrage.py
"""Arbitrage detection engine for cross-platform prediction markets."""

from dataclasses import dataclass
from typing import Optional
from agent.data_fetcher import MarketSnapshot
from agent.config import config


EVENT_MAP: dict[str, str] = {
    # "polymarket_condition_id": "kalshi_ticker"
    # Example: "0x1234...abcd": "PRES-2028-DEM"
}


@dataclass
class ArbitrageOpportunity:
    """A detected cross-platform arbitrage opportunity."""
    event_name: str
    platform_a: str
    platform_b: str
    buy_yes_on: str
    buy_no_on: str
    yes_price: float
    no_price: float
    total_cost: float
    gross_spread: float
    net_spread: float
    estimated_roi: float


def estimate_fees(cost: float) -> float:
    """
    Estimate total round-trip fees for both platforms.
    Polymarket: ~2% effective fee (maker/taker on CLOB)
    Kalshi: ~1-7 cents per contract depending on price
    This is a rough approximation. Always check current fee schedules.
    """
    return cost * 0.025  # 2.5% combined estimate


def detect_arbitrage(
    market_a: MarketSnapshot,
    market_b: MarketSnapshot,
) -> Optional[ArbitrageOpportunity]:
    """
    Check if two matched markets have an arbitrage spread.
    Returns an ArbitrageOpportunity if the net spread exceeds
    the configured MIN_SPREAD_PCT threshold, otherwise None.
    """

    # Strategy 1: Buy YES on A, buy NO on B
    cost_1 = market_a.yes_price + market_b.no_price
    gross_1 = 1.0 - cost_1
    fees_1 = estimate_fees(cost_1)
    net_1 = gross_1 - fees_1

    # Strategy 2: Buy YES on B, buy NO on A
    cost_2 = market_b.yes_price + market_a.no_price
    gross_2 = 1.0 - cost_2
    fees_2 = estimate_fees(cost_2)
    net_2 = gross_2 - fees_2

    # Pick the better strategy
    if net_1 >= net_2 and net_1 > 0:
        best_cost, best_net = cost_1, net_1
        buy_yes = market_a.platform
        buy_no = market_b.platform
        yes_px = market_a.yes_price
        no_px = market_b.no_price
    elif net_2 > 0:
        best_cost, best_net = cost_2, net_2
        buy_yes = market_b.platform
        buy_no = market_a.platform
        yes_px = market_b.yes_price
        no_px = market_a.no_price
    else:
        return None

    roi = best_net / best_cost if best_cost > 0 else 0

    if roi < config.MIN_SPREAD_PCT:
        return None

    if (market_a.yes_liquidity < config.MIN_LIQUIDITY_USD or
            market_b.no_liquidity < config.MIN_LIQUIDITY_USD):
        return None

    return ArbitrageOpportunity(
        event_name=market_a.event_slug,
        platform_a=market_a.platform,
        platform_b=market_b.platform,
        buy_yes_on=buy_yes,
        buy_no_on=buy_no,
        yes_price=yes_px,
        no_price=no_px,
        total_cost=best_cost,
        gross_spread=1.0 - best_cost,
        net_spread=best_net,
        estimated_roi=roi,
    )

Disclaimer: The fee estimation in this code is approximate. Actual fees vary by order type (limit vs market), platform tier, and current fee schedule. Always verify current fees at polymarket.com and kalshi.com. Underestimating fees is the most common mistake new arbitrage bot builders make.

Step 4: Risk Management and Guardrails

This is arguably the most important module in the entire agent. Without risk management, a bug in your arbitrage logic, a sudden API outage, or an LLM hallucination (in more advanced agents) can drain your wallet in seconds. The Agent Wallet Security guide covers this topic in depth.

Disclaimer: Risk management is not optional. Never deploy an agent with real money without session budgets, per-trade limits, daily loss limits, and a kill switch. Even if the arbitrage math is correct, execution failures, slippage, and platform issues can cause losses that compound rapidly without guardrails.

# agent/risk_manager.py
"""Risk management and spending controls for the arbitrage agent."""

import time
import logging
from dataclasses import dataclass, field
from agent.config import config
from agent.arbitrage import ArbitrageOpportunity

logger = logging.getLogger(__name__)


@dataclass
class RiskState:
    """Tracks session-level risk metrics."""
    session_spent: float = 0.0
    daily_loss: float = 0.0
    open_positions: int = 0
    trades_executed: int = 0
    session_start: float = field(default_factory=time.time)
    is_killed: bool = False


class RiskManager:
    """Enforces spending limits and risk guardrails."""

    def __init__(self):
        self.state = RiskState()

    def kill_switch(self, reason: str) -> None:
        """Emergency shutdown. Prevents all further trading."""
        self.state.is_killed = True
        logger.critical(
            f'KILL SWITCH ACTIVATED: {reason}. '
            f'Session spent: ${self.state.session_spent:.2f}. '
            f'Trades: {self.state.trades_executed}'
        )

    def can_trade(self, opp: ArbitrageOpportunity) -> tuple[bool, str]:
        """
        Check if a proposed trade passes all risk checks.
        Returns (allowed: bool, reason: str).
        """
        if self.state.is_killed:
            return False, "Kill switch is active"

        trade_cost = opp.total_cost * config.MAX_POSITION_USD

        if self.state.session_spent + trade_cost > config.SESSION_BUDGET_USD:
            return False, (
                f'Session budget exceeded: '
                f'${self.state.session_spent:.2f} + '
                f'${trade_cost:.2f} > '
                f'${config.SESSION_BUDGET_USD:.2f}'
            )

        if self.state.daily_loss >= config.DAILY_LOSS_LIMIT_USD:
            self.kill_switch("Daily loss limit reached")
            return False, "Daily loss limit reached"

        if self.state.open_positions >= config.MAX_OPEN_POSITIONS:
            return False, (
                f'Max open positions: {self.state.open_positions}'
            )

        if trade_cost > config.MAX_POSITION_USD:
            return False, (
                f'Trade exceeds per-position limit: '
                f'${trade_cost:.2f} > ${config.MAX_POSITION_USD:.2f}'
            )

        if opp.estimated_roi < config.MIN_SPREAD_PCT:
            return False, "ROI below threshold after re-check"

        return True, "All checks passed"

    def record_trade(self, cost: float) -> None:
        """Record a completed trade."""
        self.state.session_spent += cost
        self.state.trades_executed += 1
        self.state.open_positions += 1
        logger.info(
            f'Trade recorded. Session total: '
            f'${self.state.session_spent:.2f} / '
            f'${config.SESSION_BUDGET_USD:.2f}'
        )

    def record_loss(self, amount: float) -> None:
        """Record a realized loss."""
        self.state.daily_loss += amount
        if self.state.daily_loss >= config.DAILY_LOSS_LIMIT_USD:
            self.kill_switch("Daily loss limit hit")

For wallet-level spending controls (as opposed to application-level controls), the Coinbase Agentic Wallet offers enclave key isolation with built-in session caps and per-transaction limits enforced at the protocol level. Use both application-level AND wallet-level controls together. See the wallet comparison for alternatives.

Step 5: Wallet Integration

The wallet is Layer 2 of the Agent Betting Stack and the most consequential infrastructure choice you will make. For this guide, we use the Coinbase Agentic Wallet because it offers gasless USDC transactions on Base, built-in spending controls, and enclave key isolation. For a detailed comparison of all wallet options, see the Best Agent Wallet for Prediction Markets 2026 and the Coinbase Agentic Wallets Developer Guide.

Disclaimer: This wallet module is a simplified interface for educational purposes. In production, you must implement proper error handling, transaction confirmation monitoring, nonce management, and gas estimation. Never store private keys in code or environment files on shared machines. See the full security checklist at the Agent Wallet Security guide.

# agent/wallet.py
"""Wallet abstraction for the arbitrage agent."""

import logging
from dataclasses import dataclass
from typing import Optional

logger = logging.getLogger(__name__)


@dataclass
class TradeOrder:
    """A trade to execute on a specific platform."""
    platform: str         # 'polymarket' or 'kalshi'
    market_id: str
    side: str             # 'YES' or 'NO'
    amount_usd: float
    max_price: float      # Limit price (slippage protection)


@dataclass
class TradeResult:
    """Result of an attempted trade execution."""
    success: bool
    order_id: Optional[str] = None
    filled_price: Optional[float] = None
    filled_amount: Optional[float] = None
    error: Optional[str] = None


class WalletManager:
    """
    Abstraction over wallet operations.

    In production, this connects to Coinbase Agentic Wallet SDK.
    For this educational build, it logs intended trades without
    executing them (paper trading).
    """

    def __init__(self, paper_mode: bool = True):
        self.paper_mode = paper_mode
        self.balance = 0.0

    async def get_balance(self) -> float:
        """Get current USDC balance."""
        if self.paper_mode:
            return 1000.0  # Simulated balance
        raise NotImplementedError("Connect to wallet SDK")

    async def execute_trade(self, order: TradeOrder) -> TradeResult:
        """
        Execute a trade on the specified platform.

        In paper mode, this simulates execution.
        In live mode, this would:
        1. Sign the transaction with the enclave key
        2. Submit to the platform's CLOB/API
        3. Monitor for fill confirmation
        4. Return the result
        """
        if self.paper_mode:
            logger.info(
                f'[PAPER] {order.side} on {order.platform}: '
                f'${order.amount_usd:.2f} at max {order.max_price:.4f} '
                f'for market {order.market_id}'
            )
            return TradeResult(
                success=True,
                order_id="paper-" + order.market_id[:8],
                filled_price=order.max_price,
                filled_amount=order.amount_usd,
            )
        raise NotImplementedError(
            "Live trading requires Coinbase Agentic Wallet SDK. "
            "See: /guides/coinbase-agentic-wallets-guide/"
        )

    async def execute_arb_pair(
        self, yes_order: TradeOrder, no_order: TradeOrder
    ) -> tuple[TradeResult, TradeResult]:
        """
        Execute both legs of an arbitrage trade.

        CRITICAL: In production, implement atomic execution.
        If the first leg fills but the second fails, you are
        exposed to directional risk.
        """
        result_a = await self.execute_trade(yes_order)

        if not result_a.success:
            return result_a, TradeResult(
                success=False, error="Skipped: first leg failed"
            )

        result_b = await self.execute_trade(no_order)

        if not result_b.success:
            logger.warning(
                "PARTIAL FILL: First leg succeeded, second failed. "
                "Manual intervention may be required."
            )

        return result_a, result_b

The wallet module above is intentionally in paper-trading mode. Before going live, integrate with a real wallet SDK. The Coinbase Agentic Wallets Developer Guide and the x402 Protocol Explainer cover the production integration. For alternative wallet options, the Agent Wallet Comparison evaluates Safe, Lit Protocol, Turnkey, and Privy alongside Coinbase.

Step 6: The Main Orchestrator

The main module ties everything together. It runs a continuous loop: fetch data from both platforms, match events, scan for arbitrage, check risk limits, and execute (or log) trades.

# agent/main.py
"""Main orchestrator for the prediction market arbitrage agent."""

import asyncio
import logging
from agent.config import config
from agent.data_fetcher import PolymarketFetcher, KalshiFetcher
from agent.arbitrage import detect_arbitrage, EVENT_MAP
from agent.risk_manager import RiskManager
from agent.wallet import WalletManager, TradeOrder

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s"
)
logger = logging.getLogger(__name__)


async def run_scan_cycle(
    poly: PolymarketFetcher,
    kalshi: KalshiFetcher,
    risk: RiskManager,
    wallet: WalletManager,
) -> None:
    """Run one scan cycle across all mapped events."""

    logger.info("--- Starting scan cycle ---")

    poly_markets_raw = await poly.get_markets(limit=100)
    kalshi_markets_raw = await kalshi.get_markets(limit=100)

    poly_markets = {}
    for raw in poly_markets_raw:
        snapshot = poly.parse_market(raw)
        if snapshot:
            poly_markets[snapshot.market_id] = snapshot

    kalshi_markets = {}
    for raw in kalshi_markets_raw:
        snapshot = kalshi.parse_market(raw)
        if snapshot:
            kalshi_markets[snapshot.market_id] = snapshot

    logger.info(
        f'Fetched {len(poly_markets)} Polymarket, '
        f'{len(kalshi_markets)} Kalshi markets'
    )

    opportunities_found = 0
    for poly_id, kalshi_ticker in EVENT_MAP.items():
        poly_snap = poly_markets.get(poly_id)
        kalshi_snap = kalshi_markets.get(kalshi_ticker)

        if not poly_snap or not kalshi_snap:
            continue

        opp = detect_arbitrage(poly_snap, kalshi_snap)
        if opp is None:
            continue

        opportunities_found += 1
        logger.info(
            f'ARB DETECTED: {opp.event_name} | '
            f'Buy YES@{opp.buy_yes_on} ${opp.yes_price:.4f} + '
            f'Buy NO@{opp.buy_no_on} ${opp.no_price:.4f} | '
            f'Cost: ${opp.total_cost:.4f} | '
            f'Net spread: ${opp.net_spread:.4f} | '
            f'ROI: {opp.estimated_roi:.2%}'
        )

        allowed, reason = risk.can_trade(opp)
        if not allowed:
            logger.info(f'Trade blocked by risk manager: {reason}')
            continue

        position_size = min(
            config.MAX_POSITION_USD,
            config.SESSION_BUDGET_USD - risk.state.session_spent,
        )

        yes_order = TradeOrder(
            platform=opp.buy_yes_on,
            market_id=(
                poly_id if opp.buy_yes_on == "polymarket"
                else kalshi_ticker
            ),
            side="YES",
            amount_usd=position_size * opp.yes_price,
            max_price=opp.yes_price * 1.005,
        )
        no_order = TradeOrder(
            platform=opp.buy_no_on,
            market_id=(
                kalshi_ticker if opp.buy_no_on == "kalshi"
                else poly_id
            ),
            side="NO",
            amount_usd=position_size * opp.no_price,
            max_price=opp.no_price * 1.005,
        )

        res_a, res_b = await wallet.execute_arb_pair(
            yes_order, no_order
        )
        if res_a.success and res_b.success:
            trade_cost = (
                (res_a.filled_amount or 0) +
                (res_b.filled_amount or 0)
            )
            risk.record_trade(trade_cost)
            logger.info(f'Trade executed. Cost: ${trade_cost:.2f}')

    logger.info(
        f'Scan complete. Found {opportunities_found} opportunities. '
        f'Session spent: ${risk.state.session_spent:.2f}'
    )


async def main():
    """Main agent loop."""
    logger.info("=" * 60)
    logger.info("AgentBets.ai Arbitrage Agent Starting")
    logger.info("Mode: PAPER TRADING")
    logger.info("=" * 60)

    poly = PolymarketFetcher()
    kalshi = KalshiFetcher()
    risk = RiskManager()
    wallet = WalletManager(paper_mode=True)

    while not risk.state.is_killed:
        try:
            await run_scan_cycle(poly, kalshi, risk, wallet)
        except Exception as e:
            logger.error(f'Scan cycle error: {e}')

        logger.info(
            f'Sleeping {config.POLL_INTERVAL_SEC}s until next scan...'
        )
        await asyncio.sleep(config.POLL_INTERVAL_SEC)

    logger.info("Agent shut down.")


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

Run the agent with: python -m agent.main

Disclaimer: The agent starts in paper-trading mode by default. It will log detected opportunities and simulated trades but will NOT execute real transactions. Do not set paper_mode=False until you have thoroughly tested and reviewed the security checklist.

Step 7: Testing Your Agent

Never trust code that has not been tested — especially when that code handles money. The How to Verify Prediction Market Bot Performance guide covers backtesting standards and third-party audits.

# tests/test_arbitrage.py
"""Unit tests for the arbitrage detection engine."""

import pytest
from agent.data_fetcher import MarketSnapshot
from agent.arbitrage import detect_arbitrage


def make_snapshot(platform, yes, no, liq=1000.0):
    return MarketSnapshot(
        platform=platform,
        event_slug="Test Event",
        market_id="test-123",
        yes_price=yes,
        no_price=no,
        yes_liquidity=liq,
        no_liquidity=liq,
        last_updated="2026-03-05T00:00:00Z",
    )


def test_detects_clear_arb():
    """When combined cost < 1.0 minus fees, arb should be detected."""
    a = make_snapshot("polymarket", yes=0.40, no=0.62)
    b = make_snapshot("kalshi", yes=0.55, no=0.48)
    opp = detect_arbitrage(a, b)
    assert opp is not None
    assert opp.estimated_roi > 0.03


def test_no_arb_when_efficient():
    """When prices are efficient, no arb should be detected."""
    a = make_snapshot("polymarket", yes=0.52, no=0.49)
    b = make_snapshot("kalshi", yes=0.51, no=0.50)
    opp = detect_arbitrage(a, b)
    assert opp is None


def test_no_arb_low_liquidity():
    """Arb should be rejected if liquidity is below threshold."""
    a = make_snapshot("polymarket", yes=0.40, no=0.62, liq=100)
    b = make_snapshot("kalshi", yes=0.55, no=0.48, liq=100)
    opp = detect_arbitrage(a, b)
    assert opp is None


def test_chooses_best_direction():
    """Agent should pick the more profitable direction."""
    a = make_snapshot("polymarket", yes=0.35, no=0.68)
    b = make_snapshot("kalshi", yes=0.60, no=0.42)
    opp = detect_arbitrage(a, b)
    assert opp is not None
    assert opp.buy_yes_on == "polymarket"
    assert opp.buy_no_on == "kalshi"

Run tests with: pytest tests/ -v

The Deployment Ladder

Follow this progression. Do not skip steps.

  1. Unit tests — Validate arbitrage detection logic with known inputs (you just did this).
  2. Paper trading — Run the agent in paper mode against live market data. Verify it detects real opportunities and the logging is correct.
  3. Testnet — If the platform offers a testnet or sandbox (Kalshi has one), run with real API calls but fake money.
  4. Live micro-stakes — Deploy with $20–50 of real capital. Focus on execution correctness, not profit.
  5. Live production — Gradually increase capital as you gain confidence in the agent’s behavior.

Disclaimer: Most agents that lose money do so because the builder skipped testing stages. The gap between paper trading and live trading is enormous: slippage, API rate limits, network congestion, and execution latency all behave differently with real money.

Where to Go from Here

The agent you have built is a foundation. Here is how to evolve it into a production system.

Upgrade your intelligence layer. Replace the simple spread threshold with more sophisticated decision-making. The Agent Intelligence Guide covers LLM-powered analysis, sentiment scoring, and Bayesian probability updates. For OSINT-powered signal processing, see the OSINT Intelligence Guide. You can also add NLP-based event matching to replace the manual EVENT_MAP dictionary.

Expand to sportsbooks. The most profitable arbitrage opportunities often exist between prediction markets and traditional sportsbooks. The Sports Betting vs Prediction Markets guide explains how odds translate between the two. The Offshore Sportsbook API guide covers technical integration with sportsbook data feeds.

Monetize your agent. Once your agent has a proven track record, you can sell, rent, or license it on the AgentBets marketplace. The How to Sell Your Prediction Market Bot guide covers pricing strategies and licensing models. The Revenue-Sharing Models guide breaks down subscription, per-trade fees, and rev-share arrangements.

Browse existing agents. Before building everything yourself, check what is already available on the AgentBets Marketplace. See the Best Arbitrage Bots 2026 ranking for a head-to-head comparison.

If you are looking for a guide focused on building agents designed for commercial sale rather than personal use, see How to Build a Prediction Market Agent People Will Pay For.