Your First NPC
List your NPCs, connect to one, and stream a reply — in about fifteen lines.
Two calls do the work: mistscale.npcs.list() to find an NPC, and mistscale.connect(npcId) to open a conversation. Everything after that is events.
List your NPCs
Returns every NPC in your project — create one in the dashboard first if the array comes back empty.
list-npcs.ts
const npcs = await mistscale.npcs.list();
console.log(npcs.map(n => `${n.name} (${n.id})`));Connect and chat
chat.ts
const connection = mistscale.connect(npcs[0].id);
let reply = "";
connection.on("chatChunk", (e) => {
reply += e.delta; // append streamed tokens as they arrive
renderPartialReply(reply);
});
connection.on("chatRevision", (e) => {
reply = e.text; // grounding rewrote the reply — replace, don't append
renderPartialReply(reply);
});
connection.on("chatMessage", (e) => {
renderFinalReply(e.text); // the turn is settled
});
connection.on("open", () => {
connection.sendChat("Hello there!");
});If you don't want to render a live stream, just listen for chatMessage and ignore chatChunk / chatRevision entirely — it always fires once per turn with the final text, whether or not you consumed the intermediate chunks.
The connection surface
sendChat(message, senderId?)Sends a player message. senderId overrides the connection's default player id for just this message.
sendVoiceChunk(data, end?, senderId?)Streams captured audio. Covered in Voice.
setSpatialContext(location, opts?)Updates the NPC's sense of place. Covered in Spatial Context in the Web SDK.
getQuotaStatus() / getEvolutionStatus()Requests a one-off quotaStatus / evolutionStatus event with the NPC's current usage / mood snapshot.
close()Closes the connection and stops auto-reconnect.
state"connecting" | "open" | "closing" | "closed".
Events
open / close / errorConnection lifecycle. close carries {code, reason, expected} — expected is true only for a close() you called yourself.
chatChunk{chatId, delta} — one streamed token. Append it.
chatRevision{chatId, text} — the reply was corrected after generation. Replace your accumulated text with it.
chatMessage{chatId, text, metadata} — the turn is done. Fires exactly once per turn regardless of whether you used chatChunk.
chatBlocked{chatType, reason, limit, used} — the NPC hit its usage quota and did not reply.
reconnecting{attempt, delayMs} — the SDK is about to retry an unexpected disconnect. Automatic; you don't need to call anything.
Who does the NPC think it's talking to?
Every connection has a player id — auto-generated if you don't set one, in which case two browser tabs get two separate relationships with the same NPC. For a real game, pass your own stable player id (e.g. your logged-in user's id) via
mistscale.connect(npcId, { playerId }) so returning players are recognized across sessions and devices.Reconnection is automatic
If the connection drops unexpectedly, the SDK reconnects on its own with exponential backoff (1s, 2s, 4s, capped at 30s by default). A client-initiated
close() never triggers a reconnect. There is no server-side session to resume — a fresh connection just opens a new socket — but the NPC's memory of the conversation is unaffected, since that lives server-side keyed by NPC and player id, not by the socket.