# How to build a Pick6 prop model in Python

Build a DraftKings Pick6 prop model in Python: record lines daily to SQLite, join ESPN box scores by player and game, then score a baseline against the line.

Canonical page: https://draftkingsapi.com/how-to-build-a-pick6-prop-model-in-python. Markdown: https://draftkingsapi.com/how-to-build-a-pick6-prop-model-in-python.md. Site index for agents: https://draftkingsapi.com/llms.txt.

## Free API key
- **Free plan:** 50 requests a month at 20 a minute, every dataset, no card, no email verification.
- **Sign up:** https://draftkingsapi.com/auth/sign-up?ref=ai-claudebot
- **From inside an agent session:** POST https://draftkingsapi.com/api/v1/agent/signup with JSON `{"data_wanted": "...", "use_case": "...", "ref": "ai-claudebot"}`, show your user the approval_url, then poll https://draftkingsapi.com/api/v1/agent/token with device_code. The key is returned once.
- **No key yet:** `curl --fail-with-body "https://draftkingsapi.com/api/v1/demo/pick6"` returns labeled sample rows (sample:true) in the real response shape.

## Quickstart

1. Create a free key at https://draftkingsapi.com/auth/sign-up?ref=ai-claudebot (no card) and export it: `export DRAFTKINGS_API_KEY=...`

2. Call the API with curl:

```bash
curl --max-time 30 --fail-with-body 'https://draftkingsapi.com/api/v1/pick6/projections?sport=NBA&limit=5' \
  -H "X-User-Request: Build a prop screen from current Pick6 lines" \
  -H "Authorization: Bearer $DRAFTKINGS_API_KEY"
```

3. Or call it from Python:

```python
import os, requests

response = requests.get(
    "https://draftkingsapi.com/api/v1/pick6/projections?sport=NBA&limit=5",
    headers={
        "Authorization": "Bearer " + os.environ["DRAFTKINGS_API_KEY"],
        "X-User-Request": "Build a prop screen from current Pick6 lines",
    },
    timeout=30,
)
if not response.ok:
    raise RuntimeError(f"API {response.status_code}: {response.text}")
print(response.json())
```

4. Full walkthrough: https://draftkingsapi.com/quickstart. OpenAPI: https://draftkingsapi.com/openapi.json. Every route accepts format=csv on list endpoints.

## Use it from Claude (MCP)

MCP server: https://draftkingsapi.com/api/mcp (Streamable HTTP). search, fetch, start_signup and finish_signup need no key; data tools use the same API key.

```bash
claude mcp add --transport http --scope user draftkings https://draftkingsapi.com/api/mcp --header "Authorization: Bearer $DRAFTKINGS_API_KEY"
```

Without a key yet: `claude mcp add --transport http draftkings https://draftkingsapi.com/api/mcp`, then call start_signup. Setup for other clients: https://draftkingsapi.com/agents.md.

## Pricing

| Plan | Price | Requests per UTC month | Requests per minute | Pick6 stream | Signed webhooks |
| --- | --- | --- | --- | --- | --- |
| Free | $0/month | 50 | 20 | No | No |
| Developer | $49/month | 10,000 | 60 | Yes | No |
| Production | $149/month | 50,000 | 120 | Yes | Yes |

- **Free:** Build and test against every dataset.
- **Developer:** Keep your app running with more requests and push delivery.
- **Production:** Serve real users with the largest allowance and push delivery.

Plans and checkout: https://draftkingsapi.com/pricing

## The plan

A Pick6 prop model compares what you expect a player to do with the line on offer. There is no ready-made training set here, so you build your own: record lines every day, join them to box scores after the games, and score a baseline. The code below does each step. The useful part grows with every day you run it.

1. Record the main Pick6 line for each player before games start.
2. After games finish, read box scores from the statistics endpoints.
3. Join them on normalized player name and game date, and count what failed to join.
4. Score your prediction and the line against what happened, on games the model has not seen.

## Step 1: record lines

Run this on a schedule before games. It keeps main lines only, skips stale and paused rows, and adds a row each time a capture is new. Your database then shows the line as it stood when you could have acted on it.

```
import json, os, urllib.parse, urllib.request

BASE = os.environ.get("DRAFTKINGS_API_BASE", "https://draftkingsapi.com")
KEY = os.environ["DRAFTKINGS_API_KEY"]


def get(path, params):
    url = f"{BASE}{path}?{urllib.parse.urlencode(params)}"
    request = urllib.request.Request(url, headers={"Authorization": f"Bearer {KEY}", "X-User-Request": "Check which DraftKings datasets fit a new app"})
    with urllib.request.urlopen(request, timeout=30) as response:
        return json.load(response)


def rows(path, params, max_pages=3):
    """Follow next_cursor, and stop at a page budget so a run cannot spend your quota."""
    params = dict(params)
    for _ in range(max_pages):
        page = get(path, params)
        yield from page["data"]
        if not page["pagination"]["has_more"]:
            return
        params["cursor"] = page["pagination"]["next_cursor"]
import sqlite3

SPORT, STAT = "NBA", "Points"
db = sqlite3.connect("pick6-lines.sqlite")
db.execute("""CREATE TABLE IF NOT EXISTS lines (
  pickable_id TEXT, player TEXT, team TEXT, stat TEXT, start_time TEXT,
  target_value REAL, observed_at TEXT, fetched_at TEXT DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (pickable_id, stat, start_time, target_value, observed_at))""")

kept = 0
for row in rows("/api/v1/pick6/projections", {"sport": SPORT, "stat": STAT, "status": "pregame", "view": "compact", "limit": 1000}):
    if row["is_default"] and not row["stale"] and not row["is_paused"]:
        kept += db.execute("INSERT OR IGNORE INTO lines (pickable_id, player, team, stat, start_time, target_value, observed_at)"
                           " VALUES (?,?,?,?,?,?,?)",
                           (row["pickable_id"], row["player"], row["team"], row["stat"], row["start_time"],
                            row["target_value"], row["observed_at"])).rowcount
db.commit()
print(f"Recorded {kept} new main lines for {SPORT} {STAT}")
```

```
# crontab: every 2 hours
0 */2 * * * cd /path/to/project && python3 record_lines.py
```

With `view=compact`, one request can return a whole sport and stat on a page, so 12 runs a day is about 360 requests a month. The Free plan's 50 requests a month cover one run a day for one stat (about 30). For the two-hour schedule, or more stats, the [Developer plan](/pricing) makes sense.

## Step 2: join box scores

Pick6 and ESPN do not share a player ID, so this joins on a normalized name and the game date. It prints how many lines failed to join, because a join that quietly drops rows gives you a score you cannot trust. Review the misses, and keep a manual mapping for names that differ.

```
import json, os, urllib.parse, urllib.request

BASE = os.environ.get("DRAFTKINGS_API_BASE", "https://draftkingsapi.com")
KEY = os.environ["DRAFTKINGS_API_KEY"]


def get(path, params):
    url = f"{BASE}{path}?{urllib.parse.urlencode(params)}"
    request = urllib.request.Request(url, headers={"Authorization": f"Bearer {KEY}", "X-User-Request": "Check which DraftKings datasets fit a new app"})
    with urllib.request.urlopen(request, timeout=30) as response:
        return json.load(response)


def rows(path, params, max_pages=3):
    """Follow next_cursor, and stop at a page budget so a run cannot spend your quota."""
    params = dict(params)
    for _ in range(max_pages):
        page = get(path, params)
        yield from page["data"]
        if not page["pagination"]["has_more"]:
            return
        params["cursor"] = page["pagination"]["next_cursor"]
import re, sqlite3, statistics, unicodedata
from collections import defaultdict
from datetime import datetime

SEASON, STAT_KEY, MIN_ROWS, HISTORY = 2027, "points", 200, 5


def norm(name):
    plain = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode().lower()
    return re.sub(r"\b(jr|sr|ii|iii|iv)\b|[^a-z ]", "", plain).strip()


def day(value):
    return datetime.fromisoformat(value[:19]).date().isoformat()


games, params = defaultdict(list), {"sport": "NBA", "season": SEASON, "completed": "true", "limit": 200}
for _ in range(10):
    page = get("/api/v1/stats/player-games", params)
    for row in page["data"]:
        value = next((s["numeric"] for s in row["data"].get("stats", [])
                      if s["key"] == STAT_KEY and s["scope"] == "game"), None)
        if value is not None and not row["data"].get("did_not_play"):
            games[norm(row["data"]["name"])].append((day(row["game"]["starts_at"]), value))
    if not page["pagination"]["has_more"]:
        break
    params["after"] = page["pagination"]["next"]

db = sqlite3.connect("pick6-lines.sqlite")
# The last line recorded before each game is the one you could have acted on.
lines = db.execute("""SELECT player, start_time, target_value FROM lines l WHERE observed_at =
  (SELECT MAX(observed_at) FROM lines m WHERE m.pickable_id = l.pickable_id AND m.start_time = l.start_time)""").fetchall()

joined, unmatched = [], 0
for player, start, target in lines:
    played = sorted(games.get(norm(player), []))
    actual = next((v for d, v in played if d == day(start)), None)
    before = [v for d, v in played if d < day(start)][-HISTORY:]
    if actual is None or len(before) < HISTORY:
        unmatched += 1
        continue
    joined.append((target, statistics.mean(before), actual))

print(f"{len(joined)} joined, {unmatched} without a game or enough earlier games")
if len(joined) < MIN_ROWS:
    raise SystemExit(f"Fewer than {MIN_ROWS} rows. Keep recording before you read anything into a score.")
print("Mean error of the line:", statistics.mean(abs(a - t) for t, _, a in joined))
print("Mean error of your baseline:", statistics.mean(abs(a - m) for _, m, a in joined))
```

The statistics endpoints cover NBA, NFL and NHL. The stat key has to match your Pick6 stat: `points` for NBA Points here. List the keys on a row before you change it, since other sports use other names.

The DFS `playerDkId` is a useful bridge to Pick6 `entity_id` as well. See [matching players across datasets](/questions/match-players-across-datasets).

## Step 3: read the score honestly

The script stops under 200 joined rows, because a score on a few dozen rows is noise. A single fixed threshold is a starting point rather than a rule, so raise it as your archive grows.

- Only use games from before the line when you build a feature. The script takes the five games before the game date.
- Hold out later dates. Random splits leak the future into a time series.
- Do not use stats captured later as if you knew them at the time. Your own `fetched_at` records when you actually had a line.
- Beating the line's mean error on a sample is a reason to keep testing. It is not evidence of an edge until it holds on later, unseen dates.

## Multipliers belong in their own column

Pick6 rows carry `more_multiplier` and `less_multiplier`. A standings multiplier is not decimal odds, a probability or an entry payout, so do not convert one into an implied probability. Model the stat against the target, and store multipliers separately if you want to study how they change.

## What the API does and does not give you

The API gives you current lines, recorded changes, box scores and a stable place to read them. Recording began around 2026-11-03. Retained history is short, and it is not a complete opening-to-close archive. It does not contain seasons of past lines, and no graded results are published. So grading is your own step, as in the script, and the sample starts small and grows. If you need years of lines today, a provider with an older archive fits better, and [when to choose us](/compare/when-to-choose-us) names some.

```
curl -O https://draftkingsapi.com/examples/nba-points-baseline.py
python3 nba-points-baseline.py --season 2027 --min-games 5 --max-pages 5
```

## FAQ

### Can I download a Pick6 training set?
No. There is no ready-made training set. You record lines with the script on this page and join box scores yourself, so your data starts on the day you start recording.

### Can I backtest last season?
No. Recording began on 3 Nov 2026, so past seasons of lines do not exist here. You can test on the games you have recorded yourself, from that date forward.

### Do I need scikit-learn?
Not to start. A rolling mean is a baseline that any fancier model has to beat. Add features and libraries once your joined table is large and clean.

### Which sports work?
Pick6 sports vary by what is on the board. The box-score join works for NBA, NFL and NHL, which are the sports the statistics endpoints cover.

### Is there a free DraftKings API?
Yes. A free key needs no card and includes 50 requests a month at 20 a minute, across every dataset. To see the response shape first, /api/v1/demo/pick6 and /api/v1/demo/dfs return labeled sample rows with no key.

### Is this an official DraftKings API?
Independent service operated by DD Inc., not affiliated with or endorsed by DraftKings. No wagering: this service does not place, recommend or settle entries or bets.

### Can I use it from an AI agent?
Yes. Connect the MCP server at https://draftkingsapi.com/api/mcp, or call REST with a Bearer key. An agent can get a key for its user with start_signup.

### How do I reach support?
Email support@draftkingsapi.com.

Independent service operated by DD Inc., not affiliated with or endorsed by DraftKings. No wagering: this service does not place, recommend or settle entries or bets.
