blog · Aug 22, 2026 · 3 min

Build an AI character chat app with the eroq API

A complete pattern for a character chat product — persona prompts, history windows, streaming UI, credit budgeting — with working code against the eroq API.


This is the tutorial we wish had existed when we shipped our first character product. One evening of work, one working pattern: persona in, streamed replies out, costs you can predict.

The architecture in one paragraph

Your backend owns the API key, the character sheets, and the conversation store. The client talks only to your backend. Every turn, you assemble [system] + [windowed history] + [new user message], send it to /v1/chat/completions, and stream the reply back down. The API is stateless — which means your product owns memory, and that is a feature.

1. The persona prompt

Keep conduct and identity separate. Identity is short; conduct is strict:

function personaPrompt(character) {
  return [
    `You are ${character.name}. ${character.oneLineIdentity}`,
    character.voiceNotes,                    // "dry humor, short sentences, hates small talk"
    'Stay fully in character. Never mention being an AI or add out-of-character notes.',
    'Actions in *asterisks*. Advance the scene, then hand it back.',
  ].join(' ')
}

Resist the lore dump. A 2,000-word backstory in the system prompt costs you drift, not depth — inject specific facts only when the scene touches them.

2. The turn, streamed

export async function characterReply(character, history, userMessage, onDelta) {
  const res = await fetch('https://eroq.ai/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.EROQ_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'eroq-rp-mini',                 // 1 credit; rp-plus where it matters
      stream: true,
      messages: [
        { role: 'system', content: personaPrompt(character) },
        ...history.slice(-30),               // the window — see below
        { role: 'user', content: userMessage },
      ],
    }),
  })

  const reader = res.body.getReader()
  const decoder = new TextDecoder()
  let buffer = ''
  let full = ''
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
    buffer += decoder.decode(value, { stream: true })
    const events = buffer.split('\n\n')
    buffer = events.pop() ?? ''
    for (const event of events) {
      const data = event.replace(/^data: /, '')
      if (data === '[DONE]') continue
      const chunk = JSON.parse(data)
      const delta = chunk.choices?.[0]?.delta?.content
      if (delta) { full += delta; onDelta(delta) }
    }
  }
  return full
}

Pipe onDelta to your UI over your own SSE or WebSocket. First tokens on screen in under a second is what makes a character feel present.

3. The history window

Send the last 20–40 turns, never everything. Older context adds drift faster than it adds memory. For long relationships, run a cheap summarization pass every N messages and prepend the summary as a system-adjacent note — one eroq-rp-mini call (1 credit) buys you a durable memory line.

Filter the window before sending: any turn where the model broke character should not be in the context you send back, or you will get more of it.

4. Costing it

The whole point of flat pricing is that this table exists before you launch:

User behavior Calls/day Model Credits/day
Casual (20 msgs) 20 rp-mini 20
Engaged (80 msgs) 80 rp-mini 80
Power + quality scenes 150 mix ~250

At pack rates (~$0.01/credit or less), an engaged user costs about $0.80/day of model spend. Price your subscription accordingly, and add images (10 credits) and voice lines as premium moments rather than defaults.

5. The two errors to handle well

  • 402 insufficient_credits — your wallet, not your bug. Alert yourself, top up, and degrade gracefully (queue the message, tell the user the character "stepped away").
  • 502 / stream error events — refunded automatically. Retry once with backoff; the user sees a typing indicator, not an apology.

Everything else in the error guide is standard.

Ship it

That is genuinely the whole pattern: a conduct prompt, a window, a stream, a budget. Get a key, paste section 2, and your first character is talking in the quickstart's five minutes — the free 50 credits cover the whole evening of testing.

Build with the models behind this post — get an API key (50 free credits).