I Built a Python Script to Track Maine's Betting Odds. The Interesting Part Was the Column That Stayed Empty.
I was not trying to write about gambling law. I was trying to fix a bug.
In July I started pulling live moneyline data from a couple of Maine-facing sportsbook APIs for a side project, a dashboard that flags line movement on Patriots and Celtics games faster than the apps surface it. Maine legalised sports betting in 2022 and launched mobile wagering in November 2023, so it is a newer and thinner market than New Jersey or Pennsylvania. Thinner is easier to scrape. Fewer books, less noise, cleaner diffs between polling intervals.
The sportsbook side worked once the polling was tuned. Then I pointed the same code at casino endpoints and got nothing back. Not a rate limit. Not a 403. Empty.
Three hours of debugging later, the problem was not in my code.
What the Empty Column Actually Meant
My first assumption was that Maine had simply never legalised online casino gaming, which would have made the 404s boring and correct.
That assumption was wrong. In January 2026 Governor Janet Mills allowed legislation through that hands the state's four Wabanaki Nations exclusive rights to operate licensed online casinos, with each tribe permitted to partner with a single commercial operator. That made Maine the eighth state in the country to authorise regulated online casino gaming.
The reason my script found nothing is narrower and considerably more interesting than absence. The statute exists. The licensees are identified. The Gambling Control Unit is still writing the rules, and no consumer platform has launched yet. The endpoints do not exist because the market is sitting between authorisation and go-live, which is a state my data model had no way to represent.
The gap in the meantime gets filled the way it usually does. Players in Portland or Bangor wanting blackjack or slots are using platforms licensed outside the state, and guides covering online casinos in Maine map what is available during the transition. What makes Maine worth watching rather than typical is that the transition is genuinely temporary. There is a statute, named licensees and a rulemaking process underway, which is a materially different situation from a state that has never passed anything at all.
The Setup, Which Is Deliberately Boring
Requests for the fetching, pandas for the diffing, a cron job on a small VPS polling every four minutes during game windows.
import requests
import pandas as pd
from datetime import datetime
HEADERS \= {"User-Agent": "line-tracker/0.3 (research use)"}
def fetch\_odds(endpoint, market\="moneyline"):
resp \= requests.get(endpoint, headers\=HEADERS, timeout\=8)
resp.raise\_for\_status()
data \= resp.json()
return pd.json\_normalize(data.get("events", \[]))
def snapshot(endpoints):
frames \= \[]
for name, url in endpoints.items():
df \= fetch\_odds(url)
df\["book"] \= name
df\["pulled\_at"] \= datetime.utcnow()
frames.append(df)
return pd.concat(frames, ignore\_index\=True)
Almost nothing to it, which is the point. Most of the work in this kind of project is not the polling. It is throttling, stale caches, and books that quietly change their JSON shape mid-season without a changelog.
Two of the three books I track publish no rate limits anywhere. You find them by getting throttled. I took two 429s in the first week, added exponential backoff with jitter, and moved on.
Rebuilding the Model Around What Exists
The fix was not to the scraper. It was to the schema.
I had been treating market status as binary: a state either has an endpoint or it does not. That is wrong in a way that will bite anyone modelling a regulated industry.
MARKET_STATUS = {
"NJ": "live",
"PA": "live",
"MI": "live",
"ME": "authorised_pre_launch",
"TX": "no_framework",
}
def tag_market(state_code):
return MARKET_STATUS.get(state_code, "unknown")
Crude, but it turns a dead end into something chartable. More usefully, authorised_pre_launch is a real category with a predictable lifecycle: statute, rulemaking, licensing, testing, launch. Every one of those stages leaves public traces before any API appears.
The general lesson is that your data model has to reflect the regulation, not just the API shape. I assumed symmetric endpoints across states. The 404s were not a bug, they were a timeline.
Why This Matters Past One Script
Maine is closer to the median than the exception. Most US states that have legalised sports betting have not authorised online casino gaming alongside it, and the states running live mobile casino products remain a minority even with Maine now added to the list.
That asymmetry creates exactly the data gap I hit. Anyone building a research tool, a compliance dashboard or a personal project against US gambling markets cannot assume betting data and casino data arrive together. You have to check, state by state, whether the product you are modelling has been authorised, and then separately whether it has launched. Those are different questions with different answers, and conflating them is what cost me an afternoon.
For scale on why the pre-launch window is commercially interesting rather than a footnote: legal sports betting alone generated nearly $17 billion in US revenue in 2025, and a meaningful share of growth in mature states comes from casino products sitting beside the sportsbook rather than from Sunday moneylines.
For now my tracker logs Maine sportsbook lines and tags the casino column as pending rather than null. That distinction is the whole finding.
What I Would Build Next
The obvious extension is to stop treating the appearance of a licensed casino API as something to hardcode around, and start treating it as an event worth detecting.
States do not flip from authorised to live overnight, and they leave signals on the way: rulemaking notices, licensing announcements, vendor partnerships, testing windows. A scraper watching regulatory publications alongside odds feeds would catch that transition before most of the industry does, and Maine specifically has a reasonably well-defined queue of things that have to happen first.
Small script. Better lesson. Sometimes the useful thing your code surfaces is not a number, it is the shape of something that has been decided but has not arrived.
Comments
Loading comments…