← Swapped or Changed?
API

Drive Swapped or Changed? from your own code

The one metered task, line-up, is available over HTTP. It takes one familiar thing that seems different, what feels different about it, optional notes and the clues you ticked, and returns the evidence for and against four suspects (it changed, it was swapped, you changed, memory drift), why the page picked what it did, the critical unknown and the one cheap test that would settle it.

The division of labour first: the page decides; the model words it. The page adds up fixed clue points for each suspect, makes its pick with fixed rules, finds the next suspect, and chooses the critical unknown, the test and what each result of the test would point to. It sends all of that as facts. The model never changes the pick or the points; it lays out the evidence and words the test for your thing.

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":"swapped-or-changed"}), 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, what_changed, clues, 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: the reasons 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 picks

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

FieldRequiredWhat it holds
taskyesAlways line-up.
thingyesThe familiar thing, at most 60 characters. Things only, never people or pets.
what_changedyesWhat feels different, at most 200 characters.
notesnoAnything else noticed, at most 300 characters, or none.
cluesyesOne numbered line per ticked clue, each followed in square brackets by the suspects it points to and away from; or none ticked.
factsyesThe page's lines: points per suspect id, clues ticked, the pick, the leader, the next suspect and the margin, the critical unknown, the test, and which suspect each result of the test points to.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

The suspects, always in this order: changed (it changed), swapped (it was swapped), you-changed (you changed), memory-drift (memory drift). The pick, first match wins: fewer than two clues ticked, or a leader with one point or fewer, is undecided (no pick yet); otherwise the leader. Ties go to the suspect further left.

The reply

One JSON object, no prose around it:

{"task": "line-up", "headline": "...", "suspects": [{"id": "changed", "for": "...", "against": "..."}, {"id": "swapped", ...}, {"id": "you-changed", ...}, {"id": "memory-drift", ...}], "verdict": "changed", "explanation": "...", "unknown": "...", "probe": "...", "closing": "..."}
FieldWhat it holds
headlineAt most 22 words: the page's lean, or no pick yet.
suspects[]Exactly four, in line-up order: for and against, each one sentence built only from the ticked clues that name that suspect.
verdictThe pick from facts, copied exactly.
explanationAt most 50 words: why the page picked what it did, naming the next suspect.
unknown and probeThe critical unknown and the test from facts, worded for this thing.
closingOne sentence: what each result of the test would point to.

The page then checks the reply (CaseKit.check) - the pick against its own, all four suspects present, the unknown and the test present, time, day or weather words you did not give, contact details or public posting, internal ids in prose - and lists anything it finds under "The page's check".

Worked examples - all five picks

These are the 5 free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover every pick: it changed, it was swapped, you changed, memory drift and no pick yet.

Cafe flat white - pick: it changed

Input:

{
 "task": "line-up",
 "thing": "The flat white at the corner cafe",
 "what_changed": "it tastes thinner and less sweet than it used to",
 "notes": "the cups have a new logo on them",
 "clues": "1. It looks different too (label, colour, shape or layout) [points to: it changed, it was swapped; points away from: memory drift]\n2. There is a notice of a change (new version, new recipe, new owner, an update) [points to: it changed; points away from: it was swapped]\n3. It is off every time now [points to: it changed; points away from: it was swapped]\n4. Someone else noticed the same thing without being asked [points to: it changed, it was swapped; points away from: you changed, memory drift]",
 "facts": "points: changed 8, swapped 0, you-changed -2, memory-drift -2\nclues ticked: 4 of 12\npick: changed\nleader: changed; runner-up: swapped; margin: 8\ncritical unknown: whether this one came from the same place as before\ntest: Compare where this one came from (maker, label, batch, branch or copy) with the usual one.\nif the test finds that it came from the same place as the usual one: changed\nif the test finds that it came from a different place than the usual one: swapped"
}

Reply:

{
 "task": "line-up",
 "headline": "From your clues, the lean is toward a change in the flat white itself, not a swap for a different one.",
 "suspects": [
  {
   "id": "changed",
   "for": "It looks different now, there has been a notice of a change, it is off every time, and someone else noticed the same thing without being asked.",
   "against": "None of what you ticked counts against a change in it."
  },
  {
   "id": "swapped",
   "for": "It looks different now and someone else noticed the same thing without being asked.",
   "against": "A notice of a change and it being off every time both count against a swap."
  },
  {
   "id": "you-changed",
   "for": "Nothing you ticked points to a change in you.",
   "against": "Someone else noticing the same thing without being asked counts against a change in you."
  },
  {
   "id": "memory-drift",
   "for": "Nothing you ticked points to memory drift.",
   "against": "It looking different and someone else noticing the same thing both count against memory drift."
  }
 ],
 "verdict": "changed",
 "explanation": "From what you ticked, a change in the flat white has the most support, with a swap as the next closest explanation.",
 "unknown": "Whether this flat white comes from the same place as the one you used to get at the corner cafe.",
 "probe": "Look at the label on the cup and compare it with the one you had before, to see whether this flat white comes from the same maker or branch, or a different one.",
 "closing": "If it comes from the same maker or branch as before, that points to a change in it; if it comes from a different one, that points to a swap."
}

Spare door key - pick: it was swapped

Input:

{
 "task": "line-up",
 "thing": "The spare door key",
 "what_changed": "it sticks in the lock and looks shinier",
 "notes": "it was cut at a different key shop",
 "clues": "1. It looks different too (label, colour, shape or layout) [points to: it changed, it was swapped; points away from: memory drift]\n2. This one came from a different place, maker, batch or copy than usual [points to: it was swapped; points away from: it changed]\n3. You compared it side by side with an old one [points to: it changed, it was swapped; points away from: memory drift]",
 "facts": "points: changed 2, swapped 5, you-changed 0, memory-drift -4\nclues ticked: 3 of 12\npick: swapped\nleader: swapped; runner-up: changed; margin: 3\ncritical unknown: whether the usual one, from the usual place, has changed in the same way\ntest: Get one from the usual place and put it next to this one to compare them directly.\nif the test finds that the usual one is still as it was: swapped\nif the test finds that the usual one has changed in the same way: changed"
}

Reply:

{
 "task": "line-up",
 "headline": "Your clues lean toward the key being swapped for a different one, rather than the same key having changed.",
 "suspects": [
  {
   "id": "changed",
   "for": "The key looks different in its label, colour, shape or layout, and you compared it side by side with an old one.",
   "against": "This one came from a different place, maker, batch or copy than usual."
  },
  {
   "id": "swapped",
   "for": "The key looks different in its label, colour, shape or layout, it came from a different place, maker, batch or copy than usual, and you compared it side by side with an old one.",
   "against": "Nothing you ticked rules this out."
  },
  {
   "id": "you-changed",
   "for": "Nothing you ticked points to this.",
   "against": "Nothing you ticked rules this out either."
  },
  {
   "id": "memory-drift",
   "for": "Nothing you ticked points to this.",
   "against": "The key looks different in its label, colour, shape or layout, and you compared it side by side with an old one."
  }
 ],
 "verdict": "swapped",
 "explanation": "The idea that the key was swapped for a different one has the most support from what you ticked, and the next closest explanation is that it changed.",
 "unknown": "Whether the usual key, cut at the usual shop, has also become shinier and started sticking in the lock.",
 "probe": "Get a key from the usual shop, put it next to this one, and try it in the lock to see if it also sticks and looks shiny.",
 "closing": "A shop key still looking and working as usual points to a swap; one that is also shinier and sticks points to a change in the key."
}

Old armchair - pick: you changed

Input:

{
 "task": "line-up",
 "thing": "The old armchair in the living room",
 "what_changed": "it does not feel as comfy as it was",
 "notes": "I have been using a standing desk a lot",
 "clues": "1. Others shrug; only you think it is different [points to: you changed, memory drift; points away from: it changed, it was swapped]\n2. Your routine, setting or taste has changed lately [points to: you changed]",
 "facts": "points: changed -1, swapped -1, you-changed 5, memory-drift 1\nclues ticked: 2 of 12\npick: you-changed\nleader: you-changed; runner-up: memory-drift; margin: 4\ncritical unknown: whether the difference is in you now or in how you remember it\ntest: Try it again the old way and in the old setting, as close as you can, and note what still feels different.\nif the test finds that it feels as it used to when done the old way: you-changed\nif the test finds that it still feels different even when done the old way: memory-drift"
}

Reply:

{
 "task": "line-up",
 "headline": "From what you ticked, the lean is that something about you has changed, not the armchair itself.",
 "suspects": [
  {
   "id": "changed",
   "for": "None of the clues you ticked suggest that the armchair itself has changed.",
   "against": "The clue that others do not notice any difference weighs against the armchair having changed on its own."
  },
  {
   "id": "swapped",
   "for": "No ticked clue suggests a different armchair was put in its place.",
   "against": "The clue that only you notice the difference weighs against a different chair having been swapped in."
  },
  {
   "id": "you-changed",
   "for": "Only you noticing the difference, along with a change in your own routine or setting, both suggest something in you has changed.",
   "against": "Nothing you ticked rules out a change in you."
  },
  {
   "id": "memory-drift",
   "for": "Others not noticing any difference also suggests memory drift, the chair never quite being as you remember it.",
   "against": "Nothing you ticked rules out memory drift either."
  }
 ],
 "verdict": "you-changed",
 "explanation": "Among the clues you ticked, this reading of a change in you has the most support, with memory drift as the next closest explanation.",
 "unknown": "The open question is whether the armchair feels different because something about you has changed, or because of how you remember the armchair feeling before.",
 "probe": "Sit in the armchair the same way you used to, in its usual spot in the living room, and notice whether it still feels less comfortable than before.",
 "closing": "If sitting in it the old way feels the same as before, that suggests a change in you; if it still feels different, that suggests memory drift."
}

Seaside ice cream - pick: memory drift

Input:

{
 "task": "line-up",
 "thing": "The ice cream from the seaside kiosk",
 "what_changed": "it is not as creamy as I remember",
 "notes": "the first time was on a school trip",
 "clues": "1. Others shrug; only you think it is different [points to: you changed, memory drift; points away from: it changed, it was swapped]\n2. There was a long gap since you last had or used it [points to: you changed, memory drift]\n3. Your first time with it was special (a trip or an occasion) [points to: you changed, memory drift]",
 "facts": "points: changed -1, swapped -1, you-changed 4, memory-drift 5\nclues ticked: 3 of 12\npick: memory-drift\nleader: memory-drift; runner-up: you-changed; margin: 1\ncritical unknown: whether the difference is in you now or in how you remember it\ntest: Try it again the old way and in the old setting, as close as you can, and note what still feels different.\nif the test finds that it still feels different even when done the old way: memory-drift\nif the test finds that it feels as it used to when done the old way: you-changed"
}

Reply:

{
 "task": "line-up",
 "headline": "From your clues, the lean points toward memory drift rather than an actual change in the ice cream.",
 "suspects": [
  {
   "id": "changed",
   "for": "None of the clues you ticked point to a change in the ice cream itself.",
   "against": "Others shrugging it off while only you notice a difference points away from an actual change in the ice cream itself."
  },
  {
   "id": "swapped",
   "for": "None of the clues you ticked point to a swap of the ice cream for a different one.",
   "against": "Others shrugging it off while only you notice a difference points away from a swap of the ice cream for a different one."
  },
  {
   "id": "you-changed",
   "for": "Only you noticing a difference, the long gap since you last had it, and your first time being a special occasion all point to a change in you, your taste, routine or setting.",
   "against": "Nothing you ticked rules out a change in you."
  },
  {
   "id": "memory-drift",
   "for": "Only you noticing a difference, the long gap since you last had it, and your first time being a special occasion all point to memory drift, how that first time is remembered.",
   "against": "Nothing you ticked rules out memory drift, how that first time is remembered now."
  }
 ],
 "verdict": "memory-drift",
 "explanation": "This is a close call: memory drift has the most support from what you ticked, narrowly ahead of a change in you, your taste, routine or setting.",
 "unknown": "Whether the ice cream tastes different now because something in you has changed, or because of how you remember that first taste.",
 "probe": "Try the ice cream again from the same kiosk by the sea, as close to the old way as you can, and notice whether the creaminess still feels different.",
 "closing": "If it still feels different even then, that points to memory drift; if it feels as it used to, that points to a change in you."
}

Radio jingle - pick: no pick yet

Input:

{
 "task": "line-up",
 "thing": "The radio station's morning jingle",
 "what_changed": "it sounds faster and a bit higher",
 "notes": "none",
 "clues": "1. It is off every time now [points to: it changed; points away from: it was swapped]",
 "facts": "points: changed 2, swapped -1, you-changed 0, memory-drift 0\nclues ticked: 1 of 12\npick: undecided\nleader: changed; runner-up: you-changed; margin: 2\ncritical unknown: whether it is different for everyone or only for you\ntest: Ask someone who knew it before to try it fresh, without telling them what you noticed.\nif the test finds that they notice the same difference: changed\nif the test finds that it seems the same as before to them: you-changed"
}

Reply:

{
 "task": "line-up",
 "headline": "There's no pick yet on why the jingle sounds different, going only by what you've ticked.",
 "suspects": [
  {
   "id": "changed",
   "for": "You said it is off every time you hear it, which points toward a change in the thing itself.",
   "against": "Nothing you ticked rules out a change in the thing."
  },
  {
   "id": "swapped",
   "for": "Nothing you ticked points to a swap specifically.",
   "against": "You said it is off every time you hear it, which points away from a swap."
  },
  {
   "id": "you-changed",
   "for": "Nothing you ticked points to a change in you.",
   "against": "Nothing you ticked rules out a change in you either."
  },
  {
   "id": "memory-drift",
   "for": "Nothing you ticked points to memory drift.",
   "against": "Nothing you ticked rules out memory drift either."
  }
 ],
 "verdict": "undecided",
 "explanation": "Too few of the clues you've ticked point clearly toward a particular explanation yet, so what remains to separate is whether it changed or you changed.",
 "unknown": "The key thing you don't know yet is whether the jingle sounds different to everyone who hears it or just to you.",
 "probe": "Ask someone who knew the jingle before to listen to it fresh, without telling them what you've noticed about it.",
 "closing": "If they notice the same difference, that points toward a change in the thing; if it seems the same as before to them, that points toward a change in you."
}

Build the input yourself

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

const K = CaseKit;
const form = K.form({ thing: "The spare door key", off: "it sticks in the lock and looks shinier", notes: "",
  clues: { looks: true, source: true, compared: true } });
const built = K.buildInput(form);       // built.facts holds the points, the pick, the next suspect and the test
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.md(built, reply, K.title(form))    // the line-up as Markdown

The clue keys: looks, notice, source, once, every, others, onlyyou, compared, record, gap, moved, special (once/every and others/onlyyou cannot both be true).

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://swapped-or-changed.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":"swapped-or-changed"}' | 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": "line-up", "thing": "The spare door key", "what_changed": "...", "notes": "...", "clues": "...", "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 swapped-or-changed:<task>:<inputHash>:a<attempt> (the one task is line-up), 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="swapped-or-changed:line-up:$(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: swapped-or-changed:line-up:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json