Skip to content

Repository files navigation

LiveAvatar Sales Agent

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.

What's in the box

  • 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.

Quick start

# 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 dev

npm 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.

Deploy to Vercel

  1. Push this repo to your Git provider and import it in Vercel.
  2. Framework preset: Next.js. No monorepo/root-directory config needed — this is a standalone app.
  3. Add the env vars from .env.example in Project → Settings → Environment Variables (all environments).
  4. Deploy.

The agent's prompt

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 *.md in 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 the AI_SALES_AGENT_* / AI_SALES_PRODUCT_NAME / AI_SALES_COMPANY_NAME env 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.

Configuration

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

The opening line

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.

Connecting the brain

/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.com

It 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).

Session mint

/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.

After the call

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.

Scripts

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         # vitest

License

MIT — see LICENSE.

About

Public facing sales agent as part of open source

Resources

Stars

32 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages