Vonage, part of Ericsson Developers
NYC AI Builder Nights  /  Build guide

Build a Voice Agent That Tells You What to Fix

Your phone rings, and an AI agent answers. Every call gets written down. Then a second program reads those calls, finds the patterns, and asks you what to do about them.


Everyone is talking about self-improving agents right now. This is the honest version: the agent doesn't rewrite itself. It notices things and hands you the decision.

Tonight you'll build:

01What you'll need

Set these up before the session, so you spend it building instead of filling in signup forms.

Vonage

Sign up with your own mobile number. A free trial can only call the number it was registered with, and that's the number the agent will ring.

Sign up free →

Deepgram

Speech-to-text, the AI model, and the voice. New accounts come with free credit, more than enough for tonight.

Sign up →

ngrok

Gives your laptop a public URL that Vonage can reach. The free account is enough.

Sign up →

On your laptop you'll need Node.js 20 or newer, git, and ngrok, logged in with ngrok config add-authtoken <your-token>. If you use a coding agent like Claude Code, it can do the setup for you:

Prompt

Help me get this computer ready for the Vonage voice agent workshop.

Check whether Git, Node.js 20 or newer, and npm are installed. Install anything missing using the official installer or my system's package manager. Then check whether ngrok is installed, and install it if it isn't.

Run git --version, node --version, npm --version, and ngrok version yourself and tell me the results. Don't create any accounts or enter any keys for me. I'll do that part.

Housekeeping: you'll create a Vonage private key and a Deepgram API key tonight. Both go in a .env file that git ignores, so they never get committed. When you're done, delete the Vonage application from the dashboard. That kills the key.

02How it works

A voice agent has two very different jobs, and the trick is keeping them apart.

The live call has to be fast. Someone is waiting on the line. So the agent gets one job, one tool, a hard 1.5-second limit on that tool, and a fixed line to say when anything goes wrong. When a caller asks for something else, it hands them to a person instead of improvising.

Improving the agent happens later, offline. After every call, the agent writes a record of what was said, what the tool returned, how long it took, and how the call ended. Afterwards, two small programs read those records.

PieceWhat it doesWhere it lives
The agentAnswers the phone, looks up orders, hands offsrc/
The toolA fake order database with a few test orderssrc/tools/order-status.ts
Call recordsOne row per call, written before hang-updata/calls.db
The review loopFinds patterns in calls and proposes testsloops/src/review/
The eval runnerReplays every test against the agent's logicloops/src/runner/
The dashboardShows the questions and lets you answer themloops/src/dashboard/

The review loop never changes the agent, and it doesn't use an AI model. It counts. When the same thing happens enough times, it writes a question, and you decide.

03The build

Every step tells you what you should see. If you don't see it, jump to If you get stuck.

1Clone and install

terminal
$ git clone https://github.com/Vonage-Community/workshop-voice-node_deepgram-agent_loops.git
$ cd workshop-voice-node_deepgram-agent_loops
$ npm install
You should seefound 0 vulnerabilities twice. One install sets up both the agent and the loops.

2Create your Vonage application

  1. Open the Vonage dashboard → Applications and create a new application. Name it something like voice-agent-workshop.
  2. Click Generate public and private key. A file called private.key downloads. Move it into the workshop-voice-node_deepgram-agent_loops folder, keeping that exact name.
  3. Turn on Voice. For the Answer URL and Event URL, enter https://example.com/answer and https://example.com/event. This app sends its own URLs with every call, so these are never used.
  4. Create the application with the button at the bottom of the page, then copy the Application ID.
You don't need to buy a phone number. Trial accounts can't, and this agent calls you instead.

3Get your Deepgram key

In the Deepgram console, go to API Keys and create a key. Copy it now; Deepgram only shows it once.

4Start the tunnel

In a new terminal window:

terminal 2
$ ngrok http 3000
You should seeA Forwarding line with an https://… URL. Copy it and leave this window running.

5Fill in your settings

terminal
$ cp .env.example .env

Open .env and set these four:

SettingWhat goes there
VONAGE_APP_IDThe Application ID from step 2
MY_PHONE_NUMBERThe mobile number you signed up to Vonage with. Digits only, country code first: 15551234567
DEEPGRAM_API_KEYThe key from step 3
BASE_URLThe ngrok URL from step 4

Leave everything else as it is. VONAGE_NUMBER and SUPPORT_PHONE_NUMBER stay blank on a trial account.

6Start the agent

terminal 1
$ npm run dev
You should seeOrder-status voice agent listening on :3000. Leave it running.

7Call yourself

In a third terminal window:

terminal 3
$ npm run call-me
You should seeCall … placed. Answer your phone. Your phone rings, showing caller ID 123456789, the test number Vonage uses on trial accounts.

Answer it. The agent introduces itself. Say:

"Where is order A1001?"

It looks the order up and tells you it's in transit. In the npm run dev window you'll see the whole conversation go by: what you said, the tool call, how long it took, and finally outcome=completed.

8Open the dashboard

terminal 3
$ npm run dashboard

Open localhost:4000. Your call is under Recent Calls. The other panels are empty, and each one tells you the command that fills it.

9Run the loop

Run these in order, and watch the dashboard after each one.

terminal
$ npm run eval
You should see4 passed, 0 failed. These four hand-written tests are the agent's job description: answer known orders, fall back when the lookup is too slow, treat "not found" as an answer, and hand billing questions to a person. No phone call happens. The runner replays the agent's real decision code with a fake tool answer plugged in.
terminal
$ npm run seed-calls
You should see10 inserted. That's ten days of made-up call history, so there's something to find.
terminal
$ npm run review
You should seeA few patterns found and proposals written. On the dashboard, Proposals now has cards, each led by a question.
"3 callers asked about returns and got transferred. Is transferring the right behavior, or should this agent handle returns?"

Click Add to Suite on one. It moves to Decisions → Working as intended. Then run the tests again:

terminal
$ npm run eval
You should seeOne more test than before, all passing. Under Last Eval Runs, the test you just added is the last line, tagged from calls. You wrote the other four. That one came from call history.

10Make a pattern of your own

Now give the loop something real to find. Run npm run call-me twice more, and both times say:

"Hi, I want to cancel my order."

The agent says it'll connect you to support. On a trial account there's no one to transfer to, so just hang up. The handoff is still recorded.

After the first cancellation, run npm run review. Nothing new: one call isn't a pattern. After the second, run it again.

You should seeA new proposal about cancellations, and its input is exactly what you said. You just wrote a test case by talking to a phone.

04Make it yours

Change the voice

Open .env and set:

.env
DEEPGRAM_SPEAK_MODEL=aura-2-draco-en
AGENT_VERSION=order-status-v2

aura-2-draco-en and aura-2-pandora-en are British, aura-2-hyperion-en is Australian, and there are about forty more. Changing AGENT_VERSION labels every call from now on, so you can tell the new agent from the old one.

Settings are read when the server starts, so stop npm run dev (Ctrl-C), start it again, and run npm run call-me. The new call shows up on the dashboard as order-status-v2.

Try to trip it up

SayWhat should happen
"Where is order A1002?"Delivered, with a date
"Where is order SLOW123?"Any ID starting with SLOW takes too long. You'll hear the fallback line, not a made-up answer
"Where is order XYZ999?"Not found. That's an answer, not a failure
"There's a charge on my card I don't recognize"Handed off as billing
"Can you connect me to Dave? I think he works there"Handed off as a request for a person. One call is enough for a proposal, because a caller asking for a human is always worth a look
Ask about two different ordersThe second one hits the one-lookup-per-call limit

Call enough times and run npm run review again. Patterns you created start showing up as questions.

Break it on purpose

This is what the test suite is for. Open src/agent/tool-policy.ts and find the line that starts with ["billing", …. Delete it, save, and run:

terminal
$ npm run eval
You should seeRed. The test for "I want to dispute a charge" fails, because the agent no longer recognizes a billing question. Picture shipping that change on a Friday.

Now put it back:

terminal
$ git checkout src/agent/tool-policy.ts

05Point it at a real API

The agent's one tool is a fake order database. Swap it for something real, and keep the rules that make it safe on a phone call: one lookup per call, a 1.5-second limit, and the fixed fallback line when it fails. These need no API key, and their answers are short enough to say out loud:

  • Open-Meteo — weather anywhere"Do I need an umbrella in Brooklyn tomorrow?"
  • PokéAPI — every Pokémon, type, and move"What's Pikachu weak against?"
  • Open Library — books and authors"Who wrote The Left Hand of Darkness, and when?"
  • REST Countries — capitals, currencies, populations"What currency do they use in Peru?"
  • TheMealDB — recipes by name or ingredient"Give me a dinner idea with chickpeas."
  • icanhazdadjoke — exactly what it sounds like"Tell me a joke."

It's a bigger change than tonight's steps, so let a coding agent do the work, and let the tests tell you if it went right:

Prompt

In this project, the voice agent's one tool is a fake order lookup in src/tools/order-status.ts. Replace it with a real call to [API NAME] ([LINK TO ITS DOCS]) so the agent can answer [WHAT CALLERS WILL ASK].

Keep the same rules: one tool call per phone call, a 1500ms timeout, and the fixed fallback line when the call fails or times out. Update the agent's instructions in src/agent/agent-config.ts to match the new job.

Then update the four test cases in loops/eval-cases/seed-cases.json for the new tool. Run npm test and npm run eval, and don't stop until both pass.

06Quick reference

All commands run from the project folder.

CommandWhat it does
npm installInstalls the agent and the loops
npm run devStarts the agent server on port 3000
npm run call-meRings MY_PHONE_NUMBER and connects it to the agent
npm run dashboardOpens the dashboard on localhost:4000
npm run evalRuns every test and reports pass/fail
npm run seed-callsLoads ten days of sample call history
npm run reviewReads the calls and writes proposals
FileWhat it is
.envYour settings and keys. Never committed
src/agent/agent-config.tsThe agent's instructions and models
src/agent/tool-policy.tsWhich requests get handed to a person
src/tools/order-status.tsThe fake order database
loops/eval-cases/seed-cases.jsonThe four hand-written tests
data/calls.dbEvery call, every test, every decision

07If you get stuck

"Set these in .env first: …"

It lists exactly which settings are missing. Fill them in and try again.

"Vonage returned 401"

The Application ID and private.key don't match. Check that private.key is the file downloaded when you created this application, and that it's in the project folder.

The call is placed, but your phone doesn't ring

On a trial account, MY_PHONE_NUMBER must be the exact number you signed up with: digits only, country code first, no + or spaces.

Your phone rings, but you hear nothing or it hangs up

Vonage couldn't reach your laptop. Check that ngrok is still running and that BASE_URL matches its current URL. If you restarted ngrok, the URL changed: update BASE_URL, then restart npm run dev.

"NODE_MODULE_VERSION … requires …"

The database module was built for a different Node version than the one you're running, which is common with nvm. Run npm rebuild better-sqlite3, then the same command again inside loops/.

A call doesn't show up on the dashboard

Calls are recorded when they end. Hang up, and give the dashboard a few seconds to refresh.

Your trial account can't place calls at all

Ask the organizer. There are backup credentials for tonight.

Or hand it to your coding agent:

Prompt

I'm following the Vonage voice agent workshop guide at https://vonage-community.github.io/workshop-voice-node_deepgram-agent_loops/ and I'm stuck at step [NUMBER]. Here's what I see: [PASTE THE ERROR, OR DESCRIBE IT].

The project is in this folder. Check that .env has VONAGE_APP_ID, VONAGE_PRIVATE_KEY_PATH, MY_PHONE_NUMBER, DEEPGRAM_API_KEY, and BASE_URL filled in, without showing me their values. Check that private.key exists, that BASE_URL matches the ngrok URL that's running right now, and that npm run dev is running.

Explain what's wrong in plain language and fix it if you can.

Keep Going After Tonight

Run the review on a schedule and email yourself the questions. Put npm run eval in CI, so every change to the agent is tested before it ships. The whole story behind each piece is in the blog series: Add Tools and Human Transfer to a Vonage + Deepgram Voice Agent.

Have fun, and bring the weird one to the next build night.