I run a visa agency in Bali. The other half of my week goes into Apify Actors, and one of them is an election board: every political contract on Kalshi and Polymarket in one table, same field names for both, so you can read the two venues side by side.
What people want from it is one sentence — "who is favoured". What the two APIs hand you is thirty-odd numeric fields, several of which look like they answer that and do not. I spent an afternoon measuring how wrong you get by picking the wrong one.
One race, 128 candidates, $716 million
Polymarket's Republican Presidential Nominee 2028 event is one race in the UI and 128 separate yes/no markets in the API, carrying $716,560,972 of cumulative volume. Nothing on it is quiet.
Sort its candidates by 24-hour volume — Gamma's default, my Actor's default, and the one that sounds most like "what matters right now":
| # | Candidate | volume24hr |
Price |
|---|---|---|---|
| 1 | Ted Cruz | $77,802 | 0.0205 |
| 2 | John McEntee | $70,645 | 0.0015 |
| 3 | John Thune | $70,072 | 0.0005 |
| 4 | Marco Rubio | $33,909 | 0.1625 |
| 5 | Vivek Ramaswamy | $7,921 | 0.0015 |
| 6 | Ron DeSantis | $4,334 | 0.0320 |
| 7 | Donald Trump | $4,093 | 0.0195 |
| 8 | J.D. Vance | $2,777 | 0.4395 |
The market's actual favourite — 43.95%, nearly half the field's probability mass — is eighth, on $2,777 traded in a day. Three of the seven rows above him are priced at or under 0.15%.
That is not a bug in either API — it is what volume measures: disagreement and churn. Once a contract is near-certain the spread collapses to a tick and nobody trades it. The busy ones are those people still argue about at two cents.
Sorting by cumulative volume instead is worse, not better:
| Candidate | volumeNum |
Price |
|---|---|---|
| Byron Donalds | $44,556,583 | 0.0015 |
| Mike Pence | $43,740,818 | 0.0015 |
| John Thune | $38,383,281 | 0.0005 |
| Tom Brady | $33,013,140 | 0.0005 |
| Kim Kardashian | $28,597,926 | 0.0005 |
Tom Brady has thirty-three million dollars of lifetime volume on the Republican nomination at a price of five hundredths of one per cent. Long shots sit on the board for years collecting liquidity rewards, accumulating volume unrelated to anyone's belief. Of the 128 markets, 119 are priced under 1% and 94 report no 24-hour volume — nine contenders wearing a tail of 119.
So sort by price? Also no
The obvious fix — rank by outcomePrices[0] and call the top row the favourite — put this first in the same event:
Robert F. Kennedy Jr. price 0.4900 volume null bestBid null bestAsk 0.98 active false
There are two Robert F. Kennedy Jr. rows in that event. The live one trades at 0.0035 on $16.9M of volume. The other is a retired duplicate with a frozen 49% quote, no volume, no bid and a 98-cent ask — unhittable on either side. The only thing marking it is active: false.
Two one-line defences: query Gamma with active=true&closed=false, as my collector does, so the dead row never arrives; and treat the venue's status flag as your first filter — in my schema that row becomes status: "inactive" and is dropped before sorting. A stale 49% survives review precisely because 49% is a plausible number for a nomination favourite.
What I actually ask for now
The fix is structural: rank races, not rows. Events mode collapses a race into one row with every candidate sorted by probability.
{ "mode": "events", "minVolume": 5000, "maxMarkets": 10 }
Nine of the ten rows Election Odds API returned on 25 October 2026:
| Platform | Race | Favourite | Candidates |
|---|---|---|---|
| kalshi | 2028 Democratic presidential nominee | Jon Ossoff 15.5% | 49 |
| kalshi | 2028 U.S. Presidential Election winner? | J.D. Vance 20.5% | 32 |
| kalshi | Which party will win the U.S. House? | Democratic Party 90.7% | 2 |
| polymarket | Brazil Presidential Election | Flávio Bolsonaro 78.8% | 2 |
| polymarket | Republican Presidential Nominee 2028 | Ted Cruz 2.1% | 3 |
| kalshi | 2026 Midterms: Congress Balance of Power? | Democrats sweep 64.5% | 4 |
| kalshi | Texas Senate winner? | James Talarico 64.5% | 2 |
| polymarket | Balance of Power: 2026 Midterms | Democrats Sweep 66.5% | 3 |
| polymarket | Which party will win the House in 2026? | Democratic Party 91.5% | 2 |
Note row five. Grouping fixed the shape, not the favourite: it happens after the row cap, so with maxMarkets: 10 the collector keeps the busiest markets per venue — Cruz, McEntee and Thune. A 128-candidate race arrived with 3 candidates and a 2% favourite.
favourite is the favourite among the candidates in the row. On a wide race, raise maxMarkets until outcomeCount stops growing, or narrow with query. Kalshi's equivalent race returned 49 outcomes and a sane 15.5% leader for that reason — it fit.
Neither venue will tell you what an election is
The other half of the afternoon went into the filter that sounds trivial: keep elections, drop the rest.
Kalshi looks easy — one category string per event, splitting 1,600 open events cleanly:
Elections 894 · Politics 219 · Sports 137 · Economics 107
Entertainment 93 · Financials 55 · Science and Technology 39
Then you read it. Under Elections: "Who will be the next President of the Federal Reserve Bank of Atlanta?", "Who will the next Pope be?", "Who will be Israeli defense minister after the election?" — an appointment, a conclave, a coalition deal. Under Politics: "Who will be the next DNC Chair?", "Which state will vote first in the 2028 Democratic presidential primary?", "When will Wales call a snap election?". Twenty of the 219 Politics titles carry an electoral word.
Polymarket has no category field at all — just a tag list, 7.9 tags per election event on average and up to 13, mixing geography, theme and promotions: ["United States", "Rewards 20, 4.5, 50", "Politics", "US Election", "Elections", "Midterms", "Earn 3.25%", "Main Election", "Global Elections"].
Filtering on the Politics tag is what most people try. Of the 500 busiest open events, 122 carry it — and 49 of those 122 are not elections: Elon Musk tweet counts, "Will the US confirm that aliens exist by…?", "Will China invade Taiwan by end of 2026?", "Putin out as President of Russia by…?".
The field my schema calls category for Polymarket is just tags[0], in whatever order the venue returned. Of 74 election events, exactly 3 have an election word in the first tag. Values from one ten-row run: Politics, United States, Serbia, Main Election, and — for "Which party will win the House in 2026?" — Parent For Derivative, an internal bookkeeping label.
So: never filter the exported CSV on category. Kalshi says Elections on every row and Polymarket never does, making that a venue filter in disguise. Filter on tags or on the words in question — which is what the Actor does, because tags drag Fed-rate markets onto an election board.
Calling it
Neither venue needs an API key, but the merge, dedup and status filtering live in the Actor:
curl -X POST "https://api.apify.com/v2/acts/lergassy~election-odds-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"mode":"markets","minVolume":1000,"sortBy":"volume24h","maxMarkets":10}'
A row, trimmed:
{
"platform": "kalshi",
"marketId": "CONTROLH-2026-R",
"question": "Which party will win the U.S. House? — Republican Party",
"category": "Elections",
"probability": 9.9,
"yesPrice": 0.0985,
"bestBid": 0.098,
"bestAsk": 0.099,
"priceChange24h": 0.01,
"volume24h": 1314909.71,
"openInterest": 35669920.8,
"status": "open",
"closeTime": "2027-02-01T15:00:00Z",
"rules": "If the Republican Party has won control of the House in 2026, then the market resolves to Yes."
}
Two fields to read twice: probability is the number to quote, in per cent, and priceChange24h is a change in price — 0.01 is one percentage point of probability, not one per cent of the price.
When this is the wrong tool
It is not a forecast — it is what people are paying right now, fees and thin tail included. Need the whole board rather than elections (Fed decisions, weather, sports)? Use Prediction Markets API; this listing drops those on purpose. Need tick-level depth? Pull the order book from the venue — the Actor gives a snapshot, not a feed. Care about one race? Query it by keyword instead of filtering a 200-row board.
The lesson is older than prediction markets and I keep relearning it: when a payload offers several fields that could rank your rows, the one the endpoint sorts by default is the one the venue cares about, not the one your reader does. Here that gap is Ted Cruz at 2% standing in front of a 44% favourite.
Top comments (0)