# DraftKings Pick6 alerts: stream consumer

Copy a Python or Node.js consumer for the DraftKings Pick6 change stream. It filters by sport and stat, saves its cursor and resumes after any disconnect.

Canonical page: https://draftkingsapi.com/tools/alert-consumer. Markdown: https://draftkingsapi.com/tools/alert-consumer.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

## Line-change alerts in one file

Copy this, set your key, and run it. It prints each multiplier and availability change for the sport and stat you choose, and picks up where it left off after a disconnect or a restart. Point `alert` at Slack, Discord or your own queue when you are ready.

```
#!/usr/bin/env python3
"""Print Pick6 multiplier and availability changes for one sport and stat, and resume after any disconnect."""
import json, os, pathlib, time, urllib.error, urllib.request

BASE = os.environ.get("DRAFTKINGS_API_BASE", "https://draftkingsapi.com")
KEY = os.environ["DRAFTKINGS_API_KEY"]
SPORT, STAT = os.environ.get("ALERT_SPORT", "NFL"), os.environ.get("ALERT_STAT", "Receiving Yards")
STATE = pathlib.Path(".pick6-alert-cursor")
MAX_CONNECTIONS = int(os.environ.get("ALERT_MAX_CONNECTIONS", "60"))  # One request each.


def request(path, **headers):
    return urllib.request.Request(BASE + path, headers={"Authorization": f"Bearer {KEY}", "X-User-Request": "Check which DraftKings datasets fit a new app", **headers})


def bootstrap():
    with urllib.request.urlopen(request(f"/api/v1/stream/snapshot?sport={SPORT}"), timeout=60) as response:
        cursor = json.load(response)["snapshot_cursor"]
    STATE.write_text(cursor)
    return cursor


def alert(change):
    if change["sport"] != SPORT or change["stat"] != STAT:
        return  # The stream carries every sport, so filter on your side.
    before = {s["side"]: s for s in (change.get("previous_data") or {}).get("selections", [])}
    for selection in (change.get("data") or {}).get("selections", []):
        old = before.get(selection["side"])
        if old and old["standings_multiplier"] != selection["standings_multiplier"]:
            print(change["observed_at"], change["pickable_id"], change["stat"], change["data"]["target_value"],
                  selection["side"], old["standings_multiplier"], "to", selection["standings_multiplier"], flush=True)
    previous, current = change.get("previous_data") or {}, change.get("data") or {}
    if previous.get("is_paused") != current.get("is_paused"):
        print(change["observed_at"], change["pickable_id"], change["stat"], "paused" if current.get("is_paused") else "reopened", flush=True)


def read_connection(cursor):
    req = request("/api/v1/stream", **{"Last-Event-ID": cursor, "Accept": "text/event-stream"})
    with urllib.request.urlopen(req, timeout=75) as response:
        frame = {}
        for raw in response:
            line = raw.decode().rstrip("\n")
            if line:
                if not line.startswith(":") and ": " in line:
                    name, value = line.split(": ", 1)
                    frame[name] = value
                continue
            if frame.get("event") == "delivery.error":
                raise RuntimeError(frame.get("data"))
            if frame.get("event") == "pick6.change":
                alert(json.loads(frame["data"])["change"])
            if "id" in frame:  # Save only after handling, so a crash replays an event and never skips one.
                cursor = frame["id"]
                STATE.write_text(cursor)
            frame = {}
    return cursor


cursor = STATE.read_text().strip() if STATE.exists() else bootstrap()
for attempt in range(MAX_CONNECTIONS):
    try:
        cursor = read_connection(cursor)
    except urllib.error.HTTPError as error:
        if error.code == 410:  # The cursor aged out. Take a new baseline.
            cursor = bootstrap()
        elif error.code == 429:
            time.sleep(int(error.headers.get("Retry-After", "60")))
        elif error.code in (401, 403):
            raise SystemExit(f"HTTP {error.code}: check your key and plan. Retrying cannot fix this.")
        else:
            time.sleep(min(60, 2 ** min(attempt, 6)))
    except (OSError, TimeoutError):
        time.sleep(5)
print(f"Stopped after {MAX_CONNECTIONS} connections at cursor {cursor}")
```

```
export DRAFTKINGS_API_KEY=your_key
ALERT_SPORT=NFL ALERT_STAT="Receiving Yards" python3 pick6_alerts.py
```

Prefer Node? [stream-client.mjs](/examples/stream-client.mjs) does the same in Node 18 with no packages, and [discord-alerts.mjs](/examples/discord-alerts.mjs) posts to a Discord webhook.

## How resume works

The stream is plain Server-Sent Events, so nothing is lost when a connection ends.

1. Take a baseline. `/api/v1/stream/snapshot` returns current rows and a `snapshot_cursor`.
2. Connect with that cursor in `Last-Event-ID`. Each `pick6.change` event has an `id`.
3. Save the id after you handle the event, so a crash repeats one event and never skips one.
4. Reconnect when a connection ends. Checkpoint events move your cursor during quiet periods.
5. On a 410 the cursor has aged out. Take a new snapshot. On a 429, wait for `Retry-After`.

## What each event tells you

A change compares one market row with its last version: `previous_data` holds the old row and `data` holds the new one. Multipliers and availability change on the same row. A move in the main target arrives as a different market row, so to confirm a line move for a prop, read its line history.

```
curl --fail-with-body "https://draftkingsapi.com/api/v1/pick6/line-history?pickable_id=PICKABLE_ID_FROM_AN_EVENT&change_type=line_move&limit=5" \
  -H "X-User-Request: Track Pick6 line moves for an alert bot" -H "Authorization: Bearer $DRAFTKINGS_API_KEY"
```

That call costs one request, so make it only for the props you are watching. Alerts follow capture times, and each change carries its own `observed_at`. There is no latency commitment, so check that clock if minutes matter to you.

## Plans and what a stream costs

Pick6 changes: Developer and Production include the resumable stream, and Production also includes signed webhooks. Native sportsbook scopes: paid plans can use a server-side WebSocket at `/api/v1/sportsbook/ws`. ESPN-attributed sportsbook rows and DFS rows have no push delivery. One Pick6 stream connection costs one request and lasts at most 55 seconds, then you reconnect. Held open around the clock that is about 1,570 requests a day, so one continuous stream uses Developer's 10,000 requests in about 6 days and most of Production's 50,000 in a month.

To stretch an allowance, run the consumer during the hours you care about instead of all day. `ALERT_MAX_CONNECTIONS` sets a hard stop.

## On the Free plan: poll instead

The Free plan has no stream, but the change feed works the same way with a cursor. Run this from a scheduler as often as your allowance allows. Each run is one request.

```
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 pathlib

state = pathlib.Path(".pick6-poll-cursor")
after = int(state.read_text()) if state.exists() else 0
page = get("/api/v1/pick6/history", {"sport": "NFL", "stat": "Receiving Yards", "after": after, "limit": 100})
for row in page["data"]:
    print(row["change_id"], row["stat"], row["target_value"], row["observed_at"])
state.write_text(str(page["pagination"]["next_after"]))
```

```
curl --fail-with-body "https://draftkingsapi.com/api/v1/demo/pick6-history?sport=NFL&limit=5"
```

## FAQ

### Is there a WebSocket for Pick6?
Pick6 uses Server-Sent Events: a plain HTTPS response that stays open and resumes with Last-Event-ID. The WebSocket at /api/v1/sportsbook/ws is for native sportsbook scopes on paid plans.

### What happens if my consumer disconnects?
Nothing is lost while your cursor is retained. The consumer sends its saved id when it reconnects and the stream continues right after it. If the cursor has aged out you get a 410, and you take a new snapshot.

### Does the stream filter by sport?
No. The snapshot accepts a sport, but the stream carries every sport, so the consumer filters on its side as shown in the alert function.

### Can I get webhooks instead?
Yes, on the Production plan. Signed webhooks deliver the same Pick6 changes to your HTTPS endpoint, and the webhook receiver example shows how to verify them.

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