← Life Simulator /API
Get a token

Driving Life Simulator from your own code

Every chapter of a life is one call. The app sends the whole life so far as a single string and gets back a chapter as plain labelled lines. There is no session and no server-side state: the request carries everything, which is what makes a chapter reproducible and a dropped connection free.

Base URL and the envelope

All calls go to https://api.skillsafe.ai/v1/app-api and every response is wrapped:

{ "ok": true,  "data": { ... },  "meta": { "request_id": "req_...", "timestamp": "..." } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 400, "details": {} } }
CodeStatusWhat to do
unauthorized401Token missing, expired, or a guest token on a metered call. Mint a new one.
insufficient_credits402Balance is below min_credits. Top up.
validation_error400Body is not an object, or the field is missing.
rate_limited429Back off and retry. Never tight-loop.
not_found404Wrong slug or a job id that never existed.

1. The input shape

Exactly one field: chapter, a string holding the whole life so far. It must be a JSON object{"chapter": "..."}. Note that POST /estimate does no body validation whatsoever: a bare string, a number and null all return ok: true with a correct-looking hold. So a successful estimate proves the model binding and nothing at all about your input shape. Check the shape yourself; nothing upstream will.

{
  "chapter": "[LIFE SIM | CHAPTER 4]\nMODE: CHAPTER\nREGISTER: Tender - ...\nAGE NOW: 27\nTHIS CHAPTER COVERS: ages 27 to 30 (3 years)\nTHE SIX MEASURES (0-100, ...):\n  - Money 20 ...\nOPEN THREADS (at least one of these must move this chapter):\n  - [yard_debt] ...\nDECISIONS ALREADY TAKEN (an ECHO must name one of these exact ages and no other):\n  - at 22 they signed for the bench\nWHAT THEY CHOSE AT 27: ...\nPRESSURE FOR THIS CHAPTER: ...\nCHANGE BUDGET: ...\nINSTRUCTION: ..."
}

The easiest way to get a real one is to play a chapter in the app and use State as JSON; the app builds the same string from that state on every call.

2. The reply contract

Plain labelled lines. CHAPTER and CLOSE may span several lines; every other label is one line, with | between fields.

SPAN: 27 to 30
CHAPTER: <prose, several paragraphs>
ECHO: <age> | <what that decision is costing or paying now>
THREAD: <thread id> | advanced|resolved|worsened|steady | <one sentence>
THREAD-NEW: <short_id> | <what is now unfinished> | low|medium|high
PERSON: <role> | <Name> | met|closer|strained|parted|steady|died | <one clause>
DELTA: money|health|standing|ties|craft|spirit | +N or -N | <short reason>
HEADLINE: <one line about the wider world>
CROSSROADS: <the pressure that has built>
OPTION: a | <what they would do> | <what it would mean>
OPTION: b | ...
OPTION: c | ...
CLOSE: <closing mode only>
LEFT: <closing mode only>

THREAD, THREAD-NEW, PERSON, DELTA and OPTION repeat. Everything else appears at most once. The client validates every proposal before applying it: an ECHO naming an age at which nothing was decided is refused, a DELTA rise above the stated budget is capped, and a PERSON who already parted does not come back.

3. A token

Use the token page, or mint a guest token with POST /v1/app-api/guest. A guest can call /me and /estimate; /run needs a signed-in user.

4. Check the session

GET /me returns exactly three fields: subject_type, subject_id and credits. There is no username, email or name. Signed in means subject_type === "user".

5. Price it first — free

Returns model, model_alias, markup_bps, hold_credits and min_credits. The hold is what is reserved, priced at the full output cap; the settled charge is usually well under it.

6. Run a chapter

Returns { "job_id": "..." }. Poll GET /jobs/{id} until it is terminal. Always send an Idempotency-Key derived from the envelope plus an attempt counter — a reformat retry must reuse the key so a malformed first reply cannot bill twice.

7. Stream it instead

POST /run-stream returns Server-Sent Events. The wire format is one event: line, one data: line, and a blank line terminating the frame — not a type field inside the data object:

event: job
data: {"job_id":"job_..."}

event: delta
data: {"text":"CHAPTER: The yard took them on in March"}

event: done
data: {"job_id":"job_...","output_text":"...","charged_credits":812}

Event names are job, delta, done, pending and error. A parser written against a {"type":"delta"} shape never fires; that format does not exist on this platform.

What the app does that you will have to do too