← Back for More
API

Drive Back for More from your own code

The one metered task, ride, is available over HTTP. It takes a habit loop the page has already read - the moreish thing, what the person wants (keep it, loosen it, not sure yet), four stage lines (trigger, action, reward, investment) each with a pull from none to strong and the person's note, the total out of 12, the strongest stage, any stages level with it, the stages that do not pull and the verdict - and returns a ride report: a headline, one line per stage, why the strongest stage pulls hardest, one small move and a caveat.

The division of labour first: the page does the arithmetic; the model does the reading. The total, the strongest stage (ties go to the earlier stage in the loop), the gaps and the verdict (just-watching, one-more-go, round-again, all-the-way-round, in that check order) are all computed by HookKit.buildInput. After the reply, the page re-checks it against its own reading.

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":"back-for-more"}), 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, loop, verdict, total) is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the ride report is metered and needs 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: ride (the one task is ride); a missing or different one is still answered as ride. All fields are strings.

FieldRequiredWhat it holds
taskyesAlways ride.
thingyesThe moreish thing, in the person's words (up to 48 characters).
wantyeskeep it, loosen it or not sure yet.
loopyesFour numbered lines in loop order: 1. trigger | pull: firm (2 of 3) | a cue from outside | note: the paper lands on the mat while the kettle boils; the reward line also carries its kind and how much it varies.
totalyesThe total pull, as 11 of 12.
strongestyesThe stage with the highest pull (the earlier stage wins a tie), or none.
level_withyesOther stages with the same pull as the strongest, or none.
gapsyesStages whose pull is none, or none.
verdictyesThe page's verdict id; the reply copies it.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

Verdicts, in check order: a total of 3 or less is just-watching; any stage at none, or a total of 4 to 6, is one-more-go; 7 to 9 is round-again; 10 to 12 is all-the-way-round. Anything involving gambling, drinking, smoking or harm gets a move that points to proper support.

The reply

One JSON object, no prose around it:

{"task": "ride", "verdict": "all-the-way-round", "headline": "...", "stages": [{"stage": "trigger", "note": "..."}, {"stage": "action", "note": "..."}, {"stage": "reward", "note": "..."}, {"stage": "investment", "note": "..."}], "strongest": "...", "move": "...", "caveat": "..."}
FieldWhat it holds
verdictThe input's verdict, copied.
headlineOne sentence: the thing, the verdict in plain words and the total.
stages[]Four {stage, note} in loop order, each note matching its pull.
strongestWhy the strongest stage pulls hardest (and any tie).
moveOne small step that fits what the person wants.
caveatA light reading, not professional advice.

The page reads the reply with ReplyKit.normalize and checks it with ReplyKit.check, which lists, under "The page's check", a verdict that differs from the page's, missing or out-of-order stage notes, a stage rated none described as pulling, a strongest-stage line naming another stage, and numbers or weekdays the page never sent.

Worked examples - every sample

These are the free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover all four verdicts - just watching, one more go, round again and all the way round - and all three wants.

The crossword - All the way round

Input:

{
 "task": "ride",
 "thing": "the newspaper crossword",
 "want": "keep it",
 "loop": "1. trigger | pull: firm (2 of 3) | a cue from outside | note: the paper lands on the mat while the kettle boils\n2. action | pull: strong (3 of 3) | note: the pencil is already tucked in the fold, so I just sit down and start on the across clues\n3. reward | pull: strong (3 of 3) | a sense of getting better or finishing | different sometimes | note: the click when a long answer falls into place - some grids give it up early, some never do\n4. investment | pull: strong (3 of 3) | note: a stack of finished grids in the drawer, and I have learned the setter's favourite tricks",
 "total": "11 of 12",
 "strongest": "action",
 "level_with": "reward, investment",
 "gaps": "none",
 "verdict": "all-the-way-round"
}

Reply:

{
 "task": "ride",
 "verdict": "all-the-way-round",
 "headline": "The newspaper crossword pulls hard right across the loop and keeps turning you back on its own, for a total of 11 of 12.",
 "stages": [
  {
   "stage": "trigger",
   "note": "The paper landing on the mat while the kettle boils is a cue from outside, and it gives you a real pull to get going."
  },
  {
   "stage": "action",
   "note": "With the pencil already tucked in the fold, you just sit down and start on the across clues, which gives you a strong pull into the doing of it."
  },
  {
   "stage": "reward",
   "note": "The click when a long answer falls into place is a sense of getting better or finishing that varies - some grids give it up early, some never do - and it gives you a strong pull."
  },
  {
   "stage": "investment",
   "note": "The stack of finished grids in the drawer and having learned the setter's favourite tricks are what you've put in, and they give you a strong pull to keep going."
  }
 ],
 "strongest": "The action pulls hardest, because having the pencil already tucked in the fold means you just sit down and start on the across clues without any resistance, and although the reward and investment pull just as hard, action comes earliest in the loop, so it counts as the strongest stage.",
 "move": "Keep the pencil tucked in the fold so you can carry on sitting straight down and starting on the clues without delay.",
 "caveat": "This is just a light reading of your own notes and ratings, not a diagnosis or professional advice."
}

Charity shop - Round again

Input:

{
 "task": "ride",
 "thing": "browsing the charity shop",
 "want": "loosen it",
 "loop": "1. trigger | pull: strong (3 of 3) | a cue from outside | note: I pass the charity shop window on the walk home and the display keeps changing\n2. action | pull: firm (2 of 3) | note: pop in for a quick look round the shelves at the back\n3. reward | pull: strong (3 of 3) | a find or a surprise | never know what you'll get | note: might be a lamp, might be nothing at all; once it was a wool coat that fits perfectly\n4. investment | pull: faint (1 of 3) | note: the volunteers say hello and I have a loyalty stamp card",
 "total": "9 of 12",
 "strongest": "trigger",
 "level_with": "reward",
 "gaps": "none",
 "verdict": "round-again"
}

Reply:

{
 "task": "ride",
 "verdict": "round-again",
 "headline": "Browsing the charity shop draws you back in a way you can count on, with every stage of the loop pulling, for a total of 9 of 12.",
 "stages": [
  {
   "stage": "trigger",
   "note": "The trigger is a cue from outside - the charity shop window on your walk home, with its display always changing - and it gives you a strong pull."
  },
  {
   "stage": "action",
   "note": "The action is popping in for a quick look round the shelves at the back, and that gives you a real pull."
  },
  {
   "stage": "reward",
   "note": "The reward is a find or a surprise - you never know what you'll get, maybe a lamp, maybe nothing at all, though once it was a wool coat that fits perfectly - which keeps you curious and gives you a strong pull."
  },
  {
   "stage": "investment",
   "note": "The investment is the volunteers saying hello and the loyalty stamp card you have, which only gives you a light tug."
  }
 ],
 "strongest": "The trigger is the strongest stage, because the ever-changing charity shop window catching you on the walk home gives it a strong pull, just as strong as the reward, though as the earlier stage in the loop it's the one that counts as strongest.",
 "move": "You could try a different walk home so that changing window isn't right there in front of you.",
 "caveat": "This is just a light read of what you wrote and rated, not a diagnosis or professional advice."
}

Biscuit tin - One more go

Input:

{
 "task": "ride",
 "thing": "the biscuit tin",
 "want": "not sure yet",
 "loop": "1. trigger | pull: firm (2 of 3) | a feeling inside | note: a restless feeling after dinner when I am not sure what to do with myself\n2. action | pull: faint (1 of 3) | note: the tin lives on the top shelf so I have to fetch the step stool\n3. reward | pull: firm (2 of 3) | a comfort for the senses | the same every time | note: the same buttery shortbread, sweet and soft, exactly as expected\n4. investment | pull: none (0 of 3) | note: nothing really - the tin was a present and I never buy more",
 "total": "5 of 12",
 "strongest": "trigger",
 "level_with": "reward",
 "gaps": "investment",
 "verdict": "one-more-go"
}

Reply:

{
 "task": "ride",
 "verdict": "one-more-go",
 "headline": "The biscuit tin only gives you a modest tug, and with investment not pulling at all, the loop doesn't close on its own, adding up to 5 of 12.",
 "stages": [
  {
   "stage": "trigger",
   "note": "The trigger is a feeling inside - that restless feeling after dinner when you're not sure what to do with yourself - and it gives a real pull."
  },
  {
   "stage": "action",
   "note": "The action is fetching the step stool because the tin lives on the top shelf, and that only gives a light tug."
  },
  {
   "stage": "reward",
   "note": "The reward is the same buttery shortbread, sweet and soft, exactly as expected - a comfort for the senses that's dependable rather than surprising, and it still gives a real pull."
  },
  {
   "stage": "investment",
   "note": "The investment is nothing really, since the tin was a present and you never buy more, so it isn't pulling at all."
  }
 ],
 "strongest": "The trigger - that restless after-dinner feeling - pulls hardest, and although the reward pulls just as hard, the trigger counts as the strongest since it comes earlier in the loop.",
 "move": "Next time that restless after-dinner feeling shows up, simply notice it arriving, without changing anything else yet.",
 "caveat": "This is just a light reading of your own notes and ratings, not a diagnosis or professional advice."
}

Book swap box - Just watching

Input:

{
 "task": "ride",
 "thing": "the book swap box on the corner",
 "want": "keep it",
 "loop": "1. trigger | pull: faint (1 of 3) | a cue from outside | note: the little box on the corner catches my eye now and then\n2. action | pull: none (0 of 3) | note: I would have to cross the road and open the glass door, so mostly I walk on\n3. reward | pull: firm (2 of 3) | a find or a surprise | different sometimes | note: occasionally a paperback I have wanted turns up\n4. investment | pull: none (0 of 3) | note: I have never left a book in it",
 "total": "3 of 12",
 "strongest": "reward",
 "level_with": "none",
 "gaps": "action, investment",
 "verdict": "just-watching"
}

Reply:

{
 "task": "ride",
 "verdict": "just-watching",
 "headline": "The book swap box on the corner barely pulls you, easy enough that you could simply walk on past, at 3 of 12.",
 "stages": [
  {
   "stage": "trigger",
   "note": "The little box on the corner catching your eye now and then is a cue from outside, giving you just a light tug."
  },
  {
   "stage": "action",
   "note": "Having to cross the road and open the glass door is enough of an ask that this step does not pull you at all, so mostly you walk on."
  },
  {
   "stage": "reward",
   "note": "Occasionally finding a paperback you have wanted is a surprise that isn't the same every time, keeping you curious, and gives this stage a real pull."
  },
  {
   "stage": "investment",
   "note": "You have never left a book in it, so this stage does not pull you at all."
  }
 ],
 "strongest": "The reward pulls hardest, because an occasional paperback you have wanted turning up as a surprise is what draws you in most strongly.",
 "move": "To keep enjoying it, you could glance inside whenever the box catches your eye, so you don't miss a paperback you have wanted turning up.",
 "caveat": "This is just a light reading of your own notes and ratings, not a diagnosis or professional advice."
}

Build the input yourself

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

const K = HookKit, R = ReplyKit;
const built = K.buildInput(K.form({ thing: "the newspaper crossword", want: "keep", stages: {
  trigger: { note: "the paper lands on the mat", pull: "2", cue: "outside" },
  action: { note: "the pencil is tucked in the fold", pull: "3" },
  reward: { note: "the click when a long answer falls into place", pull: "3", kind: "self", varies: "sometimes" },
  investment: { note: "a stack of finished grids", pull: "3" } } }));  // built.ok is false, with built.note, until every stage has a note and a pull
const wire = K.toWire(built.input);   // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built).notes              // the page's re-check of the report
// built.plan.total, built.plan.strongest   // the score the page drew

Form keys: thing (up to 48 characters), want - one of keep, loosen, unsure - and stages, where each stage has a note (up to 160 characters) and a pull from 0 to 3; the trigger adds cue (outside or inside) and the reward adds kind (people, finds, self, comfort) and varies (same, sometimes, never).

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://back-for-more.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":"back-for-more"}' | 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": "ride", "thing": "the newspaper crossword", "want": "keep it", "loop": "1. trigger | ...", "total": "11 of 12", "strongest": "action", "level_with": "reward, investment", "gaps": "none", "verdict": "all-the-way-round"}
# - 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 back-for-more:<task>:<inputHash>:a<attempt> (the one task is ride), 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="back-for-more:ride:$(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: back-for-more:ride:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json