← Bad Barrel
API

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.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field (task, snag, setting, people, traces_to_one, staves, notes) 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: sorting a snag 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: 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)

FieldRequiredWhat it holds
taskyesAlways sort.
snagyesWhat keeps happening, 12 to 140 characters.
settingyesWhere it happens, up to 60 characters.
peopleyesHow many people share the setup, a whole number from 2 to 999, or not given.
traces_to_oneyesyes, no or not sure.
stavesyesEight 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.
notesyesExtra detail, up to 300 characters, or none.
retry_notenoOnly 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": "..."}
FieldWhat it holds
status, reasonok or declined; for declined, one sentence of at most 25 words, staves empty and every other field empty.
verdict, leverBy the rules above; lever is a stave id or none.
stavesExactly 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, tweakOnly 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.
askOnly 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_outOnly for look-closer: the one fact to find out first, at most 24 words. Otherwise empty.
closingOne 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"}
}

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')

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

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}' 

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