> ## Documentation Index
> Fetch the complete documentation index at: https://docs.trychert.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build your first agent

> Create a JavaScript LiveKit voice agent that you can call through Chert.

You are building a **LiveKit agent server**: a program that joins a room and runs
a voice conversation. Chert connects that room to FaceTime. This example uses
JavaScript and OpenAI Realtime; change its instructions to build a tutor,
interview coach, or another voice experience.

Use macOS or Linux with **Node 22.23.2 or 24.x** and npm. Check `node --version`
and `npm --version`. Install Node from [nodejs.org](https://nodejs.org/en/download)
or follow your organizer's setup instructions. If you already use nvm:

```bash theme={null}
nvm install 22.23.2
nvm use 22.23.2
```

## 1. Create your accounts and choose a name

1. Create a [LiveKit Cloud account](https://cloud.livekit.io) and a project.
   Obtain its URL, API key and API secret from the project's settings.
2. Obtain a model API key with access and credits for OpenAI Realtime. This is
   separate from your LiveKit and Chert credentials; a ChatGPT subscription is
   not an API key. See the [LiveKit Realtime integration](https://docs.livekit.io/agents/models/realtime/plugins/openai/).
3. Choose a dispatch name, such as **`team-otter-agent`**. Use that exact name in
   the env file and in `chert dev --agent team-otter-agent`.
4. Get your Chert project access, UUID and assigned FaceTime address from your
   organizer. See [Projects and lines](/facetime-cli/connect).

Your LiveKit project could be called `Otter Hackathon`; your agent can still be
`team-otter-agent`. These labels do not need to match. Give other agents in the
same LiveKit project different dispatch names unless you intentionally run
interchangeable copies of the same agent.

## 2. Create the local project

Use a new directory:

```bash theme={null}
mkdir my-facetime-agent
cd my-facetime-agent
```

Create the following files in your editor. This page includes all required
source; you do not need access to a private GitHub repository.

### package.json

```json theme={null}
{
  "name": "chert-student-agent",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "engines": { "node": "^22.23.2 || ^24.0.0" },
  "scripts": {
    "start": "node --env-file=.env.local agent.mjs start",
    "check": "node --check agent.mjs",
    "help": "node agent.mjs --help"
  },
  "dependencies": {
    "@livekit/agents": "1.8.0",
    "@livekit/agents-plugin-openai": "1.8.0"
  }
}
```

### .gitignore

Create this before adding credentials or committing your project:

```gitignore theme={null}
node_modules/
.env
.env.*
!.env.example
*.log
.DS_Store
```

### .env.local

Paste these placeholders into the file, then replace them privately in your editor:

```dotenv theme={null}
LIVEKIT_URL=wss://YOUR_PROJECT.livekit.cloud
LIVEKIT_API_KEY=YOUR_LIVEKIT_API_KEY
LIVEKIT_API_SECRET=YOUR_LIVEKIT_API_SECRET
OPENAI_API_KEY=YOUR_OPENAI_API_KEY
AGENT_NAME=team-otter-agent
```

All three `LIVEKIT_*` values must come from the same project. Both your agent and
Chert will use that project. Keep the file private:

```bash theme={null}
chmod 600 .env.local
```

Never commit the file, paste its values into chat, or execute it with `source`.
The startup command uses Node's env-file parser. Chert reads its LiveKit values;
your agent uses the model key. Existing shell variables override env-file values,
so avoid stale credentials in your terminal environment.

### agent.mjs

```javascript theme={null}
import { fileURLToPath } from 'node:url';
import { AutoSubscribe, cli, defineAgent, ServerOptions, voice } from '@livekit/agents';
import { realtime } from '@livekit/agents-plugin-openai';

// Change these instructions to build your own agent. Keep replies conversational.
const instructions = `You are a friendly AI study partner speaking over FaceTime.
Help the caller understand a topic by asking one question at a time.
Keep each reply to one or two short sentences. Be clear that you are an AI.
Wait for the caller to speak first.`;

export default defineAgent({
  entry: async ctx => {
    await ctx.connect(undefined, AutoSubscribe.AUDIO_ONLY);
    // Chert prepares a room before a caller exists. Join promptly so Chert can
    // find us, but wait before opening a paid model session. Doctor jobs may end
    // without a caller ever joining.
    const caller = await ctx.waitForParticipant();
    const session = new voice.AgentSession({
      llm: new realtime.RealtimeModel({ model: 'gpt-realtime', voice: 'marin' }),
    });
    ctx.addShutdownCallback(() => session.close());
    await session.start({
      room: ctx.room,
      agent: new voice.Agent({ instructions }),
      record: false,
      inputOptions: {
        participantIdentity: caller.identity,
        audioEnabled: true,
        videoEnabled: false,
        textEnabled: false,
        closeOnDisconnect: true,
        deleteRoomOnClose: false,
      },
      outputOptions: { audioEnabled: true, transcriptionEnabled: false },
    });
    // Participant arrival is not FaceTime media readiness. No generateReply()
    // here: the caller says hello after connection, then the model responds.
  },
});

cli.runApp(new ServerOptions({
  agent: fileURLToPath(import.meta.url),
  agentName: process.env.AGENT_NAME || 'student-agent',
}));
```

Change the `instructions` text to define your agent's personality and purpose.
Keep replies short enough for a spoken conversation.

## 3. Install and start

```bash theme={null}
npm install
npm run check
npm start
```

This creates `package-lock.json`; keep that file with your project and use
`npm ci` for subsequent fresh installs. Wait for successful LiveKit registration
and leave this terminal running. This starter does not automatically reload edits.

**`npm start` runs your agent and registers the dispatch name with LiveKit.** The
starter reads `AGENT_NAME` and explicitly passes it as `ServerOptions.agentName`.
There is no separate manual registration command for this local flow.

## 4. Connect it to FaceTime

Open a second terminal, select the same Node version, and follow
[Connect and test](/facetime-cli/quickstart) to install Chert, sign in, and check your connection. CLI beta.3 reads the
LiveKit keys from this folder automatically; separate setup is optional. Your everyday Chert command will be:

```bash theme={null}
chert dev --agent team-otter-agent
```

The agent program and Chert must both remain running. After the waiting-room
message, call your assigned line using **FaceTime Audio** and say hello first.

## Why the starter waits

Chert may dispatch a job before a caller exists, for prewarming or diagnostics.
The agent joins promptly, waits for a participant, and then opens the model
session. It does not automatically greet when a job starts: participant arrival
alone does not prove FaceTime audio is ready. Letting the caller speak first
avoids losing the greeting during connection.

The starter listens to the caller's audio, leaves room deletion to Chert, and
closes the model session on shutdown. Generic rooms allow two participants;
adding an avatar or sidecar participant needs a separate integration.

## Already have a TypeScript agent?

Keep your existing build/start commands. Set LiveKit's `ServerOptions.agentName`
to your chosen dispatch name, use the same LiveKit project credentials as Chert,
and allow jobs to connect promptly even when a caller has not arrived yet.
Avoid an unconditional greeting on job startup. Then follow the same CLI steps.
The [official LiveKit Node starter](https://github.com/livekit-examples/agent-starter-node)
provides a TypeScript project structure if you prefer one.

<Note>
  This is an audio starter. Installation, syntax and SDK compatibility checks do
  not establish working FaceTime media. Complete the two-call test on your assigned
  line before presenting it or building additional features.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.