← Bones to Reef
API

Drive Bones to Reef from your own code

The one metered task, sort-the-fall, is available over HTTP. It takes what you are winding down, when it ends, its leftovers (one per line, each with the page's stage sketch) and the people or groups who might take things. It returns a reading of the whole ending, one item per leftover (its stage, a taker, a first move and why the stage fits), a signpost note to leave where the thing used to be, and a last word.

The division of labour first: the page sorts every leftover it can by fixed word lists, and the reading follows a fixed order from the reply's own stages and takers. After a run the page checks that the reply kept every leftover in order and word for word, kept the page's stages, gave things only to takers from your list (or you, or unclaimed), reached the reading the order demands, stated the counts in the headline, never said anything was already handed over or agreed, and added no names, numbers, times or pronouns your input never had. A playful planning tool, not professional advice.

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":"bones-to-reef"}), 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, leftovers, takers and the rest of the field table) 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 sorting 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 readings

Every run sends task: sort-the-fall (the one task is sort-the-fall); a missing or different one is still answered as sort-the-fall. The four stages: 1 scavengers (days to weeks), 2 enrichers (weeks to months), 3 slow feeders (months to years), 4 reef (years). The reading, first match wins: set-a-date when ends is undated; needs-takers when unclaimed leftovers are at least half of them; reef-ready when any leftover is at stage 4; otherwise picked-clean.

FieldRequiredWhat it holds
taskyesAlways sort-the-fall.
thingyesWhat is winding down, at most 80 characters.
endsyessoon, months or undated.
leftover_countyesHow many leftover lines follow (2 to 12), as a string.
leftoversyesNumbered lines such as 1. two toolboxes [page stage: 1]; [page stage: none] when the page could not place it.
takersyesUp to 8 lines, Name: note or just a name, or (none).
retry_notenoOnly on the page's single reformat retry.

The reply is one JSON object:

{"verdict": "reef-ready", "headline": "...", "items": [{"n": 1, "leftover": "two toolboxes", "stage": 1, "taker": "Marta", "move": "...", "why": "..."}, ...], "signpost": "...", "last_word": "..."}
FieldWhat it holds
verdictThe reading id, by the order above.
headlineA plain reading with the leftover count and the unclaimed count in digits and the fullest stage (at most 26 words).
itemsOne per leftover, same order: n, leftover (exact), stage 1-4, taker (a listed name, you or unclaimed), move (at most 18 words), why (16).
signpostThe note to leave where the thing used to be (at most 45 words).
last_wordOne closing line that agrees with the reading (18).

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

Worked examples - every reading

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

The laundrette zine - no end date yet: set a date

Input:

{
 "task": "sort-the-fall",
 "thing": "the zine I photocopy for the laundrette noticeboard",
 "ends": "undated",
 "leftover_count": "5",
 "leftovers": "1. a box of 60 back issues [page stage: 1]\n2. the long-arm stapler [page stage: none]\n3. a contacts list of 14 contributors [page stage: 2]\n4. my layout template [page stage: 3]\n5. the habit of writing every month [page stage: 4]",
 "takers": "Rosa: runs the community centre library shelf\nDev: wants to start a comic"
}

Reply:

{
 "verdict": "set-a-date",
 "headline": "Winding down the zine for the laundrette noticeboard leaves 5 leftovers, 1 unclaimed, mostly at stage 1, scavengers.",
 "items": [
  {
   "n": 1,
   "leftover": "a box of 60 back issues",
   "stage": 1,
   "taker": "Rosa",
   "move": "Ask Rosa if the community centre library shelf can take the back issues.",
   "why": "A box of 60 back issues is something someone can take and use right away."
  },
  {
   "n": 2,
   "leftover": "the long-arm stapler",
   "stage": 1,
   "taker": "Dev",
   "move": "Ask Dev if the comic could use the long-arm stapler.",
   "why": "A long-arm stapler is something someone can carry off and use right away."
  },
  {
   "n": 3,
   "leftover": "a contacts list of 14 contributors",
   "stage": 2,
   "taker": "unclaimed",
   "move": "Pin a note on the laundrette noticeboard asking who would like the contacts list.",
   "why": "A contacts list of contributors is something others could keep using, not use up."
  },
  {
   "n": 4,
   "leftover": "my layout template",
   "stage": 3,
   "taker": "Dev",
   "move": "Ask Dev if the new comic could use the layout template.",
   "why": "A layout template is something that can keep helping with future issues slowly."
  },
  {
   "n": 5,
   "leftover": "the habit of writing every month",
   "stage": 4,
   "taker": "you",
   "move": "Keep writing every month, with or without the zine.",
   "why": "A habit of writing every month can keep going for years by itself."
  }
 ],
 "signpost": "This zine for the noticeboard is winding down, with no date yet. The back issues may go to Rosa's community centre library shelf, and the long-arm stapler and layout template might suit Dev's comic. Ask here if you're interested in the contacts list of contributors.",
 "last_word": "Pick an end date for the zine."
}

The language exchange - ends soon, no takers listed: needs takers

Input:

{
 "task": "sort-the-fall",
 "thing": "my weekly language-exchange meetup at the cafe",
 "ends": "soon",
 "leftover_count": "5",
 "leftovers": "1. laminated conversation cards [page stage: 1]\n2. the cafe's back-room booking on Wednesdays [page stage: 2]\n3. a group chat of 38 members [page stage: 2]\n4. a tin with £22 in it [page stage: 1]\n5. the friendships with Tomas and Ewa [page stage: 4]",
 "takers": "(none)"
}

Reply:

{
 "verdict": "needs-takers",
 "headline": "This weekly language-exchange meetup's ending leaves 5 leftovers, 4 of them unclaimed, mostly at stage 1, Scavengers.",
 "items": [
  {
   "n": 1,
   "leftover": "laminated conversation cards",
   "stage": 1,
   "taker": "unclaimed",
   "move": "Ask in the group chat whether anyone wants the conversation cards.",
   "why": "The cards are something to take and start using straight away."
  },
  {
   "n": 2,
   "leftover": "the cafe's back-room booking on Wednesdays",
   "stage": 2,
   "taker": "unclaimed",
   "move": "Ask the cafe whether someone else can keep the Wednesday booking.",
   "why": "The back-room booking is a slot that could keep being used on Wednesdays."
  },
  {
   "n": 3,
   "leftover": "a group chat of 38 members",
   "stage": 2,
   "taker": "unclaimed",
   "move": "Ask the group chat's members if someone wants to keep running it.",
   "why": "The chat's 38 members are an audience that could keep talking on their own."
  },
  {
   "n": 4,
   "leftover": "a tin with £22 in it",
   "stage": 1,
   "taker": "unclaimed",
   "move": "Ask the group chat's members openly how the £22 should be used.",
   "why": "The tin's £22 can be spent or shared right away."
  },
  {
   "n": 5,
   "leftover": "the friendships with Tomas and Ewa",
   "stage": 4,
   "taker": "you",
   "move": "Message Tomas and Ewa to keep in touch beyond the meetup.",
   "why": "Friendships with Tomas and Ewa are what will keep lasting for years."
  }
 ],
 "signpost": "This weekly language-exchange meetup at the cafe is ending soon. The conversation cards, the Wednesday back-room booking, the group chat, and the tin with £22 still need a taker - if interested, please ask.",
 "last_word": "Go find takers for the cards, the booking, the chat, and the tin."
}

The repair table - ends in a few months: reef ready

Input:

{
 "task": "sort-the-fall",
 "thing": "the repair table I run at the church hall",
 "ends": "months",
 "leftover_count": "5",
 "leftovers": "1. two toolboxes [page stage: 1]\n2. a soldering iron [page stage: 1]\n3. the mailing list of 120 people [page stage: 2]\n4. a binder of repair notes [page stage: 3]\n5. the knack for fixing lamps [page stage: 4]",
 "takers": "Marta: wants to run a repair night at the youth club\nthe church hall committee"
}

Reply:

{
 "verdict": "reef-ready",
 "headline": "5 leftovers remain from the repair table, 1 unclaimed, with stage 1, Scavengers, holding the most — ready for someone to use straight away.",
 "items": [
  {
   "n": 1,
   "leftover": "two toolboxes",
   "stage": 1,
   "taker": "Marta",
   "move": "Ask Marta whether the toolboxes suit the youth club repair night before handing them over.",
   "why": "Toolboxes are physical kit that gets used once someone takes them."
  },
  {
   "n": 2,
   "leftover": "a soldering iron",
   "stage": 1,
   "taker": "Marta",
   "move": "Ask Marta whether the soldering iron suits the youth club repair night.",
   "why": "A soldering iron is kit someone takes and uses straight away."
  },
  {
   "n": 3,
   "leftover": "the mailing list of 120 people",
   "stage": 2,
   "taker": "unclaimed",
   "move": "Ask around the church hall for someone willing to keep the list going.",
   "why": "A mailing list of 120 people is an audience a group could keep using."
  },
  {
   "n": 4,
   "leftover": "a binder of repair notes",
   "stage": 3,
   "taker": "Marta",
   "move": "Ask Marta whether the binder of repair notes would help the youth club nights.",
   "why": "A binder of repair notes is know-how that can keep helping for a long time."
  },
  {
   "n": 5,
   "leftover": "the knack for fixing lamps",
   "stage": 4,
   "taker": "you",
   "move": "Keep fixing lamps for people who ask, so the knack stays alive.",
   "why": "The knack for fixing lamps is a skill that stays with you."
  }
 ],
 "signpost": "The repair table that used to run here is ending. The two toolboxes, a soldering iron and a binder of repair notes may go to Marta for a youth club repair night. If you'd like to help keep the mailing list going, ask here.",
 "last_word": "What's ending still leaves something lasting behind — the knack for fixing lamps stays with you."
}

The postcard stall - ends soon, every leftover has a taker: picked clean

Input:

{
 "task": "sort-the-fall",
 "thing": "my vintage postcard stall at the Saturday market",
 "ends": "soon",
 "leftover_count": "4",
 "leftovers": "1. about 900 postcards in shoeboxes [page stage: 1]\n2. the gazebo [page stage: 1]\n3. the card reader [page stage: 1]\n4. my pitch at the market [page stage: 2]",
 "takers": "Joe: sells old maps on the next pitch, asked about my gazebo and card reader\nLena: collects seaside postcards\nthe market manager: hands out the pitches"
}

Reply:

{
 "verdict": "picked-clean",
 "headline": "Your postcard stall's ending leaves 4 leftovers, 0 unclaimed, with stage 1 Scavengers holding the most.",
 "items": [
  {
   "n": 1,
   "leftover": "about 900 postcards in shoeboxes",
   "stage": 1,
   "taker": "Lena",
   "move": "Ask Lena whether they want the postcards before you box them up.",
   "why": "About 900 postcards in shoeboxes are things someone can take and use right away."
  },
  {
   "n": 2,
   "leftover": "the gazebo",
   "stage": 1,
   "taker": "Joe",
   "move": "Ask Joe whether they want the gazebo before you take it down.",
   "why": "The gazebo is a structure you can hand over all at once."
  },
  {
   "n": 3,
   "leftover": "the card reader",
   "stage": 1,
   "taker": "Joe",
   "move": "Ask Joe whether they want the card reader before you pass it on.",
   "why": "The card reader is equipment someone can take and start using right away."
  },
  {
   "n": 4,
   "leftover": "my pitch at the market",
   "stage": 2,
   "taker": "the market manager",
   "move": "Ask the market manager about handing on your pitch at the market.",
   "why": "Your pitch at the market is a spot someone else could step into and keep running."
  }
 ],
 "signpost": "This vintage postcard stall is ending soon. Lena may take the postcards, Joe may take the gazebo and card reader, and the market manager may take on the pitch. Ask before anything moves.",
 "last_word": "Everything from your stall has somewhere to go; now it's just asking each one."
}

Build the input yourself

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

const K = ReefKit;
const built = K.buildInput(K.form({ thing: "the book swap shelf at work", ends: "soon", leftovers: "a crate of paperbacks\nthe sign-up sheet", takers: "Ali: runs the staff room" }));
built.facts.items                       // each leftover with the page's stage (0 = still sinking)
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, "the book swap")  // the sorting 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://bones-to-reef.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":"bones-to-reef"}' | 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-the-fall", "thing": "the repair table I run at the church hall", "ends": "months", "leftover_count": "5", "leftovers": "1. two toolboxes [page stage: 1]\n2. ...", "takers": "Marta: wants to run a repair night at the youth club\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 bones-to-reef:<task>:<inputHash>:a<attempt> (the one task is sort-the-fall), 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="bones-to-reef:sort-the-fall:$(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: bones-to-reef:sort-the-fall:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json