Mohak Garg
Back to projects
Personal Project
2026

Coffee Chat Bot: Personalized LinkedIn Outreach on Autopilot

Python + FastAPI bot that watches Gmail for new LinkedIn connections, generates a personalized coffee-chat DM via Claude, and pings me on Telegram with a one-tap 'Send DM' link — turning a 5-minute-per-person manual workflow into a single approval tap.

Product Manager & Engineer — System Design, Prompt Engineering, API Integration, Serverless Architecture

Python
FastAPI
Claude AI (Anthropic)
Gmail API + Pub/Sub
Telegram Bot API
Neon Postgres
Vercel Fluid Compute
BeautifulSoup
5 min → 1 tap
Per-connection effort
5
Integrations
< 1 min
Real-time latency
32 tests, real-fixture parsing
Test coverage
The Problem

Building a network as a PM means writing genuinely personalized coffee-chat DMs to every new LinkedIn connection — but doing this manually for 10+ accepts per week takes 30+ minutes a day, and templated messages are obviously templated. Most people abandon the habit within a week.

The Solution

Built an end-to-end automation: Gmail Push Notifications fire when 'X accepted your invitation' emails arrive, a FastAPI webhook on Vercel parses the email's HTML to extract the sender's name and headline, Claude haiku generates a casual, em-dash-free message tailored to their role, and Telegram delivers two clean message bubbles — context with a 'Send DM' link, then the message body alone for one-tap copy. Also handles bulk import from a LinkedIn CSV export with the same pipeline triggered by a /today command.

Product Artifacts

GitHub
Source code
View

Case Study

Context
  • Networking is a known PM career multiplier — but the actual unit of work (writing a personalized coffee-chat DM to each new connection) doesn't scale. 10 accepts/week × 5 minutes/message = a habit that dies in week two.
  • Generic AI tools produce dead-giveaway messages: em dashes, 'I came across your profile', polished parallel clauses. Recipients can spot it instantly, which is worse than sending nothing.
  • LinkedIn's own messaging UX doesn't help — there's no native way to draft a personalized DM the moment someone accepts. By the time you remember, the connection is cold.
  • Target: a system that catches the connection moment, drafts something genuinely personal in seconds, and lets me approve and send with a single tap from my phone.
Decision
  • Used Gmail Push Notifications (Pub/Sub) instead of polling — when LinkedIn sends 'X accepted your invitation', the bot fires within ~1 min, not on a 15-min cron.
  • Parsed the sender's name from the email's From header and the headline from the rendered HTML body. The plain-text MIME part is just boilerplate; the data lives in the HTML.
  • Claude haiku 4.5 over a heavier model — speed and cost matter when running every single connection through the pipeline.
  • Designed the Claude prompt to explicitly ban em dashes and a cliché phrase list ('delve', 'leverage', 'I hope this finds you well'), and post-process strips any that slip through. The prompt is the product.
  • Two-bubble Telegram delivery: context + 'Send DM' link in one message, the AI-generated body alone in the next. Long-press the second bubble copies clean text with zero markdown gunk.
  • Single FastAPI entrypoint at api/index.py for Vercel's modern Python runtime. Three routes (Telegram webhook, Gmail push, weekly cron) sharing one ASGI app.
Execution
  • Gmail Watch is registered against the INBOX label and renews automatically every Sunday via a Vercel cron — Gmail's 7-day expiration is otherwise a silent failure mode.
  • The Pub/Sub push handler walks Gmail history from the supplied historyId, filters to senders containing 'linkedin.com', and routes each message through the parser.
  • Email parsing matches /comm/in/<vanity> URLs against tokens in the sender's name to distinguish the actual sender from the suggestion-card URLs LinkedIn embeds in every email.
  • When a vanity URL isn't in the email (some senders don't have one), the bot falls back to Google Custom Search by name, then to a manual-search Telegram link. Graceful degradation throughout.
  • Tests use 3 sanitized real Gmail email fixtures (saved via run_once/save_email_fixtures.py). Synthetic test bodies passed unit tests but failed against real emails, so the fix moved tests onto real payloads.
  • All 10 secrets pushed to Vercel via 'vercel env add'; .env stays gitignored locally. Neon Postgres tracks every connection through pending → notified → messaged/skipped states.
Outcome
  • Per-connection effort dropped from ~5 minutes (open profile, read, draft, polish, send) to one Telegram tap — long-press, copy, paste into LinkedIn DM.
  • Real-time response: from 'they accepted' to 'message ready on my phone' is under a minute when LinkedIn delivers the email promptly.
  • Messages reference the recipient's actual headline (e.g. '0->1 product work caught my eye' vs 'I came across your profile'), so reply rates stay human.
  • Bot ships even with one external dependency degraded — Custom Search 403s at the project level, but email-HTML extraction covers ~67% of senders directly, and the rest fall back to a manual-search link rather than silently dropping.
Learnings
  • Real-world payloads beat synthetic test fixtures every time. The original parser passed 4 unit tests and failed on every actual LinkedIn email — because synthetic 'happy path' fixtures didn't match how LinkedIn actually structures messages (HTML-only content, /comm/in/ URL prefix, name in From header).
  • AI prompts need belt-and-suspenders enforcement. Banning em dashes in the prompt isn't enough; the post-processor strips them anyway. Same for the cliché phrase list. Every regression-prone constraint deserves both an instruction and a runtime guard.
  • Modern Vercel Python expects a single ASGI entrypoint, not the legacy 'every api/*.py is a function' model. Migrating from BaseHTTPRequestHandler to FastAPI took 30 minutes once I read the actual error, but the error message ('No python entrypoint found') was indirect enough to chase a different theory first.
  • Two messages > one message for chat-based copy UX. Putting the AI text in its own bubble lets users long-press without grabbing markdown wrappers. A small detail, but the difference between 'this works' and 'this is annoying every time'.
  • Designing for yourself as the only user is the fastest possible feedback loop. I felt every rough edge — the generic-sounding first draft, the em-dash giveaway, the messy two-step copy — within minutes of using it on a real connection.