← Pardon My Pleasure
API

Drive Pardon My Pleasure from your own code

The one metered task, grant-pardons, is available over HTTP. It takes 1 to 6 small guilty pleasures, each with a name, an optional line on what it looks like, and three answers: who pays for it, whether it crowds out something you care about, and where the guilt comes from. It returns a short mock-formal pardon, a first-principles reason and a next step for every pleasure.

The division of labour first: the page decides; the model words it. The page weighs each pleasure (0-4), picks its verdict, what the guilt rests on, its step, the words its pardon opens with and the most innocent pleasure, and sends those as facts. The model never changes a verdict; it writes the pardons and explains. The name the pardons are made out to is added by the page and never sent.

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":"pardon-my-pleasure"}), 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, pleasures, 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 verdicts

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

FieldRequiredWhat it holds
taskyesAlways grant-pardons.
pleasuresyesOne numbered line per pleasure (1 to 6): name | what it looks like: ... | who pays for it: ... | does it crowd out something you care about: ... | where the guilt comes from: ..., with "not given" for a blank note.
factsyesThe page's lines: per pleasure its weight, its verdict, what the guilt rests on, the words its pardon opens with and its step (an out-of-court line names the kind of thing instead and is not weighed); then the count of each verdict and the most innocent pleasure.
retry_notenoOnly on a reformat retry: why the previous reply was unusable.

The weight adds who pays (Only me 0, Someone else, a little 1, Someone else, a lot 2) and what it crowds out (No 0, Sometimes 1, Often 2). The verdict, first match wins: words that mention gambling, drink, smoking or vaping, drugs, debt, speeding, taking things, hurting yourself or someone else, or adult content are out-of-court (not for this court); anything someone else pays for is settle-up (pardoned once settled); Often, or Sometimes with My own sense it is too much, is curfew (pardoned with a curfew); everything else is pardoned (pardoned in full).

The reply

One JSON object, no prose around it:

{"task": "grant-pardons", "headline": "...", "pleasures": [{"name": "...", "verdict": "curfew", "decree": "...", "why": "...", "step": "..."}], "closing": "..."}
FieldWhat it holds
headlineAt most 22 words summing up the pardons; counts match facts.
pleasures[]One per input pleasure, same order and exact names: verdict copied from facts, decree (15 to 40 words, addressed to you, opening with the words in facts, unsigned), why (at most 50 words, first principles from that pleasure's own answers) and step (one plain move, at most 35 words, never a number or time of its own).
closingOne sentence starting with the most innocent pleasure (or saying nothing was pardoned in full).

The page then checks the reply (PardonKit.check) - each verdict against its own, every pleasure present, each pardon's opening, time, day, number or weather words you did not give, health, diet or money advice, contact details or public posting - and lists anything it finds under "The page's check".

Worked examples - all four verdicts

These are the 3 free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover all four verdicts.

A night in - verdicts: pardoned in full, pardoned with a curfew, pardoned once settled

Input:

{
 "task": "grant-pardons",
 "pleasures": "1. Cereal for dinner | what it looks like: a big bowl eaten on the sofa | who pays for it: Only me | does it crowd out something you care about: No | where the guilt comes from: What others might think\n2. Reality TV marathons | what it looks like: one more episode, then another | who pays for it: Only me | does it crowd out something you care about: Often | where the guilt comes from: My own sense it is too much\n3. Singing loudly in the shower | what it looks like: my flatmate can hear every word | who pays for it: Someone else, a little | does it crowd out something you care about: No | where the guilt comes from: What others might think",
 "facts": "1. Cereal for dinner: weight 0 of 4; verdict pardoned; guilt rests on: borrowed (what others might think); pardon opens: Pardoned in full; step: nothing to change\n2. Reality TV marathons: weight 2 of 4; verdict curfew; guilt rests on: your own (my own sense it is too much); pardon opens: Pardoned with a curfew; step: set your own limit on it so it stops crowding out what you care about\n3. Singing loudly in the shower: weight 1 of 4; verdict settle-up; guilt rests on: borrowed (what others might think); pardon opens: Pardoned once settled; step: ask whoever it costs before you start, or make it up to them\ncounts: pardoned 1, curfew 1, settle-up 1, out-of-court 0 (of 3 pleasures)\nmost innocent pleasure: Cereal for dinner"
}

Reply:

{
 "task": "grant-pardons",
 "headline": "Three pleasures come before the court: one pardoned in full, one pardoned with a curfew, and one pardoned once settled.",
 "pleasures": [
  {
   "name": "Cereal for dinner",
   "verdict": "pardoned",
   "decree": "Pardoned in full, you may sit down to your big bowl of cereal for dinner on the sofa, with nothing left to answer for.",
   "why": "You answered only me when asked who pays for your cereal for dinner, and no when asked if it crowds out something you care about, so nothing here costs anyone else or takes away what matters to you. The guilt follows what others might think rather than any real cost.",
   "step": "You can keep having your cereal for dinner exactly as it is, out in the open, with nothing about it needing to change."
  },
  {
   "name": "Reality TV marathons",
   "verdict": "curfew",
   "decree": "Pardoned with a curfew, your reality TV marathons, one more episode then another, may continue, provided you set a limit of your own choosing, upon which this court shall trust your word.",
   "why": "You answered only me when asked who pays, but often when asked what it crowds out, so your reality TV marathons do take away from what matters to you. You said the guilt here comes from my own sense it is too much, and that deserves to be taken seriously.",
   "step": "You set your own limit on the marathons, so they stop crowding out what matters to you."
  },
  {
   "name": "Singing loudly in the shower",
   "verdict": "settle-up",
   "decree": "Pardoned once settled, your singing loudly in the shower, with your flatmate hearing every word, stands forgiven the moment you have set things right with the one who bears it.",
   "why": "You answered someone else, a little for who pays, and no for what it crowds out, so it costs a little even though nothing of yours is pushed aside. The guilt comes from what others might think, not from that cost, which is still real.",
   "step": "You have a quiet word with whoever hears it, or find your own way to make it up to them."
  }
 ],
 "closing": "Cereal for dinner stands as the most innocent pleasure of the three."
}

Work day - verdicts: pardoned in full, pardoned with a curfew, pardoned once settled

Input:

{
 "task": "grant-pardons",
 "pleasures": "1. Re-reading the same comfort novel | what it looks like: not given | who pays for it: Only me | does it crowd out something you care about: No | where the guilt comes from: A rule from growing up\n2. Cat videos at my desk | what it looks like: a lunch break turns into a long scroll | who pays for it: Only me | does it crowd out something you care about: Sometimes | where the guilt comes from: My own sense it is too much\n3. Always taking the window seat | what it looks like: the rest of the team never gets a turn | who pays for it: Someone else, a lot | does it crowd out something you care about: Sometimes | where the guilt comes from: What others might think",
 "facts": "1. Re-reading the same comfort novel: weight 0 of 4; verdict pardoned; guilt rests on: borrowed (a rule from growing up); pardon opens: Pardoned in full; step: nothing to change\n2. Cat videos at my desk: weight 1 of 4; verdict curfew; guilt rests on: your own (my own sense it is too much); pardon opens: Pardoned with a curfew; step: set your own limit on it so it stops crowding out what you care about\n3. Always taking the window seat: weight 3 of 4; verdict settle-up; guilt rests on: borrowed (what others might think); pardon opens: Pardoned once settled; step: talk it over with whoever it costs and agree a fair way to keep it\ncounts: pardoned 1, curfew 1, settle-up 1, out-of-court 0 (of 3 pleasures)\nmost innocent pleasure: Re-reading the same comfort novel"
}

Reply:

{
 "task": "grant-pardons",
 "headline": "This roll brings one pleasure pardoned in full, one pardoned with a curfew and one pardoned once settled.",
 "pleasures": [
  {
   "name": "Re-reading the same comfort novel",
   "verdict": "pardoned",
   "decree": "Pardoned in full, this court restores re-reading the same comfort novel to you without reservation, guilt lifted entirely and no further proceedings required.",
   "why": "You answered only me for who pays and no for what it crowds out, so this pleasure costs no one anything and crowds out nothing you care about. This guilt traces back to a rule from growing up, not to anything it actually costs.",
   "step": "You can go on re-reading the same comfort novel openly, just as you already do, no adjustment required."
  },
  {
   "name": "Cat videos at my desk",
   "verdict": "curfew",
   "decree": "Pardoned with a curfew, you may keep cat videos at your desk on the condition that you set your own limit, this bench having better things to judge.",
   "why": "You answered only me for who pays and sometimes for what it crowds out, so the cost stays with you but the crowding out is real. The guilt you named, my own sense it is too much, deserves honest attention rather than a shrug.",
   "step": "Decide for yourself where the line is on cat videos at your desk, so the scrolling stops crowding out what you care about."
  },
  {
   "name": "Always taking the window seat",
   "verdict": "settle-up",
   "decree": "Pardoned once settled, you may go on always taking the window seat once you have made it right with whoever it costs, this court preferring fairness to any formal penalty.",
   "why": "You answered someone else, a lot for who pays and sometimes for what it crowds out, so this costs someone else for real and crowds out something you care about. The guilt you named, what others might think, is borrowed, yet what it costs that someone else stays real.",
   "step": "Have a word with whoever taking the window seat costs and agree together on a fair way to keep doing it."
  }
 ],
 "closing": "Re-reading the same comfort novel stands as the most innocent pleasure on this roll, lighter than all the rest."
}

Lazy day off - verdicts: pardoned in full, pardoned once settled, not for this court

Input:

{
 "task": "grant-pardons",
 "pleasures": "1. Ketchup on everything | what it looks like: not given | who pays for it: Only me | does it crowd out something you care about: No | where the guilt comes from: What others might think\n2. A second slice of cake | what it looks like: not given | who pays for it: Only me | does it crowd out something you care about: Sometimes | where the guilt comes from: A rule from growing up\n3. Hogging the sofa for video games | what it looks like: the others end up on the floor | who pays for it: Someone else, a lot | does it crowd out something you care about: Often | where the guilt comes from: Not sure\n4. A flutter on the horses | what it looks like: a small stake on the big races | who pays for it: Only me | does it crowd out something you care about: No | where the guilt comes from: What others might think",
 "facts": "1. Ketchup on everything: weight 0 of 4; verdict pardoned; guilt rests on: borrowed (what others might think); pardon opens: Pardoned in full; step: nothing to change\n2. A second slice of cake: weight 1 of 4; verdict pardoned; guilt rests on: borrowed (a rule from growing up); pardon opens: Pardoned in full; step: nothing to change\n3. Hogging the sofa for video games: weight 4 of 4; verdict settle-up; guilt rests on: unclear (not sure); pardon opens: Pardoned once settled; step: talk it over with whoever it costs and agree a fair way to keep it\n4. A flutter on the horses: verdict out-of-court (its own words mention gambling); not weighed; pardon opens: Not for this court; step: no pardon here; if it ever worries you, talk to someone you trust or a professional\ncounts: pardoned 2, curfew 0, settle-up 1, out-of-court 1 (of 4 pleasures)\nmost innocent pleasure: Ketchup on everything"
}

Reply:

{
 "task": "grant-pardons",
 "headline": "2 pleasures are pardoned in full, 1 is pardoned once settled, and 1 is not for this court.",
 "pleasures": [
  {
   "name": "Ketchup on everything",
   "verdict": "pardoned",
   "decree": "Pardoned in full, ketchup on everything is no cause for guilt, so slather it on freely, as is only right and proper.",
   "why": "You answered only me for who pays and no for what it crowds out, so nothing here falls on anyone else or squeezes out anything you care about. The guilt rests on what others might think, which follows someone else's rule rather than a real cost.",
   "step": "You can enjoy ketchup on everything openly and exactly as it is, with nothing here that needs to change."
  },
  {
   "name": "A second slice of cake",
   "verdict": "pardoned",
   "decree": "Pardoned in full, a second slice of cake owes no apology to anyone, and may be savored without a single backward glance.",
   "why": "You answered only me for who pays and sometimes for what it crowds out, a small enough dent to still earn a full pardon. The guilt comes from a rule from growing up, which follows someone else's rule rather than a real cost.",
   "step": "You can enjoy a second slice of cake openly, just as it is, with nothing that needs changing here."
  },
  {
   "name": "Hogging the sofa for video games",
   "verdict": "settle-up",
   "decree": "Pardoned once settled, hogging the sofa for video games is forgiven once you make things right with whoever it costs, and the books are balanced between you.",
   "why": "You answered someone else, a lot for who pays and often for what it crowds out, so this one carries a real cost to someone else and squeezes out something you care about. Where the guilt comes from, you answered not sure, so what it rests on is simply unclear.",
   "step": "You talk it over with whoever it costs and agree between you on a fair way to keep hogging the sofa for video games."
  },
  {
   "name": "A flutter on the horses",
   "verdict": "out-of-court",
   "decree": "Not for this court, a flutter on the horses falls outside what small pleasures this court was built to hear, so it is neither pardoned nor condemned here.",
   "why": "You brought a flutter on the horses, but this small court only hears small, harmless pleasures, and gambling is simply a kind it was never built to weigh.",
   "step": "If a flutter on the horses ever worries you, someone you trust or a professional is the one to talk to, not this court."
  }
 ],
 "closing": "Ketchup on everything is the most innocent pleasure on the roll, pardoned in full without a shadow of doubt."
}

Build the input yourself

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

const K = PardonKit;
const form = K.form({ alias: "", pleasures: [
  { name: "Cereal for dinner", note: "", pays: "me", crowds: "no", guilt: "others" },
  { name: "Reality TV marathons", note: "one more episode", pays: "me", crowds: "often", guilt: "own" }] });
const built = K.buildInput(form);       // built.facts holds each pleasure's weight, verdict, guilt and step
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), K.addressee(form))   // the roll as Markdown

The answer keys: pays is me, little or lot; crowds is no, sometimes or often; guilt is others, upbringing, own or unsure.

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://pardon-my-pleasure.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":"pardon-my-pleasure"}' | 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": "grant-pardons", "pleasures": "1. Cereal for dinner | what it looks like: ... | ...", "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 pardon-my-pleasure:<task>:<inputHash>:a<attempt> (the one task is grant-pardons), 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="pardon-my-pleasure:grant-pardons:$(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: pardon-my-pleasure:grant-pardons:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json