Duck
Tutorials5 min read

Self Hosted AI: Building Your Own Agent Stack for Cold Email and Lead Gen

Samet Turan— Editor··5 min read

Deploy self hosted ai agents for cold email, lead scraping, and invoicing with real prompts, cost breakdowns, and debugging tips you can use today.

Last quarter I needed to send 500 personalized cold emails, scrape LinkedIn for leads, and generate invoices — all while keeping costs under $30/mo. I tried a few SaaS tools, but the per‑seat pricing added up fast and the data never stayed in my control. After a weekend of tinkering, I built a self hosted ai stack that handles each task with local LLMs and a few scripts.

Why does my self hosted ai agent keep hallucinating invoice numbers?

When I first hooked a local LLM to my invoicing workflow, the model would invent client names or add extra line items that never existed. The problem wasn’t the model size; it was the prompt. I was asking the model to “write an invoice” without giving it a strict template or any grounding data. The model treated the request as a creative writing exercise and started filling in gaps with plausible‑sounding fiction.

What fixed it was turning the generation into a fill‑in‑the‑blank task. I passed the structured data (client name, amount, due date) as JSON and asked the model to only format it into HTML. The prompt became something like:

# You are a precise formatter. Given the following JSON, output a valid HTML invoice snippet. Do not add any extra text.
{ "client": "Acme Corp", "amount": 1250, "due_date": "2026-09-30" }

With that constraint, hallucinations dropped to zero. The lesson: never rely on a model to invent data; give it the data and ask it to transform.

What most guides get wrong about local LLMs

Many tutorials tell you to download the biggest model you can fit on your GPU and start prompting. In practice, a 7B parameter model runs fine on a $5/mo VPS with CPU‑only inference, while a 13B model needs a GPU that costs $40/mo or more. The bigger model rarely yields a noticeable quality gain for structured tasks like email copy or invoice formatting.

I’ve seen guides recommend paying for a hosted API just to avoid the hassle of quantization. That’s unnecessary if you use Ollama with a 4‑bit quantized model. The download is ~3GB, the RAM usage stays under 2GB, and latency is acceptable for batch jobs.

Concrete example: cold email pipeline with Ollama and Python

Here’s the end‑to‑end flow I use every week:

  1. Scrape LinkedIn profiles with Apify (free tier gives 1000 credits/mo, enough for ~200 leads).
  2. Store each lead as a row in a local PostgreSQL database (running in Docker).
  3. Trigger a Python script that pulls the next 20 leads, builds a prompt, and calls Ollama’s HTTP API.
  4. The prompt asks the model to produce a personalized opening line based on the lead’s job title and recent post.
  5. Script inserts the line into a templated email, logs the output, and moves the lead to a “done” table.

Sample prompt (Python f‑string):

prompt = f"""
You are a sales copywriter. Write one short, friendly opening line for a cold email to {first_name} who works as a {job_title} at {company}. Reference their recent LinkedIn post about {post_topic}. Keep it under 20 words.
"""

Running this on a $6/mo VPS (1 vCPU, 2GB RAM) costs about $0.02 per 1000 tokens. For 200 leads a month, the LLM cost is under $0.50. The rest of the stack (Apify, Docker, PostgreSQL) stays within the free tier or a few dollars.

Price opinion: $6/mo for the VPS is fair; you get full data ownership and unlimited runs. Paying $49/mo for a SaaS email‑personalization tool feels ridiculous when the same work runs locally for pennies.

How to debug when this breaks

First, check the Ollama server logs. If you see “connection refused,” the Docker container likely crashed — restart it with docker restart ollama. If the model returns empty responses, verify that the prompt isn’t being truncated; Ollama’s default context window is 2048 tokens, but some quantized models lower it to 1024. Reduce the prompt length or switch to a model with a larger context.

Second, inspect the database. A common failure is a NULL value in the job_title column, which makes the prompt produce gibberish. Add a COALESCE in your SQL to fill missing fields with “unknown”.

Third, watch the Apify actor run status. If it returns “FAILED” due to a CAPTCHA, switch to the built‑in “LinkedIn Scraper Lite” actor that uses a different method and costs fewer credits.

Finally, keep a simple health‑check endpoint in your Python script that returns 200 when the Ollama API responds within two seconds. Alert yourself via email if it spikes above five seconds — that’s usually a sign the VPS is being throttled.

One concrete gripe: the Ollama documentation hides the API endpoint for model loading behind a vague “see the README” line. I spent an hour guessing the URL before finding it in a GitHub issue.

One concrete love: the ability to version‑control the exact model file. I keep the GGUF file in a private Git repo, roll back to a previous version if a new quantized build introduces a bug, and never have to worry about a remote API changing its behavior overnight.

— and good luck finding docs for this — but once you have the stack running, adding a new agent (say, for social‑media copy) is just another prompt and a few lines of code.

For more on this exact angle, deeper coverage of AI agent platforms.

If you’d rather skip the build and deploy a working version in an afternoon, we’ve packaged this workflow as a blueprint at deepusecase.com/vault.

— The Colophon

One AI tool. Tested. Reviewed.
In your inbox every Sunday.

~3 minute read. Real outcomes from operators, not marketers.

Free. One email per Sunday. Unsubscribe in one click.