# Polymarket vs Kalshi Arbitrage — Same Question, Two Prices (`jpmarketdata/prediction-market-arbitrage`) Actor

Give it a question that trades on both Polymarket and Kalshi and get the price gap between them, after Kalshi's fee. Returns best bid and ask on each site, which site to buy on and sell on, the gap in cents per $1 contract. $0.02 per pair priced, suggestions included; refusals are free. Unofficial.

- **URL**: https://apify.com/jpmarketdata/prediction-market-arbitrage.md
- **Developed by:** [h ichi](https://apify.com/jpmarketdata) (community)
- **Categories:** Automation, Developer tools, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 contract pair priced across venues

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Polymarket vs Kalshi Arbitrage — Same Question, Two Prices

**What it does:** Takes a question that trades on both Polymarket and Kalshi and prices the gap between them, before and after the Kalshi fee (Polymarket charges none today).

**You enter:** Pairs you checked, e.g. `will-bitcoin-outperform-gold-in-2026|KXBTCVSGOLD-26` (Polymarket|Kalshi).

**You get:** One row per pair: best bid and ask on each site, which site to buy on and sell on, gap in cents per $1 before and after the fee, dollars resting at those prices, close-date gap. Pairs it suggests itself (on by default) are marked unverified, indicative only.

**Price:** $0.02 per pair priced, the pairs it suggests included. +$0.002 per site row if you also want each side (two per pair, on by default). Pairs that can't be priced are never charged.

> Unofficial — not affiliated with Polymarket or Kalshi. Reads public pages only.

### Confirmed pairs — the only tradable number

You list the pairs you have checked, as `"<polymarket-slug-or-conditionId>|<KALSHI-TICKER>"`. Only these get an `edgePct`.

| field | meaning |
|---|---|
| `matchVerified` | `true` — you confirmed this pairing |
| `direction` | e.g. `"buy polymarket / sell kalshi"` — which leg you lift |
| `edgePct` | `(bid on the dearer venue - ask on the cheaper) x 100`, cents per $1 contract |
| `edgeNetPct` | the same, less the Kalshi fee on the executed leg |
| `kalshiFeeCents` / `polymarketFeeCents` | per contract; see **Fees** below |
| `polymarketDepthUsd` / `kalshiDepthUsd` | dollars resting **at the touch**, bid and ask |
| `closeGapDays` | how far apart the two contracts settle |
| `directionsBlocked` | the round trip that was not taken, and why |

### Suggested pairs — never a trade, always a question

With `autoSuggest` on, the matcher proposes pairings it found. These carry `matchVerified: false`, **`edgePct: null`**, and their number under the deliberately different name `indicativeEdgePct` — alongside `matchScore`, `matchBasis` (what actually matched), `closeGapDays`, and **both resolution texts verbatim** (`polymarketResolution`, `kalshiResolution`) so you can confirm or reject in one glance. `confirmAs` gives you the exact string to paste into `pairs`.

Reading them matters. In a live run on 2026-08-10 the matcher proposed `OpenAI IPO before 2027?` against Kalshi's `Who will IPO before 2027? OpenAI` — same company, same year, an 8-point apparent edge. The two resolution texts do not agree: Polymarket needs the IPO **completed** by 31 Dec 2026, Kalshi needs it **confirmed** before 1 Jan 2027. Announcing is easier than completing, so Kalshi trades higher for a reason. That gap is a definition, not an arbitrage — and it is visible only because both texts travel with the suggestion.

### What the matcher refuses to do

Every gate below is a **hard drop**, not a score penalty, because a fact that is simply wrong cannot be traded off against wording similarity. Each one's losses are counted in the summary.

| gate | why |
|---|---|
| close times more than `maxCloseGapDays` apart | contracts that settle weeks apart are not the same contract |
| token overlap below `minMatchScore` | |
| numeric thresholds present on both sides but disagreeing | "BTC above $150,000" and "BTC above $120,000" are 0.75 similar as text and are opposite bets |
| dates present on both sides but disagreeing | |
| opposite direction words (above / below) | the one pairing that would flip the sign of the edge |
| a qualifier on one side only (second, exactly, not, consecutive) | "second-hottest year" vs "hottest year" is one word and the opposite bet |
| the shared words are more common than the differing ones | see below |

That last gate came out of a live run. At `minMatchScore` 0.6 the matcher paired **"Remote IPO before 2027?"** with **"Who will IPO before 2027? Kraken"** — two different companies whose entire overlap is the template `ipo / before / 2027`. A candidate is now dropped unless the rarest word the two questions *share* is at least as rare as the rarest word they *differ on*.

### What it counts

A filtered set that hides its own losses reads as complete, so the `spread_summary` record reports every drop: `droppedCloseGap`, `droppedPolarity`, `droppedAsymmetricQualifier`, `droppedNumericThresholdDisagreement`, `droppedDateDisagreement`, `droppedNoSharedSubjectWord`, `droppedBelowMatchScore`, `duplicateKalshiSuggestionsDropped`, `duplicatePolymarketSuggestionsDropped`, `unpriceableLegs`, `confirmedPairsUnresolvable`. Verified and unverified edge statistics are kept in separate fields and never averaged together.

### Depth, and what is refused

`minLiquidityUsd` applies to the size resting **at the touch** on both legs — the Polymarket side from the CLOB order book, the Kalshi side from `yes_bid_size` / `yes_ask_size`, each valued at its own price. Gamma's book-wide `liquidity` is reported separately as `bookWideLiquidityUsd` and is *not* used for this, because it is not the size behind the quote.

A pair is **refused, not priced**, when:

- either venue has no two-sided quote (one side of the book is empty);
- the depth a direction needs is below `minLiquidityUsd`, in both directions;
- a leg is not accepting orders, or is not a binary Yes/No market;
- the pair string does not resolve to one market on each venue.

Every refusal names the input to change and is returned with the record rather than dropped.

### Fees

- **Kalshi**: `ceil(coefficient x P x (1 - P))` dollars per contract, rounded up to the next cent — 2c on a 50c contract. The coefficient (published: 0.07) is an input, because a stale fee constant would quietly overstate every edge.
- **Polymarket**: no trading fee is charged today; every live market measured on 2026-08-10 carries `feesEnabled: false`. This is stated rather than silently assumed.
- **Gas / relayer cost on the Polymarket leg is not modelled.** A net edge smaller than your own transaction cost is not a trade.

### What one row looks like

Measured on 2026-08-09 (real run) — one confirmed pair, a few fields left out for length

```json
{
  "type": "confirmed_pair",
  "pair": "will-bitcoin-outperform-gold-in-2026|KXBTCVSGOLD-26",
  "matchVerified": true,
  "polymarketQuestion": "Will Bitcoin outperform Gold in 2026?",
  "kalshiQuestion": "Will Bitcoin outperform gold in 2026?",
  "polymarketYesBid": 0.17,
  "polymarketYesAsk": 0.19,
  "kalshiYesBid": 0.178,
  "kalshiYesAsk": 0.215,
  "polymarketDepthUsd": { "bid": 1.7, "ask": 19.0 },
  "kalshiDepthUsd": { "bid": 0.93, "ask": 10.24 },
  "closeGapDays": 0.0,
  "priced": true,
  "edgePct": -1.2,
  "edgeNetPct": -3.2,
  "direction": "buy polymarket / sell kalshi",
  "kalshiFeeCents": 2,
  "polymarketFeeCents": 0,
  "directionsBlocked": []
}
```

Each row also carries `feeModel` and `depthBasis` in plain words, so the numbers above can be checked without reading this page.

### Price

**$0.02 per contract pair priced** — every pair that gets a price is charged, including the ones the tool suggests itself, so ten priced pairs cost $0.20. **A refused pair is never charged**: a pair without a two-sided quote on both venues, or with a leg too thin to trade, produced no answer, and you do not pay for a non-answer.

Each venue's side of a pair is $0.002 and **on by default** (`includeIndividualLegs`), two per **priced** pair — confirmed and suggested alike. A refused pair carries no leg rows and no leg charge, because a refusal is free whole. Turn it off if you only want the pair rows. You pay per pair; there is no monthly fee.

### Input

```json
{
  "pairs": ["will-bitcoin-outperform-gold-in-2026|KXBTCVSGOLD-26"],
  "autoSuggest": true,
  "categories": ["Financials"],
  "maxEventsPerVenue": 1000,
  "maxCloseGapDays": 3,
  "minMatchScore": "0.6",
  "minLiquidityUsd": "0",
  "kalshiFeeCoefficient": "0.07"
}
```

`categories` filters the Kalshi side only, and it is the lever that matters: Kalshi carries over 8,000 open events, so scanning without a category is scanning a slice of an arbitrary ordering. Pick the categories your pairs live in (`Politics`, `Economics`, `Elections`, `Financials`, `Companies`, `Crypto`, `World`, `Sports`, `Climate and Weather`).

`maxEventsPerVenue` defaults to 200 and tops out at 1000; the example above asks for the maximum.

Reads both sites' public data only, no login, requests spaced out. Nothing is kept between runs — every run re-reads both live order books.

### If something goes wrong

- **Wrong number or a failed run?** Open a ticket on the **Issues** tab. I read every one and reply within 2 business days (Japan time).
- **You never get a fake "empty" result.** If the site can't be read, the run fails and says so.
- **A pair that can't be priced = no charge.** A pair that does get a price costs $0.02 even when the gap comes out negative — that number is the result you asked for.
- **Checked every week.** An automatic test runs this tool weekly; if the site changes, I fix it.
- **Public pages only.** No login, no personal data, and it goes easy on the site.

### More tools by the same author

- [Kalshi Arbitrage Finder — Groups Priced Against $1](https://apify.com/jpmarketdata/kalshi-odds-arbitrage)
- [Polymarket Odds, Volume & Top Markets by Topic](https://apify.com/jpmarketdata/polymarket-market-stats)
- [Polymarket Markets — Odds, Bid/Ask, Volume, One Row Each](https://apify.com/jpmarketdata/polymarket-markets)
- [Polymarket Arbitrage Finder — Groups Priced Under $1](https://apify.com/jpmarketdata/polymarket-odds-arbitrage)

All tools (Japan marketplaces, real estate, jobs, racing, prediction markets): <https://apify.com/jpmarketdata>

### Disclaimer

Unofficial, independent tool — **not affiliated with, endorsed by, or sponsored by Kalshi or Polymarket**. Product names and logos belong to their owners and only say where the data comes from. Data is read from public pages, for market research; check before you act on it.

# Actor input Schema

## `pairs` (type: `array`):

Pairs you have already checked, as "<polymarket-slug-or-conditionId>|<KALSHI-TICKER>". These are the only pairs whose edge is reported as tradable, because you have confirmed the two contracts resolve on the same thing.

## `autoSuggest` (type: `boolean`):

Scan both venues and propose pairings. Suggested pairs carry an indicative edge only, plus both resolution texts and close times, so you can confirm them and feed them back in as confirmed pairs.

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

Kalshi categories to scan when suggesting pairs, e.g. "Politics", "Economics". Ignored when suggestions are off.

## `maxEventsPerVenue` (type: `integer`):

How many open events to pull from each venue before matching.

## `maxCloseGapDays` (type: `integer`):

Two contracts that settle weeks apart are not the same contract. A candidate pair whose close times differ by more than this is not suggested at all.

## `minMatchScore` (type: `string`):

Similarity floor for suggesting a pair, between 0 and 1. Raising it yields fewer, safer suggestions.

## `minLiquidityUsd` (type: `string`):

A quote with nothing behind it is not a price. Legs with less than this resting at the best price are reported as unpriceable rather than used.

## `kalshiFeeCoefficient` (type: `string`):

Kalshi charges ceil(coefficient x price x (1 - price)) cents per contract; the published coefficient is 0.07. Exposed as an input because a stale fee constant would quietly overstate every edge.

## `includeIndividualLegs` (type: `boolean`):

Also return each venue's side of every reported pair (charged per leg).

## Actor input object example

```json
{
  "autoSuggest": true,
  "categories": [
    "Politics",
    "Economics"
  ],
  "maxEventsPerVenue": 200,
  "maxCloseGapDays": 3,
  "minMatchScore": "0.6",
  "minLiquidityUsd": "0",
  "kalshiFeeCoefficient": "0.07",
  "includeIndividualLegs": true
}
```

# 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 = {
    "categories": [
        "Politics",
        "Economics"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("jpmarketdata/prediction-market-arbitrage").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 = { "categories": [
        "Politics",
        "Economics",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("jpmarketdata/prediction-market-arbitrage").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 '{
  "categories": [
    "Politics",
    "Economics"
  ]
}' |
apify call jpmarketdata/prediction-market-arbitrage --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jpmarketdata/prediction-market-arbitrage"
        }
    }
}

```

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/dQ4uaRPUbWUAjquMt/builds/DE6zkGHkobGoWrwJM/openapi.json
