← Coal or Coin
API

Drive Coal or Coin from your own code

The one metered task, weigh, is available over HTTP. It takes 1 to 6 of your own deeds as the page's ledger lines - each deed's words, kind or naughty, small, medium or big, whether a naughty deed is made good, its coins or coal and its status - plus the totals, the verdict, the deed to mend first and the biggest kind deed. It returns a fair ledger line per deed, a way to make good each deed still carrying coal, what went well, the first amends step and a closing.

The division of labour first: the page counts the coins and coal and a fixed rule decides the verdict, each status, the deed to mend first and the biggest kind deed; the model only writes the words. After a run the page checks that the reply copied every one of those and kept to the house rules.

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":"coal-or-coin"}), 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, ledger) 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 written ledger 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: weigh (the one task is weigh); a missing or different one is still answered as weigh. A kind deed earns 1, 2 or 3 coins for small, medium or big; a naughty deed earns 1, 2 or 3 coal, cleared when made good and halved (rounded up) when partly made good. The verdict, first match wins: no coal - gold-coin; more coal than coins - lump-of-coal; otherwise - orange-and-nuts.

FieldRequiredWhat it holds
taskyesAlways weigh.
ledgeryesThe page's lines: a house rules: line, a totals: line (coins, coal, verdict, mend first, biggest kind deed), then 1 line per deed such as deed 2 | forgot my sister's birthday entirely | naughty | medium | made good: partly | coal: 1 | status: half-mended.
retry_notenoOnly on the page's single reformat retry.

Statuses are coin (kind), mended (made good), half-mended (partly made good) and coal (not yet made good). The deed to mend first carries the most coal, the earliest on a tie; the biggest kind deed is the heaviest kind deed, the earliest on a tie.

The reply is one JSON object:

{"task": "weigh", "verdict": "orange-and-nuts", "headline": "...", "deeds": [{"deed": "1", "status": "coin", "entry": "...", "make_good": ""}], "went_well": "...", "mend_first": {"deed": "2", "step": "..."}, "closing": "..."}
FieldWhat it holds
verdictCopied from the totals line.
headlineThe verdict in plain words with the coins and coal, at most 16 words.
deeds1 per deed in ledger order: deed (its number), status (copied), entry (a fair ledger line, at most 18 words) and make_good (a way to make it good for coal and half-mended, else empty).
went_wellAbout the biggest kind deed, at most 22 words.
mend_firstdeed (copied from the totals line, or empty) and step (agreeing with that deed's make_good).
closingOne light sentence that agrees with the verdict, at most 20 words.

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

Worked examples - every verdict

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

A tidy year, slips made good - a gold coin

Input:

{
 "task": "weigh",
 "ledger": "house rules: a kind deed earns 1, 2 or 3 coins for small, medium or big | a naughty deed earns 1, 2 or 3 coal for small, medium or big; made good: yes = no coal, partly = half the coal, rounded up, no = all of it | verdict: gold coin when no coal is left, lump of coal when the coal outweighs the coins, otherwise an orange and nuts\ntotals: 5 coins | 0 coal | verdict: gold-coin | mend first: none | biggest kind deed: deed 2\ndeed 1 | shovelled the snow off the neighbour's path before they were up | kind | medium | coins: 2 | status: coin\ndeed 2 | taught my dad to video-call the grandkids | kind | big | coins: 3 | status: coin\ndeed 3 | ate the last slice of the office birthday cake without asking | naughty | small | made good: yes | coal: 0 | status: mended\ndeed 4 | returned a library book 3 months late | naughty | small | made good: yes | coal: 0 | status: mended"
}

Reply:

{
 "task": "weigh",
 "verdict": "gold-coin",
 "headline": "Gold coin: 5 coins, no coal left.",
 "deeds": [
  {
   "deed": "1",
   "status": "coin",
   "entry": "Gave the neighbour a clear path by shovelling their snow before they were up.",
   "make_good": ""
  },
  {
   "deed": "2",
   "status": "coin",
   "entry": "Gave your dad the skill to video-call the grandkids.",
   "make_good": ""
  },
  {
   "deed": "3",
   "status": "mended",
   "entry": "Ate the last slice of the office birthday cake without asking - now made good.",
   "make_good": ""
  },
  {
   "deed": "4",
   "status": "mended",
   "entry": "Returned a library book 3 months late - now made good.",
   "make_good": ""
  }
 ],
 "went_well": "Teaching your dad to video-call the grandkids gave a new skill worth keeping up.",
 "mend_first": {
  "deed": "",
  "step": ""
 },
 "closing": "A gold coin year - keep teaching your dad those video-calls with the grandkids going."
}

A mixed year - an orange and nuts, partly made good, a tie for the deed to mend first

Input:

{
 "task": "weigh",
 "ledger": "house rules: a kind deed earns 1, 2 or 3 coins for small, medium or big | a naughty deed earns 1, 2 or 3 coal for small, medium or big; made good: yes = no coal, partly = half the coal, rounded up, no = all of it | verdict: gold coin when no coal is left, lump of coal when the coal outweighs the coins, otherwise an orange and nuts\ntotals: 4 coins | 2 coal | verdict: orange-and-nuts | mend first: deed 2 | biggest kind deed: deed 1\ndeed 1 | drove my friend to the airport at 5 am | kind | big | coins: 3 | status: coin\ndeed 2 | forgot my sister's birthday entirely | naughty | medium | made good: partly | coal: 1 | status: half-mended\ndeed 3 | left a passive-aggressive note on the shared fridge | naughty | small | made good: no | coal: 1 | status: coal\ndeed 4 | kept the office plant alive all summer | kind | small | coins: 1 | status: coin"
}

Reply:

{
 "task": "weigh",
 "verdict": "orange-and-nuts",
 "headline": "Orange and nuts: 4 coins and 2 coal this year.",
 "deeds": [
  {
   "deed": "1",
   "status": "coin",
   "entry": "You drove your friend to the airport at 5 am - a big, early favor.",
   "make_good": ""
  },
  {
   "deed": "2",
   "status": "half-mended",
   "entry": "You forgot your sister's birthday entirely - partly made good, with some still owed.",
   "make_good": "Finish making it good to your sister."
  },
  {
   "deed": "3",
   "status": "coal",
   "entry": "You left a passive-aggressive note on the shared fridge.",
   "make_good": "Take the note down from the shared fridge."
  },
  {
   "deed": "4",
   "status": "coin",
   "entry": "You kept the office plant alive all summer.",
   "make_good": ""
  }
 ],
 "went_well": "Driving your friend to the airport at 5 am is worth keeping up.",
 "mend_first": {
  "deed": "2",
  "step": "First, finish making it good to your sister."
 },
 "closing": "An orange and nuts year - time to finish making good on forgetting your sister's birthday."
}

A sooty year - a lump of coal

Input:

{
 "task": "weigh",
 "ledger": "house rules: a kind deed earns 1, 2 or 3 coins for small, medium or big | a naughty deed earns 1, 2 or 3 coal for small, medium or big; made good: yes = no coal, partly = half the coal, rounded up, no = all of it | verdict: gold coin when no coal is left, lump of coal when the coal outweighs the coins, otherwise an orange and nuts\ntotals: 1 coins | 5 coal | verdict: lump-of-coal | mend first: deed 1 | biggest kind deed: deed 4\ndeed 1 | borrowed my flatmate's bike and returned it with a flat tyre | naughty | medium | made good: no | coal: 2 | status: coal\ndeed 2 | said I loved a knitted jumper from my friend, then never wore it | naughty | small | made good: no | coal: 1 | status: coal\ndeed 3 | skipped my turn cleaning the bathroom 4 weeks running | naughty | big | made good: partly | coal: 2 | status: half-mended\ndeed 4 | held the lift for a stranger with a pram | kind | small | coins: 1 | status: coin"
}

Reply:

{
 "task": "weigh",
 "verdict": "lump-of-coal",
 "headline": "Lump of coal this year: 1 coin against 5 coal.",
 "deeds": [
  {
   "deed": "1",
   "status": "coal",
   "entry": "You borrowed your flatmate's bike and returned it with a flat tyre.",
   "make_good": "Repair your flatmate's flat tyre."
  },
  {
   "deed": "2",
   "status": "coal",
   "entry": "You said you loved your friend's knitted jumper, then never wore it.",
   "make_good": "Wear your friend's knitted jumper."
  },
  {
   "deed": "3",
   "status": "half-mended",
   "entry": "You skipped your turn cleaning the bathroom 4 weeks running - partly made good, with some still owed.",
   "make_good": "Finish making it good by taking your turn cleaning the bathroom."
  },
  {
   "deed": "4",
   "status": "coin",
   "entry": "You held the lift for a stranger with a pram.",
   "make_good": ""
  }
 ],
 "went_well": "Holding the lift for a stranger with a pram was a small kindness - keep it up.",
 "mend_first": {
  "deed": "1",
  "step": "Start by repairing your flatmate's flat tyre."
 },
 "closing": "The bike's flat tyre is where to start putting things right."
}

Coins and coal level - still an orange and nuts

Input:

{
 "task": "weigh",
 "ledger": "house rules: a kind deed earns 1, 2 or 3 coins for small, medium or big | a naughty deed earns 1, 2 or 3 coal for small, medium or big; made good: yes = no coal, partly = half the coal, rounded up, no = all of it | verdict: gold coin when no coal is left, lump of coal when the coal outweighs the coins, otherwise an orange and nuts\ntotals: 2 coins | 2 coal | verdict: orange-and-nuts | mend first: deed 2 | biggest kind deed: deed 1\ndeed 1 | sat with a nervous new starter at lunch all week | kind | medium | coins: 2 | status: coin\ndeed 2 | replied all to the whole company by mistake | naughty | small | made good: no | coal: 1 | status: coal\ndeed 3 | finished the oat milk and put the empty carton back | naughty | small | made good: no | coal: 1 | status: coal"
}

Reply:

{
 "task": "weigh",
 "verdict": "orange-and-nuts",
 "headline": "Orange and nuts: 2 coins and 2 coal on the ledger.",
 "deeds": [
  {
   "deed": "1",
   "status": "coin",
   "entry": "You sat with a nervous new starter at lunch all week, giving your time and company.",
   "make_good": ""
  },
  {
   "deed": "2",
   "status": "coal",
   "entry": "You replied all to the whole company by mistake.",
   "make_good": "Send an apology to the whole company for the mistaken reply."
  },
  {
   "deed": "3",
   "status": "coal",
   "entry": "You finished the oat milk and put the empty carton back.",
   "make_good": "Replace the oat milk carton you finished."
  }
 ],
 "went_well": "Sitting with a nervous new starter at lunch all week gave company worth keeping up.",
 "mend_first": {
  "deed": "2",
  "step": "First, send an apology to the whole company for the mistaken reply."
 },
 "closing": "A fair orange and nuts tally - replying all to the whole company by mistake is worth putting right."
}

Build the input yourself

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

const K = CoalKit;
const built = K.buildInput(K.form({ deeds: [
  { text: "fixed the wobbly table at the cafe", side: "kind", weight: 2 },
  { text: "forgot to water the neighbour's tomatoes", side: "naughty", weight: 1, mended: "partly" }] }));
built.input.ledger;                     // the page's lines: totals, verdict and one line per deed
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 year")        // the ledger 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://coal-or-coin.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":"coal-or-coin"}' | 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": "weigh", "ledger": "house rules: a kind deed earns 1, 2 or 3 coins ...\ntotals: 4 coins | 2 coal | verdict: orange-and-nuts | mend first: deed 2 | ...\ndeed 1 | drove my friend to the airport at 5 am | kind | big | coins: 3 | status: coin\n..."}
# - 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 coal-or-coin:<task>:<inputHash>:a<attempt> (the one task is 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="coal-or-coin: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: coal-or-coin:weigh:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json