Drive Load-Bearing Icing from your own code
The one metered task, redteam, is available over HTTP. It takes 1 gingerbread house plan - the page's lines (the fixed numbers, the house line with every size and choice and the verdict, and 6 ranked assumption lines, each with its risk, score and evidence) and the person's note - and returns the verdict, a headline, 1 attack per assumption at medium or high risk (the case for it, the attack, the cheapest test, a give-up line and the fix) and a closing line.
The division of labour first: the page does the geometry, a fixed rule rates each assumption and decides the verdict, and the score fixes the rank; the model makes the case, attacks and writes the tests. After a run the page checks that the verdict follows the rule, that every risky assumption is attacked once in rank order with its own risk, that every number in the prose is in the plan, that the glue named matches the plan, that word limits hold, and that no ids leak into the prose.
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":"load-bearing-icing"}), 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, plan, notes) 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: an attack is metered and needs a personal sign-in. |
| 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, the input and the verdicts
Every run sends task: redteam (the one task is redteam); a missing or different one is still answered as redteam. The 6 assumptions are walls-stand, roof-stays, glue-holds, roof-load, survives-display and pieces-fit, each rated low, medium or high by the rules on the page and scored impact x likelihood x cheap to test. The verdict, checked in order: a high risk on walls, roof or glue, redesign; any medium or high risk, test-first; otherwise build-it.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always redteam. |
plan | yes | The page's lines: a house numbers: line, a line such as house | Candy tower | front 12 cm wide | sides 10 cm deep | walls 18 cm tall | peak 12 cm above the walls | dough 5 mm | glue: soft frosting | walls set 1 h before the roof | roof candy: heavy | on show 5 days in a steamy room | verdict: redesign, then 6 lines such as rank 1 | glue-holds | risk: high | impact 3 x likelihood 3 x cheap to test 3 = 27 | soft frosting, walls set 1 h before the roof goes on. |
notes | yes | The person's note, or none. |
retry_note | no | Only on the page's single reformat retry. |
The reply is one JSON object:
{"task": "redteam", "verdict": "redesign", "headline": "...", "attacks": [{"assumption": "glue-holds", "risk": "high", "steelman": "...", "attack": "...", "cheapest_test": "...", "give_up_if": "If ...", "fix": "..."}, ...], "closing": "..."}
| Field | What it holds |
|---|---|
verdict | Copied from the house line. |
headline | The verdict, how many assumptions are under attack and the top-ranked one, at most 20 words. |
attacks | 1 per assumption at medium or high risk, in rank order (empty for build-it): assumption and risk (copied), steelman (at most 16 words), attack (22), cheapest_test (24), give_up_if (starts with "If", 20) and fix (22). |
closing | The first test to run, or that the plan is ready to build, at most 22 words. |
The page then checks the reply (IcingKit.check) and lists any disagreement under "The page's check".
Worked examples - every verdict
These are the four free samples on the page: the exact input the page sends and the reply that replays for free.
Sideboard cottage - test first, a creeping roof and a long display
Input:
{
"task": "redteam",
"plan": "house numbers: tray 38 x 28 cm, 3 or more trays is a long bake | each roof panel overhangs the walls by 1 cm | walls wobble past 22 times taller than thick and fall past 30 | roofs creep from 40 degrees and slide from 50 | a roof slant past 40 times the dough thickness sags | royal icing needs 2 h before the roof | soft frosting never sets hard | heavy candy needs 6 mm dough | gingerbread softens past 3 days in a steamy room, past 7 in an ordinary room, past 28 in a dry room | a test runs 1 h | hold each joint 10 min\nhouse | Sideboard cottage | front 18 cm wide | sides 12 cm deep | walls 10 cm tall | peak 6 cm above the walls | dough 6 mm | glue: royal icing | walls set 3 h before the roof | roof candy: light | on show 10 days in an ordinary room | verdict: test-first\nrank 1 | roof-stays | risk: medium | impact 3 x likelihood 2 x cheap to test 2 = 12 | roof at 45 degrees, each panel 8.5 cm from eave to peak, 14 times the dough thickness\nrank 2 | glue-holds | risk: low | impact 3 x likelihood 1 x cheap to test 3 = 9 | royal icing, walls set 3 h before the roof goes on\nrank 3 | walls-stand | risk: low | impact 3 x likelihood 1 x cheap to test 2 = 6 | 10 cm walls on 6 mm dough stand 17 times taller than thick\nrank 4 | roof-load | risk: low | impact 2 x likelihood 1 x cheap to test 2 = 4 | light candy on the roof, 6 mm dough\nrank 5 | survives-display | risk: medium | impact 2 x likelihood 2 x cheap to test 1 = 4 | 10 days on show in an ordinary room\nrank 6 | pieces-fit | risk: low | impact 1 x likelihood 1 x cheap to test 3 = 3 | 6 panels on 2 trays",
"notes": "my first gingerbread house; it will stand on the sideboard by the front door"
}
Reply:
{
"task": "redteam",
"verdict": "test-first",
"headline": "Test first: 2 parts of the plan are under attack, starting with whether the roof stays put.",
"attacks": [
{
"assumption": "roof-stays",
"risk": "medium",
"steelman": "The panels are only 14 times the dough thickness, short of the slant that causes sagging.",
"attack": "At 45 degrees, the roof is past the 40-degree point where roofs start to creep, nearing the 50-degree slide.",
"cheapest_test": "Before baking the roof, glue a spare panel with royal icing onto a board tilted to 45 degrees and watch for 1 h.",
"give_up_if": "If the panel slips on the tilted board within the hour, lower the roof peak below 40 degrees.",
"fix": "Lower the peak below 40 degrees, and hold each roof panel in place for 10 min before letting it stand propped."
},
{
"assumption": "survives-display",
"risk": "medium",
"steelman": "Nothing about the sideboard by the front door says why it would last that long.",
"attack": "On show 10 days in an ordinary room, past the 7-day point where ordinary rooms start to soften.",
"cheapest_test": "Before assembling, leave a spare scrap of gingerbread on the sideboard by the front door and check it the next morning.",
"give_up_if": "If the scrap softens by morning, choose a drier spot or fewer days on show instead.",
"fix": "Move the house to a drier spot, or shorten how many days it stays on show."
}
],
"closing": "Try gluing a spare panel onto a board tilted to 45 degrees for 1 h before you bake the real roof."
}
Tall tower by the kettle - redesign, 5 assumptions under attack
Input:
{
"task": "redteam",
"plan": "house numbers: tray 38 x 28 cm, 3 or more trays is a long bake | each roof panel overhangs the walls by 1 cm | walls wobble past 22 times taller than thick and fall past 30 | roofs creep from 40 degrees and slide from 50 | a roof slant past 40 times the dough thickness sags | royal icing needs 2 h before the roof | soft frosting never sets hard | heavy candy needs 6 mm dough | gingerbread softens past 3 days in a steamy room, past 7 in an ordinary room, past 28 in a dry room | a test runs 1 h | hold each joint 10 min\nhouse | Candy tower | front 12 cm wide | sides 10 cm deep | walls 18 cm tall | peak 12 cm above the walls | dough 5 mm | glue: soft frosting | walls set 1 h before the roof | roof candy: heavy | on show 5 days in a steamy room | verdict: redesign\nrank 1 | glue-holds | risk: high | impact 3 x likelihood 3 x cheap to test 3 = 27 | soft frosting, walls set 1 h before the roof goes on\nrank 2 | walls-stand | risk: high | impact 3 x likelihood 3 x cheap to test 2 = 18 | 18 cm walls on 5 mm dough stand 36 times taller than thick\nrank 3 | roof-stays | risk: high | impact 3 x likelihood 3 x cheap to test 2 = 18 | roof at 67 degrees, each panel 13 cm from eave to peak, 26 times the dough thickness\nrank 4 | roof-load | risk: high | impact 2 x likelihood 3 x cheap to test 2 = 12 | heavy candy on the roof, 5 mm dough\nrank 5 | survives-display | risk: high | impact 2 x likelihood 3 x cheap to test 1 = 6 | 5 days on show in a steamy room\nrank 6 | pieces-fit | risk: low | impact 1 x likelihood 1 x cheap to test 3 = 3 | 6 panels on 2 trays",
"notes": "a tall narrow tower with a steep roof, on the counter next to the kettle"
}
Reply:
{
"task": "redteam",
"verdict": "redesign",
"headline": "This plan needs a redesign: 5 assumptions are under attack, starting with the glue.",
"attacks": [
{
"assumption": "glue-holds",
"risk": "high",
"steelman": "The plan states no fact yet that the frosting would hold firm.",
"attack": "Soft frosting never sets hard, yet the walls get only 1 h before the roof goes on.",
"cheapest_test": "Glue 2 scraps together with the frosting, stand the joint on its end, and check firmness after 1 h.",
"give_up_if": "If the frosting stays soft or breaks apart, switch the glue to royal icing instead.",
"fix": "Replace the soft frosting with royal icing so the glue actually sets before the roof goes on."
},
{
"assumption": "walls-stand",
"risk": "high",
"steelman": "Nothing in the plan yet gives a reason the walls would stay upright.",
"attack": "At 36 times taller than thick, these walls pass the 22 wobble point and the 30 fall point already.",
"cheapest_test": "Cut a spare strip at 5 mm thick and 18 cm tall, stand it on edge, and watch for 1 h.",
"give_up_if": "If the strip wobbles or buckles, use thicker dough or lower the walls under 22 times taller than thick.",
"fix": "Make the dough thicker or build the walls lower, keeping them under 22 times taller than thick."
},
{
"assumption": "roof-stays",
"risk": "high",
"steelman": "No fact in the plan yet explains why the roof would stay put.",
"attack": "At 67 degrees, this roof is already past the 40 degree creep point and the 50 degree slide point.",
"cheapest_test": "Glue a spare roof panel to a board tilted to the roof's angle, then wait 1 h.",
"give_up_if": "If the panel slides on the glue, lower the peak under 40 degrees and prop each panel until set.",
"fix": "Lower the peak under 40 degrees, hold each roof panel 10 min, and prop it until set."
},
{
"assumption": "roof-load",
"risk": "high",
"steelman": "The plan offers no stated fact yet that the roof could carry this candy.",
"attack": "Heavy candy sits on only 5 mm roof dough, short of the 6 mm this candy needs.",
"cheapest_test": "Set the planned heavy candy on a spare panel tilted to the roof angle and wait 1 h.",
"give_up_if": "If the panel sags under the candy, move heavy pieces to the board or walls, or thicken to 6 mm.",
"fix": "Put fewer heavy pieces on the roof, set the rest on the board or walls, or use 6 mm dough."
},
{
"assumption": "survives-display",
"risk": "high",
"steelman": "Nothing stated yet explains why the house would survive the display days.",
"attack": "5 days on show in a steamy room already passes the 3 day steamy room softening point.",
"cheapest_test": "Leave a spare scrap of gingerbread where the house will stand, and check it the next morning.",
"give_up_if": "If the scrap softens by morning, move the house away from the steam or cut the display days.",
"fix": "Move the house to a drier spot away from the steam, or cut back the display days."
}
],
"closing": "Start by joining 2 dough scraps with the frosting and checking their firmness after 1 h."
}
Small starter house - build it, no attacks
Input:
{
"task": "redteam",
"plan": "house numbers: tray 38 x 28 cm, 3 or more trays is a long bake | each roof panel overhangs the walls by 1 cm | walls wobble past 22 times taller than thick and fall past 30 | roofs creep from 40 degrees and slide from 50 | a roof slant past 40 times the dough thickness sags | royal icing needs 2 h before the roof | soft frosting never sets hard | heavy candy needs 6 mm dough | gingerbread softens past 3 days in a steamy room, past 7 in an ordinary room, past 28 in a dry room | a test runs 1 h | hold each joint 10 min\nhouse | Little starter | front 14 cm wide | sides 10 cm deep | walls 8 cm tall | peak 4 cm above the walls | dough 6 mm | glue: royal icing | walls set 4 h before the roof | roof candy: none | on show 5 days in a dry room | verdict: build-it\nrank 1 | glue-holds | risk: low | impact 3 x likelihood 1 x cheap to test 3 = 9 | royal icing, walls set 4 h before the roof goes on\nrank 2 | walls-stand | risk: low | impact 3 x likelihood 1 x cheap to test 2 = 6 | 8 cm walls on 6 mm dough stand 13 times taller than thick\nrank 3 | roof-stays | risk: low | impact 3 x likelihood 1 x cheap to test 2 = 6 | roof at 39 degrees, each panel 6.5 cm from eave to peak, 11 times the dough thickness\nrank 4 | roof-load | risk: low | impact 2 x likelihood 1 x cheap to test 2 = 4 | no candy on the roof, 6 mm dough\nrank 5 | pieces-fit | risk: low | impact 1 x likelihood 1 x cheap to test 3 = 3 | 6 panels on 2 trays\nrank 6 | survives-display | risk: low | impact 2 x likelihood 1 x cheap to test 1 = 2 | 5 days on show in a dry room",
"notes": "none"
}
Reply:
{
"task": "redteam",
"verdict": "build-it",
"headline": "Build it - no part of the plan is at medium or high risk.",
"attacks": [],
"closing": "The plan is ready to build whenever you want to start your gingerbread house."
}
Big party centrepiece - test first, roof panels too big for the tray
Input:
{
"task": "redteam",
"plan": "house numbers: tray 38 x 28 cm, 3 or more trays is a long bake | each roof panel overhangs the walls by 1 cm | walls wobble past 22 times taller than thick and fall past 30 | roofs creep from 40 degrees and slide from 50 | a roof slant past 40 times the dough thickness sags | royal icing needs 2 h before the roof | soft frosting never sets hard | heavy candy needs 6 mm dough | gingerbread softens past 3 days in a steamy room, past 7 in an ordinary room, past 28 in a dry room | a test runs 1 h | hold each joint 10 min\nhouse | Party hall | front 38 cm wide | sides 24 cm deep | walls 14 cm tall | peak 10 cm above the walls | dough 8 mm | glue: melted sugar | walls set 0 h before the roof | roof candy: light | on show 21 days in a dry room | verdict: test-first\nrank 1 | roof-stays | risk: medium | impact 3 x likelihood 2 x cheap to test 2 = 12 | roof at 40 degrees, each panel 15.5 cm from eave to peak, 19 times the dough thickness\nrank 2 | glue-holds | risk: low | impact 3 x likelihood 1 x cheap to test 3 = 9 | melted sugar, walls set 0 h before the roof goes on\nrank 3 | pieces-fit | risk: high | impact 1 x likelihood 3 x cheap to test 3 = 9 | roof left, roof right bigger than the tray (40 x 16.5 cm)\nrank 4 | walls-stand | risk: low | impact 3 x likelihood 1 x cheap to test 2 = 6 | 14 cm walls on 8 mm dough stand 18 times taller than thick\nrank 5 | roof-load | risk: low | impact 2 x likelihood 1 x cheap to test 2 = 4 | light candy on the roof, 8 mm dough\nrank 6 | survives-display | risk: low | impact 2 x likelihood 1 x cheap to test 1 = 2 | 21 days on show in a dry room",
"notes": "a wide centrepiece for the party table, joints glued with melted sugar"
}
Reply:
{
"task": "redteam",
"verdict": "test-first",
"headline": "Test first: 2 parts of the plan are at risk, starting with whether the roof stays put.",
"attacks": [
{
"assumption": "roof-stays",
"risk": "medium",
"steelman": "The roof carries light candy, not heavy weight pulling panels down the slope.",
"attack": "At 40 degrees the roof reaches the creep point; its 19 times thickness stays under the 40 sag limit.",
"cheapest_test": "Glue a spare roof panel onto a board tilted to 40 degrees with the melted sugar, and check it after 1 h.",
"give_up_if": "If a spare panel slides or sags on the tilted board, lower the peak under 40 degrees instead.",
"fix": "Lower the peak to under 40 degrees, and hold and prop each roof panel for 10 min before adding it."
},
{
"assumption": "pieces-fit",
"risk": "high",
"steelman": "The hall's front panel is exactly 38 cm wide, matching the tray's own width precisely.",
"attack": "The roof panels measure 40 by 16.5 cm, overshooting the 38 by 28 cm tray on the long side.",
"cheapest_test": "Cut every panel from paper at its planned size and lay all of them together on the 38 by 28 cm tray.",
"give_up_if": "If the paper roof panels do not fit the tray together, bake them in parts or build smaller.",
"fix": "Bake the oversized roof panels in smaller parts and join them with the melted sugar, or build a smaller hall."
}
],
"closing": "Before anything else, glue a spare roof panel onto a board tilted to 40 degrees and watch it for 1 h."
}
Build the input yourself
Load the engine in node (global.window = global; require("./icingkit.js")), then:
const K = IcingKit;
const built = K.buildInput(K.form({ name: "Cottage", width: 18, depth: 12, height: 10, rise: 6, thick: 6,
glue: "royal-icing", wait: 3, candy: "light", days: 10, room: "normal", notes: "on the sideboard" }));
built.input.plan; // the page's lines: numbers, the house line, 6 ranked assumptions
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// built.facts.verdict // the verdict the page's rule gives
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply, "Cottage") // the attack 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://load-bearing-icing.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://load-bearing-icing.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"]
// load-bearing-icing.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://load-bearing-icing.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://load-bearing-icing.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
}
// WhatWasThat.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 WhatWasThat {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://load-bearing-icing.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://load-bearing-icing.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://load-bearing-icing.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://load-bearing-icing.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":"load-bearing-icing"}' | 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": "load-bearing-icing"})["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: "load-bearing-icing" })).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": "load-bearing-icing"}, 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", "load-bearing-icing"), 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: "load-bearing-icing" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "load-bearing-icing"])["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 = "load-bearing-icing" }))!["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": "redteam", "plan": "house numbers: tray 38 x 28 cm | ...
house | Candy tower | front 12 cm wide | ... | verdict: redesign
rank 1 | glue-holds | risk: high | ...", "notes": "a tall narrow tower ..."}
# - 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": "redteam", "plan": "house numbers: tray 38 x 28 cm | ...
house | Candy tower | front 12 cm wide | ... | verdict: redesign
rank 1 | glue-holds | risk: high | ...", "notes": "a tall narrow tower ..."}
# - 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: "redteam", plan, notes} (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 load-bearing-icing:<task>:<inputHash>:a<attempt> (the one task is redteam), 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="load-bearing-icing:redteam:$(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 = "load-bearing-icing:" + 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("verdict"))
import { createHash } from "node:crypto";
const key = `load-bearing-icing:${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.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "load-bearing-icing:" + 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 = "load-bearing-icing:" + 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 = "load-bearing-icing:#{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["verdict"]].inspect
$key = "load-bearing-icing:" . $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["verdict"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"load-bearing-icing:{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: load-bearing-icing:redteam:$(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": "redteam", "plan": "house numbers: tray 38 x 28 cm | ...
house | Candy tower | front 12 cm wide | ... | verdict: redesign
rank 1 | glue-holds | risk: high | ...", "notes": "a tall narrow tower ..."}
# - 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": "load-bearing-icing:redteam:" + 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": `load-bearing-icing:redteam:${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", "load-bearing-icing:redteam:"+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", "load-bearing-icing:redteam:" + 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" => "load-bearing-icing:redteam:#{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: load-bearing-icing:redteam:" . 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", $"load-bearing-icing:redteam:{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);