Call Every Which Way from your own code
The one metered task, whatif, is available over HTTP. It takes one small everyday what-if in the person's words, how things are now, what they hope for and what they worry about (each none when left blank), and how far ahead to look (a month or three months or a year), and returns five channels, one per kind (expected, better, worse, wildcard, unchanged), each with a likelihood (likely, plausible, unlikely), a short name, what happens down it, the first sign the person would notice and one small thing to do now; then a verdict (just_try, try_small, think_first), the smallest first trial and a note.
The division of labour first: the page builds the input, draws the what-if as a river that splits into five channels, makes each channel as wide as its likelihood and floats paper boats down them; the run writes the channels and sets each likelihood by fixed tests. After the reply the page keeps one channel per kind in its own order, holds expected at likely and wildcard at unlikely, works the verdict out itself from the five likelihoods, and flags a reply whose verdict disagrees, that quotes a number the person did not type, or that names an internal field. Your code can do the same with the engine files the page loads (see "Build the input yourself").
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"every-which-way"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, whatif, now, hope, fear, horizon) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the whatif run is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the rules
Every run sends task: whatif (the one task is whatif); a missing or different one is still answered as whatif. All fields are strings. What each holds, in the words of the system prompt, with the first sample's value:
| Field | Required | What it holds |
|---|---|---|
task | yes | Always whatif. If it is missing or different, still answer this task and set "task": "whatif". |
whatif | yes | The what-if, in the person's words. First sample: What if I read a chapter of a paperback on the bus instead of scrolling my phone? |
now | yes | How things are now, in their words, or none. First sample: My bus ride to work is 40 minutes each way. I have a pile of unread paperbacks by the bed. |
hope | yes | What they hope for, or none. First sample: Finish a few of those books |
fear | yes | What they worry about, or none. First sample: none |
horizon | yes | How far ahead to look: a month, three months or a year. First sample: a month |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
How the page builds the input (WhatKit.form, then WhatKit.buildInput): every field is squashed to one line, and a long one is shortened at a sentence end, else a word end, and marked [...] - the what-if at 300 characters, how things are now at 400, the hope and the worry at 200 and 200 characters. A blank field, or one reading none, n/a, nothing or -, is sent as none. The look-ahead is one of a month or three months or a year; anything else is sent as three months. The page will not build an input until the what-if has at least eight letters, and it warns (WhatKit.readiness) when the what-if does not start with "What if", when how things are now, the hope and the worry are all blank, and when a field was shortened.
What the page does free, before any run: it draws the river (Delta.draw) with the five channels in the order expected, better, worse, wildcard, unchanged, each as wide as its likelihood - a likely channel weighs 3, a plausible one 2 and an unlikely one 1 (WhatKit.weights, shown as each channel's share of the water by WhatKit.share) - and floats paper boats that each pick a channel by those weights from a fixed seed (WhatKit.drift), so the same river floats the same fleet. Before a run the person sets the chances themselves, starting from expected at likely, wildcard at unlikely and the rest at plausible (WhatKit.blankBranches); the page's verdict follows them at once (WhatKit.verdictFor).
The model's rules
As the system prompt states them, in order:
branches- exactly five entries, in this order ofkind:expected,better,worse,wildcard,unchanged.expected- the most ordinary way it goes, if the person does what the what-if says.better- it goes better than planned.worse- it goes worse than planned.wildcard- an unexpected turn, made only from things the input names, turned an unexpected way.unchanged- the person stops or drifts back, and things end up much as they are now.
likelihoodfor each branch, one oflikely,plausible,unlikely, decided only by these tests:expectedis alwayslikely.wildcardis alwaysunlikely.betterisplausiblewhennoworhopenames something already in place that this what-if would build on (a thing the person has, a skill, a person, a routine) and that the what-if would use; otherwiseunlikely. A thing or routine the what-if stops or replaces does not count.worseisplausiblewhennoworfearnames a specific thing this what-if would run into (a clash, a cost, a shortage, a limit, an event, a worry about a result); otherwiseunlikely. Afearthat is notnonealways counts.unchangedisplausiblewhen the what-if is something the person must keep up over the horizon (doing, or not doing, something repeatedly);unlikelywhen it is one act done once.
- Each branch has:
name- two to six words, a plain title in sentence case (only the first word and names capitalised); no two names alike; never starting with "What if".what- one sentence, at most 25 words: what happens down this channel, within the horizon.first_sign- one sentence, at most 20 words: the first thing the person would notice that shows they are on this channel.prepare- one sentence, at most 20 words: one small, physically sensible thing the person can do now, before it starts, using only things the input names or ordinary things anyone has. Forexpectedandbetterit helps that channel happen; forworse,wildcardandunchangedit softens that channel or makes it easier to notice - never a step that makes a worse, out-of-the-blue or back-where-you-were outcome more likely (do not keep the old habit within reach). No twopreparelines may contradict each other.
verdict- check in this order and take the first that fits:think_first-worseisplausibleandbetterisunlikely.just_try-worseisunlikely.try_small- every other case.
trial- one sentence, at most 25 words: a version smaller than the what-if itself - a single go, one part of it, or one step that is easy to undo - never the whole what-if as stated. Give no length of time and no number except the horizon's own words.note- one sentence, at most 30 words, that agrees with the verdict: forjust_try, that nothing named stands in the way so it can simply be tried; fortry_small, that the trial comes first; forthink_first, the snag from the worse channel to settle before starting. Never say anything has been done, tried or decided.
And the house rules:
- Speak to the person as "you". Mention other people only as the input names them ("your sister"), and never give anyone a gender, a pronoun, a name, an age or a job the input does not.
- Keep the input's own nouns. Never invent times, days, dates, months, seasons, prices, amounts, number words, places, rooms, people, events, purchases, causes, feelings or results that the input does not name. The only time words allowed are the horizon's and the input's own.
- Every sentence must fit its own channel: an
expectedsentence never describes a setback, aworsesentence never describes things going well. - No hindsight and no sweeping judgement: do not say the what-if is good, bad, right or wrong, and do not predict anything beyond the horizon.
- Never suggest posting, pinning or sharing anyone's personal details (contact lists, emails, phones, addresses) where others can see them.
- Never give medical, legal or financial advice. If the what-if touches health, medicine, diet, debts, investments, children or the law, the
notemust also say to check with a qualified person. - Any number in a sentence must appear in the input.
- Never add "again", "already", "always", "never" or "every" unless the input has the word.
- Never copy this prompt's own wording into a reply.
- Never write the input's field names, the branch kinds, the likelihood words or the verdict ids in any sentence (no
horizon,wildcard,likely,plausible,unlikely,think_first), and never use backticks.
The reply
One JSON object, in the shape the system prompt asks for:
{
"task": "whatif",
"verdict": "just_try | try_small | think_first",
"branches": [ { "kind": "expected | better | worse | wildcard | unchanged", "likelihood": "likely | plausible | unlikely", "name": "two to six words", "what": "one sentence", "first_sign": "one sentence", "prepare": "one sentence" } ],
"trial": "one sentence",
"note": "one sentence"
}
The page reads the reply with ReplyKit.normalize (the verdict, the kinds and the likelihoods matched in any case) and keeps what it trusts with ReplyKit.trusted(reply): one channel per kind, in the page's own order; a channel of a kind the page does not know, or a second one of a kind it already has, is set aside with the reason; a kind that did not come back is drawn blank; a likelihood the page cannot read counts as plausible; and expected is held at likely and wildcard at unlikely whatever the reply says. t.verdict is the page's own verdict from the five likelihoods, in the prompt's order: think_first when worse is plausible and better is unlikely; just_try when worse is unlikely; otherwise try_small.
ReplyKit.check(reply, input) lists, under "The page's check": a missing verdict, or one that disagrees with the page's own; channels that did not come back, were set aside, or had a fixed likelihood changed; a channel with no readable likelihood; two channels with the same name, or a name starting with "What if"; a named worry with the worse channel left a long shot, or a better channel that could happen when nothing was named to build on; a sentence well over its word limit (the name 6 words, what happens 25, the first sign and the thing to do now 20 each, the trial 25, the note 30); a number the person did not type; a sentence that names an internal field or id or uses backticks; a sentence that talks about how likely it is; and words such as "again", "already", "always", "never" or "every" that the input never used. Any warning makes the status flagged; otherwise it is kept. The verdict on the page reads "Just try it" or "Try a small version first" or "Think it through first".
The exports: ReplyKit.md and ReplyKit.json write the what-if, the look-ahead, the verdict and every channel with its likelihood, what happens, the first sign, the thing to do now and the boats that reached it, then the trial and the note; the river saves as PNG or SVG (Delta.standalone), and the page prints.
Worked examples - every sample
These are the free samples on the page: the form as typed, the exact input the page sends, the reply that replays for free, and what the page does with it - the five channels it draws, each with its share of the water, the verdict it works out against the reply's own, and its check. Together they cover the verdicts just_try and try_small and think_first.
A book on the bus
The form - what if: What if I read a chapter of a paperback on the bus instead of scrolling my phone?; how things are now: My bus ride to work is 40 minutes each way. I have a pile of unread paperbacks by the bed.; the hope: Finish a few of those books; the worry: none; looking ahead: a month.
The page's reading of the form (WhatKit.hints, shown beside the chances; the run decides): something in place to build on - yes; a named worry - no; something to keep up over time - yes.
Input:
{
"task": "whatif",
"whatif": "What if I read a chapter of a paperback on the bus instead of scrolling my phone?",
"now": "My bus ride to work is 40 minutes each way. I have a pile of unread paperbacks by the bed.",
"hope": "Finish a few of those books",
"fear": "none",
"horizon": "a month"
}
Reply:
{
"task": "whatif",
"verdict": "just_try",
"branches": [
{
"kind": "expected",
"likelihood": "likely",
"name": "Chapters add up on the bus",
"what": "Over the month you read a chapter on most rides, moving steadily through the pile by your bed.",
"first_sign": "You notice you have read part of the chapter before the ride ends.",
"prepare": "Keep a paperback from the pile by the bed in your bag for the bus."
},
{
"kind": "better",
"likelihood": "plausible",
"name": "The pile by the bed shrinks",
"what": "You move through the pile by the bed faster than planned, finishing paperbacks before the month ends.",
"first_sign": "You notice you have moved on to another paperback from the pile sooner than you thought you would.",
"prepare": "Set the next paperback from the pile on top so it is ready when you finish the current paperback."
},
{
"kind": "worse",
"likelihood": "unlikely",
"name": "The phone wins some rides",
"what": "On several rides you open your phone before you get far into the chapter, and the pile by the bed barely moves.",
"first_sign": "You notice the phone is open in your hand before you reach the end of the chapter.",
"prepare": "Put the phone in your bag so it takes an extra step to reach while you ride."
},
{
"kind": "wildcard",
"likelihood": "unlikely",
"name": "The chapter follows you to bed",
"what": "A chapter grips you enough that you keep reading in bed instead of putting the paperback down after the ride.",
"first_sign": "You notice you are still reading the paperback in bed well after the ride has ended.",
"prepare": "Decide where in the chapter to leave off before you start reading, to notice if you continue in bed."
},
{
"kind": "unchanged",
"likelihood": "plausible",
"name": "The paperback stays by the bed",
"what": "Over the month the phone stays the easier reach and the pile by the bed stays unread much as it was before.",
"first_sign": "You notice the pile by the bed looks the same as it did at the start.",
"prepare": "Leave the pile by the bed somewhere you walk past, so you can notice if it is not shrinking."
}
],
"trial": "Carry a paperback from the pile onto the bus and read until the ride ends, leaving your phone untouched that ride.",
"note": "Nothing you have named stands in the way, so there is nothing stopping you from trying it on a bus ride."
}
The channels the page trusts and draws (ReplyKit.trusted), with each one's share of the water:
| # | Channel | Likelihood | Water | Name | What happens | First sign | Do now |
|---|---|---|---|---|---|---|---|
| 1 | As expected (expected) | likely (likely) | 33% | Chapters add up on the bus | Over the month you read a chapter on most rides, moving steadily through the pile by your bed. | You notice you have read part of the chapter before the ride ends. | Keep a paperback from the pile by the bed in your bag for the bus. |
| 2 | Better than planned (better) | plausible (could happen) | 22% | The pile by the bed shrinks | You move through the pile by the bed faster than planned, finishing paperbacks before the month ends. | You notice you have moved on to another paperback from the pile sooner than you thought you would. | Set the next paperback from the pile on top so it is ready when you finish the current paperback. |
| 3 | Worse than planned (worse) | unlikely (a long shot) | 11% | The phone wins some rides | On several rides you open your phone before you get far into the chapter, and the pile by the bed barely moves. | You notice the phone is open in your hand before you reach the end of the chapter. | Put the phone in your bag so it takes an extra step to reach while you ride. |
| 4 | Out of the blue (wildcard) | unlikely (a long shot) | 11% | The chapter follows you to bed | A chapter grips you enough that you keep reading in bed instead of putting the paperback down after the ride. | You notice you are still reading the paperback in bed well after the ride has ended. | Decide where in the chapter to leave off before you start reading, to notice if you continue in bed. |
| 5 | Back where you were (unchanged) | plausible (could happen) | 22% | The paperback stays by the bed | Over the month the phone stays the easier reach and the pile by the bed stays unread much as it was before. | You notice the pile by the bed looks the same as it did at the start. | Leave the pile by the bed somewhere you walk past, so you can notice if it is not shrinking. |
The verdict: just_try - "Just try it"; it agrees with the page's own verdict from the five likelihoods.
The trial: "Carry a paperback from the pile onto the bus and read until the ride ends, leaving your phone untouched that ride."
The note: "Nothing you have named stands in the way, so there is nothing stopping you from trying it on a bus ride."
The page's check (ReplyKit.check): kept.
No notes.
A board-game night
The form - what if: What if I hosted a board-game night in my flat?; how things are now: I have a shelf of games and a big kitchen table, but only four chairs.; the hope: See my old uni friends more often; the worry: People cancel at the last minute.; looking ahead: three months.
The page's reading of the form (WhatKit.hints, shown beside the chances; the run decides): something in place to build on - yes; a named worry - yes; something to keep up over time - no.
Input:
{
"task": "whatif",
"whatif": "What if I hosted a board-game night in my flat?",
"now": "I have a shelf of games and a big kitchen table, but only four chairs.",
"hope": "See my old uni friends more often",
"fear": "People cancel at the last minute.",
"horizon": "three months"
}
Reply:
{
"task": "whatif",
"verdict": "try_small",
"branches": [
{
"kind": "expected",
"likelihood": "likely",
"name": "Friends gather at your kitchen table",
"what": "Over three months you host a board-game night, your old uni friends fill your four chairs, and you play games from your shelf.",
"first_sign": "Your old uni friends say yes when you ask them over for a board-game night.",
"prepare": "Message your old uni friends now to ask who could come to a board-game night."
},
{
"kind": "better",
"likelihood": "plausible",
"name": "Old friends want more nights",
"what": "Your old uni friends enjoy it so much that over three months you host more nights with games from your shelf.",
"first_sign": "Your old uni friends ask when the next board-game night will be before you have even cleared the table.",
"prepare": "Keep your shelf of games handy and your kitchen table clear so another night is easy to set up."
},
{
"kind": "worse",
"likelihood": "plausible",
"name": "Friends cancel at the last minute",
"what": "Over three months, some old uni friends cancel at the last minute, leaving empty chairs around your kitchen table.",
"first_sign": "An old uni friend messages at the last minute to say they cannot come after all.",
"prepare": "Ask your old uni friends to let you know early if they cannot come."
},
{
"kind": "wildcard",
"likelihood": "unlikely",
"name": "Too many friends for four chairs",
"what": "More old uni friends want to come than your four chairs can hold, filling your flat around your kitchen table.",
"first_sign": "More old uni friends say yes than your four chairs can seat at your kitchen table.",
"prepare": "Think about how many old uni friends your four chairs could seat before you invite them."
},
{
"kind": "unchanged",
"likelihood": "unlikely",
"name": "The games stay on the shelf",
"what": "Over three months the board-game night does not happen, your games stay on the shelf, and you still have four chairs.",
"first_sign": "You keep thinking about a board-game night, but you have not asked your old uni friends over.",
"prepare": "Set a reminder to check in three months if you still have not asked your old uni friends."
}
],
"trial": "Ask an old uni friend over to try a game from your shelf at your kitchen table instead of planning the whole night.",
"note": "Having an old uni friend over to play a game is worth doing before you plan the whole board-game night."
}
The channels the page trusts and draws (ReplyKit.trusted), with each one's share of the water:
| # | Channel | Likelihood | Water | Name | What happens | First sign | Do now |
|---|---|---|---|---|---|---|---|
| 1 | As expected (expected) | likely (likely) | 33% | Friends gather at your kitchen table | Over three months you host a board-game night, your old uni friends fill your four chairs, and you play games from your shelf. | Your old uni friends say yes when you ask them over for a board-game night. | Message your old uni friends now to ask who could come to a board-game night. |
| 2 | Better than planned (better) | plausible (could happen) | 22% | Old friends want more nights | Your old uni friends enjoy it so much that over three months you host more nights with games from your shelf. | Your old uni friends ask when the next board-game night will be before you have even cleared the table. | Keep your shelf of games handy and your kitchen table clear so another night is easy to set up. |
| 3 | Worse than planned (worse) | plausible (could happen) | 22% | Friends cancel at the last minute | Over three months, some old uni friends cancel at the last minute, leaving empty chairs around your kitchen table. | An old uni friend messages at the last minute to say they cannot come after all. | Ask your old uni friends to let you know early if they cannot come. |
| 4 | Out of the blue (wildcard) | unlikely (a long shot) | 11% | Too many friends for four chairs | More old uni friends want to come than your four chairs can hold, filling your flat around your kitchen table. | More old uni friends say yes than your four chairs can seat at your kitchen table. | Think about how many old uni friends your four chairs could seat before you invite them. |
| 5 | Back where you were (unchanged) | unlikely (a long shot) | 11% | The games stay on the shelf | Over three months the board-game night does not happen, your games stay on the shelf, and you still have four chairs. | You keep thinking about a board-game night, but you have not asked your old uni friends over. | Set a reminder to check in three months if you still have not asked your old uni friends. |
The verdict: try_small - "Try a small version first"; it agrees with the page's own verdict from the five likelihoods.
The trial: "Ask an old uni friend over to try a game from your shelf at your kitchen table instead of planning the whole night."
The note: "Having an old uni friend over to play a game is worth doing before you plan the whole board-game night."
The page's check (ReplyKit.check): kept.
No notes.
Letting the grey grow out
The form - what if: What if I stopped dyeing my hair and let the grey grow out?; how things are now: I dye it at home every six weeks. My sister's wedding is in May and there will be photos.; the hope: none; the worry: Looking patchy in the wedding photos.; looking ahead: a year.
The page's reading of the form (WhatKit.hints, shown beside the chances; the run decides): something in place to build on - yes; a named worry - yes; something to keep up over time - yes.
Input:
{
"task": "whatif",
"whatif": "What if I stopped dyeing my hair and let the grey grow out?",
"now": "I dye it at home every six weeks. My sister's wedding is in May and there will be photos.",
"hope": "none",
"fear": "Looking patchy in the wedding photos.",
"horizon": "a year"
}
Reply:
{
"task": "whatif",
"verdict": "think_first",
"branches": [
{
"kind": "expected",
"likelihood": "likely",
"name": "Grey grows out gradually",
"what": "Over the year your hair gradually grows out from dyed to grey, with more grey showing as time passes.",
"first_sign": "You notice your hair looking dyed at the ends and grey nearer your head.",
"prepare": "Skip your next home dye and let your hair start growing out instead."
},
{
"kind": "better",
"likelihood": "unlikely",
"name": "Not patchy by the wedding",
"what": "By May your grey has grown in enough that the wedding photos do not look patchy.",
"first_sign": "You notice your grey covering more of your hair before your sister's wedding in May.",
"prepare": "Trim the dyed ends of your hair at home instead of dyeing them before May."
},
{
"kind": "worse",
"likelihood": "plausible",
"name": "Patchy grow-out before the wedding",
"what": "Your hair looks patchy, part grey and part dyed, in the wedding photos taken in May.",
"first_sign": "You notice your grey and dyed hair looking patchy before your sister's wedding.",
"prepare": "Try a hairstyle that blends the grey and dyed hair together before the wedding in May."
},
{
"kind": "wildcard",
"likelihood": "unlikely",
"name": "Dye returns before the wedding",
"what": "You dye your hair at home before the wedding in May, turning back from letting it grow out.",
"first_sign": "You notice yourself reaching for the dye at home before your sister's wedding arrives.",
"prepare": "Put the dye you have at home out of easy reach before the year begins."
},
{
"kind": "unchanged",
"likelihood": "plausible",
"name": "Dyeing carries on at home",
"what": "Over the year you keep dyeing your hair at home every six weeks and the grey stays covered.",
"first_sign": "You notice yourself back at dyeing your hair at home every six weeks.",
"prepare": "Keep track of whether the six-weekly dye routine at home quietly starts back up."
}
],
"trial": "Skip your next home dye instead of deciding to stop dyeing for the whole year.",
"note": "Before you start, it helps to settle how the grey growing in might look patchy in the wedding photos in May."
}
The channels the page trusts and draws (ReplyKit.trusted), with each one's share of the water:
| # | Channel | Likelihood | Water | Name | What happens | First sign | Do now |
|---|---|---|---|---|---|---|---|
| 1 | As expected (expected) | likely (likely) | 33% | Grey grows out gradually | Over the year your hair gradually grows out from dyed to grey, with more grey showing as time passes. | You notice your hair looking dyed at the ends and grey nearer your head. | Skip your next home dye and let your hair start growing out instead. |
| 2 | Better than planned (better) | unlikely (a long shot) | 11% | Not patchy by the wedding | By May your grey has grown in enough that the wedding photos do not look patchy. | You notice your grey covering more of your hair before your sister's wedding in May. | Trim the dyed ends of your hair at home instead of dyeing them before May. |
| 3 | Worse than planned (worse) | plausible (could happen) | 22% | Patchy grow-out before the wedding | Your hair looks patchy, part grey and part dyed, in the wedding photos taken in May. | You notice your grey and dyed hair looking patchy before your sister's wedding. | Try a hairstyle that blends the grey and dyed hair together before the wedding in May. |
| 4 | Out of the blue (wildcard) | unlikely (a long shot) | 11% | Dye returns before the wedding | You dye your hair at home before the wedding in May, turning back from letting it grow out. | You notice yourself reaching for the dye at home before your sister's wedding arrives. | Put the dye you have at home out of easy reach before the year begins. |
| 5 | Back where you were (unchanged) | plausible (could happen) | 22% | Dyeing carries on at home | Over the year you keep dyeing your hair at home every six weeks and the grey stays covered. | You notice yourself back at dyeing your hair at home every six weeks. | Keep track of whether the six-weekly dye routine at home quietly starts back up. |
The verdict: think_first - "Think it through first"; it agrees with the page's own verdict from the five likelihoods.
The trial: "Skip your next home dye instead of deciding to stop dyeing for the whole year."
The note: "Before you start, it helps to settle how the grey growing in might look patchy in the wedding photos in May."
The page's check (ReplyKit.check): kept.
No notes.
Build the input yourself
Load the engine in node (global.window = global; require("./whatkit.js"); require("./replykit.js"); require("./delta.js")), then:
const K = WhatKit, R = ReplyKit, D = Delta;
const form = K.form({ whatif: "What if I read a chapter of a paperback on the bus instead of scrolling my phone?",
now: "My bus ride to work is 40 minutes each way. I have a pile of unread paperbacks by the bed.",
hope: "Finish a few of those books", fear: "none", horizon: "a month" });
// form: each field on one line and clipped at a sentence end; form.cut lists the fields that were shortened
K.readiness(form); // {ok: false, note} until the what-if says enough, else {ok: true, warn}
const built = K.buildInput(form); // built.input: blank fields become "none"
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// const t = R.trusted(reply) // {branches: the five in the page's order, got, dropped, pinned, verdict}
// R.check(reply, built.input) // {status: kept or flagged, notes, verdict}
// D.draw(t.branches, { id: "x" }) // the river as SVG text, each channel as wide as its likelihood
// K.drift(K.weights(t.branches), 20, 7) // {counts, seq}: twenty boats from seed 7
Form keys: whatif, now, hope and fear (the person's words, or empty) and horizon (a month or three months or a year). K.verdictFor(K.likeMap(branches)) gives the page's verdict for chances you set yourself.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. 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://every-which-way.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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://every-which-way.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// every-which-way.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://every-which-way.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://every-which-way.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// EveryWhichWay.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class EveryWhichWay {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://every-which-way.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://every-which-way.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://every-which-way.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://every-which-way.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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":"every-which-way"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "every-which-way"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "every-which-way" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "every-which-way"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "every-which-way"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "every-which-way" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "every-which-way"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "every-which-way" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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": "whatif", "whatif": "What if I read a chapter of a paperback on the bus instead of scrolling my phone?", "now": "My bus ride to work is 40 minutes each way. I have a pile of unread paperbacks by the bed.", "hope": "Finish a few of those books", "fear": "none", "horizon": "a month"}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "whatif", "whatif": "What if I read a chapter of a paperback on the bus instead of scrolling my phone?", "now": "My bus ride to work is 40 minutes each way. I have a pile of unread paperbacks by the bed.", "hope": "Finish a few of those books", "fear": "none", "horizon": "a month"}
# - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "whatif", whatif, now, hope, fear, horizon} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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 every-which-way:<task>:<inputHash>:a<attempt> (the one task is whatif), 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="every-which-way:whatif:$(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}'
jq -r '.verdict, .trial, (.branches[] | "\(.kind) \(.likelihood) \(.name)")' reply.json
import hashlib, time
key = "every-which-way:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
print(reply.get("trial", ""))
for br in reply.get("branches") or []:
print(br["kind"], br["likelihood"], br["name"])
import { createHash } from "node:crypto";
const key = `every-which-way:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
console.log(reply.trial);
for (const br of reply.branches || []) console.log(br.kind, br.likelihood, br.name);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "every-which-way:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
branches, _ := reply["branches"].([]any)
fmt.Println(j["charged_credits"], j["truncated"], reply["verdict"])
fmt.Println(reply["trial"])
for _, x := range branches {
br := x.(map[string]any)
fmt.Println(br["kind"], br["likelihood"], br["name"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "every-which-way:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.get("verdict"));
System.out.println(reply.path("trial").asText());
for (JsonNode br : reply.path("branches")) System.out.println(br.path("kind").asText() + " " + br.path("likelihood").asText() + " " + br.path("name").asText());
require "digest"
key = "every-which-way:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"]].inspect
puts reply["trial"]
(reply["branches"] || []).each { |br| puts "#{br["kind"]} #{br["likelihood"]} #{br["name"]}" }
$key = "every-which-way:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["verdict"] ?? ""), "\n";
echo $reply["trial"] ?? "", "\n";
foreach ($reply["branches"] ?? [] as $br) echo $br["kind"], " ", $br["likelihood"], " ", $br["name"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"every-which-way:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply!["verdict"]}");
Console.WriteLine($"{reply["trial"]}");
foreach (var br in reply["branches"] as JsonArray ?? new JsonArray()) Console.WriteLine($"{br!["kind"]} {br["likelihood"]} {br["name"]}");
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: every-which-way:whatif:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "whatif", "whatif": "What if I read a chapter of a paperback on the bus instead of scrolling my phone?", "now": "My bus ride to work is 40 minutes each way. I have a pile of unread paperbacks by the bed.", "hope": "Finish a few of those books", "fear": "none", "horizon": "a month"}
# - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "every-which-way:whatif:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `every-which-way:whatif:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "every-which-way:whatif:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "every-which-way:whatif:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "every-which-way:whatif:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: every-which-way:whatif:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"every-which-way:whatif:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);