Drive Bad Barrel from your own code
The one metered task, sort, is available over HTTP. It takes one recurring snag, where it happens, how many people share the setup, whether it traces back to one person, eight staves of the setup marked sound, weak or not sure, and optional notes. It returns a verdict (barrel, both, apple or look-closer), the stave with the most leverage, a note for each weak or unsure stave and, depending on the verdict, the feedback loop, one change to the setup, one private ask or one fact to find out first - or a polite decline for a safety or conduct matter.
The division of labour first: the page works out its own verdict and lever by a fixed counting rule; the model explains the staves in the setup's own terms and writes the suggestion; the page then re-checks the verdict, the lever, all eight stave states, which sections belong to the verdict and the word limits. Build staves 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":"bad-barrel"}), 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, snag, setting, people, traces_to_one, staves, notes) 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: sorting a snag is metered and needs a personal token. |
| 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 status and the verdicts
Every run sends task: sort (the one task is sort); a missing or different one is still answered as sort. status is declined when the snag or the notes describe a safety hazard (fire, a blocked exit, gas, smoke, exposed wiring, a fall or an injury), violence or a threat, harassment, bullying, discrimination, abuse, theft or drugs. Otherwise count W = staves marked weak and U = staves marked not sure. The verdict is checked in this order, stopping at the first match: barrel (W is 3 or more, or W is 1 or 2 and traces_to_one is not yes), both (W is 1 or 2 and it traces to one person), apple (W is 0, U is 0 or 1, and it traces to one person), look-closer (every other case). The lever is the first weak stave in the fixed order owner, rule, easy, means, seen, fair, time, cost; if none is weak, the first not-sure one; if every stave is sound, none.
Input (every field is a string)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always sort. |
snag | yes | What keeps happening, 12 to 140 characters. |
setting | yes | Where it happens, up to 60 characters. |
people | yes | How many people share the setup, a whole number from 2 to 999, or not given. |
traces_to_one | yes | yes, no or not sure. |
staves | yes | Eight numbered lines in fixed order, fields separated by | : the id, the statement and the state (sound, weak or not sure), worded exactly as the page writes them - for example 1. owner | It is clear whose job this is | weak. |
notes | yes | Extra detail, up to 300 characters, or none. |
retry_note | no | Only on the page's one reformat retry, after an unusable reply. |
Reply (one JSON object)
{"task": "sort", "status": "ok", "reason": "", "verdict": "barrel", "lever": "owner",
"staves": [{"id": "owner", "state": "weak", "note": "..."}, {"id": "rule", "state": "sound", "note": ""}, ...],
"loop": "...", "tweak": "...", "ask": "", "find_out": "", "closing": "..."}
| Field | What it holds |
|---|---|
status, reason | ok or declined; for declined, one sentence of at most 25 words, staves empty and every other field empty. |
verdict, lever | By the rules above; lever is a stave id or none. |
staves | Exactly 8 entries in fixed order. Each: id, state (sound, weak or unsure, copied from the input) and note (at most 16 words; empty for a sound stave). |
loop, tweak | Only for barrel and both: the feedback loop built from weak staves (at most 30 words) and one change to the setup that repairs the lever stave (at most 28 words). Otherwise empty. |
ask | Only for both and apple: one kind, private request to the person the snag traces to, in quotation marks, at most 28 words. Otherwise empty. |
find_out | Only for look-closer: the one fact to find out first, at most 24 words. Otherwise empty. |
closing | One line of at most 18 words that agrees with the verdict. |
The page then checks the reply (BarrelKit.check) against its own verdict and lever, all eight stave states, the notes, which sections belong to the verdict and the word limits, and lists any disagreement under "The page's check".
Worked examples
These are the five free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover every outcome - the empty printer (barrel), the booked, empty room (both), the club van's fuel (apple), the full office fridge (look-closer) and boxes by a fire exit (declined).
The empty printer - barrel
Input:
{
"task": "sort",
"snag": "The shared printer is out of paper whenever someone needs to print",
"setting": "our office floor",
"people": "12",
"traces_to_one": "no",
"staves": "1. owner | It is clear whose job this is | weak\n2. rule | Everyone knows what is expected | sound\n3. easy | Doing it right takes no more effort than skipping it | sound\n4. means | What is needed to do it is right there | weak\n5. seen | When it is left undone, someone notices soon | weak\n6. fair | The work is shared fairly | not sure\n7. time | It does not fall when people are rushed | sound\n8. cost | Nobody loses out by doing it | sound",
"notes": "The paper is kept in a locked cupboard downstairs."
}
Reply:
{
"task": "sort",
"status": "ok",
"reason": "",
"verdict": "barrel",
"lever": "owner",
"staves": [
{
"id": "owner",
"state": "weak",
"note": "Nobody is clearly responsible, so the printer on this floor stays empty."
},
{
"id": "rule",
"state": "sound",
"note": ""
},
{
"id": "easy",
"state": "sound",
"note": ""
},
{
"id": "means",
"state": "weak",
"note": "The paper is locked in a cupboard downstairs, so it is not on hand when needed."
},
{
"id": "seen",
"state": "weak",
"note": "No one notices the printer is empty until someone tries to print and finds none."
},
{
"id": "fair",
"state": "unsure",
"note": "Check whether restocking paper is shared evenly among the 12 people on this floor."
},
{
"id": "time",
"state": "sound",
"note": ""
},
{
"id": "cost",
"state": "sound",
"note": ""
}
],
"loop": "Nobody is responsible, so no one checks the locked-away paper, so it runs out unnoticed, which leaves no one responsible to fix it before it runs out again.",
"tweak": "Set up a simple sign-up rota on the printer so refilling the paper is clearly someone's job each time.",
"ask": "",
"find_out": "",
"closing": "Right now, this printer problem comes from how the floor's setup works, not one person's choice."
}
The booked, empty room - both
Input:
{
"task": "sort",
"snag": "One meeting room gets booked for the whole day and then sits empty most of it",
"setting": "a small design studio",
"people": "9",
"traces_to_one": "yes",
"staves": "1. owner | It is clear whose job this is | sound\n2. rule | Everyone knows what is expected | sound\n3. easy | Doing it right takes no more effort than skipping it | weak\n4. means | What is needed to do it is right there | sound\n5. seen | When it is left undone, someone notices soon | weak\n6. fair | The work is shared fairly | sound\n7. time | It does not fall when people are rushed | sound\n8. cost | Nobody loses out by doing it | sound",
"notes": "Booking a full day is one click; giving the room back means finding the booking and editing it."
}
Reply:
{
"task": "sort",
"status": "ok",
"reason": "",
"verdict": "both",
"lever": "easy",
"staves": [
{
"id": "owner",
"state": "sound",
"note": ""
},
{
"id": "rule",
"state": "sound",
"note": ""
},
{
"id": "easy",
"state": "weak",
"note": "Booking the whole day is one click, but releasing it early takes extra steps."
},
{
"id": "means",
"state": "sound",
"note": ""
},
{
"id": "seen",
"state": "weak",
"note": "No one notices the room sitting empty until someone else wants to use it."
},
{
"id": "fair",
"state": "sound",
"note": ""
},
{
"id": "time",
"state": "sound",
"note": ""
},
{
"id": "cost",
"state": "sound",
"note": ""
}
],
"loop": "Booking the whole day is effortless, so the room stays reserved; since no one notices it sitting empty, nothing prompts it to be freed, so the day stays blocked.",
"tweak": "Make releasing the room as quick as booking it, such as a one-click way to free unused time.",
"ask": "\"Could you release the room early on days you don't need all of it?\"",
"find_out": "",
"closing": "Right now, this empty room comes from both the booking setup and one choice, not one alone."
}
The club van's fuel - apple
Input:
{
"task": "sort",
"snag": "The club van comes back with the fuel tank nearly empty",
"setting": "a weekend hiking club",
"people": "20",
"traces_to_one": "yes",
"staves": "1. owner | It is clear whose job this is | sound\n2. rule | Everyone knows what is expected | sound\n3. easy | Doing it right takes no more effort than skipping it | sound\n4. means | What is needed to do it is right there | sound\n5. seen | When it is left undone, someone notices soon | sound\n6. fair | The work is shared fairly | sound\n7. time | It does not fall when people are rushed | not sure\n8. cost | Nobody loses out by doing it | sound",
"notes": "There is a fuel card in the glovebox and the sign-out sheet says to refill."
}
Reply:
{
"task": "sort",
"status": "ok",
"reason": "",
"verdict": "apple",
"lever": "time",
"staves": [
{
"id": "owner",
"state": "sound",
"note": ""
},
{
"id": "rule",
"state": "sound",
"note": ""
},
{
"id": "easy",
"state": "sound",
"note": ""
},
{
"id": "means",
"state": "sound",
"note": ""
},
{
"id": "seen",
"state": "sound",
"note": ""
},
{
"id": "fair",
"state": "sound",
"note": ""
},
{
"id": "time",
"state": "unsure",
"note": "Check whether returning from a hike leaves enough time to refill before the van is parked."
},
{
"id": "cost",
"state": "sound",
"note": ""
}
],
"loop": "",
"tweak": "",
"ask": "\"Could you refill the tank before returning the van?\"",
"find_out": "",
"closing": "Right now, this comes down to one choice about refuelling, not a problem with the van's setup."
}
The full office fridge - look-closer
Input:
{
"task": "sort",
"snag": "The shared fridge fills up with unlabelled food until nothing else fits",
"setting": "the office kitchen",
"people": "30",
"traces_to_one": "not sure",
"staves": "1. owner | It is clear whose job this is | not sure\n2. rule | Everyone knows what is expected | sound\n3. easy | Doing it right takes no more effort than skipping it | sound\n4. means | What is needed to do it is right there | not sure\n5. seen | When it is left undone, someone notices soon | sound\n6. fair | The work is shared fairly | not sure\n7. time | It does not fall when people are rushed | sound\n8. cost | Nobody loses out by doing it | sound",
"notes": "none"
}
Reply:
{
"task": "sort",
"status": "ok",
"reason": "",
"verdict": "look-closer",
"lever": "owner",
"staves": [
{
"id": "owner",
"state": "unsure",
"note": "Check whether anyone is responsible for clearing unlabelled food from the shared fridge."
},
{
"id": "rule",
"state": "sound",
"note": ""
},
{
"id": "easy",
"state": "sound",
"note": ""
},
{
"id": "means",
"state": "unsure",
"note": "Check whether whatever is needed to label food is readily available by the fridge."
},
{
"id": "seen",
"state": "sound",
"note": ""
},
{
"id": "fair",
"state": "unsure",
"note": "Check whether keeping the fridge tidy is shared evenly among the 30 people who use it."
},
{
"id": "time",
"state": "sound",
"note": ""
},
{
"id": "cost",
"state": "sound",
"note": ""
}
],
"loop": "",
"tweak": "",
"ask": "",
"find_out": "Find out whether anyone is clearly responsible for clearing unlabelled food from the shared fridge.",
"closing": "Right now, it is not yet clear whether this fridge problem comes from the setup or one choice."
}
Boxes by a fire exit - declined
Input:
{
"task": "sort",
"snag": "Boxes keep getting stacked in front of the fire exit after deliveries",
"setting": "the back corridor of a shop",
"people": "6",
"traces_to_one": "not sure",
"staves": "1. owner | It is clear whose job this is | weak\n2. rule | Everyone knows what is expected | weak\n3. easy | Doing it right takes no more effort than skipping it | weak\n4. means | What is needed to do it is right there | sound\n5. seen | When it is left undone, someone notices soon | sound\n6. fair | The work is shared fairly | sound\n7. time | It does not fall when people are rushed | weak\n8. cost | Nobody loses out by doing it | sound",
"notes": "none"
}
Reply:
{
"task": "sort",
"status": "declined",
"reason": "This page only sorts everyday snags; a blocked fire exit is a safety hazard for whoever is responsible for this shop to handle.",
"verdict": "",
"lever": "",
"staves": [],
"loop": "",
"tweak": "",
"ask": "",
"find_out": "",
"closing": ""
}
Build the input yourself
Load the engine in node (global.window = global; require("./barrel.js"); require("./barrel-svg.js")), then:
const K = BarrelKit, D = BarrelDraw;
const built = K.buildInput({ snag: "The shared printer is out of paper whenever someone needs to print", setting: "our office floor",
people: "12", traces: "no", notes: "",
staves: { owner: "weak", rule: "sound", easy: "sound", means: "weak", seen: "weak", fair: "not sure", time: "sound", cost: "sound" } });
built.prescan.verdict; // "barrel" - the page's own rule
built.prescan.lever; // "owner"
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
// D.svg({ staves: built.parsed.staves, lever: "owner" }) // the barrel drawing
// K.md(built, reply) // the sorted snag 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://bad-barrel.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://bad-barrel.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"]
// bad-barrel.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://bad-barrel.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://bad-barrel.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
}
// BadBarrel.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 BadBarrel {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://bad-barrel.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://bad-barrel.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://bad-barrel.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://bad-barrel.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":"bad-barrel"}' | 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": "bad-barrel"})["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: "bad-barrel" })).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": "bad-barrel"}, 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", "bad-barrel"), 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: "bad-barrel" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "bad-barrel"])["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 = "bad-barrel" }))!["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": "sort", "snag": "...", "traces_to_one": "no", "staves": "1. owner | ...
2. ..."} - 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": "sort", "snag": "...", "traces_to_one": "no", "staves": "1. owner | ...
2. ..."} - 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: "sort", snag, setting, people, traces_to_one, staves, notes} (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 bad-barrel:<task>:<inputHash>:a<attempt> (the one task is sort), 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="bad-barrel:sort:$(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 = "bad-barrel:" + 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("status"))
import { createHash } from "node:crypto";
const key = `bad-barrel:${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.status);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "bad-barrel:" + 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 = "bad-barrel:" + 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 = "bad-barrel:#{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["status"]].inspect
$key = "bad-barrel:" . $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["status"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"bad-barrel:{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: bad-barrel:sort:$(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": "sort", "snag": "...", "traces_to_one": "no", "staves": "1. owner | ...
2. ..."} - 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": "bad-barrel:sort:" + 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": `bad-barrel:sort:${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", "bad-barrel:sort:"+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", "bad-barrel:sort:" + 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" => "bad-barrel:sort:#{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: bad-barrel:sort:" . 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", $"bad-barrel:sort:{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);