Paperboy
Click here for an AI generated podcast discussing Paperboy
Paperboy was a concept I had at the start of my coding journey. At the time, I had no clear idea what an AI agent was or what it could do. I needed something that was simple enough for me to understand, but clever enough to use AI in a useful way.
When I was coding back in the 90s, RSS was pretty cool. If you had an RSS feed you were somebody and it never ceased to amaze me when new content dropped every day or week, bepending on the site. Wondering if this was still a thing, I did a little googling and found that it was not only a thing, it was stronger than ever, but how to re invent the wheel?
Paperboy was born in a flash of inspiration. At 6am everyday he would jump on his bike, scour the feeds I give him, return to Claude with the days findings for him to appraise and summarise, using a strict prompt select the best 10 of the day and email them to me.
At the time I had no idea if this was possible, no idea that I was creating an agent and no idea that it would become so much fun to make.
It took a few tries to get him up and running and I had my first taste of 'vibe coding' with Gemini and Claude, but together we built him, refined him and installed him on a headless raspberry pi, and true to his code, he has worked every day delivering the feeds I ask him to check and the topics he needs to look for.
He uses cron as his alarm clock, its really simple to set up but taught me yet another trick I can do in Linux, a simple amended file and he wakes, runs and shuts down without any input from me.
We are on version 1.04 the first versions were simpler, had less feeds to check and had terrible amnesia. He would forget yesterdays headlines and bring the same ones over and over if they fitted the criteria so I had to add an appending .txt file that he now checks against his feeds before sending them to Claude.
A very simple project, but very valuable. I have learned so much from him, not so much code, but I can read and see whats going on, but about agents, AI, tokens, Linux and how to build more robust systems, like using .env to hide keys and email details!
Code is below
# Paperboy - Version 1.04
import feedparser
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from anthropic import Anthropic
from dotenv import load_dotenv
import os
from topics import TOPICS, RSS_FEEDS
load_dotenv(os.path.expanduser('~/pythonprojects/paperboy/.env'))
client = Anthropic(api_key=os.getenv('ANTHROPIC_API_KEY'))
#added by Lumi
def load_seen_urls():
"""Reads the history file and returns a set of URLs."""
history_file = os.path.expanduser('~/pythonprojects/paperboy/seen_urls.txt')
if not os.path.exists(history_file):
return set()
with open(history_file, 'r') as f:
return set(line.strip() for line in f)
def save_new_url(url):
"""Appends a newly processed URL to the history file."""
history_file = os.path.expanduser('~/pythonprojects/paperboy/seen_urls.txt')
with open(history_file, 'a') as f:
f.write(url + '\n')
#end add by Lumi
def fetch_rss_articles():
articles = []
seen = load_seen_urls() #NEW Loads the list
for feed_url in RSS_FEEDS:
feed = feedparser.parse(feed_url)
for entry in feed.entries[:10]:
if entry.link not in seen:
articles.append({
'title': entry.title,
'url': entry.link,
'summary': entry.get('summary', '')[:500]
})
save_new_url(entry.link)
return articles
def filter_articles_with_claude(articles):
topics_str = "\n".join(TOPICS)
articles_str = ""
for i, article in enumerate(articles):
articles_str += f"{i+1}. {article['title']}\n{article['url']}\n{article['summary'][:200]}\n\n"
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
messages=[{
"role": "user",
"content": f"""You are Paperboy, an intelligent news curator.
My interests are:
{topics_str}
Here are today's articles:
{articles_str}
Select the most relevant articles for my interests. For each one you select return:
- Title
- URL
- One sentence explaining why I should read it
Be ruthless. Quality over quantity. Ignore clickbait, merino wool and instax cameras.
Select the top 10 most relevant articles. If there aren't 10 essential ones, fill the remaining slots with the most interesting 'nice-to-know' tech updates so I have a full briefing.."""
}]
)
return response.content[0].text
def send_email(content):
sender = os.getenv('EMAIL_SENDER')
password = os.getenv('EMAIL_PASSWORD')
recipient = os.getenv('EMAIL_RECIPIENT')
msg = MIMEMultipart('alternative')
msg['Subject'] = '📰 Paperboy - Your Morning Briefing'
msg['From'] = sender
msg['To'] = recipient
body = MIMEText(content, 'plain')
msg.attach(body)
with smtplib.SMTP_SSL('smtp.gmail.com', 465) as server:
server.login(sender, password)
server.sendmail(sender, recipient, msg.as_string())
print("Paperboy delivered!")
def run_paperboy():
print("Paperboy on his bike...")
articles = fetch_rss_articles()
print(f"Found {len(articles)} articles, asking Claude to filter...")
filtered = filter_articles_with_claude(articles)
send_email(filtered)
run_paperboy()