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]
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
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
A minimal project can look like:
polymarket-scanner/
├── scanner.py
├── requirements.txt
└── README.md
requirements.txt:
httpx
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
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_cursor → after_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
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
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']}"
)
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
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)
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:
- Fetch market metadata.
- Filter locally.
- Batch price/order-book requests when appropriate.
- Cache relatively static metadata.
- Move continuously changing data to WebSockets.
- Parallelize only within documented limits.
- 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
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
.envfiles - 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
]
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
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
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
The scanner creates:
[
{
"id": "123",
"question": "Example question?",
"liquidity": 25000,
"volume": 80000,
},
{
"id": "456",
"question": "Another question?",
"liquidity": 42000,
"volume": 150000,
},
]
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
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]
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
How to Build a Polymarket Trading Bot in Python
Anchor:Polymarket trading bot in Python
Why: Natural next step after market discovery.Polymarket API Explained for Developers
Anchor:Polymarket API
Why: Explains the API architecture behind the scanner.How to Read Polymarket Order Books in Python
Anchor:Polymarket order book data
Why: Extends candidate discovery into executable market-state analysis.Building a Real-Time Polymarket WebSocket Client
Anchor:Polymarket WebSocket market data
Why: Moves from periodic scanning to streaming data.Polymarket Trading Bot Architecture
Anchor:Polymarket trading bot architecture
Why: Shows how scanners fit into larger trading infrastructure.Polymarket Probability and Fair-Value Modeling
Anchor:Polymarket fair-value model
Why: Connects market selection with quantitative evaluation.
Useful Resources
- Polymarket — The trading platform and market interface.
- Polymarket Documentation — Primary technical reference for APIs, SDKs, WebSockets, and market infrastructure. ([Polymarket Documentation][9])
- Market Data Overview — Overview of Gamma, CLOB, and Data APIs and how market data is organized. ([Polymarket Documentation][1])
- List Markets — Keyset Pagination — Relevant reference for implementing the scanner's market-discovery loop. ([Polymarket Documentation][2])
- Polymarket Python SDK — Official unified Python SDK; currently documented as beta. ([GitHub][7])
- Polymarket GitHub organization — Official developer repositories and tooling. ([GitHub][10])
- Polymarket on X — Official Polymarket account.
- Polymarket changelog — Useful for tracking API and platform changes. ([Polymarket Documentation][6])
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)