← Dystopia Generator / API
Tokens SkillSafe

Driving Dystopia Generator from your own code

Everything the web app does goes through one public HTTP API. The base URL is https://api.skillsafe.ai/v1/app-api, and every call carries Authorization: Bearer <token>. Get a token on the tokens page.

This app has one contract, not a set of lanes. mode decides whether you are building a new world or continuing an existing one, and every mode returns the same dossier object.

The envelope

Every response is {"ok": true, "data": {...}} or {"ok": false, "error": {"code": "...", "message": "..."}}. Check ok before reading data.

Error codes you will actually meet

CodeHTTPWhat it means
UNAUTHORIZED401Missing, malformed or expired token. Mint a new one.
INSUFFICIENT_CREDITS402The balance cannot cover the hold. /estimate is free — call it first and compare against /me.
VALIDATION_ERROR400The body was not the shape above. error.details names the field.
RATE_LIMITED429Back off and retry. Do not tight-loop.
JOB_FAILED500The run reached a terminal failure. The job object carries the reason.

The input contract

These are the exact fields app.js submits. brief and facts are produced by the free client-side scenario engine; if you are calling the API directly you write them yourself, and they are how you steer the output away from the genre default.

FieldTypeMeaning
modestring, requiredOne of base, deepen, era, reroll. All four return the same object shape — this is a parameter of one contract, not a lane selector. base builds a new world; the other three continue an existing one and require parent and directive.
premisestring, optionalThe user’s own idea or constraints, in their words. Clipped to 2,000 characters from the end, keeping the opening, with a marker appended naming what was cut.
briefstring, requiredThe seven axes rendered as prose, plus the naming register, any stated tensions, and the distance from the genre default. This is what actually steers the world. Axes marked [LOCKED by the user] are binding.
factsobject, requiredThe same axes as machine-readable ids — facts.axes carries optimizes, provides, belief, tech, ruler, control and scale — plus register, seed, divergence, locked and tensions. The model echoes these ids back in honoured_axes, and the app compares the two.
tonestring, optionalplain, documentary, literary or wry. The register of the writing, not of the regime.
lengthstring, optionalbrief, full or deep. Controls how much material comes back, not how many sections.
parentstring, non-base modesA compact recap of the world being continued: name, slogan, premise, how it took power, ideology, its exact vocabulary, what is scarce, how dissent is handled, its fault lines and its named people. Budgeted to about 4,200 characters and clipped on meaning — identity and vocabulary survive, atmosphere is dropped, and what was cut is stated.
directivestring, non-base modesWhat to change or expand. Generated by the app from the mode and the user’s choice.
facetstring, deepen onlyWhich part to expand: resistance, district, day, economy, law, children, border, faith.
era_offsetnumber, era onlyYears to move, negative for earlier. One of -100, -25, 10, 25, 100, 300.
reroll_axisstring, reroll onlyWhich axis id was turned.
retry_notestring, optionalPresent only on the app’s automatic reformat retry, describing how the previous reply failed to parse. The retry reuses an idempotency key derived from the same input, so it cannot double-bill.

The output contract

The model returns one JSON object and nothing else, as the output.output string of the job. name and premise are the only required fields; every array may be empty. The app’s parser tolerates a code fence and leading prose, and repairs a truncated object by walking bracket depth, so a stream that dies mid-dossier still renders what arrived.

{
  "name": "The Standing Register",
  "shortName": "the Register",
  "tagline": "Nothing that happened is allowed to stop happening.",
  "premise": "2-4 sentences orienting the reader.",
  "seizure":  { "title": "", "years": "", "account": "" },
  "ideology": { "creed": "", "logic": "", "enemy": "" },
  "lexicon":  [ { "term": "", "gloss": "", "plain": "" } ],
  "daily":    { "morning": "", "work": "", "evening": "", "law": "" },
  "scarcity": [ { "item": "", "why": "", "workaround": "" } ],
  "dissent":  { "posture": "", "apparatus": "", "fate": "", "story": "" },
  "cracks":   [ { "crack": "", "who": "", "pressure": "" } ],
  "figures":  [ { "name": "", "role": "", "note": "" } ],
  "hooks":    [ "" ],
  "sensory":  [ "" ],
  "honoured_axes": {
    "optimizes": "remembrance", "provides": "quiet", "belief": "grateful",
    "tech": "engineered", "ruler": "drift", "control": "informants", "scale": "fleet"
  },
  "caution": "present only when a content limit changed what was asked for"
}

honoured_axes is the reconciliation hook: it echoes back the seven axis ids the model believes it honoured, and the app diffs that against what it sent. A mismatch is surfaced to the user rather than silently accepted.

1. Get a token

Open the tokens page, sign in, and copy the token or the ready-made shell export. A guest token works for browsing but cannot run the model: runs are metered and need an account.

2. Check who you are

/me returns exactly three fields: subject_type, subject_id and credits. A personal token reports subject_type: "user"; that is the only signed-in test there is.

3. Price the run — free, no job

/estimate costs nothing and creates no job. It returns hold_credits (what will be reserved), min_credits, model, model_alias and markup_bps. Present the hold as reserved, never as the price — the actual charge is usually far lower, because the hold prices the full output cap.

4. Run it, and poll

Send an Idempotency-Key header on every run. Deriving it from a hash of the input plus the mode plus an attempt counter means a network blip, or an automatic reformat retry, can never bill twice.

/run returns {"job_id": "job_..."}. Poll it until it reaches a terminal state; the dossier arrives as the output.output string.

5. Or stream it

/run-stream is the same body over SSE and is what the web app uses. The deltas arrive as the JSON object is written, which is what lets the progress card advance on real signals — it watches for the section keys appearing in the stream — rather than on a timer.

6. Worked example: iterating on a world

The second run is almost never “make another one”. It is “keep this and change one thing”. Set mode, pass the parent recap and a directive, and you get the same dossier shape back with continuity intact — the same regime name, the same vocabulary, the same people.

This example deepens the resistance. mode: "era" with an era_offset moves the same world through time instead; mode: "reroll" keeps the regime and swaps one axis.

Notes that will save you a round trip