← Thirteen Bands
API

Drive Thirteen Bands from your own code

The one metered task, band-reading, is available over HTTP. It takes the 13 painted segments of a fuzzy caterpillar, its colours, hair, length, where it was found, what it was doing, notes and whether anyone touched it, and returns a three-layer reading: what was reported, four explanations (its age, how long it has fed, a different caterpillar, the winter folklore) each with what would tell it apart, what it adds up to, safe steps and a safety line.

The division of labour first: the page decides; the model explains. The page counts the bands, finds the pattern and the look-alike signs, picks the call and fixes the fit of each explanation before the run, and sends all of it in facts. The model copies the call and never re-ranks the fits. It never forecasts weather.

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":"thirteen-bands"}), 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, bands, colour, hair, length, where, doing, notes, touched, facts) 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: a reading 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 calls

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

FieldRequiredWhat it holds
taskyesAlways band-reading.
bandsyes13 comma-separated colours, head to tail: black, rust, pale or other or not seen, then (head to tail).
colouryesOne of: Black and rust or orange-brown; Yellow, cream or straw; White or grey; Another colour or a mix.
hairyesOne of: Short, even, stiff bristles; Tufts, lashes or long pencil-like hairs; Soft and long, like fur; Not sure.
lengthyesLike 4.5 cm, or not given.
whereyesWhere it was found, up to 80 characters.
doingyesOne of: Crawling along; Curled up in a ball; Still, not moving; Eating a plant; Not sure.
notesyesUp to 200 characters, or not given.
touchedyesyes if someone touched it with bare skin, else no.
factsyesThe page's lines: segments seen, rust share, pattern, look-alike signs, the place word that suggests a move, the verdict, the fit of each explanation and the best fit.
retry_notenoOnly on the page's one reformat retry.

The call, first match wins: check-id when there is any look-alike sign (a colour other than black and rust, tufts or fur-like hair, a pale segment, a length over 7 cm); move-it when where contains a place word such as path, road, step, porch, patio, doorway, indoors, kitchen or car; otherwise leave-it. The fits: with a look-alike sign, species is strong and age and feeding weak; with the classic pattern (black ends, one rust block) age is strong, feeding some, species weak; otherwise age, feeding and species are all some. The forecast is always weak. The best fit is the highest fit, ties broken in the order age, feeding, species, forecast.

The reply

{"task": "band-reading", "verdict": "move-it", "headline": "...", "observation": [{"from": "bands", "text": "..."}], "hypotheses": [{"id": "age", "reading": "...", "tell_apart": "..."}, {"id": "feeding", ...}, {"id": "species", ...}, {"id": "forecast", ...}], "assessment": "...", "todo": ["..."], "safety": "...", "closing": "..."}
FieldWhat it holds
verdictThe call copied from facts: check-id, move-it or leave-it.
headlineAt most 20 words: the best fit and what to do.
observation[]2 to 5 restatements of the entry, each {from, text}, from one of bands, colour, hair, length, where, doing, notes.
hypotheses[]Exactly 4, in order age, feeding, species, forecast: reading (at most 35 words) and tell_apart (at most 30).
assessmentAt most 45 words tying the best fit to the call.
todo[]1 to 3 steps matching the call, each at most 25 words.
safety, closing1 sentence each (at most 30 and 20 words).

The page then checks the reply (BandKit.check) - the call against its own, all four explanations present, fields cited that were left blank, numbers not in the entry, weather talk outside the folklore entry, handling advice on a caterpillar to identify first, internal field names and word limits - and lists any disagreement under "The page's check".

Worked examples - every call

These are the 3 free samples on the page: the exact input the page sends and the reply that replays for free.

Crossing the garden path - classic bands - call: move it a little

Input:

{
 "task": "band-reading",
 "bands": "black, black, black, rust, rust, rust, rust, rust, rust, black, black, black, black (head to tail)",
 "colour": "Black and rust or orange-brown",
 "hair": "Short, even, stiff bristles",
 "length": "4.5 cm",
 "where": "crossing the garden path",
 "doing": "Crawling along",
 "notes": "it kept going when I stepped over it",
 "touched": "no",
 "facts": "segments seen: 13 of 13 (black 7, rust 6, pale or other 0, not seen 0)\nrust share of the segments seen: 46%\npattern: black at both ends with one rust block in the middle\nlook-alike signs: none\nplace word that suggests a move: path\nverdict: move-it\nfit: age strong; feeding some; species weak; forecast weak\nbest fit: age"
}

Reply:

{
 "task": "band-reading",
 "verdict": "move-it",
 "headline": "The bands most likely show its age, so move it off the garden path.",
 "observation": [
  {
   "from": "bands",
   "text": "You saw 13 segments: 7 black and 6 rust, with black at both ends and a rust block in the middle."
  },
  {
   "from": "where",
   "text": "You found it crossing the garden path."
  },
  {
   "from": "doing",
   "text": "It was crawling along."
  },
  {
   "from": "notes",
   "text": "It kept going even after you stepped over it."
  },
  {
   "from": "length",
   "text": "It measured 4.5 cm long."
  }
 ],
 "hypotheses": [
  {
   "id": "age",
   "reading": "As it grows, each moult widens the rust part, so the 6 rust segments out of 13 fit its age well, and this explains the banding best.",
   "tell_apart": "Seeing this same caterpillar again later with a wider rust band would show it has aged and moulted further."
  },
  {
   "id": "feeding",
   "reading": "How long it has been feeding also shifts band widths; its length of 4.5 cm shows some growth, but this only partly accounts for the rust share you saw.",
   "tell_apart": "Comparing its size with other woolly bears from the same path would show whether it has simply fed and grown longer than most."
  },
  {
   "id": "species",
   "reading": "Its bristles are short, even and stiff and its colours are black and rust, matching a woolly bear, not a different kind of caterpillar, so this fits poorly.",
   "tell_apart": "Seeing long tufted hairs on it would point to a different caterpillar instead."
  },
  {
   "id": "forecast",
   "reading": "Folklore reads a wide rust band as a sign of a mild winter ahead, but the black-and-rust pattern here comes from this caterpillar's own growth, not the season, so the claim fits poorly.",
   "tell_apart": "Comparing band widths among other woolly bears found on the same path would show they disagree rather than match."
  }
 ],
 "assessment": "Taken together, the bands most likely reflect this caterpillar's age rather than foretelling weather or a different kind, so the steps below are about helping it off the path safely, not acting on the rust share as a forecast.",
 "todo": [
  "Move it a short distance off the garden path onto nearby plants, leaves or soil, using a leaf, card or gloves."
 ],
 "safety": "Handle it with a leaf, card or gloves instead of bare skin, and wash your hands afterward.",
 "closing": "Thank you for noticing it and taking care on the garden path."
}

Curled up in the log pile - irregular bands, touched barehanded - call: leave it be

Input:

{
 "task": "band-reading",
 "bands": "black, black, black, black, rust, rust, black, rust, rust, black, black, black, black (head to tail)",
 "colour": "Black and rust or orange-brown",
 "hair": "Short, even, stiff bristles",
 "length": "not given",
 "where": "on the log pile behind the shed",
 "doing": "Curled up in a ball",
 "notes": "I picked it up before I knew what it was",
 "touched": "yes",
 "facts": "segments seen: 13 of 13 (black 9, rust 4, pale or other 0, not seen 0)\nrust share of the segments seen: 31%\npattern: black and rust, but not one rust block between black ends\nlook-alike signs: none\nplace word that suggests a move: none\nverdict: leave-it\nfit: age some; feeding some; species some; forecast weak\nbest fit: age"
}

Reply:

{
 "task": "band-reading",
 "verdict": "leave-it",
 "headline": "Age looks like the leading partial explanation for the bands, and leaving it where it is fits the reading.",
 "observation": [
  {
   "from": "bands",
   "text": "You counted 13 segments: 9 black and 4 rust, mixed rather than one solid rust band in the middle."
  },
  {
   "from": "where",
   "text": "You found it on the log pile behind the shed."
  },
  {
   "from": "doing",
   "text": "It was curled up in a ball."
  },
  {
   "from": "notes",
   "text": "You mentioned picking it up before knowing what it was."
  }
 ],
 "hypotheses": [
  {
   "id": "age",
   "reading": "Growing older usually widens the rust band after each moult, which could explain the 4 rust segments among the 9 black ones, though the broken pattern means age only partly fits.",
   "tell_apart": "If you saw this same caterpillar again with a wider rust band than it has now, that would point to age rather than the other explanations."
  },
  {
   "id": "feeding",
   "reading": "How long it has spent feeding shifts how wide each band grows, which explains part of the 9 black and 4 rust segments, though not fully why they don't form one rust block.",
   "tell_apart": "Seeing it noticeably larger than other woolly bears nearby, despite similar colouring, would point to how long it has been feeding."
  },
  {
   "id": "species",
   "reading": "The bands don't settle into the usual single rust block between black ends, which leaves some open question about whether this is a banded woolly bear rather than a similar-looking caterpillar.",
   "tell_apart": "Seeing longer tufts or hair-like spines rather than short, even bristles would point to a different caterpillar, not a woolly bear."
  },
  {
   "id": "forecast",
   "reading": "Folklore reads a wider rust band as a sign of a milder coming winter, but the 31% rust share here reflects this caterpillar's own growth, and widths differ between caterpillars, so they can't predict weather.",
   "tell_apart": "If you compared rust band widths on several woolly bears found nearby, you would likely find they disagree with each other."
  }
 ],
 "assessment": "Taken together, age looks like the leading partial explanation for the pattern you saw, though feeding and species remain open too, and none of this changes that leaving it where it is fits what the bands show.",
 "todo": [
  "Leave it on the log pile behind the shed rather than moving it.",
  "Watching it or snapping a photo from where it is now is fine."
 ],
 "safety": "Wash your skin with soap and water, and seek medical advice if you notice a rash, swelling or strong pain afterward.",
 "closing": "Take care, and feel free to keep watching it from where it is."
}

White and tufted on the porch railing - look-alike signs - call: check what it is first

Input:

{
 "task": "band-reading",
 "bands": "pale or other, pale or other, pale or other, pale or other, pale or other, pale or other, pale or other, pale or other, pale or other, pale or other, pale or other, pale or other, pale or other (head to tail)",
 "colour": "White or grey",
 "hair": "Tufts, lashes or long pencil-like hairs",
 "length": "3 cm",
 "where": "on the porch railing",
 "doing": "Still, not moving",
 "notes": "black tufts stick up along its back",
 "touched": "no",
 "facts": "segments seen: 13 of 13 (black 0, rust 0, pale or other 13, not seen 0)\nrust share of the segments seen: 0%\npattern: some segments are pale or another colour\nlook-alike signs: the colour picked: white or grey; the hair picked: tufts, lashes or long pencil-like hairs; 13 pale or other-coloured segments\nplace word that suggests a move: porch\nverdict: check-id\nfit: age weak; feeding weak; species strong; forecast weak\nbest fit: species"
}

Reply:

{
 "task": "band-reading",
 "verdict": "check-id",
 "headline": "The colour, hair and all-pale segments most likely point to a different caterpillar; compare it with a guide before touching.",
 "observation": [
  {
   "from": "bands",
   "text": "All 13 segments you reported are pale or another colour, with no black or rust bands at all."
  },
  {
   "from": "colour",
   "text": "You picked white or grey as the colour you saw."
  },
  {
   "from": "hair",
   "text": "You picked tufts, lashes or long pencil-like hairs as the hair type."
  },
  {
   "from": "where",
   "text": "You found it on the porch railing."
  }
 ],
 "hypotheses": [
  {
   "id": "age",
   "reading": "Band width usually shifts as a caterpillar moults and grows, but none of the segments here show black or rust, so age cannot be read from this pattern.",
   "tell_apart": "Seeing black and rust bands on it, rather than an all-pale colour, would support reading its bands for age instead of species."
  },
  {
   "id": "feeding",
   "reading": "How long it has been feeding usually shifts band widths, but with no black or rust shown, feeding time does not explain this pale pattern well.",
   "tell_apart": "If you saw black and rust bands that differ in width from other woolly bears in the same place, that would point to feeding time rather than species."
  },
  {
   "id": "species",
   "reading": "The white or grey colour, the tufts or pencil-like hairs you picked, and all 13 pale segments all point away from a banded woolly bear and toward another species.",
   "tell_apart": "Seeing tufted, uneven bristles instead of short, even ones on this caterpillar would point to another species, not a woolly bear."
  },
  {
   "id": "forecast",
   "reading": "Folklore reads a wider rust band as a sign of a milder winter, but band width actually tracks a caterpillar's own growth, and here none of the segments show rust to read that way.",
   "tell_apart": "Comparing band widths among other woolly bears found in the same place would show them disagreeing rather than agreeing with each other."
  }
 ],
 "assessment": "Overall, the colour, hair type and uniformly pale segments point away from a banded woolly bear and toward a different fuzzy caterpillar, one of several kinds that can irritate skin, so comparing it with a guide before any contact is the sound next step.",
 "todo": [
  "Avoid touching it with bare skin.",
  "Take a clear photo and share it with a local nature group or guide for identification.",
  "Since it's on the porch railing, you could slide a leaf or card beneath it to shift it aside, not your skin."
 ],
 "safety": "Use a leaf, card or gloves instead of bare hands if you need to handle it, and wash your hands afterward.",
 "closing": "A calm, careful look at it will help you tell what kind of caterpillar this is."
}

Build the input yourself

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

const K = BandKit;
const form = K.form({ bands: "bbbrrrrrrbbbb".split(""), colour: "black-rust", hair: "even", length: "4.5",
  where: "crossing the garden path", doing: "crawling", notes: "", touched: false });
const built = K.buildInput(form);       // built.facts holds the counts, pattern, signs, call and fits
const wire = K.toWire(built.input);     // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes          // the page's re-check of the reply
// K.md(built, reply, K.title(form))    // the reading 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://thirteen-bands.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":"thirteen-bands"}' | 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": "band-reading", "bands": "black, black, black, rust, ... (head to tail)", "where": "crossing the garden path", ...}
# - 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 thirteen-bands:<task>:<inputHash>:a<attempt> (the one task is band-reading), 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="thirteen-bands:band-reading:$(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: thirteen-bands:band-reading:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json