# Polymarket & Kalshi Arbitrage Monitor (`automation-lab/polymarket-kalshi-arbitrage-monitor`) Actor

Match public Polymarket and Kalshi binary contracts, compare outcome prices, and export confidence evidence with candidate cross-market spread calculations.

- **URL**: https://apify.com/automation-lab/polymarket-kalshi-arbitrage-monitor.md
- **Developed by:** [Stas Persiianenko](https://apify.com/automation-lab) (community)
- **Categories:** Business, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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 & Kalshi Arbitrage Monitor

Compare open binary contracts across Polymarket and Kalshi, inspect current outcome prices, and export explainable candidate spread calculations.

This prediction market app monitor reads the two venues' public APIs, identifies likely equivalent propositions, evaluates both YES/NO venue combinations, and saves ranked records to an Apify dataset.

It is designed for recurring prediction market arbitrage research, pricing dashboards, and analyst review—not automated trading.

### What does this Actor do?

The Actor:

1. loads open public binary contracts from Kalshi;
2. loads open public binary contracts from Polymarket;
3. blocks pairs with conflicting parties, offices, directions, or numeric terms;
4. scores remaining pairs using shared terms, numeric terms, and expiry proximity;
5. compares `Kalshi YES + Polymarket NO` with the reverse combination;
6. subtracts your configurable fee and slippage estimate;
7. ranks and exports matched contracts with source links and matching evidence.

Every result contains both source quotes. You can inspect why a pair matched instead of relying on an opaque label.

### Who is this prediction market app monitor for?

- **Prediction-market analysts** comparing prices across platforms.
- **Quant researchers** building an observation history from scheduled runs.
- **Traders** creating a shortlist for manual settlement-rule and depth checks.
- **Journalists and academics** studying cross-platform pricing differences.
- **Data teams** feeding matched prediction market prices into a warehouse or dashboard.
- **Automation builders** feeding analytical output into their own external alert workflows.

### Why use it?

Polymarket and Kalshi describe related contracts differently.
A direct title join misses useful pairs, while a loose keyword join creates dangerous false matches.

This Actor combines:

- public structured APIs rather than browser automation;
- event and contract context from both venues;
- conservative proposition-conflict checks;
- an explainable confidence score;
- displayed bid/ask, liquidity, volume, expiry, and source identity;
- configurable topic, confidence, expiry, liquidity, fee, and spread filters;
- stable match IDs for recurring monitoring.

It does **not** claim that a high score proves identical settlement rules.
Manual verification remains essential.

### What data can I export?

| Field | Meaning |
| --- | --- |
| `rank` | Position after candidate status, net spread, and confidence sorting |
| `matchId` | Stable hash of the Kalshi and Polymarket market IDs |
| `matchedTitle` | Human-readable Kalshi event and proposition |
| `strategy` | Lower-cost displayed YES plus NO venue combination |
| `candidateArbitrage` | Whether estimated net spread meets your threshold |
| `combinedCost` | Displayed ask cost for a theoretical $1 payout |
| `grossSpreadPercent` | `(1 - combinedCost) × 100` |
| `estimatedFeesPercent` | User-provided fee and slippage estimate |
| `estimatedNetSpreadPercent` | Gross spread minus estimated fees |
| `matchConfidence` | Similarity score from 0 to 1 |
| `matchReasons` | Shared terms, numeric evidence, and expiry evidence |
| `expiryDifferenceHours` | Difference between displayed expiry timestamps |
| `kalshi` | Kalshi identity, URL, expiry, prices, liquidity, and volume |
| `polymarket` | Polymarket identity, URL, expiry, prices, liquidity, and volume |
| `observedAt` | UTC observation timestamp |

Price, liquidity, or volume fields can be `null` when a source does not provide them.

### How to run the monitor

1. Open the Actor in Apify Console.
2. Leave `keywords` empty for broad discovery, or add a real topic such as `Alexandria Ocasio-Cortez`.
3. Choose how many contracts to scan per venue.
4. Keep `minMatchConfidence` at `0.4` initially.
5. Set a realistic combined fee and slippage estimate.
6. Keep `includeNonProfitable` enabled when you need all matched price comparisons.
7. Disable it when you only need pairs meeting `minNetSpreadPercent`.
8. Click **Start**.
9. Review the **Matched prediction contracts** dataset view.
10. Open both source URLs and compare full settlement rules before acting.

### Input parameters

#### `keywords`

Optional topic phrases.
A phrase must appear in both contracts in a pair.
Use it to focus a run on a state, candidate, asset, league, or macro topic.

#### `maxItems`

Maximum matched records saved to the default dataset.
Allowed range: 1–1,000.
Default: 25.

#### `maxMarketsPerVenue`

Maximum open binary contracts loaded from each venue.
Allowed range: 100–5,000.
Default: 1,500.

Higher coverage takes longer but may find more current overlap.

#### `minMatchConfidence`

Minimum lexical, numeric, and expiry confidence.
Allowed range: 0.2–1.
Default: 0.4.

Lower values increase recall and false-match risk.
Raise the value for precision-sensitive workflows.

#### `maxExpiryDifferenceHours`

Reject pairs whose displayed expiries differ by more than this value.
Default: 720 hours.

Different timestamps can still represent similar settlement periods, so always inspect the rules.

#### `estimatedFeesPercent`

Combined fee and slippage estimate subtracted from the gross spread.
Default: 2%.

This is an analytical assumption, not a source-provided quote.

#### `minNetSpreadPercent`

Threshold used for `candidateArbitrage` and, when filtering is enabled, dataset inclusion.
Default: 0%.

#### `minLiquidityUsd`

Require both source contracts to report at least this displayed liquidity.
Default: 0.

A zero value preserves contracts where liquidity is omitted.

#### `includeNonProfitable`

When `true`, matched contracts remain useful for cross-market monitoring even if the estimated spread is below the threshold.
When `false`, only threshold-meeting pairs are saved.

### Example input

```json
{
  "keywords": ["Alexandria Ocasio-Cortez"],
  "maxItems": 10,
  "maxMarketsPerVenue": 5000,
  "minMatchConfidence": 0.4,
  "maxExpiryDifferenceHours": 720,
  "estimatedFeesPercent": 2,
  "minNetSpreadPercent": 0,
  "includeNonProfitable": true
}
```

This current-data example returns matched 2028 nomination propositions while they remain open on both venues.
Prediction-market inventories change, so a topic can naturally return no records later.

### Example output

```json
{
  "rank": 1,
  "matchId": "a761264d80a3dd2169e7",
  "matchedTitle": "2028 Democratic presidential nominee: Will Alexandria Ocasio-Cortez be the Democratic Presidential nominee in 2028?",
  "strategy": "Buy Kalshi YES + Polymarket NO",
  "candidateArbitrage": true,
  "combinedCost": 0.98,
  "grossSpreadPercent": 2,
  "estimatedFeesPercent": 2,
  "estimatedNetSpreadPercent": 0,
  "matchConfidence": 0.893,
  "matchReasons": [
    "7 shared terms: 2028, democratic, presidential, nominee, alexandria, ocasio, cortez",
    "6 shared adjacent phrases",
    "matching numeric terms: 2028",
    "contract expiries differ by 15 hours"
  ],
  "expiryDifferenceHours": 15,
  "kalshi": {
    "venue": "Kalshi",
    "marketId": "KXPRESNOMD-28-AOC",
    "url": "https://kalshi.com/markets?search=KXPRESNOMD-28-AOC",
    "yesAsk": 0.18,
    "noAsk": 0.83
  },
  "polymarket": {
    "venue": "Polymarket",
    "marketId": "559653",
    "url": "https://polymarket.com/event/democratic-presidential-nominee-2028",
    "yesAsk": 0.201,
    "noAsk": 0.8
  },
  "observedAt": "2026-08-15T07:01:59.971Z"
}
```

Quotes move continuously.
The example shows the record shape, not a current recommendation.

### How matching confidence works

The matcher first creates candidate pairs sharing at least one indexed term.
It then scores:

- overlap relative to the shorter title;
- Jaccard overlap across both titles;
- matching numeric proposition terms;
- displayed expiry proximity.

It rejects pairs with conflicting:

- political parties;
- political offices;
- above/below or before/after directions;
- increase/decrease directions;
- numeric proposition terms.

`matchReasons` exposes the supporting evidence.
Even a score of `0.99` is not a settlement guarantee.

### How candidate spread calculations work

For each pair, the Actor compares:

```text
Kalshi YES ask + Polymarket NO ask
Polymarket YES ask + Kalshi NO ask
```

It selects the cheaper displayed combination.

```text
gross spread % = (1 - combined cost) × 100
estimated net spread % = gross spread % - estimatedFeesPercent
```

The calculation does not model order-book depth, partial fills, deposits, withdrawals, taxes, changing quotes, or venue-specific settlement risk.

### How much does it cost to monitor Polymarket and Kalshi spreads?

The Actor uses pay-per-event pricing:

- one `start` event per run;
- one `item` event for each matched record actually saved;
- no charge for rejected, duplicate, or failed candidate pairs.

The active rates are a **$0.01 start** plus the applicable tiered item rate.
At the BRONZE item rate of **$0.0016 per match**, example totals are:

| Saved matches | Example total |
| ---: | ---: |
| 1 | $0.0116 |
| 10 | $0.026 |
| 100 | $0.17 |

Apify account tiers can apply different item rates.
Always check the pricing panel for the active tier before a run.
Compute usage is covered by the Actor under PPE pricing.

### Scheduling recurring arbitrage monitoring

Create an Apify Schedule with a stable input.
For monitoring, keep `includeNonProfitable` enabled so a spread history does not disappear when it crosses zero.

A useful workflow is:

1. run every 15–60 minutes;
2. export dataset rows to a table keyed by `matchId` and `observedAt`;
3. compare the latest two observations;
4. alert only after your own confidence, liquidity, rule-equivalence, and fee checks;
5. retain source URLs for auditability.

The Actor itself does not place orders or send trading alerts.

### Export to spreadsheets, databases, and BI tools

The default dataset works with:

- CSV and Excel exports;
- Google Sheets integrations;
- webhooks and Make;
- Zapier;
- REST API clients;
- Python notebooks;
- JavaScript data pipelines;
- BigQuery, Snowflake, or PostgreSQL loaders;
- dashboard tools such as Looker Studio and Power BI.

Use `matchId` as a cross-run pair key.
Use `observedAt` as the observation time.
Do not use `rank` as a stable identifier.

### Run through the Apify API with cURL

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/automation-lab~polymarket-kalshi-arbitrage-monitor/runs?token=$APIFY_TOKEN&waitForFinish=300" \
  -H "Content-Type: application/json" \
  -d '{
    "keywords": ["Alexandria Ocasio-Cortez"],
    "maxItems": 10,
    "maxMarketsPerVenue": 5000,
    "minMatchConfidence": 0.4,
    "includeNonProfitable": true
  }'
```

Fetch dataset items from the `defaultDatasetId` returned by the run.
Do not put tokens in committed source code.

### Run with JavaScript

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('automation-lab/polymarket-kalshi-arbitrage-monitor').call({
  keywords: ['Alexandria Ocasio-Cortez'],
  maxItems: 10,
  maxMarketsPerVenue: 5000,
  minMatchConfidence: 0.4,
  includeNonProfitable: true,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

### Run with Python

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("automation-lab/polymarket-kalshi-arbitrage-monitor").call(
    run_input={
        "keywords": ["Alexandria Ocasio-Cortez"],
        "maxItems": 10,
        "maxMarketsPerVenue": 5000,
        "minMatchConfidence": 0.4,
        "includeNonProfitable": True,
    }
)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

### Use with Apify MCP

Add the Actor to Claude Code:

```bash
claude mcp add --transport http apify \
  "https://mcp.apify.com?tools=automation-lab/polymarket-kalshi-arbitrage-monitor"
```

#### Claude Desktop setup

Add this server to Claude Desktop's MCP configuration:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com?tools=automation-lab/polymarket-kalshi-arbitrage-monitor"
    }
  }
}
```

#### Cursor setup

Add the same `apify` HTTP server URL under Cursor **Settings → MCP** and enable it for the workspace.

#### VS Code setup

Add the same `apify` HTTP server URL to the VS Code MCP server configuration, then start the server from the MCP tools panel.

Example prompts:

- “Run the Polymarket Kalshi monitor for Alexandria Ocasio-Cortez and summarize matched prices.”
- “Scan both platforms, keep non-profitable matches, and return the five highest-confidence pairs.”
- “Run with zero estimated fees and show positive displayed gross spreads for manual review.”

Treat model summaries as secondary analysis; preserve the source dataset and URLs.

### Limits and failure behavior

- The public source APIs can change, rate-limit, or temporarily fail.
- The Actor retries transient failures and uses a second public Kalshi API host.
- Deterministic source errors fail the run rather than returning fabricated or stale records.
- Coverage is bounded to 5,000 open contracts per venue to keep runtime and pair evaluation predictable.
- A successful run can contain zero records when no contracts meet all filters.
- Keywords apply to both sides of a pair.
- Non-binary and Kalshi multivariate contracts are excluded.
- Prices are snapshots and can change before any order is placed.
- Displayed liquidity does not guarantee executable depth at the shown price.
- Similar wording does not guarantee identical resolution criteria.

### Responsible use and legality

The Actor accesses public, unauthenticated market-data endpoints.
Use the output in accordance with Apify's terms, each source's terms, and laws applicable to you.

Prediction-market access and trading rules differ by location.
This Actor does not bypass login, geofencing, or access controls.
It does not provide financial, legal, tax, or investment advice.
It does not execute trades.

Before relying on a pair:

1. open both source links;
2. compare the complete settlement wording and authoritative sources;
3. confirm contract side orientation;
4. inspect live order-book depth;
5. account for every fee and transfer cost;
6. confirm your eligibility to use each venue;
7. make your own independent decision.

### Troubleshooting

#### Why did the run return no matches?

Current overlap may be naturally sparse.
Remove keywords, increase `maxMarketsPerVenue`, keep `includeNonProfitable` enabled, or cautiously lower `minMatchConfidence`.
Do not lower confidence merely to force a result.

#### Why was an apparent opportunity omitted?

The pair may have conflicting numbers, direction words, political parties, offices, expiry distance, liquidity, or confidence.
It may also sit beyond the bounded open-contract window.

#### Why is an obvious false match present?

No automated semantic matcher is perfect.
Raise `minMatchConfidence`, add a topic keyword, and report the two source URLs and IDs for reproducible improvement.
Never trade solely from the Actor's candidate label.

#### Why is `candidateArbitrage` false?

The pair is still a useful cross-market comparison, but its estimated net spread is below `minNetSpreadPercent` after subtracting `estimatedFeesPercent`.
Set `includeNonProfitable` to `false` to omit those rows.

#### Does this guarantee risk-free profit?

No.
Settlement wording, timing, fills, depth, fees, eligibility, transfers, and price movement create material risk.
The Actor outputs candidates for verification, not guarantees.

### Related automation-lab Actors

- [Polymarket Markets Scraper](https://apify.com/automation-lab/polymarket-markets-scraper) exports individual Polymarket markets without cross-venue matching.
- [Kalshi Markets Scraper](https://apify.com/automation-lab/kalshi-markets-scraper) exports detailed Kalshi markets and filters without cross-venue matching.

Use the source-specific Actors when you need broader raw inventory or venue fields.
Use this Actor when the buyer job is matched cross-market monitoring and spread calculation.

### FAQ

#### Are prices live?

They are public API snapshots observed during the run, not streaming quotes.
Schedule repeated runs for a time series.

#### Can I search only one venue?

No.
This product emits cross-venue matches.
Use the related source-specific Actor for single-venue exports.

#### Can I add multiple keywords?

Yes.
A contract is accepted when any configured phrase appears, but each pair must pass the same keyword scope on both venues.

#### Does the Actor need a proxy or account?

No.
It uses public anonymous APIs and exposes no automatic paid-proxy fallback.

#### Does it place trades?

No.
It only exports analytical records.

#### How should I deduplicate scheduled results?

Use `matchId` for the pair and `observedAt` for each snapshot.

#### Can I use the data in an agent workflow?

Yes.
Use Apify MCP or the REST API, but require the agent to show both source URLs, confidence reasons, and settlement-rule caveats.

### Support

For reproducible help, include:

- the Actor run URL;
- exact input;
- the two source market IDs or URLs;
- expected versus observed behavior;
- whether the source pages and APIs were available at run time.

Do not include API tokens, credentials, or private trading information.

# Actor input Schema

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

Optional topics that must appear in both matched contracts, such as Bitcoin, election, or Fed.

## `maxItems` (type: `integer`):

Maximum ranked contract matches saved to the dataset.

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

Maximum open binary contracts loaded from each prediction market platform.

## `minMatchConfidence` (type: `number`):

Explainable lexical, numeric, and expiry similarity threshold. Raise this to trade coverage for precision.

## `maxExpiryDifferenceHours` (type: `integer`):

Reject pairs whose displayed contract expiries differ by more than this amount.

## `estimatedFeesPercent` (type: `number`):

User-supplied fee and slippage estimate subtracted from the gross two-leg spread.

## `minNetSpreadPercent` (type: `number`):

Threshold used to label a pair as a candidate arbitrage opportunity.

## `minLiquidityUsd` (type: `number`):

Require both contracts to report at least this displayed liquidity. Zero keeps contracts where a venue omits liquidity.

## `includeNonProfitable` (type: `boolean`):

Keep equivalent-market comparisons even when the current fee-adjusted spread is below the candidate threshold.

## Actor input object example

```json
{
  "maxItems": 20,
  "maxMarketsPerVenue": 1500,
  "minMatchConfidence": 0.4,
  "maxExpiryDifferenceHours": 720,
  "estimatedFeesPercent": 2,
  "minNetSpreadPercent": 0,
  "minLiquidityUsd": 0,
  "includeNonProfitable": true
}
```

# Actor output Schema

## `overview` (type: `string`):

Open the ranked overview view in the default dataset.

# 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 = {
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("automation-lab/polymarket-kalshi-arbitrage-monitor").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 = { "maxItems": 20 }

# Run the Actor and wait for it to finish
run = client.actor("automation-lab/polymarket-kalshi-arbitrage-monitor").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 '{
  "maxItems": 20
}' |
apify call automation-lab/polymarket-kalshi-arbitrage-monitor --silent --output-dataset

```

## MCP server setup

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

```

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/f4K6QPCD9Bski3sf9/builds/xM4T8aiTyNBwbAfeE/openapi.json
