← Back to Dashboard

Polymarket API Reference

Version: 3.1 | Last Updated: April 27, 2026 (gamma volume24hr regression note + 2-pass collector + /api/integrity) | Status: ✅ VERIFIED WORKING

What changed in v3.1 (Apr 27, 2026): - Gamma volume24hr field is intermittently NULL (~50% null rate per live probe). Long-term volume fields (volume, volume1wk, volume1mo, volume1yr) remain reliable. See §1 + Finding #272. - Collector now runs 2 passes (active=true&archived=false AND closed=true&archived=false) to capture resolved-but-not-archived markets that the single-pass approach missed (Finding #229). - Markets-table status field now reflects Polymarket's closed flag, not active (Finding #229). - New endpoint /api/integrity (§9) for live integrity-monitor surface (replaces v2/v3 watchdogs Apr 27). - New endpoint /api/architecture-findings for dashboard Testing Findings tab.


Table of Contents

  1. Critical Implementation Notes
  2. Gamma API
  3. CLOB API
  4. Polymarket data-api (was: Dome API; Dome was acquired by Polymarket and EOL'd Apr 28, 2026 — Findings #287/#288/#289)
  5. Database Mapping
  6. Rate Limits
  7. Common Pitfalls
  8. Code Examples

1. Critical Implementation Notes

🔴 Data Source Selection (CRITICAL)

Data Type Correct Source Wrong Source / Notes
Live Prices Gamma API outcomePrices ~~CLOB API /book~~
Conviction/Probability Gamma API outcomePrices ~~Orderbook midpoint~~
Cumulative Volume (gate) max(markets.volume_total, Gamma "volume") Trader's SIGNAL_MIN_VOLUME_TOTAL=$1,000 gate (Finding #311 lowered it from $2k) uses this via effective_cumulative_volume_usd() shared module. Apr 30, 2026 (Finding #282) changed semantic from "DB else Gamma" → max(DB, Gamma) because once-per-15-min markets.volume_total sync became stale within seconds for active markets, silently rejecting tradeable signals (e.g., MRNA #2026-05-01 at $6.28 < $2k while live Gamma was $3,734).
24h Volume (display only) price_snapshots.volume_24h (DB) ⚠️ Gamma's volume24hr field is intermittently NULL (~50% null rate per Apr 27 live probe — Finding #272). Use price_snapshots.volume_24h (collector captures it on every cycle when Gamma briefly returns it) as fallback. Do NOT use 24h volume for the trader's volume gate.
Orderbook Depth CLOB API /book N/A
Historical Trades Polymarket data-api /trades?market=<cid> (Findings #287/#288 — formerly Dome /orders until Apr 28, 2026 EOL) N/A
Historical Prices CLOB /prices-history?market=<token>&startTs=...&endTs=... (Findings #287/#289 — formerly Dome /market-price; rate limit 1,000 req/10s) ~~Candlesticks~~
Equity 1m bars Polygon /v2/aggs/ticker/.../range/1/minute/... Cached in equity_bars_1m hypertable (Finding #240, NEW Apr 26) — read via src/earnings/equity_bar_cache.py:fetch_minute_bars_cached().

🔴 JSON String Fields (CRITICAL)

The following Gamma API fields are JSON STRINGS, not arrays:

# These fields need json.loads() before use!
- outcomePrices: '["0.75", "0.25"]'  # NOT ["0.75", "0.25"]
- clobTokenIds: '["123...", "456..."]'
- outcomes: '["Yes", "No"]'

# Correct parsing:
import json
outcome_prices = market.get('outcomePrices', '[]')
if isinstance(outcome_prices, str):
    outcome_prices = json.loads(outcome_prices)
yes_price = float(outcome_prices[0]) if outcome_prices else 0.5

🔴 API Parameter Names (CRITICAL)

Parameter Correct Wrong
Filter by condition ID condition_ids (plural, snake_case) ~~conditionId~~
Filter closed closed=true N/A
Filter active active=true N/A

2. Gamma API

Base URL: https://gamma-api.polymarket.com

Endpoints

Endpoint Purpose Key Fields
/markets List/search markets conditionId, outcomePrices, volume
/events List events (multi-market) markets array
/tags Get available tags For filtering

Get Market Data

import requests
import json

GAMMA_URL = 'https://gamma-api.polymarket.com'

def get_market_by_condition_id(condition_id: str) -> dict:
    """Fetch market data from Gamma API."""
    resp = requests.get(
        f'{GAMMA_URL}/markets',
        params={'condition_ids': condition_id}  # NOTE: plural!
    )
    resp.raise_for_status()
    markets = resp.json()

    if not markets:
        return None

    market = markets[0]

    # Parse JSON string fields
    outcome_prices = market.get('outcomePrices', '[]')
    if isinstance(outcome_prices, str):
        outcome_prices = json.loads(outcome_prices)

    clob_token_ids = market.get('clobTokenIds', '[]')
    if isinstance(clob_token_ids, str):
        clob_token_ids = json.loads(clob_token_ids)

    return {
        'condition_id': market.get('conditionId'),
        'question': market.get('question'),
        'yes_price': float(outcome_prices[0]) if outcome_prices else 0.5,
        'no_price': float(outcome_prices[1]) if len(outcome_prices) > 1 else 0.5,
        'volume': market.get('volume', 0),
        'token_id_yes': clob_token_ids[0] if clob_token_ids else None,
        'token_id_no': clob_token_ids[1] if len(clob_token_ids) > 1 else None,
        'active': market.get('active', False),
        'closed': market.get('closed', False),
    }

Find Earnings Markets — 2-pass collector pattern (Finding #229, Apr 25, 2026)

def get_all_earnings_markets() -> list:
    """
    Find all earnings beat markets.

    NOTE: tag=Earnings filter is unreliable!
    Must search ALL markets and filter by question.

    CRITICAL: As of Apr 25, 2026 (Finding #229), Polymarket's
    `?active=true&archived=false` filter does NOT include markets in the
    `closed=true active=true` state — settled-but-not-archived earnings
    markets that sit there for days/weeks before being archived. We must
    run TWO passes and de-dupe by condition_id, otherwise every recently-
    resolved earnings market loses its final volume_total update on the
    day it resolves.
    """
    markets_by_cid = {}

    for params in [
        {'tag_id': 1013, 'limit': 500, 'active': 'true', 'archived': 'false'},
        {'tag_id': 1013, 'limit': 500, 'closed': 'true', 'archived': 'false'},  # ← critical
    ]:
        offset = 0
        while True:
            resp = requests.get(
                f'{GAMMA_URL}/events/pagination',
                params={**params, 'offset': offset},
            )
            resp.raise_for_status()
            data = resp.json()
            events = data.get('data', []) if isinstance(data, dict) else (data or [])
            if not events:
                break
            for event in events:
                for m in event.get('markets', []):
                    cid = m.get('conditionId') or m.get('condition_id')
                    if not cid:
                        continue
                    # Closed-pass wins (latest volume settlement) since we process
                    # active first, closed second.
                    markets_by_cid[cid] = m
            offset += 500

    return list(markets_by_cid.values())

Status mapping (production code in continuous_collector.sync_markets_from_gamma):

# Status reflects Polymarket's `closed` flag (settled), NOT the `active` flag
# (which can be True even after settlement). Finding #229.
'status': 'closed' if m.get('closed') else 'open'

Market Response Structure

{
  "conditionId": "0x13c8546e...",
  "question": "Will NVIDIA (NVDA) beat quarterly earnings?",
  "outcomePrices": "[\"0.75\", \"0.25\"]",  // JSON STRING!
  "clobTokenIds": "[\"35741...\", \"19559...\"]",  // JSON STRING!
  "outcomes": "[\"Yes\", \"No\"]",  // JSON STRING!
  "volume": 12345.67,
  "liquidity": 5000.0,
  "active": true,
  "closed": false,
  "tags": ["Finance", "Earnings"]
}

3. CLOB API

Base URL: https://clob.polymarket.com

⚠️ WARNING: Use Only for Orderbook Depth!

The CLOB API /book endpoint returns order levels, NOT market prices!

Endpoints

Endpoint Purpose Use For
/book Orderbook depth Liquidity analysis ONLY
/price Current price Backup only
/midpoint Midpoint price Backup only

Get Orderbook Depth

CLOB_URL = 'https://clob.polymarket.com'

def get_orderbook_depth(token_id: str) -> dict:
    """
    Get orderbook for liquidity analysis.

    WARNING: Do NOT use bids/asks midpoint as the market price!
    """
    resp = requests.get(
        f'{CLOB_URL}/book',
        params={'token_id': token_id}
    )
    resp.raise_for_status()
    orderbook = resp.json()

    bids = orderbook.get('bids', [])
    asks = orderbook.get('asks', [])

    # Calculate depth (for liquidity scoring)
    bid_depth = sum(float(b['size']) for b in bids[:5])
    ask_depth = sum(float(a['size']) for a in asks[:5])

    return {
        'bids': bids,
        'asks': asks,
        'bid_depth': bid_depth,
        'ask_depth': ask_depth,
        'spread': float(asks[0]['price']) - float(bids[0]['price']) if bids and asks else None
    }

4. Polymarket data-api

Base URL: https://data-api.polymarket.com

Migration note (Findings #287/#288/#289, May 10, 2026): the previous "Dome API" section described https://api.domeapi.io/v1. Dome was acquired by Polymarket and all Dome APIs reached end-of-life on April 28, 2026. All consumers (backfill_trades.py, live_trades_collector.py) now hit the new Polymarket data-api. No API key required (public endpoints).

Rate Limits (per Polymarket rate-limits doc)

Endpoint Limit vs Dome free tier
/trades 200 req / 10s = 20 q/s sustained 20× faster
/positions, /closed-positions 150 req / 10s n/a
General data-api 1,000 req / 10s n/a

Endpoints we use

Endpoint Purpose Used by
/trades?market=<cid>&limit=N&offset=M Historical trades for a market backfill_trades.py (Finding #288 smart Option A)
/trades?asset_id=<token>&limit=N Recent trades for a token live_trades_collector.py (#289 cosmetic rename)
/positions / /closed-positions User positions not actively used by earnings strategy
/activity User actions (REDEEM/SPLIT/MERGE) not actively used

Companion CLOB API endpoints

For historical prices (replaces Dome /market-price):

Endpoint Purpose Rate limit
clob.polymarket.com/prices-history ?market=<token>&startTs=N&endTs=N&interval=1m\|1h\|1d&fidelity=N 1,000 req/10s
clob.polymarket.com/batch-prices-history POST body — up to 20 markets per request 1,000 req/10s

Historical orderbook: NO direct replacement after Dome /orderbooks EOL. Live capture continues via WSS wss://ws-subscriptions-clob.polymarket.com/ws/market (via polymarket-websocket.service) and polymarket-collector.service.

Trade record shape (data-api /trades)

Top-level array (NOT wrapped in orders like Dome was). Each record:

{
  "proxyWallet": "0x...",
  "side": "BUY|SELL",
  "asset": "<token_id_int>",
  "conditionId": "0x...",
  "size": 5.03,
  "price": 0.999,
  "timestamp": 1768102036,
  "title": "Bitcoin Up or Down — May 9, 4:05PM-4:10PM ET",
  "slug": "btc-updown-...",
  "outcome": "Down",
  "transactionHash": "0x..."
}

Field migration vs old Dome /orders shape: | Old Dome field | New PM data-api field | |---|---| | tx_hash | transactionHash (camelCase) | | user | proxyWallet | | condition_id | conditionId (camelCase) | | shares_normalized | size (already normalized) | | token_label | outcome | | order_hash | (NOT in PM response — drop) | | taker | (NOT in PM response — takerOnly=true default) |

Get Historical Trades (Polymarket replacement)

import requests

DATA_API = 'https://data-api.polymarket.com'

def get_market_trades(condition_id: str, offset: int = 0, limit: int = 500) -> list:
    """Get historical trades for a market via Polymarket data-api.

    No auth required. Newest-first ordering. Pagination via offset (max 10,000).
    Free tier: 200 req/10s = 20 q/s sustained.
    """
    resp = requests.get(
        f'{DATA_API}/trades',
        params={
            'market': condition_id,
            'limit': limit,
            'offset': offset,
            'takerOnly': 'true',  # OpenAPI default
        },
        timeout=30,
    )
    resp.raise_for_status()
    return resp.json()  # top-level array; NOT wrapped

For the production-grade implementation with smart Option A targeting (T1 delta, T2 smart re-fetch, T3 earnings-only scope, circuit breaker, CLI flags), see src/data_tracker/collectors/backfill_trades.py (Findings #269 + #288).


5. Database Mapping

Gamma API → Database

Gamma API Field DB Table DB Column Notes
conditionId markets condition_id Primary identifier
clobTokenIds[0] markets token_id_yes For CLOB queries
clobTokenIds[1] markets token_id_no For CLOB queries
question markets question Market question
outcomePrices[0] price_snapshots yes_price The conviction!
outcomePrices[1] price_snapshots no_price 1 - yes_price
volume markets / price_snapshots volume_total / volume_24h Trading volume
active markets status 'open' or 'closed'

Price Snapshots Format

price_snapshots:
  time: TIMESTAMP WITH TIME ZONE  -- UTC
  condition_id: VARCHAR(66)       -- Hex string (0x...)
  yes_price: DECIMAL(10,4)        -- 0.0000 to 1.0000
  no_price: DECIMAL(10,4)         -- 1 - yes_price
  volume_24h: DECIMAL(20,2)       -- USD
  liquidity: DECIMAL(20,2)        -- USD

Collection Frequency

Collector Frequency Data Source
continuous_collector.py (polymarket-collector.service) Every ~1s loop Live trades, prices, orderbook gamma + clob (py-clob-client SDK)
live_trades_collector.py (polymarket-live-trades.service) Adaptive 10/30/120s per market Live trades w/ wallet + tx detail data-api /trades?asset_id=
websocket_collector.py (polymarket-websocket.service) Streaming Live orderbook + price changes WSS ws-subscriptions-clob market channel
backfill_trades.py (polymarket-trades-backfill.timer) Weekly Sun 06:00 ET Historical trades (T1 delta) data-api /trades?market= (Finding #288 — formerly Dome)
gamma_collector.py Periodic Market metadata Gamma /markets + /events
polymarket-data-backfill.timer (Sun 04:00 ET) Weekly Volume + conviction self-heal gamma + data-api /trades (#231)
polymarket-resolver.service (daily 06:00 ET) Daily actual_beat outcome resolver (4-tier) gamma + Polygon + Finnhub
polymarket-equity-bars.service (daily 10:00 UTC) Daily Equity 1m bars for upcoming-earnings symbols Polygon /v2/aggs

6. Rate Limits

Gamma API

Endpoint Limit
General 4,000 req/10s
/markets 300 req/10s

CLOB API

Tiered based on account level.

Polymarket data-api (Free Tier — replaces Dome free tier)

Per https://docs.polymarket.com/api-reference/rate-limits.md (verified May 10, 2026):

Endpoint Limit
/trades 200 req / 10s = 20 q/s sustained (was: Dome 1 q/s — 20× faster)
/positions / /closed-positions 150 req / 10s
General data-api 1,000 req / 10s

Backfill Estimates (post-#288 migration): - ~929 earnings markets × ~10 calls each cold = ~9,000 requests / ~8 minutes (vs ~3 hours on Dome) - Warm weekly delta: ~900 calls / ~1-2 minutes (T1 delta filter trims most pages)


7. Common Pitfalls

❌ Pitfall 1: Using CLOB for Prices

# WRONG - CLOB bids/asks are ORDER LEVELS, not the price!
resp = requests.get(f'{CLOB_URL}/book?token_id={token_id}')
orderbook = resp.json()
price = (float(orderbook['bids'][0]['price']) + float(orderbook['asks'][0]['price'])) / 2

# CORRECT - Use Gamma API outcomePrices
resp = requests.get(f'{GAMMA_URL}/markets', params={'condition_ids': condition_id})
market = resp.json()[0]
outcome_prices = json.loads(market['outcomePrices'])
price = float(outcome_prices[0])

❌ Pitfall 2: Wrong Parameter Name

# WRONG - Uses singular camelCase
resp = requests.get(url, params={'conditionId': cid})

# CORRECT - Uses plural snake_case
resp = requests.get(url, params={'condition_ids': cid})

❌ Pitfall 3: Not Parsing JSON Strings

# WRONG - Treats as array directly
outcome_prices = market['outcomePrices']
yes_price = float(outcome_prices[0])  # ERROR!

# CORRECT - Parse JSON string first
outcome_prices = market.get('outcomePrices', '[]')
if isinstance(outcome_prices, str):
    outcome_prices = json.loads(outcome_prices)
yes_price = float(outcome_prices[0]) if outcome_prices else 0.5

❌ Pitfall 4: Relying on Tags

# WRONG - tag=Earnings misses many markets!
resp = requests.get(url, params={'tag': 'Earnings'})

# CORRECT - Search by question pattern
for m in all_markets:
    if 'beat' in m['question'].lower() and 'earnings' in m['question'].lower():
        earnings_markets.append(m)

❌ Pitfall 5: Not Filtering by Status

# WRONG - Includes resolved markets
all_markets = fetch_all_markets()

# CORRECT - Filter by open status
for m in all_markets:
    if m.get('closed') == False or m.get('active') == True:
        open_markets.append(m)

8. Code Examples

Complete Market Data Fetcher

import requests
import json
from typing import Optional, Dict, List

GAMMA_URL = 'https://gamma-api.polymarket.com'
CLOB_URL = 'https://clob.polymarket.com'

class PolymarketClient:
    """
    Polymarket API client with correct data source usage.
    """

    def get_market_price(self, condition_id: str) -> Optional[Dict]:
        """
        Get current price from Gamma API (CORRECT source).
        """
        resp = requests.get(
            f'{GAMMA_URL}/markets',
            params={'condition_ids': condition_id}
        )
        resp.raise_for_status()
        markets = resp.json()

        if not markets:
            return None

        market = markets[0]

        # Parse JSON string fields
        outcome_prices = market.get('outcomePrices', '[]')
        if isinstance(outcome_prices, str):
            outcome_prices = json.loads(outcome_prices)

        return {
            'condition_id': market.get('conditionId'),
            'yes_price': float(outcome_prices[0]) if outcome_prices else 0.5,
            'no_price': float(outcome_prices[1]) if len(outcome_prices) > 1 else 0.5,
            'volume': market.get('volume', 0),
            'question': market.get('question'),
        }

    def get_orderbook_depth(self, token_id: str) -> Optional[Dict]:
        """
        Get orderbook for DEPTH analysis only (NOT price!).
        """
        resp = requests.get(
            f'{CLOB_URL}/book',
            params={'token_id': token_id}
        )
        resp.raise_for_status()
        orderbook = resp.json()

        bids = orderbook.get('bids', [])
        asks = orderbook.get('asks', [])

        return {
            'bid_depth': sum(float(b['size']) for b in bids[:5]),
            'ask_depth': sum(float(a['size']) for a in asks[:5]),
            'spread': float(asks[0]['price']) - float(bids[0]['price']) if bids and asks else None,
        }

    def find_earnings_markets(self) -> List[Dict]:
        """
        Find all open earnings beat markets.

        NOTE: This single-pass example is illustrative only. Production
        collection MUST use the 2-pass pattern (active + closed, de-duped by
        condition_id) shown in §2 "Find Earnings Markets — 2-pass collector
        pattern" (Finding #229), otherwise settled-but-not-archived markets
        lose their final volume_total update on the day they resolve.
        """
        earnings = []
        offset = 0

        while True:
            resp = requests.get(
                f'{GAMMA_URL}/markets',
                params={'closed': 'false', 'limit': 500, 'offset': offset}
            )
            resp.raise_for_status()
            markets = resp.json()

            if not markets:
                break

            for m in markets:
                question = m.get('question', '').lower()
                if 'beat' in question and 'earnings' in question:
                    earnings.append(self.get_market_price(m['conditionId']))

            offset += 500

        return [e for e in earnings if e is not None]

9. Dashboard API Endpoints

The earnings dashboard exposes the following REST endpoints:

Core Endpoints

Endpoint Method Description
/api/health GET Health check
/api/state GET Full dashboard state (mode, capital, positions, etc.)
/api/signals GET Current BMO/AMC trading signals
/api/positions GET Active positions from database
/api/earnings-calendar GET Upcoming earnings with Polymarket data
/api/db-stats GET Database statistics
/api/conviction-feed GET Recent conviction changes

Trading Service Endpoints

Endpoint Method Description
/api/trader-status GET Earnings trader service status (running, mode, last activity)
/api/ibkr-status GET IBKR gateway connection status
/api/testing-prod-status GET TESTING_PROD mode progress
/api/slippage-stats GET Execution quality statistics

Integrity & Findings Endpoints (NEW Apr 27, 2026)

Endpoint Method Description
/api/integrity GET Live integrity-monitor snapshot. Returns {summary: {total, ok, warn, fail, unknown}, checks: [...]} for the ~47 declarative rules in src/data_tracker/integrity_monitor.py (history: 37 mid-May 2026, 34 pre-#269/#291). JSON-snapshot-first (5-min freshness), DB fallback. Phase 4 of Database Integrity Audit (Finding #259).
/api/architecture-findings GET Renders docs/FINDINGS.md for the dashboard's Testing Findings tab (per memory.md §J — single source of truth).

Control Endpoints

Removed (Finding #467.A, Sep 26, 2026): /api/set-trading-mode, /api/testing-prod/confirm, /api/testing-prod/reset (all POST). Each was already a no-op — get_orchestrator() always returns None (the trader runs as its own systemd service; the dashboard is DB/file read-only) — but they were still unauthenticated write endpoints on the public dashboard. Change mode via docs/OPERATIONS.md "Change Trading Mode" (edit trading_mode.txt + restart the trader service).

Example: Get Dashboard State

import requests

resp = requests.get('http://your-server:8080/api/state')
state = resp.json()

print(f"Mode: {state['mode']}")
print(f"Capital: ${state['capital']:,.2f}")
print(f"Active Positions: {state['active_positions']}")
print(f"IBKR Connected: {state['ibkr_connected']}")

Example: Get Trader Status

resp = requests.get('http://your-server:8080/api/trader-status')
status = resp.json()

print(f"Service: {status['status']}")  # 'running' or 'stopped'
print(f"Mode: {status['mode']}")
print(f"Last Activity: {status['last_activity']}")

Validation Checklist

Before deploying, verify:


This document consolidates: POLYMARKET_API_REFERENCE.md, data_tracker_spec.md

Last updated: April 27, 2026 (v3.1 — Gamma volume24hr regression note + 2-pass collector + /api/integrity)