← Keep Apart
API

Drive Keep Apart from your own code

The one metered task, separate, is available over HTTP. It takes two wants that seem to cancel each other out, where they collide and what was tried, plus the page's own check of the four separations (time, place, condition, parts) and the verdict. It returns one sentence naming the clash, an idea for every open separation (and why each shut one stays shut), a short plan with what each want keeps, a first thing to try, a sign to watch for and a closing line.

The division of labour first: a fixed rule on the page decides which separations are open and picks the verdict; the model only writes the words. After a run the page checks that the reply copied the verdict and every separation's state, kept shut lines out of the plan and kept to the house rules. A playful thinking aid, not professional 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": "..." } }

The token is minted for this app (the guest call takes {"slug":"keep-apart"}), 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, want_a, want_b, checks, verdict) 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 written split 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 verdicts

Every run sends task: separate (the one task is separate); a missing or different one is still answered as separate. A separation is shut when the person answers "yes" to needing both wants together on that line (same time, same place, every situation, the whole thing) and open on "no" or "not sure". The verdict, checked in order: every separation shut - trade-off; otherwise the first open separation, in the order time, place, condition, parts, that has a clue word in the person's text - split-time, split-place, split-condition or split-parts; otherwise the first open separation in that order.

FieldRequiredWhat it holds
taskyesAlways separate.
want_a, want_byesThe two wants, at most 80 characters each.
clash, triedyesWhere they collide (160) and what was tried (120), or (not given).
checksyes4 lines such as place: open (you answered no, both are not needed in the same place) - clue words: table.
verdictyesThe page's verdict id.
retry_notenoOnly on the page's single reformat retry.

The reply is one JSON object:

{"verdict": "split-place", "clash": "...", "separations": [{"id": "time", "state": "closed", "idea": "..."}, {"id": "place", "state": "open", "idea": "..."}, {"id": "condition", "state": "open", "idea": "..."}, {"id": "parts", "state": "open", "idea": "..."}], "resolution": {"name": "...", "steps": ["...", "..."], "keeps_a": "...", "keeps_b": "..."}, "first_try": "...", "watch_for": "...", "closing": "..."}
FieldWhat it holds
verdictCopied from the input.
clashOne sentence naming the clash in words from both wants, at most 24 words.
separations4 objects in the order time, place, condition, parts: state copied from checks, idea a way to separate on an open line (at most 22 words) or why a shut line stays shut (at most 14).
resolutionname (at most 8 words), 2 to 4 steps (at most 20 words each) built on the verdict's line, and what each want keeps (keeps_a, keeps_b, at most 20 words each).
first_try, watch_for, closingA small first thing to try (20), a sign the split is not holding (18) and a closing line that matches the verdict (18).

The page then checks the reply (TrapKit.check) and lists any disagreement under "The page's check".

Worked examples - every verdict

These are the five free samples on the page: the exact input the page sends and the reply that replays for free.

Book vs background TV - split in time, with a shut place line

Input:

{
 "task": "separate",
 "want_a": "a quiet hour of reading",
 "want_b": "the TV on for company",
 "clash": "both happen in the living room after dinner",
 "tried": "headphones, which I find uncomfortable",
 "checks": "time: open (you answered no, both are not needed at the same time) - clue words: hour, after, dinner\nplace: closed (you answered yes, both are needed in the same place) - clue words: living room\ncondition: open (you answered not sure whether both are needed in every situation) - clue words: none\nparts: closed (you answered yes, both are needed for the whole thing) - clue words: none",
 "verdict": "split-time"
}

Reply:

{
 "verdict": "split-time",
 "clash": "You want a quiet hour of reading, but the TV on for company claims that same time in your living room after dinner.",
 "separations": [
  {
   "id": "time",
   "state": "open",
   "idea": "Keep your quiet reading hour separate from TV time by reading first, then switching the TV on after that hour ends."
  },
  {
   "id": "place",
   "state": "closed",
   "idea": "You already said this must stay the living room, so no split here."
  },
  {
   "id": "condition",
   "state": "open",
   "idea": "Pick a simple sign that tells you when your quiet reading hour is on, not the TV for company."
  },
  {
   "id": "parts",
   "state": "closed",
   "idea": "You already said this must stay whole, so no split here."
  }
 ],
 "resolution": {
  "name": "Read First, Then Switch On the TV",
  "steps": [
   "Set aside your quiet hour of reading first, before switching anything on in the living room.",
   "Once that hour ends, switch the TV on for company, keeping the activities apart in time.",
   "Set the headphones aside, since this split no longer needs them now that reading and TV have separate times."
  ],
  "keeps_a": "Your quiet hour of reading keeps its own uninterrupted time, before the TV for company ever switches on.",
  "keeps_b": "The TV on for company still gets its own stretch of time, right after your reading hour wraps up."
 },
 "first_try": "Try doing your reading hour before switching the TV on, just this next time you're in the room.",
 "watch_for": "Watch for reaching to turn the TV on again before your reading hour has actually ended.",
 "closing": "Give this order a try, your reading and the TV for company can each still find their moment."
}

Puzzle on the dining table - split by place

Input:

{
 "task": "separate",
 "want_a": "a clear dining table for meals",
 "want_b": "to leave my jigsaw puzzle out until it is finished",
 "clash": "the puzzle covers the table at every meal",
 "tried": "(not given)",
 "checks": "time: closed (you answered yes, both are needed at the same time) - clue words: none\nplace: open (you answered no, both are not needed in the same place) - clue words: table\ncondition: open (you answered not sure whether both are needed in every situation) - clue words: none\nparts: open (you answered not sure whether both are needed for the whole thing) - clue words: none",
 "verdict": "split-place"
}

Reply:

{
 "verdict": "split-place",
 "clash": "A clear dining table for meals clashes with leaving your jigsaw puzzle out until it's finished on that table.",
 "separations": [
  {
   "id": "time",
   "state": "closed",
   "idea": "You said both need the same time, so this line stays shut."
  },
  {
   "id": "place",
   "state": "open",
   "idea": "Set up your puzzle on a separate spot, away from the table, so the table stays clear for meals."
  },
  {
   "id": "condition",
   "state": "open",
   "idea": "Keep the puzzle on the table except when something else genuinely needs that space."
  },
  {
   "id": "parts",
   "state": "open",
   "idea": "Split the table into a section kept for the puzzle and a section cleared for meals."
  }
 ],
 "resolution": {
  "name": "A Dedicated Spot for the Puzzle",
  "steps": [
   "Choose a spot away from the table that can hold your puzzle without being disturbed.",
   "Move your puzzle to that spot so the table stays clear for meals.",
   "Keep working on the puzzle in its own spot whenever you like."
  ],
  "keeps_a": "Your dining table stays clear and ready for meals, just as you want.",
  "keeps_b": "Your jigsaw puzzle stays out in its own spot until it's finished."
 },
 "first_try": "Set up a spot for the puzzle, away from the table, and move it there.",
 "watch_for": "Notice if the puzzle starts creeping back onto the table during meals.",
 "closing": "Give the new spot a try, and your table and puzzle can both have their place."
}

Cycle in, arrive fresh - split by condition, no clue word (the fallback)

Input:

{
 "task": "separate",
 "want_a": "to cycle to the office for exercise",
 "want_b": "to look fresh for client meetings",
 "clash": "I arrive sweaty on meeting days",
 "tried": "a fresh shirt in my bag, which gets creased",
 "checks": "time: closed (you answered yes, both are needed at the same time) - clue words: none\nplace: closed (you answered yes, both are needed in the same place) - clue words: office\ncondition: open (you answered no, both are not needed in every situation) - clue words: none\nparts: closed (you answered yes, both are needed for the whole thing) - clue words: none",
 "verdict": "split-condition"
}

Reply:

{
 "verdict": "split-condition",
 "clash": "Cycling to the office for exercise leaves you arriving sweaty instead of fresh for client meetings.",
 "separations": [
  {
   "id": "time",
   "state": "closed",
   "idea": "Your answer keeps both needed at the same time, so this line stays shut."
  },
  {
   "id": "place",
   "state": "closed",
   "idea": "Your answer keeps both needed in the office, so this line stays shut."
  },
  {
   "id": "condition",
   "state": "open",
   "idea": "Cycle for exercise on days without client meetings, and swap to the shirt from your bag only on meeting days."
  },
  {
   "id": "parts",
   "state": "closed",
   "idea": "Your answer keeps both needed for the whole thing, so this line stays shut."
  }
 ],
 "resolution": {
  "name": "Match your shirt routine to meeting days",
  "steps": [
   "On days without a client meeting, skip worrying about looking fresh and just cycle in.",
   "On days with a client meeting, roll the shirt in your bag instead of folding it.",
   "Change into that shirt at the office as soon as you arrive from cycling in."
  ],
  "keeps_a": "You keep cycling to the office for your exercise every day, meeting or not.",
  "keeps_b": "You keep looking fresh for client meetings by changing into the shirt right after cycling in."
 },
 "first_try": "Try rolling the fresh shirt in your bag instead of folding it, before your next client meeting.",
 "watch_for": "Watch for the shirt still coming out creased on a client meeting day.",
 "closing": "This small shift can let you keep cycling and still look ready for every client meeting."
}

Minimal shelf, every book - split into parts, skipping an open line with no clue word

Input:

{
 "task": "separate",
 "want_a": "a neat, minimal bookshelf",
 "want_b": "to keep every book I love on display",
 "clash": "the shelf looks crammed and cluttered",
 "tried": "(not given)",
 "checks": "time: closed (you answered yes, both are needed at the same time) - clue words: none\nplace: closed (you answered yes, both are needed in the same place) - clue words: none\ncondition: open (you answered no, both are not needed in every situation) - clue words: none\nparts: open (you answered no, both are not needed for the whole thing) - clue words: shelf",
 "verdict": "split-parts"
}

Reply:

{
 "verdict": "split-parts",
 "clash": "Keeping every book you love on display can make a neat, minimal bookshelf feel crammed and cluttered.",
 "separations": [
  {
   "id": "time",
   "state": "closed",
   "idea": "You said both need the same time, so this line stays shut."
  },
  {
   "id": "place",
   "state": "closed",
   "idea": "You said both need the same place, so this stays closed."
  },
  {
   "id": "condition",
   "state": "open",
   "idea": "During a certain occasion, let every book stay out; otherwise keep the shelf neat and minimal."
  },
  {
   "id": "parts",
   "state": "open",
   "idea": "Give the bookshelf a tidy section and a separate section where every loved book can gather."
  }
 ],
 "resolution": {
  "name": "Split the Bookshelf into Separate Parts",
  "steps": [
   "Split the bookshelf into parts: a spare, tidy part and a part holding every loved book.",
   "Arrange the tidy part with open spacing between the books so it stays calm and minimal.",
   "Let every loved book live in that part, without trimming down the collection."
  ],
  "keeps_a": "The tidy part keeps the bookshelf feeling neat and minimal.",
  "keeps_b": "The full part keeps every loved book on display."
 },
 "first_try": "Pick a part of the bookshelf and start grouping your loved books there.",
 "watch_for": "Watch for loved books spilling back into the tidy part and crowding it again.",
 "closing": "Give the parts a try and watch the clutter ease while every book stays on display."
}

Spontaneous vs planned weekends - every line shut, a real trade-off

Input:

{
 "task": "separate",
 "want_a": "to keep my weekends spontaneous",
 "want_b": "to plan weekend trips well ahead",
 "clash": "plans and spontaneity fight over the same Saturdays",
 "tried": "planning only the first half of each trip",
 "checks": "time: closed (you answered yes, both are needed at the same time) - clue words: weekends, weekend, saturdays\nplace: closed (you answered yes, both are needed in the same place) - clue words: none\ncondition: closed (you answered yes, both are needed in every situation) - clue words: none\nparts: closed (you answered yes, both are needed for the whole thing) - clue words: half",
 "verdict": "trade-off"
}

Reply:

{
 "verdict": "trade-off",
 "clash": "You want your weekends to stay spontaneous and also want to plan weekend trips well ahead.",
 "separations": [
  {
   "id": "time",
   "state": "closed",
   "idea": "You've already said this must happen at the same time, so it stays closed."
  },
  {
   "id": "place",
   "state": "closed",
   "idea": "Your answer already keeps this in the same place, so it stays shut."
  },
  {
   "id": "condition",
   "state": "closed",
   "idea": "You said this holds in every situation, so no switch applies here."
  },
  {
   "id": "parts",
   "state": "closed",
   "idea": "You already said this needs to stay whole, so no dividing here."
  }
 ],
 "resolution": {
  "name": "A fair middle-ground plan",
  "steps": [
   "Plan your weekend trips a little less tightly, leaving fewer details fixed before you set out.",
   "Accept that some choices will be settled ahead of time, even while you still crave spontaneity.",
   "Revisit this balance after each trip and adjust how loosely or tightly you plan ahead next time."
  ],
  "keeps_a": "You keep a real measure of spontaneity, deciding some things on the spot even after plans are made.",
  "keeps_b": "You keep real forward planning for your trips, just with fewer details locked down than before."
 },
 "first_try": "For your next trip, plan slightly less firmly than you normally would, and notice how that feels.",
 "watch_for": "You find yourself locking down every detail again, leaving no room at all for spontaneity.",
 "closing": "A fair middle ground between planning and spontaneity is well within your reach."
}

Build the input yourself

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

const K = TrapKit;
const built = K.buildInput(K.form({ want_a: "a tidy hallway", want_b: "my bike close to the door", clash: "", tried: "",
  answers: { time: "yes", place: "no", condition: "unsure", parts: "no" } }));
built.facts.verdict                     // the verdict the page's rule gives
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, "my trap")        // the split 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://keep-apart.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":"keep-apart"}' | 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": "separate", "want_a": "a clear dining table for meals", "want_b": "to leave my jigsaw puzzle out until it is finished", "clash": "...", "tried": "(not given)", "checks": "time: closed ...\nplace: open ...", "verdict": "split-place"}
# - 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 keep-apart:<task>:<inputHash>:a<attempt> (the one task is separate), 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="keep-apart:separate:$(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: keep-apart:separate:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json