Staging keys and repeatable evaluation
The demo sandbox key is great for a first copy-paste, but it is the wrong tool for an eval loop: it is rate-limited at issuance, capped at 150 messages, blocks images + managed delivery, and expires. To evaluate a character repeatedly — across prompt tweaks, model changes, or regression runs — you want a persistent staging key with real limits, plus a pattern that gives every run an identical clean starting state.
This page covers both: getting a durable dev key (instead of fighting the demo rate limit), and the fresh-session pattern that makes repeated evals deterministic.
Staging key vs demo sandbox key
There are two ways to get a key, and they are for different jobs.
The demo key’s issuance rate limit is exactly what blocks an eval loop: re-minting a fresh
demo key for each run trips 429. A staging key is issued once and reused, so the
rate limit never enters your loop. Use
Self-serve access to create
or select a workspace, approve your coding agent, create a staging app, and issue a
runtime key for that app.
Get a persistent staging key
For a dedicated staging key, use the self-serve account flow:
a human approves the coding agent, the agent creates a staging app through
/v1/account/*, and the agent issues a runtime API key for that app.
Account-plane routes are guide-documented, not part of the generated runtime API
Reference/SDK yet; this keeps normal runtime integrations focused on app keys and
/v1/* Character OS calls. Once you have the runtime key, confirm it resolves with
GET /v1/me, which returns the app, environment, and tenant it maps to.
Keep LOREOS_STAGING_KEY server-side like any other LoreOS key — it can create state and
trigger metered model work.
Raise the limits past the demo caps
A demo key is capped at 150 messages — enough for several real multi-turn conversations, but still tight
for a real eval suite (and it can’t generate images). A staging key uses your app
cap instead. Set the cap high enough for one full suite run in the console/account
plane when enabled, or through the preview provisioning channel. Then monitor settled
usage with GET /v1/usage and rate/cap behavior with GET /v1/rates.
Per-end-user caps are public and can be set with
POST /v1/external-users/{external_user_ref}/budget-policy, but most eval loops use a fresh
end-user per run and rely on the app cap.
When a run does hit the cap, the send returns 402 budget_exceeded before spending on
the model, so a runaway eval fails safely and cheaply rather than burning credits.
Deterministic repeated evals: a fresh session per run
The key to reproducible evals is controlling starting state. A LoreOS character accumulates relational and world-model state per end-user as a conversation progresses — so two runs over the same session are not comparable: the second run starts where the first left off.
The clean pattern is the opposite of resetting: for each eval run, create a fresh
session with a fresh external_user_ref. A brand-new external_user_ref has no prior
relational or world-model state, so every run begins from an identical clean slate with
no shared state between runs.
Use a new external_user_ref per run (a timestamp or UUID), not just a new session for the
same user. A fresh session already gives the character a clean relational starting point,
but world-model and memory state accrue against the end-user — so a brand-new
external_user_ref is what guarantees nothing (relational, world-model, or memory)
carries over from a previous run.
Fresh user, session reset, and memory deletion are different operations
LoreOS does have POST /v1/sessions/{session_id}/reset, but it does not erase the old
session or the end-user’s memories. It creates a new session for the same character and
external user, preserves the previous session for history/audit, and can move an existing
channel binding to the new session. That makes reset useful for continuing the same user in
a new room, not for proving an isolated regression test.
For repeatable regression tests, keep using a fresh unique external_user_ref. Use reset only
when the test is explicitly about same-user continuity or channel rebinding.
Default path: built-in Conversation Eval
For persona, voice, boundary, grounding, recall, and relationship-pacing regression, start with the built-in eval operation. It creates an isolated sandbox session against the real reply engine, runs the scripted turns, stores the transcript, evaluates deterministic assertions, and optionally runs the tone/relationship-pacing judge.
The create transaction pins character_version_id and character_version_number. The generated
session, every reply, deterministic checks, and judge use that immutable version even if the active
character version changes while the eval is running. status is terminal only at done or
failed; the presence of created_at, session_id, or a partial transcript is not completion.
Conversation Eval is metered because it invokes the real reply engine and, when tone or pacing rubrics are present, an LLM judge. The report is an app-scoped persisted eval artifact containing the synthetic transcript; do not put customer/private transcripts into a regression eval without the same consent, access, retention, and deletion policy as the source data.
Use the manual fresh-session loop below only when you need event-level assertions, exact delivery timing, webhook/Telegram adapter behavior, or UI rendering proof that the built-in eval does not model.
Manual path: a worked session eval loop
Put it together: get and reuse the staging key once (above), then per eval — create a fresh session, send your scripted messages, poll for the replies, assert, and discard the session.
What makes this reproducible:
- The key is minted once, so you never hit the demo issuance rate limit mid-suite.
- Each run gets a fresh
external_user_ref, so there is no shared relational or world-model state — every run starts identically. - Limits come from your app’s budget policy, so volume isn’t capped at the demo’s twenty
messages; raise
limit_creditsto cover a full run. - Replies are asynchronous, so each assertion waits on the event log for that turn’s
message.created— assert onpayload.bubbles, not on thePOST /messagesresponse.
Notes for stable evals
- Pass the structural gate before judging semantics. A minimal smoke-test character
(display name + one bio line) produces flat, less differentiated replies, which makes
assertions noisy. Use
POST /v1/characters/validate(dry-run: schema + presence checks, without persisting) to reachauthoring_readiness.status = "ready", then run Conversation Eval.readyandauthoring_quality_scoredo not mean the persona or voice is good;semantic_quality_evaluatedisfalseuntil you run and review conversation outcomes. - Reply variance is expected. LoreOS replies are model-generated and intentionally lifelike, so two runs of the same script won’t be byte-identical even from an identical clean slate. Assert on properties (a fact recalled, a tone, the absence of forbidden content, relationship pacing) rather than exact strings.
fastmode keeps accuracy. Evaluating withreply_mode: "fast"(the default) only skips the advisory voice-quality critic; grounding, safety, and accuracy are unchanged, so it is a faithful target for accuracy/behavior assertions. Usedeeponly if your eval is specifically grading voice polish.- Per-end-user caps aren’t needed for eval. Because each run uses a throwaway
external_user_ref, the app-level budget policy is sufficient. (If you do want to cap a single end-user in production, that isPOST /v1/external-users/{ref}/budget-policy, not the app-level route above.)