← Six Stones
API

Drive Six Stones from your own code

The one metered task, cross, is available over HTTP. It takes a near bank and a far bank - two everyday things - a tone (playful, dry or kind) and the hops of a crossing between them, two to seven lines as n | from | to | why. It returns every hop with the strongest true link between its two things and the grade that link earns (solid, slippery or gap), a stone that would fix each weak hop, the counts, the verdict, the weakest hop and a crossing of the model's own on solid links only.

The division of labour first: the page numbers the hops; the model names the links and writes the words; the page derives every grade from its link, counts them, works out the verdict and the weakest hop itself, draws the river and re-checks the reply - one hop back per hop laid and copied word for word, every grade matching its link, the counts, the verdict, the weakest hop and the shortcut running bank to bank on solid links.

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":"six-stones"}), 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, tone, near, far, hops, 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: testing a crossing 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 status

Every run sends task: cross (the one task is cross); a missing or different one is still answered as cross. The status field holds one of two ids, checked in this order: declined when a bank or a stone is about violence, weapons, injury, illness, death, crime, drugs, drink or anything sexual or hateful, or is a specific real person or a named character; otherwise ok.

For ok, every hop's link is the first true kind in a fixed order - solid links made-of, part-of, kind-of, comes-from, found-in, used-for, causes, goes-with; then slippery links looks-like, same-colour, sounds-like, same-word; else none - and its grade follows from the link alone. The verdict holds one of three ids, checked in this order: fell-in when any hop is a gap, otherwise wet-feet when any hop is slippery, otherwise dry-feet.

Input (every field is a string)

FieldRequiredWhat it holds
taskyesAlways cross.
toneyesplayful, dry or kind.
nearyesThe thing the crossing starts from, up to 40 characters (cut on a word past that).
faryesThe thing it must reach, up to 40 characters.
hopsyes2 to 7 lines as n | from | to | why, numbered from 1, chaining from near to far over 1 to 6 stones. why is the person's reason, or -. Repeated stones are dropped and pipes in the text become slashes.
page_factsyesOne key | ... per line: hop_count, stone_count, harm_words, capitalised_words and cross_rule.
clip_notenoWhat the page left out, if anything.
retry_notenoOnly on the page's one reformat retry, after an unusable reply.

Reply (one JSON object)

{"task": "cross", "status": "ok", "reason": "", "crossing_label": "zebra to piano",
 "hops": [{"hop": 1, "from": "a zebra", "to": "a zebra crossing", "link": "same-word", "grade": "slippery", "why": "...", "fix_stone": "..."},
          {"hop": 2, ...}, {"hop": 3, ...}],
 "hop_count": 3, "solid_count": 1, "slippery_count": 2, "gap_count": 0,
 "verdict": "wet-feet", "verdict_reason": "...", "weakest_hop": 1,
 "shortcut": [{"from": "a zebra", "to": "...", "link": "part-of", "why": "..."}, ...],
 "shortcut_note": "...", "closing": "..."}
FieldWhat it holds
status, reasonThe status by the order above; for declined, at most 30 words saying why (empty for ok).
crossing_label2 to 8 lowercase words naming the crossing (empty when declined).
hopsOne per input hop, in order ([] when declined): hop, from and to copied from the input, link and grade as above, why (at most 20 words, saying plainly whether the person's reason holds) and fix_stone (empty for a solid hop; otherwise one everyday thing of 1 to 4 words that would turn the hop into two solid ones).
hop_count, solid_count, slippery_count, gap_countThe number of hops and how many have each grade.
verdict, verdict_reasonThe verdict id by the order above, and at most 30 words on how the grades lead to it, naming the weakest hop.
weakest_hopThe first gap; if none, the first slippery hop; if every hop is solid, 0.
shortcut, shortcut_noteThe model's own crossing from near to far, 1 to 7 steps, each with from, to, a solid link and a why of at most 16 words, chained end to end; and at most 25 words comparing it with the person's.
closingA light line of at most 16 words.

The page then checks the reply (StoneKit.check): one hop back for each hop sent, copied word for word, every grade equal to the grade its link earns, the four counts, the verdict and the weakest hop by the rule, a suggested stone that never repeats an end of its hop, a shortcut that starts at the near bank, ends at the far bank, never breaks and uses solid links only, no he or she, no internal field names in prose, and the word limits. The river always follows the links. A declined reply should carry no hops; a tested crossing that the page's harm words flag is pointed out.

Worked examples

These are the four free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover every outcome - a cup of tea to the ocean over water, rain and a cloud (dry feet), a zebra to a piano by its name and its stripes (wet feet), a pillow to a volcano by way of a parrot (fell in), and a pickpocket to a stolen wallet, which is about a crime, so it is declined.

A cup of tea to the ocean - dry feet

Input:

{
 "task": "cross",
 "tone": "kind",
 "near": "a cup of tea",
 "far": "the ocean",
 "hops": "1 | a cup of tea | water | tea is mostly water\n2 | water | rain | -\n3 | rain | a cloud | rain falls from clouds\n4 | a cloud | the ocean | -",
 "page_facts": "hop_count | 4\nstone_count | 3\nharm_words | none\ncapitalised_words | none\ncross_rule | one entry per hop in order; link = first true kind (solid list, then slippery list, else none); grade from link; fell-in if any gap, else wet-feet if any slippery, else dry-feet; weakest = first gap, else first slippery, else 0"
}

Reply:

{
 "task": "cross",
 "status": "ok",
 "reason": "",
 "crossing_label": "from a cup of tea to the ocean",
 "hops": [
  {
   "hop": 1,
   "from": "a cup of tea",
   "to": "water",
   "link": "made-of",
   "grade": "solid",
   "why": "Yes, that holds: a cup of tea is made mostly of water.",
   "fix_stone": ""
  },
  {
   "hop": 2,
   "from": "water",
   "to": "rain",
   "link": "made-of",
   "grade": "solid",
   "why": "Rain is just water falling from the sky.",
   "fix_stone": ""
  },
  {
   "hop": 3,
   "from": "rain",
   "to": "a cloud",
   "link": "comes-from",
   "grade": "solid",
   "why": "Yes, that holds: rain does come from a cloud.",
   "fix_stone": ""
  },
  {
   "hop": 4,
   "from": "a cloud",
   "to": "the ocean",
   "link": "comes-from",
   "grade": "solid",
   "why": "A cloud forms from water evaporating off the ocean.",
   "fix_stone": ""
  }
 ],
 "hop_count": 4,
 "solid_count": 4,
 "slippery_count": 0,
 "gap_count": 0,
 "verdict": "dry-feet",
 "verdict_reason": "Every hop holds as a real link, so this crossing stays dry all the way across.",
 "weakest_hop": 0,
 "shortcut": [
  {
   "from": "a cup of tea",
   "to": "water",
   "link": "made-of",
   "why": "A cup of tea is made largely of water."
  },
  {
   "from": "water",
   "to": "the ocean",
   "link": "made-of",
   "why": "The ocean is simply made of water, nothing more."
  }
 ],
 "shortcut_note": "My crossing needs only two hops, both made-of links, while yours took four hops through rain and cloud.",
 "closing": "Dry feet the whole way across, nicely laid indeed."
}

A zebra to a piano - wet feet

Input:

{
 "task": "cross",
 "tone": "playful",
 "near": "a zebra",
 "far": "a piano",
 "hops": "1 | a zebra | a zebra crossing | same name\n2 | a zebra crossing | piano keys | both have black and white stripes\n3 | piano keys | a piano | the keys are on the piano",
 "page_facts": "hop_count | 3\nstone_count | 2\nharm_words | none\ncapitalised_words | none\ncross_rule | one entry per hop in order; link = first true kind (solid list, then slippery list, else none); grade from link; fell-in if any gap, else wet-feet if any slippery, else dry-feet; weakest = first gap, else first slippery, else 0"
}

Reply:

{
 "task": "cross",
 "status": "ok",
 "reason": "",
 "crossing_label": "from a zebra to a piano",
 "hops": [
  {
   "hop": 1,
   "from": "a zebra",
   "to": "a zebra crossing",
   "link": "looks-like",
   "grade": "slippery",
   "why": "You are right that they share a word, but it is really the stripy look that links them, just skin-deep.",
   "fix_stone": "stripes"
  },
  {
   "hop": 2,
   "from": "a zebra crossing",
   "to": "piano keys",
   "link": "same-colour",
   "grade": "slippery",
   "why": "Piano keys are not actually striped, but you are right that both wear black and white.",
   "fix_stone": "paint"
  },
  {
   "hop": 3,
   "from": "piano keys",
   "to": "a piano",
   "link": "part-of",
   "grade": "solid",
   "why": "Your reason holds, piano keys really are part of a piano, nothing sneaky going on.",
   "fix_stone": ""
  }
 ],
 "hop_count": 3,
 "solid_count": 1,
 "slippery_count": 2,
 "gap_count": 0,
 "verdict": "wet-feet",
 "verdict_reason": "Two hops lean on looks and colour instead of real links, leaving you a little damp; hop 1 slips first.",
 "weakest_hop": 1,
 "shortcut": [
  {
   "from": "a zebra",
   "to": "a savanna",
   "link": "found-in",
   "why": "Zebras live on a savanna."
  },
  {
   "from": "a savanna",
   "to": "an elephant",
   "link": "found-in",
   "why": "Elephants roam that savanna too."
  },
  {
   "from": "an elephant",
   "to": "ivory",
   "link": "comes-from",
   "why": "Ivory comes from an elephant's tusks."
  },
  {
   "from": "ivory",
   "to": "piano keys",
   "link": "made-of",
   "why": "Piano keys were once made from ivory."
  },
  {
   "from": "piano keys",
   "to": "a piano",
   "link": "part-of",
   "why": "Those keys simply belong to a piano."
  }
 ],
 "shortcut_note": "My crossing takes five hops through real animal and material links, longer than your three but solid all the way; not the only path.",
 "closing": "Soggy socks and all, this zebra-to-piano crossing still holds together nicely."
}

A pillow to a volcano - fell in

Input:

{
 "task": "cross",
 "tone": "dry",
 "near": "a pillow",
 "far": "a volcano",
 "hops": "1 | a pillow | feathers | pillows are stuffed with them\n2 | feathers | a parrot | parrots have feathers\n3 | a parrot | a volcano | parrots live inside volcanoes",
 "page_facts": "hop_count | 3\nstone_count | 2\nharm_words | none\ncapitalised_words | none\ncross_rule | one entry per hop in order; link = first true kind (solid list, then slippery list, else none); grade from link; fell-in if any gap, else wet-feet if any slippery, else dry-feet; weakest = first gap, else first slippery, else 0"
}

Reply:

{
 "task": "cross",
 "status": "ok",
 "reason": "",
 "crossing_label": "pillow to feathers to parrot to volcano",
 "hops": [
  {
   "hop": 1,
   "from": "a pillow",
   "to": "feathers",
   "link": "made-of",
   "grade": "solid",
   "why": "A pillow can be stuffed with feathers, so your reason holds.",
   "fix_stone": ""
  },
  {
   "hop": 2,
   "from": "feathers",
   "to": "a parrot",
   "link": "part-of",
   "grade": "solid",
   "why": "Feathers are indeed part of a parrot, so that holds too.",
   "fix_stone": ""
  },
  {
   "hop": 3,
   "from": "a parrot",
   "to": "a volcano",
   "link": "none",
   "grade": "gap",
   "why": "Parrots do not live inside volcanoes, and nothing else links a parrot to a volcano.",
   "fix_stone": "an island"
  }
 ],
 "hop_count": 3,
 "solid_count": 2,
 "slippery_count": 0,
 "gap_count": 1,
 "verdict": "fell-in",
 "verdict_reason": "Two hops hold as solid links, but hop 3 has no real link between a parrot and a volcano, so the crossing falls in.",
 "weakest_hop": 3,
 "shortcut": [
  {
   "from": "a pillow",
   "to": "feathers",
   "link": "made-of",
   "why": "A pillow can be made of feathers."
  },
  {
   "from": "feathers",
   "to": "a parrot",
   "link": "part-of",
   "why": "Feathers are part of a parrot's body."
  },
  {
   "from": "a parrot",
   "to": "an island",
   "link": "found-in",
   "why": "A parrot can live on an island."
  },
  {
   "from": "an island",
   "to": "a volcano",
   "link": "causes",
   "why": "A volcano can cause an island to form."
  }
 ],
 "shortcut_note": "My crossing uses more hops but stays solid throughout, moving through a parrot and an island before reaching the volcano.",
 "closing": "That last hop lands you in the river, not on the far bank."
}

A pickpocket to a stolen wallet - declined

Input:

{
 "task": "cross",
 "tone": "playful",
 "near": "a pickpocket",
 "far": "a stolen wallet",
 "hops": "1 | a pickpocket | a crowded train | -\n2 | a crowded train | a stolen wallet | -",
 "page_facts": "hop_count | 2\nstone_count | 1\nharm_words | stolen, pickpocket\ncapitalised_words | none\ncross_rule | one entry per hop in order; link = first true kind (solid list, then slippery list, else none); grade from link; fell-in if any gap, else wet-feet if any slippery, else dry-feet; weakest = first gap, else first slippery, else 0"
}

Reply:

{
 "task": "cross",
 "status": "declined",
 "reason": "This app links everyday things, not people, characters or anything about harm. Try a crossing between two ordinary things instead.",
 "crossing_label": "",
 "hops": [],
 "hop_count": 0,
 "solid_count": 0,
 "slippery_count": 0,
 "gap_count": 0,
 "verdict": "",
 "verdict_reason": "",
 "weakest_hop": 0,
 "shortcut": [],
 "shortcut_note": "",
 "closing": "Thanks for stopping by the river."
}

Build the input yourself

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

const K = StoneKit, D = RiverDraw;
const built = K.buildInput({ tone: "playful", near: "a zebra", far: "a piano",
  stones: "a zebra crossing - same name
piano keys - both have black and white stripes" });
built.parsed.hops;               // [{n: 1, from: "a zebra", to: "a zebra crossing", why: "same name"}, ...]
built.prescan.harm;              // [] - harm, crime, drink or drug words in the banks, stones or reasons
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.crossing(K.gradesOf(reply.hops))                   // counts, verdict and weakest hop, worked by the page
// D.svg({ near: "a zebra", far: "a piano", stones: ["a zebra crossing", "piano keys"], grades: K.gradesOf(reply.hops) })   // the river as SVG
// K.md(built, reply)                                   // the tested crossing 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://six-stones.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":"six-stones"}' | 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": "cross", "tone": "playful", "near": "...", "far": "...", "hops": "1 | near | stone | why\n2 | stone | far | -", "page_facts": "hop_count | 2\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 six-stones:<task>:<inputHash>:a<attempt> (the one task is cross), 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="six-stones:cross:$(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: six-stones:cross:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json