Building My Personal AI Assistant with Clawdbot
A self-hosted Telegram bot running on a €4/month VPS that manages my calendar, emails, GitHub activity, and suggests blog topics based on my Twitter likes
I got tired of opening 6 different apps just to figure out what I need to do today. So I built a Telegram bot that knows all of it and responds in seconds.
It's not perfect. Sometimes it misses emails or the calendar sync breaks. But when I send it a voice message at 7am asking "what's my day look like?", getting back my weather, meetings, and important emails in one response beats checking Gmail, Google Calendar, and Weather.com separately.
What it actually is
Clawdbot is a Python application running on a Hetzner VPS in Germany. Costs about €4/month. The bot uses the Telegram Bot API to receive messages from me, processes them with Claude (via Anthropic's API), and responds back.
The real work happens in the integrations. It connects to:
- Google Calendar across 3 accounts (personal, work, side projects)
- Gmail for email summaries
- GitHub with OAuth so it can check my repos
- OpenAI's Whisper for voice transcription
- OpenWeatherMap for weather data
Everything runs on that one cheap VPS. Postgres database stores conversation history so the bot remembers context from previous chats. Been running for about a month now, using maybe $10-15/month in Claude API costs.
The morning briefing
This is the part I actually use every day. Around 7am I ask "what's today?" and get back something like:
Weather: 52°F, cloudy in Brooklyn
Rain chance: 40% after 2pm
Calendar:
• 10am - Team standup (Work Calendar)
• 2pm - Client demo (Work Calendar)
• 6pm - Dinner with Sarah (Personal)
Recent emails (5 unread):
• GitHub: PR #234 merged in portfolio-v2
• Newsletter: JavaScript Weekly #623
• Work: Q4 planning doc ready for reviewHere's roughly how that works:
# Example: Daily briefing function structure
async def generate_daily_briefing(user_id: str) -> str:
weather = await get_weather(user_location)
events = await get_calendar_events(user_id, days=1)
emails = await get_unread_emails(user_id, limit=5)
prompt = f"""Generate a morning briefing:
Weather: {weather}
Calendar: {events}
Emails: {emails}
"""
return await claude.complete(prompt)Nothing fancy. It just fetches data from APIs and asks Claude to format it nicely. The real win is having it all in one place without opening Chrome.
Voice messages
This was easier than I expected. Telegram makes it trivial to download voice messages, then I pipe them through Whisper:
# Telegram handler for voice messages
@bot.on_message(filters.voice)
async def handle_voice(client, message):
# Download and transcribe with Whisper
audio_path = await message.download()
transcript = await whisper.transcribe(audio_path)
# Process as regular text message
response = await process_message(message.from_user.id, transcript)
await message.reply(response)I use this way more than I thought I would. Typing on mobile is annoying, so I just hit record and say "check if I have any meetings in the next 2 hours" while making coffee.
Blog topic suggestions (the newest part)
This is experimental but interesting. The bot can now pull signals from places I spend time online and suggest things I might want to write about.
Right now it fetches:
- My Twitter/X likes and bookmarks (last 50 of each)
- GitHub repos I've starred recently
- Issues and PRs I've commented on
Then it runs all that through Claude with a prompt asking "what topics is this person clearly interested in based on their activity?"
async def suggest_blog_topics(user_id: str) -> list[str]:
# Fetch signals from various sources
twitter_likes = await fetch_twitter_likes(limit=50)
twitter_bookmarks = await fetch_twitter_bookmarks(limit=50)
github_stars = await fetch_github_stars(limit=30)
github_activity = await fetch_recent_github_activity()
prompt = f"""Based on this user's recent activity, suggest 5 blog post topics they might want to write about:
Twitter Likes/Bookmarks: {twitter_likes + twitter_bookmarks}
GitHub Stars: {github_stars}
Recent GitHub Activity: {github_activity}
Focus on topics that intersect with their demonstrated interests.
"""
return await claude.complete(prompt)Yesterday it suggested I write about "Cloudflare Workers optimization patterns" because I'd liked 3 tweets about edge computing and starred a D1 database library. Actually a decent idea.
The suggestions aren't always perfect. Sometimes it picks up on random tangents or misreads sarcastic tweets I liked. But it's better than staring at a blank page wondering what to write about.
Things that break
The Google Calendar OAuth token expires sometimes and I have to manually refresh it. Haven't automated that yet.
Voice transcription works great for English but mangles anything in another language. Whisper just gives up and returns gibberish.
The bot occasionally times out on complex requests if it needs to hit multiple APIs. I should probably add better timeout handling.
And sometimes the daily briefing fires at 7am when I'm still asleep and I wake up to 3 follow-up messages it sent itself trying to be helpful. Working on making that less annoying.
The infrastructure is boring (in a good way)
I wanted something I could forget about, so I went simple:
- Hetzner VPS: €4/month for a VM that's way more powerful than I need
- Postgres: Running locally on the VPS, no managed database
- Python 3.11: FastAPI + python-telegram-bot library
- systemd: Keeps the bot running, restarts if it crashes
- nginx: Reverse proxy for the occasional webhook endpoint
No Docker, no Kubernetes, no fancy deployment pipeline. Just git pull and systemctl restart clawdbot when I make changes.
Logs go to journalctl. Backups are a cron job that dumps Postgres to a file and uploads it to Backblaze B2 once a day. Takes about 20 seconds to deploy changes.
What I'd do differently
If I started over today I'd probably use Cloudflare Workers instead of a VPS. The OAuth refresh token problem would be easier to solve with Workers + D1 for storage. And I wouldn't have to think about server maintenance.
I'd also add rate limiting earlier. Right now if I spam the bot with questions it just keeps hitting Claude's API until I run out of credits. Found that out the hard way testing voice messages.
The conversation history is just a flat list of messages in Postgres. Should've designed it with threads or topics so the bot doesn't get confused when I switch contexts. Sometimes it thinks I'm still talking about my calendar when I've moved on to asking about GitHub.
What's actually useful
Honestly? The morning briefing and voice messages. Everything else is cool but I don't use it daily.
I thought I'd be asking it coding questions all the time but I still just use Claude or Cursor for that. The GitHub integration is neat for checking PR status but not essential.
The blog topic suggestion feature is interesting but I've only used it twice. Might be more useful if it ran automatically once a week and just sent me ideas without me asking.
What I didn't expect: I use it as a task reminder system now. I tell it "remind me to call mom this weekend" and it... actually reminds me. The conversation history means it remembers things I mentioned days ago, which is surprisingly valuable.
If you want to build something similar
The code isn't open source (yet—it's too messy) but the stack is straightforward:
- Pick a cheap VPS (Hetzner, DigitalOcean, Linode)
- Set up a Telegram bot with BotFather
- Use python-telegram-bot for the bot framework
- Add Claude API for LLM responses
- Integrate whatever services you actually use
Start simple. My first version just echoed messages back. Then I added Claude. Then calendar. Then email. Each integration took an afternoon.
The hardest part wasn't the code—it was getting all the OAuth flows working for Google services. That took 2 days and I still don't fully understand Google's OAuth consent screen requirements.
Total cost: about €20/month (VPS + Claude API). Lines of Python: ~2,400 (probably 800 of that is OAuth boilerplate). Time running: 31 days without a crash.
It's not changing my life but it's making mornings slightly less annoying. That's good enough.