# Prediction Spread Scanner — Kalshi × Polymarket (`nanare-sudo/prediction-spread-scanner`) Actor

Cross-venue prediction market data: normalized Kalshi + Polymarket markets, orderbooks, LLM-assisted event matching with confidence scores, and executable spread analysis against real orderbook depth including fees. Research/data tool for quants, dashboards and AI agents.

- **URL**: https://apify.com/nanare-sudo/prediction-spread-scanner.md
- **Developed by:** [Leonardo Berisha](https://apify.com/nanare-sudo) (community)
- **Categories:** Agents, Other, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 verified cross-venue matches

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

## Prediction Spread Scanner — Kalshi × Polymarket

Get **Kalshi and Polymarket prediction market data** in one normalized schema, with **verified cross-venue event matching** (confidence-scored, with resolution-difference notes) and — the part no market overview gives you — the **executable spread against real orderbook depth**: what the price difference between both venues actually is once you walk the book at a $100 or $1,000 order size and subtract Kalshi's trading fee. A research and data tool for market analysts, academics, quants and AI agents. No API keys required.

**This is a data product, not a trading tool.** It reads public, unauthenticated market-data endpoints, places no orders, and its output is market research data — not financial advice and not an inducement to trade. Whether and where you may use prediction market platforms depends on your local law.

### What is Prediction Spread Scanner?

Kalshi and Polymarket often list markets on the same real-world event — elections, Fed decisions, economic data, sports, crypto prices — at different prices. Comparing them naively fails twice: keyword matching pairs markets that resolve differently, and top-of-book price gaps evaporate once fees and orderbook depth are taken into account. This actor solves both:

- **Normalized markets** — both venues in one schema: title, category, close time, `yes_bid`/`yes_ask`/`yes_price` (always 0..1 dollars), 24h volume, liquidity, CLOB token ids. Parses Kalshi's new August-2026 `*_dollars`/`*_fp` field generation *and* the legacy integer-cent fields.
- **3-stage verified matching** — (1) blocking by category and close-date window, (2) IDF-weighted token-overlap scoring over the loaded market universe (no external embedding service), (3) rule verification of resolution criteria: numeric thresholds, years, months, comparison direction, and entity/office contrast markers (nominee vs. election winner, VP vs. president, "Jr." suffixes). Every match carries `match_confidence` (0–1) and `resolution_diff_notes` telling you exactly what to double-check.
- **Optional LLM verification** — bring your own OpenRouter-compatible API key and stage 3 is done by an LLM comparing the actual resolution rules; without a key, verification is rule-based and records carry `llm_verified: false`.
- **Executable spread analysis** — for each verified pair, both orderbooks are fetched (Polymarket CLOB + Kalshi, with the YES ask correctly derived from resting NO bids) and the net spread is simulated by walking price levels at your target order sizes: `executable_spread_100`, `executable_spread_1000`, fees, slippage, `depth_limited`, and the direction (`buy_yes_kalshi_sell_polymarket` or `buy_yes_polymarket_sell_kalshi`). Kalshi's fee formula `0.07 × p × (1−p)` is applied per fill level on the Kalshi leg; Polymarket CLOB fees are currently zero (kept as a documented constant).
- **Persistent match cache** — verified pairs are stored in a named key-value store, so repeat runs skip re-verification: faster, cheaper, and no repeated LLM cost.
- **Free modes** — `discover` (category/volume overview of both venues) and `canary` (health self-test of every endpoint) charge nothing at all.

### Creator benchmark (reproducible, 2026-08-19)

Not a user review — a run you can reproduce yourself with the default input:

| Metric | Value |
|---|---|
| Markets loaded | 1,600 (800 per venue) |
| Candidate pairs after blocking | 312 |
| Verified matches (median confidence 0.83) | 17 |
| Median **gross** spread (midpoint difference) | $0.0045 |
| Largest gross spread | $0.0385 |
| Median **executable** spread @ $100 | $0.0006 |
| **Pairs with negative executable spread @ $100** | **47%** |
| Runtime / cost | 69 s / $0.02 |

That last row is the reason this actor exists: almost half of the price differences that a naive
"arbitrage scanner" would report **disappear or go negative** once fees and orderbook depth are
applied. This tool tells you which ones survive — and how confident the underlying market match is.

**Open methodology, sample data and code:**
[github.com/nanare-sudo/kalshi-polymarket-spreads](https://github.com/nanare-sudo/kalshi-polymarket-spreads)
— sample matches/spreads (CSV + JSON), an executed Jupyter notebook, Python/JavaScript/MCP
examples, and the full matching + fee methodology.

### Input

```json
{
    "mode": "scan",
    "maxMarketsPerVenue": 800,
    "orderSizesUsd": [100, 1000],
    "minConfidence": 0.65,
    "spreadSignalThreshold": 0.01
}
```

Modes: `scan` (full pipeline, default) · `markets` (normalized list, one or both venues) · `orderbooks` · `matches` (matching only, no orderbooks) · `discover` (free) · `canary` (free). Narrow the universe with `titleFilter` (e.g. `"fed"`, `"midterm"`) or `categoriesFilter` (`politics`, `economics`, `crypto`, `sports`, …).

### Output

A verified match with spread analysis (real record from a test run, Aug 2026):

```json
{
    "type": "spread",
    "pair_id": "KXPRESNOMR-28-RDS__561979",
    "kalshi": { "ticker": "KXPRESNOMR-28-RDS", "title": "Will Ron DeSantis be the nominee for the Presidency for the Republican party?", "best_bid": 0.053, "best_ask": 0.054, "midpoint": 0.0535 },
    "polymarket": { "id": "561979", "question": "Will Ron DeSantis win the 2028 Republican presidential nomination?", "best_bid": 0.028, "best_ask": 0.029, "midpoint": 0.0285 },
    "match_confidence": 0.74,
    "llm_verified": false,
    "resolution_diff_notes": ["close times differ by 15h"],
    "direction": "buy_yes_polymarket_sell_kalshi",
    "gross_spread_midpoint": 0.025,
    "executable_spread_100": 0.0197,
    "executable_spread_1000": 0.0167,
    "fees_total": { "100": 11.94, "1000": 108.30 },
    "depth_limited": false,
    "signal": true
}
```

`executable_spread_100` is the net edge per share in dollars after walking both books for a $100 buy leg and applying fees — here ~2 cents/share survive out of a 2.5-cent midpoint gap, and at $1,000 slippage compresses it to 1.7 cents. Records where depth runs out are flagged `depth_limited: true`; spreads can be negative once fees eat the gap — that is the point of computing them.

### Pricing

Pay per event — you only pay for data you actually receive:

| Event | Price |
|---|---|
| Actor start | $0.05 |
| Normalized market (markets mode) | $0.0015 |
| Orderbook pair | $0.002 |
| Verified cross-venue match | $0.02 |
| Executable spread signal (net spread ≥ your threshold) | $0.05 |

`discover` and `canary` are completely free. A typical full `scan` (both venues, ~20 verified matches, a few signals) costs well under $1 — testable on Apify's free plan credits.

### Use with AI agents (MCP)

Every mode works through the [Apify MCP server](https://mcp.apify.com) — point your agent (Claude, ChatGPT, LangGraph, CrewAI …) at Apify and it can call this actor directly, e.g. *"scan Kalshi and Polymarket for matched markets on the 2026 midterms and report the executable spreads at $500"*. One `mode` switch, sensible defaults, stable output schemas, no API keys needed.

### FAQ

**Is this legal?** The actor only reads public, unauthenticated market-data APIs that both exchanges document and rate-limit for programmatic use. It does not log in, does not place orders, does not collect personal data, and does not circumvent any access control. It is a market data / research tool; what you do with the data is up to you and your local law.

**Is this financial advice? Does a positive spread mean free money?** No and no. Output is research data. A computed spread is a snapshot that ignores execution risk, settlement-rule differences between venues (read `resolution_diff_notes`!), capital lockup until resolution, withdrawal costs, and platform access restrictions in your jurisdiction. Prices can move before any order would fill.

**How is this different from naive keyword arb finders?** Three ways: matching is verified in 3 stages with a confidence score instead of raw keyword overlap; spreads are computed against actual orderbook depth at concrete order sizes instead of top-of-book; and Kalshi's `0.07·p·(1−p)` fee is netted out per fill level. Wide "spreads" that die on fees or 5-contract books are labeled as such.

**Which API generations does it parse?** Both: Kalshi's new `*_dollars`/`*_fp` string fields (Aug 2026) with fallback to legacy integer cents, and Polymarket Gamma's JSON-string-encoded arrays. The Kalshi YES ask is derived from resting NO bids (`1 − best_no_bid`), and the Polymarket CLOB's unsorted ask arrays are re-sorted before walking.

**Do repeat runs get cheaper?** Yes — verified pairs persist in the `match-cache` key-value store (TTL configurable). A follow-up scan re-verifies nothing that is still fresh and goes straight to the orderbooks.

**What about rate limits?** The actor batches sequentially with small delays, far below the documented limits (Gamma 300–500 req/10s, CLOB 1,500 req/10s, Kalshi ~20 read/s), retries with exponential backoff and fails over to Kalshi's fallback host.

### Keywords

polymarket api, kalshi api, prediction market data api, polymarket kalshi spread, cross-market comparison, prediction market arbitrage data, orderbook depth, executable spread, event matching, prediction market odds, kalshi orderbook, polymarket clob orderbook, midterms 2026 prediction markets, market research data, cross-venue price differences

# Actor input Schema

## `mode` (type: `string`):

`scan` = full pipeline: load both venues, 3-stage event matching, orderbook pairs, executable spread analysis. `markets` = normalized market list (one or both venues). `orderbooks` = normalized YES-side orderbooks. `matches` = cross-venue matching only, no orderbooks. `discover` = free category/counter overview of both venues. `canary` = free health self-test of every endpoint.

## `venues` (type: `string`):

Which venue(s) to load in `markets`, `orderbooks` and `discover` mode. `scan` and `matches` always use both.

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

Cap on normalized markets loaded per venue (most liquid first). Larger = better matching coverage, longer runs.

## `titleFilter` (type: `string`):

Case-insensitive substring filter on market/event titles, e.g. `fed`, `midterm`, `bitcoin`. Applied on both venues after loading.

## `categoriesFilter` (type: `array`):

Restrict to coarse categories: `politics`, `economics`, `crypto`, `sports`, `sci_tech`, `weather`, `culture`, `health`, `other`. Empty = all.

## `minVolume24h` (type: `integer`):

Drop markets with 24h volume below this (venue-native units: Kalshi contracts, Polymarket USD). 0 = no filter.

## `minLiquidity` (type: `integer`):

Drop markets with reported liquidity below this. 0 = no filter.

## `closeDateWindowDays` (type: `integer`):

Stage-1 blocking: only pair markets whose close times are within ± this many days. Markets with a missing close date pass with a note.

## `minMatchScore` (type: `number`):

Stage-2 threshold (0..1): minimum IDF-weighted token overlap for a pair to reach verification.

## `minConfidence` (type: `number`):

Stage-3 threshold (0..1): minimum verified confidence for a pair to count (and be charged) as a match.

## `orderSizesUsd` (type: `array`):

Target order sizes for the executable-spread simulation (level-walking against real orderbook depth). Default \[100, 1000] produces `executable_spread_100` and `executable_spread_1000`.

## `spreadSignalThreshold` (type: `number`):

A pair counts (and is charged) as `executable-spread-signal` only when the net executable spread per share at some order size reaches this value. `0.01` = 1 cent/share.

## `maxPairsToScan` (type: `integer`):

Cap on verified matches whose orderbook pairs are fetched and analyzed in `scan` mode.

## `maxOrderbooks` (type: `integer`):

Cap on orderbooks fetched in `orderbooks` mode.

## `includeCandidates` (type: `boolean`):

Also push up to 20 below-threshold candidate pairs with their scores and diff notes (free, not charged) — useful to tune thresholds.

## `useMatchCache` (type: `boolean`):

Cache verification results in the named key-value store `match-cache`. Repeat runs skip re-verification (faster, and no repeated LLM cost).

## `cacheTtlHours` (type: `integer`):

Cached verifications older than this are re-verified.

## `llmApiKey` (type: `string`):

Optional OpenRouter-compatible API key. When set, stage-3 verification of top candidates is done by an LLM (resolution-rule comparison); otherwise verification is purely rule-based and records carry `llm_verified: false`.

## `llmModel` (type: `string`):

Model id for the OpenRouter-compatible endpoint.

## `llmBaseUrl` (type: `string`):

OpenAI-compatible chat-completions base URL.

## `maxLlmCalls` (type: `integer`):

Cost cap for LLM verification. Candidates beyond this fall back to rule-based confidence.

## Actor input object example

```json
{
  "mode": "scan",
  "venues": "both",
  "maxMarketsPerVenue": 800,
  "categoriesFilter": [],
  "minVolume24h": 0,
  "minLiquidity": 0,
  "closeDateWindowDays": 1,
  "minMatchScore": 0.2,
  "minConfidence": 0.65,
  "orderSizesUsd": [
    100,
    1000
  ],
  "spreadSignalThreshold": 0.01,
  "maxPairsToScan": 40,
  "maxOrderbooks": 50,
  "includeCandidates": false,
  "useMatchCache": true,
  "cacheTtlHours": 168,
  "llmModel": "openai/gpt-4o-mini",
  "llmBaseUrl": "https://openrouter.ai/api/v1",
  "maxLlmCalls": 40
}
```

# Actor output Schema

## `records` (type: `string`):

All result records (type field: market | orderbook-pair | match | match-candidate | spread-signal | venue-category | canary) as JSON array.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("nanare-sudo/prediction-spread-scanner").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("nanare-sudo/prediction-spread-scanner").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 '{}' |
apify call nanare-sudo/prediction-spread-scanner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nanare-sudo/prediction-spread-scanner"
        }
    }
}

```

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/oGvkQHUjlfxUrGLvj/builds/1mhii6AbIfueamtGV/openapi.json
