What counts as movement
| Event type | Movement signal? |
|---|---|
| offer.new | No previous line. This is the first time this service recorded the offer. |
| offer.changed | Only when previous_line is not null and differs from line. |
| offer.unavailable | The offer left the observed board. In a partial collection this may be a gap, not a removal. |
| offer.restored | The offer was observed again after being unavailable. |
A runnable Python consumer
This script stores its cursor and processed event IDs in SQLite, prints moves of at least one point, and stops without advancing when the source is not fresh or partial. Run it from cron or a scheduler; each run reads only events newer than the last committed cursor.
import json, os, sqlite3, time
from urllib.parse import urlencode
from urllib.request import Request, urlopen
KEY = os.environ["PRIZEPICKS_API_KEY"]
db = sqlite3.connect("movement.db")
db.execute("CREATE TABLE IF NOT EXISTS state (k TEXT PRIMARY KEY, v TEXT)")
db.execute("CREATE TABLE IF NOT EXISTS seen (event_id TEXT PRIMARY KEY)")
def get_changes(params):
req = Request("https://prizepicksapi.com/api/v1/changes?" + urlencode(params),
headers={"Authorization": "Bearer " + KEY})
with urlopen(req, timeout=20) as res:
return json.load(res)
def poll(league="NBA", threshold=1.0):
row = db.execute("SELECT v FROM state WHERE k = 'cursor'").fetchone()
cursor = row[0] if row else None
while True:
params = {"league": league, "limit": 200}
if cursor:
params["after"] = cursor
page = get_changes(params)
status = page["meta"]["source_status"]
if status not in ("fresh", "partial"):
print("source is", status, "- alerts paused, cursor unchanged")
return
for event in page["data"]:
if db.execute("SELECT 1 FROM seen WHERE event_id = ?", (event["id"],)).fetchone():
continue
prev = event["previous_line"]
if event["type"] == "offer.changed" and prev is not None and abs(event["line"] - prev) >= threshold:
print(event["player_name"], event["stat"], prev, "->", event["line"], "at", event["observed_at"])
db.execute("INSERT INTO seen VALUES (?)", (event["id"],))
if page["data"]:
cursor = page["data"][-1]["cursor"]
db.execute("INSERT OR REPLACE INTO state VALUES ('cursor', ?)", (cursor,))
db.commit() # alert side effects and cursor commit together
if not page["meta"]["next_cursor"]:
return
time.sleep(6.1)
poll()Replace print with your notifier, but keep the order: perform the side effect, mark the event seen, and commit the cursor in the same transaction. If the process crashes before commit, the next run replays those events, and the seen table prevents duplicate alerts.
Pick a threshold per market
A half-point move means something different in a points market than in a low-count market. Key thresholds on stat (and, if you track it from projections, market period) rather than using one global number. Change events carry stat but not period; look up the projection by projection_id when period matters.
Budget the polling interval
One poll with no new events is still one request. Polling every five minutes is 288 requests per day, which exceeds a Free monthly allowance within a few days; hourly is 24 per day. Choose the interval from your plan and from how quickly an alert is actually useful. Webhooks remove empty polls on the Production plan.
Limits to state in your alert
- The time shown is when this service observed the change, not an exact upstream change time.
- A move can be missed if it happened and reverted between two collections.
- The line is the base line. Promotional or user-specific offers are not modeled.
- An alert is informational data, not a recommendation.
Frequently asked questions
Does every offer.changed event mean the line moved?
No. A change can involve selections, status details, player, or event details. Compare previous_line with line to detect numeric movement.
Should I poll or use webhooks?
Polling /changes works on every plan and is easy to replay. Webhooks push the same events and require the Production plan.
References
Current API documentation ↗Machine-readable OpenAPI contract ↗SQLite: INSERT OR REPLACE ↗PrizePicks API is an independent data service. It is not affiliated with or endorsed by PrizePicks. Observations, completeness, and freshness are separate claims; consult source status before relying on a stored line.