Skip to main content

The Opportunity

The same prediction market often exists on multiple venues (Polymarket, Kalshi) with different prices. These discrepancies create arbitrage opportunities. Market Motion helps you:
  1. Discover markets across venues
  2. Match equivalent markets via entity connections
  3. Compare prices programmatically

Finding Cross-Venue Markets

async function findCrossVenueMarkets(query) {
  const [polymarket, kalshi] = await Promise.all([
    fetch(`/api/markets/search?q=${query}&venue=polymarket`),
    fetch(`/api/markets/search?q=${query}&venue=kalshi`)
  ]);

  return {
    polymarket: await polymarket.json(),
    kalshi: await kalshi.json()
  };
}

const markets = await findCrossVenueMarkets('chiefs super bowl');

By Entity

More reliable—same entity links to markets on all venues:
async function getEntityMarkets(entitySlug) {
  const response = await fetch(`/api/entities/${entitySlug}`);
  const { entity } = await response.json();

  // Group markets by venue
  const byVenue = {};
  for (const exposure of entity.marketExposures || []) {
    const venue = exposure.market.venue;
    if (!byVenue[venue]) byVenue[venue] = [];
    byVenue[venue].push(exposure.market);
  }

  return byVenue;
}

const markets = await getEntityMarkets('kansas-city-chiefs');
// { polymarket: [...], kalshi: [...] }

Price Comparison

import requests
from dataclasses import dataclass

@dataclass
class ArbitrageOpportunity:
    entity: str
    market_title: str
    polymarket_price: float
    kalshi_price: float
    spread: float

def find_arbitrage(entity_slug: str, threshold: float = 0.03):
    """Find arbitrage opportunities for an entity's markets."""

    response = requests.get(f"{BASE_URL}/entities/{entity_slug}")
    entity = response.json()["entity"]

    # Group market exposures by similar titles
    markets_by_title = {}
    for exposure in entity.get("marketExposures", []):
        market = exposure["market"]
        # Normalize title for matching
        key = market["title"].lower().strip()
        if key not in markets_by_title:
            markets_by_title[key] = {}
        markets_by_title[key][market["venue"]] = market

    opportunities = []

    for title, venues in markets_by_title.items():
        if "polymarket" in venues and "kalshi" in venues:
            poly_price = venues["polymarket"].get("price", 0)
            kalshi_price = venues["kalshi"].get("price", 0)
            spread = abs(poly_price - kalshi_price)

            if spread > threshold:
                opportunities.append(ArbitrageOpportunity(
                    entity=entity["displayName"],
                    market_title=title,
                    polymarket_price=poly_price,
                    kalshi_price=kalshi_price,
                    spread=spread
                ))

    return opportunities

Systematic Scanning

def scan_category_for_arbitrage(category: str):
    """Scan all entities in a category for arbitrage opportunities."""

    # Get all entities in category
    response = requests.get(
        f"{BASE_URL}/entities",
        params={"category": category, "limit": 100}
    )
    entities = response.json()["items"]

    all_opportunities = []

    for entity in entities:
        opps = find_arbitrage(entity["slug"])
        all_opportunities.extend(opps)

    # Sort by spread size
    all_opportunities.sort(key=lambda x: x.spread, reverse=True)

    return all_opportunities

# Find all sports arbitrage opportunities
opps = scan_category_for_arbitrage("sports")
for opp in opps[:10]:
    print(f"{opp.market_title}")
    print(f"  Polymarket: {opp.polymarket_price:.2%}")
    print(f"  Kalshi: {opp.kalshi_price:.2%}")
    print(f"  Spread: {opp.spread:.2%}")

Market Matching Challenges

Markets on different venues may have:
  • Slightly different titles
  • Different resolution criteria
  • Different expiration times
Always verify markets are truly equivalent before trading.

Matching Strategies

StrategyProsCons
Title similaritySimpleFalse matches possible
Entity-basedReliable entity matchMarkets may differ in scope
Manual verificationMost accurateDoesn’t scale
def are_markets_equivalent(market_a, market_b):
    """Check if two markets are truly equivalent for arbitrage."""

    # Same entity exposure
    if market_a.get("entity_slug") != market_b.get("entity_slug"):
        return False

    # Similar expiration (within 24 hours)
    exp_a = parse_date(market_a.get("endDate"))
    exp_b = parse_date(market_b.get("endDate"))
    if abs((exp_a - exp_b).days) > 1:
        return False

    # Title similarity > 80%
    similarity = calculate_similarity(
        market_a["title"].lower(),
        market_b["title"].lower()
    )
    if similarity < 0.8:
        return False

    return True

Execution Considerations

Market Motion is for discovery and context, not execution. For arbitrage:
1

Discover with Market Motion

Find cross-venue opportunities using entity-market links
2

Verify manually or programmatically

Confirm markets are truly equivalent
3

Execute on venue APIs

Use Polymarket and Kalshi APIs directly for trading
4

Monitor with Market Motion

Track entity attributes for signals to close positions

Dedicated Arbitrage Endpoints

For systematic arbitrage, use the dedicated endpoints instead of manual searching:

Real-Time Considerations

Market Motion caches market data. For real-time arbitrage execution, use venue APIs directly for price quotes. Use Market Motion for:
  • Discovering which markets to monitor
  • Understanding entity relationships
  • Tracking attribute changes that affect prices