I live in Braves territory. I'm not opening the MLB app. Here's a one-liner.
I live on the Georgia coast, about four hours from Truist Park and roughly eight hundred miles from Busch Stadium. Every time the Cardinals play the Braves — which happens every season as NL opponents, though not as division rivals — I'm surrounded by people who think the tomahawk chop is a legitimate cultural contribution.
I don't always have the game on. Life doesn't pause for nine innings, and sometimes you're in a terminal window when the Cardinals are threatening in the seventh and you just need to know. The phone is in the other room. The MLB app wants me to log in, accept notifications, and sit through a gambling ad before it shows me the count. ESPN's website loads 47 trackers before the first byte of baseball content.
So I built a one-liner. A hundred and fifty characters of shell script, wrapped around MLB's free public API, that prints the Cardinals score in monospace text and gets out of the way. No login. No ads. No "engagement." Just baseball.
Here's how it works, how I built it, and how to make it your own — even if your mistake in life was rooting for the Cubs.
The One-Liner
curl -s "https://statsapi.mlb.com/api/v1/schedule?sportId=1&teamId=138&date=$(date +%Y-%m-%d)&hydrate=linescore" | python3 -c "
import json,sys
d=json.load(sys.stdin)
for date in d.get('dates',[]):
for g in date.get('games',[]):
away=g['teams']['away']['team']['name']+' ('+str(g['teams']['away'].get('score','-'))+')'
home=g['teams']['home']['team']['name']+' ('+str(g['teams']['home'].get('score','-'))+')'
state=g['status'].get('detailedState','Unknown')
print(f\"{away} @ {home} [{state}]\")
if g.get('linescore'):
print(' '+''.join([str(i.get('runs','-')).rjust(3) for i in g['linescore'].get('innings',[])]))
"
Team ID 138 is the Cardinals. Swap that number and it works for any team in baseball — even the Cubs (112), though I'm not sure why you'd want to. The whole thing is two piped commands: curl fetches raw JSON from MLB's Stats API, and a tiny Python script parses roughly 40 kilobytes of JSON into the 80 characters a human actually wants to read.
I typed a version of this into a terminal about three months ago and have used it almost every day since. Ozzie — that's my front-door AI agent, named after Ozzie Smith — can run it for me now. I message him /score on Telegram and he fires the one-liner and replies with the result. The whole round trip takes about 600 milliseconds, most of which is MLB's API thinking about its response.
What You Get
On a game day, after the final out:
St. Louis Cardinals (4) @ Atlanta Braves (2) [Final]
0 1 0 0 1 0 2 0 0
The inning-by-inning line at the bottom tells the story in a way the final score never does. Three runs in the seventh? You see the breakout. A pitchers' duel through six where both starters are dealing? The goose eggs speak for themselves. A blown save in the ninth that turns a 3-1 lead into a 4-3 loss? That single digit in the final column hits different when you see the string of zeros that preceded it.
If the game is in progress, you get [In Progress] with the current inning. If it hasn't started yet, [Scheduled] with the first pitch time. If it's been postponed — rain, snow, a freak infestation of cicadas in Cincinnati — you get [Postponed].
The script handles edge cases without drama. Doubleheaders print both games. Off days and the All-Star break print nothing at all, which is correct — there's no score to report. Spring training, postseason, it all works the same way. The API doesn't care whether it's March or October; a game is a game.
How the API Works
MLB's Stats API is the best-kept secret in sports data. It's free, public, and requires no authentication of any kind. No API key, no OAuth flow, no "apply for developer access" form that sits in a queue for six weeks before a product manager ghosts you. Just a URL that returns JSON.
The league built it to power MLB.com, the At Bat app, and every digital property that shows you baseball scores. They built it for themselves and left the door open. It's been publicly accessible for over a decade — through three commissioners, a pandemic, a lockout, and every other disruption that's hit the sport since 2015. There is no indication it's going anywhere.
The URL breaks down into four simple parts:
sportId=1— Major League Baseball. There are IDs for the minors too (11 for Triple-A, 12 for Double-A, and so on), but 1 is the show.teamId=138— St. Louis Cardinals. This is the only part you need to change. Want Braves scores? 144. Dodgers? 119. Yankees? 147.date=$(date +%Y-%m-%d)— today's date in YYYY-MM-DD format. The shell expands this beforecurlruns, so the API always sees the current date. Change it to any date string and you've got a time machine for baseball scores.hydrate=linescore— the magic word. Without it, you get a schedule with team names and game status but no actual runs. Withhydrate=linescore, the API embeds the full inning-by-inning linescore inside each game object. There are other hydrate values too:hydrate=probablePitchergets you the starters,hydrate=decisionsgets you the win/loss/save.
The API returns a truly massive JSON blob for even a single game — hundreds of lines covering everything from the venue's elevation to the umpire's middle name. The Python script is a funnel. It finds exactly the fields a fan checking a score cares about and discards the rest. teams.away.team.name, teams.away.score, status.detailedState, and linescore.innings[].runs. That's it.
What Else the API Exposes
Once you start poking around https://statsapi.mlb.com/api/v1/, you realize the depth of what's available:
- Live game feeds with pitch-by-pitch data — velocity, pitch type, spin rate, location. This is the same feed that powers Gameday. You can reconstruct an entire at-bat from the JSON: every pitch, every swing, every called strike.
- Player stats going back to the 19th century. Career totals, season splits, platoon splits, advanced metrics. Want to know a player's OPS against left-handed pitching in night games on the road since the All-Star break? It's in there somewhere.
- Standings by division, league, and wild card. Updated in real time during the season.
- Rosters with full biographical data, handedness, position, and 40-man status.
- Transactions — trades, injuries, call-ups, options, designations for assignment.
- Historical box scores for games played before the designated hitter existed, before night games existed, before the American League existed.
The entire API is self-documenting through hypermedia — every response includes URLs to related resources. Browse it in a browser and follow the links. You'll lose an afternoon if you're not careful.
How I Actually Built It
The one-liner didn't spring fully formed from the forehead of a baseball deity. I built it iteratively, in the way most useful terminal tools get built: I wanted something, I hacked together the simplest version that worked, and I've tweaked it every few weeks since.
Version 1 was just curl with the bare URL and no Python. Printed 40KB of raw JSON to the terminal. Technically contained the score, but so does a haystack technically contain a needle. I squinted at it for about fifteen seconds, found "home":{"score":4, and decided this was not a viable long-term interface.
Version 2 piped through python3 -c with a minimal parser that printed team names and scores. One line of output. No inning-by-inning. No game state. I used this for about a month. It answered "did they win or lose" but not "how did it happen."
Version 3 added the linescore and game state. This is the current version, more or less. Three lines of output. The whole story of the game in 80 characters of monospace.
Version 4 is the Ozzie integration. I wired the one-liner into a slash command on my Telegram bot. Now I type /score while I'm on the couch and Ozzie fires it and replies with the result. Jim — my code-generation agent, named after Jim Edmonds, the guy who made diving catches look routine — set up the integration. Albert — my heavyweight reasoning agent, named after, well, me — wrote most of the blog post you're reading. The fleet does the work; I just type /score and watch the Cardinals lose to the Braves by two runs in extras.
That last part isn't a hypothetical. That happened. Twice last week.
Variations
The one-liner is a starting point. Here's what you can build on top of it, from simple to ambitious.
Shell Alias
Put this in your .bashrc or .zshrc:
alias score='curl -s "https://statsapi.mlb.com/api/v1/schedule?sportId=1&teamId=138&date=$(date +%Y-%m-%d)&hydrate=linescore" | python3 -c "import json,sys; d=json.load(sys.stdin); [print(f\"{g[\"teams\"][\"away\"][\"team\"][\"name\"]} ({g[\"teams\"][\"away\"].get(\"score\",\"-\")}) @ {g[\"teams\"][\"home\"][\"team\"][\"name\"]} ({g[\"teams\"][\"home\"].get(\"score\",\"-\")}) [{g[\"status\"].get(\"detailedState\",\"Unknown\")}]\") or (g.get(\"linescore\") and print(\" \"+\"\".join([str(i.get(\"runs\",\"-\")).rjust(3) for i in g[\"linescore\"].get(\"innings\",[])]))) for date in d.get(\"dates\",[]) for g in date.get(\"games\",[])]"'
Now score works from any terminal. The alias is compressed into one Python line — harder to read, easier to copy-paste. Trade-offs.
Shell Script
For something readable and parameterized:
#!/bin/bash
# ~/bin/cards-score — check today's Cardinals score
TEAM_ID="${1:-138}"
DATE="${2:-$(date +%Y-%m-%d)}"
curl -s "https://statsapi.mlb.com/api/v1/schedule?sportId=1&teamId=${TEAM_ID}&date=${DATE}&hydrate=linescore" | python3 << 'PYEOF'
import json, sys
d = json.load(sys.stdin)
for date in d.get('dates', []):
for g in date.get('games', []):
away = f"{g['teams']['away']['team']['name']} ({g['teams']['away'].get('score', '-')})"
home = f"{g['teams']['home']['team']['name']} ({g['teams']['home'].get('score', '-')})"
state = g['status'].get('detailedState', 'Unknown')
print(f"{away} @ {home} [{state}]")
if g.get('linescore'):
inn = ''.join(str(i.get('runs', '-')).rjust(3) for i in g['linescore'].get('innings', []))
print(f" {inn}")
PYEOF
Now cards-score checks the Cardinals. cards-score 144 checks the Braves. cards-score 138 2026-05-15 checks a specific date. The Python is readable and debuggable. Future you will appreciate this version when something breaks at 11 PM and you're trying to remember how the JSON is structured.
Watch Mode
During a game, use watch to refresh every 30 seconds:
watch -n 30 cards-score
Your terminal becomes a live scoreboard. Ctrl-C when the game ends. I've done this exactly twice. Both times the Cardinals were losing by the seventh and I spent the last hour of the game watching the score not change while telling myself I should be doing something productive.
Season Tracker
Log every game to a file:
echo "$(date +%Y-%m-%d),$(cards-score | head -1)" >> ~/cards-2026.csv
At season's end you have a complete record. Parse it with Python, plot it, and produce a summary that no commercial app can give you: your team's season, through your lens, with your commentary.
Multi-Team Dashboard
Run the script for three or four teams in parallel:
echo "138 144 112 119" | xargs -n1 -P4 cards-score
Parallel execution means all four teams resolve in the time of the slowest single request. Useful for tracking a division race when the Cardinals, Brewers, and Cubs are all within three games of each other in September and you need to know everything, immediately, all the time, and your family is starting to ask questions about your emotional state.
Team ID Reference
For when you want to check on your friend's team or confirm that the Cubs lost:
| Team | ID | |------|----| | St. Louis Cardinals | 138 | | Atlanta Braves | 144 | | Chicago Cubs | 112 | | Milwaukee Brewers | 158 | | Cincinnati Reds | 113 | | Pittsburgh Pirates | 134 | | Los Angeles Dodgers | 119 | | San Francisco Giants | 137 | | San Diego Padres | 135 | | New York Mets | 121 | | New York Yankees | 147 | | Boston Red Sox | 111 | | Houston Astros | 117 | | Philadelphia Phillies | 143 |
The full list of 30 teams is available in any implementation of the script worth its salt. These IDs haven't changed in years and likely won't — they're baked into every system that consumes the Stats API.
Minor leagues use the same API with different sportId values: 11 for Triple-A (Memphis Redbirds!), 12 for Double-A, 13 for High-A, 14 for Single-A. The structure is identical. Team IDs are different — search https://statsapi.mlb.com/api/v1/teams?sportId=11 to find your local affiliate.
Beyond Baseball: Other Free Sports APIs
MLB isn't the only league with a public API, though it's the gold standard by a wide margin.
NHL has a nearly identical API at https://statsapi.web.nhl.com/api/v1/. The structure is so close to MLB's that the same parsing patterns work with minor field name changes. The Blues are team ID 19, which I know because I check their scores too, and also because the 2019 Stanley Cup run is still paying emotional dividends.
NBA is harder. The official API requires authentication. Community-maintained wrappers exist, and reverse-engineered endpoints work until they don't. This is the weak spot in the free-sports-API landscape.
NFL is the worst. The league tightly controls its data, and there's no public API comparable to MLB's. Community projects scrape publicly available data, but they break when sites change HTML structure. If you want NFL scores from the terminal, you're probably going to need a paid API tier and a lot more patience.
Soccer has several options. football-data.org offers a free tier covering the Premier League, Bundesliga, Serie A, and La Liga. It's well-documented and stable. Not the same as baseball, but on a Premier League Saturday morning with coffee, it scratches the same itch.
College sports are a patchwork. The NCAA doesn't offer a unified public API. Individual conferences have endpoints that work intermittently. This is the frontier where most "build your own scoreboard" projects eventually give up and open ESPN.com like everyone else.
The lesson: baseball fans are uniquely fortunate. MLB's decision to build a comprehensive, well-documented, open API — no keys, no quotas, no authentication — is unusual among major sports leagues and remarkably generous. It's the closest thing to a public utility in sports data. I don't know which executive made that call, but they deserve a plaque in Cooperstown.
Why the Terminal?
The obvious question: there are at least a dozen ways to check a baseball score in 2026. Why build a terminal command?
It's Already Open
I spend most of my day in a terminal — managing the homelab, checking on agents, writing, tinkering. Switching contexts to a phone takes 15-30 seconds and breaks my flow. I'll pick up the phone to check the score and somehow end up scrolling Twitter for five minutes before remembering why I unlocked the screen in the first place.
Typing score takes two seconds and I'm already back to what I was doing. The terminal command is a dead end — it answers exactly the question I asked, nothing more. No algorithmic feed. No "you might also like." No notification asking me to rate the experience.
It Composes
Once the score is text in a terminal, it can go anywhere. I've wired it into:
- Ozzie's Telegram bot. I message
/scoreand get the result back in the chat thread alongside my weather briefing and crypto report. One interface, everything I need. - A season log. Every Cardinals game gets appended to a CSV that lives on my Nextcloud server. At the end of the season I'll have 162 rows of raw data to analyze however I want.
- A notification trigger. When the score changes in a close game, a watchdog script fires a Telegram alert. I've configured this exactly once, during a September series against the Brewers with the division on the line, and then disabled it because it turns out I don't actually need my phone buzzing every time a runner reaches second.
- The daily briefing. Jim's evening cron job includes the Cardinals score alongside the crypto report and energy summary. Five PM, every day: how the portfolio did, how the battery held up, and whether the Cardinals blew a lead in the eighth.
No Ads, No Accounts, No "Engagement"
The MLB app wants me to log in and watch a gambling ad before showing me the pitch count. Google wants to know who I am. ESPN wants to track me across 47 domains before rendering the first run of the box score.
The Stats API doesn't know I exist. It doesn't set cookies. It doesn't serve advertisements. It doesn't log my query as a "user session" to report in a quarterly earnings call. The only data that changes hands is JSON, and the only thing in that JSON is baseball.
I'm not a DAU, an MAU, or a retention metric. I'm just someone who sent an HTTP request and got back the answer to one question. There's something genuinely satisfying about that, in an era where every interaction with technology is mediated by a platform that wants something from me — attention, data, money, a subscription, all of the above. The Stats API just wants to tell me the score. That's the whole deal.
It Outlasts Everything
APIs change. Apps get discontinued. Services get acquired and shut down. But curl has been stable since 1997 and Python since 1991. The one-liner I wrote in March 2026 will work in March 2036. The MLB app will have been redesigned six times, renamed three times, and will almost certainly require a premium tier for "real-time inning-by-inning updates" by then.
This is the quiet argument for terminal tools: they're boring technology, and boring technology is the most reliable kind. Nobody is disrupting curl. No venture-backed startup is pivoting python3 -c into an enterprise SaaS platform. The foundations don't move.
The Joy of Small Tools
This is a tiny thing. A hundred and fifty characters of shell script wrapped around a free public API. It didn't require a business model, a terms of service, or a data retention policy. It solves exactly one problem — "what's the Cardinals score?" — and solves it completely.
There's a philosophy here about building small tools instead of adopting big platforms. The terminal one-liner doesn't try to be a sports platform. It doesn't want to show me highlights, sell me tickets, predict outcomes, or recommend related content. It answers one question and gets out of the way. In an era where every piece of software aspires to be an ecosystem, there's deep satisfaction in a tool that just does the thing and stops.
The next time you find yourself opening an app to check one piece of information — a sports score, a stock price, the weather, whether a flight is delayed — ask yourself: is there an API for this? Can I curl it? The answer is yes more often than you'd think, and the result will be faster, cleaner, and more yours than anything that comes with a terms of service.
Also: the Braves lost again. I checked.
MLB Stats API: https://statsapi.mlb.com/api/v1/ — no key required, no rate limits for personal use. The API powers every official MLB digital property. It's been open to the public for over a decade and shows no signs of closing. If you build something with it, don't hammer the servers at 10,000 requests per second and we'll all be fine.