# Sports Odds Comparison: Best Price & Arbitrage (`m_ctim/sports-odds-comparison`) Actor

Compare pre-game moneyline, spread and total odds across Pinnacle and Bovada for NFL, NBA, MLB, NHL, college and soccer. Best price per outcome, no-vig fair odds, arbitrage detection, and a line-movement monitor. Plain HTTP, no login, no proxies.

- **URL**: https://apify.com/m\_ctim/sports-odds-comparison.md
- **Developed by:** [Timothy Kelvin](https://apify.com/m_ctim) (community)
- **Categories:** Sports
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

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

## Sports Odds Comparison: Best Price & Arbitrage

Compare pre-game betting odds across **Pinnacle** and **Bovada** in one run. For every upcoming game you get each book's moneyline, spread and total, the best available price per outcome, no-vig fair odds, and an arbitrage flag. Monitor mode tracks line movement between runs.

Covers the NFL, NCAA football, NBA, WNBA, NCAA basketball, MLB, NHL, English Premier League and Serie A on both books. La Liga, Bundesliga, MLS, the Champions League and UFC currently come from Pinnacle only.

It reads the books' public odds feeds over plain HTTP. No login, no browser, no proxies, and a run takes seconds.

### What you get

**One `line` row per book, market and outcome:**

```json
{
  "rowType": "line",
  "eventId": "nfl-2026-09-27-new-england-patriots-at-jacksonville-jaguars",
  "league": "nfl",
  "startTime": "2026-09-27T17:00:00.000Z",
  "homeTeam": "Jacksonville Jaguars",
  "awayTeam": "New England Patriots",
  "book": "pinnacle",
  "oddsStatus": "priced",
  "market": "spread",
  "outcome": "home",
  "outcomeName": "Jacksonville Jaguars",
  "line": -2.5,
  "priceAmerican": -121,
  "priceDecimal": 1.8264,
  "impliedProb": 0.5475,
  "scrapedAt": "2026-09-25T18:39:56.021Z"
}
```

**One `summary` row per game, market and line**, comparing the books. Here's a real one, where the best price on each side sits at a different book:

```json
{
  "rowType": "summary",
  "eventId": "nfl-2026-09-27-new-york-jets-at-detroit-lions",
  "market": "moneyline",
  "line": null,
  "booksCompared": ["pinnacle", "bovada"],
  "outcomes": [
    {
      "outcome": "home",
      "outcomeName": "Detroit Lions",
      "bestBook": "pinnacle",
      "bestPriceAmerican": -294,
      "bestPriceDecimal": 1.3401,
      "fairProb": 0.7208,
      "fairPriceAmerican": -258
    },
    {
      "outcome": "away",
      "outcomeName": "New York Jets",
      "bestBook": "bovada",
      "bestPriceAmerican": 250,
      "bestPriceDecimal": 3.5,
      "fairProb": 0.2792,
      "fairPriceAmerican": 258
    }
  ],
  "fairSource": "pinnacle",
  "isArbitrage": false,
  "arbMarginPct": -3.092,
  "arbStakes": null
}
```

- **Best price:** the highest payout for each outcome across the books, and which book has it.
- **Fair odds:** the book's margin removed, so the outcome probabilities sum to 100%. They come from Pinnacle when it prices the market, because it's the sharpest, lowest-margin book; otherwise from the best prices.
- **Arbitrage:** `isArbitrage` is true when backing every outcome at its best price guarantees a profit. `arbMarginPct` is that guaranteed return (negative means no arb, and how far away it is), and `arbStakes` says what share of your stake to put on each outcome.

Spreads and totals are only compared when the books offer the **same line**: -2.5 at one book and -3 at another are different bets.

### Input

| Field | What it does |
|---|---|
| `leagues` | Any of `nfl`, `ncaaf`, `nba`, `wnba`, `ncaab`, `mlb`, `nhl`, `epl`, `serie-a`, `la-liga`, `bundesliga`, `mls`, `ucl`, `ufc`. Default `nfl`, `nba`, `epl`. |
| `books` | `pinnacle`, `bovada`, or both (default). |
| `markets` | `moneyline`, `spread`, `total`. All by default. Full-game main lines only. |
| `dateFrom`, `dateTo` | Optional UTC dates, `YYYY-MM-DD`. |
| `oddsFormat` | `both` (default), `american` or `decimal`. Implied probability is always included. |
| `includeSummary` | Add the best-price, fair-odds and arbitrage rows. Default `true`. |
| `arbitrageOnly` | Return only summary rows where an arbitrage exists. |
| `mode` | `snapshot` (default) or `monitor` (see below). |
| `monitorStoreName` | Where monitor mode keeps its memory. Use a different name per scheduled monitor. |
| `maxEvents` | Max games, soonest first. Default 200. |

Tonight's NBA and NHL moneylines, decimal odds:

```json
{ "leagues": ["nba", "nhl"], "markets": ["moneyline"], "oddsFormat": "decimal" }
```

Arbitrage scan across every league:

```json
{ "leagues": ["nfl", "ncaaf", "nba", "mlb", "nhl", "epl", "serie-a"], "arbitrageOnly": true }
```

### Monitor mode: line movement

Set `mode` to `monitor` and schedule the actor, for example every 15 minutes. Each run compares against the previous one and returns only lines that moved, with what they moved from. For example (illustrative prices):

```json
{
  "rowType": "line",
  "eventId": "nfl-2026-09-27-new-england-patriots-at-jacksonville-jaguars",
  "book": "pinnacle",
  "market": "moneyline",
  "outcome": "home",
  "changeType": "price_moved",
  "previousPriceAmerican": -140,
  "priceAmerican": -154,
  "deltaProbPts": 2.3
}
```

`deltaProbPts` is the move in implied probability, in percentage points; positive means the outcome got more likely. `changeType` is `price_moved`, `line_moved` (the spread or total itself changed), or `new`. The first monitor run has nothing to compare against, so it saves a baseline and reports every line as `new`.

### Pricing

Charged **per game returned**, not per row. A game costs the same whether you ask for one market or three, and with or without the summary rows. In monitor mode only games with movement count.

### Good to know

- **Pre-game only.** Live in-play lines are excluded.
- **Arbitrage is rare and brief.** Most runs find none, and one that exists may close within minutes. Books also limit stakes and can void mispriced bets, so treat the flag as a lead to check, not a guaranteed profit.
- **Unmatched games.** A game is merged across books only when both team names and the start time agree. If the books spell a team differently in a way the matcher can't be sure of, the game appears once per book rather than risk pairing prices from different games.
- **Not priced yet.** Games a book lists without odds come back as a row with `oddsStatus: "not_priced_yet"` rather than disappearing.
- **When a book is unavailable.** The actor identifies itself honestly and spaces its requests. If a book rate-limits or refuses it, that book is skipped for the run with a note in the log, and the other book's odds are still returned. It does not use proxies or disguise itself to get around a refusal.
- **Bovada doesn't always serve every league.** When tested from Apify's servers, Bovada returned NBA, NHL and Premier League odds but declined NFL, MLB and NCAA football. Those leagues then come from Pinnacle only, and the log says so. Coverage can change either way.
- **Feeds can change.** These are the books' public web feeds, not documented APIs, so a book can change or restrict them at any time.

### Disclaimer

This actor is unofficial and is not affiliated with, endorsed by, or connected to Pinnacle, Bovada, or any sportsbook. Odds are provided for information and research, and can change at any moment; always confirm a price with the book before acting on it. Betting laws vary by country and state, and some of these books do not accept customers in every location. You are responsible for complying with the laws where you live and with each book's own terms.

# Actor input Schema

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

Which leagues to compare. La Liga, Bundesliga, MLS, Champions League and UFC currently come from Pinnacle only.

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

Which books to compare. Best prices and arbitrage need at least two.

## `markets` (type: `array`):

Full-game main lines only.

## `dateFrom` (type: `string`):

Only games starting on or after this date, YYYY-MM-DD. Leave empty for everything the books have listed.

## `dateTo` (type: `string`):

Only games starting on or before this date, YYYY-MM-DD.

## `oddsFormat` (type: `string`):

Implied probability is always included.

## `includeSummary` (type: `boolean`):

Add one summary row per game and market: the best price per outcome across books, no-vig fair probabilities, and an arbitrage flag with its margin.

## `arbitrageOnly` (type: `boolean`):

Return only summary rows where an arbitrage exists across the selected books.

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

Monitor mode remembers the previous run's lines and returns only what changed, with the old price and the move in probability points. Schedule it to track line movement.

## `monitorStoreName` (type: `string`):

Name of the key-value store that holds the last snapshot. Use a different name for each separate monitor you schedule.

## `maxEvents` (type: `integer`):

Stop after this many games, soonest first.

## `saveRawResponses` (type: `boolean`):

Also store each book's raw response in the run's key-value store. Only needed when reporting a problem.

## Actor input object example

```json
{
  "leagues": [
    "nfl",
    "nba",
    "epl"
  ],
  "books": [
    "pinnacle",
    "bovada"
  ],
  "markets": [
    "moneyline",
    "spread",
    "total"
  ],
  "oddsFormat": "both",
  "includeSummary": true,
  "arbitrageOnly": false,
  "mode": "snapshot",
  "monitorStoreName": "sports-odds-monitor",
  "maxEvents": 200,
  "saveRawResponses": false
}
```

# 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 = {
    "leagues": [
        "nfl",
        "nba",
        "epl"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("m_ctim/sports-odds-comparison").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 = { "leagues": [
        "nfl",
        "nba",
        "epl",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("m_ctim/sports-odds-comparison").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 '{
  "leagues": [
    "nfl",
    "nba",
    "epl"
  ]
}' |
apify call m_ctim/sports-odds-comparison --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,m_ctim/sports-odds-comparison"
        }
    }
}
```

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/rIRaKM8PX8Nmq1f8I/builds/GHrjfe6U9ufa90hLr/openapi.json
