Text every World Cup final score to a friends' Signal group

A small AI agent that texted the final score of every 2026 FIFA World Cup match to a Signal group chat of three friends, each one written up by "Claudia," a flirty robot soccer commentator. Task: A small AI agent that texted the final score of every 2026 FIFA World Cup match to a Signal group chat of three friends, each one written up by "Claudia," a flirty robot soccer commentator. I built it with Claude Code in one evening after a friend asked what an AI agent actually is. The full story is on Substack: I Built My Friend a Flirty Robot That Texts Him World Cup Scores. How it did It ran for the whole tournament, 2026-06-11 to 2026-07-19, and is now retired. The last message it sent, after the final: Spain just made Argentina sweat it out, boys—took extra time but La Roja sealed it 1-0 and sent Messi's crew home 🤖⚽ Sometimes the best things are worth the wait, if you know what I mean 💋 MetLife Stadium • East Rutherford, New Jersey • USA How it works Every 30 minutes, a macOS LaunchDaemon runs poll.py. Each run: Pulls ESPN's public World Cup scoreboard. No API key needed. Finds matches that have finished. Skips any match it has already sent (tracked in sent.json). Asks Claude Haiku to write the result up as Claudia. Sends it to the Signal group with signal-cli. Records the match as sent, but only after a confirmed successful send. A failed send just retries on the next poll. That's what makes it an agent rather than a chatbot: nobody is at the keyboard. It wakes up on its own, checks the world, decides whether anything is worth saying, and acts. The guardrail: let it flirt, never let it get the score wrong The score is the one thing the message has to get right. A creative AI is also the thing most likely to fumble it. So Claudia never gets the last word. Every write-up goes through a plain, non-AI check (score_ok() in poll.py) before it sends: Both final scores must appear in the text. At least one team name must appear. Only one is required, so Claudia can still say "USMNT" or "La Roja." For a penalty shootout, the text must also name the winner, mention penalties, and include both shootout scores. Otherwise "what a 1–1 tie!" would pass the score check while being flat wrong. If the write-up fails, or the API is down, or there's no API key, the bot sends the plain version instead. For a shootout it looks like this: Worst case, the message isn't funny. The score is never wrong. Lessons that cost real time ESPN's default scoreboard only shows today's matches, in UTC. A match that finishes late in the evening can drop off before the bot ever sees it as final. Always fetch yesterday and today and merge them. Knockout matches aren't draws. ESPN's status.type.name tells you how a match ended (STATUS_FULL_TIME, STATUS_FINAL_AET, STATUS_FINAL_PEN). Without checking it, a 1–1 penalty win reads as a tie. This one actually shipped: the group was told Paraguay–Germany ended 1–1, when Paraguay had won on penalties. Fixed the same day, along with the stricter shootout check above. Use the JVM build of signal-cli, not Homebrew's. The Homebrew build has a bug that silently breaks sending. The JVM build needs JDK 25 or newer. Don't hammer Signal after failed sends. A burst of retries trips a timed rate limit, and each retry extends it. The first guardrail was too strict. It demanded the exact team name ESPN uses, so a perfectly good "USMNT" write-up got thrown out. Fence the number, give the names some room. Running it yourself This was built for one Mac, one group chat and one tournament, so expect to adapt it. You'll need: macOS, Python 3 (standard library only), JDK 25+, the JVM build of signal-cli unpacked into signal-cli-dist/, a Signal account, and optionally an Anthropic API key for Claudia. Copy config.example.json to config.json and fill in your Signal number, the group ID (from signal-cli listGroups), and your API key. Leave the key blank to send plain score lines only. Link the Mac as a secondary Signal device with ./link-signal.sh. It opens a QR code to scan from your phone. Test it without sending anything: set "dry_run": true in config.json and run python3 poll.py. In worldcup-scores-bot.plist, replace YOUR_USERNAME and the folder path with your own. Then install it: For a different competition, change the ESPN league slug (fifa.world) in poll.py and delete sent.json. Files; tools: Claude Code, Claude. Human role: The original post does not say how a person stayed involved.; limitations: Prerequisites: macOS, Python 3, JDK 25+, JVM build of signal-cli, Signal account, and optional Anthropic API key. Constraints: ESPN scoreboard requires merging yesterday/today data; knockout matches need status.type.name checks to distinguish penalties from draws. Integration uncertainty: signal-cli Homebrew build has bugs; use JVM build. Human approvals: Required for initial configuration and dry-run validation..

Tools

Claude Code, Claude

Published on Agent Work Atlas:

Overview

A small AI agent that texted the final score of every 2026 FIFA World Cup match to a Signal group chat of three friends, each one written up by "Claudia," a flirty robot soccer commentator.

Workflow

A small AI agent that texted the final score of every 2026 FIFA World Cup match to a Signal group chat of three friends, each one written up by "Claudia," a flirty robot soccer commentator. I built it with Claude Code in one evening after a friend asked what an AI agent actually is. The full story is on Substack: I Built My Friend a Flirty Robot That Texts Him World Cup Scores. How it did It ran for the whole tournament, 2026-06-11 to 2026-07-19, and is now retired. The last message it sent, after the final: Spain just made Argentina sweat it out, boys—took extra time but La Roja sealed it 1-0 and sent Messi's crew home 🤖⚽ Sometimes the best things are worth the wait, if you know what I mean 💋 MetLife Stadium • East Rutherford, New Jersey • USA How it works Every 30 minutes, a macOS LaunchDaemon runs poll.py. Each run: Pulls ESPN's public World Cup scoreboard. No API key needed. Finds matches that have finished. Skips any match it has already sent (tracked in sent.json). Asks Claude Haiku to write the result up as Claudia. Sends it to the Signal group with signal-cli. Records the match as sent, but only after a confirmed successful send. A failed send just retries on the next poll. That's what makes it an agent rather than a chatbot: nobody is at the keyboard. It wakes up on its own, checks the world, decides whether anything is worth saying, and acts. The guardrail: let it flirt, never let it get the score wrong The score is the one thing the message has to get right. A creative AI is also the thing most likely to fumble it. So Claudia never gets the last word. Every write-up goes through a plain, non-AI check (score_ok() in poll.py) before it sends: Both final scores must appear in the text. At least one team name must appear. Only one is required, so Claudia can still say "USMNT" or "La Roja." For a penalty shootout, the text must also name the winner, mention penalties, and include both shootout scores. Otherwise "what a 1–1 tie!" would pass the score check while being flat wrong. If the write-up fails, or the API is down, or there's no API key, the bot sends the plain version instead. For a shootout it looks like this: Worst case, the message isn't funny. The score is never wrong. Lessons that cost real time ESPN's default scoreboard only shows today's matches, in UTC. A match that finishes late in the evening can drop off before the bot ever sees it as final. Always fetch yesterday and today and merge them. Knockout matches aren't draws. ESPN's status.type.name tells you how a match ended (STATUS_FULL_TIME, STATUS_FINAL_AET, STATUS_FINAL_PEN). Without checking it, a 1–1 penalty win reads as a tie. This one actually shipped: the group was told Paraguay–Germany ended 1–1, when Paraguay had won on penalties. Fixed the same day, along with the stricter shootout check above. Use the JVM build of signal-cli, not Homebrew's. The Homebrew build has a bug that silently breaks sending. The JVM build needs JDK 25 or newer. Don't hammer Signal after failed sends. A burst of retries trips a timed rate limit, and each retry extends it. The first guardrail was too strict. It demanded the exact team name ESPN uses, so a perfectly good "USMNT" write-up got thrown out. Fence the number, give the names some room. Running it yourself This was built for one Mac, one group chat and one tournament, so expect to adapt it. You'll need: macOS, Python 3 (standard library only), JDK 25+, the JVM build of signal-cli unpacked into signal-cli-dist/, a Signal account, and optionally an Anthropic API key for Claudia. Copy config.example.json to config.json and fill in your Signal number, the group ID (from signal-cli listGroups), and your API key. Leave the key blank to send plain score lines only. Link the Mac as a secondary Signal device with ./link-signal.sh. It opens a QR code to scan from your phone. Test it without sending anything: set "dry_run": true in config.json and run python3 poll.py. In worldcup-scores-bot.plist, replace YOUR_USERNAME and the folder path with your own. Then install it: For a different competition, change the ESPN league slug (fifa.world) in poll.py and delete sent.json. Files

Reported result

Outcome unknown. No measured results or success metrics are provided in the source. Proposed acceptance check: Verify that the bot correctly identifies a finished match, generates a message passing the score_ok() validation, and successfully sends it to the Signal group without duplicates.

Human role

The original post does not say how a person stayed involved.

Limitations

Prerequisites: macOS, Python 3, JDK 25+, JVM build of signal-cli, Signal account, and optional Anthropic API key. Constraints: ESPN scoreboard requires merging yesterday/today data; knockout matches need status.type.name checks to distinguish penalties from draws. Integration uncertainty: signal-cli Homebrew build has bugs; use JVM build. Human approvals: Required for initial configuration and dry-run validation.

Try it

I want to build a small AI agent that texts final scores of [replaceable: tournament name, e.g., 2026 FIFA World Cup] matches to a [replaceable: messaging platform, e.g., Signal] group chat of [replaceable: number] friends. Each message should be written by a persona named [replaceable: persona name, e.g., Claudia] with a [replaceable: tone, e.g., flirty robot soccer commentator] style. Use [replaceable: coding tool, e.g., Claude Code] to help me write the Python script. Steps: 1. Create a Python script (poll.py) that fetches scores from [replaceable: data source, e.g., ESPN public scoreboard]. 2. Implement logic to find finished matches and skip already sent ones using a local JSON file. 3. Use [replaceable: LLM API, e.g., Claude Haiku] to generate the message in the specified persona. 4. Add a strict validation function to ensure the score and team names are correct before sending. 5. Send the message using [replaceable: CLI tool, e.g., signal-cli]. 6. Set up a scheduler (e.g., macOS LaunchDaemon) to run this every 30 minutes. Deliverables: The Python script, configuration file, and scheduler plist. Quality check: Run a dry-run mode first to verify message generation and validation without sending. Confirm with me before enabling actual sending. Handle errors by retrying on the next poll. Ask clarifying questions if I haven't provided the specific API keys or group IDs.

Original source

Source reports are not independently verified. Publication date is the catalog publication date, not the original article date.

More cases · Everyday life