← Blame the Thing
API

Drive Blame the Thing from your own code

The one metered task, diagnose, is available over HTTP. It takes one everyday thing that tripped you up - what it is, what you were trying to do, and what you did and what happened - and returns the five-row design diagnostic (signs, mapping, blocks, feedback, undo) with a note per row, a score out of ten, a verdict (blame the thing, a bit of both, or just a slip), the culprit row, whether it was a slip or a mistake, an embossed label to stick on the thing, and one note for you and one for whoever made it.

The division of labour first: the page listens; the model judges. The page reads your description for keyword cues, a possible problem per row, and sends them as cues: hints, never the verdict. The model judges every row from the description itself and applies fixed rules for the score, the verdict and the culprit; the page works them out again from the rows and re-checks the reply.

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": "..." } }

The token is minted for this app (the guest call takes {"slug":"blame-the-thing"}), 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, thing, goal, happened, often, others, cues) 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: the diagnosis is metered and needs a personal sign-in.
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 input and the rules

Every run sends task: diagnose (the one task is diagnose); a missing or different one is still answered as diagnose. All fields are strings.

FieldRequiredWhat it holds
taskyesAlways diagnose.
thingyesThe everyday thing, in your words (the page keeps up to 60 characters).
goalyesWhat you were trying to do (up to 160 characters).
happenedyesWhat you did and what happened (up to 600 characters; the page keeps the opening and the ending of a longer paste, with [...] in the middle).
oftenyesHow often it trips you up: first time, now and then, every time or not said.
othersyesWhether other people get it wrong too: just me, others too or not sure.
cuesyesThe page's keyword cues, one line per row it heard a possible problem in, row: "words heard" (for example discover: "no sign"), or none. Hints only: every row is judged from the description.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

The five rows, always in this order: discover (could you tell what to do and where?), mapping (do the controls line up with what they change?), constraints (were wrong moves blocked?), feedback (could you tell what happened, straight away?) and recover (could you undo a wrong move?).

The rules: a row's ok is false only when the description shows that problem; a row the description shows working, or says nothing about, is true. The score is 2 for every row that is true, so 0 to 10. Verdict, first match wins: score 4 or less, the-thing; else 8 or less, a-bit-of-both; else a-slip. The culprit is the first row in the order above that is false, or an empty string. The error type is mistake when your idea of how the thing works was off, otherwise slip.

The reply

One JSON object, no prose around it:

{"task": "diagnose", "headline": "...", "rows": [{"id": "discover", "ok": false, "note": "..."}, {"id": "mapping", "ok": true, "note": "..."}, ...], "score": 4, "verdict": "the-thing", "culprit": "discover", "error_type": "mistake", "error_why": "...", "label": "PUSH", "label_where": "...", "for_you": "...", "for_maker": "..."}
FieldWhat it holds
headlineOne sentence, at most 20 words, that fits the verdict.
rows[]Exactly five, in the order discover, mapping, constraints, feedback, recover, each with id, ok (true or false) and a one-sentence note that agrees with it.
scoreA number: 2 for every row whose ok is true.
verdictthe-thing, a-bit-of-both or a-slip.
culpritThe id of the first row that failed, or an empty string.
error_typeslip or mistake.
error_whyOne sentence: why it was a slip or a mistake, in your own words.
labelThe embossed label: at most 24 characters, in capitals, letters, digits, spaces and a few plain marks.
label_whereWhere to stick it, on or beside the part the culprit row is about.
for_youOne small thing to do differently next time with the thing as it is.
for_makerThe design change for whoever made or installed the thing.

The page reads the reply with ReplyKit.normalize, works out the score, the verdict and the culprit again from the rows (ReplyKit.settle) and shows its own, then checks the reply (ReplyKit.check) - a missing row makes the score provisional, a score, verdict or culprit that does not follow from the rows, no slip or mistake, a row without a note, a label tidied for embossing, a missing note, a row the page heard a cue in but the reply passed, numbers that are not in your description, the page's internal names, anything that judges you, contact details, and other people mentioned when you did not say others get it wrong - and lists anything it finds under "The page's check".

Worked examples - every verdict, slips and mistakes

These are the free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover every verdict and both kinds of error.

The glass door at work - blame the thing (4 out of 10, mistake)

Input:

{
 "task": "diagnose",
 "thing": "the glass door into the office",
 "goal": "Get through the door into the office in the morning",
 "happened": "It has the same big bar handle on both sides, so I pull it. It needs a push. There is no sign saying push or pull, and nothing stops me pulling. When I pull, the door just doesn't budge and gives no click or sign, so I can't tell if it is locked or if I am doing it wrong.",
 "often": "every time",
 "others": "others too",
 "cues": "discover: \"no sign\"\nconstraints: \"nothing stops\"\nfeedback: \"no click\""
}

Reply:

{
 "task": "diagnose",
 "headline": "The glass door into the office let you down, not the other way round.",
 "rows": [
  {
   "id": "discover",
   "ok": false,
   "note": "There is no sign saying push or pull, and the same bar handle sits on both sides."
  },
  {
   "id": "mapping",
   "ok": true,
   "note": "The description does not say whether the handle lines up with what it does to the door."
  },
  {
   "id": "constraints",
   "ok": false,
   "note": "Nothing stops you from pulling, even though the door needs a push instead."
  },
  {
   "id": "feedback",
   "ok": false,
   "note": "The door gives no click or sign when you pull, so you cannot tell if it is locked or wrong."
  },
  {
   "id": "recover",
   "ok": true,
   "note": "The description does not mention whether you could undo the pull or try again differently."
  }
 ],
 "score": 4,
 "verdict": "the-thing",
 "culprit": "discover",
 "error_type": "mistake",
 "error_why": "You wanted to get into the office, but you pulled the bar handle because it seemed to mean pull, when the door needed a push.",
 "label": "PUSH TO OPEN",
 "label_where": "Ask whoever manages the office first, then stick the label on the door above the handle.",
 "for_you": "Next time, try pushing the bar handle instead of pulling it, since pulling does not open the door.",
 "for_maker": "Add a sign on the door that says push or pull, so the handle's direction is clear before anyone pulls it."
}

The self-checkout - a bit of both (6 out of 10, slip)

Input:

{
 "task": "diagnose",
 "thing": "the self-checkout at the supermarket",
 "goal": "Pay for a few things without help",
 "happened": "I put my own bag on the bagging area before scanning anything, because that is what I do at the till. The screen kept saying unexpected item in the bagging area and would not let me scan. There was no message about where my bag should go, and in the end I had to wait for an assistant to come and reset it.",
 "often": "first time",
 "others": "not sure",
 "cues": "feedback: \"no message\"\nrecover: \"had to wait for an assistant\""
}

Reply:

{
 "task": "diagnose",
 "headline": "The self-checkout at the supermarket didn't show where your bag should go, and a familiar habit led you astray.",
 "rows": [
  {
   "id": "discover",
   "ok": false,
   "note": "You say there was no message about where your bag should go, so nothing showed you that before you put it down."
  },
  {
   "id": "mapping",
   "ok": true,
   "note": "You didn't mention anything about whether the self-checkout's buttons or controls matched what they changed."
  },
  {
   "id": "constraints",
   "ok": true,
   "note": "The self-checkout stopped you from scanning once it flagged the unexpected item, so the wrong move could not carry on."
  },
  {
   "id": "feedback",
   "ok": true,
   "note": "The screen did tell you straight away, saying unexpected item in the bagging area, so you knew something needed fixing."
  },
  {
   "id": "recover",
   "ok": false,
   "note": "You had to wait for an assistant to come and reset it, with no way to clear it yourself."
  }
 ],
 "score": 6,
 "verdict": "a-bit-of-both",
 "culprit": "discover",
 "error_type": "slip",
 "error_why": "You wanted to pay for a few things without help, but you put your bag down out of habit from the till, before scanning.",
 "label": "SCAN BEFORE BAGGING",
 "label_where": "Ask staff before sticking it, then place it beside the bagging area, clear of any display or control.",
 "for_you": "Next time, wait to set your bag down until after scanning an item, then bag things as you go.",
 "for_maker": "Whoever installs these machines could add a clear sign or on-screen note showing customers where to put their bag before scanning."
}

The hob knobs - a bit of both (8 out of 10, mistake)

Input:

{
 "task": "diagnose",
 "thing": "the hob in my kitchen",
 "goal": "Heat a pan on the back right ring",
 "happened": "The four knobs are in a row along the front and I turned the second one, which I thought was the back right. The front left ring started glowing instead. I spotted the glow straight away and turned it off, then tried the next knob along.",
 "often": "now and then",
 "others": "just me",
 "cues": "mapping: \"in a row\""
}

Reply:

{
 "task": "diagnose",
 "headline": "The hob's knobs don't show which ring they control, and that gap caught you this time.",
 "rows": [
  {
   "id": "discover",
   "ok": true,
   "note": "The description doesn't mention whether anything showed you what each knob was for."
  },
  {
   "id": "mapping",
   "ok": false,
   "note": "You turned the knob you thought matched the back right ring, but the front left ring lit up instead."
  },
  {
   "id": "constraints",
   "ok": true,
   "note": "The description doesn't mention anything stopping the wrong knob from being turned."
  },
  {
   "id": "feedback",
   "ok": true,
   "note": "The glow let you see straight away that the wrong ring had come on."
  },
  {
   "id": "recover",
   "ok": true,
   "note": "You turned off the glowing ring and were able to try another knob straight after."
  }
 ],
 "score": 8,
 "verdict": "a-bit-of-both",
 "culprit": "mapping",
 "error_type": "mistake",
 "error_why": "You believed the second knob controlled the back right ring, so you turned it expecting that ring to heat up.",
 "label": "MARK KNOB TO RING",
 "label_where": "Stick it on the front of the hob, beside the knobs, not on any knob, control or marking.",
 "for_you": "Turn one knob at a time, and check that the back right ring glows before you place the pan on it.",
 "for_maker": "Add clear markings or a diagram above the knobs showing which knob controls which ring."
}

The kettle - just a slip (10 out of 10, slip)

Input:

{
 "task": "diagnose",
 "thing": "my kettle",
 "goal": "Boil water for a cup of tea",
 "happened": "I filled it, put it on its base and sat back down, then found it still cold when I went back. The switch has a blue light that comes on when it is boiling, and the light was off. I had just not pressed the switch.",
 "often": "first time",
 "others": "just me",
 "cues": "none"
}

Reply:

{
 "task": "diagnose",
 "headline": "Your kettle was working fine, pressing the switch was the step that didn't happen this time.",
 "rows": [
  {
   "id": "discover",
   "ok": true,
   "note": "The description does not say whether it was clear what to do and where."
  },
  {
   "id": "mapping",
   "ok": true,
   "note": "The description does not say anything about mixing up which control does what."
  },
  {
   "id": "constraints",
   "ok": true,
   "note": "The description does not mention any wrong move being allowed or hard to avoid."
  },
  {
   "id": "feedback",
   "ok": true,
   "note": "The blue light stayed off exactly because the switch had not been pressed, matching the kettle's real state."
  },
  {
   "id": "recover",
   "ok": true,
   "note": "The description does not mention any trouble getting the kettle going again once this was noticed."
  }
 ],
 "score": 10,
 "verdict": "a-slip",
 "culprit": "",
 "error_type": "slip",
 "error_why": "You wanted to boil water for tea, filled the kettle and set it on its base, but pressing the switch didn't happen.",
 "label": "PRESS SWITCH TO START",
 "label_where": "Stick it on the kettle's body just beside the switch, not covering the blue light.",
 "for_you": "Press the switch, then look for the blue light before walking away.",
 "for_maker": "Add a short beep when the switch is pressed, so missing that step is easier to notice before walking away."
}

Build the input yourself

Load the engine in node (global.window = global; require("./faultkit.js"); require("./replykit.js")), then:

const K = FaultKit, R = ReplyKit;
const built = K.buildInput({ thing: "the glass door into the office", goal: "Get into the office",
  happened: "Same handle on both sides, so I pulled. It needed a push. No sign either way.",
  often: "every", others: "others" });  // built.ok is false, with built.note, when the form is too thin
const wire = K.toWire(built.input);     // POST this to /estimate and /run; built.facts.cues holds the page's cues
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.settle(reply)                      // the score, verdict and culprit worked out from the rows
// R.check(reply, built).notes          // the page's re-check of the reply
// R.md(built, reply, K.title(built.form))   // the diagnosis as Markdown

Form keys: thing, goal, happened, often (first, sometimes, every or "" for not said) and others (me, others or "" for not sure).

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://blame-the-thing.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":"blame-the-thing"}' | 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": "diagnose", "thing": "the glass door into the office", "goal": "...", "happened": "...", "often": "every time", "others": "others too", "cues": "...", ...}
# - 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 blame-the-thing:<task>:<inputHash>:a<attempt> (the one task is diagnose), 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="blame-the-thing:diagnose:$(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: blame-the-thing:diagnose:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json