← Pretty Messy
API

Drive Pretty Messy from your own code

The one metered task, mess-weigh, is available over HTTP. It takes 1 lovely-but-messy thing or habit, what was already tried, and up to 8 rows - each a delight or a mess with its size, how often it happens and its weight - plus the page's verdict, and returns 1 problem, 3 ways out (sweep and keep, prune back, replant) and 1 recommendation.

The division of labour first: a fixed rule weighs the rows and picks the verdict; the model frames the problem and the ways out. After a run the page checks that the verdict matches its weighing, that all 3 ways come back in order, that the recommendation names the main delight and the worst mess, that every number comes from the rows and that no ids leak into the prose.

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":"pretty-messy"}), 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, tried, rows, 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: a reading 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 verdict

Every run sends task: mess-weigh (the one task is mess-weigh); a missing or different one is still answered as mess-weigh. Each row's weight is its size (tiny 1 to huge 5) times how often (rarely 1, now and then 2, most weeks 3, most days 4). The rule, in order: 4 x delights >= 5 x messes sweep; 5 x delights >= 4 x messes prune; otherwise replant. The main delight and the worst mess are the heaviest row on each side, the earlier row on a tie.

FieldRequiredWhat it holds
taskyesAlways mess-weigh.
thingyesThe thing or habit in the person's words, up to 60 characters.
triedyesWhat was already tried, up to 100 characters, or nothing yet.
rowsyes1 line per row: item 1 | delight | size: huge | how often: most days | weight: 20 | says: ....
factsyesThe page's lines: delight total:, mess total:, verdict:, main delight:, worst mess:.

The reply

job.output.output is a JSON string:

{"task": "mess-weigh", "verdict": "sweep", "problem": "...", "options": [{"kind": "sweep", "title": "...", "keeps": "...", "costs": "...", "first_step": "..."}, {"kind": "prune", ...}, {"kind": "replant", ...}], "recommendation": "...", "closing": "..."}
FieldWhat it holds
verdictThe page's verdict, copied: sweep, prune or replant.
problem1 sentence naming the main delight and the worst mess, at most 30 words.
optionsExactly 3, in order sweep, prune, replant: kind, title (at most 8 words), keeps and costs (at most 20 words each) and first_step (at most 30 words).
recommendationStarts with the verdict's way name, gives the 2 totals and names the main delight and worst mess as item numbers, at most 40 words.
closing1 light sentence, at most 20 words.

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

Worked examples - every verdict

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

White linen sofa - delights 32 against messes 16: sweep and keep

Input:

{
 "task": "mess-weigh",
 "thing": "white linen sofa",
 "tried": "nothing yet",
 "rows": "item 1 | delight | size: huge | how often: most days | weight: 20 | says: sinking into it every evening\nitem 2 | delight | size: medium | how often: most days | weight: 12 | says: the whole room feels bright\nitem 3 | mess | size: big | how often: most weeks | weight: 12 | says: every coffee drip shows\nitem 4 | mess | size: tiny | how often: most days | weight: 4 | says: cushions slump out of shape",
 "facts": "delight total: 32 (2 delights)\nmess total: 16 (2 messes)\nverdict: sweep (delights at least 1.25 times the messes)\nmain delight: item 1 (weight 20)\nworst mess: item 3 (weight 12)"
}

Reply:

{
 "task": "mess-weigh",
 "verdict": "sweep",
 "problem": "How do you keep sinking into it every evening without every coffee drip showing on the white linen sofa?",
 "options": [
  {
   "kind": "sweep",
   "title": "A daily drip check",
   "keeps": "You keep sinking into it every evening and the whole room feels bright, with nothing about it altered.",
   "costs": "Cushions slump out of shape still, plus the effort of keeping up a new routine for every coffee drip.",
   "first_step": "Keep a small tray or cloth nearby so you can wipe up every coffee drip the moment it happens."
  },
  {
   "kind": "prune",
   "title": "Sink in less often",
   "keeps": "You still keep sinking into it, though on fewer evenings than before.",
   "costs": "You get fewer evenings of sinking into it, and cushions can still slump out of shape a little.",
   "first_step": "Pick some evenings to relax somewhere else, so the white linen sofa meets fewer coffee drips."
  },
  {
   "kind": "replant",
   "title": "Try a chair instead",
   "keeps": "You still get the feeling of sinking into it, just somewhere that is not the white linen sofa.",
   "costs": "The whole room no longer feels as bright, since the white linen sofa itself is gone.",
   "first_step": "Try sitting and relaxing in a plain armchair or on a floor cushion for a few evenings to see how it feels."
  }
 ],
 "recommendation": "Sweep and keep, since delights reach 32 against messes at 16, item 1 is the heart of it, and item 3 is what a routine should target.",
 "closing": "Keep sinking in and keep a cloth close by, small habits can protect the white linen sofa too."
}

Jigsaw on the dining table - delights 22 against messes 25: prune back

Input:

{
 "task": "mess-weigh",
 "thing": "1000-piece jigsaw on the dining table",
 "tried": "nothing yet",
 "rows": "item 1 | delight | size: big | how often: most days | weight: 16 | says: a calm break after work\nitem 2 | mess | size: big | how often: most days | weight: 16 | says: no room to eat at the table\nitem 3 | delight | size: small | how often: most weeks | weight: 6 | says: the cheer when a section clicks\nitem 4 | mess | size: medium | how often: most weeks | weight: 9 | says: pieces end up under the sofa",
 "facts": "delight total: 22 (2 delights)\nmess total: 25 (2 messes)\nverdict: prune (delights and messes close to even)\nmain delight: item 1 (weight 16)\nworst mess: item 2 (weight 16)"
}

Reply:

{
 "task": "mess-weigh",
 "verdict": "prune",
 "problem": "How do you keep a calm break after work without losing room to eat at the dining table?",
 "options": [
  {
   "kind": "sweep",
   "title": "Clear space for meals",
   "keeps": "You keep a calm break after work and the cheer when a section clicks, with nothing about the jigsaw changed.",
   "costs": "Pieces can still end up under the sofa, plus the effort of clearing the table before each meal.",
   "first_step": "Set a tray or box to hold the puzzle pieces so you can clear a space to eat at the table before each meal."
  },
  {
   "kind": "prune",
   "title": "Shrink its table footprint",
   "keeps": "You still get a calm break after work, just with the jigsaw taking up less of the table.",
   "costs": "Pieces can still end up under the sofa, and you get less table space for the puzzle itself.",
   "first_step": "Set out a board or tray that limits the jigsaw to a smaller section of the table, leaving the rest free to eat at."
  },
  {
   "kind": "replant",
   "title": "A break away from the table",
   "keeps": "You still get a calm break after work, just somewhere other than at the dining table.",
   "costs": "You give up the cheer when a section clicks, since the jigsaw itself is no longer there.",
   "first_step": "Try taking a calm break after work with a short walk or a book instead of the jigsaw."
  }
 ],
 "recommendation": "Prune back, because delights sit at 22 while messes reach 25, keeping item 1 going but easing item 2 by giving the jigsaw a smaller claim on the table.",
 "closing": "A smaller table claim could still let that calm break happen, worth a first try."
}

Glitter nails at home, a tray already tried - delights 12 against messes 22: replant

Input:

{
 "task": "mess-weigh",
 "thing": "glitter nail art at home",
 "tried": "a tray under my hands while I paint",
 "rows": "item 1 | delight | size: big | how often: now and then | weight: 8 | says: sparkly nails for nights out\nitem 2 | delight | size: small | how often: now and then | weight: 4 | says: trying a new look\nitem 3 | mess | size: big | how often: most days | weight: 16 | says: glitter on every surface for days\nitem 4 | mess | size: medium | how often: now and then | weight: 6 | says: removal takes ages",
 "facts": "delight total: 12 (2 delights)\nmess total: 22 (2 messes)\nverdict: replant (messes well over the delights)\nmain delight: item 1 (weight 8)\nworst mess: item 3 (weight 16)"
}

Reply:

{
 "task": "mess-weigh",
 "verdict": "replant",
 "problem": "How do you get sparkly nails for nights out without glitter ending up on every surface for days?",
 "options": [
  {
   "kind": "sweep",
   "title": "Expand the tray routine",
   "keeps": "You keep sparkly nails for nights out and keep trying a new look, with nothing about it changed.",
   "costs": "Removal still takes ages, plus the extra effort of wiping down surfaces beyond the tray each time.",
   "first_step": "Beyond the tray you already use, add a cloth to wipe nearby surfaces right after you finish painting your nails."
  },
  {
   "kind": "prune",
   "title": "Glitter on fewer nails",
   "keeps": "You still get sparkly nails for nights out, just with glitter on fewer nails each time.",
   "costs": "Removal still takes ages for the glitter nails you do keep, and some glitter can still spread nearby.",
   "first_step": "Try glitter on fewer nails next time, keeping the tray under your hands as you already do."
  },
  {
   "kind": "replant",
   "title": "Shimmer without the spread",
   "keeps": "You can still get sparkly nails for nights out using a shimmer polish without any loose glitter.",
   "costs": "You give up some chances of trying a new look, since the loose glitter nail art itself is gone.",
   "first_step": "Try a shimmer polish without loose glitter for your next nights out instead of the glitter nail art."
  }
 ],
 "recommendation": "Replant, because delights only reach 12 against messes at 22, with item 3 far outweighing item 1, so a different way to get the sparkle fits better.",
 "closing": "A shimmer polish might give you that sparkle for nights out without the days of cleanup after."
}

Build the input yourself

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

const K = MessKit;
const built = K.buildInput(K.form({ thing: "white linen sofa", tried: "",
  items: [{ side: "bloom", text: "sinking into it every evening", size: 5, often: "daily" },
          { side: "petal", text: "every coffee drip shows", size: 4, often: "weekly" }] }));
built.input.facts;                      // the page's lines: totals, verdict, main delight, worst mess
const wire = K.toWire(built.input);     // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// built.facts.verdict                  // the verdict the page's rule gives
// K.check(reply, built).notes          // the page's re-check of the reply
// K.md(built, reply, "my sofa")        // the weighing 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://pretty-messy.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":"pretty-messy"}' | 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": "mess-weigh", "thing": "white linen sofa", "tried": "nothing yet", "rows": "item 1 | delight | size: huge | ...", "facts": "delight total: 32 ..."}
# - 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 pretty-messy:<task>:<inputHash>:a<attempt> (the one task is mess-weigh), 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="pretty-messy:mess-weigh:$(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: pretty-messy:mess-weigh:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json