← Pile of Shame
Pile API

Drive Pile of Shame from your own code

The one metered task, sort, is available over HTTP. It takes a game pile (title, hours left, progress and notes per game), a play budget, an optional mood and the page's own numbers. It returns a call for every game - play next, a two-hour try, the shelf or retire with thanks - a play order that fits the budget, a first-session tip for each queued game, a farewell for each retired one, a mood note and a first step.

The division of labour first: the page owns the parsing, the budget, the pile ratio, the verdict rule and the week-by-week slots; the model owns the calls, the order within the budget and the wording; the page re-checks the reply. Build pile and page_facts with the page's own script (see Build the input yourself) rather than by hand.

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": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"pile-of-shame"}), 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, pile, budget, mood, page_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 sort is metered and needs a personal token.
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 and the verdicts

Every run sends task: sort (the one task is sort); a missing or different one is still answered as sort. The verdict is a rule on the pile ratio (known hours left over budget hours, the ratio line of page_facts): clearable at 1.00 or less, trim above 1.00 up to 3.00, avalanche above 3.00. The model copies it; it never decides it.

FieldRequiredWhat it holds
taskyesAlways sort.
pileyesA JSON-encoded array (a string) of up to 40 games: {"id":"g1","title":"...","hours":"12","progress":"40","notes":"..."}. hours is hours left to finish, or "" when unknown; progress is a percent, 0 = not started; notes at most 200 characters.
budgetyesPlain words: hours a week, weeks, total hours.
moodyesWhat the player wants from play right now, or (not given).
page_factsyesThe page's numbers, one key | ... per line: budget_hours, known_hours, unknown_hours_games, ratio, verdict (with its rule) and started.
clip_notenoWhat the page cut, if anything.
retry_notenoOnly on the automatic reformat retry.

The reply

One JSON object, in output.output of the finished job:

{"task": "sort", "title": "...", "verdict": "clearable | trim | avalanche", "verdict_reason": "...",
 "games": [{"id": "g1", "call": "play-next | sample | shelve | retire", "order": 1, "why": "...", "reentry": "...", "thanks": ""}],
 "mood_note": "...", "first_step": "..."}
FieldWhat it holds
verdict / verdict_reasonThe rule above, and one sentence (at most 30 words) quoting the known hours and the budget hours.
gamesOne entry per game, in pile order. The call is the first rule that applies: retire only when the game's own note says the interest is gone; sample for an unstarted game with unknown hours or an unsure note (costs 2 hours); shelve for a started game with unknown hours; then play-next while the queue fits budget_hours, and shelve for the rest. order numbers the queue 1, 2, 3 with no gaps (null off the queue); why at most 25 words; reentry (queued games only) at most 25; thanks (retired games only) at most 20.
mood_note / first_step / titleHow the queue answers the mood (at most 50 words); one small thing to do before the first session (at most 30); a playful title (at most 8).

The page then checks the reply (PileKit.check): the verdict against the rule, one call per game, retirements backed by the player's own note, tries only for unstarted games, no queued game without hours, the queue total against the budget, the order, the word limits, and no weekday, month, season, he or she, or internal field name in the text. It never looks up a game: every fact comes from the pile.

Worked examples, one per verdict

These are the three free samples on the page (made-up games): the exact input and a stand-in model's reply, unedited. Together they cover clearable, trim and avalanche, and all four calls.

A small cozy pile - clearable

Input:

{
 "task": "sort",
 "pile": "[{\"id\":\"g1\",\"title\":\"Moss & Mortar\",\"hours\":\"12\",\"progress\":\"70\",\"notes\":\"building my village, left off right after the harvest fair; love it\"},{\"id\":\"g2\",\"title\":\"Lantern Ferry\",\"hours\":\"6\",\"progress\":\"0\",\"notes\":\"short and relaxing, saving it for a calm week\"},{\"id\":\"g3\",\"title\":\"Gearheart Tactics\",\"hours\":\"15\",\"progress\":\"20\",\"notes\":\"hard fights, stuck on the second boss; still want to beat it\"},{\"id\":\"g4\",\"title\":\"Pocket Orchard\",\"hours\":\"\",\"progress\":\"0\",\"notes\":\"heard it's good, no idea how long\"}]",
 "budget": "5 hours a week for 9 weeks, 45 hours in all",
 "mood": "Cozy, low-stress evenings after work",
 "page_facts": "budget_hours | 45\nknown_hours | 33\nunknown_hours_games | 1 (Pocket Orchard)\nratio | 0.73\nverdict | clearable (rule: clearable when the ratio is 1.00 or less, trim above 1.00 up to 3.00, avalanche above 3.00)\nstarted | Moss & Mortar (70%); Gearheart Tactics (20%)"
}

Reply:

{
 "task": "sort",
 "title": "A Pile You Can Actually Clear",
 "verdict": "clearable",
 "verdict_reason": "With 33 hours of known game time against a 45 hours budget, the whole pile fits comfortably in the weeks ahead.",
 "games": [
  {
   "id": "g1",
   "call": "play-next",
   "order": 2,
   "why": "Already 70% in and loved, so finishing it keeps real progress moving during these low-stress evenings.",
   "reentry": "Pick up right after the harvest fair, where you left off, for an easy return to the village.",
   "thanks": ""
  },
  {
   "id": "g2",
   "call": "play-next",
   "order": 1,
   "why": "Not started yet, but short and relaxing - a calm match for these low-stress evenings, so it leads the queue.",
   "reentry": "Start with a short, easy session - it's built to be relaxing, so there's no pressure to rush.",
   "thanks": ""
  },
  {
   "id": "g3",
   "call": "play-next",
   "order": 3,
   "why": "20% in and still determined to beat the tough fight you're stuck on - good momentum to keep going.",
   "reentry": "Jump back in at the second boss - try a fresh strategy or two before diving into the fight again.",
   "thanks": ""
  },
  {
   "id": "g4",
   "call": "sample",
   "order": 4,
   "why": "Not started, with no sense of its length but good word of mouth - a two-hour try is a safe first step.",
   "reentry": "Give it a short first session and see how the opening hours feel before deciding what's next.",
   "thanks": ""
  }
 ],
 "mood_note": "The queue favors your request for cozy, low-stress evenings: Lantern Ferry's calm, relaxing note puts it first, then Moss & Mortar continues its 70% progress, Gearheart Tactics keeps its 20% momentum going, and Pocket Orchard gets a two-hour try at the end since its length is still unknown.",
 "first_step": "Before Lantern Ferry's first session, carve out one short, calm block of time - no prep needed beyond that."
}

A mixed bag - trim

Input:

{
 "task": "sort",
 "pile": "[{\"id\":\"g1\",\"title\":\"Saltglass Odyssey\",\"hours\":\"60\",\"progress\":\"15\",\"notes\":\"huge, I keep restarting it\"},{\"id\":\"g2\",\"title\":\"Wyrmwatch\",\"hours\":\"25\",\"progress\":\"80\",\"notes\":\"left off right before the final area, want to finish\"},{\"id\":\"g3\",\"title\":\"Neon Ferry Racing\",\"hours\":\"8\",\"progress\":\"0\",\"notes\":\"bought it on sale, honestly not interested anymore\"},{\"id\":\"g4\",\"title\":\"Hollow Bell\",\"hours\":\"10\",\"progress\":\"0\",\"notes\":\"might be too spooky for me, curious though\"},{\"id\":\"g5\",\"title\":\"Starling Drift\",\"hours\":\"\",\"progress\":\"40\",\"notes\":\"lost my place, no idea how much is left\"},{\"id\":\"g6\",\"title\":\"Tiny Lighthouse\",\"hours\":\"4\",\"progress\":\"0\",\"notes\":\"short and calm\"},{\"id\":\"g7\",\"title\":\"Ember Sonata\",\"hours\":\"30\",\"progress\":\"0\",\"notes\":\"been looking forward to this one for ages\"}]",
 "budget": "6 hours a week for 10 weeks, 60 hours in all",
 "mood": "Finishing things I already started, in short sessions",
 "page_facts": "budget_hours | 60\nknown_hours | 137\nunknown_hours_games | 1 (Starling Drift)\nratio | 2.28\nverdict | trim (rule: clearable when the ratio is 1.00 or less, trim above 1.00 up to 3.00, avalanche above 3.00)\nstarted | Saltglass Odyssey (15%); Wyrmwatch (80%); Starling Drift (40%)"
}

Reply:

{
 "task": "sort",
 "title": "Trim Time: Finish What You Started",
 "verdict": "trim",
 "verdict_reason": "With 137 hours of games against 60 hours of play time, there's more here than fits, so it's time to trim the pile.",
 "games": [
  {
   "id": "g1",
   "call": "shelve",
   "order": null,
   "why": "You keep restarting it, and 60 hours left doesn't fit this budget alongside shorter, closer-to-done picks, so it waits.",
   "reentry": "",
   "thanks": ""
  },
  {
   "id": "g2",
   "call": "play-next",
   "order": 1,
   "why": "You're 80% in and already want to finish, right at the final area - perfect for finishing what's started.",
   "reentry": "Jump back in right before the final area - you're close, so even a short session should get you near the end.",
   "thanks": ""
  },
  {
   "id": "g3",
   "call": "retire",
   "order": null,
   "why": "Bought on sale and honestly not interested anymore, so it's time to let this one go.",
   "reentry": "",
   "thanks": "No hard feelings - it's fine to let this one go and make room on the shelf."
  },
  {
   "id": "g4",
   "call": "sample",
   "order": 3,
   "why": "You're curious but unsure about the spookiness, so a short two-hour try settles it without a big commitment.",
   "reentry": "Give it a short first session to test the spooky mood; a quick look at the controls helps too.",
   "thanks": ""
  },
  {
   "id": "g5",
   "call": "shelve",
   "order": null,
   "why": "You're partway in but hours left are unknown, so it can't be costed yet - add them and it joins the queue.",
   "reentry": "",
   "thanks": ""
  },
  {
   "id": "g6",
   "call": "play-next",
   "order": 2,
   "why": "Short and calm at 4 hours, an easy fit for short sessions without using much of the budget.",
   "reentry": "Start with a quick session; at just 4 hours, you could finish it in one sitting without much planning.",
   "thanks": ""
  },
  {
   "id": "g7",
   "call": "shelve",
   "order": null,
   "why": "You've been looking forward to this one, but its 30 hours don't fit alongside shorter, closer-to-finishing picks this time.",
   "reentry": "",
   "thanks": ""
  }
 ],
 "mood_note": "The top pick is the game at 80%, right before the end, so you can finish what you started. Then a short four-hour game keeps things light, and a two-hour try explores something new without much commitment.",
 "first_step": "Before diving in, check your last save so you remember exactly where you left off before the final area."
}

No mood given - avalanche

Input:

{
 "task": "sort",
 "pile": "[{\"id\":\"g1\",\"title\":\"Kingdom of Ash and Clover\",\"hours\":\"90\",\"progress\":\"10\",\"notes\":\"feel like I should play it since everyone did\"},{\"id\":\"g2\",\"title\":\"Copper Tide\",\"hours\":\"40\",\"progress\":\"0\",\"notes\":\"\"},{\"id\":\"g3\",\"title\":\"Pocket Lantern Quest\",\"hours\":\"12\",\"progress\":\"55\",\"notes\":\"stopped at the swamp, want to go back\"},{\"id\":\"g4\",\"title\":\"Garden of Gears\",\"hours\":\"20\",\"progress\":\"0\",\"notes\":\"not sure it is for me\"},{\"id\":\"g5\",\"title\":\"Starfall Couriers\",\"hours\":\"35\",\"progress\":\"0\",\"notes\":\"can't wait\"},{\"id\":\"g6\",\"title\":\"Mosswick Mysteries\",\"hours\":\"6\",\"progress\":\"90\",\"notes\":\"one case left!\"},{\"id\":\"g7\",\"title\":\"Boulder Bros\",\"hours\":\"15\",\"progress\":\"30\",\"notes\":\"got bored of the grinding\"},{\"id\":\"g8\",\"title\":\"Velvet Circuit\",\"hours\":\"\",\"progress\":\"0\",\"notes\":\"\"}]",
 "budget": "3 hours a week for 8 weeks, 24 hours in all",
 "mood": "(not given)",
 "page_facts": "budget_hours | 24\nknown_hours | 218\nunknown_hours_games | 1 (Velvet Circuit)\nratio | 9.08\nverdict | avalanche (rule: clearable when the ratio is 1.00 or less, trim above 1.00 up to 3.00, avalanche above 3.00)\nstarted | Kingdom of Ash and Clover (10%); Pocket Lantern Quest (55%); Mosswick Mysteries (90%); Boulder Bros (30%)"
}

Reply:

{
 "task": "sort",
 "title": "Your Pile Is Technically an Avalanche",
 "verdict": "avalanche",
 "verdict_reason": "Your pile holds 218 hours of games but your budget is only 24 hours, so this is an avalanche, not a dent.",
 "games": [
  {
   "id": "g1",
   "call": "retire",
   "order": null,
   "why": "You feel you should play this only because everyone else did - that's obligation, not real interest, so retire it.",
   "reentry": "",
   "thanks": "It's okay to let this go - obligation isn't a reason to keep playing."
  },
  {
   "id": "g2",
   "call": "shelve",
   "order": null,
   "why": "At forty hours and not yet started, this is too long to fit what's left in the queue right now.",
   "reentry": "",
   "thanks": ""
  },
  {
   "id": "g3",
   "call": "play-next",
   "order": 2,
   "why": "You're over halfway through and said you want back in, so this comes right after the top pick.",
   "reentry": "Open your last save at the swamp and ease back in - you were already most of the way there.",
   "thanks": ""
  },
  {
   "id": "g4",
   "call": "sample",
   "order": 3,
   "why": "You're not sure about this one yet, so a two-hour try is a fair way to decide.",
   "reentry": "Start fresh, play a short first session, and see how the controls and pace feel.",
   "thanks": ""
  },
  {
   "id": "g5",
   "call": "shelve",
   "order": null,
   "why": "You can't wait to start this one, but thirty-five hours doesn't fit the queue with what's already in it.",
   "reentry": "",
   "thanks": ""
  },
  {
   "id": "g6",
   "call": "play-next",
   "order": 1,
   "why": "You're ninety percent in with six hours left and one case to close, so this is the top pick.",
   "reentry": "Load your last save and finish that final case - you are nearly at the end.",
   "thanks": ""
  },
  {
   "id": "g7",
   "call": "retire",
   "order": null,
   "why": "You said the grinding bored you, and thirty percent in is proof you gave it a real shot.",
   "reentry": "",
   "thanks": "It's okay to let this go - burning out on the grind is a fine reason to stop."
  },
  {
   "id": "g8",
   "call": "sample",
   "order": 4,
   "why": "Its length isn't known yet, so a two-hour try will show whether it's worth more time.",
   "reentry": "Start it up, play a short first session, and get a feel for the controls.",
   "thanks": ""
  }
 ],
 "mood_note": "No mood was given, so the order follows progress and length: the ninety-percent game leads, then the one you already want back into, followed by two short tries to see what's worth more of your time.",
 "first_step": "Before your first session, open your last save and reread your note about that final case so it's ready."
}

Build the input yourself

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

const K = PileKit;
const built = K.buildInput({ weekly: "6", from: "2026-10-26", to: "2027-01-03", mood: "Short sessions",
  pile: "Wyrmwatch | 25 | 80% | left off before the final area\nTiny Lighthouse | 4 | 0 | short and calm" });
built.verdict;                                   // "clearable" - 29 known hours against a 60-hour budget
built.ratio;                                     // 0.48
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 sort
// K.schedule(built, reply)                      // each queued game with the weeks it fills

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://pile-of-shame.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":"pile-of-shame"}' | 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": "sort", "pile": "[{\"id\":\"g1\",\"title\":\"Saltglass Odyssey\",\"hours\":\"60\",\"progress\":\"15\",\"notes\":\"huge, I keep restarting it\"},{\"id\":\"g2\",\"title\":\"Wyrmwatch\",\"hours\":\"25\",\"progress\":\"80\",\"notes\":\"left off right before the final area, want to finish\"},{\"id\":\"g3\",\"title\":\"Neon Ferry Racing\",\"hours\":\"8\",\"progress\":\"0\",\"notes\":\"bought it on sale, honestly not interested anymore\"},{\"id\":\"g4\",\"title\":\"Hollow Bell\",\"hours\":\"10\",\"progress\":\"0\",\"notes\":\"might be too spooky for me, curious though\"},{\"id\":\"g5\",\"title\":\"Starling Drift\",\"hours\":\"\",\"progress\":\"40\",\"notes\":\"lost my place, no idea how much is left\"},{\"id\":\"g6\",\"title\":\"Tiny Lighthouse\",\"hours\":\"4\",\"progress\":\"0\",\"notes\":\"short and calm\"},{\"id\":\"g7\",\"title\":\"Ember Sonata\",\"hours\":\"30\",\"progress\":\"0\",\"notes\":\"been looking forward to this one for ages\"}]", "budget": "6 hours a week for 10 weeks, 60 hours in all", "mood": "Finishing things I already started, in short sessions", "page_facts": "budget_hours | 60\nknown_hours | 137\nunknown_hours_games | 1 (Starling Drift)\nratio | 2.28\nverdict | trim (rule: clearable when the ratio is 1.00 or less, trim above 1.00 up to 3.00, avalanche above 3.00)\nstarted | Saltglass Odyssey (15%); Wyrmwatch (80%); Starling Drift (40%)"}
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 pile-of-shame:<task>:<inputHash>:a<attempt> (the one task is sort), 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="pile-of-shame:sort:$(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: pile-of-shame:sort:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json