The 2026/27 Champions League kicked off on September 8. Thirty-six clubs, an eight-round league phase, and every matchweek crams eight or nine games into a Tuesday night and eight or nine more into Wednesday, nearly all of them kicking off at the same minute. If you want to add a Champions League section to your product — a score wall, goal alerts, a pre-match odds comparison, an in-play line tracker — the hard part is never the UI. It’s three upstream questions: where do the fixtures come from, how do you poll nine simultaneous matches without hitting a rate limit, and how do you line the odds up with the scores for the same game?

This guide answers exactly those three. We’ll use the AnySport football data API, start from the Champions League league_id, and walk the whole chain: confirm the season → pull this week’s fixtures → poll live scores across multiple matches → fetch pre-match and in-play odds → subscribe to WebSocket push for the entire competition on one connection. Endpoints, fields, and IDs are all real. Copy, paste, run.

What you can build with Champions League data

Around a single Champions League season, these are the products people actually ship:

  • A Champions League score wall — every match of the night on one screen: score, minute, phase.
  • Goal and red-card alert bots — “Man City 1-0 PSG, 23′” pushed to Telegram, Discord, or Slack.
  • The league-phase table — the new format puts 36 teams in one big table; the top 8 go straight to the round of 16 and 9th–24th play a knockout play-off. The table is content in itself.
  • Pre-match odds comparison — all 18 fixtures of a matchweek with 1X2, Asian handicap, and over/under lines before kickoff.
  • In-play odds monitoring — track the main market once the whistle blows for line-movement alerts or backtesting.
  • Analytics platforms — historical scores plus odds as model inputs; the Champions League is the highest-traffic sample you’ll get.

The data needs split into two layers: the score layer (fixtures, live scores, events) and the odds layer (pre-match, in-play). We’ll cover the design decisions you need to know first, then write code for each layer.

Before you start: five things to know

1. The Champions League league_id and the season string

Every AnySport entity ID carries a semantic prefix: asl_ for leagues, ast_ for teams, as_ for matches, asp_ for players. The Champions League’s league ID is:

asl_33740788c32040dc88ecfcf1c2b23023

The season is a string. A single-year tournament like the World Cup is 2026. A cross-year competition like the Champions League spells out both years with a hyphen: 2026-2027. Not 2026/27, not 2026-27, not 2026. This is the first thing that trips people up on a Champions League integration; step 2 below confirms it against the /seasons endpoint.

2. One match_id runs through every endpoint

The same Champions League fixture carries the same as_-prefixed match_id across fixtures, live scores, the event feed, pre-match odds, in-play odds, and WebSocket push. You find the match once in the fixtures list and reuse that ID everywhere. There is no “fixture ID to odds ID” mapping table to maintain, which removes an entire layer of glue code from an odds comparison page.

3. Everything is UTC, and Champions League kickoffs sit right on a date boundary

Every time field in AnySport is UTC. Fixtures split date and time into two fields; you stitch them into 2026-09-15T19:00:00Z and convert to local time yourself.

Champions League kickoffs make this bite harder than most competitions. The 21:00 CEST slot is 19:00 UTC. For anyone in Asia or Australia that’s already the next calendar day — 3:00 AM in Singapore, 5:00 AM in Sydney — so querying by your local date silently misses the whole evening. For the US it’s 3:00 PM Eastern, and the early 18:45 CEST slot lands at lunchtime. Either way, don’t compute absolute dates by hand. Use the relative-window endpoint /matches/window.

4. status has three shapes, not one enum

The fixtures endpoint’s status field comes in three forms:

  1. A text status: Not Started / HT / FT / AET / Pen. / Postp. and so on
  2. HH:MM (e.g. 19:00): not started; the value is the UTC kickoff time
  3. A bare number (e.g. 23, 67): in progress; the value is the current match minute

“Is this match live?” is a shape check, not a string comparison. The docs explicitly steer you away from using the status query parameter to filter for in-progress matches: pull a date window and classify on the client.

5. Which data lives on which plan

DataEndpointsMinimum plan
Fixtures, live scores, standings/matches /livescore /standingsFree trial (7 days)
Lineups, injuries, players/lineups /injuries /playersBasic (99 USDT/mo)
Pre-match odds/odds/prematchPro (199 USDT/mo)
In-play odds, WebSocket/odds/live wss://…/ws/livePremium (399 USDT/mo)

The entire score layer runs on the free trial. The odds layer needs Pro or Premium.

Champions League data sources compared

Every provider covers the Champions League. The difference isn’t whether you can get it, it’s latency, odds, and price:

ProviderPositioningUCL live scoresUCL oddsNotes
Football-Data.orgEntry-level / open-source friendlyYes (minute-level)NoFree tier is tightly rate-limited; no odds, no push
TheSportsDBCommunity databaseWeakNoMostly static reference data, poor real-time
API-FootballMainstream developer choiceYesYesBroad coverage, good docs, real-time via polling
SportMonksSubscription commercialYesYes (add-on)Tiered plans; odds and lineups cost extra
The Odds API / BetsAPIOdds aggregators / betting-orientedPartialYesIn-play depth varies; BetsAPI has compliance hurdles
Sportradar / Stats Perform (Opta)Enterprise official dataYes (millisecond)YesFastest and most complete; contract-based, very expensive, gated access
AnySportDeveloper-friendly + second-level pushYes (REST + WebSocket)Yes (pre-match + in-play)One match_id everywhere, WebSocket subscriptions filtered by league, crypto billing

(Exact prices and coverage change; check each provider’s site. This table is for positioning only.)

The practical read: Sportradar and Opta are built for sportsbooks and broadcasters, and small teams won’t get past the sales call. Football-Data.org is fine for a weekend project but a Champions League score wall will hit its rate limit fast, and there are no odds. The middle tier is where most developers land. AnySport’s edge there is one match_id shared between scores and odds plus a WebSocket that subscribes to the whole competition on one connection, which matters a lot when nine matches kick off at once.

Step by step: from the Champions League league_id to WebSocket

Step 0: Authentication and a tiny client

Sign up at dashboard.anysport.io; after email verification your API key is on the dashboard home page. There is exactly one way to authenticate: the apikey request header, lowercase.

curl -H "apikey: YOUR_API_KEY" \
  "https://data.anysport.io/v1/soccer/leagues?league_id=asl_33740788c32040dc88ecfcf1c2b23023"

⚠️ Gotcha 1: No ?api_key= query parameter, no X-API-Key header. Anything else returns 401 UNAUTHORIZED. WebSocket is the one exception: there the key goes in the URL. See step 5.

Every REST endpoint lives under https://data.anysport.io/v1/soccer, and every response uses the same success / data / meta / traceId envelope. Wrap it once and reuse it throughout:

import requests
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

API_KEY = "YOUR_API_KEY"
BASE = "https://data.anysport.io/v1/soccer"
HEADERS = {"apikey": API_KEY}

UCL_LEAGUE_ID = "asl_33740788c32040dc88ecfcf1c2b23023"
UCL_SEASON = "2026-2027"          # cross-year seasons spell out both years

def get(path, **params):
    r = requests.get(f"{BASE}{path}", headers=HEADERS, params=params, timeout=10)
    if r.status_code == 429:
        raise RuntimeError("Rate limited. Check your caching strategy.")
    r.raise_for_status()
    body = r.json()
    if not body["success"]:
        raise RuntimeError(f"{body['error']['code']}: {body['error']['message']} (traceId={body.get('traceId')})")
    return body["data"]

Include the traceId when you contact support. It shortens the investigation considerably.

Step 1: Confirm the league and the season

Look the league up by ID and check is_cup while you’re there:

league = get("/leagues", league_id=UCL_LEAGUE_ID)[0]
print(league["name"], league["country"], "is_cup =", league["is_cup"])

/leagues returns league_id / name / country / country_id / is_cup. International competitions like the Champions League come back with is_cup: true.

Then confirm the 2026-2027 season string actually exists via /seasons, and whether it has standings and results yet:

seasons = get("/seasons", league_id=UCL_LEAGUE_ID)
for s in seasons:
    if s["season"] == UCL_SEASON:
        print(s)
        # {'league_id': 'asl_3374...', 'season': '2026-2027', 'has_results': True, 'has_standings': True}

⚠️ Gotcha 2: /seasons is the only authoritative source for valid season values. A cross-year season is 2026-2027. Writing 2026-27 or 2026 doesn’t error; it returns empty data or a different season, which is much harder to debug. Run /seasons once before you write anything else.

Step 2: Pull this matchweek’s fixtures

Use the recent matches endpoint. from_days and to_days are day offsets from today (UTC), so you never compute dates by hand, and because the window already constrains the dates you don’t need to pass season:

curl -H "apikey: YOUR_API_KEY" \
  "https://data.anysport.io/v1/soccer/matches/window?from_days=0&to_days=2&league_id=asl_33740788c32040dc88ecfcf1c2b23023"

Each fixture looks like this (fields per the docs; the match_id and team_id values are whatever the API returns for you):

{
  "match_id": "as_…",
  "date": "2026-09-15",
  "time": "19:00",
  "status": "19:00",
  "league": "…",
  "league_id": "asl_33740788c32040dc88ecfcf1c2b23023",
  "season": "2026-2027",
  "home": { "name": "…", "team_id": "ast_…" },
  "away": { "name": "…", "team_id": "ast_…" },
  "venue": ""
}

Finished matches also carry ht_score / ft_score, matches that went to extra time carry et_score, and shootouts carry penalty_score.

Split the night’s fixtures into three groups and convert kickoffs to your local timezone:

LOCAL_TZ = ZoneInfo("America/New_York")     # or "Asia/Singapore", "Europe/London", ...

def kickoff_local(m, tz=LOCAL_TZ):
    """Stitch the split date + time fields into a UTC datetime, then convert."""
    utc = datetime.strptime(f"{m['date']} {m['time']}", "%Y-%m-%d %H:%M").replace(tzinfo=timezone.utc)
    return utc.astimezone(tz)

def classify(status: str) -> str:
    """Three-shape status check."""
    if status in ("HT", "ET", "P", "Break Time") or status.isdigit():
        return "live"
    if status in ("FT", "AET", "Pen.", "Awarded", "WO"):
        return "finished"
    return "upcoming"      # HH:MM / Not Started / TBA / "" / Postp. etc.

matches = get("/matches/window", from_days=0, to_days=2, league_id=UCL_LEAGUE_ID, limit=100)

groups = {"upcoming": [], "live": [], "finished": []}
for m in matches:
    groups[classify(m.get("status", ""))].append(m)

for m in groups["upcoming"]:
    print(kickoff_local(m).strftime("%a %H:%M"), m["home"]["name"], "vs", m["away"]["name"], m["match_id"])
# Tue 12:45  …   (the 18:45 CEST slot)
# Tue 15:00  …   (the 21:00 CEST slot)

⚠️ Gotcha 3: A Champions League matchday has two kickoff slots, 16:45 and 19:00 UTC. from_days=0 means UTC today. If you’re east of UTC+5 those matches are “tomorrow morning” locally but still “today” to the API. Size the window in UTC, not in your local calendar.

⚠️ Gotcha 4: /matches/window defaults to_days to 7, caps the span at 90 days, and caps limit at 100. One matchweek is 18 fixtures over two or three days, so to_days=2 with limit=100 fits on one page. Pulling the whole league phase means paginating; check meta.total_pages.

Step 3: Live scores across multiple matches — do the rate-limit math first

Livescore endpoint (/livescore) is a per-match endpoint. match_id is required; there is no “give me every live match” call.

def live_score(match_id):
    rows = get("/livescore", match_id=match_id)
    return rows[0] if rows else None

The response (a real payload from the docs, Argentina 3-1 Switzerland at the 2026 World Cup; Champions League matches have identical fields):

{
  "match_id": "as_3b37c382f39a4235aa93705f053b2da4",
  "home": "Argentina",
  "away": "Switzerland",
  "league": "World Cup 2026",
  "minute": 120,
  "phase": "finished",
  "home_score": 3,
  "away_score": 1,
  "ht_score": "1-0",
  "reg_score": "1-1",
  "et_score": "3-1",
  "updated_at": "2026-07-12T03:45:50Z"
}

⚠️ Gotcha 5: /livescore does not return status. It returns phase (regular / extra_time / penalty / finished). Full time is phase == "finished", period. minute is a continuous clock that includes stoppage time and never resets, so “are we in extra time?” is minute > 90, not a phase check — ordinary attacking frames during extra time still report regular. The league phase has no extra time, so this only bites from the knockout rounds in February 2027, but write it correctly now and you won’t touch it later.

The real problem with a Champions League score wall is concurrency versus rate limits. Every plan has both a per-second and a per-hour cap, and exceeding either returns 429:

PlanPer secondPer hour9 concurrent matches, one poll round every 30 s
Free trial11201,080 req/h — you hit the wall 7 minutes after kickoff
Basic12,000Gets you through one night with no headroom
Pro310,000Fine
Premium620,000Fine, and you should be on WebSocket anyway

So a REST-polled score wall has to be disciplined: poll only the live group, cache the fixtures list, space out the rounds, and serialize requests to respect the per-second cap:

import time

def poll_wall(live_matches, interval=60):
    """Poll only in-progress matches; refresh the groups from /matches/window every few minutes."""
    while live_matches:
        still_live = []
        for m in live_matches:
            s = live_score(m["match_id"])
            time.sleep(1.1)          # Free / Basic: 1 request per second, serialized
            if not s:
                continue
            print(f"{s['home']} {s['home_score']}-{s['away_score']} {s['away']}  "
                  f"{s['minute']}'  {s['phase']}")
            if s["phase"] != "finished":
                still_live.append(m)
        live_matches = still_live
        time.sleep(interval)

This loop makes roughly 540 requests an hour for nine matches at one round per minute, which already blows through the free trial’s 120. The free trial is for proving the pipeline works. A production Champions League score wall is either Pro with polling or Premium with WebSocket. WebSocket is the right answer, and step 5 covers it.

Step 3½: The goal event feed

/livescore gives you the result. For “Man City 1-0, 23′, Haaland” you need the Match stats endpoint, /matches/{match_id}/situation, which returns a full snapshot of every event so far plus eleven match stats. Each poll returns the whole history, so you replace local state wholesale rather than stitching deltas.

def new_goals(match_id, seen: set):
    """Return goal events not yet processed; `seen` holds handled seq numbers."""
    s = get(f"/matches/{match_id}/situation")
    fresh = [e for e in s["events"] if e["type"] == "goal" and e["seq"] not in seen]
    seen.update(e["seq"] for e in fresh)
    return fresh

seen = set()
for e in new_goals("as_YOUR_MATCH_ID", seen):
    print(f"{e['minute']}'  {e.get('team','')}  {e['text']}")

Event type has seven values: goal / yellow_card / red_card / corner / penalty / substitution / event. stats is a fixed set of eleven keys with [home, away] pairs, e.g. "c": [8, 2] means eight corners to two.

⚠️ Gotcha 6: The situation endpoint’s status (values like home_corner, var_goal, full_time) and the event type are open enums that grow over time. Degrade unknown values to a generic event instead of throwing. And before kickoff the endpoint returns 404 — treat it as “no data yet”, not as a system error.

Step 4: Pre-match and in-play odds

Odds use the same match_id. Pre-match odds comes from /odds/prematch (Pro and above):

curl -H "apikey: YOUR_API_KEY" \
  "https://data.anysport.io/v1/soccer/odds/prematch?match_id=as_YOUR_MATCH_ID"

The response (a real payload from the docs, England vs Argentina in the World Cup semi-final, trimmed):

{
  "match_id": "as_37f0da0520bb4e9a9588f80994cefc5b",
  "home": "England",
  "away": "Argentina",
  "bookmaker": "bet365",
  "phase": "prematch",
  "main_market": "match_winner",
  "home_odd": 2.62,
  "draw_odd": 3.0,
  "away_odd": 3.0,
  "markets": [
    { "market_code": "match_winner",
      "options": [ { "name": "Home", "odd": 2.62 }, { "name": "Draw", "odd": 3.0 }, { "name": "Away", "odd": 3.0 } ] },
    { "market_code": "asian_handicap",
      "options": [ { "name": "Home -1.25", "odd": 4.8 }, { "name": "Away +1.25", "odd": 1.18 }, { "name": "Home -1", "odd": 4.4 } ] }
  ]
}

Once the match starts, switch to Live Odds (/odds/live) with the same match_id. The top-level fields match, but two structural things differ:

Pre-match /odds/prematchIn-play /odds/live
main_marketmatch_winnerfulltime_result
1X2 option labelsHome / Draw / Away1 / X / 2
Where the line livesInside the option name (Home -1.25)A separate markets[].handicap field, omitted on markets with no line

Here’s the core of a matchweek odds comparison page: pull the main market for all 18 fixtures and convert to implied probability.

def implied(odds_row):
    raw = {"1": odds_row["home_odd"], "X": odds_row["draw_odd"], "2": odds_row["away_odd"]}
    total = sum(1 / v for v in raw.values())        # strip the overround
    return {k: round(1 / v / total, 3) for k, v in raw.items()}

for m in groups["upcoming"]:
    rows = get("/odds/prematch", match_id=m["match_id"])
    if not rows:
        continue                                    # market not open yet
    o = rows[0]
    print(f"{o['home']} vs {o['away']}  "
          f"{o['home_odd']} / {o['draw_odd']} / {o['away_odd']}  {implied(o)}")
    time.sleep(0.4)                                 # Pro: 3 requests per second

⚠️ Gotcha 7: Pre-match and in-play are two physically separate feeds, and the league field is named differently in each. The docs’ own example is World: World Championship - Play Offs pre-match versus World Cup 2026 in-play, for the same tournament. Don’t filter Champions League odds by league name. Iterate the match_id list you got from the fixtures endpoint. That’s what the shared-ID design is for.

⚠️ Gotcha 8: In-play markets shrink dynamically. After a goal the affected markets are pulled and re-listed, the count drops toward the end of the match, and after full time markets can be []. Never assume a given market_code is present. “Next N minutes” markets also carry a double space in the option name (Goals/Over 0.5); split on \s+.

Step 5: WebSocket — the whole Champions League on one connection

Premium includes WebSocket push, which is the right architecture for a score wall or in-play monitor. Four streams can share one connection:

StreamSubscribe codeAck codePush type
Scores2020020201score_update
Situation (events + stats)2000020001situation_update
In-play odds2010020101odds_live_update
Pre-match odds2030020301odds_prematch_update
Heartbeat20010none—

The subscribe message’s data accepts three filters: match_id, league_id, and country_id. For the Champions League, league_id is the one you want: one subscription covers every match of the night instead of subscribing per fixture.

import asyncio, json, uuid
import websockets    # pip install websockets

API_KEY = "YOUR_API_KEY"
WS_URL = f"wss://api.anysport.io/v1/ws/live?api_key={API_KEY}"
UCL_LEAGUE_ID = "asl_33740788c32040dc88ecfcf1c2b23023"

def sub(code, **data):
    return json.dumps({"code": code, "trace": str(uuid.uuid4()), "data": data})

async def heartbeat(ws):
    while True:
        await asyncio.sleep(30)                      # server drops you after 60 s of silence
        await ws.send(json.dumps({"code": 20010, "trace": "hb"}))

async def main():
    async with websockets.connect(WS_URL) as ws:
        print(await ws.recv())                       # {"type": "connected", ...}

        # One connection: scores + in-play odds, both filtered to the Champions League
        await ws.send(sub(20200, league_id=UCL_LEAGUE_ID))
        await ws.send(sub(20100, league_id=UCL_LEAGUE_ID))
        asyncio.create_task(heartbeat(ws))

        async for raw in ws:
            msg = json.loads(raw)
            t = msg.get("type")
            if t == "score_update":
                d = msg["data"]
                print(f"[score] {d['home_team']} {d['home_score']}-{d['away_score']} {d['away_team']}"
                      f"  {d.get('minute','')}'  phase={d.get('phase','')}")
            elif t == "odds_live_update":
                d = msg["data"]
                print(f"[odds]  {d['home_team']} vs {d['away_team']}  "
                      f"{d.get('home_odd')} / {d.get('draw_odd')} / {d.get('away_odd')}")
            elif t == "error":
                print("connection-level error:", msg["message"]); break   # heartbeat timeout / rate limit: reconnect
            elif "code" in msg:
                print("ack:", msg["code"], msg.get("msg"))  # 20201 / 20101 = subscribed

asyncio.run(main())

A score push looks like this (docs sample):

{
  "type": "score_update",
  "data": {
    "type": "score",
    "match_id": "…",
    "home_team": "Manchester United",
    "away_team": "Arsenal",
    "league": "Premier League",
    "phase": "2H",
    "minute": 67,
    "home_score": 2,
    "away_score": 1,
    "ht_score": "1-0",
    "reg_score": "2-1",
    "et_score": null,
    "penalty_score": null,
    "source_updated_at": "2026-07-13T15:67:01Z"
  },
  "timestamp": "2026-07-13T15:67:01.250Z"
}

⚠️ Gotcha 9: WebSocket inverts three REST conventions. The host is api.anysport.io, not data.anysport.io. The key goes in the URL as ?api_key= (or in the subscribe message’s data.api_key), not in a header. Filters go in the subscribe message’s data, never in the URL. A rejected handshake surfaces as an HTTP 401 / 403 / 429 before any WebSocket message arrives, so handle it in your connect path.

⚠️ Gotcha 10: Push payloads don’t share REST’s field names. Teams are home_team / away_team (REST: home / away), phase is 1H / HT / 2H / FT (REST: regular / finished), and in-play odds arrive as strings ("2.10") where REST sends numbers. Keep the two parsers separate.

⚠️ Gotcha 11: A failed subscription returns 40301 (no permission or expired plan) and the connection stays open; fix and resend. Re-sending a subscribe code for the same stream replaces the old filter, no reconnect needed. Only a type: "error" message (heartbeat timeout, or more than 60 requests a minute) closes the connection, and that’s when you reconnect with backoff. The docs list league_id as an optional extension filter, so as a belt-and-braces measure, also filter client-side against the match_id set from step 2.

Bonus: The league-phase table

The 36-team table is standard content for any Champions League page, and /standings is on the free trial. season is required and must be 2026-2027:

table = get("/standings", league_id=UCL_LEAGUE_ID, season=UCL_SEASON)
for row in table[:8]:                                  # top 8 qualify directly for the round of 16
    print(row["position"], row["team"], row["played"], row["points"], row["goal_difference"], row["form"])

Fields are position / team / team_id / played / wins / draws / losses / goals_for / goals_against / goal_difference / points / form. form is a string like WWD, which front ends usually render as a row of coloured W/D/L chips. The table doesn’t change second to second, so fetch it once and cache it until the matchday ends.

Champions League gotchas: quick reference

#GotchaDo this instead
1Passing the keyREST uses the apikey header; ?api_key= and X-API-Key don’t work
2Season stringCross-year is 2026-2027; confirm with /seasons first
3Kickoff timesEverything is UTC; 19:00 UTC is the next day east of UTC+5; use /matches/window, not local dates
4Fixture paginationOne matchweek fits in limit=100; the full league phase needs meta.total_pages
5Full-time check/livescore uses phase == "finished"; it has no status field
6Open enumsSituation status, event type, market_code: degrade unknowns, don’t throw; pre-kickoff situation is 404
7Filtering odds by leaguePre-match and in-play name leagues differently; iterate match_ids instead
8In-play marketsThey shrink; markets can be [] after full time; option names can carry double spaces
9WebSocket addressapi.anysport.io, key in the URL, filters in the subscribe data
10Push field nameshome_team / phase=2H / string odds; separate parser from REST
11Failed subscription40301 keeps the connection open, resend; only type: error disconnects
12Rate limitsPer-second and per-hour caps, either returns 429; the free trial’s 120/h can’t run a score wall; cache fixtures and standings
13Pre-match vs in-play labelsMap Home/Draw/Away to 1/X/2; the handicap line lives in different places

Wrapping up

Getting the 2026/27 Champions League into your product comes down to five steps:

  1. Confirm league and season — /leagues + /seasons, giving you asl_33740788c32040dc88ecfcf1c2b23023 and 2026-2027.
  2. Pull the matchweek — /matches/window?league_id=…&from_days=0&to_days=2, convert UTC to local, classify by the three-shape status.
  3. Live scores — /livescore per match_id, do the rate-limit math before you write the poll loop, use phase for full time.
  4. Odds — the same match_id against /odds/prematch and /odds/live, with a thin mapping layer for labels and handicap lines.
  5. Second-level push — one WebSocket connection, filtered by league_id, scores and in-play odds subscribed together.

Two things make AnySport unusually low-friction for this competition: one match_id from fixtures all the way through to odds, so an odds comparison page needs no ID mapping, and WebSocket subscriptions filtered by league_id, so nine simultaneous matches still cost you one connection. The rest is remembering the thirteen gotchas above.

Seven league-phase rounds remain. Sign up at anysport.io for a free 7-day trial and the score layer can be running tonight. The odds layer needs Pro (199 USDT/mo) or Premium (399 USDT/mo). All plans are billed in USDT with no contract.