AI Agents
I built my own LinkedIn outreach tool (766 commits later)
· Updated · 6 min read
S
Get AI Agent RFP Template
The RFP I use for agent discovery — scope, eval, and pricing.
No spam, unsubscribe anytime. I only email teardowns.
· Updated · 6 min read
The RFP I use for agent discovery — scope, eval, and pricing.
No spam, unsubscribe anytime. I only email teardowns.
TGB Hunt is a self-hosted, open-source LinkedIn agent I built to find recruiters and hiring managers for roles I'm chasing — not a tool recruiters use to find candidates. It owns research, matching, and intro drafting; a human (me) owns the identity and every send. GPLv3, 766 commits, 6 releases, one macOS app (Python/Django/Playwright). The hard part wasn't the LLM; it's the judgment boundary between what the agent may say and what must stay human.
Every LinkedIn automation tool I tried dies the same death: it scales the sending and skips the reading. The result is a thousand identical "I came across your profile" messages and a burned account. So I built my own — 766 commits later, TGB Hunt does twenty minutes of research per prospect in three seconds, drafts an intro that proves the research happened, and then refuses to send anything on its own. The human gate isn't a compliance afterthought; it's the entire product thesis.
The system has four moving parts, each designed to keep the human in the loop:
Target discovery — describe the roles you're chasing (e.g., "VP Engineering at Series B fintech in NYC"), and the agent finds matching recruiters and hiring managers across LinkedIn, cross-referencing company size, recent funding, and hiring signals.
Personalized intros — for each target, the agent pulls public context (recent posts, company news, shared connections) and drafts an intro in your tone, referencing specifics — not boilerplate. The output is a draft you edit, not a message that fires.
Follow-up cadence — it tracks conversation state, remembers who last said what, and cues a sane nudge when a thread goes cold. No ghosting; the human decides when to send.
Human gate — nothing sends itself. Every message is a draft until you hit approve. The "😳 I sent 400 messages" scenario is impossible by design.
The whole thing runs on your laptop. Zero cloud bill. Your LinkedIn cookies never leave the machine.
Fail-closed by default. The worst thing an outreach experiment can do is spam. I switched to strong approval flows early (step 3 of every run), which killed the blast scenario before it could happen. The default action for any draft is "wait for approval."
Quality data > bigger model. Personalization came from a small enrichment step per prospect — company size, funding round, recent blog post, shared connection, tech stack signals from job posts. The LLM just crafted prose. Cheaper and more accurate than a bigger model guessing from thin air. A 7B local model with good context beats a 70B API with no context.
Versioned every prompt. 766 commits lived in changelogs. Each release was reproducible; if an intro style overfit yesterday's niche, you could roll it back in minutes. The prompt history is the product memory.
Self-hosted, one-click macOS app. Packaged with PyInstaller — no server, no cloud bill, your data stays on your machine. That constraint forced the architecture toward local-first tooling (Playwright for browser automation, SQLite for state). The .app installs via drag-and-drop, asks for LinkedIn session cookie once, then runs headless or headed.
The LLM is the writer, not the researcher. Here's the enrichment chain per prospect:
This enrichment takes ~3 seconds per prospect and costs ~$0.002 in API calls. The LLM then gets a structured JSON blob and writes the intro. Garbage in, garbage out — the enrichment is the product.
The drafting prompt went through six released versions. The structure that survived:
The failure modes that forced those rules, in order of discovery:
An outreach prompt is an eval problem, not a writing problem. Every variant got measured against reply rate before it earned the default slot.
When the target writes back. The draft must hand off to a human fast — the agent shouldn't "handle" real conversation. The handoff latency is the quality ceiling. I built a "reply notification → open draft → edit → send" flow that takes ~15 seconds.
When volume rises. Once you scale past permission, you're just reinventing spam. We kept it modest on purpose — the system earns trust by restraint. The daily cap is configurable; default 15 drafts/day.
When tone deviates. An LLM writes "too polished." Your own voice wins more read receipts than perfect grammar. The best intros felt like you wrote them after 20 minutes of research, not like a template. I added a "tone calibration" step: feed the model 5 of your past sent messages, it learns your cadence.
LinkedIn session fragility. Cookies expire, challenges trigger, UI changes break selectors. The Playwright layer has a "session health" monitor that pauses the run and alerts you before it burns the account.
The goal is reply rate on qualified personas — not messages blasted. I tracked every preview against its reply share and killed variants that under-indexed. The signal is downstream: did the human get a meeting?
| Metric | Target | Achieved |
|---|---|---|
| Draft approval rate | >80% | 87% |
| Reply rate (qualified) | >25% | 31% |
| Meeting booked / draft | >5% | 8% |
| Spam reports | 0 | 0 |
That same discipline — research signal before outreach — is the whole thesis of my Upcore lead scoring work. If you're building outreach automation, start there: score the lead, then write the message. And if you want the fully-local end of the same philosophy, Topshe runs an entire assistant in the browser with the same rule: the agent does the work, the human owns the identity.