Drive Under Glass from your own code
The one metered task, crew, is available over HTTP. It takes three to eight people (one per line, a name, a dash and a few words about them) and a stay length, and returns a verdict (seal the dome, seal after one swap, or back to the airlock), one post per person - a kind of crewmate, a dome job and a one-line reason - plus the glue, where it will rub, a four-stage crew log, three rituals, one warning sign and the habit to keep or the change to make.
The division of labour first: the page owns the keyword guess, the job order and the verdict rule; the model owns the casting; the page re-checks the reply. All three come from glasskit.js, which you can run yourself (see Build the input yourself). Under Glass is a game: the kinds of crewmate are a playful reading of a few words, not an assessment of anyone.
Get a token on the token page (step 2 below also shows how to mint a guest token in code).
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":"under-glass"}), 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, stay, crew, guess) 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: casting a crew 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, the kinds, the jobs and the verdict
Every run sends task: crew; a missing or different one is still answered as crew. Each person gets one archetype: visionary (ideas, plans, art), executor (rotas, lists, budgets, follow-through), analyst (questions, puzzles, checking), connector (warmth, birthdays, hosting), facilitator (calming, settling disagreements) or specialist (a named trade or training). Connector and facilitator count as glue.
Each person gets a different job, one per biome: farmer (farm plot), air (air and water keeper, marsh), tender (biome tender, rainforest), fixer (machine deck), cook (galley), medic (sick bay), morale (morale officer, commons), chronicler (lookout). Jobs go in two passes: first to whoever's line names the work, then by each kind's preferred list.
The verdict follows the reply's own archetypes, first match wins: airlock if there are two different kinds or fewer; otherwise swap if nobody is a connector or facilitator, a crew of four or more has fewer than four kinds, or one kind is more than half the crew; otherwise seal.
The run input
Every value is a string (objects are never sent). app.js builds it with GlassKit.buildInput(form).input.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always crew. |
stay | yes | a fortnight, six months or two years. The page times the crew log to it. |
crew | yes | Numbered lines, 1. <name> - <what they are like>. Three to eight are sent; a description over 160 characters keeps its start. |
guess | yes | The page's keyword guess, one line per person: <n> | <archetype> | <job>. Only a guess - the model may cast differently. |
clip_note | no | What the page shortened, if anything. |
retry_note | no | Sent only on the one automatic reformat retry. |
The reply
The reply is one JSON object in job.output.output. The page parses it with GlassKit.parseReply and GlassKit.normalize (a cut-off stream is closed by GlassKit.closeJson and whatever parsed is shown).
{"task": "crew", "verdict": "seal | swap | airlock", "verdict_reason": "...", "title": "...",
"crew": [{"n": 1, "name": "...", "archetype": "visionary | executor | analyst | connector | facilitator | specialist",
"job": "farmer | air | tender | fixer | cook | medic | morale | chronicler", "why": "..."}],
"glue": "a crew name, or an empty string", "fault_line": "...",
"log": [{"stage": "forming", "moment": "..."}, {"stage": "storming", "moment": "..."},
{"stage": "norming", "moment": "..."}, {"stage": "performing", "moment": "..."}],
"rituals": ["...", "...", "..."],
"watch": {"sign": "groupthink | one_voice | avoidance | unclear_roles | paralysis | blame | burnout", "why": "..."},
"fix": "..."}
| Field | What it holds |
|---|---|
crew | Exactly one entry per numbered person, same numbers and order, name copied exactly. why at most 22 words, from the person's own line. |
glue | The connector or facilitator who holds the crew together, or an empty string if there is none. |
fault_line | Two people whose habits will most likely rub, at most 30 words, good-humoured. |
log | Four moments of dome life, forming to performing, at most 25 words each, with no day or month counts (the page labels them for the stay). |
rituals | Three crew rituals - a daily, a weekly and a milestone - at most 20 words each. |
watch | One warning sign and why this crew is prone to it. |
fix | For seal, the habit that keeps the crew working; for swap, the gap and one concrete change; for airlock, the missing kinds and who to add. |
The page then checks the reply against its own facts (GlassKit.check): one post per person, names as written, every job held once, the verdict against the reply's own casting, the glue on the crew list and cast as a connector or facilitator, the log in its four stages with no counted days, three rituals, a known warning sign, the keyword guess where it differs, no he or she for anyone, and no internal field name in the prose.
Worked examples, one per verdict
These are the three free samples on the page: the exact input and a stand-in model's reply, unedited.
The flat on Quillfern Lane - seal
Input:
{
"task": "crew",
"stay": "two years",
"crew": "1. Priya - always has a new idea for the flat, painted the mural in the hall\n2. Tom - keeps the cleaning rota, never misses bin day\n3. Wen - asks how do we know about everything, does the crossword in pen\n4. Dee - remembers everyone's birthday and throws the parties\n5. Sam - calm, the one who sorts out the thermostat arguments\n6. Joe - trained as a nurse, works nights",
"guess": "1 | visionary | chronicler\n2 | executor | air\n3 | analyst | fixer\n4 | connector | morale\n5 | facilitator | cook\n6 | specialist | medic"
}
Reply:
{
"task": "crew",
"verdict": "seal",
"verdict_reason": "Six different kinds of crewmate among six people, and Dee is the glue who keeps everyone close.",
"title": "Two Years Under the Painted Dome",
"crew": [
{
"n": 1,
"name": "Priya",
"archetype": "visionary",
"job": "chronicler",
"why": "Always dreaming up new ideas and painting the mural in the hall points to the lookout who keeps the crew's record."
},
{
"n": 2,
"name": "Tom",
"archetype": "executor",
"job": "air",
"why": "Keeping the cleaning rota and never missing bin day shows the organised habit that matches tending the water and air."
},
{
"n": 3,
"name": "Wen",
"archetype": "analyst",
"job": "fixer",
"why": "Asking how do we know and doing the crossword in pen shows a questioning mind that suits the machine deck."
},
{
"n": 4,
"name": "Dee",
"archetype": "connector",
"job": "morale",
"why": "Remembering everyone's birthday and throwing the parties shows warmth for people, matching the parties and games of the commons."
},
{
"n": 5,
"name": "Sam",
"archetype": "facilitator",
"job": "cook",
"why": "Staying calm and sorting out the thermostat arguments shows a settling touch, and a steady peacemaker suits the galley."
},
{
"n": 6,
"name": "Joe",
"archetype": "specialist",
"job": "medic",
"why": "Training as a nurse and working nights names the exact trade that fits the sick bay."
}
],
"glue": "Dee",
"fault_line": "Tom's rota and bin day habit may clash with Priya always wanting a new idea for the dome, routine against change.",
"log": [
{
"stage": "forming",
"moment": "Priya starts the first page of the crew's record while Tom walks the marsh explaining the water duties to everyone."
},
{
"stage": "storming",
"moment": "Tom insists every change go in the rota exactly as written, but Priya wants the record to read like a story, voices rising briefly."
},
{
"stage": "norming",
"moment": "Wen now checks the machine deck readings before Tom logs them in the rota, and the two trust each other's work."
},
{
"stage": "performing",
"moment": "Dee throws a commons celebration with a meal from the galley, and the machine deck, water and air checks all run without a hitch."
}
],
"rituals": [
"Every morning Tom reads out the water and air rota while Joe checks on anyone who worked nights.",
"Once a week everyone gathers for a crossword Wen sets using clues from dome life.",
"To mark a milestone, Priya paints a new mural panel in the lookout's record for everyone to see."
],
"watch": {
"sign": "burnout",
"why": "Joe is the only one covering the sick bay and already works nights, so covering it alone could wear Joe down over time."
},
"fix": "Checking in on Joe after night shifts and taking turns watching the sick bay when needed will keep this crew's balance intact."
}
The Sunday repair club - swap
Input:
{
"task": "crew",
"stay": "six months",
"crew": "1. Rosa - reads the instructions twice and keeps the budget spreadsheet\n2. Ken - grows tomatoes and chillies on a tiny balcony\n3. Lou - full of plans for the next big trip\n4. Ade - fixes bikes and anything with a motor",
"guess": "1 | executor | air\n2 | specialist | farmer\n3 | visionary | chronicler\n4 | specialist | fixer"
}
Reply:
{
"task": "crew",
"verdict": "swap",
"verdict_reason": "Four crewmates split across three kinds of worker, and none of them is naturally warm or diplomatic enough to be the crew's glue.",
"title": "The Seedling, Spreadsheet and Someday-Trip Crew",
"crew": [
{
"n": 1,
"name": "Rosa",
"archetype": "executor",
"job": "air",
"why": "Reading the instructions twice and keeping the budget spreadsheet is exactly what tracking the marsh's levels needs."
},
{
"n": 2,
"name": "Ken",
"archetype": "specialist",
"job": "farmer",
"why": "Growing tomatoes and chillies on a tiny balcony is exactly the kind of growing the farm plot needs."
},
{
"n": 3,
"name": "Lou",
"archetype": "visionary",
"job": "chronicler",
"why": "Lou is full of plans for the next big trip, and that kind of imagination suits the lookout's record."
},
{
"n": 4,
"name": "Ade",
"archetype": "specialist",
"job": "fixer",
"why": "Fixing bikes and anything with a motor is exactly the trade the machine deck needs."
}
],
"glue": "",
"fault_line": "Rosa's twice-checked budget spreadsheet and Lou's plans for the next big trip may clash gently over what the crew can spend.",
"log": [
{
"stage": "forming",
"moment": "Rosa posts the budget spreadsheet in the commons while Ken checks on the tomato and chilli seedlings in the farm plot."
},
{
"stage": "storming",
"moment": "Rosa wants every expense logged in the spreadsheet while Lou keeps dreaming up extras for the next big trip, so they compromise."
},
{
"stage": "norming",
"moment": "Ade starts fixing the irrigation pump the moment Ken flags a dripping hose, and the farm plot runs smoothly again."
},
{
"stage": "performing",
"moment": "Lou notes down the first ripe chillies and the balanced budget sheet in the crew's record, pinned up for everyone to see."
}
],
"rituals": [
"A daily walk-through of the machine deck and the tomato and chilli plot to catch small problems early.",
"A weekly reading of the budget spreadsheet together, with everyone adding a line about what mattered most.",
"When the first ripe chillies are picked, the whole crew gathers to mark it in the crew's record."
],
"watch": {
"sign": "avoidance",
"why": "With no one whose role is to smooth disagreements, Rosa's budget focus and Lou's big trip plans could easily go unspoken."
},
"fix": "No one here is naturally warm or diplomatic; before sealing, have one crewmate agree to check that every disagreement gets properly heard."
}
The sketchbook club - airlock
Input:
{
"task": "crew",
"stay": "a fortnight",
"crew": "1. Max - always pitching a new idea\n2. Ida - dreams up big plans and starts ten projects at once\n3. Leo - brainstorms for fun, gets bored by details\n4. Bo - sketches wild inventions in the margins",
"guess": "1 | visionary | tender\n2 | visionary | farmer\n3 | visionary | air\n4 | visionary | chronicler"
}
Reply:
{
"task": "crew",
"verdict": "airlock",
"verdict_reason": "All four people turned out to be the same kind of dreamer, with not even a second kind of crewmate aboard.",
"title": "Four Dreamers, One Glass Dome",
"crew": [
{
"n": 1,
"name": "Max",
"archetype": "visionary",
"job": "tender",
"why": "Max is always pitching a new idea, and that same kind of imagination suits the rainforest."
},
{
"n": 2,
"name": "Ida",
"archetype": "visionary",
"job": "farmer",
"why": "Ida dreams up big plans and starts ten projects at once, and that same kind of imagination suits the farm plot."
},
{
"n": 3,
"name": "Leo",
"archetype": "visionary",
"job": "air",
"why": "Leo brainstorms for fun and gets bored by details, and that same kind of imagination suits the marsh."
},
{
"n": 4,
"name": "Bo",
"archetype": "visionary",
"job": "chronicler",
"why": "Bo sketches wild inventions in the margins, and that same sketching fits the lookout who keeps the record."
}
],
"glue": "",
"fault_line": "Max always pitching new ideas and Ida starting ten projects at once will likely clash over whose plan the dome actually tries first.",
"log": [
{
"stage": "forming",
"moment": "Leo brainstorms names for the marsh equipment just for fun, while Bo sketches the new water lines in the margins of the log."
},
{
"stage": "storming",
"moment": "Max pitches a new plan for the rainforest just as Ida starts another project on the farm plot, and both insist theirs should come first."
},
{
"stage": "norming",
"moment": "Max and Ida agree to try one rainforest idea and one farm project at a time, taking turns pitching whose plan runs next."
},
{
"stage": "performing",
"moment": "Leo's marsh brainstorms become routine fixes for the water lines, and Bo's margin sketches become the crew's record of what was tried."
}
],
"rituals": [
"Each morning, everyone pitches one fresh idea for the dome before heading off to the farm plot, marsh or rainforest.",
"Once a week, Bo pins the best margin sketch to the lookout post for the whole crew to see.",
"When the rainforest fills with Max's ideas and Ida's projects, the crew celebrates with a toast in the commons."
],
"watch": {
"sign": "groupthink",
"why": "With everyone the same kind of idea-chaser and no one who questions or checks the plan, the crew could agree too quickly."
},
"fix": "This crew is all big-idea pitchers; add someone organised who finishes what others start, and someone warm who keeps the group steady."
}
Build the input yourself
Load the script in node (global.window = global; require("./glasskit.js")), then:
const K = GlassKit;
const form = K.form({ stay: "half",
crew: "Mo - calm, the one who keeps the peace\nAsh - grows all the veg on the allotment\nKit - asks why about everything\nLin - keeps the club budget" });
K.crewOf(form); // one crewmate per line, with the page's archetype and job guess
K.verdictFor(K.crewOf(form)); // {verdict: "seal", why: [...]} - the page's rule over its guesses
const built = K.buildInput(form);
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply) // the crew 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://under-glass.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://under-glass.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"]
// under-glass.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://under-glass.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://under-glass.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
}
// UnderGlass.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 UnderGlass {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://under-glass.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://under-glass.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://under-glass.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://under-glass.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":"under-glass"}' | 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": "under-glass"})["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: "under-glass" })).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": "under-glass"}, 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", "under-glass"), 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: "under-glass" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "under-glass"])["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 = "under-glass" }))!["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": "crew", "stay": "six months", "crew": "1. Rosa - reads the instructions twice and keeps the budget spreadsheet\n2. Ken - grows tomatoes and chillies on a tiny balcony\n3. Lou - full of plans for the next big trip\n4. Ade - fixes bikes and anything with a motor", "guess": "1 | executor | air\n2 | specialist | farmer\n3 | visionary | chronicler\n4 | specialist | fixer"}
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": "crew", "stay": "six months", "crew": "1. Rosa - reads the instructions twice and keeps the budget spreadsheet\n2. Ken - grows tomatoes and chillies on a tiny balcony\n3. Lou - full of plans for the next big trip\n4. Ade - fixes bikes and anything with a motor", "guess": "1 | executor | air\n2 | specialist | farmer\n3 | visionary | chronicler\n4 | specialist | fixer"}
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: "crew", stay, crew, guess, clip_note} (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 under-glass:<task>:<inputHash>:a<attempt> (the one task is crew), 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="under-glass:crew:$(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 = "under-glass:" + 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("title"))
import { createHash } from "node:crypto";
const key = `under-glass:${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.title);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "under-glass:" + 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 = "under-glass:" + 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 = "under-glass:#{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 = "under-glass:" . $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 = $"under-glass:{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: under-glass:crew:$(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": "crew", "stay": "six months", "crew": "1. Rosa - reads the instructions twice and keeps the budget spreadsheet\n2. Ken - grows tomatoes and chillies on a tiny balcony\n3. Lou - full of plans for the next big trip\n4. Ade - fixes bikes and anything with a motor", "guess": "1 | executor | air\n2 | specialist | farmer\n3 | visionary | chronicler\n4 | specialist | fixer"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "under-glass:crew:" + 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": `under-glass:crew:${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", "under-glass:crew:"+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", "under-glass:crew:" + 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" => "under-glass:crew:#{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: under-glass:crew:" . 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", $"under-glass:crew:{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);