← Yes, And
API

Drive Yes, And from your own code

The one metered task, notes, is available over HTTP. It takes the audience suggestion, the page's facts about the scene (the players, the line count, each player's questions and the lines that open like a denial) and the scene itself as numbered lines. It returns one verdict (blocking, no_game, game_dropped or found_the_game), what every line offered and what happened to it, the base reality, the first unusual thing and the game, the lines where the game came back, a listening moment, three notes, three suggested next lines and a closing line.

The division of labour first: the page reads the scene into lines, counts and cue words; the model reads the meaning; the page then re-checks the reply against the scene - one entry per line, each fate decided by a later line from another player, the verdict against the order, real players in the next lines. A playful practice aid, not a teacher's assessment.

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":"yes-and"}), 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, suggestion, page_facts, scene) 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: the coach's notes are 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: notes (the one task is notes); a missing or different one is still answered as notes. The verdict field holds one of four ids, by a fixed check order over the reply's own fields that stops at the first that fits: blocking when any line's offer is blocked; no_game when no unusual thing is found; game_dropped when the game comes back fewer than 2 times; otherwise found_the_game. The page recomputes the verdict from the reply's own fields and flags any mismatch.

Input (every field is a string)

FieldRequiredWhat it holds
taskyesAlways notes.
suggestionyesThe audience suggestion, up to 60 characters, or none given.
page_factsyesOne key | ... per line: players (count and names), lines (count sent), questions (per player, with line numbers), denial_openers (line numbers whose first words look like a denial, or none) and verdict_rule.
sceneyesOne n | player | text per line. A scene over 40 lines sends lines 1 to 30 and the last 10, with a [... lines a to b cut ...] marker.
clip_notenoWhat the page shortened, if anything (a long scene, or lines over 280 characters).
retry_notenoOnly on the page's one reformat retry, after an unusable reply.

Reply (one JSON object)

{"task": "notes", "verdict": "found_the_game", "verdict_reason": "...",
 "base_reality": {"who": "...", "where": "...", "what": "..."},
 "first_unusual": {"line": 3, "what": "..."}, "game": "...",
 "lines": [{"n": 1, "offer": "...", "fate": "built", "by": 2}], "game_moves": [5, 7],
 "listening": {"line": 5, "underneath": "...", "reply_line": 6, "picked_up": false},
 "notes": [{"line": 7, "note": "..."}], "next_beats": [{"speaker": "Priya", "line": "..."}], "closing": "..."}
FieldWhat it holds
verdict, verdict_reasonThe verdict id by the check order above; at most 30 words on what decided it.
linesExactly one entry per line sent: n, the offer (at most 12 words, or "" for a bare acknowledgement), its fate (none, blocked, built, accepted or ignored, by its own check order) and by, the later line by another player that decided it (0 for ignored and none).
base_realityWho, where and what, at most 12 words each, from the scene's own words.
first_unusual, game, game_movesThe first line that breaks from the ordinary (line 0 when none), the game in at most 20 words, and the later lines where it came back.
listeningOne line, what was underneath it (at most 20 words, quoting the line), the next reply by another player and whether that reply picked it up.
notesExactly 3, each tied to a line, at most 25 words; note 1 is on the moment that decided the verdict.
next_beats, closingExactly 3 suggested next lines, in the speaker's own words, for real players and never one player twice in a row; a closing line of at most 20 words.

The page then checks the reply (SceneKit.check): the verdict against its own fields, one entry per line, every fate decided by a later line from another player, game moves after the first unusual line, the listening reply line, three notes on real lines with note 1 on the deciding moment, a questions note when a player asked 3 or more, three next lines for real players that give words rather than describe them, pronouns, stray numbers, leaked field ids and the word limits. It also reconciles its own cue words: a line that opens like a denial but is not read as a block is flagged for a second look.

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 - a lighthouse keeper scared of the dark (found the game), a leaning wedding cake that keeps getting denied (blocking), an ordinary chat at a bus stop (no game yet), and a museum guide who asks the paintings' permission once and never again (game dropped).

The lighthouse keeper - found the game

Input:

{
 "task": "notes",
 "suggestion": "lighthouse",
 "page_facts": "players | 2 | Mo; Priya\nlines | 9\nquestions | Mo 0; Priya 0\ndenial_openers | none\nverdict_rule | blocking when any line's offer is blocked; else no_game when no unusual thing is found; else game_dropped when the game is played again fewer than 2 times; else found_the_game",
 "scene": "1 | Mo | Priya, the new bulb for the lighthouse came today. Biggest one they make.\n2 | Priya | Great. Let's get it up the stairs before it gets dark.\n3 | Mo | Before dark, yes. I'm scared of the dark. I sleep with every light on.\n4 | Priya | A lighthouse keeper who is scared of the dark. That explains all the night-lights in the hall.\n5 | Mo | Nine night-lights in the hall, and one inside my pillow for emergencies.\n6 | Priya | And you picked the job with the biggest light on the whole coast.\n7 | Mo | The bigger the bulb, the braver I feel. This new one should make me fearless.\n8 | Priya | Then we fit it at noon, when it is brightest, and you stand right next to it.\n9 | Mo | Noon, with every curtain open and the night-lights on, just in case."
}

Reply:

{
 "task": "notes",
 "verdict": "found_the_game",
 "verdict_reason": "No line is blocked, an unusual fear of the dark appears at line 3, and the game returns 6 times after that, so found the game.",
 "base_reality": {
  "who": "Mo is the lighthouse keeper; Priya helps fit the new bulb.",
  "where": "At the lighthouse, climbing the stairs with the new bulb.",
  "what": "Fitting the lighthouse's new, brightest bulb before dark, then at noon."
 },
 "first_unusual": {
  "line": 3,
  "what": "A lighthouse keeper who is scared of the dark."
 },
 "game": "If Mo fears the dark, then every corner of Mo's life needs backup light, even at the brightest moment.",
 "lines": [
  {
   "n": 1,
   "offer": "A new, biggest bulb has arrived for the lighthouse.",
   "fate": "built",
   "by": 2
  },
  {
   "n": 2,
   "offer": "Proposes carrying the bulb up the stairs before dark.",
   "fate": "built",
   "by": 3
  },
  {
   "n": 3,
   "offer": "Mo reveals being scared of the dark, sleeping with every light on.",
   "fate": "built",
   "by": 4
  },
  {
   "n": 4,
   "offer": "Points out the irony of a dark-fearing lighthouse keeper's night-lights.",
   "fate": "built",
   "by": 5
  },
  {
   "n": 5,
   "offer": "Specifies nine night-lights in the hall, one hidden in the pillow.",
   "fate": "built",
   "by": 6
  },
  {
   "n": 6,
   "offer": "Highlights the irony of choosing the coast's biggest light job.",
   "fate": "built",
   "by": 7
  },
  {
   "n": 7,
   "offer": "Claims bigger bulbs make Mo braver; expects to feel fearless.",
   "fate": "built",
   "by": 8
  },
  {
   "n": 8,
   "offer": "Proposes fitting the bulb at noon, right next to Mo.",
   "fate": "built",
   "by": 9
  },
  {
   "n": 9,
   "offer": "Insists on open curtains and night-lights even at noon, just in case.",
   "fate": "none",
   "by": 0
  }
 ],
 "game_moves": [
  4,
  5,
  6,
  7,
  8,
  9
 ],
 "listening": {
  "line": 3,
  "underneath": "Underneath the joke, Mo admits real fear, saying \"I'm scared of the dark,\" wanting it taken seriously.",
  "reply_line": 4,
  "picked_up": false
 },
 "notes": [
  {
   "line": 9,
   "note": "Line 9 works because Mo still needs every light on even at noon, proving the fear outlasts the brightest bulb."
  },
  {
   "line": 4,
   "note": "Priya's line 4 instantly turns Mo's fear into the lighthouse's funniest detail, building fast."
  },
  {
   "line": 3,
   "note": "Line 4 answers the words but skips the real fear under \"I'm scared of the dark,\" worth a beat of real care."
  }
 ],
 "next_beats": [
  {
   "speaker": "Priya",
   "line": "Then I will keep the new bulb shining all night, right by your pillow."
  },
  {
   "speaker": "Mo",
   "line": "Then I will fit a second new bulb in the hall too, just in case."
  },
  {
   "speaker": "Priya",
   "line": "Then the whole coast will run on our brightest bulbs, day and night."
  }
 ],
 "closing": "A strong game already alive in these words; next time, let it grow even bigger."
}

The leaning cake - blocking

Input:

{
 "task": "notes",
 "suggestion": "wedding cake",
 "page_facts": "players | 2 | Dee; Rob\nlines | 10\nquestions | Dee 0; Rob 1 (line 10)\ndenial_openers | 2, 4, 6\nverdict_rule | blocking when any line's offer is blocked; else no_game when no unusual thing is found; else game_dropped when the game is played again fewer than 2 times; else found_the_game",
 "scene": "1 | Dee | Rob, the wedding starts in an hour and the cake is leaning to the left.\n2 | Rob | No, it's not leaning. That's just how the table is.\n3 | Dee | The table is level. I checked it with my phone. Help me hold up the top tier.\n4 | Rob | We don't have a top tier. It's a one-tier cake.\n5 | Dee | Fine. Help me hold up the one tier.\n6 | Rob | I can't, I'm the groom. I'm not allowed to see the cake before the wedding.\n7 | Dee | Then close your eyes and push where I tell you.\n8 | Rob | Okay, eyes closed. Tell me where.\n9 | Dee | Left hand on the bottom. Gently. It's straightening.\n10 | Rob | It feels warm. Is the cake supposed to feel warm?"
}

Reply:

{
 "task": "notes",
 "verdict": "blocking",
 "verdict_reason": "4 lines are blocked: 1, 2, 3 and 5, so the scene is blocking.",
 "base_reality": {
  "who": "Dee is preparing the cake; Rob is the groom.",
  "where": "Near the wedding cake, shortly before the wedding begins.",
  "what": "Fixing the cake together before the wedding begins."
 },
 "first_unusual": {
  "line": 6,
  "what": "Rob is not allowed to see the cake before the wedding."
 },
 "game": "If Rob cannot see the cake, every task with it must happen by touch.",
 "lines": [
  {
   "n": 1,
   "offer": "Says the wedding starts in an hour and the cake leans left.",
   "fate": "blocked",
   "by": 2
  },
  {
   "n": 2,
   "offer": "Denies the cake is leaning and blames the table instead.",
   "fate": "blocked",
   "by": 3
  },
  {
   "n": 3,
   "offer": "Says the table is level; asks for help with the top tier.",
   "fate": "blocked",
   "by": 4
  },
  {
   "n": 4,
   "offer": "Denies a top tier exists and says the cake is one-tier.",
   "fate": "accepted",
   "by": 5
  },
  {
   "n": 5,
   "offer": "Concedes and asks Rob to help hold up the one-tier cake.",
   "fate": "blocked",
   "by": 6
  },
  {
   "n": 6,
   "offer": "Refuses to help; Rob is the groom who cannot see the cake.",
   "fate": "built",
   "by": 7
  },
  {
   "n": 7,
   "offer": "Tells Rob to close eyes and push as guided.",
   "fate": "accepted",
   "by": 8
  },
  {
   "n": 8,
   "offer": "",
   "fate": "none",
   "by": 0
  },
  {
   "n": 9,
   "offer": "Directs where to push gently and says the cake is straightening.",
   "fate": "ignored",
   "by": 0
  },
  {
   "n": 10,
   "offer": "Reports the cake feels warm and asks if that is normal.",
   "fate": "none",
   "by": 0
  }
 ],
 "game_moves": [
  7,
  8,
  10
 ],
 "listening": {
  "line": 1,
  "underneath": "Dee is anxious about time and wants reassurance or help, not an argument about \"leaning\".",
  "reply_line": 2,
  "picked_up": false
 },
 "notes": [
  {
   "line": 2,
   "note": "Line 2 denies the cake is leaning; saying \"yes, and let's fix it together\" would accept Dee's offer instead."
  },
  {
   "line": 7,
   "note": "Dee turns the \"close your eyes\" idea into a game, finally building on an offer instead of blocking it."
  },
  {
   "line": 6,
   "note": "Line 6 blocks Dee's request with \"I can't\"; accepting by guiding Rob's hands would keep the help offer alive."
  }
 ],
 "next_beats": [
  {
   "speaker": "Rob",
   "line": "You're right, it's leaning! Let's shift the tier so it balances evenly."
  },
  {
   "speaker": "Dee",
   "line": "Good, now let's steady the bottom so the whole cake holds."
  },
  {
   "speaker": "Rob",
   "line": "There, it's balanced now - we saved the cake together."
  }
 ],
 "closing": "Next time, try accepting the first offer before building anything new."
}

The bus stop - no game yet

Input:

{
 "task": "notes",
 "suggestion": "bus stop",
 "page_facts": "players | 2 | Ana; Jules\nlines | 11\nquestions | Ana 5 (lines 1, 3, 5, 7, 9); Jules 0\ndenial_openers | none\nverdict_rule | blocking when any line's offer is blocked; else no_game when no unusual thing is found; else game_dropped when the game is played again fewer than 2 times; else found_the_game",
 "scene": "1 | Ana | Is this the stop for the number 12?\n2 | Jules | Yes, the 12 stops here. It should come in about five minutes.\n3 | Ana | Have you been waiting long?\n4 | Jules | About ten minutes. I am going to the library.\n5 | Ana | Oh, nice. What are you reading at the moment?\n6 | Jules | A book about gardens. I have a small garden at home.\n7 | Ana | Do you grow vegetables?\n8 | Jules | Some tomatoes and a few beans.\n9 | Ana | That sounds nice. Is it hard to grow tomatoes?\n10 | Jules | They just need sun and water, and a little patience.\n11 | Ana | Here comes the bus."
}

Reply:

{
 "task": "notes",
 "verdict": "no_game",
 "verdict_reason": "Nothing in the scene breaks from ordinary reality, so it is no game yet; the players simply trade real facts about buses, libraries and tomatoes.",
 "base_reality": {
  "who": "Ana and Jules, two people waiting for the bus together.",
  "where": "At a bus stop, waiting for a bus to arrive.",
  "what": "Making conversation about waiting, a library book and growing tomatoes."
 },
 "first_unusual": {
  "line": 0,
  "what": ""
 },
 "game": "",
 "lines": [
  {
   "n": 1,
   "offer": "Asks whether this is the right stop for a particular bus.",
   "fate": "built",
   "by": 2
  },
  {
   "n": 2,
   "offer": "Confirms the stop and says the bus is coming soon.",
   "fate": "accepted",
   "by": 11
  },
  {
   "n": 3,
   "offer": "Asks how long Jules has already been waiting.",
   "fate": "built",
   "by": 4
  },
  {
   "n": 4,
   "offer": "Reveals that Jules is on the way to the library.",
   "fate": "built",
   "by": 5
  },
  {
   "n": 5,
   "offer": "Asks what Jules is reading at the moment.",
   "fate": "built",
   "by": 6
  },
  {
   "n": 6,
   "offer": "Reveals the book is about gardens and Jules has one at home.",
   "fate": "built",
   "by": 7
  },
  {
   "n": 7,
   "offer": "Asks whether Jules grows vegetables in that garden.",
   "fate": "built",
   "by": 8
  },
  {
   "n": 8,
   "offer": "Reveals Jules grows tomatoes and beans.",
   "fate": "built",
   "by": 9
  },
  {
   "n": 9,
   "offer": "Asks whether tomatoes are hard to grow.",
   "fate": "built",
   "by": 10
  },
  {
   "n": 10,
   "offer": "Explains tomatoes need sun, water and patience.",
   "fate": "ignored",
   "by": 0
  },
  {
   "n": 11,
   "offer": "Announces the bus is arriving, ending their talk.",
   "fate": "none",
   "by": 0
  }
 ],
 "game_moves": [],
 "listening": {
  "line": 9,
  "underneath": "Ana's \"that sounds nice\" shows real interest, not just a plain yes or no question.",
  "reply_line": 10,
  "picked_up": true
 },
 "notes": [
  {
   "line": 6,
   "note": "Line 6's book about gardens could have turned odd, but the scene kept things ordinary."
  },
  {
   "line": 9,
   "note": "Ana asks question after question; a statement here would add a new fact instead of just asking."
  },
  {
   "line": 8,
   "note": "Jules keeps adding real detail, like the tomatoes and beans, instead of answering briefly."
  }
 ],
 "next_beats": [
  {
   "speaker": "Jules",
   "line": "My tomatoes only grow this well because I read them a few pages every night."
  },
  {
   "speaker": "Ana",
   "line": "I'm not surprised the whole bus seems to listen whenever you read out loud."
  },
  {
   "speaker": "Jules",
   "line": "Maybe that is why the bus always seems to know when I bring a garden book."
  }
 ],
 "closing": "Next time, let one small unusual detail slip in early and see where the game could go."
}

The museum guide - game dropped

Input:

{
 "task": "notes",
 "suggestion": "museum",
 "page_facts": "players | 3 | Kit; Lou; Bea\nlines | 11\nquestions | Kit 0; Lou 1 (line 6); Bea 1 (line 8)\ndenial_openers | none\nverdict_rule | blocking when any line's offer is blocked; else no_game when no unusual thing is found; else game_dropped when the game is played again fewer than 2 times; else found_the_game",
 "scene": "1 | Kit | Welcome to the museum. I'm your guide for the tour this morning.\n2 | Lou | Thanks. My sister and I have come all the way across town for this.\n3 | Kit | Wonderful. Before we start, I need to ask the paintings if they're ready for visitors.\n4 | Bea | Of course. We can wait right here by the entrance.\n5 | Kit | First stop is the main hall. It was built a long time ago.\n6 | Lou | The floor is beautiful. Is it marble?\n7 | Kit | Yes, all marble. Please walk slowly, it gets slippery.\n8 | Bea | We'll walk slowly. Where is the gift shop?\n9 | Kit | Down the stairs on the left, next to the cafe.\n10 | Lou | Let's look at the gift shop after the tour, Bea.\n11 | Bea | Good idea. And then lunch in the cafe."
}

Reply:

{
 "task": "notes",
 "verdict": "game_dropped",
 "verdict_reason": "Game dropped: the odd paintings idea surfaced once at line 3 and came back 0 times after that.",
 "base_reality": {
  "who": "Kit is the museum guide; Lou and Bea are visitors",
  "where": "A museum, including the entrance, main hall, gift shop and cafe",
  "what": "Kit guides Lou and Bea on a museum tour"
 },
 "first_unusual": {
  "line": 3,
  "what": "Kit says paintings must be asked if they are ready"
 },
 "game": "If paintings have moods, Kit must constantly check and report what they want",
 "lines": [
  {
   "n": 1,
   "offer": "Welcomes everyone to the museum and introduces the morning tour",
   "fate": "accepted",
   "by": 2
  },
  {
   "n": 2,
   "offer": "Lou and a sister traveled across town for the tour",
   "fate": "ignored",
   "by": 0
  },
  {
   "n": 3,
   "offer": "Paintings must be asked if they are ready for visitors",
   "fate": "accepted",
   "by": 4
  },
  {
   "n": 4,
   "offer": "Offers to wait by the entrance while Kit checks",
   "fate": "ignored",
   "by": 0
  },
  {
   "n": 5,
   "offer": "Main hall is the first stop and very old",
   "fate": "built",
   "by": 6
  },
  {
   "n": 6,
   "offer": "Floor is beautiful; asks whether it is marble",
   "fate": "built",
   "by": 7
  },
  {
   "n": 7,
   "offer": "Confirms marble floor and warns it gets slippery",
   "fate": "accepted",
   "by": 8
  },
  {
   "n": 8,
   "offer": "Agrees to walk slowly and asks about the gift shop",
   "fate": "built",
   "by": 9
  },
  {
   "n": 9,
   "offer": "Gift shop is downstairs next to the cafe",
   "fate": "built",
   "by": 10
  },
  {
   "n": 10,
   "offer": "Proposes visiting the gift shop after the tour",
   "fate": "built",
   "by": 11
  },
  {
   "n": 11,
   "offer": "Agrees, and proposes lunch in the cafe afterward",
   "fate": "none",
   "by": 0
  }
 ],
 "game_moves": [],
 "listening": {
  "line": 2,
  "underneath": "Wants the trip to feel worthwhile after coming \"all the way across town\" for this",
  "reply_line": 3,
  "picked_up": false
 },
 "notes": [
  {
   "line": 4,
   "note": "This is where the paintings idea could have continued, but the scene moved on to waiting instead."
  },
  {
   "line": 9,
   "note": "Good build: answering exactly where the gift shop is, adding the cafe right next to it."
  },
  {
   "line": 2,
   "note": "The sister and the trip across town are never mentioned again after this line."
  }
 ],
 "next_beats": [
  {
   "speaker": "Kit",
   "line": "I just heard the painting in the main hall say it's not ready yet."
  },
  {
   "speaker": "Lou",
   "line": "Then let's ask the marble floor if it agrees with the painting."
  },
  {
   "speaker": "Bea",
   "line": "Now the cafe says it wants to serve lunch to the paintings too."
  }
 ],
 "closing": "A charming odd idea appeared once - a next scene could let it return again and again."
}

Build the input yourself

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

const K = SceneKit;
const built = K.buildInput({ suggestion: "lighthouse", scene: "Mo: The new bulb came today.\nPriya: Great. Let's get it upstairs.\nMo: I'm scared of the dark.\nPriya: A keeper scared of the dark!" });
built.scene.players;             // ["Mo", "Priya"]
built.prescan.denial;            // line numbers that open like a denial
const wire = K.toWire(built.input);            // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.verdictFrom(reply)                        // the verdict the reply's own fields give
// K.check(reply, built).notes                 // the page's re-check of the reply
// K.md(built, reply)                          // the notes 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://yes-and.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":"yes-and"}' | 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": "notes", "suggestion": "lighthouse", "page_facts": "players | 2 | Mo; Priya\n...", "scene": "1 | Mo | ..."} - 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 yes-and:<task>:<inputHash>:a<attempt> (the one task is notes), 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="yes-and:notes:$(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: yes-and:notes:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json