# Polymarket & Kalshi Odds - Matched Markets & Price Gaps (`that_red_bird/polymarket-kalshi-odds`) Actor

See where Polymarket and Kalshi disagree. Pulls live odds, volume and liquidity from both, matches the same question with a confidence score, and shows the price gap and movers since your last run. Not financial advice. $0.001 per market, $0.003 per gap.

- **URL**: https://apify.com/that_red_bird/polymarket-kalshi-odds.md
- **Developed by:** [mohamed alaya](https://apify.com/that_red_bird) (community)
- **Categories:** News, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 market checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Polymarket & Kalshi Odds - Matched Markets & Price Gaps

See where the two biggest prediction markets disagree. Pulls live odds from Polymarket and Kalshi, pairs up markets that ask the same question, and shows the price gap, plus which markets moved since your last run.
Click Start, get results in about a minute. No API key.

**Not financial advice.** This Actor reports public prices. A gap is not a guaranteed profit (see "Limits").

### What you get

The dataset has three row types, set in the `type` field.

| type | What it is | Charged |
|---|---|---|
| `pair` | The same question on both venues, with both prices and the gap | $0.003 (gap-found) |
| `market` | One market on one venue: price, bid/ask, volume, liquidity, end date | $0.001 (market-checked) |
| `mover` | A market that moved since your last run and is not already a `market` row | $0.001 (market-checked) |

**Pair fields**

| Field | Meaning |
|---|---|
| question, polymarketQuestion, kalshiQuestion | The question as each venue words it |
| matchConfidence (0-100) | How sure the matcher is that both ask the same thing, with `matchReasons` explaining why |
| polymarketYesPct, kalshiYesPct | Probability of YES on each venue, in percent (bid/ask midpoint, else last trade) |
| gapPts | Absolute difference in percentage points |
| higherOn | Which venue prices YES higher |
| spreadAdjustedEdgePts, edgeDirection | The gap after crossing the bid/ask spread (buy YES on one venue, NO on the other). Before fees. Can be negative or empty |
| polymarketVolumeUsd, kalshiVolumeContracts, \*Liquidity | Size of each market (Polymarket in USD, Kalshi in contracts) |
| closesAt, \*Url | Earliest end date and links to both markets |
| \*ChangeSinceLastRunPts | How far each side moved since your previous run |

**Market and mover fields:** venue, question, yesPct, yesBidPct, yesAskPct, priceBasis, volume, volumeUnit, volume24h, liquidity, closesAt, change24hPts (Polymarket only), changeSinceLastRunPts, isMover, hasMatchOnOtherVenue, url.

### 3 ways to use it

1. **Where do they disagree today?** (the default run: about 25 biggest markets per venue plus every gap of 3+ points)
   `{}`
2. **Only one topic, wider net**:
   `{"keywords": ["fed", "rate cut", "cpi"], "minGapPts": 2, "minMatchConfidence": 75, "includeMarkets": false}`
3. **Scheduled alerts to Slack or Discord** (run every 15 minutes, state remembers prices):
   `{"keywords": ["bitcoin"], "moverMinPts": 4, "webhookUrl": "https://hooks.slack.com/services/..."}`

### Input guide

| Field | What to enter | Default | Tip |
|---|---|---|---|
| keywords | Words to look for in market titles | empty (all) | Match is on question, event title and category |
| categories | Kalshi categories, e.g. Politics, Economics | empty | Polymarket has no category field |
| minVolume | Skip thin markets | 1000 | Polymarket in USD, Kalshi in contracts |
| minGapPts | Smallest gap to report, in points | 3 | 2 finds more, noisier gaps |
| minMatchConfidence | How sure a match must be | 80 | Below 70 expect false matches |
| minProbabilityPct | Ignore long shots near 0% or 100% | 3 | Removes 0.1% vs 0.4% noise |
| includeMarkets / maxMarketsPerVenue | Deliver market rows | on / 25 | Off = only gaps and movers |
| scanDepth | Markets read per venue for matching | 1000 | More = more matches, slower |
| trackMovers, moverMinPts, maxMovers | Movers since last run | on / 5 / 20 | First run has no previous prices |
| stateStoreName | Where prices are remembered | `polymarket-kalshi-odds-state` | Use one name per watchlist |
| webhookUrl | Slack, Discord, Make or Zapier URL | empty | One message per run when something is found |

### Output example

```json
{
  "type": "pair",
  "question": "Will Alexandria Ocasio-Cortez win the 2028 Democratic presidential nomination?",
  "matchConfidence": 89,
  "matchReasons": ["question words 85% alike", "close the same day of each other"],
  "polymarketYesPct": 17,
  "kalshiYesPct": 13.5,
  "gapPts": 3.5,
  "higherOn": "polymarket",
  "spreadAdjustedEdgePts": 2.7,
  "edgeDirection": "Buy YES on Kalshi + buy NO on Polymarket",
  "closesAt": "2028-11-07T15:00:00.000Z",
  "polymarketUrl": "https://polymarket.com/event/democratic-presidential-nominee-2028",
  "kalshiUrl": "https://kalshi.com/markets/kxpresnomd"
}
```

### Pricing (pay per event)

| Event | Price |
|---|---|
| Market checked (one market or mover row delivered) | $0.001 |
| Gap found (one matched pair at or above your minimum gap) | $0.003 |

- Default run (about 50 market rows and a few gaps): **about $0.06**
- Gaps only (`includeMarkets: false`), 10 gaps: **$0.03**
- Every 15 minutes for a day, gaps only, about 10 gaps each: **about $2.90**

Competing Actors charge $0.01 to $0.05 per result. You pay only for rows that are delivered.

### Use it with

- **Before:** nothing needed. **Then:** Google Sheets, a Slack or Discord channel, or a newsletter.
- Make, Zapier, n8n: trigger on "run succeeded", or set `webhookUrl` for a direct alert.
- **AI agents / MCP:** call `that_red_bird/polymarket-kalshi-odds` with `{"keywords": ["election"]}` and let the agent summarize disagreements between venues.
- Schedule it every 5 to 60 minutes with the same `stateStoreName` to catch movers.

### FAQ

- **Is this financial advice?** No. It reports public prices and a heuristic match. You decide what to do with it.
- **Is a gap an arbitrage?** Not by itself. The two markets can resolve on different rules, close at different times, charge different fees, and have thin order books. `spreadAdjustedEdgePts` shows the gap after the bid/ask spread, but before fees, deposit costs and slippage.
- **How reliable is the matching?** It compares wording, numbers and end dates. High scores (85+) with "same numbers" and a close end date are usually right. Always open both links and compare the resolution rules.
- **Why are there no movers on my first run?** There is nothing to compare with yet. Run it again with the same `stateStoreName`.
- **Why are some markets missing?** Only binary Yes/No markets that are open are included. Multi-leg combos are skipped. Markets past their end date are skipped.
- **Does it need an account or API key?** No. It uses the public Polymarket Gamma API and Kalshi's public market data.

### Limits

Prices are snapshots at run time. Volume units differ (USD vs contracts). Kalshi does not give a 24h change, so only Polymarket rows have `change24hPts`. Some pairs are false matches. No trading, no order placement, no personal data.

### Support

Open an issue on the Issues tab. If this saved you time, a short review helps other people find it.

# Actor input Schema

## `keywords` (type: `array`):

Only markets whose question, event title or category contains one of these words, e.g. 'fed', 'bitcoin', 'election', 'nba'. Leave empty for all markets.

## `categories` (type: `array`):

Optional. Keep only Kalshi markets in these categories, e.g. 'Politics', 'Economics', 'Financials', 'Climate and Weather', 'Sports'. Polymarket has no category field, so use keywords for it. Leave empty for all.

## `minVolume` (type: `integer`):

Skip thin markets. Polymarket volume is in US dollars, Kalshi volume is in contracts (each contract pays $1 at most). 0 = no filter.

## `minGapPts` (type: `integer`):

A matched pair is reported only when the two venues' YES prices differ by at least this many percentage points. 3 means 3 points, for example 62% against 59%.

## `minMatchConfidence` (type: `integer`):

How sure the title and date matcher must be that two markets ask the same question. 80 is a good balance. Lower finds more pairs but with more false matches; always read both markets' rules.

## `minProbabilityPct` (type: `integer`):

Skip pairs where both venues price the outcome below this percent or above 100 minus this percent. Removes long-shot noise like 0.1% against 0.4%. 0 = keep everything.

## `includeMarkets` (type: `boolean`):

Also deliver the biggest markets of each venue with price, bid/ask, volume, liquidity and end date (one row per market, billed per row). Turn off to receive only price gaps and movers.

## `maxMarketsPerVenue` (type: `integer`):

How many market rows to deliver for each venue, biggest volume first. Does not limit how many markets are scanned for matching.

## `scanDepth` (type: `integer`):

How many active markets to read from each venue for matching. More markets means more matches but a slower run. Polymarket is read biggest volume first.

## `trackMovers` (type: `boolean`):

Remember each market's price in a key-value store and flag markets that moved since your previous run. The first run has nothing to compare with, so it shows no movers.

## `moverMinPts` (type: `integer`):

A market counts as a mover when its YES price changed by at least this many percentage points since the last run.

## `maxMovers` (type: `integer`):

Largest movers that are not already in the market rows are added as extra rows, up to this number.

## `stateStoreName` (type: `string`):

Name of the key-value store that remembers prices between runs. Use a different name for each separate watchlist or schedule.

## `webhookUrl` (type: `string`):

Optional. A Slack, Discord, Make or Zapier webhook. When a run finds price gaps or movers, one message with the top results is posted. Leave empty for no alert.

## Actor input object example

```json
{
  "keywords": [],
  "categories": [],
  "minVolume": 1000,
  "minGapPts": 3,
  "minMatchConfidence": 80,
  "minProbabilityPct": 3,
  "includeMarkets": true,
  "maxMarketsPerVenue": 25,
  "scanDepth": 1000,
  "trackMovers": true,
  "moverMinPts": 5,
  "maxMovers": 20,
  "stateStoreName": "polymarket-kalshi-odds-state",
  "webhookUrl": ""
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `downloadCsv` (type: `string`):

No description

## `summary` (type: `string`):

No description

## `count` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "keywords": [],
    "categories": [],
    "minVolume": 1000,
    "minGapPts": 3,
    "minMatchConfidence": 80,
    "minProbabilityPct": 3,
    "includeMarkets": true,
    "maxMarketsPerVenue": 25,
    "scanDepth": 1000,
    "trackMovers": true,
    "moverMinPts": 5,
    "maxMovers": 20,
    "stateStoreName": "polymarket-kalshi-odds-state",
    "webhookUrl": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("that_red_bird/polymarket-kalshi-odds").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "keywords": [],
    "categories": [],
    "minVolume": 1000,
    "minGapPts": 3,
    "minMatchConfidence": 80,
    "minProbabilityPct": 3,
    "includeMarkets": True,
    "maxMarketsPerVenue": 25,
    "scanDepth": 1000,
    "trackMovers": True,
    "moverMinPts": 5,
    "maxMovers": 20,
    "stateStoreName": "polymarket-kalshi-odds-state",
    "webhookUrl": "",
}

# Run the Actor and wait for it to finish
run = client.actor("that_red_bird/polymarket-kalshi-odds").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "keywords": [],
  "categories": [],
  "minVolume": 1000,
  "minGapPts": 3,
  "minMatchConfidence": 80,
  "minProbabilityPct": 3,
  "includeMarkets": true,
  "maxMarketsPerVenue": 25,
  "scanDepth": 1000,
  "trackMovers": true,
  "moverMinPts": 5,
  "maxMovers": 20,
  "stateStoreName": "polymarket-kalshi-odds-state",
  "webhookUrl": ""
}' |
apify call that_red_bird/polymarket-kalshi-odds --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,that_red_bird/polymarket-kalshi-odds"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Gxb28ZgMpZNFo6jPx/builds/NIfml1Eg4jQprtn8R/openapi.json
