Skip to content

NPCs

NPCs are characters your players talk to: give one a personality, a background and a voice, and your players talk to it out loud, in any language, and it remembers them between calls. NPCs belong to your account in the cloud console, any of your apps can call them with its API key, and they cost nothing while idle.

This guide is for app owners. NPCs live under the NPCs tab of the console, next to your apps and nodes, under the same login. Everything the console does is also a REST call, so a script or an agent can create and edit NPCs for you with an account token.

One NPC opens on four tabs:

tabwhat is on it
Overviewthe numbers - players, calls, minutes heard, cost - then what each part of a call runs on (reply model, transcription, voice model, voice, speed, emotion, sampling), and what the model is told: personality, background, greeting, example exchanges
Sandboxcall the NPC yourself, from the browser. It is a real call, billed and remembered like any other, and it runs the exact client your game will
Phraseswrite the lines this character says without being asked, render them in its voice, listen, and download them as files your game ships (below)
Interactionseveryone who has talked to it, through which app, how many turns, and what the NPC remembers about them

The console is where you write and try an NPC. Wiring one into a game is this page: mint a ticket (step 2), open the call (step 3).

What you get

  • Speaks how they speak. Language is detected, never configured. Speak Korean mid-sentence and the reply comes back in Korean, in the same voice. Verified across Korean, Japanese, Chinese, French, German, Spanish, Italian, Portuguese, Russian, Hindi, Dutch, Polish, Swedish, Turkish and English.
  • Their own voice. Pick a stock voice or clone one from a short sample and point any number of NPCs at it. Speed and delivery are per NPC.
  • Interrupt like a person. Talk over an NPC and it drops to a murmur within 350 ms, then hands over the moment the new answer is ready. No dead air.
  • A memory, not a session. Every NPC keeps a bond with each player it talks to: the recent turns are replayed on the next call, and when a call ends the NPC rewrites a short summary and the facts that matter, so the tenth call opens knowing them. The console's Interactions tab shows those players per NPC, with what the NPC remembers about each.
  • Your models, your rules. Any transcription, reply or voice model your providers offer, per NPC. Defaults are set; nothing is locked, and the Overview tab shows which model each part of a call is actually running on.
  • Test it before you ship it. Every NPC has a Sandbox tab in the console. It runs the exact client your game will.
  • Lines you do not pay to invent. Barks - "hey, wanna drink?", "nice evening, isn't it" - are the same three sentences forever. Write them once, render them in the NPC's voice, download the mp3s and play them from disk. Same throat as the conversation, no model, no call, no latency.

1. Create an NPC

In the console, open NPCs and create one: a name, a slug, a personality, a greeting and a few example exchanges. The examples shape tone far more than the description does, and examples in a language pull replies toward it. Pick a voice.

Then open the NPC's Sandbox tab and click the microphone: the console places a real call, as you, over the same protocol your game will use. Change a line, call again. The Overview tab is where you check what it ended up running on, and every call from the sandbox shows up under Interactions like any player's.

2. Mint a ticket per player

Your server holds an app's API key (the same arrr_… key connect() uses) and trades it for a short-lived ticket, scoped to one NPC and one of your players. Any app you own can call any NPC you own; the app whose key minted the ticket is what the call and the player are filed under:

bash
curl -X POST https://cloud.arrr.fun/api/apps/APP_ID/npcs/yuki/token \
  -H "Authorization: Bearer arrr_..." \
  -H "Content-Type: application/json" \
  -d '{"playerId":"user-42","displayName":"Sam"}'
json
{"token":"...","websocketUrl":"wss://cloud.arrr.fun/ws/npc?token=...",
 "npcId":"npc_...","npcSlug":"yuki","npcName":"Yuki","greeting":"Hey you.","expiresIn":300}

The ticket dies in five minutes and names one NPC and one player, so it is safe in a URL. playerId is your own id for the player; it is what the NPC's memory is keyed by. The key also works as an x-api-key header or as apiKey in the body. A game that has no server can mint from the browser with the key it already ships, at the cost of anyone with the key being able to place calls under your app.

3. Open the call

The browser client handles the microphone, end-of-utterance detection, interruption and playback. tokenUrl points at your endpoint, which returns the JSON above.

html
<script src="https://cloud.arrr.fun/npc/arrr-npc.js"></script>
<script>
  const call = new arrrNpc.NpcCall({ tokenUrl: '/api/npc-ticket' });
  await call.start();                       // mic on, conversation live
  call.on('heard', h => show('you', h.text));
  call.on('delta', t => append(t));
  call.on('reply', r => done(r.stats));
  call.say('typed text works too');
  call.stop();
</script>

Or from the package: import { NpcCall } from 'arrr-network'. Pass ticket: <the JSON> instead of tokenUrl if you already have one.

Without the client, the protocol is small:

you send
{"type":"start","sampleRate":16000}announce the rate
binaryPCM16 mono little-endian
{"type":"commit"}end of utterance: transcribe and reply
{"type":"text","content":"..."}typed input, same pipeline
{"type":"cancel"}barge-in: abandon the reply in progress
{"type":"bye"}hang up
you receive
ready heard silence
thinkMs reply.delta reply.donetext as it is written, then stats
audio.start binary audio.endPCM16 mono 44100
errorcarries the provider's own words

Pre-recorded phrases

Some lines never need thinking about. A bartender says "what can I get you?" a thousand times a night, and paying a model to invent it each time is slower and dearer than playing a file. So the NPC's voice is available on its own: text in, audio out, downloaded and shipped with your game.

In the console, open the NPC's Phrases tab, write one line per line, and press Render. Every line comes back playable, with a Download beside it and Download all (.zip) for the set - the same bytes you just listened to, not a second take. The list is kept on the NPC, so when you change its voice you open the tab and render the set again.

The same thing over the API, for a build script:

bash
# one line, one file
curl -X POST https://cloud.arrr.fun/api/npcs/NPC_ID/speak \
  -H "Authorization: Bearer arrr_acct_..." -H "Content-Type: application/json" \
  -d '{"text":"Hey, wanna drink?"}' -o hey-wanna-drink.mp3

# a whole set, as a zip of mp3s plus a manifest
curl -X POST https://cloud.arrr.fun/api/npcs/NPC_ID/speak \
  -H "Authorization: Bearer arrr_acct_..." -H "Content-Type: application/json" \
  -d '{"phrases":["Hey, wanna drink?","Nice evening","Another round?"]}' \
  -o barks.zip

A game's own key works too, against the same NPC, so a build step needs no account token: POST /api/apps/APP_ID/npcs/mo/speak with Authorization: Bearer arrr_….

Each file is named after what it says (hey-wanna-drink.mp3), and the zip carries a manifest.json mapping every file to its text, language, character count and cost - load that in your game rather than guessing filenames. Names are ASCII, so a set unpacks the same everywhere; the manifest keeps the original text.

you can pass
textone line. The response is the audio file itself
phrasesup to 60 lines. The response is a zip of them plus manifest.json
formatmp3 (default, 44.1 kHz) or wav (16-bit PCM, 44.1 kHz)
languageleave it out and it is detected; pass en, ko, … to force it
voiceId ttsModel speed emotionoverride the NPC's own settings for this render only
filenamewhat a single line downloads as

Lines go through the same normalisation a spoken reply does, so "idk" is read as "I don't know" rather than spelled out. Language is detected from the set rather than from each line: three words carry almost no signal - "hey, wanna drink?" on its own reads as Dutch to any detector - while a set of barks read together is unmistakable. A line in a script that names its own language (Korean, Japanese, Chinese, Hindi, Russian) keeps it regardless, so one Korean bark among English ones is safe.

Rendering is metered exactly like a call - characters synthesised, at the same rate - and shows up in the console's NPCs list under Phrases. Playing the file afterwards costs nothing, forever, which is the point.

Pricing

You pay for conversation, not capacity. NPCs are configuration, not servers: a hundred idle NPCs cost nothing.

meteredwhat it covers
Speech inseconds of audio transcribed
Replytokens in and out of the model you chose
Voice outcharacters synthesised, whether spoken live or rendered to a file
NPCsfree, however many you keep
Players rememberedfree, however long the bond lasts

Every call itemises its own usage, so a bill is always explainable down to the individual call. The console's NPCs list is that table: one row per NPC, with players, calls, turns, phrases, speech, tokens and cost side by side. Click a row to open it.

Automating it: account tokens

Under API in the console, create an account token (arrr_acct_…). It does what your login does, over REST: apps, API keys, NPCs, node tokens. Give it to a script or an agent, keep it on a server or in that agent's environment, and revoke it on its own when you are done. It is not an app key: an app's arrr_… key ships inside a game and only speaks for that app; an account token speaks for your whole account.

bash
curl -X POST https://cloud.arrr.fun/api/npcs \
  -H "Authorization: Bearer arrr_acct_..." -H "Content-Type: application/json" \
  -d '{"slug":"bartender","name":"Mo","persona":"You are Mo, the bartender on the rooftop...",
       "greeting":"What can I get you?","primer":[["hey","Evening. What can I get you?"]]}'

Reference

All endpoints take and return JSON. Console endpoints accept a session token from sign-in or an account token; game endpoints accept an app's API key (Authorization: Bearer arrr_…, an x-api-key header, or apiKey in the body) or the owner's token.

Your NPCs (session or account token)

GET /api/npcsyour NPCs with players, calls, minutes and cost, plus your apps
POST /api/npcscreate: slug, name, persona, background, greeting, primer ([[player, reply], …]), phrases (lines it says without being asked), voiceId, ttsModel, speed (0.6 to 1.5), emotion (["curiosity:high"]), sttModel, llmModels (up to 3), temperature, topP, maxTokens
GET /api/npcs/:idone NPC, your apps, and the service defaults an unset field falls back to
PUT /api/npcs/:idedit any of the fields above; the rest keep their values
DELETE /api/npcs/:idarchive; calls and memory are kept
GET /api/npcs/:id/playersits interactions: everyone who has talked to it, through which app, and what it remembers about them
POST /api/npcs/:id/tokena ticket to call it yourself, from anywhere, with no app - what the sandbox uses
POST /api/npcs/:id/speaktext in, audio out: { text } returns one file, { phrases: […] } a zip of them. See pre-recorded phrases
GET /api/npc-usageusage per NPC across every app
GET /api/npc/catalogvoices, models, the speed range, emotion names
GET /api/npc/healthwhether the service can place calls
GET POST DELETE /api/account-tokensyour account tokens

What a game does with an app's key

GET /api/apps/:appId/npcsyour NPCs, public fields: id, slug, name, greeting, voiceId, phrases
POST /api/apps/:appId/npcs/:slug/token{ playerId, displayName?, ttlSeconds? }{ token, websocketUrl, npcId, npcSlug, npcName, greeting, expiresIn }
POST /api/apps/:appId/npcs/:slug/speakthe same rendering with the app's key, metered against that app
wss://cloud.arrr.fun/ws/npc?token=…the call; the messages are listed above

Errors come back as { "error": "why" } with the usual status: 400 for bad input, 401 for a missing or wrong credential, 404 for an NPC that is not yours, 409 for a slug you already use, 429 for too many tickets, 503 when the service has no vendor keys.

Limits

An app holds up to 25 calls at once by default, may mint 600 tickets a minute, and may render 60 batches of phrases a minute - up to 60 lines, 1000 characters a line and 12000 characters a batch. Ask if you need more.