# Game characters

A character players talk to from your game: your server keeps the key and hands each player a one-use secret, and the player's client talks to Nur directly.

What you get:

- A character with a persona and a voice that players talk to out loud, in any
  language, cutting in whenever they like.
- Your API key stays on your server. Each player's client gets a one-use
  secret that opens a session already set up as your character, and it cannot
  change what you lock.
- Live game state the character reads before every reply, tools your game
  runs, and memories kept per player.
- Audio goes straight from the player's client to EESI. Your server never
  relays it.

## Run it

Your server mints a secret for each player:

```bash
npm install github:eesi-ai/eesi-sdk-typescript
export EESI_API_KEY=sk-eesi-...
node server.mjs
```

```js server.mjs
import { createServer } from "node:http";
import { NurClient } from "@eesi/sdk";

const nur = new NurClient();
const kael = nur.npc({ name: "Kael", persona: "The gatekeeper of Eldermoor. Wry, watchful, fair to honest folk.", voice: "Atlas" });

createServer(async (request, response) => {
  // Your own login decides who the player is, never the request.
  const secret = await kael.clientSecret({ playerId: "player-1", context: { location: "the northern gate" }, lock: "all" });
  response.writeHead(200, { "content-type": "application/json" }).end(JSON.stringify(secret.join));
}).listen(8080);
```

The player's client asks for it and starts talking:

```js game.js
import { joinSession } from "@eesi/sdk";
import { browserAudio } from "@eesi/sdk/browser";

const join = await (await fetch("/join", { method: "POST" })).json();
const session = await joinSession(join, { audio: await browserAudio() });
```

Kael greets the player first. `session.on("transcript.final", ...)` and
`session.on("response.completed", ...)` give you both sides as text, for
subtitles. `session.close()` ends the conversation.

`secret.join` holds the session's address and how replies are asked for. It
never holds your key. A secret opens one session and expires after a minute
unless you set `expiresInSeconds` (up to ten minutes).

## In an engine

Unity, Unreal, Godot or anything else with a WebSocket opens `join.url` and
speaks the [realtime protocol](/realtime/live): 16-bit PCM in, the
character's voice out, events as JSON. The same secret also opens a WebRTC
call (`join.callsUrl` with `join.token`), which brings echo cancellation on
phones and in browsers.

## Make it part of the game

- **Live state.** Mint the secret with `control: true`, and your server can
  update what the character knows while the player talks:
  `const session = await kael.control(secret).start(); await session.context.update({ health: "6/20" })`.
- **Tools.** Give the character functions your game runs, such as opening a
  gate or selling an item. With `control: true` they run on your server, which
  checks each call first.
- **Speaking first.** `session.cue("The player just drew a sword.")` makes the
  character react as soon as nobody is talking.
- **Several players.** `nur.multiplayerNpc(...)` keeps one character's story
  straight across everyone who talks to it.

## Next

<CardGroup cols={2}>
  <Card title="Characters" icon="user" href="/realtime/characters">
    Personas, presets, tools, live state, memory, rooms and languages.
  </Card>
  <Card title="Client secrets" icon="key" href="/realtime/client-secrets">
    Locks, call length, silence hang-up and signed reply webhooks.
  </Card>
  <Card title="Tools and MCP servers" icon="wrench" href="/realtime/tools">
    Functions your game runs, and when the character asks first.
  </Card>
  <Card title="Live conversation" icon="bolt" href="/realtime/live">
    The protocol an engine speaks over the WebSocket.
  </Card>
</CardGroup>
