← Which Way It Blows
API

Drive Which Way It Blows from your own code

The one metered task, vane, is available over HTTP. It takes one everyday invention, tool, service or habit - the leaf blower, the group chat, voice notes - with a tone (wry, warm or gleeful), one to six notes numbered from 1 on how it shows up in the person's life, and the page's facts. It returns a status (ok or declined), and for ok a plain label for the thing, exactly four winds in a fixed order - what it enhances, pushes aside, brings back and flips into - each naming one thing, with a headline, a short reading, the note it draws on, a word-for-word quote and a strength from 1 to 5, then the prevailing wind, its reason and a closing line.

The division of labour first: the page numbers the notes; the model routes them to winds and writes the words; the page draws the vane, points the arrow at the single strongest wind and re-checks the reply - four winds in order, quotes word for word from their notes, strengths in range (a wind with no note behind it blows at 1 or 2), exactly one wind at the top and a prevailing wind that matches it. People, nature, weapons and drugs are declined. A thinking toy for fun, not professional, technical or policy advice.

Base URL and the envelope

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

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

The token is minted for this app (the guest call takes {"slug":"which-way-it-blows"}), 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, thing, tone, notes, page_facts) is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: reading the winds is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field and the status

Every run sends task: vane (the one task is vane); a missing or different one is still answered as vane. The status field holds one of two ids, checked in this order: declined when the thing itself is not something people made or do - a person or group of people, a living thing or piece of nature (an animal, a plant, the weather), or a weapon or a drug - decided by what the thing is, so "my flatmate's coffee machine" is a machine; otherwise ok. A brand or product name is read as its general kind of thing.

For ok, the prevailing field holds one of four ids - enhances, obsolesces, retrieves or reverses - the law of the single wind with the highest strength. The page shows them as enhances, pushes aside, brings back and flips into.

Input (every field is a string)

FieldRequiredWhat it holds
taskyesAlways vane.
thingyesThe invention, tool, service or habit, as typed, up to 60 characters (cut on a word past that).
toneyeswry, warm or gleeful.
notesyes1 to 6 lines as n | note, numbered from 1; bullets stripped, repeats dropped, each up to 140 characters.
page_factsyesOne key | ... per line: note_count, person_words_in_thing, nature_words_in_thing, capitalised_words_in_thing and vane_rule.
clip_notenoWhat the page left out, if anything.
retry_notenoOnly on the page's one reformat retry, after an unusable reply.

Reply (one JSON object)

{"task": "vane", "status": "ok", "reason": "", "thing_label": "the leaf blower",
 "winds": [{"law": "enhances", "what": "...", "headline": "...", "reading": "...", "note": 0, "note_quote": "", "strength": 2},
           {"law": "obsolesces", ...}, {"law": "retrieves", ...},
           {"law": "reverses", "what": "...", "headline": "...", "reading": "...", "note": 2, "note_quote": "...", "strength": 5}],
 "prevailing": "reverses", "prevailing_reason": "...", "closing": "..."}
FieldWhat it holds
status, reasonThe status by the order above; for declined, at most 30 words saying why (empty for ok).
thing_label1 to 5 lowercase words naming the thing plainly, starting with "the" or "a" (empty when declined).
windsExactly 4, law in the order enhances, obsolesces, retrieves, reverses ([] when declined). Each has what (1 to 4 words), headline (2 to 6), reading (at most 30), note (the note number it draws on, or 0), note_quote (1 to 8 words copied from that note, or "") and strength (1 to 5: 5 when two or more notes are routed to the wind, 4 for one, 3 for a hint, 1 or 2 with no note).
prevailing, prevailing_reasonThe law of the single strongest wind and at most 25 words on why ("" when declined).
closingA light line of at most 16 words.

The page then checks the reply (VaneKit.check): four winds in order, each naming something different from the others and from the thing, quotes word for word from the note they point at, a strength of 3 to 5 for a wind with a note and 1 or 2 without one, at least two winds drawing on notes, exactly one wind at the top, a prevailing wind that matches the strengths, no he or she, no brand-like word repeated from the thing, and the word limits. The arrow always follows the strengths. A declined reply should carry no winds; a reading for a thing that reads as a person, nature or a weapon is flagged.

Worked examples

These are the five free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover every outcome - a leaf blower (flips into prevails), a family group chat (pushes aside prevails), voice notes (brings back prevails), an e-bike (enhances prevails), and a flatmate, who is a person, so the reading is declined.

The leaf blower - flips into prevails

Input:

{
 "task": "vane",
 "thing": "the leaf blower",
 "tone": "wry",
 "notes": "1 | the leaves just end up on the neighbour's side\n2 | then the neighbour blows them straight back\n3 | everyone on the street runs one at eight on Saturday\n4 | I can't hear the radio while it's running",
 "page_facts": "note_count | 4\nperson_words_in_thing | none\nnature_words_in_thing | none\ncapitalised_words_in_thing | none\nvane_rule | 4 winds in order enhances, obsolesces, retrieves, reverses; strengths 1-5; exactly one highest; prevailing = the highest"
}

Reply:

{
 "task": "vane",
 "status": "ok",
 "reason": "",
 "thing_label": "the leaf blower",
 "winds": [
  {
   "law": "enhances",
   "what": "reach down the street",
   "headline": "The Whole Street Agrees",
   "reading": "At eight on Saturday the whole street runs one, so clearing leaves turns into something the block quietly agrees on.",
   "note": 3,
   "note_quote": "runs one at eight on Saturday",
   "strength": 3
  },
  {
   "law": "obsolesces",
   "what": "raking leaves by hand",
   "headline": "Rake? What Rake?",
   "reading": "A rake used to be the whole plan before a blower took over the job of scattering leaves around instead.",
   "note": 0,
   "note_quote": "",
   "strength": 2
  },
  {
   "law": "retrieves",
   "what": "the old street sweep",
   "headline": "Yesteryear's Street Sweep",
   "reading": "There's a whiff of the old street sweep in it, the kind of job that used to take a broom and patience.",
   "note": 0,
   "note_quote": "",
   "strength": 1
  },
  {
   "law": "reverses",
   "what": "a never-ending leaf standoff",
   "headline": "Leaves: The Return Match",
   "reading": "You blow the leaves over, the neighbour blows them straight back, and somehow nothing ends up actually gone.",
   "note": 2,
   "note_quote": "the neighbour blows them straight back",
   "strength": 5
  }
 ],
 "prevailing": "reverses",
 "prevailing_reason": "note 2 shows the leaves just bouncing back and forth, so the clearing never actually finishes.",
 "closing": "Four winds blow, but the leaves just keep drifting back where they started."
}

The family group chat - pushes aside prevails

Input:

{
 "task": "vane",
 "thing": "the family group chat",
 "tone": "warm",
 "notes": "1 | nobody in the family phones anyone anymore\n2 | the paper address book in the drawer never gets opened now\n3 | I keep it on mute",
 "page_facts": "note_count | 3\nperson_words_in_thing | none\nnature_words_in_thing | none\ncapitalised_words_in_thing | none\nvane_rule | 4 winds in order enhances, obsolesces, retrieves, reverses; strengths 1-5; exactly one highest; prevailing = the highest"
}

Reply:

{
 "task": "vane",
 "status": "ok",
 "reason": "",
 "thing_label": "the family group chat",
 "winds": [
  {
   "law": "enhances",
   "what": "quick family contact",
   "headline": "Close Without The Calls",
   "reading": "A chat like this gathers your whole family in the same thread, so news reaches everyone without separate calls.",
   "note": 0,
   "note_quote": "",
   "strength": 2
  },
  {
   "law": "obsolesces",
   "what": "ringing people up",
   "headline": "The Phone Stays Quiet",
   "reading": "Nobody in the family rings each other anymore - this chat quietly stands in for every phone call that used to happen.",
   "note": 1,
   "note_quote": "nobody in the family phones anyone anymore",
   "strength": 5
  },
  {
   "law": "retrieves",
   "what": "a shared family letter",
   "headline": "A Letter Finds New Life",
   "reading": "Families used to pass letters round to share their news - this chat brings back that same gathered feeling for you, in a new shape.",
   "note": 0,
   "note_quote": "",
   "strength": 1
  },
  {
   "law": "reverses",
   "what": "a quiet hum",
   "headline": "Turned Down To A Hum",
   "reading": "You keep it on mute, so the chat built to keep you close ends up as a quiet hum you barely notice.",
   "note": 3,
   "note_quote": "I keep it on mute",
   "strength": 4
  }
 ],
 "prevailing": "obsolesces",
 "prevailing_reason": "Note 1 and note 2 both show phoning and the address book fading, making the obsolesces wind blow hardest here.",
 "closing": "The family's phone calls have gone quiet, and this small chat carries on without them."
}

Voice notes - brings back prevails

Input:

{
 "task": "vane",
 "thing": "voice notes",
 "tone": "warm",
 "notes": "1 | my aunt's voice notes ramble on like a long handwritten letter\n2 | I play them on the walk home like a radio show\n3 | I never reply with one myself",
 "page_facts": "note_count | 3\nperson_words_in_thing | none\nnature_words_in_thing | none\ncapitalised_words_in_thing | none\nvane_rule | 4 winds in order enhances, obsolesces, retrieves, reverses; strengths 1-5; exactly one highest; prevailing = the highest"
}

Reply:

{
 "task": "vane",
 "status": "ok",
 "reason": "",
 "thing_label": "the voice notes",
 "winds": [
  {
   "law": "enhances",
   "what": "company on the walk",
   "headline": "a walking companion",
   "reading": "They ride along on the walk home, becoming easy company without you having to do a thing.",
   "note": 2,
   "note_quote": "on the walk home",
   "strength": 3
  },
  {
   "law": "obsolesces",
   "what": "typing out texts",
   "headline": "thumbs get a rest",
   "reading": "Typing out a message can feel like effort, and voice notes let your thumbs take a little rest.",
   "note": 0,
   "note_quote": "",
   "strength": 2
  },
  {
   "law": "retrieves",
   "what": "a handwritten letter",
   "headline": "a letter you can hear",
   "reading": "Your aunt's voice notes ramble on, every bit as long and unhurried as a handwritten letter.",
   "note": 1,
   "note_quote": "like a long handwritten letter",
   "strength": 5
  },
  {
   "law": "reverses",
   "what": "an endless ramble",
   "headline": "short message, long story",
   "reading": "A quick hello can turn into a long ramble, closer to a letter than the fast catch-up it promised to be.",
   "note": 1,
   "note_quote": "ramble on",
   "strength": 3
  }
 ],
 "prevailing": "retrieves",
 "prevailing_reason": "Note 1 and note 2 both compare the notes to older things, a letter and a radio show, so retrieves blows hardest.",
 "closing": "Mostly a lovely old letter in disguise, with a little rambling detour along the way."
}

The e-bike - enhances prevails

Input:

{
 "task": "vane",
 "thing": "the e-bike",
 "tone": "gleeful",
 "notes": "1 | it gets me up the big hill without breaking a sweat\n2 | I can ride twice as far as on my old bike\n3 | the battery needs charging every few rides",
 "page_facts": "note_count | 3\nperson_words_in_thing | none\nnature_words_in_thing | none\ncapitalised_words_in_thing | none\nvane_rule | 4 winds in order enhances, obsolesces, retrieves, reverses; strengths 1-5; exactly one highest; prevailing = the highest"
}

Reply:

{
 "task": "vane",
 "status": "ok",
 "reason": "",
 "thing_label": "the e-bike",
 "winds": [
  {
   "law": "enhances",
   "what": "easier hill climbs",
   "headline": "Zoom Up Any Hill",
   "reading": "It carries you up the big hill without breaking a sweat, turning a steep climb into gleeful ease.",
   "note": 1,
   "note_quote": "without breaking a sweat",
   "strength": 5
  },
  {
   "law": "obsolesces",
   "what": "the old bike",
   "headline": "Bye Bye Old Bike",
   "reading": "It leaves your old bike outpaced, nudging plain pedalling gleefully aside.",
   "note": 2,
   "note_quote": "twice as far as on my old bike",
   "strength": 3
  },
  {
   "law": "retrieves",
   "what": "the leisurely bike ride",
   "headline": "Cycling Joy Returns",
   "reading": "It brings back joyful, easy cycling for anyone who had given up on pedalling, almost gleefully like flying.",
   "note": 0,
   "note_quote": "",
   "strength": 2
  },
  {
   "law": "reverses",
   "what": "the charging chore",
   "headline": "Plugged In Again",
   "reading": "The battery needs charging every few rides, which gleefully turns the no-effort cruise into a plugged-in chore.",
   "note": 3,
   "note_quote": "the battery needs charging every few rides",
   "strength": 4
  }
 ],
 "prevailing": "enhances",
 "prevailing_reason": "Note 1 and note 2 both show real gains, easier climbs and greater range, so enhances wins gleefully.",
 "closing": "Your gleeful gains lead the way, though a charging niggle won't quite keep up!"
}

A flatmate - declined

Input:

{
 "task": "vane",
 "thing": "my flatmate",
 "tone": "wry",
 "notes": "1 | always borrows the good pan\n2 | sings in the shower",
 "page_facts": "note_count | 2\nperson_words_in_thing | flatmate\nnature_words_in_thing | none\ncapitalised_words_in_thing | none\nvane_rule | 4 winds in order enhances, obsolesces, retrieves, reverses; strengths 1-5; exactly one highest; prevailing = the highest"
}

Reply:

{
 "task": "vane",
 "status": "declined",
 "reason": "This toy reads inventions and habits, not people, nature, weapons or drugs - maybe name an invention or habit instead.",
 "thing_label": "",
 "winds": [],
 "prevailing": "",
 "prevailing_reason": "",
 "closing": "Fair enough - people don't need four winds, only inventions do."
}

Build the input yourself

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

const K = VaneKit, D = VaneDraw;
const built = K.buildInput({ thing: "the leaf blower", tone: "wry",
  notes: "the leaves just end up on the neighbour's side
then the neighbour blows them straight back" });
built.parsed.notes;              // ["the leaves just end up on the neighbour's side", ...]
built.prescan.person;            // [] - person words in the thing
const wire = K.toWire(built.input);            // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes          // the page's re-check of the reply
// K.top(reply.winds)                   // the prevailing wind by strength, or "" on a tie
// D.svg(K.fromReply(reply), { title: reply.thing_label })   // draw the vane as SVG
// K.md(built, reply)                   // the reading 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://which-way-it-blows.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":"which-way-it-blows"}' | 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": "vane", "thing": "...", "tone": "wry", "notes": "1 | ...\n2 | ...", "page_facts": "note_count | 2\n..."} - 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 which-way-it-blows:<task>:<inputHash>:a<attempt> (the one task is vane), 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="which-way-it-blows:vane:$(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: which-way-it-blows:vane:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json