Mox Technologies

Case study · demo build

AI voice agent that qualifies a real-estate lead and books the call

A new lead gets a conversation right away instead of waiting for a callback. The agent asks the four questions an agent would ask, checks the calendar, and books the call.

This is a portfolio build, not a client project. The webhook, availability logic and booking database are live on this site. The calendar is a demo calendar, and the sample conversation on the demo page is a written example. Live browser calling is switched off until a spending cap is in place, because every minute of voice is billed.

19automated tests
3 minhard cap per call
2tools the agent can call

The problem

Real-estate leads come in at all hours, and most of them want to talk while the idea is fresh. The first conversation is nearly always the same four questions: why are you moving, how soon, how are you paying, and where are you looking. That makes it a good fit for a voice agent, as long as it's honest about being an AI and never pretends to give advice it can't give.

What I built

A VAPI assistant named Avery, working for a fictional brokerage. Its first sentence says it's an AI assistant and that the call may be recorded. It asks one question at a time and handles the common pushback ("just browsing", "I already have an agent", "how did you get my number"). If the caller says not to call again, it stops and ends the call. It won't quote home values or give legal or financial advice.

Once it has the answers, it calls two tools on a webhook I host. The first, check_availability, returns open 30-minute slots over the next three business days, 9 to 5 Pacific, minus anything already booked. Avery offers two of them. The second, book_call, saves the booking with a lead score from 0 to 100 and returns a sentence Avery reads back as confirmation. When the call ends, VAPI posts a summary and the structured answers, and those get stored too.

How it works

Caller ──▶ VAPI (speech, model, voice)
              │  tool call + shared secret header
              ▼
       /api/voice/tool  (Cloudflare Pages Function)
              │  check_availability · book_call · end-of-call report
              ▼
       D1 database  (bookings with UNIQUE slot, call summaries)

Decisions that matter

The webhook is on the public internet, so every request must carry a shared secret, and the comparison runs in constant time. VAPI can retry a tool call, and two callers can grab the same slot at the same moment, so the database has a unique constraint on the slot itself. The in-code check only exists to produce a friendly sentence. Time slots go through the platform's own timezone data rather than hand-written offset math, which keeps the November daylight-saving change from shifting appointments by an hour.

The lead score is deterministic. The same answers always produce the same number, so a human can see why one lead outranks another.

What the tests caught

A test that books across the daylight-saving boundary failed on the first run because the plus and minus signs in the timezone offset were swapped. It was fixed before anything shipped. A click-through demo would never have found it, since it only shows up on certain dates.

During deployment I also found that VAPI accepted a secret field but didn't return it when I read the assistant back. I switched to a custom header on every server block and confirmed it stuck before relying on it.

Taking it to production

A real deployment adds a phone number, writes the lead and booking into the client's CRM, and checks consent before any outbound call. The recording notice has to follow the client's state rules. I'd also add an idempotency key on bookings, a rate limit on the webhook that doesn't depend on the secret, and a voice the brand has actually picked.

Stack