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

# Demo app: talk to a character

> A complete, runnable demo with the LIVE VIDEO avatar: copy, set env vars, run.

A full working example of the [Characters API](/characters): one Node file, zero
dependencies. It keeps your **secret key on the server**, mints a short-lived
**session token** per player, and drops the **Alakazam embed**, so the character
streams as **live video** and talks back in voice, with the chat built in. This is
the exact production embed pattern.

<Note>
  The full source is shown below. Copy it into a file and run it.
</Note>

## 1. Get a character id

Create one, or import a shipped example (see [Example characters](/characters#example-characters)):

```bash theme={null}
curl -X POST https://api.alakazam.gg/v1/characters \
  -H "Authorization: Bearer sk_live_…" -H "Content-Type: application/json" \
  --data @gojo.json          # an example file, shaped { "world": … }
# → { "id": "…", "kind": "character", … }   ← this id is ALAKAZAM_CHARACTER_ID
```

## 2. Save this as `character-demo.mjs`

```javascript theme={null}
// Node 18+ (built-in http + fetch). Zero dependencies.
import { createServer } from "node:http";

const API_BASE = (process.env.ALAKAZAM_API_BASE || "https://api.alakazam.gg").replace(/\/$/, "");
const EMBED_HOST = (process.env.ALAKAZAM_EMBED_HOST || "https://play.alakazam.gg").replace(/\/$/, "");
const SECRET = process.env.ALAKAZAM_SECRET_KEY || "";
const CHARACTER_ID = process.env.ALAKAZAM_CHARACTER_ID || "";
const PORT = parseInt(process.env.PORT || "3000", 10);
// One demo player so cross-session memory persists. In your app this is YOUR
// user's id — memory is scoped per (app, character, playerIdentity).
const PLAYER = process.env.ALAKAZAM_PLAYER || "demo-player";

if (!SECRET || !CHARACTER_ID) { console.error("Set ALAKAZAM_SECRET_KEY and ALAKAZAM_CHARACTER_ID."); process.exit(1); }

// Mint a short-lived session token server-side — the secret key never leaves here.
async function mintToken() {
  const r = await fetch(`${API_BASE}/v1/sessions/token`, {
    method: "POST",
    headers: { Authorization: `Bearer ${SECRET}`, "Content-Type": "application/json" },
    body: JSON.stringify({ worldId: CHARACTER_ID, playerIdentity: PLAYER }),
  });
  if (!r.ok) throw new Error(`mint failed (${r.status}): ${await r.text()}`);
  return (await r.json()).token;
}

createServer(async (req, res) => {
  try {
    if (req.method === "GET" && (req.url === "/" || req.url.startsWith("/?"))) {
      const token = await mintToken();
      const src = `${EMBED_HOST}/embed.html?token=${encodeURIComponent(token)}`;
      res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
      res.end(`<!doctype html><meta charset="utf-8"/>
        <meta name="viewport" content="width=device-width, initial-scale=1"/>
        <style>html,body{margin:0;height:100%;background:#000}iframe{width:100%;height:100%;border:0}</style>
        <iframe src="${src}" allow="autoplay; fullscreen; microphone"></iframe>`);
      return;
    }
    res.writeHead(404); res.end();
  } catch (e) { res.writeHead(500); res.end(String(e?.message || e)); }
}).listen(PORT, () => console.log(`✓ http://localhost:${PORT}`));
```

## 3. Run it

```bash theme={null}
export ALAKAZAM_SECRET_KEY=sk_live_…
export ALAKAZAM_CHARACTER_ID=…              # from step 1
export ALAKAZAM_EMBED_HOST=https://play.alakazam.gg   # where the embed player is served
node character-demo.mjs                     # → http://localhost:3000
```

Open `http://localhost:3000`, tap to start, and the character **streams as live
video**, talks back in voice, and takes typed messages. Because you pass a stable
`ALAKAZAM_PLAYER`, it **remembers** across sessions
([cross-session memory](/characters#cross-session-memory-per-player)).

<Note>
  `ALAKAZAM_EMBED_HOST` is wherever the Alakazam **embed player** (`embed.html`) is
  served. Use your hosted player. For local development against the repo, run the
  play-app dev server and point at it (Vite defaults to `http://localhost:5173`).
</Note>

## Going to production

This is the embed pattern verbatim: your backend mints the token, and your page drops
the `<iframe>`. To lock the token to your domain, pass an `origin` when minting and
add it to your app's `embedding_origins`. See [Embedding](/embedding).
