← On the Bonfire
API

Drive On the Bonfire from your own code

The one metered task, flip, is available over HTTP. It takes 2 to 6 small rules you live by and, for each, what you fear would happen if you broke it, plus the no-exceptions words the page found. For each rule it returns the rule's exact opposite (the flip), what that flip would break using only your stated fear, two scores from 0 to 3 and a verdict (burn, keep or trim, with a smaller version when trimmed), then one small first experiment and a tally - or a polite decline when a rule is about health, safety, the law, money owed, food or someone in your care.

The division of labour first: the page reads each rule's wording (no-exceptions words, how tightly it is worded, whether a fear is named) and applies a fixed sorting rule; the model flips each rule and scores it from your fear and the rule's own words; the page then re-checks every verdict against the model's own scores, the rule count, the smaller versions, the tally and the word limits. Build rules, fears and absolutes the same way (see "Build the input yourself") so the lines match.

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"on-the-bonfire"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field (task, count, rules, fears, absolutes) is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: flip-testing a pile is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field, the status and the verdicts

Every run sends task: flip (the one task is flip); a missing or different one is still answered as flip. status is declined when any rule or fear is about medicine or health treatment, safety, the law, money owed, food, eating or body weight, or the care of a child, a pet or another person. Otherwise each rule gets two whole-number scores: breaks (0 to 3, judged only from its fear - none stated or only about looks, feelings or others' opinions is 0) and costs (0 to 3, from the rule's own words, plus 1 when it allows no exceptions). The verdict is checked in this order, stopping at the first match: burn (breaks is 0), keep (breaks is at least costs), trim (every other case).

Input (every field is a string)

FieldRequiredWhat it holds
taskyesAlways flip.
countyesHow many rules, 2 to 6, as digits.
rulesyesNumbered lines, one per rule, each at least three words and at most 90 characters - for example 1. Always make the bed before breakfast.
fearsyesNumbered lines, one per rule, giving what you fear if you broke it, or none stated - for example 1. none stated.
absolutesyesNumbered lines n. word, word with the no-exceptions words the page found in a rule (always, never, every, only and the like), or none. A trimmed rule's smaller version must drop every one of them.
retry_notenoOnly on the page's one reformat retry, after an unusable reply.

Reply (one JSON object)

{"task": "flip", "status": "ok", "reason": "",
 "rules": [{"n": 1, "rule": "Always make the bed before breakfast", "flip": "...", "what_breaks": "...", "breaks": 0, "costs": 2, "verdict": "burn", "smaller": "", "why": "..."}, ...],
 "first_flip": "...", "closing": "..."}
FieldWhat it holds
status, reasonok or declined; for declined, one sentence of at most 25 words, rules empty and first_flip and closing empty.
rulesExactly count entries in input order. Each: n, rule (copied), flip (at most 14 words), what_breaks (at most 25 words), breaks and costs (whole numbers 0 to 3), verdict, smaller (at most 16 words, only for trim) and why (at most 28 words).
first_flipOne small experiment, at most 30 words: break the first burned rule once, or try the first trimmed rule's smaller version; empty when every rule is kept.
closingOne sentence of at most 20 words giving the count of each verdict, as digits.

The page then checks the reply (FireKit.check) and lists any disagreement under "The page's check".

Worked examples

These are the four free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover every outcome - household quirks, desk habits and weekend rules (between them burn, keep and trim) and a pile with a health rule (declined).

Household quirks

Input:

{
 "task": "flip",
 "count": "4",
 "rules": "1. Always make the bed before breakfast\n2. Never leave dishes in the sink overnight\n3. Fold every towel in thirds before it goes in the cupboard\n4. Put my keys in the bowl by the door the moment I get home",
 "fears": "1. none stated\n2. the pans get crusted and take ages to scrub\n3. the cupboard looks messy\n4. I lose them and miss the train",
 "absolutes": "1. always\n2. never\n3. every"
}

Reply:

{
 "task": "flip",
 "status": "ok",
 "reason": "",
 "rules": [
  {
   "n": 1,
   "rule": "Always make the bed before breakfast",
   "flip": "Never make the bed before breakfast.",
   "what_breaks": "This breaks nothing you named, since no fear was given for this rule.",
   "breaks": 0,
   "costs": 1,
   "verdict": "burn",
   "smaller": "",
   "why": "Keeping this costs a small moment of effort each time, while breaking it breaks nothing you named."
  },
  {
   "n": 2,
   "rule": "Never leave dishes in the sink overnight",
   "flip": "Always leave dishes in the sink overnight.",
   "what_breaks": "The flip would leave pans crusted, though the scrubbing could be made up afterwards.",
   "breaks": 1,
   "costs": 2,
   "verdict": "trim",
   "smaller": "Rinse just the pans; other dishes can wait overnight.",
   "why": "Keeping this costs a small nightly task with no exceptions, while the flip only brings extra scrubbing you can fix later."
  },
  {
   "n": 3,
   "rule": "Fold every towel in thirds before it goes in the cupboard",
   "flip": "Stuff every towel into the cupboard unfolded.",
   "what_breaks": "This breaks nothing you named, since the fear is only about how the cupboard looks.",
   "breaks": 0,
   "costs": 1,
   "verdict": "burn",
   "smaller": "",
   "why": "Keeping this costs a small folding step with no exceptions, while breaking it breaks nothing you named beyond how things look."
  },
  {
   "n": 4,
   "rule": "Put my keys in the bowl by the door the moment I get home",
   "flip": "Never put your keys in the bowl when you get home.",
   "what_breaks": "The flip would mean losing your keys and missing the train.",
   "breaks": 2,
   "costs": 0,
   "verdict": "keep",
   "smaller": "",
   "why": "Keeping this costs only a small moment of effort, while the flip risks losing your keys and missing the train."
  }
 ],
 "first_flip": "Try breaking 'make the bed before breakfast' once, on purpose, the next time it comes up, and notice what actually happens.",
 "closing": "2 rules go on the bonfire, 1 stays on the woodpile and 1 is trimmed to kindling."
}

Desk habits

Input:

{
 "task": "flip",
 "count": "4",
 "rules": "1. Reply to every email within an hour, even after work\n2. Never take a lunch break away from my desk\n3. Back up my laptop every Friday afternoon\n4. Only write notes in blue ink",
 "fears": "1. people will think I am slow\n2. I fall behind on my list\n3. I lose months of work if it dies\n4. none stated",
 "absolutes": "1. every\n2. never\n3. every\n4. only"
}

Reply:

{
 "task": "flip",
 "status": "ok",
 "reason": "",
 "rules": [
  {
   "n": 1,
   "rule": "Reply to every email within an hour, even after work",
   "flip": "Never reply to an email within an hour, even after work.",
   "what_breaks": "This breaks nothing you named, since the fear is only about what other people might think.",
   "breaks": 0,
   "costs": 3,
   "verdict": "burn",
   "smaller": "",
   "why": "Keeping this costs real time and effort with no exceptions, even outside work, while breaking it breaks nothing you named."
  },
  {
   "n": 2,
   "rule": "Never take a lunch break away from my desk",
   "flip": "Always take your lunch break away from your desk.",
   "what_breaks": "The flip would mean falling behind on your list, though you could catch up afterwards.",
   "breaks": 1,
   "costs": 3,
   "verdict": "trim",
   "smaller": "Stay at your desk for lunch most days.",
   "why": "Keeping this costs a chunk of your day with no exceptions, while the flip only risks falling behind, which you could fix later."
  },
  {
   "n": 3,
   "rule": "Back up my laptop every Friday afternoon",
   "flip": "Never back up your laptop, not even Fridays.",
   "what_breaks": "The flip would mean losing months of work, which cannot be undone.",
   "breaks": 3,
   "costs": 2,
   "verdict": "keep",
   "smaller": "",
   "why": "Keeping this costs a little time each Friday with no exceptions, while the flip could mean losing months of work for good."
  },
  {
   "n": 4,
   "rule": "Only write notes in blue ink",
   "flip": "Write your notes in any ink but blue.",
   "what_breaks": "This breaks nothing you named, since this rule has no stated fear.",
   "breaks": 0,
   "costs": 1,
   "verdict": "burn",
   "smaller": "",
   "why": "Keeping this costs only a small choice of pen with no exceptions, while breaking it breaks nothing you named."
  }
 ],
 "first_flip": "Try letting one email wait past the hour, even after work, once on purpose, the next time it comes up, and notice what happens.",
 "closing": "2 rules go on the bonfire, 1 is trimmed to kindling and 1 stays on the woodpile."
}

Weekend rules

Input:

{
 "task": "flip",
 "count": "4",
 "rules": "1. Finish every book I start, however dull\n2. Never buy the cheaper brand of anything\n3. Arrive at least twenty minutes early to everything\n4. Water the houseplants every Sunday morning",
 "fears": "1. I feel like a quitter\n2. cheap things break and I end up buying twice\n3. I miss the start of things\n4. they dry out and die",
 "absolutes": "1. every\n2. never, anything\n3. everything\n4. every"
}

Reply:

{
 "task": "flip",
 "status": "ok",
 "reason": "",
 "rules": [
  {
   "n": 1,
   "rule": "Finish every book I start, however dull",
   "flip": "Never finish a book you start, however dull.",
   "what_breaks": "This breaks nothing you named, since the fear is only about how you'd feel about yourself.",
   "breaks": 0,
   "costs": 3,
   "verdict": "burn",
   "smaller": "",
   "why": "Keeping this costs real reading time with no exceptions, while breaking it breaks nothing you named beyond how you'd feel."
  },
  {
   "n": 2,
   "rule": "Never buy the cheaper brand of anything",
   "flip": "Always buy the cheapest brand of everything.",
   "what_breaks": "The flip would mean buying cheap things that break and cost you twice.",
   "breaks": 2,
   "costs": 3,
   "verdict": "trim",
   "smaller": "Buy the better brand most of the time.",
   "why": "Keeping this costs more money on every purchase with no exceptions, while the flip only risks buying twice, which costs extra later."
  },
  {
   "n": 3,
   "rule": "Arrive at least twenty minutes early to everything",
   "flip": "Arrive at least 20 minutes late to everything.",
   "what_breaks": "The flip would mean missing the start of things you go to.",
   "breaks": 2,
   "costs": 3,
   "verdict": "trim",
   "smaller": "Arrive twenty minutes early most of the time.",
   "why": "Keeping this costs real time before everything with no exceptions, while the flip only risks missing the start of things."
  },
  {
   "n": 4,
   "rule": "Water the houseplants every Sunday morning",
   "flip": "Never water the houseplants, not even Sunday mornings.",
   "what_breaks": "The flip would mean the houseplants dry out and die.",
   "breaks": 2,
   "costs": 2,
   "verdict": "keep",
   "smaller": "",
   "why": "Keeping this costs a little time each Sunday with no exceptions, while the flip risks the houseplants drying out and dying."
  }
 ],
 "first_flip": "Try putting down a dull book before finishing it, once on purpose, the next time it comes up, and notice what happens.",
 "closing": "1 rule goes on the bonfire, 2 are trimmed to kindling and 1 stays on the woodpile."
}

A pile with a health rule - declined

Input:

{
 "task": "flip",
 "count": "2",
 "rules": "1. Never skip a dose of my blood pressure medicine\n2. Always iron my shirts on Sunday night",
 "fears": "1. my blood pressure goes up\n2. none stated",
 "absolutes": "1. never\n2. always"
}

Reply:

{
 "task": "flip",
 "status": "declined",
 "reason": "This page never flips rules about medicine or health treatment, so please keep following this one or ask a professional.",
 "rules": [],
 "first_flip": "",
 "closing": ""
}

Build the input yourself

Load the engine in node (global.window = global; require("./fire.js"); require("./fire-svg.js")), then:

const K = FireKit, D = FireDraw;
const built = K.buildInput({ rules: [{ text: "Always make the bed before breakfast", fear: "", guess: "keep" }, { text: "Put my keys in the bowl by the door", fear: "I lose them", guess: "keep" }] });
built.input.absolutes;                   // "1. always" - the no-exceptions words the page found
built.parsed.rules.map(x => x.lean);     // ["burn", ""] - no fear named leans toward the fire
const wire = K.toWire(built.input);      // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes           // the page's re-check of the reply
// K.verdictOf(0, 2)                     // "burn" - the page's own sorting rule
// K.score(built.parsed, reply)          // your hunches: { called, right, open }
// D.svg({ rules: reply.rules.map(r => ({ n: r.n, text: r.rule, verdict: r.verdict })) })   // the bonfire drawing
// K.md(built, reply)                    // the sorted pile as Markdown

Step 1 — a tiny client helper

One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.

BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here"   # from https://on-the-bonfire.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
  curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
    ${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}

Step 2 — a token

A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.

# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"on-the-bonfire"}' | jq -r '.data.token')

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "flip", "count": "2", "rules": "1. Always make the bed before breakfast\n2. Only write notes in blue ink", "fears": "1. none stated\n2. none stated", "absolutes": "1. always\n2. only"} - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses on-the-bonfire:<task>:<inputHash>:a<attempt> (the one task is flip), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="on-the-bonfire:flip:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
  J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
  case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json   # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}' 

Step 6 — POST /run-stream

The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.

# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
  -H "Idempotency-Key: on-the-bonfire:flip:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json