← Want Behind
API

Drive Want Behind from your own code

The one metered task, interview, is available over HTTP. Each call is one turn of an interview about one everyday wish: you send the wish, the turns so far with your answers and the page's reading, and get back the next turn - the interviewer's read of the want underneath the wish with an honest confidence, then either one question with a guess attached, a six-line restate of the want, or a step back to something more basic.

The division of labour first: the page reads and keeps the interview; the model takes one turn. Every call is stateless - the page sends the whole interview so far as transcript, and the reading it makes for free (how the wish is phrased, which parts of the want you said in your own words, any "should want" phrases) as facts. The model applies fixed rules for the kind of turn from its own confidence; the page re-checks every turn against those rules.

Base URL and the envelope

Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:

{ "ok": true,  "data":  { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }

The token is minted for this app (the guest call takes {"slug":"want-behind"}), 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, wish, context, transcript, reaction, round, facts) is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: interview turns are metered and need a personal sign-in.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field, the input and the rules

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

FieldRequiredWhat it holds
taskyesAlways interview.
wishyesThe wish as typed, e.g. I want a new phone.
contextyesAnything else said, plus lines asked yourself - <part>: <answer> for the page's free questions; or nothing more given.
transcriptyesEarlier turns, oldest first, one line each: r1 ask (20%) | Q: ... | your guess: ... | they said: ..., r5 restate (90%) | they said: ... or r6 step back (30%) | Q: ... | they said: ...; none yet on turn 1. A long interview keeps turn 1 and the newest turns and names the gap (rounds 2-3 left out to save space).
reactionyesThe answer to the last turn (after a restate, the correction), or none yet.
roundyesThe number of this turn, from 1; at most 10.
factsyesThe page's reading, one line each: shape (thing, change, feeling, vague, other), said and not_said (from who, why_now, success, constraint, out_of_scope), should_flag (none, or quoted phrases from the newest thing you wrote), rounds_left.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

The kind of turn, first match wins, from the reply's own confidence (how well it could predict your answers to its next three questions): no turns left - restate at 60% or more, else step_back; turn 6 or later below 60%, unless the last turn was a restate - step_back; 90% or more - restate; otherwise ask. On turn 1 with no context, confidence is at most 40%. When should_flag is not none and the turn asks, heard quotes the phrase and the question asks what you would want with no one to justify it to.

The reply

One JSON object, no prose around it:

{"task": "interview", "round": 1, "mode": "ask", "hypothesis": "You want ...", "confidence": 20, "missing": ["who", ...], "heard": "", "question": "...?", "guess": "My guess: ...", "options": ["...", "..."], "restate": null, "satisfiers": [], "check_first": "", "step_back": ""}
FieldWhat it holds
round, modeThe turn number, and ask, restate or step_back.
hypothesisThe current read of the want, starting "You want", at most 30 words.
confidenceA whole number 0-100 in steps of 5.
missingParts it still cannot predict (ids as in facts); empty on a restate.
heardThe flagged "should want" phrase it took up, or empty.
question, guess, optionsOn ask: one question, a guess starting "My guess:", and two to four answers to tap. On step_back: a more basic question and its answers (no guess).
restateOn restate: outcome, for_whom, why_now, success, constraint, out_of_scope (lines never said start with "Probably"); otherwise null.
satisfiers, check_firstOn restate: two to four {option, kind, why} (kind is the wish, smaller or different; at least one is not the wish) and one thing to check first.
step_backOn step_back: what seems to be missing, at most 40 words.

The page then checks the turn (WishKit.check) - the turn number, the kind of turn for its confidence, the first-turn cap, the "should want" rule, one question ending in a question mark with a guess, no question asked twice, a complete restate with a smaller or different route and a check-first, and no numbers you never gave - and lists anything it finds under "The page's check of this turn". Confirming a restate is not a run: only an explicit yes on the page confirms it, and "sounds good" is treated as a polite exit.

Worked examples - every kind of turn

These are the 5 free samples on the page: the exact input the page sends for the next turn and the reply that replays for free. Together they cover a first question, a question after a "should want" answer, a restate, a question after a corrected restate, and a step back.

A new phone - turn 1, a question at 20%

Input:

{
 "task": "interview",
 "wish": "I want a new phone",
 "context": "nothing more given",
 "transcript": "none yet",
 "reaction": "none yet",
 "round": "1",
 "facts": "shape: thing\nsaid: none\nnot_said: who, why_now, success, constraint, out_of_scope\nshould_flag: none\nrounds_left: 9"
}

Reply:

{
 "task": "interview",
 "round": 1,
 "mode": "ask",
 "hypothesis": "You want your phone to stop getting in your way, so the day runs smoothly again without you wrestling with it.",
 "confidence": 20,
 "missing": [
  "who",
  "why_now",
  "success",
  "constraint",
  "out_of_scope"
 ],
 "heard": "",
 "question": "What has your current phone been doing lately that made you start wanting a new one?",
 "guess": "My guess: it has started to annoy you in small daily ways, slow or quick to run flat, rather than you simply fancying something shiny.",
 "options": [
  "It's slow and dies by the afternoon",
  "The camera lets me down",
  "Nothing's wrong, I just fancy a change",
  "It's cracked and a bit broken"
 ],
 "restate": null,
 "satisfiers": [],
 "check_first": "",
 "step_back": ""
}

Getting a dog - turn 3, a question at 45%

Input:

{
 "task": "interview",
 "wish": "I want to get a dog",
 "context": "We live in a small flat and both work from the office three days a week.",
 "transcript": "r1 ask (25%) | Q: When you picture an ordinary evening with a dog, what is the best part of it? | your guess: My guess: it's the welcome at the door and the excuse to get outside, more than the dog itself. | they said: The walks, mostly. I sit at a desk all day and a dog would get me outside every evening, rain or not.\nr2 ask (40%) | Q: What stops you heading out for an evening walk now, on your own? | your guess: My guess: on your own it feels a bit pointless, so staying in wins, and a dog would turn the walk into something that has to happen. | they said: Nothing really, I just don't go. I should probably wait until we have a garden though, everyone says flats are no good for dogs.",
 "reaction": "Nothing really, I just don't go. I should probably wait until we have a garden though, everyone says flats are no good for dogs.",
 "round": "3",
 "facts": "shape: thing\nsaid: who, constraint\nnot_said: why_now, success, out_of_scope\nshould_flag: \"i should\", \"should probably\", \"everyone\"\nrounds_left: 7"
}

Reply:

{
 "task": "interview",
 "round": 3,
 "mode": "ask",
 "hypothesis": "You want a dog that gets you outside every evening, and the talk about gardens and flats may be what everyone else wants for you.",
 "confidence": 45,
 "missing": [
  "why_now",
  "success",
  "out_of_scope"
 ],
 "heard": "everyone",
 "question": "If you had no one to justify it to, would you want a dog in the flat you have now?",
 "guess": "My guess: yes, the wish for the dog and the walks is yours, and the waiting for a garden is the part you picked up from other people.",
 "options": [
  "Yes, I'd get one now",
  "No, I'd still wait for a garden",
  "Really I just want the evening walks",
  "I'm honestly not sure"
 ],
 "restate": null,
 "satisfiers": [],
 "check_first": "",
 "step_back": ""
}

Learning Spanish - turn 5, the restate at 90%

Input:

{
 "task": "interview",
 "wish": "I want to learn Spanish properly",
 "context": "I've started and stopped an app three times.",
 "transcript": "r1 ask (20%) | Q: With no one to justify it to, what would you most like to be able to do in Spanish? | your guess: My guess: hold an easy conversation with someone, rather than pass a test, and the app kept drifting away from that. | they said: Just talk, really. Chat over dinner without someone translating for me. I don't care about tests.\nr2 ask (40%) | Q: Whose dinner table is it, the one where you would like to join in? | your guess: My guess: people you see often who speak Spanish among themselves, so for now you sit there while someone else does the talking for you. | they said: My partner's family. When they visit I smile and nod while my partner translates the jokes, and I hate missing out.\nr3 ask (65%) | Q: When you picture the visit where it finally works, what are you doing that you can't do now? | your guess: My guess: laughing at a joke before it gets translated and answering a question yourself, rather than speaking without a single mistake. | they said: Getting the jokes, and asking his mum about her garden myself. Not perfect grammar, just being part of it.\nr4 ask (80%) | Q: What made the app fizzle out each time you started it? | your guess: My guess: it felt like homework, drilling words a long way from the dinner table, so it was easy to let slide on a busy week. | they said: It felt like homework, all drills and streaks. I've only got evenings, and I want enough to chat by the summer when they're back.",
 "reaction": "It felt like homework, all drills and streaks. I've only got evenings, and I want enough to chat by the summer when they're back.",
 "round": "5",
 "facts": "shape: change\nsaid: who, why_now, success, constraint, out_of_scope\nnot_said: none\nshould_flag: none\nrounds_left: 5"
}

Reply:

{
 "task": "interview",
 "round": 5,
 "mode": "restate",
 "hypothesis": "You want enough everyday Spanish to get the jokes and chat with your partner's family yourself when they visit, not perfect grammar or tests.",
 "confidence": 90,
 "missing": [],
 "heard": "",
 "question": "",
 "guess": "",
 "options": [],
 "restate": {
  "outcome": "Enough everyday Spanish to get the jokes and chat at the table when your partner's family visit.",
  "for_whom": "You, so you are part of it with your partner's family, his mum included.",
  "why_now": "When they visit you smile and nod while your partner translates the jokes, and you hate missing out.",
  "success": "By the summer, you get the jokes and ask his mum about her garden yourself.",
  "constraint": "You've only got evenings, and anything that feels like homework fizzles out.",
  "out_of_scope": "Not perfect grammar, not tests, not drills and streaks."
 },
 "satisfiers": [
  {
   "option": "Learn the phrases from their table, a few each evening",
   "kind": "smaller",
   "why": "It fits your evenings and aims straight at getting the jokes, instead of drills that feel like homework."
  },
  {
   "option": "Regular spoken practice with a tutor or a conversation partner",
   "kind": "different",
   "why": "Chatting out loud is the skill you need for his mum and the garden, and it skips the grammar drills."
  },
  {
   "option": "Some Spanish-only evenings with your partner at home",
   "kind": "different",
   "why": "It is practice at a real table, and your partner already knows the jokes and the family."
  }
 ],
 "check_first": "Ask your partner what the family usually talk about at dinner, then try a few of those phrases out loud tonight.",
 "step_back": ""
}

Moving to the coast - turn 6, a question at 60%

Input:

{
 "task": "interview",
 "wish": "I want to move to the coast",
 "context": "nothing more given",
 "transcript": "r1 ask (20%) | Q: What would a day on the coast give you that your days right now don't? | your guess: My guess: room to breathe, slower mornings and the water nearby, more than any particular town or job. | they said: Space, I think. I grew up by the sea and the city feels cramped lately.\nr2 ask (35%) | Q: What has made the city feel cramped lately, compared with how it used to feel? | your guess: My guess: it's less about the streets and more about how your weeks go, busy but a bit empty, with nothing that feels like home. | they said: Long weeks indoors, and nobody to see at weekends. Back home there was always someone around, and the beach was right there.\nr3 ask (55%) | Q: When you picture a good weekend on the coast, what are you actually doing? | your guess: My guess: a long walk by the water with no plan, rather than anything busy, and the sea is the part that matters most. | they said: Walking along the front, then popping in somewhere for tea. Nothing planned, nothing booked.\nr4 ask (75%) | Q: What is the biggest thing keeping you in the city right now? | your guess: My guess: work, since a move means finding something by the coast, and that is the part that makes it feel far off. | they said: My job. I can't do it from home, and rent by the sea isn't cheap either.\nr5 restate (90%) | they said: Not quite. It's not the sea I miss, it's being near my sister.",
 "reaction": "Not quite. It's not the sea I miss, it's being near my sister.",
 "round": "6",
 "facts": "shape: change\nsaid: who, why_now, constraint, out_of_scope\nnot_said: success\nshould_flag: none\nrounds_left: 4"
}

Reply:

{
 "task": "interview",
 "round": 6,
 "mode": "ask",
 "hypothesis": "You want to be near your sister again, close enough for unplanned weekends and popping in for tea, and the sea was standing in for her.",
 "confidence": 60,
 "missing": [
  "success",
  "constraint"
 ],
 "heard": "",
 "question": "Where does your sister live now, back home by the sea or somewhere else?",
 "guess": "My guess: back home, which is why the wish came out as the coast, and the tea after the walk may well have been with her.",
 "options": [
  "Back home, by the sea",
  "Somewhere else, not by the sea",
  "Not far, but I rarely see her",
  "I'm not sure where she'll settle"
 ],
 "restate": null,
 "satisfiers": [],
 "check_first": "",
 "step_back": ""
}

Sorting my life out - turn 6, a step back at 30%

Input:

{
 "task": "interview",
 "wish": "I want to sort my life out",
 "context": "nothing more given",
 "transcript": "r1 ask (10%) | Q: If you could sort out a single corner of life first, which would it be? | your guess: My guess: a single area is quietly dragging the rest down, and everything else would feel lighter once it moved. | they said: I don't know, everything really. It all feels a bit of a mess.\nr2 ask (15%) | Q: Where did you notice the mess most this past week? | your guess: My guess: at home, in the small undone jobs you see every day, more than anything big at work. | they said: Work I guess, and the flat. I never get round to anything.\nr3 ask (25%) | Q: What is a thing you keep meaning to get round to, but never do? | your guess: My guess: something small in the flat that you walk past daily, which has started to feel bigger than it really is. | they said: There's a whole list. Nothing big, it just all adds up somehow.\nr4 ask (30%) | Q: How would you know, on an ordinary evening, that your life felt sorted? | your guess: My guess: you would sit down without that nagging list in your head, rather than having everything actually finished. | they said: Not sure. Maybe the list done, maybe just not thinking about it. Depends on the day.\nr5 ask (35%) | Q: When did life last feel sorted to you, even for a little while? | your guess: My guess: there was a stretch that felt calmer, and something about how it ran is what you are trying to get back. | they said: I don't know. Maybe a while back? Hard to say what was different then.",
 "reaction": "I don't know. Maybe a while back? Hard to say what was different then.",
 "round": "6",
 "facts": "shape: vague\nsaid: none\nnot_said: who, why_now, success, constraint, out_of_scope\nshould_flag: none\nrounds_left: 4"
}

Reply:

{
 "task": "interview",
 "round": 6,
 "mode": "step_back",
 "hypothesis": "You want the weight of work, the flat and a list of small undone things to lift, but what you want instead is still a blank.",
 "confidence": 30,
 "missing": [
  "who",
  "why_now",
  "success",
  "constraint",
  "out_of_scope"
 ],
 "heard": "",
 "question": "If nothing at all was hanging over you, what would you do with a free day?",
 "guess": "",
 "options": [
  "Something I've been missing out on",
  "Rest and do nothing at all",
  "See people I've lost touch with",
  "I honestly have no idea"
 ],
 "restate": null,
 "satisfiers": [],
 "check_first": "",
 "step_back": "After all these questions I still can't guess how you would answer what comes next, so something more basic is missing: what sorting things out is for, the life you would get back afterwards."
}

Build the input yourself

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

const K = WishKit;
const interview = K.form({ wish: "I want a new phone", context: "", turns: [] });   // turns: [{ reply, answer }]
const built = K.buildInput(interview);    // built.facts holds the page's reading; built.ok is false until the last turn is answered
const wire = K.toWire(built.input);       // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes             // the page's re-check of the turn
// interview.turns.push({ reply, answer: "The battery dies by evening" });   // and build the next turn

Form keys: wish, context (optional), self (optional answers to the page's own questions, keyed who, why_now, success, constraint, out_of_scope or honest) and turns. A turn with no answer stops buildInput until it is answered, and so does an answer to a restate that only says "sounds good".

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://want-behind.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":"want-behind"}' | 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": "interview", "wish": "I want a new phone", "context": "nothing more given", "transcript": "none yet", "reaction": "none yet", "round": "1", "facts": "...", ...}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

Step 5 — POST /run, then poll the job

Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses want-behind:<task>:t<round>:<inputHash>:a<attempt> (the one task is interview); its one reformat retry (with retry_note added) is attempt 2, and “ask me something else” reruns the same input under a new attempt number - each a new key and a real run. The job's output.output is the reply text; truncated: true means the balance cut it short.

KEY="want-behind:interview:$(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: want-behind:interview:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json