Skip to content

Live Sports Scoreboard

Works with

WiFi boards with an LED matrix or display — Adafruit Matrix Portal S3 (recommended), or any WiFi board + HT16K33 matrix or MAX7219 matrix

Libraries used: adafruit_requests · adafruit_ht16k33 / adafruit_matrixportal · adafruit_display_text


What you will build

A live scoreboard that fetches real game data from the ESPN public API over WiFi and scrolls the scores across an LED matrix — no account, no API key, no subscription required. It updates every minute. Every time you glance at it during a match, it has the latest score. Add a buzzer and it screams when the home team scores.

This project is based on the Adafruit LED Matrix FIFA World Cup Scoreboard guide. Credit: Adafruit Learning Systemhttps://learn.adafruit.com/led-matrix-fifa-world-cup-scoreboard

The Matrix Portal S3 is the ideal board for this: it has a built-in ESP32-S3 with WiFi, a 64×32 RGB LED matrix connector, and CircuitPython support out of the box. But the core API fetch and JSON parsing code runs on any WiFi CircuitPython board.


Parts list

Part Notes
Adafruit Matrix Portal S3 Adafruit #5778 — includes WiFi and matrix connector
64×32 RGB LED matrix panel Adafruit #2278 or compatible HUB75 panel
5V 4A power supply The matrix draws significant current
USB-C cable For flashing firmware
Alternative: Any WiFi CircuitPython board + HT16K33 8×8 matrix Adafruit #1080 for the matrix, I2C wiring

Wiring

graph TD
    MP["Matrix Portal S3"]
    MATRIX["64x32 RGB LED Matrix\n(HUB75 connector)"]
    PWR["5V 4A Power Supply"]

    MP -- "HUB75 ribbon cable (direct)" --> MATRIX
    PWR -- "5V / GND barrel jack" --> MATRIX
    PWR -- "5V to Portal power pins" --> MP

Option B: WiFi board + HT16K33 matrix (I2C)

graph TD
    MCU["WiFi CircuitPython Board\n(ESP32-S2 / Pico W)"]
    MAT["HT16K33\n8x8 or 16x8 LED Matrix"]
    PWR["3.3V from board"]

    MCU -- "3.3V" --> MAT
    MCU -- "GND" --> MAT
    MCU -- "SCL" --> MAT
    MCU -- "SDA" --> MAT

Complete code

The code below uses the Matrix Portal S3 with adafruit_matrixportal. For HT16K33, see the adaptation note after the code.

import time
import board
import wifi
import socketpool
import ssl
import adafruit_requests
import displayio
import terminalio
from adafruit_matrixportal.matrix import Matrix
from adafruit_display_text.scrolling_label import ScrollingLabel

# ----------------------------------------------------------------
# Configuration
# ----------------------------------------------------------------
WIFI_SSID     = "YourNetworkName"
WIFI_PASSWORD = "YourPassword"

# ESPN Soccer/Football scoreboard endpoint
# Change "soccer" to "basketball", "football", "baseball", etc.
# Change the league slug to match your sport:
#   soccer:  "usa.1" (MLS), "eng.1" (Premier League), "esp.1" (La Liga)
#   basketball: "nba"
SPORT   = "soccer"
LEAGUE  = "eng.1"   # English Premier League
ESPN_URL = f"https://site.api.espn.com/apis/site/v2/sports/{SPORT}/{LEAGUE}/scoreboard"

REFRESH_INTERVAL = 60   # seconds between API calls
# ----------------------------------------------------------------

# --- WiFi ---
print(f"Connecting to {WIFI_SSID}...")
wifi.radio.connect(WIFI_SSID, WIFI_PASSWORD)
print(f"Connected. IP: {wifi.radio.ipv4_address}")

pool    = socketpool.SocketPool(wifi.radio)
session = adafruit_requests.Session(pool, ssl.create_default_context())

# --- Matrix Portal display ---
matrix  = Matrix(bit_depth=2)
display = matrix.display
group   = displayio.Group()
display.root_group = group

scroll_label = ScrollingLabel(
    terminalio.FONT,
    text="Fetching scores...",
    color=0x00FF00,
    max_characters=64,
    animate_time=0.04,
)
scroll_label.x = 0
scroll_label.y = 12
group.append(scroll_label)


def fetch_scores():
    """Fetch ESPN scoreboard JSON and return a formatted score string."""
    print("Fetching scores from ESPN...")
    try:
        response = session.get(ESPN_URL, timeout=10)
        data     = response.json()
        response.close()
    except Exception as e:
        print(f"Fetch error: {e}")
        return "Network error"

    events = data.get("events", [])
    if not events:
        return "No games today"

    parts = []
    for event in events:
        # Safely navigate nested JSON
        competitions = event.get("competitions", [])
        if not competitions:
            continue
        comp         = competitions[0]
        competitors  = comp.get("competitors", [])
        if len(competitors) < 2:
            continue

        # ESPN puts home team at index 0 or 1 — use homeAway field
        home = next((c for c in competitors if c.get("homeAway") == "home"), competitors[0])
        away = next((c for c in competitors if c.get("homeAway") == "away"), competitors[1])

        home_name  = home.get("team", {}).get("abbreviation", "???")
        away_name  = away.get("team", {}).get("abbreviation", "???")
        home_score = home.get("score", "-")
        away_score = away.get("score", "-")
        status     = event.get("status", {}).get("type", {}).get("shortDetail", "")

        parts.append(f"{away_name} {away_score}-{home_score} {home_name} ({status})")

    return "  |  ".join(parts) if parts else "No scores available"


last_fetch = -REFRESH_INTERVAL  # force immediate fetch on startup

while True:
    now = time.monotonic()

    # Refresh scores on schedule
    if now - last_fetch >= REFRESH_INTERVAL:
        score_text   = fetch_scores()
        scroll_label.full_text = score_text + "     "   # trailing spaces for clean loop
        last_fetch   = now
        print(f"Scores: {score_text}")

    # Advance the scrolling animation one frame
    scroll_label.update()

    time.sleep(0.02)

Tip

HT16K33 adaptation: Replace the Matrix Portal / adafruit_matrixportal import and display setup with an I2C HT16K33 matrix. Use adafruit_ht16k33.matrix.Matrix8x8 and write score abbreviations with matrix.print(). The WiFi, adafruit_requests, and JSON parsing sections are identical.


How it works

The ESPN public API and JSON structure

ESPN runs a public JSON API that is used by its own website and apps — and which anyone can call without authentication. The scoreboard endpoint returns a JSON object with an "events" array. Each event represents one game and contains a "competitions" array, which in turn contains a "competitors" array with the two teams, their scores, and their "homeAway" designation. The "status" object tells you whether the game is live, scheduled, or final. This structure is consistent across sports — the same parsing logic works for basketball, baseball, and American football by changing the SPORT and LEAGUE constants.

Parsing nested JSON safely with .get()

API responses are not always structured exactly as you expect — fields can be missing if a game has not started yet, or the API changes slightly between seasons. Using .get("key", default) instead of ["key"] means your code returns the default value instead of crashing with a KeyError when a field is absent. The pattern event.get("status", {}).get("type", {}).get("shortDetail", "") chains three .get() calls: if any level of nesting is missing, the chain short-circuits to an empty string. This is the correct way to traverse unknown or partially populated JSON in any language.

Scrolling text on a matrix

A 64×32 LED matrix can only show a few characters at a time at a readable size. adafruit_display_text.scrolling_label.ScrollingLabel handles the animation: you set full_text to however long a string you like, and each call to .update() advances the scroll position by one pixel if enough time has passed (controlled by animate_time). Calling .update() in a tight loop with a short time.sleep() gives smooth scrolling without blocking the main program. Because the scroll position persists between API refreshes, you can update full_text mid-scroll and the display will seamlessly transition to the new content.


Remix ideas

Remix idea

Sound the alarm when the home team scores. Track the home team's score between fetches. If it increases, trigger a buzzer tone. See the simpleio tones reference for the one-line simpleio.tone() call you need.

Remix idea

Show standings instead of live scores. ESPN's API also has a standings endpoint: swap scoreboard for standings in the URL and parse the standings array instead of events. Display each team's win/loss record in the scroll.

Remix idea

Push scores to a TV or smart home display. Send the score string to an MQTT broker and subscribe to it from Home Assistant or Node-RED to display on a dashboard. The MQTT and Home Assistant builder covers the full MQTT setup.


Go deeper