Reddit MLB

Published on 2025 05 31·...
Reddit MLB

Our Client

Reddit is one of the largest and most active platforms on the internet, with over 108 million daily and 401 million weekly active users engaging across 100,000+ communities, contributing to a staggering 22 billion+ posts and comments.

With such a massive, engaged user base, Reddit offers an unparalleled opportunity for community-driven experiences. Whether you're into breaking news, sports, TV fan theories, or a never-ending stream of the internet's cutest animals, there's a community on Reddit for you.

What We Did

We designed an interactive MLB scoreboard experience on Reddit to bring together the platform's highly engaged sports fans. This approach brings real-time, in-platform updates to r/MLB, enhancing fan interaction without redirecting them off Reddit.

Meet the Team

Timeframe

March — June 2025

Tools

Design: Figma

Development: Devvit, SportsRadar

Workflow: Notion, Slack, Github

The Product

Our Users

Among over 500 million active accounts and 100k+ active communities, sports are one of Reddit's most popular and fastest-growing interest groups. Currently, Reddit is partnered with a number of major sports leagues to bring together highly engaged fans from over 1000 communities.


By partnering with major leagues, Reddit has already seen increased engagement through live updates in football, basketball, soccer, and cricket communities. Our goal was to bring this same real-time, interactive experience to MLB fans, meeting their desire for instant scores, stats, and game updates within the r/MLB community.

Competitive Analysis

To understand what baseball statistics users are familiar with seeing, we conducted market research on other live scoreboards from MLB.com, ESPN, and Yahoo Sports. We deduced that there are three use cases: pre-, live, and post-game, each with its own stats.

Competitive Analysis

Information Architecture

The IA was designed around the three main phases of a game: pre-game, live game, and post-game.


In each phase, we sorted information based on our 4 main tabs which were summary, play-by-play, stats, and box score that covered the most basic stats and tables that most MLB scoreboards show based on our market analysis.

Information Architecture

Pre-Game

The pre-game tab is targeted towards users primarily looking for pitching matchups, lineups, standings, and game time to keep users updated. The summary tab also features a poll for users to interact with and view other's choices and increase engagement.

Live-Game

The live game scoreboard allows users to see what inning it is, how many outs the offensive team has, who's on base, a breakdown of all plays and scoring plays, as well as the box score. In our initial iterations, we explored a key moments tab in which users would be redirected to a third-party video streaming service to watch highlights. However, after receiving feedback from stakeholders, we decided to omit the feature to keep the experience on the Reddit platform.

Summary

Play by Play

Box Score

Post-Game

The post-game scoreboard has an extensive overview of all game stats and allows users to catch up in case they missed the live game.

Post Game Desktop Views Post Game Mobile Views

Additional Feature: Drag and Drop Moderator Dashboard

Alongside the live scoreboard app, we explored how to further elevate the moderator experience by designing a customizable dashboard tailored to individual preferences.


The result? A drag-and-drop widget sidebar that lets moderators personalize their workspace with features most relevant to their style of game coverage.


As sports fans ourselves, we know every game has multiple layers — pre-games, post-games, Playoffs, Sports Betting, and more. What matters most can vary from one moderator to another, and trying to juggle everything at once can get overwhelming.


With this feature, moderators can:


  1. Add and arrange widgets such as Game Highlights, Player Statistics, Playoff Brackets, and more
  2. Customize the layout to match their flow and priorities
  3. Adapt in real-time as the game evolves

This flexibility not only enhances usability but also brings a human touch to moderation — fueling engagement and keeping the r/MLB community energized and passionate throughout the season.

Proof of concept mod dashboard prototype

Devvit

Devvit is Reddit's official app platform that allows developers to build fully interactive apps directly inside Reddit posts. Unlike traditional web development, Devvit combines frontend views, backend logic, Reddit API access, and storage all in one codebase, natively hosted by Reddit.


For our project, using Devvit was essential. It's what made it possible for our live MLB scoreboard to be downloaded and installed by any subreddit through the Devvit app store. This means mods don't need to run a server or do anything technical they just install the app, pick a game, and the post automatically updates in real-time with scores, stats, and polls.


That said, learning Devvit wasn't simple. With no access to typical web tools like HTML, CSS, or scrolling, we had to rethink our entire frontend layout and build everything using Devvit's minimal UI primitives. But the challenge paid off it gave us a clean, maintainable setup that's deeply integrated with Reddit, portable across communities, and scalable as Devvit evolves.

Sports Radar

Sports Radar is our unified data-ingestion layer for pulling all scheduled games, team profiles, and (optionally) odds metadata from the SportsRadar API. We ingest this data once per session to power both the pre-game lobby and the live scoreboard.

What we needed

  • Daily schedule: list of every game (date, time, teams) for the current day (and lookahead window)
  • Team metadata: abbreviations, venue
  • Live game stats and scores

Fetch strategy & frequency

All of these strategies are subject to change as we are still testing/updating based on what works best.

1. Initial load

GET https://api.sportradar.us/nfl/trial/v7/en/games/{{today}}/schedule.json?api_key={{API_KEY}}<br></br>

2. Cache-aware refresh

  • Read the response's Cache-Control: max-age={{n}} header
  • Schedule the next fetch after n seconds (e.g. if max-age=3600, re-fetch in 1 hr)
  • Fallback: if no header present, fall back to a 30 min poll interval

3. Error & rate-limit handling

  • 429 → exponential backoff (500 ms × 2ⁿ up to 8 s)
  • 5xx → retry once after 5 s

async function refreshSchedule() {  
  const url = `https://api.sportradar.us/nfl/trial/v7/en/games/${today}/schedule.json?api_key=${API_KEY}`;  
  const res = await context.http.get({ url });  
  const schedule = await res.body.json();

  // Determine next fetch time from Cache-Control  
  const cc = res.headers.get('cache-control') || '';  
  const match = cc.match(/max-age=(\\d+)/);  
  const delay = match ? parseInt(match[1], 10) * 1000 : 30 * 60 * 1000;  
  setTimeout(refreshSchedule, delay);  
  await context.redis.set('schedule_cache', JSON.stringify(schedule.games));  
}

We tried to poll every 10s for live updates. However, we found this to be inefficient and a waste of resources. Ultimately, for schedule data we relied on SportsRadar's max-age hints to minimize unnecessary requests.

Form set up

We started with setting up the API key with Devvit. This is set up in Devvit.addSettings(). In the CLI run devvit settings set <api-name>. Then we set the API key.


import { SettingScope } from '@devvit/public-api';

Devvit.addSettings([  
  {  
    name: 'sportsradar-api-key',  
    label: 'SportsRadar API key',  
    type: 'string',  
    isSecret: true,  
    scope: SettingScope.App,  
  },  
]);

Forms in Devvit allow us to collect structured input from Reddit users right from within a custom post. This is powerful for dynamic content like sports data, where a user might want to pull live stats or search for a specific game. We used forms to allow a user to input a game ID from SportsRadar, which we then used to call specific endpoints and populate the rest of the UI with live data. We created a GameSelectionForm.tsx file to hold the contents of our game selection form.

Devvit exposes the useForm() hook to declaratively define and launch interactive forms. Forms can contain text, number, boolean, or select inputs. We started with taking in a string for the gameId to retrieve a JSON of the boxscore data. After being able to hit the initial endpoint, we built atop this pre-existing form. We took in the year, month, and day as the parameters for Daily Schedule this allowed us to retrieve all gameIds from that date.


GET `https://api.sportradar.us/mlb/trial/v8/en/games/${year}/${month}/${day}/schedule.json`

The response was used to allow the user to select from all the games on the given date. We did this through creating a second form that maps all relevant games as options for the user to select.



import { Devvit } from '@devvit/public-api';
const gameSelectionForm = Devvit.createForm(
                    {
                        fields: [
                            {
                                type: 'select',
                                name: 'gameId',
                                label: 'Select Game',
                                required: true,
                                options: data.games.map((game: any) => ({
                                    label: `${game.away.name} @ ${game.home.name}`,
                                    value: game.id
                                }))
                            },
                            {
                                type: 'string',
                                name: 'title',
                                label: 'Post Title',
                                required: true,
                                defaultValue: 'MLB Game Scorecard',
                            }
                        ],
                    },
                )

The selected gameId was used to fetch Game Boxscore

Pre game

For the pre-game phase of our MLB scoreboard application, we built out the core features that show relevant information before a game starts. For data handling, the game information is fetched once when a post is created.


For data handling, the game information is fetched once when a post is created. We use the SportsRadar API and cache the results in Redis for faster access. We also set up logic to refresh the data based on the API's cache-control headers, with a fallback to refresh every 30 minutes if no header is present. To handle errors, we added exponential backoff for rate limits, a single retry for server errors, and a fallback to cached data if the API fails.


// Use useAsync for initial data load
const { loading, error, data: asyncGameData } = useAsync<any>(async () => {
  try {
    if (!context.postId) {
      throw new Error('No post ID available');
    }
    const gameInfo = await GameService.getGameData(context, context.postId);
    if (gameInfo) {
      setIsLive(gameInfo.status === 'in-progress');
      if (gameInfo.status === 'in-progress') {
        await LiveUpdateService.addToActiveGames(context, gameInfo.id);
      }
    }
    return gameInfo;
  } catch (err) {
    console.error('[Scoreboard] Error loading game data:', err);
    throw err;
  }
});

// Set up realtime updates for live games
const channelName = gameId ? `game_updates_${gameId}` : 'waiting_for_gameid';
const channel = useChannel({
  name: channelName,
  onMessage: (updatedData: any) => {
    if (updatedData?.game) {
      console.log('[Scoreboard] Received game update:', updatedData);
      setGameData(parseGameBoxscore(updatedData.game));
      setLastUpdateTime(Date.now());
    }
  },
});

This setup covers the key functionality needed for the pre-game phase and keeps the user interface up to date without overloading the backend.


In order to bring users back to the scoreboard post during the time of a game, we created a countdown and remind me feature. The countdown is driven by a helper function, getCountdown, which calculates the time difference between the game's scheduled start (properly parsed for timezone) and the current time, returning days and hours. In the "Pregame" view, these values are displayed with components for days and hours left until the game. We also implemented a "Remind Me" feature that allows users to get a notification in their Reddit user inbox when they have pressed a button located next to the countdown.


To allow users to interact with the application during the pregame phase, we created a community poll feature where a user can click a button to vote which team they believe is more likely to win, unlocking a view of poll predictions. Polling relies on two core functions — voteForTeam, which atomically updates a Redis counter per user to prevent duplicate votes, and getPollResults, which fetches totals and computes percentages via the Devvit SDK. The interface uses a useAsync hook to load initial results, updates local state on each vote, and renders vote bars by setting <vstack>widths according to the calculated percentages for a more cohesive look. We used Devvit docs for Redis transactions to aid in our implementation of this.



async function voteForTeam(team: string) {
                const currentUser = await context.reddit.getCurrentUser();
                const userId = currentUser?.id;
                const userKey = `poll:${team}:user:${userId}`;
                const hasVoted = await context.redis.get(userKey);
                if (!hasVoted) {
                    await context.redis.set(userKey, '1');
                    const txn = await context.redis.watch(`poll:${team}`);
                    await txn.multi();
                    await txn.incrBy(`poll:${team}`, 1); // Increment the selected team's votes
                    await txn.exec();
                }
}
            async function getPollResults(homeTeam: string, awayTeam: string) {
                const [home, away] = await context.redis.mGet([`poll:${homeTeam}`, `poll:${awayTeam}`]);
                const homeVotes = parseInt(home ?? '0', 10);
                const awayVotes = parseInt(away ?? '0', 10);
                const total = homeVotes + awayVotes;
                return {
                    home: homeVotes,
                    away: awayVotes,
                    total,
                    homePct: total ? Math.round((homeVotes / total) * 100) : 0,
                    awayPct: total ? Math.round((awayVotes / total) * 100) : 0,
                };
            }

Live game

The Live Game module streams in-play updates (scores, key plays) into our live-scoreboard UI based on SportsRadar's game status fields.


Data sources & endpoints

All of these strategies are subject to change as we are still testing/updating based on what works best.


Boxscore GET <https://api.sportradar.us/nfl/trial/v7/en/games/{{game_id}>}/boxscore.json


Play-by-play


GET <https://api.sportradar.us/nfl/trial/v7/en/games/{{game_id}>}/pbp.json


Update strategy


  1. Cache-directive driven
  • Read Cache-Control: max-age={{m}} on each response
  • Schedule the next fetch after m seconds (SportsRadar typically returns 5–15 s for live feeds)

  1. Phase detection
  • Inspect game.status to switch views
  • Delegate phase-switch logic to our GamePhase handler

  1. Polling fallback
  • If no caching header, poll every 10 s during live, 30 s in final

async function fetchLive(gameId) {
  const url = `https://api.sportradar.us/nfl/trial/v7/en/games/${gameId}/pbp.json?api_key=${API_KEY}`;
  const res = await context.http.get({ url });
  const { plays, game } = await res.body.json();
  
  // Respect cache hint
  const cc = res.headers.get('cache-control') || '';
  const next = cc.match(/max-age=(\d+)/)
    ? parseInt(cc.match(/max-age=(\d+)/)[1], 10) * 1000
    : 10 * 1000;
  
  // Phase switching
  GamePhase.handle(game.status);
  
  const events = plays.map(p => ({
    time: p.clock,
    quarter: p.period.number,
    description: p.text,
    team: p.offense === game.home.id ? 'home' : 'away',
  }));
  
  setLiveEvents(events);
  setTimeout(() => fetchLive(gameId), next);
}

  • Cache-aware fetches drastically reduced request volume vs. blind polling
  • Observed intermittent 429s when rapidly switching games; proposed per-game backoff queues
  • Phase logic encapsulated in a GamePhase utility that drives UI state and fetch cadence

Post game

For the post-game phase, we implemented a set of features that focus on showing the final outcome of the game in a clear and direct way. The first step was phase detection. In app.tsx, there is a logic to detect whether the game had ended based on the game status. This step is important because it determines when to render the post-game view, sets up the correct tab structure, and ensures we're working with final game data.


Once the post-game phase is active, we fetch the extended summary from the SportsRadar API. This endpoint gives us detailed team stats, a full summary of how the game played out, and any available historical context. and we display that information in postgame.tsx.


We also built a score display that emphasizes the winner. The winning team's score is shown in bold, while the losing team's score uses regular text. We added a "Final" label to make it obvious that the game has ended. This small visual structure helps users quickly understand the outcome. To support this, wrote logic that determines the winner based on the final scores. It also handles tie games and generates a short summary sentence that explains the result. This logic feeds into both the visual score display and the extended summary.


Frontend


During frontend development we encountered several inherent limitations in Devvit's frontend primitives, which required us to devise workarounds rather than rely on conventional CSS or HTML. Below is a concise, formal overview of the challenges and our solutions:


Component Customization Constraints


Typography: The platform did not support importing custom font-faces or external stylesheets. However, we were able to achieve a look that was cohesive with Reddit's design system using the Devvit capabilities. To do so, we made all text styling utilize the exist Devvit <text> component's built-in props (size, weight, color). For components that we needed to create that Devvit did not have built-ins for such as a horizontal line bar we relied on the <hstack> elements to create a workaround that suited our app. Below is an example of a horizontal bar we created in order to match our designs:


<vstack width="100%" maxWidth={600} padding="medium" gap="large" cornerRadius="large">
  {/* Horizontal Line */}
  <hstack width="100%" height="1px" backgroundColor="#000000" />
  {/* Tabs Section */}
  <hstack width="100%" gap="small" alignment="center middle">
    <button
      appearance={selectedTab= 'summary' ? 'primary' : 'secondary'}
      size="medium"
      onPress={()=> setSelectedTab('summary')}
    >
      Summary
    </button>
  </hstack>
</vstack>

Layout and Spacing Challenges


Due to the smaller size of the app, we needed to adjust the way our various frontend components were laid out to make the UI/UX more concise. For example, our "Pregame" view needed to display multiple widgets — countdown timer, polling interface, probable pitcher images, and game metadata — within a desktop smaller viewport. Without media queries or responsive grids, we used percentage-based stacks and explicit gaps to balance readability against component density.


Pagination Without Scrolling


Devvit posts do not support scrolling, so we implemented both horizontal and vertical pagination for pages that contained a lot of information. Additionally, we also used components such as buttons for to sort out information that was separate for each team (such as team roster) to enhance readability.


Throughout the process, the built-in Devvit AI bot was extremely valuable for quickly confirming which props and components were supported (e.g. cornerRadius on <image>, size tokens on <button>), reducing trial-and-error and ensuring our workarounds aligned with the platform's evolving API.

Challenges

Designing Within Reddit's Ecosystem

One of the most unique challenges was designing for Reddit's Devvit platform, where conventional web tools like HTML, CSS, and even scrolling don't exist. We had to get creative with how we presented complex, dynamic data like live scores and game stats within a limited UI toolkit, all while making it feel native to Reddit's ecosystem.


Balancing Technical Scope with Community Simplicity

Our project had to bridge two very different audiences: technical moderators who install the app and casual fans just looking to check scores. That meant building a powerful backend system with polling, reminders, and live updates while keeping the user interface incredibly simple, familiar, and fun to interact with.

Final Reflection

This project with Reddit and Devvit has been one of the most technically demanding, creatively rewarding experiences we've had.


In just a few weeks, our team designed and shipped a live MLB scoreboard system that blends AI, data pipelines, and social interaction right inside Reddit. We explored a brand-new platform, faced edge-case errors from third-party APIs, reimagined frontend layouts without scrolling, and navigated complex Redis state management... all while trying to keep things fun for baseball fans.


But the best part? Will be seeing the scoreboard actually used in real subreddits. Watching fans vote on predictions, ask for reminders, and return during games reminded us that this wasn't just an app—it was an experience people wanted to engage with.


Thanks for the opportunity, Reddit.