Prediction markets like Polymarket price real world questions as tradable YES and NO shares, which makes them a clean signal for elections, crypto, sports and macro events. The useful part: all of the data is served by public JSON endpoints, so you can read markets, prices and trades with no login and no API key.
Here is how the pieces fit together.
Three public APIs
Polymarket exposes three hosts, all keyless:
- Gamma, for market metadata:
https://gamma-api.polymarket.com - CLOB, for the live order book and price history:
https://clob.polymarket.com - Data, for recent trades:
https://data-api.polymarket.com
A plain GET with a normal User Agent header is enough:
async function getJson(url) {
const res = await fetch(url, { headers: { Accept: 'application/json' } });
if (!res.ok) throw new Error(`HTTP ${res.status} ${url}`);
return res.json();
}
List active markets
The Gamma markets endpoint pages with limit and offset and filters by status:
GET https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=100
Each market carries the fields you want: question, outcomes, outcomePrices, volume24hr, liquidityNum, endDate, conditionId and clobTokenIds. Note that outcomes and outcomePrices arrive as JSON encoded strings, so parse them:
const outcomes = JSON.parse(market.outcomes || '[]');
const prices = JSON.parse(market.outcomePrices || '[]').map(Number);
The search gotcha
Here is the trap that cost me a rewrite. The obvious move is to add search to the markets endpoint:
GET https://gamma-api.polymarket.com/markets?search=trump
That query returns rows, so it looks like it works. It does not. The markets endpoint ignores search and returns the same generic list for every term. Searching trump and searching bitcoin give identical results. If you charge per row on that output, you are billing for the wrong data.
Real keyword search lives on a separate endpoint:
GET https://gamma-api.polymarket.com/public-search?q=trump&events_status=active
It returns matching events, and each event holds a nested markets array with the same field shape as the markets endpoint. So you flatten events into markets, filter by status client side, and page with pagination.hasMore:
async function searchMarkets(query) {
const out = [];
let page = 1;
while (out.length < cap) {
const url = `https://gamma-api.polymarket.com/public-search`
+ `?q=${encodeURIComponent(query)}&events_status=active&page=${page}`;
const data = await getJson(url);
const events = data.events || [];
if (events.length === 0) break;
for (const ev of events) {
for (const m of ev.markets || []) {
if (m.active && !m.closed) out.push(m);
}
}
if (!data.pagination?.hasMore) break;
page += 1;
}
return out;
}
Now trump returns Trump markets and bitcoin returns Bitcoin markets, which is the whole point.
Optional enrichment
Once you have a market you can attach depth from the other two hosts, one request each:
- Order book per outcome token:
GET https://clob.polymarket.com/book?token_id=<tokenId> - YES price history:
GET https://clob.polymarket.com/prices-history?market=<tokenId>&interval=1d - Recent trades per market:
GET https://data-api.polymarket.com/trades?market=<conditionId>
Keep these off by default. They add a request per outcome or per market, and most jobs only need the market snapshot.
Cost
Because every call is a light JSON GET, a run is cheap. No headless browser, no proxy, no key. That is the appeal of building on public endpoints: the data is already open, you just have to read the right one.
If you want this packaged instead of maintained by hand, I run it as a keyless pay per use actor on Apify. Give it keywords or a category and it returns one JSON row per market with the optional order book, trades and price history. The first rows of every run are free so you can check the output.
Top comments (0)