An open-source, self-hostable AI sales agent ("Wayne") built on the LiveAvatar platform. A single Next.js app: a real-time interactive avatar that talks to visitors, qualifies leads, and writes them to your CRM.
Deploy it to Vercel with a LiveAvatar API key and a handful of env vars.
- Real-time avatar session — browser ↔ LiveKit via
@heygen/liveavatar-web-sdk. - The brain (
/api/chat/completions) — an OpenAI-compatible endpoint the avatar's LiveKit agent calls each turn. Assembles a persona + real-world context (date/weather) + optional lead profile + optional CRM history, then streams Anthropic as OpenAI-shaped SSE. - Session close (
/api/ai-sales/session-end) — optional Anthropic summary → Notion CRM upsert → Slack notification. - Optional integrations, all env-gated: Notion, Slack, and lead enrichment
via a swappable
LeadResolver. Leave the env var blank to disable.
# 1. Install (Node >= 22)
npm install # or pnpm install / yarn
# 2. Provision your LiveAvatar account and write .env.local
npm run setup # asks for your API key, then provisions what's missing
# 3. Add your Anthropic key to .env.local
# ANTHROPIC_API_KEY=sk-ant-...
# 4. Run (port 3003)
npm run devnpm run setup picks an avatar, creates the context that supplies the opening
line, and generates the local signing secret. Connecting the brain needs a URL
LiveAvatar can reach, so re-run with
--url https://your-deployment.example.com once you've deployed — see
Connecting the brain. Until then the avatar talks
using your account's default LLM rather than this app's prompt.
Anything already set in .env.local is reused, so re-running is safe.
Stuck? docs/PROVISIONING.md covers the whole path
end to end, including a troubleshooting table for the failure modes that are
hard to diagnose from the symptom alone.
Open http://localhost:3003.
- Push this repo to your Git provider and import it in Vercel.
- Framework preset: Next.js. No monorepo/root-directory config needed — this is a standalone app.
- Add the env vars from
.env.examplein Project → Settings → Environment Variables (all environments). - Deploy.
The system prompt is assembled from the Markdown files in
src/lib/ai-sales/brain/prompt-parts/, concatenated in filename order
(00_prompt_outline.md, 01_agent_info.md, …). Edit a part and redeploy to
change the agent's behavior. Rules:
- Every
*.mdin the directory is a part — adding or removing one needs no code change. - Files prefixed with
_are ignored, so you can park a part without deleting it. - YAML frontmatter and HTML comments are stripped, so
<!-- … -->notes are documentation for you, not input for the model. {{AGENT_NAME}},{{AGENT_ROLE}},{{PRODUCT_NAME}}and{{COMPANY_NAME}}are substituted from theAI_SALES_AGENT_*/AI_SALES_PRODUCT_NAME/AI_SALES_COMPANY_NAMEenv vars.
Several parts ship as scaffolds with an OPERATOR SCAFFOLD comment marking
where to add your own pricing, competitor handling, and customer stories.
Set PROMPT_PARTS_DIR to point at a different directory to replace the
bundled prompt wholesale at deploy time — useful for keeping a real sales
prompt outside the repo.
The parts directory is bundled into the serverless functions at build time
via outputFileTracingIncludes in next.config.js.
Grouped the same way as .env.example.
1. The session — who the agent is and how it connects. npm run setup
fills all of this in; the API key is the only value it can't invent.
| Var | Required | Purpose |
|---|---|---|
LIVEAVATAR_API_KEY |
✅ | Sent as X-API-KEY when minting a session |
AI_SALES_AVATAR_ID |
✅ | Avatar UUID; prefilled with the public demo avatar, so it works as-is |
AI_SALES_CONTEXT_ID |
✅ | Supplies the spoken opening line — see below |
AI_SALES_AGENT_NAME / AI_SALES_AGENT_ROLE / AI_SALES_PRODUCT_NAME / AI_SALES_COMPANY_NAME |
— | Agent identity, in both the prompt and the UI; prefilled |
AI_SALES_VOICE_ID |
— | Voice UUID; blank uses the avatar's default voice |
AI_SALES_LANGUAGE |
— | Spoken language (default en) |
AI_SALES_MAX_SESSION_DURATION |
— | Per-session cap in seconds (default 600), ≤ your tier's limit |
LIVEAVATAR_API_URL |
— | Override the API host |
2. The brain — what the agent actually says. Skip this section and the avatar still connects and greets people, but answers come from your account's default LLM rather than this app's prompt.
| Var | Required | Purpose |
|---|---|---|
ANTHROPIC_API_KEY |
✅ | Powers the conversation and the session-end summary |
AI_SALES_LLM_CONFIG_API_KEY |
✅ | Protects this app's /api/chat/completions — see below |
LLM_CONFIGURATION_ID |
— | Routes each turn to this app's brain; blank = account default LLM |
PROMPT_PARTS_DIR |
— | Override the bundled prompt-parts directory |
AI_SALES_LEAD_RESOLVER |
— | Lead-enrichment implementation; blank = no-op stub, no lookups |
3. After the call — summary fan-out on disconnect. Both integrations are independent; leave either blank to skip it.
| Var | Required | Purpose |
|---|---|---|
AI_SALES_SESSION_SIGNING_SECRET |
✅ | HMAC binding session-end to a session you minted |
NOTION_TOKEN / NOTION_DATABASE_ID |
— | CRM upsert on session end |
SLACK_WEBHOOK_URL |
— | Slack ping on session end |
AI_SALES_CONTEXT_ID is required. A context supplies the avatar's
opening_text — the first thing it says the moment the room connects, spoken
before any model is in the loop. Mint a session without one and the avatar
connects and then stays silent.
Create a context once (POST /v1/contexts, or in the dashboard). To get the
per-visitor greeting this app builds — which uses the visitor's name and, if a
lead resolver is configured, their company — include the placeholder
${opening_intro} in the context's opening text. The mint passes the generated
line through dynamic_variables and LiveAvatar substitutes it at dispatch.
Everything the avatar says after that first line comes from this app's prompt
via LLM_CONFIGURATION_ID, so the context's own knowledge fields can stay
minimal — it's carrying the greeting, not the persona.
/api/chat/completions is an OpenAI-compatible endpoint served by this app.
LiveAvatar calls it over the public internet on every conversational turn, so
it has to be reachable and it has to be authenticated. Wire it up once:
Easiest path — npm run setup provisions all of it and writes .env.local:
npm run setup -- --url https://your-deployment.example.comIt inspects the account first and only creates what's missing, so it's safe to re-run. Doing it by hand instead:
# 1. Register the key that will protect your endpoint. Pick any strong random
# value — this is the same string you put in AI_SALES_LLM_CONFIG_API_KEY.
curl -X POST https://api.liveavatar.com/v1/secrets \
-H "X-API-KEY: $LIVEAVATAR_API_KEY" -H 'Content-Type: application/json' \
-d '{"secret_type":"OPENAI_API_KEY","secret_name":"sales-agent-brain","secret_value":"<your-value>"}'
# 2. Point an LLM configuration at your deployed endpoint.
curl -X POST https://api.liveavatar.com/v1/llm-configurations \
-H "X-API-KEY: $LIVEAVATAR_API_KEY" -H 'Content-Type: application/json' \
-d '{"secret_id":"<from step 1>","display_name":"sales-agent","model_name":"sales-agent","base_url":"<your-api-url>"}'Note base_url is the base, not the full route — LiveAvatar appends
/chat/completions itself, exactly as an OpenAI client would. Passing the
complete route produces a doubled path that 404s: the avatar speaks its opening
line and then goes silent, with nothing in your logs, because the request never
reaches a function.
Put the returned id in LLM_CONFIGURATION_ID and redeploy. At runtime
LiveAvatar presents the key back as Authorization: Bearer <value>, exactly as
it would to OpenAI, and the app compares the two in constant time.
Leave LLM_CONFIGURATION_ID unset and sessions fall back to your account's
default LLM — the avatar still talks, but not with this app's prompt. That's a
useful first step when checking the session flow locally, since step 2 needs a
publicly reachable URL (deploy a preview, or tunnel with ngrok/cloudflared).
/api/ai-sales/session mints the browser's session token via
POST /v1/sessions/token on the public LiveAvatar API, authenticated with
LIVEAVATAR_API_KEY and created in FULL mode. It needs an API key, an
avatar id, and — to route the conversation through this app's own brain — an
LLM_CONFIGURATION_ID. Sessions run up to AI_SALES_MAX_SESSION_DURATION
seconds.
When the account is at its concurrency ceiling or out of credits, the mint
returns 503 { error: { code: "busy" } } and the UI shows a busy state with a
retry button.
When a session disconnects, the app summarizes the transcript with Anthropic and fans the result out to Notion and Slack. Both are optional and independent — leave either set of env vars blank and that half is skipped silently.
npm run setup does not provision these; they're your own workspaces.
Notion. Create an internal integration at
notion.so/my-integrations, share your
database with it, then set NOTION_TOKEN and NOTION_DATABASE_ID. The app
upserts by email — one page per lead, with each conversation appended — so the
database needs matching properties.
We've published the template we use, so you can duplicate it instead of building the schema by hand:
LiveAvatar Sales Agent — Notion template
Slack. Create an incoming webhook and set SLACK_WEBHOOK_URL. The webhook
URL is bound to a single channel when you create it, so you choose the
destination there rather than in this app. Notion runs first so its page link
can be included in the Slack message.
Add ?debug=1 to the page URL during testing — it prefixes the Notion row with
[test] so QA sessions stay filterable from real traffic.
npm run dev # dev server, port 3003
npm run build # production build
npm run start # serve the build
npm run setup # provision a few setups
npm run lint # eslint (max-warnings 0)
npm run typecheck # tsc --noEmit
npm run test # vitestMIT — see LICENSE.