← Knock-On
API
Tokens

Drive Knock-On from your own code

Both metered lanes are available over HTTP. ripple takes one speculative change - a what-if - with the world it happens in, how fast it arrives, the parts of life to trace and any knock-ons you already have in mind, and returns a verdict - spreads, fizzles or sharpen - with up to fourteen knock-on effects over three rings (each with its domain, the effect it grew from, who gains or loses and a tone), tensions between effects and story hooks. relic takes one of those knock-ons and returns found documents from inside that world - notices, letters, diary entries, classified ads, product labels or overheard remarks - each with a placard naming its writer, what they cannot see and what the reader learns between the lines. A natural use is a writing tool or a game master's prep notebook that turns a premise into a ready-made world.

The division of labour first: the page owns the premise check and the pond; the model imagines; the page re-checks the reply. The premise check, the page's guess at where your own knock-ons land, clipping and the pond geometry come from knockkit.js and pond.js, which run in node as well as in the browser. KnockKit.check holds each reply to the input: every outer-ring effect follows from an effect one ring in, every effect sits in a domain you picked, every one of your knock-ons is placed exactly once, the verdict follows its rules, every document kind you asked for came back with a blind spot, and no number or internal label turns up in prose that you never wrote.

Knock-On writes speculative fiction, not forecasts. Get a token on the token page (step 2 below also shows how to mint a guest token in code).

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":"knock-on"}), 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 such as task, premise, world or domains 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: both lanes are metered and need 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

Every run sends task: ripple or relic. One system prompt routes on it; a missing or unknown task is treated as ripple. A ripple verdict is decided by three ordered, non-overlapping rules - the first that applies wins:

OrderVerdictWhen
1sharpenThe premise names no concrete change - only a topic or mood, no statement of what is different, or a contradiction. No effects; 2-3 decisions to make first.
2fizzlesOtherwise, honestly traced, the effects stay inside one or two of the picked domains (too small, too slow, too local or easily absorbed). 2-5 effects on rings 1-2, why it stays small, and a bigger version to try.
3spreadsOtherwise: 9-14 effects across at least three picked domains - at least three on ring 1, three on ring 2 and two on ring 3 - with 1-3 tensions and 2-3 story hooks.

A relic reply has no verdict: exactly one document per requested kind, in the order requested.

The declared input schema carries scalars only, so array- and object-valued fields travel JSON-encoded as strings (KnockKit.toWire does this). Every value is a string.

Input fields

FieldTypeNotes
taskstringripple or relic. Required.
premisestringThe change, often starting "What if". Up to 400 characters, cut at a sentence end past that. Required.
world, settingstringours (present-day world, setting empty) or invented (a world described in setting, up to 1,200 characters). world is required.
pacestringovernight, over years or over generations.
domainsstring (JSON)[{id, name}], three to eight of daily, work, law, home, culture, belief, tools, land. Required.
seeds, page_leanstring (JSON), stringripple only: your own knock-ons [{id, text, page_domain}] (ids S1.., up to six), and the page's reading of the premise from wording alone (trace or sharpen).
knock_on, kindsstring (JSON)relic only: the picked effect {ring, domain, text, who, chain} (chain = the effects it grew from, innermost first), and one to three [{id, name}] of notice, letter, diary, ad, label, overheard.
clip_note, retry_notestringWhat the page cut, when it cut something; and only on a reformat retry, what was wrong.

The replies

The job's output.output is a string holding one JSON object. For ripple: task, verdict, title, summary, nodes ([{id, ring, domain, from, seed, text, who, tone}]; ids K1..; tone is gain, loss or mixed), tensions ([{between, text}]), hooks ([{nodes, text}]), questions, why_small and bigger. For relic: task, title, summary, documents ([{kind, heading, author, body, blind_spot, reveals}]) and through_line. Parse with KnockKit.parseReply, then normalize(reply, task), then check(reply, form), which returns the page's notes.

Worked examples

Replies produced against the app's prompt for the seven sample runs (made-up premises; long fields and lists shortened; the full replies ship in example-result.js). Together they cover every verdict and both lanes.

ripple - grayscale phone screens after 9 pm, with two of your own knock-ons: spreads

Input:

{
 "task": "ripple",
 "premise": "What if every phone screen in the world turned grayscale after 9 pm, and nobody could switch the colour back on until morning?",
 "world": "ours",
 "setting": "",
 "pace": "overnight",
 "domains": "[{\"id\":\"daily\",\"name\":\"Daily life\"},{\"id\":\"work\",\"name\":\"Work & money\"},{\"id\":\"law\",\"name\":\"Law & power\"},{\"id\":\"home\",\"name\":\"Home & family ... (225 characters)",
 "seeds": "[{\"id\":\"S1\",\"text\":\"Night photographers start shooting in black and white on purpose\",\"page_domain\":\"daily\"},{\"id\":\"S2\",\"text\":\"Late-night food delivery orders drop because the food photos look grim\", ... (222 characters)",
 "page_lean": "trace"
}

Reply:

{
 "task": "ripple",
 "verdict": "spreads",
 "title": "The grey hour after nine",
 "summary": "Every phone screen in the world slides into grey tones the moment nine in the evening passes, and stays that way until morning with no setting anywhere to bring the colour back early. Because screens sit underneath photography, buying and selling, safety alerts, bedtime routines and how people describe what they see, the change reaches far past any one corner of life - so it spreads rather than staying small or needing to be sharpened first.",
 "nodes": [
  {
   "id": "K1",
   "ring": 1,
   "domain": "daily",
   "from": "",
   "seed": "",
   "text": "The first grey evening catches people mid-scroll: everyone pokes at their settings looking for the switch to bring the colour back, and word spreads fast that there isn't one.",
   "who": "phone users caught by the first grey evening",
   "tone": "loss"
  },
  {
   "id": "K2",
   "ring": 1,
   "domain": "tools",
   "from": "",
   "seed": "",
   "text": "Camera previews lose their colour at the same hour, so anyone trying to frame a shot after nine sees only grey tones on the screen before pressing the shutter.",
   "who": "people taking photos at night",
   "tone": "mixed"
  },
  {
   "id": "K3",
   "ring": 1,
   "domain": "work",
   "from": "",
   "seed": "",
   "text": "Delivery and shopping apps display their photos in grey once nine passes, so a plate of food or a piece of clothing looks drained of appeal right as people decide whether to buy.",
   "who": "shops and delivery services trading after nine",
   "tone": "loss"
  },
  "... (12 in all)"
 ],
 "tensions": [
  {
   "between": [
    "K5",
    "K6"
   ],
   "text": "The same grey tone that a photographer treats as a feature is the one that makes a dinner look too unappetising to order."
  },
  "... (2 in all)"
 ],
 "hooks": [
  {
   "nodes": [
    "K10"
   ],
   "text": "A hobbyist whose grey-hour photographs start getting shared has to decide whether to admit that the colour was there all along, just hidden by the hour."
  },
  "... (3 in all)"
 ],
 "questions": [],
 "why_small": "",
 "bigger": ""
}

ripple - dragon-scale coin in an invented mountain kingdom: spreads

Input:

{
 "task": "ripple",
 "premise": "What if the dragons' shed scales became the only coin the mountain kingdom accepts?",
 "world": "invented",
 "setting": "A cold mountain kingdom of terraced farms and mining towns. A handful of old dragons sleep in the high peaks and shed scales once a year. A guild of climbers al ... (242 characters)",
 "pace": "over years",
 "domains": "[{\"id\":\"work\",\"name\":\"Work & money\"},{\"id\":\"law\",\"name\":\"Law & power\"},{\"id\":\"home\",\"name\":\"Home & family\"},{\"id\":\"culture\",\"name\":\"Language ... (231 characters)",
 "seeds": "[{\"id\":\"S1\",\"text\":\"Scale-climbers become richer than the nobles\",\"page_domain\":\"work\"}]",
 "page_lean": "trace"
}

Reply:

{
 "task": "ripple",
 "verdict": "spreads",
 "title": "The kingdom that spends in scales",
 "summary": "This traces what happens when the mountain kingdom decides the dragons' shed scales are the only coin it will accept, replacing the crown's silver outright. Over the years, the old currency, the crown's power to mint it, and who counts as wealthy all bend around a coin that only a handful of sleeping dragons can produce once a year, while new land disputes, lending, rituals and sayings grow up around it, so it spreads rather than stays small.",
 "nodes": [
  {
   "id": "K1",
   "ring": 1,
   "domain": "law",
   "from": "",
   "seed": "",
   "text": "The crown's mint falls silent, since the silver it strikes can no longer buy anything now that shed scales are the only coin the kingdom accepts.",
   "who": "the crown and mint workers",
   "tone": "loss"
  },
  {
   "id": "K2",
   "ring": 1,
   "domain": "work",
   "from": "",
   "seed": "S1",
   "text": "The climbers' guild, already working the denning grounds, becomes richer than the nobles, since whoever already holds scales now holds the kingdom's whole wealth.",
   "who": "scale climbers and old noble families",
   "tone": "mixed"
  },
  {
   "id": "K3",
   "ring": 1,
   "domain": "land",
   "from": "",
   "seed": "",
   "text": "The high slopes that serve as the dragons' denning grounds, once just a climb for armour, are fenced and watched as the source of all the kingdom's money.",
   "who": "guild watchers and local herders",
   "tone": "mixed"
  },
  "... (12 in all)"
 ],
 "tensions": [
  {
   "between": [
    "K2",
    "K9"
   ],
   "text": "The climbers' guild grew rich because they already knew the denning grounds best, but the crown's new permits turn that same ground into fenced property even the guild must now pay to cross."
  },
  "... (2 in all)"
 ],
 "hooks": [
  {
   "nodes": [
    "K10"
   ],
   "text": "A watcher's claim to sense which dragon is closest to shedding is put to the test the year none of them seem to shed at all."
  },
  "... (3 in all)"
 ],
 "questions": [],
 "why_small": "",
 "bigger": ""
}

ripple - clocks that stop adding leap seconds: spreads

Input:

{
 "task": "ripple",
 "premise": "What if the world's clocks stopped adding leap seconds, so clock time drifts away from the sun so slowly that nobody alive would notice?",
 "world": "ours",
 "setting": "",
 "pace": "over generations",
 "domains": "[{\"id\":\"daily\",\"name\":\"Daily life\"},{\"id\":\"work\",\"name\":\"Work & money\"},{\"id\":\"law\",\"name\":\"Law & power\"},{\"id\":\"belief\",\"name\":\"Belief & ri ... (184 characters)",
 "seeds": "[]",
 "page_lean": "trace"
}

Reply:

{
 "task": "ripple",
 "verdict": "spreads",
 "title": "Clocks drifting slowly from the sun",
 "summary": "The world's clocks stop adding leap seconds, so clock time drifts away from the sun so slowly that nobody alive would notice, but the gap is built to keep growing across generations. Because that drift reaches into how law defines time, how sun-dependent trades check their instruments, how households and congregations keep their calendars, and how machines are built and sold, its effects spread across several of the given domains rather than staying stuck in one corner.",
 "nodes": [
  {
   "id": "K1",
   "ring": 1,
   "domain": "law",
   "from": "",
   "seed": "",
   "text": "The body that used to announce the periodic correction stops doing so, and civil time comes to be defined purely by the steady count of a clock rather than tied to how fast the earth turns.",
   "who": "the timekeeping authority",
   "tone": "mixed"
  },
  {
   "id": "K2",
   "ring": 1,
   "domain": "tools",
   "from": "",
   "seed": "",
   "text": "Computers, broadcasts and satellite systems that used to pause for the periodic correction run on without interruption, and the small routine that handled it drops out of what engineers need to maintain.",
   "who": "systems engineers",
   "tone": "gain"
  },
  {
   "id": "K3",
   "ring": 1,
   "domain": "work",
   "from": "",
   "seed": "",
   "text": "People whose work depends on the sun's exact position - surveyors, navigators, anyone timing a task by the sky - stop fully trusting the public clock and start checking it against the sun themselves.",
   "who": "surveyors and navigators",
   "tone": "loss"
  },
  "... (12 in all)"
 ],
 "tensions": [
  {
   "between": [
    "K2",
    "K3"
   ],
   "text": "The people who maintain the clocks feel relieved to be rid of the old correction, while the people who depend on the sun's exact position feel they can no longer fully trust the public clock."
  },
  "... (3 in all)"
 ],
 "hooks": [
  {
   "nodes": [
    "K7"
   ],
   "text": "A story could follow the first person to make a living simply telling others how far behind the sun the clock has drifted."
  },
  "... (3 in all)"
 ],
 "questions": [],
 "why_small": "",
 "bigger": ""
}

ripple - one exact-change vending machine: fizzles

Input:

{
 "task": "ripple",
 "premise": "What if the vending machine on the third floor of one small office only took exact change, while every other machine stayed the same?",
 "world": "ours",
 "setting": "",
 "pace": "overnight",
 "domains": "[{\"id\":\"daily\",\"name\":\"Daily life\"},{\"id\":\"work\",\"name\":\"Work & money\"},{\"id\":\"law\",\"name\":\"Law & power\"},{\"id\":\"home\",\"name\":\"Home & family ... (225 characters)",
 "seeds": "[]",
 "page_lean": "trace"
}

Reply:

{
 "task": "ripple",
 "verdict": "fizzles",
 "title": "One office's exact-change machine",
 "summary": "Overnight, the vending machine on the third floor of one small office stops accepting anything but exact change, while every other machine in the office keeps working exactly as it always has. It is a real change, but a narrow one: anyone who wants a snack or drink can keep a few coins handy or simply use a different machine, so the effect is absorbed within a day rather than spreading through the office.",
 "nodes": [
  {
   "id": "K1",
   "ring": 1,
   "domain": "work",
   "from": "",
   "seed": "",
   "text": "The machine on the third floor stops accepting anything but exact change, so without the right coins a buyer simply cannot complete a purchase there.",
   "who": "people who want a snack from it",
   "tone": "loss"
  },
  {
   "id": "K2",
   "ring": 1,
   "domain": "daily",
   "from": "",
   "seed": "",
   "text": "People who still want something from that particular machine start keeping a few coins set aside just for it, tucked in a drawer or a pocket.",
   "who": "regulars of that machine",
   "tone": "mixed"
  },
  {
   "id": "K3",
   "ring": 2,
   "domain": "work",
   "from": "K1",
   "seed": "",
   "text": "Rather than hunt for coins, most people simply switch to one of the other machines, so that one machine's takings quietly shrink while the others see a touch more trade.",
   "who": "that machine's owner versus the other machines",
   "tone": "mixed"
  },
  "... (4 in all)"
 ],
 "tensions": [],
 "hooks": [],
 "questions": [],
 "why_small": "The change touches only one machine among several in one small office, and the workaround is easy: carry a few coins, or use one of the machines that work exactly as before. With such an easy way around it, the disruption never spreads beyond the handful of people who use that machine.",
 "bigger": "What if every vending machine across the whole office, on every floor, switched overnight to taking only exact change, with no other way to pay anywhere in the building?"
}

ripple - "technology changes everything": sharpen

Input:

{
 "task": "ripple",
 "premise": "What if technology changed everything?",
 "world": "ours",
 "setting": "",
 "pace": "overnight",
 "domains": "[{\"id\":\"daily\",\"name\":\"Daily life\"},{\"id\":\"work\",\"name\":\"Work & money\"},{\"id\":\"law\",\"name\":\"Law & power\"},{\"id\":\"home\",\"name\":\"Home & family ... (303 characters)",
 "seeds": "[]",
 "page_lean": "sharpen"
}

Reply:

{
 "task": "ripple",
 "verdict": "sharpen",
 "title": "A change too vague to trace",
 "summary": "The premise only says technology changed everything, without saying which technology, what it now does, or what stops working - so there is no concrete change yet to follow. Until that is pinned down, nothing can honestly be traced, so the premise needs to get more specific first.",
 "nodes": [],
 "tensions": [],
 "hooks": [],
 "questions": [
  "Which piece of technology is it, and what can it newly do that it couldn't before?",
  "Does it change for everyone at once, or does it reach some people before others?",
  "What stops working, or becomes impossible, once this is true?"
 ],
 "why_small": "",
 "bigger": ""
}

relic - documents from one grayscale knock-on

Input:

{
 "task": "relic",
 "premise": "What if every phone screen in the world turned grayscale after 9 pm, and nobody could switch the colour back on until morning?",
 "world": "ours",
 "setting": "",
 "pace": "overnight",
 "domains": "[{\"id\":\"daily\",\"name\":\"Daily life\"},{\"id\":\"work\",\"name\":\"Work & money\"},{\"id\":\"law\",\"name\":\"Law & power\"},{\"id\":\"home\",\"name\":\"Home & family ... (225 characters)",
 "knock_on": "{\"ring\":3,\"domain\":\"work\",\"text\":\"Delivery services start writing a short description of each dish for the grey hours, since a photo alone no longer does the selling.\",\"who\":\"delivery services\",\"chain\":[\"Delivery and shopping apps display their photos in grey  ... (501 characters)",
 "kinds": "[{\"id\":\"notice\",\"name\":\"Official notice\"},{\"id\":\"overheard\",\"name\":\"Overheard remark\"},{\"id\":\"ad\",\"name\":\"Classified ad\"}]"
}

Reply:

{
 "task": "relic",
 "title": "Menus rewritten for the grey hours",
 "summary": "A platform memo, a murmured aside and a hiring advert all show kitchens relearning how to sell a dish once a photograph stops doing the convincing after nine in the evening.",
 "documents": [
  {
   "kind": "notice",
   "heading": "Notice to partner kitchens",
   "author": "the delivery platform's partner-support team",
   "body": "Effective immediately: every dish offered after nine in the evening must carry a short written description alongside its photograph. A photograph alone no longer earns an order once the grey hours begin, and kitchens that leave their late listings blank will be ranked beneath those that do. Keep the description plain and appetising - what is in the dish, how it is served, nothing more. Submit updated text before closing each evening; listings still missing one after the cut-off will be flagged for review.",
   "blind_spot": "The notice blames unfinished listings for the drop in orders without admitting that the grey light itself is what drained the photographs of any pull to begin with.",
   "reveals": "Between the lines, the platform is quietly shifting the cost of replacing a photograph's pull onto the kitchens that depend on it."
  },
  "... (3 in all)"
 ],
 "through_line": "None of the three says it outright, but together they show that selling a dish after nine in the evening has quietly become a job of writing, not photography."
}

relic - documents from one dragon-coin knock-on

Input:

{
 "task": "relic",
 "premise": "What if the dragons' shed scales became the only coin the mountain kingdom accepts?",
 "world": "invented",
 "setting": "A cold mountain kingdom of terraced farms and mining towns. A handful of old dragons sleep in the high peaks and shed scales once a year. A guild of climbers al ... (242 characters)",
 "pace": "over years",
 "domains": "[{\"id\":\"work\",\"name\":\"Work & money\"},{\"id\":\"law\",\"name\":\"Law & power\"},{\"id\":\"home\",\"name\":\"Home & family\"},{\"id\":\"culture\",\"name\":\"Language ... (231 characters)",
 "knock_on": "{\"ring\":3,\"domain\":\"belief\",\"text\":\"Certain watchers near the denning grounds claim they alone can sense which dragon is closest to shedding, turning careful guesswork into something close to prophecy.\",\"who\":\"self-declared dragon-watchers\",\"chain\":[\"The high  ... (574 characters)",
 "kinds": "[{\"id\":\"letter\",\"name\":\"Letter\"},{\"id\":\"label\",\"name\":\"Product label\"},{\"id\":\"diary\",\"name\":\"Diary entry\"}]"
}

Reply:

{
 "task": "relic",
 "title": "Living by the watchers' word",
 "summary": "These three documents show how a household economy has come to wait on self-declared watchers who claim to sense a dragon's mood near the denning grounds.",
 "documents": [
  {
   "kind": "letter",
   "heading": "To a sibling, from the watch",
   "author": "a dragon-watcher",
   "body": "Sibling, the denning watch keeps me on the mountain more nights than I would choose, but the coin follows wherever the dragons turn in their sleep, and folk trust my sense of that more than they trust the mint. I will not explain how I know a shedding is close — only that I know, and that knowing has kept the roof paid this year. Nobody has yet asked what happens to us if I guess wrong. I would rather they kept not asking. — the one who watches now",
   "blind_spot": "They cannot see that what calms their neighbours is the certainty in their own voice, not any real closeness to a dragon's mind.",
   "reveals": "Between the lines, the letter shows a watcher who needs the belief in their own gift as much as the household needs the coin it buys."
  },
  "... (3 in all)"
 ],
 "through_line": "Together, the documents show that nobody truly knows a dragon's mood from outside the fence, yet everyone's coin now depends on someone claiming to."
}

Build the input yourself

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

const K = KnockKit;
const form = K.form({ premise: "What if every library book had to be returned by sunset?", world: "ours",
  pace: "overnight", domains: ["daily", "work", "law", "home", "culture"],
  seeds: "Night readers start buying books instead" });
console.log(K.flags(form), K.lean(form), K.seeds(form));   // the premise check, the page's lean, your knock-ons
const wire = K.toWire(K.buildInput("ripple", form).input);   // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output), "ripple");
// const { notes } = K.check(reply, form);                   // the page's re-check of the reply
// for relic: pick a node, add its chain, and build the second lane's input
// const node = Object.assign({}, reply.nodes[0], { chain: K.chainOf(reply, reply.nodes[0].id) });
// const relicWire = K.toWire(K.buildInput("relic", Object.assign(form, { kinds: ["notice", "diary"] }), node).input);

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://knock-on.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":"knock-on"}' | 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": "ripple", "premise": "What if every phone screen in the world turned grayscale after 9 pm, and nobody could switch the colour back on until morning?", "world": "ours", "setting": "", "pace": "overnight", "domains": "[{\"id\":\"daily\",\"name\":\"Daily life\"}, ...]", "seeds": "[{\"id\":\"S1\",\"text\":\"Night photographers start shooting in black and white on purpose\",\"page_domain\":\"culture\"}, ...]", "page_lean": "trace"}
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 knock-on:<task>:<inputHash>:a<attempt> (the lanes are ripple and relic), 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="knock-on:ripple:$(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: knock-on:ripple:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json