← Fond Gripes
API

Drive Fond Gripes from your own code

The one metered task, gripes, is available over HTTP. It takes who the list is for, what you call them, a tone, up to eight numbered habits, the thing you love most and the page's own notes. It returns three checks, a verdict (too_sharp, sideways, too_sweet or sweet_spot), a care and challenge balance from 0 to 5 each, one line per habit (an "I hate..." line, or a kind note on why the habit stays out), the turn that starts "But mostly I", up to three fixes and a title.

The division of labour first: the page owns the habit check (looks or traits, insult words, sore spots, sideways digs, compliments in disguise, habits too short to picture), its guess, the verdict rule, the corners of the care and challenge square and the fortune teller; the model owns the three checks, the keep decisions and all the wording; the page then re-checks the reply.

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":"fond-gripes"}), 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, who, name, tone, habits, love, 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: writing a list 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: gripes (the one task is gripes); a missing or different one is still answered as gripes. The verdict is a rule on the reply's own three checks, applied in order: too_sharp when mean is true; sideways when veiled is true; too_sweet when toothless is true; otherwise sweet_spot. Each verdict owns a corner of the care and challenge square: the sweet spot care 3-5 and challenge 3-5; too sweet care 3-5 and challenge 0-2; too sharp care 0-2 and challenge 3-5; sideways care 0-2 and challenge 0-2.

FieldRequiredWhat it holds
taskyesAlways gripes.
whoyesOne of partner, friend, sibling, roommate, parent, other.
nameyesWhat the person calls them (up to 40 characters), or not given.
toneyesgentle, cheeky or deadpan.
habitsyesNumbered lines 1. ..., at most 8; a habit over about 180 characters keeps its start and end with the middle marked as cut.
loveyesThe thing the person loves most about them (up to about 240 characters), or not given - then the turn comes back with a bracketed blank.
page_factsyesThe page's notes, one key | ... per line: habit_count, habit_flags (per habit: looks or a trait, an insult word, a sore spot, a sideways dig, a compliment in disguise, too short to picture), page_guess (a hint only) and verdict_rule.
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": "gripes", "title": "...", "checks": {"mean": false, "veiled": false, "toothless": false},
 "verdict": "too_sharp | sideways | too_sweet | sweet_spot", "verdict_reason": "...",
 "balance": {"care": 4, "challenge": 4},
 "lines": [{"habit": 1, "keep": true, "line": "I hate ...", "note": ""},
           {"habit": 2, "keep": false, "line": "", "note": "..."}],
 "turn": "But mostly I ...", "fixes": ["..."]}
FieldWhat it holds
checksThree true/false judgements of the habits as written: mean (an insult, a jab at looks or a trait, or a real hurt such as money owed), veiled (a real complaint made through sarcasm), toothless (fewer than two real, picturable habits - the rest compliments in disguise or too vague).
verdict / verdict_reasonThe rule above on the three checks, and one sentence (at most 30 words).
balancecare and challenge, whole numbers 0 to 5 inside the verdict's corner.
linesOne {habit, keep, line, note} per habit, in order. Kept: a line of at most 25 words starting "I hate", speaking to them as "you". Left out (keep false): an empty line and a kind note of at most 15 words.
turn / fixes / titleOne sentence of at most 25 words starting "But mostly I" (with a bracketed blank when love is not given); one to three fixes of at most 20 words; a playful title of at most 6 words.

The page then checks the reply (GripeKit.check): the verdict against the rule on the reply's own checks, care and challenge inside the verdict's corner, one line per habit in order, kept lines that start "I hate", left-out habits with a reason, a turn that starts "But mostly I" (with a blank when the love box was empty), word limits, and nothing the person did not say - no new numbers, days or occasions, no he or she, no insults and no jabs at looks.

Worked examples, one per sample

These are the four free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover all four verdicts - a roommate who leaves one spoonful of cereal (the sweet spot), a partner who is "too generous" (too sweet), a sibling list with a jab at looks and money still owed (too sharp) and dishes that are always "soaking" (sideways).

One spoonful of cereal left - the sweet spot

Input:

{
 "task": "gripes",
 "who": "roommate",
 "name": "not given",
 "tone": "cheeky",
 "habits": "1. Leaves exactly one spoonful of cereal in the box and puts the box back\n2. Sings the wrong words to every song with total confidence\n3. Says 'five more minutes' and means forty\n4. Alphabetises the spice rack but never closes a cupboard door\n5. Narrates what the cat is thinking",
 "love": "You make every boring errand feel like an adventure",
 "page_facts": "habit_count | 5\nhabit_flags | 1: none / 2: none / 3: none / 4: none / 5: none\npage_guess | unknown - from word lists only, a hint that can be wrong\nverdict_rule | check in order and stop at the first that fits: 1) too_sharp when mean is true; 2) sideways when veiled is true; 3) too_sweet when toothless is true; 4) otherwise sweet_spot"
}

Reply:

{
 "task": "gripes",
 "title": "A Very Loving List of Grievances",
 "checks": {
  "mean": false,
  "veiled": false,
  "toothless": false
 },
 "verdict": "sweet_spot",
 "verdict_reason": "All five habits are specific, affectionate quirks with nothing unkind or hidden underneath, so the list lands in the sweet spot.",
 "balance": {
  "care": 4,
  "challenge": 4
 },
 "lines": [
  {
   "habit": 1,
   "keep": true,
   "line": "I hate how you leave exactly one spoonful of cereal in the box and then shamelessly put the box back anyway.",
   "note": ""
  },
  {
   "habit": 2,
   "keep": true,
   "line": "I hate how you sing the wrong words to every single song with such total confidence.",
   "note": ""
  },
  {
   "habit": 3,
   "keep": true,
   "line": "I hate how you say 'five more minutes' and somehow always mean forty.",
   "note": ""
  },
  {
   "habit": 4,
   "keep": true,
   "line": "I hate how you alphabetise the spice rack like a genius but can't ever close a cupboard door.",
   "note": ""
  },
  {
   "habit": 5,
   "keep": true,
   "line": "I hate how confidently you narrate what the cat is thinking, as if it's obvious.",
   "note": ""
  }
 ],
 "turn": "But mostly I hate how you make every boring errand feel like an adventure.",
 "fixes": [
  "Lead with the 'five more minutes meaning forty' line since it's the most specific and sets the tone right away.",
  "Save the line about narrating the cat's thoughts for last so it lands as a warm surprise."
 ]
}

Too generous, works too hard - too sweet

Input:

{
 "task": "gripes",
 "who": "partner",
 "name": "not given",
 "tone": "gentle",
 "habits": "1. You are too generous with everyone\n2. You always remember everything I mention\n3. You work too hard\n4. Too kind",
 "love": "not given",
 "page_facts": "habit_count | 4\nhabit_flags | 1: a compliment in disguise (too generous) / 2: a compliment in disguise (always remember) / 3: a compliment in disguise (work too hard) / 4: a compliment in disguise (too kind); too short to picture\npage_guess | too_sweet - from word lists only, a hint that can be wrong\nverdict_rule | check in order and stop at the first that fits: 1) too_sharp when mean is true; 2) sideways when veiled is true; 3) too_sweet when toothless is true; 4) otherwise sweet_spot"
}

Reply:

{
 "task": "gripes",
 "title": "Too Sweet For Your Own Good",
 "checks": {
  "mean": false,
  "veiled": false,
  "toothless": true
 },
 "verdict": "too_sweet",
 "verdict_reason": "Every habit reads as a compliment in disguise, with nothing specific enough to tease, so the list leans too sweet rather than sharp.",
 "balance": {
  "care": 5,
  "challenge": 0
 },
 "lines": [
  {
   "habit": 1,
   "keep": true,
   "line": "I hate how impossibly generous you are with everyone.",
   "note": ""
  },
  {
   "habit": 2,
   "keep": true,
   "line": "I hate that you remember absolutely everything I mention.",
   "note": ""
  },
  {
   "habit": 3,
   "keep": true,
   "line": "I hate that you work entirely too hard at everything you do.",
   "note": ""
  },
  {
   "habit": 4,
   "keep": true,
   "line": "I hate that you are so endlessly kind.",
   "note": ""
  }
 ],
 "turn": "But mostly I hate how much I love [the thing you love most] about you.",
 "fixes": [
  "Swap 'too generous with everyone' for a specific habit you can actually picture, not just admire.",
  "Trade 'too kind' for a small, real moment - it's too short to tease as it stands.",
  "Turn 'you work too hard' into one small, specific moment instead of a general compliment."
 ]
}

A sibling list with a jab at looks - too sharp

Input:

{
 "task": "gripes",
 "who": "sibling",
 "name": "Bean",
 "tone": "cheeky",
 "habits": "1. Still owes me money from the concert\n2. Has a nose like a ski jump\n3. Starts laughing at their own jokes before the punchline\n4. Too lazy to ever text back",
 "love": "You always have my back when it counts",
 "page_facts": "habit_count | 4\nhabit_flags | 1: a sore spot (owes, money) / 2: looks or a trait (nose) / 3: none / 4: an insult word (lazy)\npage_guess | too_sharp - from word lists only, a hint that can be wrong\nverdict_rule | check in order and stop at the first that fits: 1) too_sharp when mean is true; 2) sideways when veiled is true; 3) too_sweet when toothless is true; 4) otherwise sweet_spot"
}

Reply:

{
 "task": "gripes",
 "title": "Bean's Fine Print of Fondness",
 "checks": {
  "mean": true,
  "veiled": false,
  "toothless": false
 },
 "verdict": "too_sharp",
 "verdict_reason": "A debt that's still owed and a comment about their nose read as real hurts rather than fond teasing, which tips this list too sharp.",
 "balance": {
  "care": 1,
  "challenge": 4
 },
 "lines": [
  {
   "habit": 1,
   "keep": false,
   "line": "",
   "note": "Money they owe is a real issue that deserves its own honest talk between them."
  },
  {
   "habit": 2,
   "keep": false,
   "line": "",
   "note": "Their nose isn't something to tease - looks are off limits here."
  },
  {
   "habit": 3,
   "keep": true,
   "line": "I hate how you crack up at your own jokes before you even reach the punchline.",
   "note": ""
  },
  {
   "habit": 4,
   "keep": false,
   "line": "",
   "note": "Not texting back might be real, but calling them lazy crosses into insult."
  }
 ],
 "turn": "But mostly I hate how much I love that you always have my back when it counts.",
 "fixes": [
  "Drop the habit about owing money from the concert and raise it directly with them.",
  "Swap the nose comment for a real small habit instead of a look they can't change.",
  "Soften 'lazy' into a warmer tease about texting back, since the word itself stings."
 ]
}

The dishes are always soaking - sideways

Input:

{
 "task": "gripes",
 "who": "roommate",
 "name": "not given",
 "tone": "deadpan",
 "habits": "1. Funny how the dishes are always 'soaking' when it is your turn\n2. Must be nice to sleep until noon while some of us work\n3. Hums the whole time while brushing teeth\n4. Calls every dog 'buddy', even the ones that ignore you",
 "love": "You still laugh at all my terrible jokes",
 "page_facts": "habit_count | 4\nhabit_flags | 1: a sideways dig (funny how) / 2: a sideways dig (must be nice, some of us) / 3: none / 4: none\npage_guess | sideways - from word lists only, a hint that can be wrong\nverdict_rule | check in order and stop at the first that fits: 1) too_sharp when mean is true; 2) sideways when veiled is true; 3) too_sweet when toothless is true; 4) otherwise sweet_spot"
}

Reply:

{
 "task": "gripes",
 "title": "The Humming, Soaking, Buddy-Calling Files",
 "checks": {
  "mean": false,
  "veiled": true,
  "toothless": false
 },
 "verdict": "sideways",
 "verdict_reason": "Two habits turn real complaints about dishes and sleep-versus-work into sideways digs ('funny how', 'must be nice') instead of saying them plainly, which decides sideways.",
 "balance": {
  "care": 1,
  "challenge": 2
 },
 "lines": [
  {
   "habit": 1,
   "keep": false,
   "line": "",
   "note": "This is a real fairness complaint about chores - it deserves them hearing it directly."
  },
  {
   "habit": 2,
   "keep": false,
   "line": "",
   "note": "The sleep-versus-work split sounds like a real gripe - worth saying to them plainly."
  },
  {
   "habit": 3,
   "keep": true,
   "line": "I hate that you hum nonstop the whole time you're brushing your teeth, without fail.",
   "note": ""
  },
  {
   "habit": 4,
   "keep": true,
   "line": "I hate that you call every single dog 'buddy', even the ones that just ignore you completely.",
   "note": ""
  }
 ],
 "turn": "But mostly I hate how much I love that you still laugh at all my terrible jokes.",
 "fixes": [
  "Say the dishes-on-your-turn frustration straight to them, not as 'funny how' sarcasm.",
  "Raise the sleeping-until-noon-versus-working split as a real conversation, not a 'must be nice' jab.",
  "Lead with the humming-while-brushing-teeth line - it's the gentlest tease and sets the tone."
 ]
}

Build the input yourself

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

const K = GripeKit;
const built = K.buildInput({ who: "roommate", tone: "cheeky", name: "", habits: "Leaves one sip of milk in the carton
Hums the same three notes all day", love: "You make the worst days feel short" });
built.pre;                                       // { habits: [{ text, flags }], count, real, guess, ... } - the free habit check
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 written list
// K.verdictFor(reply.checks)                    // the verdict rule on the reply's own checks

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://fond-gripes.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":"fond-gripes"}' | 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": "gripes", "who": "roommate", "name": "not given", "tone": "cheeky", "habits": "1. ...", "love": "...", "page_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 fond-gripes:<task>:<inputHash>:a<attempt> (the one task is gripes), 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="fond-gripes:gripes:$(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: fond-gripes:gripes:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json