DEV Community

Cover image for Polymarket Market Scanner in Python: Build One
Bo$onaX
Bo$onaX

Posted on

Polymarket Market Scanner in Python: Build One

Build a Polymarket Market Scanner in Python

A trading bot should not start by placing orders. It should first answer a simpler question:

Which Polymarket markets are worth looking at right now?

That is the job of a market scanner.

A useful Polymarket market scanner continuously turns a large universe of prediction markets into a smaller, structured candidate set based on conditions such as market status, liquidity, volume, expiration, price, category, and order-book availability.

Polymarket currently exposes public market data without authentication, including market discovery through the Gamma API and order-book/pricing data through the CLOB API. ([Polymarket Documentation][1])

In this tutorial, we will build a read-only Python scanner that discovers active markets, applies quantitative filters, and produces candidates that a later strategy or execution engine can evaluate.

Trading-risk disclaimer: A scanner identifies markets matching predefined conditions. It does not establish that a trade is profitable. Spread, liquidity, fees, slippage, latency, adverse selection, model error, and resolution risk still matter.

What You'll Learn

By the end, you will understand:

  • How Polymarket market discovery works
  • How to use the current keyset market endpoint
  • How to paginate through markets safely
  • How to filter markets quantitatively
  • How to separate discovery from price/order-book analysis
  • How to design a scanner that can evolve into a trading system
  • What changes are required for production deployment

1. Market Scanner Architecture

The important design decision is to separate market discovery from market evaluation.

A scanner should not download every order book every time it scans the entire market universe. That creates unnecessary network traffic and makes scaling harder.

A better architecture is:

flowchart LR
    A[Gamma Market Discovery] --> B[Market Universe]
    B --> C[Basic Filters]
    C --> D[Candidate Markets]
    D --> E[CLOB Price / Order Book]
    E --> F[Quantitative Filters]
    F --> G[Ranked Opportunities]
    G --> H[Strategy Engine]
    H --> I[Execution Engine]
Enter fullscreen mode Exit fullscreen mode

The Gamma API provides market discovery and metadata. The CLOB API provides market prices and order-book information. Polymarket documents these as separate parts of its market-data architecture. ([Polymarket Documentation][1])

That separation is useful because metadata changes much more slowly than order-book state.


2. Which API Should the Scanner Use?

For broad market discovery, the current documentation provides a keyset-paginated endpoint:

GET https://gamma-api.polymarket.com/markets/keyset
Enter fullscreen mode Exit fullscreen mode

It supports filters such as closed, liquidity ranges, volume ranges, date ranges, tags, and ordering. The endpoint uses an opaque next_cursor returned by one request as after_cursor for the next request. Its maximum limit is currently 100. ([Polymarket Documentation][2])

This is preferable for a large scanner to repeatedly requesting arbitrary offsets.

The CLOB API is then appropriate when the scanner needs order-book information such as bids, asks, midpoint, or prices. ([Polymarket Documentation][1])

For a read-only scanner, authentication is not required for public market data. ([Polymarket Documentation][1])


3. Project Setup

For a simple scanner, Python's standard HTTP tooling is enough.

python -m venv .venv

# Windows
.venv\Scripts\activate

# Linux/macOS
source .venv/bin/activate

pip install httpx
Enter fullscreen mode Exit fullscreen mode

A minimal project can look like:

polymarket-scanner/
├── scanner.py
├── requirements.txt
└── README.md
Enter fullscreen mode Exit fullscreen mode

requirements.txt:

httpx
Enter fullscreen mode Exit fullscreen mode

No private key or trading credential is necessary for this version.


4. Fetch Markets With Keyset Pagination

The first component is a market-discovery client.

from __future__ import annotations

import time
import httpx

GAMMA_URL = "https://gamma-api.polymarket.com/markets/keyset"


def fetch_markets(
    client: httpx.Client,
    limit: int = 100,
    max_pages: int = 5,
) -> list[dict]:
    markets: list[dict] = []
    cursor: str | None = None

    for _ in range(max_pages):
        params = {
            "limit": limit,
            "closed": "false",
            "ascending": "false",
        }

        if cursor:
            params["after_cursor"] = cursor

        response = client.get(GAMMA_URL, params=params)
        response.raise_for_status()

        payload = response.json()

        page = payload.get("markets", [])
        markets.extend(page)

        cursor = payload.get("next_cursor")

        if not cursor or not page:
            break

        time.sleep(0.05)

    return markets
Enter fullscreen mode Exit fullscreen mode

The key detail is that the cursor is treated as opaque data. Do not try to construct or interpret it yourself.

The official documentation specifically describes next_cursorafter_cursor pagination and rejects the offset parameter for this endpoint. ([Polymarket Documentation][2])


5. Add Basic Market Filters

The raw market universe is usually much larger than the universe a strategy actually needs.

For example, a scanner might require:

  • market is active
  • market is not closed
  • sufficient liquidity
  • sufficient volume
  • a known question
  • an acceptable expiration horizon
def filter_markets(
    markets: list[dict],
    min_liquidity: float = 10_000,
    min_volume: float = 25_000,
) -> list[dict]:

    candidates = []

    for market in markets:
        if market.get("closed"):
            continue

        if not market.get("active"):
            continue

        liquidity = float(market.get("liquidity") or 0)
        volume = float(market.get("volume") or 0)

        if liquidity < min_liquidity:
            continue

        if volume < min_volume:
            continue

        candidates.append(market)

    return candidates
Enter fullscreen mode Exit fullscreen mode

The numbers above are example configuration values, not claims about optimal thresholds.

A production scanner should make them configurable rather than hard-coding assumptions.

The market API exposes fields including liquidity, volume, question, outcome information, dates, and other metadata. ([Polymarket Documentation][3])


6. Parsing Outcome Prices

Polymarket's market data represents outcomes and outcome prices as corresponding arrays. For binary markets, the first outcome and first price can commonly represent the first outcome in that market's outcome ordering. ([Polymarket Documentation][1])

Because API representations can contain serialized JSON strings, parse them defensively:

import json


def parse_outcomes(market: dict) -> list[tuple[str, float]]:
    outcomes_raw = market.get("outcomes", "[]")
    prices_raw = market.get("outcomePrices", "[]")

    try:
        outcomes = json.loads(outcomes_raw)
        prices = json.loads(prices_raw)
    except (TypeError, json.JSONDecodeError):
        return []

    result = []

    for outcome, price in zip(outcomes, prices):
        try:
            result.append((outcome, float(price)))
        except (TypeError, ValueError):
            continue

    return result
Enter fullscreen mode Exit fullscreen mode

The scanner can now reason about price levels without making assumptions about the underlying market question.


7. A Complete Basic Scanner

Putting the components together:

from __future__ import annotations

import json
import logging
import httpx

GAMMA_URL = "https://gamma-api.polymarket.com/markets/keyset"

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)


def parse_outcomes(market: dict) -> list[tuple[str, float]]:
    try:
        outcomes = json.loads(market.get("outcomes", "[]"))
        prices = json.loads(market.get("outcomePrices", "[]"))
    except (TypeError, json.JSONDecodeError):
        return []

    result = []

    for outcome, price in zip(outcomes, prices):
        try:
            result.append((str(outcome), float(price)))
        except (TypeError, ValueError):
            pass

    return result


def scan_markets(
    min_liquidity: float = 10_000,
    min_volume: float = 25_000,
    max_pages: int = 5,
) -> list[dict]:

    candidates = []

    with httpx.Client(timeout=10.0) as client:
        cursor = None

        for _ in range(max_pages):
            params = {
                "limit": 100,
                "closed": "false",
                "ascending": "false",
            }

            if cursor:
                params["after_cursor"] = cursor

            try:
                response = client.get(GAMMA_URL, params=params)
                response.raise_for_status()
                payload = response.json()
            except httpx.HTTPError as exc:
                logging.error("Market request failed: %s", exc)
                break

            for market in payload.get("markets", []):
                if not market.get("active"):
                    continue

                liquidity = float(market.get("liquidity") or 0)
                volume = float(market.get("volume") or 0)

                if liquidity < min_liquidity:
                    continue

                if volume < min_volume:
                    continue

                candidates.append({
                    "id": market.get("id"),
                    "question": market.get("question"),
                    "slug": market.get("slug"),
                    "liquidity": liquidity,
                    "volume": volume,
                    "outcomes": parse_outcomes(market),
                })

            cursor = payload.get("next_cursor")

            if not cursor:
                break

    return candidates


if __name__ == "__main__":
    for market in scan_markets():
        print(
            f"{market['liquidity']:>12,.0f} "
            f"{market['volume']:>12,.0f} "
            f"{market['question']}"
        )
Enter fullscreen mode Exit fullscreen mode

This is deliberately read-only.

That is a useful engineering boundary: discovery should not have permission to trade.


8. Scanner → Order Book Evaluation

Metadata filtering is only the first stage.

Suppose 5,000 markets exist but only 100 satisfy your basic liquidity and volume criteria. There is little reason to request detailed order books for all 5,000.

Instead:

5,000 markets
     ↓
metadata filters
     ↓
100 candidates
     ↓
order-book queries
     ↓
20 liquid candidates
     ↓
strategy/model evaluation
Enter fullscreen mode Exit fullscreen mode

The CLOB provides endpoints for individual and batch order-book and price queries. ([Polymarket Documentation][1])

For a latency-sensitive scanner, the WebSocket market channel is another option. The documented market channel provides real-time order-book, price, and market-lifecycle updates. ([Polymarket Documentation][4])

This creates two different scanner modes:

Batch scanner

Useful for research, scheduled screening, dashboards, and periodic candidate discovery.

Streaming scanner

Useful when the system must react to changing market state rather than repeatedly polling.


9. Ranking Candidates

Filtering answers:

Does this market qualify?

Ranking answers:

Which qualifying markets deserve attention first?

A simple ranking function could combine normalized liquidity and volume:

def score_market(market: dict) -> float:
    liquidity = market["liquidity"]
    volume = market["volume"]

    return (liquidity ** 0.5) * (volume ** 0.5)
Enter fullscreen mode Exit fullscreen mode

This is not a trading signal. It is merely a prioritization mechanism.

A more sophisticated scanner could rank by:

  • bid/ask spread
  • displayed depth
  • recent volume
  • time to expiration
  • price distance from a model estimate
  • volatility
  • market category
  • event-level concentration
  • historical price movement

The crucial principle is to keep market selection separate from trade prediction.

A scanner should tell the strategy engine where to look, not secretly become the strategy itself.


10. Production Considerations

Rate limits

Polymarket documents rate limits across the Gamma, Data, and CLOB APIs. Current documentation lists Gamma /markets at 300 requests per 10 seconds and CLOB /book at 1,500 requests per 10 seconds, among other limits. ([Polymarket Documentation][5])

Do not build a scanner around the assumption that unlimited polling is acceptable.

Use:

  • bounded concurrency
  • batching where supported
  • caching
  • exponential backoff
  • incremental updates
  • WebSockets for continuously changing data

Rate-limit values can change, so production systems should treat the official rate-limit documentation as the source of truth.

Keyset pagination

Use the current keyset endpoint for large market scans rather than designing new code around offset pagination. Polymarket announced the keyset endpoints in April 2026 and subsequently documented a maximum limit of 100. ([Polymarket Documentation][6])

SDK selection

Polymarket now maintains a unified Python SDK, polymarket-client, which is currently described by the project as beta. ([GitHub][7])

The older py-clob-client repository is archived, while Polymarket's current Python SDK repository recommends the unified SDK for new projects. ([GitHub][8])

For a small scanner, direct documented HTTP calls can be perfectly reasonable. For a larger application, evaluate the current official SDK and its stability before committing your architecture to a particular interface.


11. Failure Modes and Common Mistakes

Scanning every order book

This wastes requests and increases latency.

Better: filter the universe first.

Using stale market assumptions

Market APIs evolve. Fields, endpoints, SDKs, and pagination behavior can change.

Better: verify production assumptions against the current documentation and changelog.

Treating liquidity as executable liquidity

A liquidity number does not tell you exactly how much size you can execute at your desired price.

Better: inspect the actual order book before making execution decisions.

Assuming the displayed probability is your edge

A market price is not automatically a trading opportunity.

Better: compare market state against an independently constructed model and account for execution costs.

Mixing discovery and execution

A scanner that can also immediately submit trades creates unnecessary operational risk.

Better: keep discovery, signal generation, risk management, and execution as separate components.


12. Performance Considerations

The first optimization should usually be reducing unnecessary requests, not micro-optimizing Python.

A sensible progression is:

  1. Fetch market metadata.
  2. Filter locally.
  3. Batch price/order-book requests when appropriate.
  4. Cache relatively static metadata.
  5. Move continuously changing data to WebSockets.
  6. Parallelize only within documented limits.
  7. Measure request latency and processing time.

For example, maintain two datasets:

market_metadata
    id
    question
    category
    liquidity
    volume
    end_date

market_state
    best_bid
    best_ask
    midpoint
    depth
    timestamp
Enter fullscreen mode Exit fullscreen mode

This prevents the scanner from repeatedly rebuilding static information.


13. Security

A market scanner does not need a private key.

Keep it that way.

If the scanner eventually connects to a trading engine:

  • keep private keys outside source code
  • use environment variables or a secret manager
  • restrict credentials to the minimum required permissions
  • separate research and production environments
  • never log secrets
  • never commit .env files
  • isolate order execution from market discovery

Polymarket's authenticated trading workflows require credentials, but public market discovery does not. ([Polymarket Documentation][1])

A read-only scanner is therefore an excellent first component to build and test before introducing trading credentials.


14. Testing Strategy

A scanner should be testable without contacting the live API.

Separate network access from filtering logic:

def select_candidates(
    markets: list[dict],
    min_liquidity: float,
) -> list[dict]:

    return [
        market
        for market in markets
        if market.get("active")
        and float(market.get("liquidity") or 0) >= min_liquidity
    ]
Enter fullscreen mode Exit fullscreen mode

Then test synthetic inputs:

def test_liquidity_filter():
    markets = [
        {"active": True, "liquidity": "50000"},
        {"active": True, "liquidity": "1000"},
        {"active": False, "liquidity": "90000"},
    ]

    result = select_candidates(markets, 10_000)

    assert len(result) == 1
Enter fullscreen mode Exit fullscreen mode

Also test:

  • missing fields
  • malformed JSON
  • HTTP errors
  • empty pages
  • expired cursors
  • duplicate markets
  • unexpected API fields
  • rate-limit responses

Network tests should be separate integration tests.


15. Monitoring and Observability

A production scanner should answer:

  • How many markets were discovered?
  • How many passed each filter?
  • How long did discovery take?
  • How many CLOB requests were made?
  • How many requests failed?
  • What is the current scan cycle duration?
  • When was each candidate last updated?

Useful metrics include:

markets_discovered
markets_filtered
candidate_count
api_request_count
api_error_count
scan_duration_ms
candidate_age_seconds
Enter fullscreen mode Exit fullscreen mode

Logging the number of candidates rejected by each filter is especially valuable.

If liquidity filtering suddenly eliminates 99% of markets, you want to know whether the market changed—or your API parsing broke.


16. Practical Example: Building a Shortlist

Imagine a strategy only wants markets that satisfy:

active = true
closed = false
liquidity >= configured threshold
volume >= configured threshold
Enter fullscreen mode Exit fullscreen mode

The scanner creates:

[
    {
        "id": "123",
        "question": "Example question?",
        "liquidity": 25000,
        "volume": 80000,
    },
    {
        "id": "456",
        "question": "Another question?",
        "liquidity": 42000,
        "volume": 150000,
    },
]
Enter fullscreen mode Exit fullscreen mode

The next component can then request detailed market state.

This architecture is much easier to reason about than a single Python script that simultaneously discovers markets, calculates signals, manages risk, signs orders, and submits trades.


17. Advanced Improvements

Once the basic scanner works, several upgrades become possible.

Tag-aware scanning

Use market tags to build specialized scanners for categories such as politics, crypto, sports, or economics.

Expiration-aware scanning

Prioritize markets based on time remaining until their end date.

Spread filtering

Exclude markets where the executable spread is too wide for your model.

Depth-aware filtering

A market can appear liquid while having insufficient depth at the price your strategy needs.

Model-driven scanning

Instead of simply ranking markets by volume, calculate:

model_probability - market_probability
Enter fullscreen mode Exit fullscreen mode

Then investigate candidates with sufficiently large discrepancies.

That discrepancy is not automatically profit. It is a research signal that still requires execution and risk analysis.

Real-time architecture

For continuously updating systems:

flowchart TD
    A[Market Discovery] --> B[Candidate Registry]
    B --> C[WebSocket Subscriptions]
    C --> D[Live Order Book State]
    D --> E[Feature Engine]
    E --> F[Signal Engine]
    F --> G[Risk Engine]
    G --> H[Execution]
Enter fullscreen mode Exit fullscreen mode

The scanner becomes the front door to the trading system rather than the entire system.


Frequently Asked Questions

What is a Polymarket market scanner?

A Polymarket market scanner is software that discovers and filters markets according to predefined conditions such as activity, liquidity, volume, price, expiration, or order-book characteristics.

Does a Polymarket scanner need API credentials?

Not for public market discovery. Polymarket documents public market data as accessible without authentication. ([Polymarket Documentation][1])

Should I use the Gamma API or CLOB API?

Use Gamma for broad market discovery and metadata, then use CLOB data when you need current prices or order-book information. ([Polymarket Documentation][1])

Can a scanner identify profitable trades?

It can identify candidates that satisfy a model's conditions, but it cannot guarantee profitability. Execution costs, liquidity, model error, and market risk remain.

Should a scanner use REST or WebSockets?

REST is appropriate for discovery and periodic scans. WebSockets are useful when the system needs continuously updated order-book and market-state information. ([Polymarket Documentation][4])


Conclusion

A good Polymarket market scanner is not complicated because of its number of lines of Python. It is complicated because it sits at the boundary between market discovery, real-time data, quantitative filtering, and eventually execution.

The strongest architecture is therefore modular:

discover → filter → enrich → rank → evaluate → execute

Start with public market data. Use keyset pagination for broad discovery. Reduce the universe before requesting expensive real-time data. Keep your scanner read-only until its data pipeline is reliable. Then add order-book state, quantitative models, risk controls, and execution as independent components.

That approach produces infrastructure that can evolve from a simple research script into a serious automated trading system without turning the scanner itself into an untestable monolith.


Related Articles

  1. How to Build a Polymarket Trading Bot in Python
    Anchor: Polymarket trading bot in Python
    Why: Natural next step after market discovery.

  2. Polymarket API Explained for Developers
    Anchor: Polymarket API
    Why: Explains the API architecture behind the scanner.

  3. How to Read Polymarket Order Books in Python
    Anchor: Polymarket order book data
    Why: Extends candidate discovery into executable market-state analysis.

  4. Building a Real-Time Polymarket WebSocket Client
    Anchor: Polymarket WebSocket market data
    Why: Moves from periodic scanning to streaming data.

  5. Polymarket Trading Bot Architecture
    Anchor: Polymarket trading bot architecture
    Why: Shows how scanners fit into larger trading infrastructure.

  6. Polymarket Probability and Fair-Value Modeling
    Anchor: Polymarket fair-value model
    Why: Connects market selection with quantitative evaluation.


Useful Resources

I could not verify the specific Medium, DEV.to, or YouTube resources belonging to your content series from the information provided, so I have intentionally not fabricated those URLs.


About the Author

Bo$onaX

I write about Polymarket trading bots, prediction-market infrastructure, algorithmic trading, Python automation, Web3 development, and quantitative strategies.

Contact:
X: [https://x.com/xxniiinxx]
Youtube: [https://youtube.com/@bosonax]
Telegram: [https://t.me/bosonax]

Top comments (0)