# Kalshi Sports Betting Odds – NFL, NBA, MLB & NHL Lines (`rowfeed/kalshi-sports-odds-scraper`) Actor

One row per game from Kalshi, the CFTC-regulated exchange: both teams' prices, vig-free probabilities, overround, volume, spreads, totals and price history. NFL, NBA, MLB, NHL, EPL, UCL and more.

- **URL**: https://apify.com/rowfeed/kalshi-sports-odds-scraper.md
- **Developed by:** [Rowfeed](https://apify.com/rowfeed) (community)
- **Categories:** Sports, Developer tools, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 games

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

## Kalshi Sports Odds Scraper – Game Lines, Spreads & Totals

Get live Kalshi sports odds as one clean JSON row per game: moneyline prices for both teams, joined spread and total lines, and vig-removed fair probabilities.
Built for odds bots, sports dashboards and AI agents that need to ask "what are the Kalshi odds for this game right now" without a login, an API key or a headless browser.
Kalshi is the only CFTC-regulated, licensed exchange for sports event contracts in the US, so its prices come from real money changing hands, not a bookmaker's model. Plain HTTPS calls with retries and a silent-failure check, so a scheduled run keeps working when the exchange hiccups.

### What you get

- **One row per game** – both teams' YES price, bid, ask, mid, implied probability, volume and open interest, plus a 3-way `draw` leg whenever Kalshi lists a Draw/Tie market (soccer, including the "Reg Time: Tie" variant; `null` for two-way sports).
- **Spreads and totals joined in** – every point-spread and total-points/goals market for the game attached as `spreads`/`totals` lists, matched to the moneyline by Kalshi's own event code.
- **Vig removed for you** – `overround` (the book's built-in edge) and `fair_probability_a`/`_b`/`_draw` (each side's mid price re-normalised to sum to 1) computed for every game still trading, plus `favorite`.
- **Finished games show the result** – with `status: "settled"` each side carries Kalshi's own `result` and payout instead of a stale price (see "Settled and closed games" below).
- **Covers 17 leagues** – NFL, NBA, MLB, NHL, EPL, NCAAF, NCAAB, UCL, La Liga, Serie A, Bundesliga, Ligue 1, MLS, WNBA, ATP and WTA tennis, and UFC fights (one row per fight, the two fighters in `team_a`/`team_b`), or poll one game directly by its event ticker.

### Sample row

One open NFL game, `maxGames: 1`. Trimmed here: `spreads`/`totals` to one entry each (a real run returns every joined market), and `team_b` to its prices (real rows give it the same fields as `team_a`).

```json
{
  "event_ticker": "KXNFLGAME-26SEP24ATLGB",
  "league": "NFL",
  "series_ticker": "KXNFLGAME",
  "title": "Atlanta vs Green Bay",
  "sub_title": "ATL vs GB (Sep 24)",
  "expires_at": "2026-09-25T06:15:00Z",
  "close_time": "2026-09-27T00:15:00Z",
  "status": "active",
  "team_a": {
    "name": "Green Bay",
    "ticker": "KXNFLGAME-26SEP24ATLGB-GB",
    "last_price": 0.71,
    "yes_bid": 0.70,
    "yes_ask": 0.71,
    "mid": 0.705,
    "implied_probability": 0.705,
    "volume": 864084.8,
    "volume_24h": 448478.57,
    "open_interest": 687846.03
  },
  "team_b": {
    "name": "Atlanta",
    "ticker": "KXNFLGAME-26SEP24ATLGB-ATL",
    "last_price": 0.30,
    "yes_bid": 0.29,
    "yes_ask": 0.30,
    "mid": 0.295,
    "implied_probability": 0.295
  },
  "draw": null,
  "overround": 0.01,
  "fair_probability_a": 0.705,
  "fair_probability_b": 0.295,
  "favorite": "Green Bay",
  "spreads": [
    { "ticker": "KXNFLSPREAD-26SEP24ATLGB-GB28", "yes_sub_title": "GB Packers wins by over 27.5 points", "floor_strike": 27.5, "yes_bid": 0.04, "yes_ask": 0.05 }
  ],
  "totals": [
    { "ticker": "KXNFLTOTAL-26SEP24ATLGB-26", "yes_sub_title": "Over 25.5 points scored", "floor_strike": 25.5, "yes_bid": 0.93, "yes_ask": 0.94 }
  ],
  "url": "https://kalshi.com/markets/kxnflgame",
  "scraped_at": "2026-09-23T16:39:50+00:00"
}
```

Every row also carries `home_team`/`away_team` (always `null` - Kalshi doesn't label a home side, and this Actor never guesses), `start_time` (always `null` for the same honesty reason - use `expires_at`/`close_time` instead), and `raw_event` with Kalshi's own event fields.

### Filters

| Input | Default | What it does |
|---|---|---|
| `leagues` | `["NFL","NBA","MLB","NHL","EPL","NCAAF","UCL"]` | Leagues to scrape. Also: NCAAB, UCL, LALIGA, SERIEA, BUNDESLIGA, LIGUE1, MLS, WNBA, ATP, WTA, UFC. |
| `seriesTickers` | `[]` | Extra game series by ticker (advanced), e.g. `["KXWNBAGAME"]`, for leagues not in the list above. |
| `eventTickers` | `[]` | Poll specific games directly, e.g. `["KXNFLGAME-26SEP24ATLGB"]`. An unknown ticker becomes an uncharged error row and never fails the run: it is your input, not an outage. |
| `team` | `""` | Case-insensitive substring match against the title and both team names, e.g. `"Green Bay"` or `"GB"`. |
| `status` | `open` | `open`, `closed`, `settled` or `all`. What settled and closed rows contain is described below. |
| `includeSpreadsTotals` | `true` | Join spread/total markets onto each row. |
| `includeHistory` | `false` | Add 7 days of hourly candlesticks for both moneyline markets. |
| `maxGames` | `100` | Cap on rows (1-1000), soonest-expiring first; `settled` and `closed` games most recent first. |

Leagues or games with nothing matching the status filter simply contribute zero rows - not an error. A default run (no filters) returns whichever of the 7 default leagues currently have games in that status.

Tennis and UFC have moneyline markets only, so `spreads` and `totals` are empty lists there. A newly listed fight or match that nobody has traded yet can show placeholder quotes (for example bid 0.05 / ask 0.83 on both fighters of a Contender Series bout, with zero `volume`); check `team_a.volume` and `team_b.volume` before treating those prices as a market view.

### Settled and closed games

`status: "settled"` returns finished games. When Kalshi settles a market it empties the order book, so every leg reads `yes_bid` 0 and `yes_ask` 1 and there is no price left. On those rows (`status` is `finalized`) the fields `mid`, `implied_probability`, `fair_probability_a`/`_b`/`_draw`, `overround` and `favorite` are `null`, never a made-up 50/50. Instead, each leg and each joined spread or total market carries Kalshi's own outcome:

- `result`: `"yes"` for the side that won, `"no"` for the others, and `"scalar"` when Kalshi paid out a fraction (an NFL tie pays half on both teams).
- `settlement_value`: what one YES contract paid out, in dollars: `1.0`, `0.0`, or the fraction (`0.5` for a tie).

`last_price` is kept as Kalshi reports it. On a settled leg it is the final trade (usually 0.99 or 0.01), not a pre-game price. Here is the sample game above after it finished, trimmed to the fields that change:

```json
{
  "event_ticker": "KXNFLGAME-26SEP24ATLGB",
  "status": "finalized",
  "team_a": { "name": "Green Bay", "last_price": 0.01, "yes_bid": 0.0, "yes_ask": 1.0, "mid": null, "implied_probability": null, "result": "no", "settlement_value": 0.0 },
  "team_b": { "name": "Atlanta", "last_price": 0.99, "yes_bid": 0.0, "yes_ask": 1.0, "mid": null, "implied_probability": null, "result": "yes", "settlement_value": 1.0 },
  "overround": null,
  "fair_probability_a": null,
  "fair_probability_b": null,
  "favorite": null
}
```

Settled rows come most recent first, so `maxGames` keeps the latest results. Kalshi only serves market data for recently settled games. On 25 September 2026 the cut-off was about 18 July for every league, so how far back you get depends on each league's calendar. MLB, tennis and UFC went back to mid-July. NFL and the big European soccer leagues only went back to their first games in August, NHL to its first preseason games in mid-September, and NBA returned nothing because its season ended in June. Older games have moved to Kalshi's archive and come back without markets, so the Actor skips them and counts them as `skipped_incomplete` in the run's `STATS` record. Use `status: "settled"` for recent results, not for season-long history.

`status: "closed"` returns games where trading has ended but Kalshi has not settled them yet. That window usually lasts minutes, so the list is often empty. Kalshi shows those markets with an empty book too, so the price fields are `null`, and `result` appears only once Kalshi has decided the outcome. Open games never carry `result` or `settlement_value`.

### Pricing

Pay per event, no subscription: **$1 per 1,000 games**, **$2 per 1,000 history fetches** and **$0.001 per run start** (kept tiny so you can poll one game at a time). Fetching spreads and totals is free - they ride along on the game row. Set a maximum charge on the run and the Actor stops cleanly when it is reached, charging only for rows actually saved.

### Details

- **Source**: Kalshi's public trade API v2 (`api.elections.kalshi.com/trade-api/v2`). No authentication, no proxies, no browser, no personal data. **Not affiliated with Kalshi.**
- **Prices are probabilities**: Kalshi is a real-money exchange, so every YES price is a dollar amount between $0 and $1 that a trader is willing to pay for a $1 payout if that outcome happens - which makes it, by construction, the market's implied probability of that outcome.
- **Overround and fair probability**: `overround` sums every leg's ask price and subtracts 1, so it's the total edge baked into the current asks (a healthy two-way market runs a few cents over 0). `fair_probability_*` divides each leg's mid price by the sum of all legs' mids, so the vig cancels out and the fair probabilities always sum to 1. A settled or closed game has no quote, so both are `null` there.
- **Reliability**: 429 and 5xx responses are retried with exponential backoff (5 tries), a 200 without the expected data counts as a failure, and one bad ticker never stops the run - it becomes an error row (`event_ticker`, `error`, `errorMessage`) and the rest continues. A run fails only when it produced no rows *and* the source failed (network, rate limit, blocked); an unknown ticker or a league with nothing open is a successful run with no charged rows.
- **Run stats**: the `STATS` record in the run's key-value store holds games, history fetches, error rows, skipped incomplete events, request and error counts per category.
- **Output**: one dataset row per game with the columns above, sorted soonest-expiring first (settled and closed games most recent first). Export as JSON, CSV or Excel, fetch through the Apify API, or schedule runs and pipe them into Google Sheets, Make, Zapier, n8n or your own code. Eligible for agentic use via Apify's MCP server.

# Actor input Schema

## `leagues` (type: `array`):

Leagues to scrape, one row per game. Defaults to the seven most-traded leagues. Leaves with no open games simply contribute zero rows, not an error.

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

Extra Kalshi game series to scrape by ticker, e.g. KXWNBAGAME, for leagues not in the list above. Spread/total series are looked up by replacing GAME with SPREAD/TOTAL.

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

Fetch exactly these game events, e.g. KXNFLGAME-26SEP24ATLGB. Useful for polling one game repeatedly. An unknown ticker becomes an error row instead of failing the run. When this is set (and seriesTickers is empty) only these games are fetched; the leagues sweep is skipped so per-game polling costs one row per game.

## `team` (type: `string`):

Case-insensitive substring match against the game title and both team names, e.g. "Green Bay" or "GB".

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

Which games to return: open (trading now), closed (trading ended, not yet settled), settled (final result known), or all.

## `includeSpreadsTotals` (type: `boolean`):

Join each game's point-spread and total-points/goals markets onto its row as `spreads`/`totals` lists. Off returns moneyline only, one fewer request pair per league.

## `includeHistory` (type: `boolean`):

Fetch the last 7 days of hourly candlesticks for each game's two moneyline markets. Adds one `history` event ($2 per 1,000) per game where at least one candlestick series was fetched.

## `maxGames` (type: `integer`):

Keep this many game rows after filtering: soonest-expiring first, or most recent first for settled and closed games. Each row is one `game` event ($1 per 1,000).

## Actor input object example

```json
{
  "leagues": [
    "NFL",
    "NBA",
    "MLB",
    "NHL",
    "EPL",
    "NCAAF",
    "UCL"
  ],
  "seriesTickers": [
    "KXWNBAGAME"
  ],
  "eventTickers": [
    "KXNFLGAME-26SEP24ATLGB"
  ],
  "team": "",
  "status": "open",
  "includeSpreadsTotals": true,
  "includeHistory": false,
  "maxGames": 100
}
```

# Actor output Schema

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

// Run the Actor and wait for it to finish
const run = await client.actor("rowfeed/kalshi-sports-odds-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 = {}

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rowfeed/kalshi-sports-odds-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/nBcvgxaNoqkdJtlBx/builds/Q9hlTbIshNLGQuHdS/openapi.json
