← Still Going
API

Drive Still Going from your own code

The one metered task, report, is available over HTTP. It takes a life table the page has already computed - one line per item (arrived, went or still going, months), the counts, the median life, the share still going at 6, 12 and 24 months, a Weibull shape and the verdict - and returns a deadpan actuary's note: a headline, a plain reading of the curve, one life note per item, one practical change aimed at the commonest way they go, a word on the next one and a caveat.

The division of labour first: the page does the arithmetic; the model does the reading. The Kaplan-Meier curve (with items still going or given away in working order censored), the median, the Weibull shape, the verdict (too-few-to-tell, dies-young, wears-out, luck-of-the-draw, in that check order) and the leading cause with its tie-break are all computed by LifeKit.buildInput. After the reply, the page re-checks it against its own numbers.

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":"still-going"}), 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, items, verdict, median) 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 note 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: report (the one task is report); a missing or different one is still answered as report. All fields are strings.

FieldRequiredWhat it holds
taskyesAlways report.
kindyesWhat the items are, in the person's words (up to 40 characters).
as_ofyesThe month the page counted up to, as Oct 2026.
itemsyesOne numbered line per item: 1. black folding | arrived Oct 2023 | went Nov 2023 (lost) | lasted 1 month | shorter than the median; still-going and given-away items say so instead of went.
countsyesHow many ended, by cause, and how many have not ended.
medianyesThe Kaplan-Meier median life, or not reached.
still_going_atyesThe share still going at 6, 12 and 24 months, or beyond the longest record.
shapeyesThe censored Weibull shape to two decimals, or none.
verdictyesThe page's verdict id; the reply copies it.
leading_causeyeslost, broke, wore out, borrowed, never back or none; ties go to the first in that order.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

Verdicts, in check order: fewer than 4 endings is too-few-to-tell; a shape under 0.8 is dies-young; over 1.25 is wears-out; otherwise luck-of-the-draw. An item that arrived and went in the same month counts as half a month.

The reply

One JSON object, no prose around it:

{"task": "report", "verdict": "luck-of-the-draw", "headline": "...", "reading": "...", "lives": [{"item": "black folding", "note": "..."}], "advice": "...", "next_one": "...", "caveat": "..."}
FieldWhat it holds
verdictThe input's verdict, copied.
headlineOne sentence: the kind, the verdict in plain words and the median.
readingOne or two sentences on the curve, using only the page's figures.
lives[]One {item, note} per item, in input order; item is the exact name.
adviceOne practical change aimed at the leading cause.
next_one / caveatWhat follows for the next item, and how sure this is.

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, items with no life note or notes for unknown items, a still-going item described as ended, 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 - dies young, luck of the draw, wears out and too few to tell.

Phone chargers - Dies young

Input:

{
 "task": "report",
 "kind": "phone chargers",
 "as_of": "Oct 2026",
 "items": "1. white braided | arrived Feb 2023 | went Mar 2023 (broke) | lasted 1 month | about the median\n2. short one | arrived Apr 2023 | went Apr 2023 (broke) | lasted under a month | shorter than the median\n3. the long one | arrived May 2023 | went Aug 2025 (wore out) | lasted 27 months | longer than the median\n4. bedside | arrived Sep 2023 | went Nov 2023 (broke) | lasted 2 months | about the median\n5. work bag | arrived Jan 2024 | still going | 33 months so far | already past the median\n6. blue | arrived Jun 2024 | went Jul 2024 (broke) | lasted 1 month | about the median\n7. travel one | arrived Aug 2024 | went Jan 2026 (lost) | lasted 17 months | longer than the median\n8. kitchen | arrived Feb 2025 | still going | 20 months so far | already past the median",
 "counts": "8 items: 6 ended (1 lost, 4 broke, 1 wore out); 2 not ended (2 still going)",
 "median": "2 months",
 "still_going_at": "6 months: 50%; 12 months: 50%; 24 months: 38%",
 "shape": "0.58",
 "verdict": "dies-young",
 "leading_cause": "broke"
}

Reply:

{
 "task": "report",
 "verdict": "dies-young",
 "headline": "For phone chargers, the early months are the dangerous stretch: one that gets past them tends to keep going, and the median life here is 2 months.",
 "reading": "Half of these phone chargers were done within 2 months, yet the curve flattens out from there: 50% were still going at 6 months and at 12 months, and 38% at 24 months. Of the 8 chargers, 6 have ended (1 lost, 4 broke, 1 wore out), which fits a kind of thing where the early days carry the real risk and a survivor tends to settle in for the long run.",
 "lives": [
  {
   "item": "white braided",
   "note": "It broke after 1 month, about the median."
  },
  {
   "item": "short one",
   "note": "It broke in under a month, shorter than the median."
  },
  {
   "item": "the long one",
   "note": "It wore out after 27 months, longer than the median."
  },
  {
   "item": "bedside",
   "note": "It broke after 2 months, about the median."
  },
  {
   "item": "work bag",
   "note": "It is still going at 33 months so far, already past the median."
  },
  {
   "item": "blue",
   "note": "It broke after 1 month, about the median."
  },
  {
   "item": "travel one",
   "note": "It was lost after 17 months, longer than the median."
  },
  {
   "item": "kitchen",
   "note": "It is still going at 20 months so far, already past the median."
  }
 ],
 "advice": "Try coiling the cable loosely and unplugging it by the plug rather than the cord, so you spare the next one the strain that tends to break this kind of thing.",
 "next_one": "Give the next charger a thorough check in its first few months, since any fault in this kind of thing tends to show itself early.",
 "caveat": "With only 8 chargers and 6 endings, this is an estimate rather than a hard rule."
}

Umbrellas - Luck of the draw

Input:

{
 "task": "report",
 "kind": "umbrellas",
 "as_of": "Oct 2026",
 "items": "1. black folding | arrived Oct 2023 | went Nov 2023 (lost) | lasted 1 month | shorter than the median\n2. golf size | arrived Jan 2024 | went Mar 2024 (broke) | lasted 2 months | shorter than the median\n3. the spotty one | arrived Feb 2024 | went Nov 2024 (lost) | lasted 9 months | about the median\n4. clear dome | arrived Jul 2024 | went Sep 2025 (lost) | lasted 14 months | longer than the median\n5. spare at work | arrived Dec 2024 | went Mar 2025 (borrowed, never back) | lasted 3 months | shorter than the median\n6. red | arrived Jan 2025 | still going | 21 months so far | already past the median\n7. green | arrived May 2025 | went Apr 2026 (lost) | lasted 11 months | longer than the median\n8. tiny one | arrived Apr 2026 | still going | 6 months so far | not yet past the median",
 "counts": "8 items: 6 ended (4 lost, 1 broke, 1 borrowed, never back); 2 not ended (2 still going)",
 "median": "9 months",
 "still_going_at": "6 months: 63%; 12 months: 31%; 24 months: beyond the longest record",
 "shape": "1.06",
 "verdict": "luck-of-the-draw",
 "leading_cause": "lost"
}

Reply:

{
 "task": "report",
 "verdict": "luck-of-the-draw",
 "headline": "For umbrellas, the endings happen at a steady rate no matter the age, with a median life of 9 months.",
 "reading": "The median life is 9 months, with 63% of umbrellas still going at 6 months and 31% at 12 months. Of the 6 that have ended (4 lost, 1 broke, 1 borrowed, never back), age makes no difference to the odds, so an umbrella that's been around a while is no likelier to go than a new one.",
 "lives": [
  {
   "item": "black folding",
   "note": "The black folding umbrella lasted 1 month before it was lost, shorter than the median."
  },
  {
   "item": "golf size",
   "note": "The golf size lasted 2 months before it broke, shorter than the median."
  },
  {
   "item": "the spotty one",
   "note": "The spotty one lasted 9 months before it was lost, about the median."
  },
  {
   "item": "clear dome",
   "note": "The clear dome lasted 14 months before it was lost, longer than the median."
  },
  {
   "item": "spare at work",
   "note": "The spare at work lasted 3 months before it was borrowed and never came back, shorter than the median."
  },
  {
   "item": "red",
   "note": "The red umbrella has been going 21 months so far, already past the median."
  },
  {
   "item": "green",
   "note": "The green umbrella lasted 11 months before it was lost, longer than the median."
  },
  {
   "item": "tiny one",
   "note": "The tiny one has been going 6 months so far, not yet past the median."
  }
 ],
 "advice": "Since going missing is how most of your umbrellas have ended, try keeping it clipped to the same bag each time so you can keep track of it more easily.",
 "next_one": "For the next umbrella, there's no reason to retire it early for its age, since an older one isn't at any greater risk than a newer one.",
 "caveat": "With only 8 umbrellas and 6 endings counted, this is an estimate rather than a hard rule."
}

Phone cases - Wears out

Input:

{
 "task": "report",
 "kind": "phone cases",
 "as_of": "Oct 2026",
 "items": "1. clear | arrived Mar 2022 | went Feb 2023 (wore out) | lasted 11 months | about the median\n2. black rubber | arrived Feb 2023 | went Mar 2024 (wore out) | lasted 13 months | about the median\n3. leather flip | arrived Mar 2024 | went Mar 2025 (wore out) | lasted 12 months | about the median\n4. glitter | arrived Sep 2024 | went Aug 2025 (broke) | lasted 11 months | about the median\n5. plain grey | arrived Mar 2025 | went May 2026 (wore out) | lasted 14 months | longer than the median\n6. wallet one | arrived Aug 2025 | given away Jan 2026, still working | 5 months in use | not yet past the median\n7. green silicone | arrived May 2026 | still going | 5 months so far | not yet past the median",
 "counts": "7 items: 5 ended (1 broke, 4 wore out); 2 not ended (1 still going, 1 given away, still working)",
 "median": "12 months",
 "still_going_at": "6 months: 100%; 12 months: 40%; 24 months: beyond the longest record",
 "shape": "11.19",
 "verdict": "wears-out",
 "leading_cause": "wore out"
}

Reply:

{
 "task": "report",
 "verdict": "wears-out",
 "headline": "Phone cases here tend to wear out more as they age, with a typical one lasting 12 months.",
 "reading": "At 6 months 100% of phone cases were still going, at 12 months - the median - that had dropped to 40%, and by 24 months the figure is beyond the longest record, in keeping with endings growing more likely the longer a case stays in service. Of the 7 items, 5 have ended (1 broke, 4 wore out), while 2 have not (1 still going, 1 given away, still working).",
 "lives": [
  {
   "item": "clear",
   "note": "It wore out after 11 months, about the median."
  },
  {
   "item": "black rubber",
   "note": "It wore out after 13 months, about the median."
  },
  {
   "item": "leather flip",
   "note": "It wore out after 12 months, about the median."
  },
  {
   "item": "glitter",
   "note": "It broke after 11 months, about the median."
  },
  {
   "item": "plain grey",
   "note": "It wore out after 14 months, longer than the median."
  },
  {
   "item": "wallet one",
   "note": "It was given away after 5 months, still working, and not yet past the median."
  },
  {
   "item": "green silicone",
   "note": "It is still going at 5 months, not yet past the median."
  }
 ],
 "advice": "You could extend the next one's life by keeping it free of grit and pocket debris, and planning to swap it out on your own schedule before it wears through.",
 "next_one": "For your next phone case, expect to replace it around 12 months in and plan for that.",
 "caveat": "With only 7 phone cases counted and 5 endings so far, this reading is a rough estimate rather than a hard rule."
}

Mugs - Too few to tell

Input:

{
 "task": "report",
 "kind": "mugs",
 "as_of": "Oct 2026",
 "items": "1. the big blue one | arrived Jun 2023 | went Feb 2024 (broke) | lasted 8 months | shorter than the median\n2. souvenir mug | arrived Jun 2023 | still going | 40 months so far | already past the median\n3. plain white | arrived Nov 2023 | given away Nov 2024, still working | 12 months in use | not yet past the median\n4. chipped one | arrived Jan 2024 | went Jul 2026 (broke) | lasted 30 months | about the median\n5. striped | arrived Jul 2025 | still going | 15 months so far | not yet past the median",
 "counts": "5 items: 2 ended (2 broke); 3 not ended (2 still going, 1 given away, still working)",
 "median": "30 months",
 "still_going_at": "6 months: 100%; 12 months: 80%; 24 months: 80%",
 "shape": "1.61",
 "verdict": "too-few-to-tell",
 "leading_cause": "broke"
}

Reply:

{
 "task": "report",
 "verdict": "too-few-to-tell",
 "headline": "For mugs, there have been too few endings yet to say how the risk changes with age, with a median life of 30 months so far.",
 "reading": "Of 5 mugs, 2 have ended so far (2 broke), which isn't enough to read how the pattern runs with age. At 6 months 100% were still going, 80% by 12 months, and still 80% through 24 months, with a median of 30 months reached among this group so far.",
 "lives": [
  {
   "item": "the big blue one",
   "note": "The big blue one lasted 8 months before it broke, shorter than the median."
  },
  {
   "item": "souvenir mug",
   "note": "The souvenir mug is still going at 40 months so far, already past the median."
  },
  {
   "item": "plain white",
   "note": "Plain white was given away still working after 12 months in use, not yet past the median."
  },
  {
   "item": "chipped one",
   "note": "The chipped one lasted 30 months before it broke, about the median."
  },
  {
   "item": "striped",
   "note": "Striped is still going at 15 months so far, not yet past the median."
  }
 ],
 "advice": "You might try keeping your mugs stored with a little space between them, rather than stacked tight, to spare them the bumps that tend to chip or crack a mug.",
 "next_one": "Keep logging each new mug's arrival and departure, and look at this again once more mugs have ended.",
 "caveat": "With just 5 mugs tracked and only 2 having ended, this is a small sample rather than a firm rule."
}

Build the input yourself

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

const K = LifeKit, R = ReplyKit;
const built = K.buildInput(K.form({ kind: "umbrellas", as_of: "2026-10", rows: [
  { name: "black folding", arrived: "2023-10", went: "2023-11", how: "lost" },
  { name: "red", arrived: "2025-01", went: "", how: "going" },
  { name: "golf size", arrived: "2024-01", went: "2024-03", how: "broke" } ] }));  // built.ok is false, with built.note, until the rows are complete
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 note
// built.plan.steps, built.plan.median, built.plan.shape  // the curve the page drew

Row keys: name (up to 40 characters), arrived and went (months such as 2024-03 or Mar 2024), how - one of going, lost, broke, wore, borrowed, given. Up to 12 rows.

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://still-going.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":"still-going"}' | 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": "report", "kind": "umbrellas", "as_of": "Oct 2026", "items": "1. ...", "counts": "...", "median": "9 months", "still_going_at": "...", "shape": "1.06", "verdict": "luck-of-the-draw", "leading_cause": "lost"}
# - 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 still-going:<task>:<inputHash>:a<attempt> (the one task is report), 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="still-going:report:$(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: still-going:report:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json