← Snug Season
API

Drive Snug Season from your own code

The one metered task, ready, is available over HTTP. It takes the warmers a person owns (each with its type, an age band and their own words on how it looks), the places they get cold, a short note, and the page's facts (its reading of each warmer, its fixed table of checks per type and the cold spots nothing covers). It returns one verdict (retire_one, check_first, fill_gap or snug), the warmers to retire, a phased checklist for the rest (before first use, every use, end of season, each check with a pass-if line), one idea per uncovered cold spot, the safety lines that apply and a short closing line.

The division of labour first: the page owns the table of checks, the age limits, the word hints, which cold spots are covered and the order applied with those hints; the model owns reading the person's words (is that damage, or a denial of it? has it been looked at?) and all the wording; the page then re-checks the reply against its own reading. Not professional advice.

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":"snug-season"}), 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, items, spots, note, page_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 tailored check 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 and the verdicts

Every run sends task: ready (the one task is ready); a missing or different one is still answered as ready. The verdict follows a fixed check order, stopping at the first that fits: retire_one when a warmer is past the page's age limit (a hot water bottle 3 years old or more; an electric blanket over 10 years) or its words describe damage or wear (a crack, a leak, sticky or perished rubber, scorch marks, fraying, bare wires, soles worn smooth - the model's reading; "no cracks" is not damage); check_first when a warmer has nothing said about it, has not been looked at, or a hot water bottle or electric blanket has an unknown age; fill_gap when a ticked cold spot has nothing on the list that warms it; otherwise snug.

Input (every field is a string)

FieldRequiredWhat it holds
taskyesAlways ready.
itemsyesOne warmer per line: <number> | <type> | age: <age> | condition: <the person's words>. Types: hot water bottle, electric blanket or heated throw, microwavable heat pack, rechargeable hand warmer, single-use hand warmers, slippers, throw or blanket, other warmer. Ages: under 2 years, 2 to 3 years, 3 to 10 years, over 10 years, not sure. Up to 8 warmers.
spotsyesWhere they get cold, separated by ;: feet, hands, lap and legs, back and shoulders, neck.
noteyesThe person's own words, at least 3; over 1,200 characters it keeps its start and end with the middle marked as cut.
page_factsyesOne key | ... per line: items, one item_<n> line per warmer (age against the limit, condition words, a damage hint), one table_<type> line per warmer type (the fixed checks by phase, each with its pass-if), covered_spots, uncovered_spots, a gap_idea per uncovered spot, page_pick and verdict_rule. Build it with SnugKit.buildInput (below) so the facts match the page's.
clip_notenoWhat the page shortened, if anything.
retry_notenoOnly on the page's one reformat retry, after an unusable reply.

Reply (one JSON object)

{"task": "ready", "title": "...", "verdict": "retire_one", "verdict_reason": "...", "retire": [{"n": 1, "why": "..."}],
 "checks": [{"phase": "first_use", "n": 2, "check": "...", "pass_if": "..."}], "gaps": [{"spot": "hands", "idea": "..."}],
 "safety": "...", "cosy_line": "..."}
FieldWhat it holds
verdict, verdict_reasonThe verdict id by the check order above; at most 30 words on what decided it.
retireFor retire_one, one entry per warmer to retire: its number n and a reason of at most 20 words. Empty for any other verdict.
checks3 to 14 checks for the warmers not retired, by phase (first_use, each_use, season_end); every phase has one and every such warmer has a first_use check. check at most 16 words, pass_if at most 14, both based on the page's table for that type.
gapsExactly one entry per uncovered cold spot: the spot words and an idea of at most 18 words, generic, never a brand.
safety, cosy_line, titleSafety points that apply to the listed types (at most 40 words; never an electric blanket together with a hot water bottle when both are listed), a playful closing line (at most 20) and a title of at most 8 words.

The page then checks the reply (SnugKit.check): the verdict against its order and hints, the retire list, every check's item number, phase and word limits, every band covered and every warmer given a first-use check, the gaps against the uncovered spots, the never-together and not-boiling lines, every number in the prose, names the input never uses, leaked field ids and the word limits.

Worked examples

These are the four free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover all four verdicts - an old hot water bottle with sticky, cracked rubber (retire one), an electric blanket not looked at since spring (check first), hands and lap covered but nothing for cold feet (fill a gap), and four checked warmers covering every cold spot (snug).

An old hot water bottle with sticky, cracked rubber - retire one

Input:

{
 "task": "ready",
 "items": "1 | hot water bottle | age: 3 to 10 years | condition: The rubber has gone sticky and there is a hairline crack by the neck\n2 | slippers | age: under 2 years | condition: Soles still grip and the backs hold my heels",
 "spots": "feet; hands",
 "note": "Working from home in a chilly flat, and my feet and hands go numb by mid-afternoon.",
 "page_facts": "items | 2 items listed\nitem_1 | hot water bottle; age 3 to 10 years - past the page's limit (commonly replaced every 2 to 3 years); condition: \"The rubber has gone sticky and there is a hairline crack by the neck\"; damage_hint: the words may describe damage (sticky) - a hint only, read them\nitem_2 | slippers; age under 2 years; condition: \"Soles still grip and the backs hold my heels\"; damage_hint: none found - a hint only, read the words\ntable_bottle | before first use: Bend and stretch the rubber all over, especially around the neck (pass if no cracks, splits or sticky patches) / Fill with warm tap water, close the stopper and squeeze it over the sink (pass if not a single drip); every use: Fill about two-thirds with hot, not boiling, water and let the air out (pass if stopper closes firmly and a cover is on); end of season: Empty it and let it dry with the stopper out, stored flat (pass if dry inside, away from sunlight)\ntable_slippers | before first use: Check the soles and the backs (pass if soles still grip and the backs hold your heels); every use: Keep them fully on your feet on stairs and hard floors (pass if heels held, no slipping); end of season: Wash or air them as the label says, then store them dry (pass if dry inside)\ncovered_spots | feet (items 1, 2)\nuncovered_spots | hands\ngap_idea | hands: fingerless gloves, or a hand warmer kept in a glove or pocket\npage_pick | retire_one (the order applied with the page's word hints)\nverdict_rule | check in order and stop at the first that fits: retire_one when any item is past the page's age limit or its condition words describe damage or wear; check_first when any item's condition is blank, says it has not been checked or the person is not sure, or a hot water bottle or electric blanket has an unknown age; fill_gap when a ticked cold spot has no listed item that warms it; otherwise snug"
}

Reply:

{
 "task": "ready",
 "title": "Chilly Flat, Numb Feet and Hands",
 "verdict": "retire_one",
 "verdict_reason": "Retire one: the hot water bottle is past its 2 to 3 year limit and its rubber is sticky with a hairline crack by the neck.",
 "retire": [
  {
   "n": 1,
   "why": "Past the 2 to 3 year limit; rubber is sticky with a hairline crack by the neck."
  }
 ],
 "checks": [
  {
   "phase": "first_use",
   "n": 2,
   "check": "Check the soles and the backs",
   "pass_if": "Soles still grip and backs hold your heels"
  },
  {
   "phase": "each_use",
   "n": 2,
   "check": "Keep slippers fully on your feet on stairs and hard floors",
   "pass_if": "Heels held, no slipping"
  },
  {
   "phase": "season_end",
   "n": 2,
   "check": "Wash or air them as the label says, then store dry",
   "pass_if": "Dry inside"
  }
 ],
 "gaps": [
  {
   "spot": "hands",
   "idea": "Fingerless gloves, or a hand warmer kept in a glove or pocket"
  }
 ],
 "safety": "Never fill a hot water bottle with boiling water. Stop using anything that is damaged, too hot or smells of burning.",
 "cosy_line": "One step at a time, let's ease those numb feet and hands through the mid-afternoon."
}

An electric blanket not looked at since spring - check first

Input:

{
 "task": "ready",
 "items": "1 | electric blanket or heated throw | age: 3 to 10 years | condition: Haven't checked it since it went in the cupboard in spring\n2 | throw or blanket | age: under 2 years | condition: Fine, washed it last week",
 "spots": "lap and legs; back and shoulders",
 "note": "Evenings on the sofa reading, and my lap and shoulders get cold first.",
 "page_facts": "items | 2 items listed\nitem_1 | electric blanket or heated throw; age 3 to 10 years - within the page's limit (commonly replaced at 10 years); condition: \"Haven't checked it since it went in the cupboard in spring\"; damage_hint: none found - a hint only, read the words\nitem_2 | throw or blanket; age under 2 years; condition: \"Fine, washed it last week\"; damage_hint: none found - a hint only, read the words\ntable_eblanket | before first use: Unroll it and look over the fabric, the cord, the plug and the controller (pass if no scorch marks, fraying, bare wires or loose parts); every use: Lay it flat, use it on its own, and switch off when you are done (pass if no folds, no hot water bottle with it); end of season: Let it cool, then roll it loosely rather than folding it (pass if stored rolled, nothing heavy on top)\ntable_throw | before first use: Shake it out and look it over (pass if no holes or loose threads); every use: Keep it clear of heaters, fires and candles (pass if at least an arm's length from any heat); end of season: Wash it as its label says and store it dry (pass if fully dry before it is folded away)\ncovered_spots | lap and legs (items 1, 2); back and shoulders (items 1, 2)\nuncovered_spots | none\npage_pick | check_first (the order applied with the page's word hints)\nverdict_rule | check in order and stop at the first that fits: retire_one when any item is past the page's age limit or its condition words describe damage or wear; check_first when any item's condition is blank, says it has not been checked or the person is not sure, or a hot water bottle or electric blanket has an unknown age; fill_gap when a ticked cold spot has no listed item that warms it; otherwise snug"
}

Reply:

{
 "task": "ready",
 "title": "Cosy Sofa Evening Reading Kit",
 "verdict": "check_first",
 "verdict_reason": "Check first: the electric blanket hasn't been checked since it went in the cupboard in spring.",
 "retire": [],
 "checks": [
  {
   "phase": "first_use",
   "n": 1,
   "check": "Unroll it and look over the fabric, cord, plug and controller.",
   "pass_if": "No scorch marks, fraying, bare wires or loose parts."
  },
  {
   "phase": "first_use",
   "n": 2,
   "check": "Shake the throw out and look it over.",
   "pass_if": "No holes or loose threads."
  },
  {
   "phase": "each_use",
   "n": 1,
   "check": "Lay it flat, use it on its own, switch off when finished.",
   "pass_if": "No folds, and no hot water bottle used with it."
  },
  {
   "phase": "each_use",
   "n": 2,
   "check": "Keep it clear of heaters, fires and candles.",
   "pass_if": "At least an arm's length from any heat."
  },
  {
   "phase": "season_end",
   "n": 1,
   "check": "Let it cool, then roll it loosely rather than folding.",
   "pass_if": "Stored rolled, with nothing heavy on top."
  },
  {
   "phase": "season_end",
   "n": 2,
   "check": "Wash it as its label says and store it dry.",
   "pass_if": "Fully dry before it is folded away."
  }
 ],
 "gaps": [],
 "safety": "Keep throws and blankets clear of heaters, fires and candles. Stop using anything that feels damaged, gets too hot, or smells of burning.",
 "cosy_line": "Let's give that blanket and throw a once-over, then settle in for cosy reading evenings on the sofa."
}

Hands and lap covered, nothing for cold feet - fill a gap

Input:

{
 "task": "ready",
 "items": "1 | rechargeable hand warmer | age: under 2 years | condition: Charges fine, the case is not cracked or swollen\n2 | throw or blanket | age: 2 to 3 years | condition: Big wool throw, no holes",
 "spots": "feet; hands; lap and legs",
 "note": "Long evenings at my desk, and my toes are always the first to freeze.",
 "page_facts": "items | 2 items listed\nitem_1 | rechargeable hand warmer; age under 2 years; condition: \"Charges fine, the case is not cracked or swollen\"; damage_hint: none found - a hint only, read the words\nitem_2 | throw or blanket; age 2 to 3 years; condition: \"Big wool throw, no holes\"; damage_hint: none found - a hint only, read the words\ntable_handrech | before first use: Charge it with its own cable on a hard surface while you are nearby (pass if case not swollen, cracked or very hot); every use: Start on the lowest setting with a layer between it and your skin (pass if warm, never hot or painful); end of season: Charge it part way and store it somewhere cool and dry (pass if kept away from heaters and sunlight)\ntable_throw | before first use: Shake it out and look it over (pass if no holes or loose threads); every use: Keep it clear of heaters, fires and candles (pass if at least an arm's length from any heat); end of season: Wash it as its label says and store it dry (pass if fully dry before it is folded away)\ncovered_spots | hands (items 1); lap and legs (items 2)\nuncovered_spots | feet\ngap_idea | feet: thick socks or slippers that grip, or a covered hot water bottle\npage_pick | fill_gap (the order applied with the page's word hints)\nverdict_rule | check in order and stop at the first that fits: retire_one when any item is past the page's age limit or its condition words describe damage or wear; check_first when any item's condition is blank, says it has not been checked or the person is not sure, or a hot water bottle or electric blanket has an unknown age; fill_gap when a ticked cold spot has no listed item that warms it; otherwise snug"
}

Reply:

{
 "task": "ready",
 "title": "Long Desk Evenings, Cold Toes Kit",
 "verdict": "fill_gap",
 "verdict_reason": "Fill a gap: feet are a cold spot with no listed warmer covering them, while hands and lap and legs are already covered.",
 "retire": [],
 "checks": [
  {
   "phase": "first_use",
   "n": 1,
   "check": "Charge it with its cable on a hard surface while nearby",
   "pass_if": "Case isn't swollen, cracked or very hot"
  },
  {
   "phase": "first_use",
   "n": 2,
   "check": "Shake the throw out and look it over",
   "pass_if": "No holes or loose threads"
  },
  {
   "phase": "each_use",
   "n": 1,
   "check": "Start on the lowest setting with a layer between it and your skin",
   "pass_if": "Feels warm, never hot or painful"
  },
  {
   "phase": "each_use",
   "n": 2,
   "check": "Keep the throw clear of heaters, fires and candles",
   "pass_if": "At least an arm's length from heat"
  },
  {
   "phase": "season_end",
   "n": 1,
   "check": "Charge it part way, then store somewhere cool and dry",
   "pass_if": "Kept away from heaters and sunlight"
  },
  {
   "phase": "season_end",
   "n": 2,
   "check": "Wash it as the label says, then store it dry",
   "pass_if": "Fully dry before it's folded away"
  }
 ],
 "gaps": [
  {
   "spot": "feet",
   "idea": "Thick socks or slippers that grip, or a covered hot water bottle"
  }
 ],
 "safety": "Keep the throw clear of heaters, fires and candles, and stop using the hand warmer if it feels too hot or damaged.",
 "cosy_line": "Time to pull the throw over your lap, charge up the hand warmer, and give those toes their turn next."
}

Four checked warmers, every cold spot covered - snug

Input:

{
 "task": "ready",
 "items": "1 | hot water bottle | age: under 2 years | condition: No cracks or leaks, the stopper seals tight, it has a knitted cover\n2 | electric blanket or heated throw | age: under 2 years | condition: Looked it over last week: no scorch marks or fraying, the controller works\n3 | slippers | age: under 2 years | condition: Soles grip well\n4 | microwavable heat pack | age: under 2 years | condition: No scorch marks, smells normal",
 "spots": "feet; lap and legs; neck",
 "note": "Draughty old house, and I start to feel the cold early in the evening.",
 "page_facts": "items | 4 items listed\nitem_1 | hot water bottle; age under 2 years - within the page's limit (commonly replaced every 2 to 3 years); condition: \"No cracks or leaks, the stopper seals tight, it has a knitted cover\"; damage_hint: none found - a hint only, read the words\nitem_2 | electric blanket or heated throw; age under 2 years - within the page's limit (commonly replaced at 10 years); condition: \"Looked it over last week: no scorch marks or fraying, the controller works\"; damage_hint: none found - a hint only, read the words\nitem_3 | slippers; age under 2 years; condition: \"Soles grip well\"; damage_hint: none found - a hint only, read the words\nitem_4 | microwavable heat pack; age under 2 years; condition: \"No scorch marks, smells normal\"; damage_hint: none found - a hint only, read the words\ntable_bottle | before first use: Bend and stretch the rubber all over, especially around the neck (pass if no cracks, splits or sticky patches) / Fill with warm tap water, close the stopper and squeeze it over the sink (pass if not a single drip); every use: Fill about two-thirds with hot, not boiling, water and let the air out (pass if stopper closes firmly and a cover is on); end of season: Empty it and let it dry with the stopper out, stored flat (pass if dry inside, away from sunlight)\ntable_eblanket | before first use: Unroll it and look over the fabric, the cord, the plug and the controller (pass if no scorch marks, fraying, bare wires or loose parts); every use: Lay it flat, use it on its own, and switch off when you are done (pass if no folds, no hot water bottle with it); end of season: Let it cool, then roll it loosely rather than folding it (pass if stored rolled, nothing heavy on top)\ntable_slippers | before first use: Check the soles and the backs (pass if soles still grip and the backs hold your heels); every use: Keep them fully on your feet on stairs and hard floors (pass if heels held, no slipping); end of season: Wash or air them as the label says, then store them dry (pass if dry inside)\ntable_wheat | before first use: Look over the cover and smell it (pass if no scorch marks and no burnt smell); every use: Heat it only for the time on its label, and let it cool before reheating (pass if warm to hold, never too hot to touch); end of season: Store it dry in a sealed bag (pass if dry and smelling normal)\ncovered_spots | feet (items 1, 2, 3); lap and legs (items 1, 2, 4); neck (items 4)\nuncovered_spots | none\npage_pick | snug (the order applied with the page's word hints)\nverdict_rule | check in order and stop at the first that fits: retire_one when any item is past the page's age limit or its condition words describe damage or wear; check_first when any item's condition is blank, says it has not been checked or the person is not sure, or a hot water bottle or electric blanket has an unknown age; fill_gap when a ticked cold spot has no listed item that warms it; otherwise snug"
}

Reply:

{
 "task": "ready",
 "title": "Early Evening Chill in a Draughty House",
 "verdict": "snug",
 "verdict_reason": "No warmer is past its age limit or damaged, none need checking first, and feet, lap and legs, and neck are all covered, so this kit is snug.",
 "retire": [],
 "checks": [
  {
   "phase": "first_use",
   "n": 1,
   "check": "Bend and stretch the rubber all over, then fill and squeeze it over the sink.",
   "pass_if": "No cracks, splits or sticky patches, and not a single drip."
  },
  {
   "phase": "first_use",
   "n": 2,
   "check": "Unroll it and look over the fabric, cord, plug and controller.",
   "pass_if": "No scorch marks, fraying, bare wires or loose parts."
  },
  {
   "phase": "first_use",
   "n": 3,
   "check": "Check the soles and the backs before wearing them this season.",
   "pass_if": "Soles still grip and the backs hold your heels."
  },
  {
   "phase": "first_use",
   "n": 4,
   "check": "Look over the cover and give it a smell.",
   "pass_if": "No scorch marks and no burnt smell."
  },
  {
   "phase": "each_use",
   "n": 1,
   "check": "Fill it about two-thirds with hot, not boiling, water and let the air out.",
   "pass_if": "Stopper closes firmly and a cover is on."
  },
  {
   "phase": "each_use",
   "n": 2,
   "check": "Lay it flat, use it alone, and switch off when finished.",
   "pass_if": "No folds, no hot water bottle with it."
  },
  {
   "phase": "each_use",
   "n": 3,
   "check": "Keep them fully on your feet on stairs and hard floors.",
   "pass_if": "Heels held, no slipping."
  },
  {
   "phase": "each_use",
   "n": 4,
   "check": "Heat it only for the time on its label, letting it cool before reheating.",
   "pass_if": "Warm to hold, never too hot to touch."
  },
  {
   "phase": "season_end",
   "n": 1,
   "check": "Empty it and let it dry with the stopper out, stored flat.",
   "pass_if": "Dry inside, away from sunlight."
  },
  {
   "phase": "season_end",
   "n": 2,
   "check": "Let it cool, then roll it loosely rather than folding.",
   "pass_if": "Stored rolled, nothing heavy on top."
  },
  {
   "phase": "season_end",
   "n": 3,
   "check": "Wash or air them as the label says, then store dry.",
   "pass_if": "Dry inside."
  },
  {
   "phase": "season_end",
   "n": 4,
   "check": "Store it dry, in a sealed bag.",
   "pass_if": "Dry and smelling normal."
  }
 ],
 "gaps": [],
 "safety": "Never use an electric blanket together with a hot water bottle, and never fill a hot water bottle with boiling water.",
 "cosy_line": "A draughty old house meets a hot water bottle, blanket, heat pack and slippers for those early evenings."
}

Build the input yourself

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

const K = SnugKit;
const built = K.buildInput({ items: [{ type: "bottle", age: "y3_10", cond: "The rubber has gone sticky" }, { type: "slippers", age: "under_2", cond: "Soles still grip" }],
  spots: { feet: true, hands: true }, note: "Working from home in a chilly flat." });
built.j[0];                                      // { label: "item 1 - hot water bottle", old: true, damage: "sticky", retire: true, ... } - the page's reading
built.cv.uncovered;                              // ["hands"] - cold spots nothing on the list warms
built.pick;                                      // "retire_one" - the order applied with the page's word hints
built.base;                                      // the page's own checklist, phase by phase
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.csv(built, reply, {})                       // the checklist as CSV

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://snug-season.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":"snug-season"}' | 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": "ready", "items": "1 | hot water bottle | age: under 2 years | condition: ...", "spots": "feet; hands", "note": "...", "page_facts": "..."} - 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 snug-season:<task>:<inputHash>:a<attempt> (the one task is ready), 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="snug-season:ready:$(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: snug-season:ready:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json