# Kalshi Markets Scraper (`scrapyx/kalshi-markets-scraper`) Actor

Prediction markets from Kalshi's public trade API: every open event and its markets with bid/ask/last in dollars, implied probability, volume, open interest, close and expiration times, strikes and rules, by category or series. Skips the exchange's auto-minted parlay markets.

- **URL**: https://apify.com/scrapyx/kalshi-markets-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 results

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/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

## Kalshi Markets Scraper

Prediction markets from **Kalshi**, the CFTC-regulated US exchange, via its
public trade API: every open event and each of its markets with Yes/No bid
and ask, last price, implied probability, volume and open interest in
contracts, open/close/expiration times, strikes, settlement rules, and the
event's title, category and market count on every row.

HTTP only, no key, no login, no browser. One request per 200 events.

### What it is for

- **Odds monitoring** by category or series, on a schedule.
- **Cross-exchange comparison** — pair with the Polymarket Markets Scraper on
  the same schedule.
- **Settlement research** — `status: settled` returns finalized markets with
  their results.

### Input

| field | what it does |
| --- | --- |
| `categories` | Politics, Elections, Economics, Financials, Crypto, Sports, … Empty = everything. |
| `seriesTickers` | e.g. `KXBTC`, `KXFED`, `KXNEWPOPE`. |
| `eventTickers` | e.g. `KXNEWPOPE-70`. |
| `status` | `open` (default), `closed`, `settled`, `unopened`. |
| `excludeMultivariate`, `minVolume`, `closingAfter`, `closingBefore`, `sortBy` | Filters — see below. |
| `includeEventRows` | Also emit one `EVENT` row per event. |
| `maxItems`, `maxConcurrency`, `minRequestInterval`, `proxyConfiguration` | Limits. |

### Five things about this API worth knowing before you trust a run

#### 1. `/markets` is 99.6% auto-minted parlays; `/events` is the catalogue

`/markets?status=open` returned 120,000+ rows without ending — 119,549 of
them `KXMVE…` combination markets ("yes Arsenal, yes Barcelona 1st half, …")
with zero volume, and the 451 real ones all brand new. A "list open markets"
built on it never reaches a real question. This Actor lists through `/events`
(13,538 open events, ends cleanly, zero parlays) and still flags and, by
default, excludes any multivariate market that arrives via a series or
ticker target.

#### 2. The documented price fields are null; the values are strings next door

`yes_bid: null` beside `yes_bid_dollars: "0.0420"`; `volume: null` beside
`volume_fp: "24113.00"`. On every one of 125,696 markets measured. Prices
here are floats in dollars (0–1), volume and open interest floats in
contracts, parsed from those strings.

#### 3. An event's status is not its markets' status

`status=settled` events carry markets that are still `active` (431 of 885 in
one page); `status=open` events carry markets already `finalized`. Each
market is kept only if its **own** status belongs to the family you asked
for; the rest are counted as `filteredOut.statusMismatch`.

#### 4. Some filters are honoured, some are silently ignored

`min_close_ts` works on `/events`; `max_close_ts` and `category` do not (the
page comes back unchanged). So `closingAfter` is sent upstream, while
`closingBefore` and `categories` are applied locally as the walk proceeds —
which means a category walk may read many pages to find its rows; the
summary reports how many events it skipped. `status` and `limit` are
validated loudly upstream (HTTP 400) and refused here first.

#### 5. Half of Kalshi is sports

Open events by category on the day this was built: Sports 6,994, Elections
3,351, Entertainment 709, Politics 643, Financials 621, Economics 552,
Climate 207, Science & Tech 142, Crypto 105, … A walk with no category is
mostly match markets. Every summary carries `categoriesSeen`.

### Other things measured

- **`liquidity` is `0` on every market** through both endpoints — a dead
  field upstream. It is emitted as received; `openInterest` is the real
  depth signal.
- There is no server-side sort. `sortBy` orders the rows a run collected
  (within `maxItems`), not the whole exchange.
- The API tolerated 40 unpaced requests in 11 s. Pages with nested markets
  are ~3.6 MB; a full open walk is 68 pages.
- URLs are `https://kalshi.com/markets/{series}/{event}` in lowercase; the
  Actor never requests kalshi.com itself (only the API origin).

### Output

- **`MARKET`** — `ticker`, `eventTicker`, `seriesTicker`, `title`,
  `yesSubTitle`, `noSubTitle`, `url`, `status`, `result`, `yesBid`, `yesAsk`,
  `noBid`, `noAsk`, `lastPrice`, `previousPrice`, `impliedProbability`,
  `spread`, `volume`, `volume24h`, `openInterest`, `liquidity`,
  `notionalValue`, `openTime`, `closeTime`, `expirationTime`, `hasClosed`,
  `canCloseEarly`, `earlyCloseCondition`, `marketType`, `strikeType`,
  `floorStrike`, `capStrike`, `customStrike`, `isMultivariate`,
  `rulesPrimary`, `eventTitle`, `eventCategory`, `eventMutuallyExclusive`,
  `eventMarketCount`, `eventUrl`, `seriesUrl`.
- **`EVENT`** (optional) — `eventTicker`, `seriesTicker`, `title`, `category`,
  `url`, `mutuallyExclusive`, `strikeDate`, `settlementSources`,
  `marketCount`, `outcomes` (ticker, label, lastPrice, volume),
  `totalVolume`, `totalOpenInterest`.
- **`SEARCH_SUMMARY`** — one per target: `eventsSeen`, `eventsReturned`,
  `marketsReturned`, `pagesFetched`, `stoppedReason`, `filteredOut`
  (category, multivariate, minVolume, closingBefore, statusMismatch),
  `categoriesSeen`, `closedRows`, `rowsWithoutQuote`.
- **`ERROR`** — `invalid_input`, `bad_request`, `payload_shape_changed`,
  `fetch_failed`, with detail.

### Known limits

- Snapshot prices only; order-book depth, trades and candlesticks are on
  other endpoints of the same API and not fetched.
- `volume24h` is `0` on most markets as served by the API.

# Actor input Schema

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

Kalshi categories to keep: Politics, Elections, Economics, Financials, Crypto, Commodities, Companies, Climate and Weather, Science and Technology, Entertainment, Sports, Mentions, Social, World, Health, AI. Empty = everything (about half of Kalshi is sports match markets). Applied locally while walking the exchange - the API's own category filter is inert.

## `seriesTickers` (type: `array`):

Restrict the walk to specific series, e.g. KXBTC (Bitcoin price ranges), KXNEWPOPE, KXFED. Each is its own target. Unknown tickers return nothing.

## `eventTickers` (type: `array`):

Specific events, e.g. KXNEWPOPE-70. One request each; an unknown ticker is reported as not\_found.

## `status` (type: `string`):

Which markets to list. The API rejects anything else with a 400.

## `excludeMultivariate` (type: `boolean`):

On by default. Kalshi auto-mints tens of thousands of KXMVE... combination markets ('yes Arsenal, yes Barcelona 1st half, ...') with zero volume; /markets is 99.6% of them. The events walk this Actor uses does not surface them, but series and ticker targets can; each row carries isMultivariate.

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

Skip markets with lifetime volume below this. Applied locally.

## `closingAfter` (type: `string`):

Only markets closing on or after this date, YYYY-MM-DD. Sent to the API.

## `closingBefore` (type: `string`):

Only markets closing on or before this date, YYYY-MM-DD. Applied locally (the API ignores its own max\_close\_ts on the events endpoint).

## `sortBy` (type: `string`):

The API has no sort; this orders the rows the run collected (within maxItems), not the whole exchange.

## `includeEventRows` (type: `boolean`):

Off by default: rows are MARKETs, each with its event's ticker, title, category and market count. On: an EVENT row with an outcome/price summary precedes each event's markets.

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

Overall cap on MARKET rows across every target. ~13,500 open events carry ~125,000 markets; a full walk is 68 pages of ~3.6 MB.

## `maxConcurrency` (type: `integer`):

Parallel requests across targets.

## `minRequestInterval` (type: `integer`):

Politeness delay between request starts. The API tolerated 40 unpaced requests in 11 s.

## `proxyConfiguration` (type: `object`):

Optional. Not needed: the API answered from a plain IP. Enable Apify's free datacenter proxy only if a cloud run reports fetch\_failed.

## Actor input object example

```json
{
  "categories": [
    "Politics"
  ],
  "status": "open",
  "excludeMultivariate": true,
  "sortBy": "none",
  "includeEventRows": false,
  "maxItems": 500,
  "maxConcurrency": 2,
  "minRequestInterval": 0,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# 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"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/kalshi-markets-scraper").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"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/kalshi-markets-scraper").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"
  ]
}' |
apify call scrapyx/kalshi-markets-scraper --silent --output-dataset

```

## MCP server setup

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

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/Y62xSPRY996jdbhwu/builds/PbQntgHsdE5GrLqDH/openapi.json
