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.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, thing, goal, happened, often, others, cues) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the diagnosis is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. 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.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always diagnose. |
thing | yes | The everyday thing, in your words (the page keeps up to 60 characters). |
goal | yes | What you were trying to do (up to 160 characters). |
happened | yes | What 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). |
often | yes | How often it trips you up: first time, now and then, every time or not said. |
others | yes | Whether other people get it wrong too: just me, others too or not sure. |
cues | yes | The 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_note | no | Only 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": "..."}
| Field | What it holds |
|---|---|
headline | One 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. |
score | A number: 2 for every row whose ok is true. |
verdict | the-thing, a-bit-of-both or a-slip. |
culprit | The id of the first row that failed, or an empty string. |
error_type | slip or mistake. |
error_why | One sentence: why it was a slip or a mistake, in your own words. |
label | The embossed label: at most 24 characters, in capitals, letters, digits, spaces and a few plain marks. |
label_where | Where to stick it, on or beside the part the culprit row is about. |
for_you | One small thing to do differently next time with the thing as it is. |
for_maker | The 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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://blame-the-thing.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// blame-the-thing.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://blame-the-thing.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://blame-the-thing.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// BlameTheThing.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class BlameTheThing {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://blame-the-thing.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://blame-the-thing.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://blame-the-thing.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://blame-the-thing.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "blame-the-thing"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "blame-the-thing" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "blame-the-thing"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "blame-the-thing"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "blame-the-thing" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "blame-the-thing"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "blame-the-thing" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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}'
inp = json.load(open("input.json")) # {"task": "diagnose", "thing": "the glass door into the office", "goal": "...", "happened": "...", "often": "every time", "others": "others too", "cues": "...", ...}
# - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "diagnose", thing, goal, happened, often, others, cues} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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}'
import hashlib, time
key = "blame-the-thing:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"), reply.get("score"), reply.get("label"))
import { createHash } from "node:crypto";
const key = `blame-the-thing:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict, reply.score, reply.label);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "blame-the-thing:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "blame-the-thing:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "blame-the-thing:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"], reply["score"], reply["label"]].inspect
$key = "blame-the-thing:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["verdict"] ?? ""), " ", ($reply["score"] ?? ""), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"blame-the-thing:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
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
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "diagnose", "thing": "the glass door into the office", "goal": "...", "happened": "...", "often": "every time", "others": "others too", "cues": "...", ...}
# - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "blame-the-thing:diagnose:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `blame-the-thing:diagnose:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "blame-the-thing:diagnose:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "blame-the-thing:diagnose:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "blame-the-thing:diagnose:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: blame-the-thing:diagnose:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"blame-the-thing:diagnose:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);