A Discord bot named after Harry Doyle, the sardonic, bourbon-fueled announcer from Major League (voiced by Bob Uecker). Harry delivers real MLB Statcast data with the same dry pessimism he used to narrate the Indians' improbable season.
| Command | Description |
|---|---|
/strikezone [first] [last] [year] |
Plots a pitcher's strike zone (colored by pitch type) for the given season |
/battedzone [first] [last] [year] |
Plots all pitches thrown to a batter in a season |
/spraychart [first] [last] [year] |
Plots where a batter hits the ball on a stadium spray chart |
/hotzones [first] [last] [year] |
Show a batter's performance across the strike zone as a 3x3 thermal grid |
/matchupzone [p_first] [p_last] [b_first] [b_last] [year] |
Zone plot of one pitcher vs one batter |
/stadium [team] |
Show a ballpark's name, location, and visual outline |
| Command | Description |
|---|---|
/stats [first] [last] [year] |
FanGraphs season stats — auto-detects pitcher or batter |
/career [first] [last] |
Career aggregate stats from FanGraphs |
/compare [p1] [p2] [year] |
Side-by-side FanGraphs stat comparison of two players |
/arsenal [first] [last] [year] |
Pitcher's pitch mix — velocity, spin rate, and usage |
/exitvelo [first] [last] [year] |
Batter's exit velocity and barrel stats |
/percentile [first] [last] [year] |
Statcast percentile ranks (Higher = better vs. league average) |
/hotcold [first] [last] [days] |
Rolling Statcast stats for the last N days (7, 14, or 30) |
/leaderboard [stat] [year] |
Top 10 players for any FanGraphs stat (e.g. ERA, WAR, HR) |
/junkstats |
Get an absurdly specific and weird baseball fact powered by Gemini |
| Command | Description |
|---|---|
/matchup [p_first] [p_last] [b_first] [b_last] [year] |
Head-to-head Statcast text stats: AVG, H, PA, K |
| Command | Description |
|---|---|
/standings [year] |
Show MLB division standings for a given season |
/schedule [team] [year] |
Show a team's recent results and upcoming games |
/livescore [team] |
Get today's live score for a specific team |
/nextgame [team] |
Show a team's next scheduled game and probable pitchers |
/roster [team] |
Show a team's current active 26-man roster |
/injury [team] |
Show a team's current IL (Injured List) stints |
/transactions [team] |
Show recent roster moves for a team |
# 1. Install dependencies (creates .venv automatically)
cd /path/to/Harry-bot
uv sync
# 2. Set your API tokens
cp .env.example .env
# Edit .env and paste your DISCORD_TOKEN and GEMINI_API_KEY
# 3. Run the bot
export DISCORD_TOKEN=your_token_here
export OWNER_ID=your_discord_id_here # Optional: locks DMs to you
uv run python main.py
# Or use the installed script entrypoint:
uv run harryuv run pytest- discord.py — async Discord gateway + slash commands via
app_commands - pybaseball — Statcast / Baseball Savant data +
plotting.plot_strike_zone() asyncio.to_thread()— all blocking pybaseball/matplotlib calls run in a thread pool so the event loop stays responsiveinteraction.response.defer(thinking=True)— prevents Discord's 3-second interaction timeoutplt.close(fig)— called after every plot save to prevent matplotlib memory leaks- Gemini 3.1 Flash Preview — powers
/junkstatsvia thegoogle-genaiSDK for absurdly specific baseball facts
Harry is optimized to run in resource-constrained environments (e.g., a 256MB Fly.io container).
- PyArrow Zero-Copy Engine: Monkey-patched Pandas to use the PyArrow C++ engine for all CSV/JSON reads. This reduced peak memory usage during data fetching from ~250MB to <5MB.
- Global Sentinel Lazy Loading: Matplotlib and Pybaseball modules are deferred using a sentinel pattern in
statcast.py. They are only initialized on the first command execution, keeping the idle RAM floor as low as possible. - Headless Plotting: The
Aggbackend is forced globally inmain.pyto prevent loading heavy GUI frameworks (Tkinter/Qt). - Lean Discord Client: Internal message and member caching is disabled in
HarryBotto prevent memory bloat over time.
Errors are delivered in the voice of Harry Doyle. Sample:
"Juuust a bit outside... of what I can find. No results, pal."
In the Discord Developer Portal, enable:
botscopeapplications.commandsscope- Send Messages + Attach Files + Embed Links permissions
Harry is configured to deploy as a background worker on Fly.io using the included Dockerfile and fly.toml.
- Install the
flyctlCLI to set up the app. - Initialize the app without deploying:
fly launch --no-deploy
- Open the Fly.io Dashboard in your browser.
- Navigate to your new
harry-botapp -> Secrets. - Add your
DISCORD_TOKEN,GEMINI_API_KEY, andOWNER_IDto the Secrets UI. - Deploy the bot via the CLI or UI (if linked to GitHub):
fly deploy
This project is licensed under the Apache License 2.0. See the LICENSE file for details.