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": {} } }
| Code | Status | What to do |
|---|---|---|
unauthorized | 401 | Token missing, expired, or a guest token on a metered call. Mint a new one. |
insufficient_credits | 402 | Balance is below min_credits. Top up. |
validation_error | 400 | Body is not an object, or the field is missing. |
rate_limited | 429 | Back off and retry. Never tight-loop. |
not_found | 404 | Wrong 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".
curl -s https://api.skillsafe.ai/v1/app-api/me \ -H "Authorization: Bearer YOUR_TOKEN"
import json, urllib.request
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/me",
headers={"Authorization": "Bearer YOUR_TOKEN"})
print(json.load(urllib.request.urlopen(req))["data"])
const r = await fetch("https://api.skillsafe.ai/v1/app-api/me", {
headers: { Authorization: "Bearer YOUR_TOKEN" }
});
console.log((await r.json()).data);
req, _ := http.NewRequest("GET", "https://api.skillsafe.ai/v1/app-api/me", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
io.Copy(os.Stdout, res.Body)
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/me"))
.header("Authorization", "Bearer YOUR_TOKEN")
.build();
System.out.println(HttpClient.newHttpClient()
.send(req, HttpResponse.BodyHandlers.ofString()).body());
require "net/http"
uri = URI("https://api.skillsafe.ai/v1/app-api/me")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer YOUR_TOKEN"
puts Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/me");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer YOUR_TOKEN"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($ch);
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_TOKEN");
Console.WriteLine(await http.GetStringAsync("https://api.skillsafe.ai/v1/app-api/me"));
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.
curl -s https://api.skillsafe.ai/v1/app-api/estimate \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"chapter":"[LIFE SIM | CHAPTER 4]\nMODE: CHAPTER\n..."}'
import json, urllib.request
body = json.dumps({"chapter": envelope}).encode()
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/estimate", data=body,
headers={"Authorization": "Bearer YOUR_TOKEN",
"Content-Type": "application/json"})
print(json.load(urllib.request.urlopen(req))["data"]["hold_credits"])
const r = await fetch("https://api.skillsafe.ai/v1/app-api/estimate", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({ chapter: envelope })
});
console.log((await r.json()).data.hold_credits);
body, _ := json.Marshal(map[string]string{"chapter": envelope})
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/estimate", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
String body = mapper.writeValueAsString(Map.of("chapter", envelope));
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/estimate"))
.header("Authorization", "Bearer YOUR_TOKEN")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
req = Net::HTTP::Post.new(URI("https://api.skillsafe.ai/v1/app-api/estimate"))
req["Authorization"] = "Bearer YOUR_TOKEN"
req["Content-Type"] = "application/json"
req.body = { chapter: envelope }.to_json
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/estimate");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["chapter" => $envelope]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer YOUR_TOKEN", "Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($ch);
var body = new StringContent(
JsonSerializer.Serialize(new { chapter = envelope }),
Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/estimate", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
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.
curl -s https://api.skillsafe.ai/v1/app-api/run \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: life-4-a1b2c3" \
-d '{"chapter":"[LIFE SIM | CHAPTER 4]\nMODE: CHAPTER\n..."}'
import json, urllib.request
body = json.dumps({"chapter": envelope}).encode()
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/run", data=body,
headers={"Authorization": "Bearer YOUR_TOKEN",
"Content-Type": "application/json"})
print(json.load(urllib.request.urlopen(req))["data"]["job_id"])
const r = await fetch("https://api.skillsafe.ai/v1/app-api/run", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({ chapter: envelope })
});
console.log((await r.json()).data.job_id);
body, _ := json.Marshal(map[string]string{"chapter": envelope})
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
String body = mapper.writeValueAsString(Map.of("chapter", envelope));
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/run"))
.header("Authorization", "Bearer YOUR_TOKEN")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
req = Net::HTTP::Post.new(URI("https://api.skillsafe.ai/v1/app-api/run"))
req["Authorization"] = "Bearer YOUR_TOKEN"
req["Content-Type"] = "application/json"
req.body = { chapter: envelope }.to_json
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["chapter" => $envelope]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer YOUR_TOKEN", "Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($ch);
var body = new StringContent(
JsonSerializer.Serialize(new { chapter = envelope }),
Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/run", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
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.
curl -N https://api.skillsafe.ai/v1/app-api/run-stream \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"chapter":"[LIFE SIM | CHAPTER 4]\nMODE: CHAPTER\n..."}'
import json, urllib.request
body = json.dumps({"chapter": envelope}).encode()
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/run-stream-stream", data=body,
headers={"Authorization": "Bearer YOUR_TOKEN",
"Content-Type": "application/json"})
print(json.load(urllib.request.urlopen(req))["data"]["hold_credits"])
const r = await fetch("https://api.skillsafe.ai/v1/app-api/run-stream-stream", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_TOKEN",
"Content-Type": "application/json"
},
body: JSON.stringify({ chapter: envelope })
});
console.log((await r.json()).data.hold_credits);
body, _ := json.Marshal(map[string]string{"chapter": envelope})
req, _ := http.NewRequest("POST", "https://api.skillsafe.ai/v1/app-api/run-stream-stream", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("SKILLSAFE_TOKEN"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
String body = mapper.writeValueAsString(Map.of("chapter", envelope));
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create("https://api.skillsafe.ai/v1/app-api/run-stream-stream"))
.header("Authorization", "Bearer YOUR_TOKEN")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
req = Net::HTTP::Post.new(URI("https://api.skillsafe.ai/v1/app-api/run-stream-stream"))
req["Authorization"] = "Bearer YOUR_TOKEN"
req["Content-Type"] = "application/json"
req.body = { chapter: envelope }.to_json
$ch = curl_init("https://api.skillsafe.ai/v1/app-api/run-stream-stream");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["chapter" => $envelope]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer YOUR_TOKEN", "Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($ch);
var body = new StringContent(
JsonSerializer.Serialize(new { chapter = envelope }),
Encoding.UTF8, "application/json");
var res = await http.PostAsync("https://api.skillsafe.ai/v1/app-api/run-stream-stream", body);
Console.WriteLine(await res.Content.ReadAsStringAsync());
What the app does that you will have to do too
- Own the state. The model proposes changes; validate each one before applying it. Be generous about what the person already has and strict about a claimed gain.
- Keep the envelope bounded. The app renders, measures and degrades down a fixed ladder against a 5,200-character budget, and never degrades the traits, the measures, the open threads, the cast, the decision ages or the choice just made.
- Guard the output. The app scans every field of every reply, not just the prose, and withholds a chapter rather than softening it. See llms.txt for what it catches and, more usefully, what it measurably does not.