# Sports Betting Odds – DraftKings, FanDuel, BetMGM & Kalshi (`rowfeed/sports-betting-odds-scraper`) Actor

Moneylines, spreads and totals from DraftKings, FanDuel, BetMGM and more for NFL, NCAAF, MLB, NHL and soccer, with no-vig fair probabilities for US sports and a Kalshi price join per game. Public betting splits when Action Network publishes them.

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

## Pricing

from $3.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

Get one clean JSON row per game with moneylines, spreads and totals from the major US sportsbooks (DraftKings, FanDuel, BetMGM, BetRivers and more), no-vig fair probabilities, and a Kalshi exchange price for comparison.
Built for odds bots, betting research and AI agents that need "what are the real odds for this game right now" across books in one call, without a login, an API key or a headless browser.
Covers NFL, NCAAF, MLB, NHL, WNBA and soccer (MLS). Plain HTTPS calls with retries, a silent-failure check and an automatic BetRivers/Kambi fallback, so a scheduled run keeps working when one source hiccups.

### What you get

- **One row per game** – moneyline, spread and total from every book Action Network tracks (DraftKings, FanDuel, BetMGM, BetRivers, Caesars, bet365 where posted, plus the Consensus and Open lines), each with its own `updated_at`.
- **The math done for you** – `best_moneyline_home`/`_away` (the highest-payout book per side), `implied_probability_*` (raw, vig included) and `fair_probability_*` (no-vig, re-normalised to sum to 1) – see "How the odds are computed" below.
- **A Kalshi price on the same row** – each game is matched to its Kalshi exchange event by team name and US Eastern date (plus start time where Kalshi's ticker carries one, as for MLB), adding `kalshi.home_probability`/`away_probability` and `kalshi_vs_books_edge` (Kalshi vs. the no-vig book price, per side). Unmatched games simply get `null` Kalshi fields – never guessed.

### Sample row

One NFL game, `books` trimmed to 3 of the 6 entries a real run returns.

```json
{
  "league": "NFL",
  "league_name": "nfl",
  "game_id": 290877,
  "start_time": "2026-09-27T17:00:00.000Z",
  "status": "scheduled",
  "home_team": "Pittsburgh Steelers",
  "away_team": "Cincinnati Bengals",
  "books": [
    { "book": "Consensus", "book_id": 15, "moneyline_home": 156, "moneyline_away": -190, "spread_home": 3.5, "spread_home_odds": -110, "spread_away": -3.5, "spread_away_odds": -109, "total": 42.5, "over_odds": -111, "under_odds": -108, "updated_at": "2026-09-24T18:01:26.158255+00:00" },
    { "book": "DK NJ", "book_id": 68, "moneyline_home": 154, "moneyline_away": -185, "spread_home": 3.5, "spread_home_odds": -105, "spread_away": -3.5, "spread_away_odds": -115, "total": 42.5, "over_odds": -110, "under_odds": -110, "updated_at": "2026-09-24T17:41:39.17149+00:00" }
  ],
  "best_moneyline_home": { "book": "FanDuel NJ", "odds": 160 },
  "best_moneyline_away": { "book": "Open", "odds": -125 },
  "implied_probability_home": 0.390625,
  "implied_probability_away": 0.6551724137931034,
  "fair_probability_home": 0.37351880473982485,
  "fair_probability_away": 0.6264811952601752,
  "probability_source": "consensus",
  "public_betting": { "moneyline": { "home_bets_pct": null, "away_bets_pct": null, "home_money_pct": null, "away_money_pct": null }, "spread": { "...": "..." }, "total": { "...": "..." } },
  "kalshi": { "event_ticker": "KXNFLGAME-26SEP27CINPIT", "home_yes_ask": 0.38, "away_yes_ask": 0.63, "home_probability": 0.375, "away_probability": 0.625 },
  "kalshi_vs_books_edge": { "home": 0.0015, "away": -0.0015 },
  "source": "action_network",
  "scraped_at": "2026-09-25T08:50:14+00:00",
  "url": "https://www.actionnetwork.com/nfl/odds"
}
```

You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

### Input

| Field | Default | What it does |
|---|---|---|
| `leagues` | `["nfl","ncaaf","mlb","nhl","soccer"]` | Also: `wnba`, `mls`, `nba`, `ncaab`. NBA and NCAAB have no games until October and simply contribute zero rows until then – not an error. |
| `books` | `[]` (all) | Action Network book ids to keep, e.g. `["68","69"]` for DraftKings + FanDuel only. Common ids: 15 Consensus, 30 Open, 68 DraftKings NJ, 69 FanDuel NJ, 71 BetRivers NJ, 75 BetMGM NJ. |
| `includeKalshi` | `true` | Join Kalshi exchange prices onto each row. |
| `includeSplits` | `true` | Attach `public_betting` (share of bets/money per side) when Action Network reports it. |
| `maxGames` | `100` | Cap on rows across all requested leagues, soonest-starting first (1–1000). |

### How the odds are computed

- **Implied probability** is the standard American-odds conversion: `100 / (odds + 100)` for a positive price, `|odds| / (|odds| + 100)` for a negative one. This still includes the book's built-in edge (the "vig").
- **Fair (no-vig) probability** divides each side's implied probability by the sum of both sides' implied probabilities, so the vig cancels out and `fair_probability_home + fair_probability_away` always sums to 1.
- Both are computed from the **Consensus** book's moneyline when it has both sides; when Consensus is missing, they fall back to the **best available price per side** across books instead (`probability_source` says which). `public_betting` splits come from that same book's line; Action Network has left them empty on every game we sampled so far, so expect `null` values until it publishes them again.
- Consensus is Action Network's own number, passed through as-is. Now and then it mirrors a single outlier book: in September 2026 one NFL game's Consensus matched FanDuel's line while four other books and Kalshi agreed the other way, giving a 37-point `kalshi_vs_books_edge`. An edge that large is almost always a data issue on one side, so compare `books[]` before acting on it.
- **`kalshi_vs_books_edge`** is Kalshi's own implied probability (its YES price, which *is* a probability on a real-money exchange) minus this row's fair probability, per side. A near-zero edge means the two markets agree; a large one flags a real discrepancy worth a second look, not a guaranteed profit.
- **Soccer rows are raw lines only.** Most books post soccer as a 2-way line without a draw (draw-no-bet style), a few post a 3-way line with `moneyline_draw`, and Kalshi's soccer markets are 3-way. Comparing those would mislead, so for soccer the derived fields (`best_moneyline_*`, `implied_probability_*`, `fair_probability_*`, `probability_source`, `kalshi_vs_books_edge`) are `null`. Each book's own prices stay in `books[]`, and the `kalshi` block carries Kalshi's own home/draw/away prices.
- Odds are always **American format** (e.g. `-110`, `+150`), for information only – not betting advice, and nothing here places a bet for you.

### Kalshi matching

Each game is matched to a Kalshi event by comparing team names (Kalshi's own parsed name must appear inside the sportsbook's full team name, e.g. Kalshi's "Los Angeles C" inside "Los Angeles Chargers") and requiring the same US Eastern date, the date Kalshi puts in its tickers. When the ticker also carries a start time (MLB does, e.g. `KXMLBGAME-26SEP251840PITDET` = 6:40 PM ET), the game must start within 2 hours of it, so back-to-back series games and doubleheaders land on the right event. A pair with no candidate, or more than one, is left unmatched – Kalshi fields are `null`, never a guess.

### Sources and reliability

- **Odds**: Action Network's public scoreboard JSON (`api.actionnetwork.com`), no login, no API key. If Action Network fails after retries, the Actor falls back to BetRivers' public odds from Kambi for NFL, NCAAF, MLB, NHL and WNBA, marking `source: "kambi"` on those rows. Kambi's public feed carries one market per game: the spread for NFL, NCAAF and WNBA, the moneyline for MLB and NHL. Whatever it lacks (including totals) stays `null` rather than being estimated.
- A game with no sportsbook price at all yet is skipped and never charged.
- **Exchange prices**: Kalshi's public trade API (`api.elections.kalshi.com`), the CFTC-regulated US exchange for event contracts.
- 429 and 5xx responses retry with exponential backoff (5 tries); a 200 without the expected data counts as a failure. A run fails only when it produced zero rows *and* a request failed; an off-season league with nothing scheduled is a successful run with zero rows.
- The `STATS` record in the run's key-value store holds games pushed, Kalshi matches, which leagues used Action Network vs. the Kambi fallback, requests and errors by category.
- **Not affiliated with Action Network, Kalshi, DraftKings, FanDuel, BetMGM, BetRivers, Caesars, bet365 or any other sportsbook.**

### Pricing

Pay per event, no subscription: **$0.003 per game row** and **$0.001 per run start**. Set a maximum charge on the run and the Actor stops cleanly when it is reached, charging only for rows actually saved.

# Actor input Schema

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

Leagues to scrape, one row per game. NBA and NCAAB have no games until October and simply contribute zero rows until then, not an error.

## `books` (type: `array`):

Action Network book ids to keep, e.g. 68 for DraftKings NJ. Empty = every book Action Network returns for the game. Common ids: 15 Consensus, 30 Open, 68 DraftKings NJ, 69 FanDuel NJ, 71 BetRivers NJ, 75 BetMGM NJ.

## `includeKalshi` (type: `boolean`):

Match each game to its Kalshi exchange event by team names and date, and attach kalshi.\* fields plus kalshi\_vs\_books\_edge. A game Kalshi doesn't list, or that can't be matched with confidence, gets null kalshi fields - never guessed.

## `includeSplits` (type: `boolean`):

Attach the public\_betting block (share of bets and share of money per side, when Action Network reports them). Off saves nothing computationally but keeps rows smaller.

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

Keep this many game rows, soonest-starting first, after filtering. Each row is one "game" event.

## Actor input object example

```json
{
  "leagues": [
    "nfl",
    "ncaaf",
    "mlb",
    "nhl",
    "soccer"
  ],
  "books": [
    "15",
    "68",
    "69"
  ],
  "includeKalshi": true,
  "includeSplits": true,
  "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/sports-betting-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/sports-betting-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/sports-betting-odds-scraper --silent --output-dataset

```

## MCP server setup

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