Drive Twelve Each from your own code
The one metered step - the ruling on a 12-12-12 declutter board - is available over HTTP. A natural use is a home-organizing app, a moving checklist or a chat assistant that already holds a list of what is in a room: your code sends the board, and gets back a bin and a one-sentence reason for every card the person has not placed themselves, plus second looks at any irreplaceable thing they were about to throw out.
The split first: the page reads the lines; the model rules; the page checks it. The facts on each line (quantities, last used, broken, whose it is, where it belongs, sentimental, a record, a safety item), the page's suggested bin and the counts come from binkit.js, which runs in node as well as in the browser. Its plan function then checks the reply before anything moves: every asked card must be ruled, a sentimental item or a record is never tossed or donated (the page sends it to the hold box instead), a safety item is never removed unless its line says it is broken or expired, a second look may only move the person's own placements toward keep or the hold box, and every cited id must be on the board.
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":"twelve-each"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or task 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: the ruling 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 comes first
Every run sends task. There is one task, rule. It returns title, summary; rulings [{id, bin, why, guard, rehome_to, hold_days, cite}]; second_looks [{id, to, why, cite}]; first_fifteen (three to five steps); dont_buy (one sentence); caveats.
| bin | Means |
|---|---|
toss | Rubbish, recycling or e-waste: broken, worn out, expired, dried out, a single glove. |
donate | Usable by someone else: never used, outgrown, does not fit, surplus duplicates, unused for a year or more. |
rehome | Put back where it lives, or give back to its owner; rehome_to names the room or the person. |
keep | In use, load-bearing, or a safety item. |
hold | The hold box - a staged removal: box it, date it, and let it go if nobody missed it within hold_days (14 to 90). |
guard is sentimental, record, safety, no-evidence or empty. Cites are the board's ids: cards I1…I60 and N-goal. A second look's to is only ever hold or keep.
The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (BinKit.toWire does this). Only task is required by the schema.
Input fields
| Field | Type | Notes |
|---|---|---|
task | string | Always rule. |
zone | string | The zone's name (60 characters). |
today | string | YYYY-MM-DD; hold-box dates count from it. |
items | string (JSON) | [{id, text, qty, facts[], bin, by, page_why?, where?, owner?}], up to 60 cards. facts: worn, never, stale, recent, dup, nofit, maybe, sentimental, record, safety, elsewhere, borrowed. by: page, none, you, ruling. |
ask | string (JSON) | The ids to rule on: every card with by page or none. |
counts | string (JSON) | {toss, donate, rehome, keep, hold, stuck, target} in quantities; target is 12. |
goal | string | Optional, the person's words; past 600 characters the middle is cut around a marker. |
retry_note | string | Only on a reformat retry. |
The reply
The job's output.output is a string holding one JSON object. Parse it with BinKit.parseReply, then normalizeRule, then check it with plan(reply, state, today), which returns {moves, notes, after}: the moves the page would make with its guards enforced, the notes (ok, info, warn, bad) and the counts after applying.
Worked examples
Replies produced against the app's prompt for the sample zones (made-up items; lists shortened; the full replies ship in example-result.js). The page replays each of them free from its sample buttons. Inputs are shown with the long fields folded.
task: rule - Hall closet before winter
Input:
{
"task": "rule",
"zone": "Hall closet",
"today": "2026-10-19",
"items": "[{\"id\":\"I1\",\"text\":\"Winter coat - wore it last winter\",\"qty\":1,\"facts\":[\"recent\"],\"bin\":\"keep\",\"by\":\"page\",\"page_why\":\"In use.\"},{\"id\":\"I2\",\"text\":\"Kids' snow boots x2, outgrown\",\"qty\":2,\"facts\":[\"nofit\"],\"bin\":\"donate\",\"by\":\"page\",\"page_why\":\"It does not fit or suit you - it may suit someone else.\"},\"... 14 more cards\"]",
"ask": "[\"I1\",\"I2\",\"I3\",\"I4\",\"I5\",\"I6\",\"I7\",\"I8\",\"I9\",\"I10\",\"I11\",\"I12\",\"I13\",\"I14\",\"I15\",\"I16\"]",
"counts": "{\"toss\":9,\"donate\":23,\"rehome\":2,\"keep\":3,\"hold\":1,\"stuck\":3,\"target\":12}",
"goal": "Winter is coming and I need room for the coats and boots we actually wear. Two kids, one closet."
}
Reply:
{
"task": "rule",
"title": "Hall Closet: Room For The Coats You Wear",
"summary": "After these rulings, toss sits at 9 of the 12 target (short by 3) and rehome at just 2 of 12 (short by 10, since almost nothing here actually belongs somewhere else) - donate has already passed its mark at 23, mostly the fourteen tote bags and five spare umbrellas. The biggest win is clearing those duplicates along with the outgrown kids' boots and the too-small ski jacket, freeing real space for the coats and boots you still wear. Grandma's scarf, last year's costume, the extension cord, and the unmatched vacuum attachments go in the hold box; the fire extinguisher and winter car kit stay put as safety items.",
"rulings": [
{
"id": "I1",
"bin": "keep",
"why": "You wore this coat last winter, so it is in use.",
"guard": "",
"rehome_to": "",
"hold_days": null,
"cite": [
"I1"
]
},
{
"id": "I2",
"bin": "donate",
"why": "The kids have outgrown these, so a child who still fits them can use them.",
"guard": "",
"rehome_to": "",
"hold_days": null,
"cite": [
"I2",
"N-goal"
]
},
{
"id": "I3",
"bin": "toss",
"why": "Seven single gloves with no pairs left - nobody else can use them either.",
"guard": "",
"rehome_to": "",
"hold_days": null,
"cite": [
"I3"
]
},
"..."
],
"second_looks": [],
"first_fifteen": [
"Bag the 7 single gloves and 2 broken umbrellas for the trash.",
"Box the 14 tote bags, 5 extra umbrellas, outgrown snow boots, too-small ski jacket, and old ice skates for donation.",
"Carry the rain boots to the mudroom and the casserole dish back to the neighbor.",
"..."
],
"dont_buy": "Clearing nine toss items and twenty-three donations should free enough space in the one closet - no new bins or shelving needed.",
"caveats": [
"This rules only on what the lines say, not the physical condition of each item.",
"The vacuum attachments are held rather than donated in case the matching vacuum turns up - check before deciding."
]
}
task: rule - Garage shelf
Input:
{
"task": "rule",
"zone": "Garage shelf",
"today": "2026-10-19",
"items": "[{\"id\":\"I1\",\"text\":\"Paint cans x4 - dried out\",\"qty\":4,\"facts\":[\"worn\",\"dup\"],\"bin\":\"toss\",\"by\":\"page\",\"page_why\":\"Broken or worn out - nobody else can use it either.\"},{\"id\":\"I2\",\"text\":\"Drill - borrowed from my neighbor Sam\",\"qty\":1,\"facts\":[\"borrowed\"],\"bin\":\"rehome\",\"by\":\"page\",\"page_why\":\"It belongs to your neighbor - give it back.\",\"owner\":\"neighbor\"},\"... 12 more cards\"]",
"ask": "[\"I1\",\"I2\",\"I3\",\"I4\",\"I5\",\"I6\",\"I7\",\"I8\",\"I9\",\"I10\",\"I11\",\"I12\",\"I13\",\"I14\"]",
"counts": "{\"toss\":11,\"donate\":6,\"rehome\":2,\"keep\":2,\"hold\":0,\"stuck\":4,\"target\":12}"
}
Reply:
{
"task": "rule",
"title": "Garage Shelf: Toss Hits Twelve, Rehome Lags",
"summary": "Toss already hits its twelve - the dried-out paint, the cracked cooler, the two dead light strands, and the router's box and manual add up to 12 - the biggest win on this shelf. Donate sits at 6 of 12, six short, and rehome at 2 of 12, ten short, so there's more sorting ahead once everything else is settled. The floor tiles, the mystery screws, and the old license plates have no evidence either way, so they go in a dated hold box instead of a guess. The first-aid kit stays guarded in keep as a safety item - replace it, then dispose of the expired one safely.",
"rulings": [
{
"id": "I1",
"bin": "toss",
"why": "Dried-out paint can't be donated or recycled as ordinary trash - it needs a hazardous-waste drop-off.",
"guard": "",
"rehome_to": "",
"hold_days": null,
"cite": [
"I1"
]
},
{
"id": "I2",
"bin": "rehome",
"why": "It's borrowed from neighbor Sam, not yours to keep - give it back.",
"guard": "",
"rehome_to": "Sam",
"hold_days": null,
"cite": [
"I2"
]
},
{
"id": "I3",
"bin": "donate",
"why": "Never used, so someone else can get the use out of it.",
"guard": "",
"rehome_to": "",
"hold_days": null,
"cite": [
"I3"
]
},
"..."
],
"second_looks": [],
"first_fifteen": [
"Bag the cracked cooler and the two dead light strands for the trash.",
"Carry the drill back to Sam.",
"Set the car wax back in the shed.",
"..."
],
"dont_buy": "Once these items clear the shelf there's space to spare, so skip buying any new bins or organizers.",
"caveats": [
"This rules only on what the lines say - check local hazardous-waste rules for the paint and e-waste rules for the lights before tossing them.",
"Keep the old first-aid kit in place until its replacement is actually in hand, not just on order."
]
}
task: rule - Memory drawer (badly sorted) - second looks
Input:
{
"task": "rule",
"zone": "Junk and memory drawer",
"today": "2026-10-19",
"items": "[{\"id\":\"I1\",\"text\":\"Grandpa's pocket watch - doesn't work\",\"qty\":1,\"facts\":[\"worn\",\"sentimental\"],\"bin\":\"toss\",\"by\":\"you\"},{\"id\":\"I2\",\"text\":\"Birth certificate copy\",\"qty\":1,\"facts\":[\"record\"],\"bin\":\"toss\",\"by\":\"you\"},\"... 11 more cards\"]",
"ask": "[\"I3\",\"I4\",\"I5\",\"I7\",\"I8\",\"I9\",\"I10\",\"I11\",\"I12\",\"I13\"]",
"counts": "{\"toss\":31,\"donate\":19,\"rehome\":0,\"keep\":2,\"hold\":71,\"stuck\":1,\"target\":12}",
"goal": "I want the drawer to close. I already threw a few things in the toss pile."
}
Reply:
{
"task": "rule",
"title": "Drawer Clears Out, Memories Wait",
"summary": "Toss ends up well past its twelve at 50 items once the instruction manuals, takeout menus, dead phones, dried-out pens and old batteries join it, while donate and rehome both sit at zero - twelve short each, since nothing here turned out to be truly donatable or belongs to another room. The biggest win is clearing all of that out without touching anything load-bearing. Sentimental keepsakes - the birthday cards, the kids' art, grandpa's watch - and both records - the birth certificate and the fridge warranty - are boxed on hold rather than removed, and the spare keys stay in keep as a safety item.",
"rulings": [
{
"id": "I3",
"bin": "hold",
"why": "These 40 birthday cards are a sentimental duplicate, so they're boxed for now - keep a few favourites and photograph the rest.",
"guard": "sentimental",
"rehome_to": "",
"hold_days": 30,
"cite": [
"I3"
]
},
{
"id": "I4",
"bin": "hold",
"why": "This warranty is for a fridge you've already sold, but it's still a record, so it's boxed until you check how long this kind of paper should be kept.",
"guard": "record",
"rehome_to": "",
"hold_days": 30,
"cite": [
"I4"
]
},
{
"id": "I5",
"bin": "toss",
"why": "Three dead phones are broken and unusable by anyone else - they go to e-waste recycling.",
"guard": "",
"rehome_to": "",
"hold_days": null,
"cite": [
"I5"
]
},
"..."
],
"second_looks": [
{
"id": "I1",
"to": "hold",
"why": "Grandpa's watch is sentimental even though it's broken - worth boxing instead of tossing it outright.",
"cite": [
"I1"
]
},
{
"id": "I2",
"to": "hold",
"why": "A birth certificate copy is a record, so it deserves a hold to check what's needed before it goes, not a straight toss.",
"cite": [
"I2"
]
}
],
"first_fifteen": [
"Bag the 3 dead phones for e-waste recycling.",
"Recycle the 9 instruction manuals and 10 takeout menus.",
"Sort the 25 pens, toss the dried-out ones, and drop the loose batteries at a hazardous-waste collection point.",
"..."
],
"dont_buy": "Once the manuals, menus, pens, phones and batteries clear out, the drawer should close on its own - no new bin or organiser needed.",
"caveats": [
"This reads only the lines you entered, not what's actually inside each hold box.",
"Check local rules before dropping off batteries, phones or paint.",
"How long to keep the fridge warranty depends on where you live - check before shredding it."
]
}
Build the input yourself
Load the script in node (global.window = {}; require("./binkit.js")), then:
const B = BinKit;
const st = B.state({
zone: "Hall closet",
list: "Umbrellas x5\nRain boots - belongs in the mudroom\nGrandma's scarf\nExtension cord, might need it",
goal: "Room for the winter coats.",
placed: {} // { "line text, lowercased": { bin: "keep", by: "you" } }
}, 2026);
const { input } = B.buildInput(st, { today: "2026-10-19" });
const wire = B.toWire(input); // POST this to /estimate and /run
// later: const reply = B.normalizeRule(B.parseReply(job.output.output));
// const { moves, notes, after } = B.plan(reply, st, "2026-10-19");
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://twelve-each.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://twelve-each.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"]
// twelve-each.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://twelve-each.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://twelve-each.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
}
// TwelveEach.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 TwelveEach {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://twelve-each.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://twelve-each.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://twelve-each.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://twelve-each.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":"twelve-each"}' | 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": "twelve-each"})["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: "twelve-each" })).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": "twelve-each"}, 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", "twelve-each"), 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: "twelve-each" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "twelve-each"])["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 = "twelve-each" }))!["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": "rule", "zone": "...", "today": "2026-10-19", "items": "[...]", "ask": "[...]", "counts": "{...}", "goal": "..."}
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": "rule", "zone": "...", "today": "2026-10-19", "items": "[...]", "ask": "[...]", "counts": "{...}", "goal": "..."}
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: "rule", zone, today, items, ask, counts, goal?}
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 twelve-each:rule:<inputHash>:a<attempt>, 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="twelve-each:rule:$(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 = "twelve-each:" + 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 lane's JSON object
print(j["charged_credits"], j.get("truncated"), len(reply.get("rulings", [])))
import { createHash } from "node:crypto";
const key = `twelve-each:${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.rulings || []).length);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "twelve-each:" + 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 = "twelve-each:" + 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 = "twelve-each:#{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["title"]].inspect
$key = "twelve-each:" . $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["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"twelve-each:{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: twelve-each:rule:$(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": "rule", "zone": "...", "today": "2026-10-19", "items": "[...]", "ask": "[...]", "counts": "{...}", "goal": "..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "twelve-each:rule:" + 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": `twelve-each:rule:${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", "twelve-each:rule:"+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", "twelve-each:rule:" + 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" => "twelve-each:rule:#{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: twelve-each:rule:" . 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", $"twelve-each:rule:{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);