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.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, tone, near, far, hops, page_facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: testing a crossing is metered and needs a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. 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)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always cross. |
tone | yes | playful, dry or kind. |
near | yes | The thing the crossing starts from, up to 40 characters (cut on a word past that). |
far | yes | The thing it must reach, up to 40 characters. |
hops | yes | 2 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_facts | yes | One key | ... per line: hop_count, stone_count, harm_words, capitalised_words and cross_rule. |
clip_note | no | What the page left out, if anything. |
retry_note | no | Only 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": "..."}
| Field | What it holds |
|---|---|
status, reason | The status by the order above; for declined, at most 30 words saying why (empty for ok). |
crossing_label | 2 to 8 lowercase words naming the crossing (empty when declined). |
hops | One 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_count | The number of hops and how many have each grade. |
verdict, verdict_reason | The verdict id by the order above, and at most 30 words on how the grades lead to it, naming the weakest hop. |
weakest_hop | The first gap; if none, the first slippery hop; if every hop is solid, 0. |
shortcut, shortcut_note | The 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. |
closing | A 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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://six-stones.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// six-stones.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://six-stones.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://six-stones.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// SixStones.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class SixStones {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://six-stones.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://six-stones.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://six-stones.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://six-stones.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
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')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "six-stones"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "six-stones" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "six-stones"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "six-stones"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "six-stones" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "six-stones"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "six-stones" }))!["token"]!.GetValue<string>();
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}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["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}'
inp = json.load(open("input.json")) # {"task": "cross", "tone": "playful", "near": "...", "far": "...", "hops": "1 | near | stone | why\n2 | stone | far | -", "page_facts": "hop_count | 2\n..."} - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "cross", tone, near, far, hops, page_facts} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
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}'
import hashlib, time
key = "six-stones:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("status"))
import { createHash } from "node:crypto";
const key = `six-stones:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.status);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "six-stones:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "six-stones:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "six-stones:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["status"]].inspect
$key = "six-stones:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["status"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"six-stones:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
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
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "cross", "tone": "playful", "near": "...", "far": "...", "hops": "1 | near | stone | why\n2 | stone | far | -", "page_facts": "hop_count | 2\n..."} - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "six-stones:cross:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `six-stones:cross:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "six-stones:cross:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "six-stones:cross:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "six-stones:cross:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: six-stones:cross:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"six-stones:cross:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);