Documentation
AI characters in a scene
Ask a character for a live reply mid-scene. Every reply is a paid model call.
A novel can stop mid-scene, send the player's line to a model, and display what the character answers. The reply arrives in a normal Ren'Py variable, so the rest of the scene is written the way any other scene is written.
Each live reply is one real model call billed to the signed-in player after they approve the in-game cost dialog: the provider's cost for the tokens actually used, plus 20%, rounded up to whole credits (1 credit = $0.01). Guests receive the authored offline fallback. Nothing about a live call is free, and a loop that asks in a while block spends on every pass.
In the editor the Cast panel on the right is where you add those characters (game/ai-cast.json). The script still uses the ai statement below.
Install the SDK in the engine
The statement lives in the renpy-js package @renpyjs/ai. The parser must know the statement before a script is compiled, and the runtime must have the handler installed before the scene runs.
// once, where the player is created
import { installAI, registerAIStatement } from '@renpyjs/ai';
registerAIStatement();
const ai = installAI(controller.runtime, {
project: 'vnp_your_project_id',
characters: [{
id: 'mara',
name: 'Mara',
persona: 'Night nurse on ward nine. Tired, blunt, kind under it.',
greeting: 'You again.',
fallback: 'Mara says nothing.',
}],
});
A scene where the character answers freely
ai <character> <expression> as <variable> sends the expression as the player's line and writes the reply into the variable. The variable holds text that is already escaped, so a reply can never inject Ren'Py text tags.
define mara = Character("Mara")
label ward_nine:
scene bg ward
show mara neutral
mara "You again. Ask, then let me work."
$ question = renpy.input("Ask Mara anything", length=200)
ai mara question as mara_reply
if _ai_ok:
mara "[mara_reply]"
else:
mara "..."
"She is not listening. ([_ai_error])"
menu:
"Ask something else":
jump ward_nine
"Let her work":
jump corridor
What the endpoint does
POST /api/visualnovel/ai/character takes project, either an inline persona (character, name, persona, greeting, scenario) or netwrck_character naming one of your own netwrck characters, plus player_input, optional scene, history, model, max_tokens and session. It answers with text, renpy_text, model, usage, cost, billed, latency_ms and job_id.
It requires a logged-in account with owner, public or valid share access to the project. The call is charged to that reader, never silently to the author. A referenced netwrck character still belongs to the author, and a character marked adult is refused with 403 unless the reader allows adult content. GET /api/visualnovel/ai/characters lists your own characters and the current limits.
The agent is pinned to grok-4.6 at low reasoning effort and billed in API credits on actual token usage.
curl -X POST https://netwrck.com/api/visualnovel/ai/character \
-H 'content-type: application/json' \
-d '{"project":"vnp_x","character":"mara","name":"Mara",
"persona":"Night nurse on ward nine.",
"player_input":"Do you trust the captain?"}'
Price
Base model price plus 20%, on the tokens the call actually used, rounded up to whole credits. The agent uses grok-4.6 at low reasoning effort; larger prompts and outputs cost more.
The charge is written to the visual novel billing ledger with the provider cost, the markup and the job id, and is visible at /api/visualnovel/billing/ledger. If a reply comes back empty or unusable after the provider was already paid, that ledger row is refunded exactly once.
Every ai statement spends credits. Test with mode: 'offline' or dry_run: true, both of which are free.
Failure, timeout, rollback and saves
A failed or timed out call writes the character's fallback line and sets _ai_ok to False and _ai_error to the reason. The scene keeps running; nothing throws and nothing stalls. The server's deadline is 55 seconds and the SDK waits 75, so a call the server charged for is never abandoned by the client first.
Every request carries a request_id. The same request_id inside ten minutes is answered from the server's stored reply with replayed: true, without a second model call and without a second charge, so a retry or a lost response cannot bill twice.
A paid reply blocks rollback past it, so a player cannot rewind and be billed twice for the same moment. Identical questions inside one playthrough are answered from a memo cache instead of a second call, which also means the same question in the same scene gives the same answer.
A save stores the reply like any other variable, so loading shows the same line without calling again. Conversation memory is per playthrough and capped at 12 turns server side, 6 exchanges client side by default.
Rate limits
One request per second per account with a burst of four, and at most two calls in flight per account. Player input is capped at 2 KB, the scene summary at 4 KB, the persona at 6 KB, output at 600 tokens, and a reply is trimmed to four lines and 900 characters.
There is no daily or per-project spending cap: the limits above shape the rate, the account balance is the ceiling. The SDK's maxCalls and budgetCredits options are the per-playthrough guard, and they are client side.
Netwrck