← dipakkr.dev

Build log · One-person company · AI automation

How a one-person company runs a team of AI agents to handle 500+ sales conversations a month

Inbound leads answered in minutes instead of never. Negotiations, follow-ups, and payment chasing on autopilot, with a human approving every send. Plus the money the AI found that I didn't know I had.

Deepak · ToolJunction · July 2026 · 14 min read

I run ToolJunction, an AI discovery platform where founders and marketers find the right AI and SaaS products, and where the companies building those products compete to be seen. Visibility here is worth real money to them: a placement means showing up exactly where buyers are comparing tools, ranking in the searches that convert, and increasingly, being present in the sources that AI search engines and LLMs cite when someone asks "what's the best tool for X". So the demand side never sleeps: every month, more than 500 companies land in our inbox asking for placements, listings, sponsorships, and coverage.

Here's the uncomfortable part: that inbox is a real revenue stream, and the whole operation behind it is one person. Me. I build the platform, write the content, do the SEO, handle partnerships. Somewhere at the bottom of that list sits the inbox where the money actually arrives.

Every email in it is a deal, from $200 to even $2,000. And a deal dies quietly if nobody replies within a day or two. I was that nobody. Replying meant context-switching out of real work, remembering what I quoted someone three weeks ago, chasing people who said "sounds good" and vanished. When I finally exported six months of the mailbox and analyzed it, the damage was worse than I feared:

That's not an inbox problem. That's a hole in the boat.

Why I didn't just buy a tool

First instinct: someone must have solved this. I looked at sales CRMs, email automation platforms, outreach tools. They all fell into the same two buckets:

  1. Too big for my scale. Enterprise CRMs want pipelines, teams, integrations, onboarding calls. And even after all that setup, they organize the work; they don't do it. What I actually needed done: read an email from a stranger, work out why they're reaching out and what would actually solve it for them, reply with the right price, follow up when the conversation goes quiet, learn from how each first conversation lands, and deliver the value before chasing the money. That's a salesperson's job description, not a database's.
  2. Automation without understanding. Sequence tools can send template follow-ups on a schedule. But my problem isn't sending emails, it's knowing WHAT to send. "Sounds good, send details" means one thing after a quote and something completely different after a payment receipt. No template tool knows the difference.

Add the setup time and the learning curve, and the math was obvious: I'd spend a week configuring someone else's tool to do 60% of the job. Or I could build exactly the thing I needed. I'm one person; the tool has to fit ME.

So I built my own CRM with an AI brain. Python, SQLite, and a Claude subscription. Here's how, and here's everything that broke on the way.

First: how do you even read email with code?

If you've never touched email programmatically, this part is genuinely simpler than you think.

Your email doesn't live in your Gmail or Zoho app. It lives on a server, and the app is just a viewer. IMAP is the protocol every mail app uses to talk to that server, and nothing stops your own code from speaking it. You connect with a host, a login, and a password, and the server hands you numbered messages. Python ships with everything you need:

import imaplib

M = imaplib.IMAP4_SSL("imappro.zoho.in", 993)      # your provider's IMAP host
M.login("contact@tooljunction.io", APP_PASSWORD)   # an app password, not your real one
M.select("INBOX")

# every message that arrived after the last one I processed
typ, data = M.uid("search", None, "UID 1201:*")

# read one WITHOUT marking it as read
typ, raw = M.uid("fetch", data[0].split()[0], "(BODY.PEEK[])")

That's the whole trick. SMTP is the same idea for sending. Two details that matter in practice:

Every email tool you've ever paid for is these two protocols plus opinions. Mine is too.

The architecture

System architecture: Zoho mailbox, autopilot cycle, LLM deal desk, human review loop Zoho Mail: contact@ separate mailbox · IMAP inbox + Sent, SMTP out agent.py cycle: runs every 5 minutes via launchd Watermark sync UID SEARCH N+1:* · BODY.PEEK, never marks read Scope + noise filters contact@ only · newsletters and bounces binned Hard guardrails (plain code) offers ≥ $250, bulk ≥ 5, cooldowns → a human LLM deal desk: one call per mail full transcript + hard facts + price ladder → JSON Backlog worker follow-ups, 2/cycle DRAFT reply written, queued ESCALATE big or unclear → you DECLINE off-niche, polite, final IGNORE spam, receipts, noise CRM dashboard: where the team works Review queue edit · discard · approve Approve = send SMTP, In-Reply-To threaded SQLite (WAL) threads · drafts · wins workflow truth only Zoho saves the Sent copy · next cycle mirrors it back and marks the draft sent
The full loop: watermarked sync, filters, guardrails, one LLM judgment, human approval, threaded send, mirror.

The way I think about it now: I hired a small team of AI agents, each with exactly one job. A drafter that reads whole conversations and writes the next reply. A deal-desk brain that decides what each inbound message needs. A follow-up worker that revives threads that went quiet. A stricter auditor that re-checks pipeline states and is forbidden to write anything. A sweeper that recovers whatever gets stuck. None of them is allowed to send an email on its own.

Mechanically, it's humble: a scheduled job (macOS launchd, every 5 minutes) runs one cycle: pull new mail, decide what each message needs, write drafts, sync what got sent. A small web dashboard shows the team everything and takes approvals. SQLite sits in the middle holding workflow state. That's the whole system: no framework, no queue, no ORM, ~2,900 lines of Python including the entire frontend.

The interesting decisions are in the middle layer.

Code decides everything with a dollar sign. The AI decides everything with nuance. Before any AI runs, plain Python drops mail that isn't addressed to the sales address, bins newsletters and bounces by header inspection, and force-escalates anything big: offers of $250 or more, bulk orders of 5+ posts. A language model can be argued out of a rule. An if statement cannot.

One AI judgment per conversation, not a pipeline of classifiers. Version 1 of this system (more on it below) classified the LAST message. The rebuilt brain gets the ENTIRE conversation transcript, my price list, my niche rules, and, critically, a block of facts computed by code:

FACTS (trust these over your own inference): We have replied 2 time(s)
in this thread. The last message was from them, 0 day(s) ago.

Models read tone and intent well. They count turns and judge elapsed time badly. Handing over pre-computed facts killed an entire class of embarrassing drafts, like follow-ups to conversations we never actually replied to.

The model must answer in strict JSON, an action rather than prose:

{"action": "DRAFT", "category": "guest_post", "stage": "negotiation",
 "subject": "Re: ...", "body": "...",
 "reason": "they countered $105 on our $150 quote; counter once toward standard"}

That reason field gets stored on the thread and shown in the dashboard. Every draft is explainable after the fact. This mattered more than any prompt trick: it turned "the AI is being weird" into an audit trail I can read.

Any failure falls to a human, never to a template. Model timeout, garbage output, unparseable JSON: the mail lands in a review queue for me. The worst-case output of this system is a human reading an email. There is no path where a bad AI reply goes out on its own.

And the AI runs on a CLI, not an API key. The drafting brain is the claude command-line tool in headless mode, authenticated by the same login as my terminal:

r = subprocess.run(
    [cli, "-p", prompt, "--output-format", "json", "--model", model],
    capture_output=True, text=True, timeout=180)

One subprocess per decision, capped at 20 calls per 5-minute cycle, overflow deferred to the next cycle. No key management, no separate billing, and upgrading the model is a config string.

Version 1 deserved its 5/100

I have to confess what came before the AI brain, because the failure shaped everything.

The first engine was keywords and templates: see "guest post" and "price", send the rate card. It demoed great. Then I read what it prepared for real threads and rated it 5 out of 100:

The lesson: understanding a deal means reading the whole conversation, not pattern-matching the last message. That's exactly the thing LLMs are good at and regex is hopeless at. Version 2 exists because version 1 was confidently, fluently wrong.

The bugs that almost sank it quietly

Every scary bug in this project failed SILENTLY. Three examples, with the fixes, so you don't repeat them.

1. The sync that went blind when I read my own mail. The first version asked IMAP for UNSEEN (unread) messages. Then I read a few emails on my phone, and the agent never saw them, because they were no longer unread. The dashboard sat 12 hours stale while looking perfectly healthy. Fix: remember the highest message UID you've processed and ask for UID SEARCH last+1:* instead. Read status becomes irrelevant. Advance that watermark inside the same database transaction that stores the message, so a crash can never skip mail.

2. The alias that was secretly a whole different mailbox. The system ran for half a day connected to my personal mailbox, believing contact@ was an alias of it. It was actually a separate Zoho user with its own inbox, its own IMAP toggle, its own app password. My test mail "disappeared"; every instinct said the sync code was broken. The code was perfect. It was reading the wrong mailbox. Check your connection-level assumptions before debugging your logic: one test-mail round trip would have saved me hours.

3. Every sent email existed twice. When you approve a draft, the CRM records it and sends via SMTP. Zoho then saves its own copy to the Sent folder, which my next sync cycle happily imported as a SECOND copy of the same reply. The thread looked like we were pestering buyers twice. The dedupe compares whitespace-normalized body prefixes within a 2-day window, because the mirrored copy doesn't reliably carry the same Message-ID.

The dashboard where approvals happen

Nothing sends on its own. Every draft waits in a mail-client-style dashboard where my (now 3-person) team reads, edits if needed, and clicks Approve:

Thread view: buyer inquiry with an AI draft marked READY TO SEND and Approve, Edit and Discard actions
A drafted reply waiting for approval. The chip states exactly what will happen; nothing sends without a click.

The decisions that turned out to be load-bearing:

Draft lifecycle: one active draft per thread, from LLM to sent LLM writes draft cycle or backlog pending in review queue edit edited your version sends approve sending atomic claim, no teammate double-send sent confirmed by mirror skipped discarded or superseded discard newer draft arrives Invariant: one active draft per thread. Recording a new draft first retires every older unsent one, otherwise Approve is ambiguous about which reply goes out.
Draft lifecycle. The dashed edge is the fix: a newer draft retires the older one instead of coexisting with it.
Overview dashboard: booked revenue, weighted pipeline, average deal size, funnel and revenue prediction
The morning view: money on the table, the funnel, and what needs a human today.

The audit that found money in dead threads

Killing version 1 left a mess behind: 531 threads stuck in a "needs human" state the old engine had dumped them into, with reasons like "could not classify intent". The new drafter correctly refuses to touch escalated threads, so these were invisible to automation. Forever. A graveyard with deals buried in it.

So I wrote a second, stricter prompt: a classifier that is FORBIDDEN to write replies. It only assigns the true pipeline state, and it must cite its evidence:

- AWAITING_PAYMENT: BOTH sides explicitly agreed a specific deal (price +
  what's being bought), and payment is genuinely the next step. Merely
  MENTIONING prices, rates, budgets, or invoices is NOT enough.

It re-read all 531 conversations overnight. What it found is the single best argument for the whole project, and it's in the results section below.

The audit also surfaced two infrastructure bugs worth stealing the fixes for:

  1. "database is locked." The audit's long connection collided with the 5-minute cycle's writes and SQLite's default 5-second patience ran out. PRAGMA journal_mode=WAL plus timeout=30 on every connection. Gone.
  2. Greedy JSON extraction. re.search(r"\{.*\}", raw, re.S) works until the model returns TWO JSON objects, and greedy matches from the first { to the last }. Walk the string with json.JSONDecoder().raw_decode and take the first valid object instead. If you parse LLM output with a regex, you have this bug; you just haven't logged it yet.

The monitoring page earned its keep in one night

The dashboard has a /logs page: a live, color-coded tail of the agent's actual log file. I built it because sync chatter was cluttering the header. The very night it shipped, while taking a screenshot for this post, I saw this:

Live activity log showing the same two threads being escalated every five minutes
Same two senders, escalated every cycle: 00:45, 00:51, 00:56, 01:01... The monitoring page was one day old.

The follow-up worker escalated a thread, recorded the escalation, but never marked the THREAD as escalated, so it re-picked the same thread every cycle. New AI call, new escalation row, new desktop notification, forever. One sender had accumulated 56 duplicate escalations.

Two invariants fixed it for good: escalating a thread must move it out of the worker's selection set, and the escalate function itself now refuses to open a second escalation for a sender who already has one open. Belt and suspenders, because the next caller with this bug is already being written.

The same screenshot exposed a second infinite loop: the Sent-folder sync skipped self-addressed mail BEFORE advancing its position marker, so one test email I'd sent to myself was re-fetched every cycle until the end of time. The general rule: in watermark loops, advance the watermark for every item you VISIT, not every item you process. Skip paths are where "forever" bugs live.

Deliberately boring: it still doesn't send on its own

The system has a full-autonomy mode built in: drafts auto-send after a 30-minute grace period unless a human discards them. It is tested and OFF. The plan is stages: weeks of every draft human-approved (the send lock is a config interlock the UI cannot override), then auto-send for routine first-touch quotes only, then wider. An AI that emails real buyers about real money earns autonomy gradually. And the stage where a human reads everything is exactly where all six bugs above got caught.

Results

<10 minfrom inquiry to ready-to-approve draft, was 5.6 hours median (or never)
85drafts in the review queue by the first morning, backlog included
9deals found already PAID and never recorded, in "dead" threads
14deals with work delivered and money still collectible, now chased automatically
845 → 7threads in the "needs human" queue after the AI audit
2,900lines of Python, standard library only, including the whole CRM

The audit alone surfaced over $1,000 of real money from threads everyone had written off: buyers who wrote "the invoice has been paid" into the void, published articles never paid for. Every pipeline number on the dashboard is now either set by the AI with a cited reason, audited by it, or set by hand. I trust my own CRM, which is a strange sentence to be able to write.

If you build one of these

  1. Feed the model the whole conversation plus code-computed facts. Let code own every number that matters.
  2. Force strict-JSON decisions with a reason field, and store the reasons. Explainability beats prompt engineering.
  3. Fail to a human, never to a template.
  4. Watermarks over read-flags. PEEK over politeness. Advance on visit, not on success.
  5. Run a stricter second prompt over your own state machine now and then. Mine found four figures of forgotten money.
  6. Build the logs page before you think you need it. Mine was one day old when it caught two infinite loops.

All screenshots are from the production system; buyer identities are pseudonymized. Every outbound email is still human-approved.