Inspiration

A while back my car's brakes started grinding and I lost most of an afternoon calling garages. Six numbers. Two didn't answer. One put me on hold long enough that I forgot why I'd called. By the time I had three prices I couldn't remember which one had said "parts included" and which hadn't, and I'd written the warranty detail on the back of a receipt I later threw away.

That afternoon is the whole product. Not "wouldn't it be cool if an AI could make phone calls" — that part exists. What I actually wanted was the answer: which garage, how much, when, and can I trust it. The calls were just the annoying path to get there.

So Dial is built on one promise: tell it what you need done in the real world, and it works out who to contact, calls them, and brings back a result that's been verified by an actual conversation with an actual person. Not a search result. Not an estimate. What they said on the phone.

What it does

You type something like "Find the cheapest front brake pad replacement near me, this week, with a warranty." Dial reads it, asks a clarifying question or two only if it genuinely needs to, then finds real businesses nearby with phone numbers it has verified. It calls them. It keeps calling until it has enough comparable answers to make a recommendation — a goal, not a fixed count — and then stops, because every extra call costs money and rings a real person.

When it's done you get one recommendation, the reason it won, and — this mattered to me — the reason a cheaper option didn't win. One click and Dial calls back to book it. Or you call them yourself; the number's right there.

Then there's the part I kept quiet about until the end of the demo video: the same engine works in the other direction. A business imports its customer list from a spreadsheet, picks a workflow — appointment reminders, lead callbacks, or a job described in its own words — and Dial rings everyone and hands back a structured outcome per person. One person, two hundred calls, one afternoon.

There's a native mobile app with everything the web has.

How I built it

It's a TypeScript monorepo: a Fastify API, a Next.js 16 front end, an Expo app, and eleven internal packages sharing one set of Zod schemas, so the phone, the browser and the server can't disagree about what a task is.

Orchestration runs on a Postgres job queue — FOR UPDATE SKIP LOCKED, exponential backoff, and a deterministic idempotency key on every outbound call so a retried job can never ring the same shop twice. Gemini interprets the request and writes the clarifying questions. Discovery merges Google Places with OpenStreetMap. CALL-E places the calls and returns a transcript plus a structured result against a schema I hand it per call type.

The rule that shaped everything: never fabricate. "Unknown" is a first-class answer in every schema. If a shop wouldn't quote, the result says they wouldn't quote — in their words — rather than leaving a blank that reads as zero. At one point I designed a per-fact confidence display and then killed it, because the provider gives me one confidence per call, and splitting that into "price: 92%, warranty: 61%" would have meant inventing numbers. It looked great. It was a lie.

Challenges I ran into

The call that wouldn't hang up. Early on, a real call ran fifteen exchanges against a restaurant's answering machine. The recording said, three different ways, "we don't take enquiries by phone." Dial politely rephrased and asked again. For minutes. The brief had rules about manner — don't argue, don't pressure — and nothing about when to stop, and an agent that's been told to be polite will be polite forever. Now the brief has explicit end conditions: ask at most twice, hang up on a website redirect, and if two replies say the same thing, you're talking to a machine.

The request that couldn't be done. I typed "find me dinosaur meat in Dubai" as a joke and Dial went and found eighty-nine butchers. It was going to call them. Every layer of the pipeline was working perfectly and the whole thing was absurd, because nothing had ever asked whether the request made sense. Now the interpreter checks for that first, before emergencies, before anything — and tells you plainly that the task is impossible instead of spending your money proving it.

"Completed" was hiding two different stories. A task that found what you asked for and one that rang everyone and came up short ended with the same green pill. When I dug into one that stopped after two calls out of a possible ten, the real reason was that eighteen of the twenty shops were closed at that hour — and the screen said nothing about it, so it looked like Dial had quit early with plenty in reserve. I built a "what Dial is doing" panel that shows the goal, how close it is, how many businesses were actually reachable, and — once it stops — why, in a sentence. The first version of that sentence had its own bug: a task that never placed a call read "no business Dial reached could answer this" directly above "businesses called: 0." It hadn't reached anyone. That line now tells the truth too.

Ranking that lied. The comparison had a comment saying it ranked on timing, then price, then confidence. The code sorted by price, full stop. A shop that couldn't do the job until next month was beating one that could do it tomorrow because it was a few pounds cheaper. Fixed the sort, fixed the comment, and added the "here's why the cheaper one lost" line so a user could catch me next time.

The date field that erased itself. Typing an appointment date deleted each character as you entered it — on web and mobile. Same symptom, and I assumed the same cause. It wasn't. The web form refused to hold a half-typed value; the mobile one was pattern-matching complete dates only. Two fixes for what looked like one bug, and a lesson about assuming.

Smaller ones: a spreadsheet importer written with zero dependencies, because pulling in a huge xlsx library to read a contact list felt wrong — it's a ZIP reader, an inflate call and a SpreadsheetML parser in three hundred lines; a white checkmark on pale indigo that passed contrast in light mode and failed it badly in dark, fixed with a token that inverts per theme; and a country picker that only accepted two-digit dialling codes when plenty of the world has three.

What I learned

Honesty in the UI is a design constraint, not a nice-to-have, and it's harder than it sounds. Every shortcut I took toward a nicer-looking screen — a confidence bar, a "best so far," a green pill that meant two different things — turned out to be a place where the screen would eventually say something untrue. The version that survived is the one where every number on the page is counted from a row that exists.

I also learned to stop reasoning about the code and go look. Nearly every real problem got diagnosed by querying the live database or the provider's API directly, not by reading the pipeline and guessing. The code always looked right. The data said otherwise.

What's next

An evidence view that shows which business contributed which claim, so a recommendation can be traced back to the exact sentence someone said on the phone. Letting Dial negotiate within limits you set — "get it under £150 if you can" — through the same authorization flow that already gates bookings. And more business workflow templates, because the appointment-reminder one turned out to be the feature small businesses asked about first.

Built With

Share this project:

Updates

Submission history