DEV Community

Domonique Luchin
Domonique Luchin

Posted on

Before: Manually Calling Leads. After: The AI Does It.

Two months ago I was calling leads myself. Running six businesses. Doing it manually.

Here is what that looked like:

  • Pull a distressed property lead from my scraper
  • Look up the owner
  • Call from my personal number
  • Leave voicemails nobody returned
  • Track callbacks in a spreadsheet
  • Forget half of them

Now the AI handles the call within 90 seconds of the lead hitting the database.

The Stack

Lead scraper pulls distressed property data from 14 Texas counties nightly. Foreclosures, delinquent taxes, code violations, 311 complaints. Scores each lead 0-100 based on distress signals.

Supabase stores everything. The lead lands in the properties table. A database trigger fires a pg_cron job that drops the lead into the dial_queue table.

VAPI handles the AI voice call. It is connected to Asterisk via SIP. When the dial queue has a pending lead, a BullMQ worker picks it up, calls the VAPI API, and the AI calls the homeowner.

The AI introduction:
"Hey, this is Marcus calling from Load Bearing Capital. We buy houses in the Houston area, cash, any condition. I noticed your property at [address] and wanted to see if you had any interest in a cash offer. Is now a good time?"

Asterisk logs the call. VAPI sends back the transcript. GPT-4o-mini classifies the outcome: interested, not interested, callback, voicemail, disconnected.

Outcome lands in call_outcomes. If the lead is interested, it escalates to me via Telegram.

The Numbers

Before: maybe 20 calls per day when I had time.
After: 200+ calls per day, every day, whether I am working or not.

Callback rate is roughly the same. But volume is 10x.

The real win is follow-up. The system runs three-touch sequences automatically. Call on day 1. SMS on day 3. Call again on day 7. No leads fall through.

Setting This Up

Step 1: Supabase dial_queue table

CREATE TABLE dial_queue (
  id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  lead_id uuid REFERENCES leads(id),
  phone text NOT NULL,
  status text DEFAULT 'pending',
  attempts integer DEFAULT 0,
  max_attempts integer DEFAULT 3,
  scheduled_at timestamptz DEFAULT now(),
  assigned_agent text DEFAULT 'vapi-lbc'
);
Enter fullscreen mode Exit fullscreen mode

Step 2: BullMQ worker

const dialWorker = new Worker('dial-queue', async (job) => {
  const { lead_id, phone, agent_id } = job.data;
  await vapi.calls.create({
    assistantId: agent_id,
    customer: { number: phone }
  });
}, { connection });
Enter fullscreen mode Exit fullscreen mode

Step 3: VAPI assistant

Build your script in VAPI. Connect it to your Asterisk SIP trunk. Test it by calling your own number first.

Step 4: Outcome handling

VAPI sends a webhook when the call ends. Your endpoint updates the call_outcomes table. A pg_cron job processes outcomes every 15 minutes and queues follow-ups.

What Still Requires Humans

Contract negotiation. Anything above $5,000 in decision-making. Anything the AI flags as "needs human" in the outcome.

Everything else runs without me.

Top comments (0)