Drive Squawk Sheet from your own code
The one metered task, squawk, is available over HTTP. It takes a paper plane's test throws - one line per throw in the pilot's own words, each already read as a squawk and filed under an ATA 100 maintenance chapter - plus the plane and the page's decisions. It returns the engineer's side of the tech log: the release status, one entry per throw (the defect as logged, one corrective action and why it works), the first fix, how to throw next and the release line.
The division of labour first: the page reads and counts; the model words it. The page reads each line for what the plane did (damage, dive, climb then stall, spiral, banks left, banks right, wobble, lands short, a good flight, or not clear), files it under a chapter, picks the standard trim fix, sets the release status by a fixed rule, chooses the first fix and the work order, notes whether the plane turned both ways and whether it was breezy, and sends them as facts. The model never changes those. After the reply, the page lists anything that does not match. A paper plane is a toy; not professional advice.
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":"squawk-sheet"}), 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, plane, reports, facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the engineer's entries are metered and need 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 rules
Every run sends task: squawk (the one task is squawk); a missing or different one is still answered as squawk. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always squawk. |
plane | yes | design | paper: ... | where: ... | tail mark: ... | throw it: ... - the design (Classic dart, Wide-wing glider, Nose-lock glider, Long-wing glider or My own design), the paper (A4 printer paper, US Letter printer paper, Half a sheet, Thin card, A notebook page or Other paper), where (indoors, outdoors, calm or outdoors, breezy), the tail mark (up to 12 characters, or none) and the pace this design likes to be thrown at: firm for the dart, gentle for the three gliders, medium for your own design. |
reports | yes | One throw a line, in order: N. the pilot's own words | reads as: ... | chapter C. “reads as” lists what the page found, most important first (damage, dive, climb, then stall, spiral, banks left, banks right, wobble, lands short, good flight), or not clear when it found nothing it could read; C is the ATA 100 chapter. One to eight lines; a line is up to 200 characters. |
facts | yes | The page's decisions, one a line: verdict, squawks (with how many read clearly and how many not clear), damage, faults, good flights, first fix, work order, turns both ways, wind (breezy or none) and left out (report lines past the eighth). Build it with SquawkKit.buildInput. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
The chapters: 5 time limits and maintenance checks (a good flight, or not clear), 8 levelling and weighing (lands short), 27 flight controls (dive, climb then stall, banks left or right), 53 fuselage (damage), 55 stabilizers (wobble) and 57 wings (spiral, and damage when the line names a wing, tip or edge). A line is filed under the chapter of the first thing it reads as.
The verdict, checked in this order: refold when a line reads as damage (torn, soggy, crushed); else trim when a line reads as a flight fault (dive, climb then stall, spiral, banks left, banks right, wobble, lands short); else test-fly when fewer than two lines read as a good flight; else cleared.
The reply
One JSON object, no prose around it:
{"task": "squawk", "verdict": "trim", "entries": [{"n": 1, "chapter": 27, "defect": "...", "action": "...", "why": "..."}], "first_fix": "...", "next_test": "...", "sign_off": "..."}
| Field | What it holds |
|---|---|
verdict | refold, trim, test-fly or cleared - always the page's. |
entries | Exactly one object per line of reports, in the same order, each {"n": int, "chapter": int, "defect": "...", "action": "...", "why": "..."}. n is the line's number and chapter its chapter, as numbers. defect restates the line as a log entry: at most 16 words, past tense, in the pilot's own words, keeping every number with its unit and adding nothing. action is the standard fix for the first thing the line reads as, with its direction words and amounts (1 to 2 mm, one paper clip) kept exactly; a spiral that also banks one way adds that side's bank fix. why is that fix's reason in one sentence. |
first_fix | One sentence. For refold: fold a fresh plane before anything else, naming the damaged squawk. For trim: the first-fix squawk and its action, and only that one adjustment before the next throw. For test-fly and cleared: no adjustment is due. |
next_test | One or two sentences about the next throws: a level throw at the design's pace (firm, gentle or medium) and to write down what the plane does each time - with the fresh plane for refold, the same way each time when it turned both ways, and indoors or out of the wind when it was breezy. |
sign_off | One short tech-log sentence matching the verdict: not released, a fresh plane is needed (refold); not released yet, make the first fix and fly it again (trim); awaiting a test flight (test-fly); or released to service with the number of good flights as a numeral (cleared). |
The page reads the reply with ReplyKit.parseReply and ReplyKit.normalize, then re-checks it against the pilot's lines with ReplyKit.check and lists, under the page's check, a release status that differs from the page's, a throw with no entry, an extra or out-of-order entry, a chapter that differs, a defect over 16 words or with a number the line does not give, an action that bends the wrong way or the wrong side (or prescribes an adjustment where no fault was read), a number that is not in the line or the standard fix, a first fix that does not name the right squawk, a next throw at the wrong pace or without the same-way or out-of-the-wind note, a release line that does not match the status or miscounts the good flights, and any promised result, tape, pins, coins or risky place, a day or time, or a he or she the reports do not give.
Worked examples - every release status
These are the free samples on the page: the exact input the page sends for each and the engineer's entries that replay for free. Together they cover every release status - refold, trim then re-fly, test flight needed and cleared to fly.
A dart that dives - trim
Input:
{
"task": "squawk",
"plane": "Classic dart | paper: A4 printer paper | where: indoors | tail mark: FOLD-1 | throw it: firm",
"reports": "1. dived nose first into the carpet after about 2 m | reads as: dive | chapter 27\n2. threw it harder, it shot up, stalled and dropped | reads as: climb, then stall | chapter 27\n3. veered right into the bookcase | reads as: banks right | chapter 27\n4. curved left on a gentle throw | reads as: banks left | chapter 27",
"facts": "verdict: trim\nsquawks: 4 (read clearly: 4, not clear: 0)\ndamage: none\nfaults: squawk 1 dive; squawk 2 climb, then stall; squawk 3 banks right; squawk 4 banks left\ngood flights: 0\nfirst fix: squawk 1 (dive)\nwork order: squawk 1, squawk 2, squawk 3 and squawk 4\nturns both ways: yes (squawk 3 and squawk 4)\nwind: none\nleft out: none"
}
Reply:
{
"task": "squawk",
"verdict": "trim",
"entries": [
{
"n": 1,
"chapter": 27,
"defect": "Dived nose first into the carpet after about 2 m.",
"action": "Bend the back edge of both wings up a little, about 1 to 2 mm, the same on each side.",
"why": "Back edges bent up work like an up elevator: the air pushes the back down and the nose up."
},
{
"n": 2,
"chapter": 27,
"defect": "Shot up, stalled and dropped after a harder throw.",
"action": "Flatten the back edges of both wings a little; if they are already flat, add one paper clip to the nose.",
"why": "Back edges bent up too far lift the nose until the wings lose their lift; less bend, or a little weight at the front, keeps the nose from rearing."
},
{
"n": 3,
"chapter": 27,
"defect": "Veered right into the bookcase.",
"action": "Bend the back edge of the right wing down a little, or the back edge of the left wing up a little.",
"why": "A back edge bent down gives that wing more lift and one bent up gives it less, so the plane rolls back level."
},
{
"n": 4,
"chapter": 27,
"defect": "Curved left on a gentle throw.",
"action": "Bend the back edge of the left wing down a little, or the back edge of the right wing up a little.",
"why": "A back edge bent down gives that wing more lift and one bent up gives it less, so the plane rolls back level."
}
],
"first_fix": "Fix squawk 1 first: bend the back edge of both wings up a little, about 1 to 2 mm, the same on each side, and make only that one adjustment before the next throw.",
"next_test": "Throw it level, a firm throw, and write down what it does each time. Squawk 3 veered right and squawk 4 curved left, so throw it the same way each time and watch which way it turns before trimming that turn.",
"sign_off": "Not released yet; make the first fix, fly it again and log what it does."
}
A soggy wing - refold
Input:
{
"task": "squawk",
"plane": "Nose-lock glider | paper: A4 printer paper | where: outdoors, calm | tail mark: none | throw it: gentle",
"reports": "1. glided nicely across the lawn | reads as: good flight | chapter 5\n2. turned left and landed in the wet grass - now the left wing is soggy | reads as: damage, banks left | chapter 57\n3. after drying it off it wobbled and sank fast | reads as: wobble, lands short | chapter 55",
"facts": "verdict: refold\nsquawks: 3 (read clearly: 3, not clear: 0)\ndamage: squawk 2\nfaults: squawk 3 wobble\ngood flights: 1 (squawk 1)\nfirst fix: squawk 2 (damage)\nwork order: none\nturns both ways: no\nwind: none\nleft out: none"
}
Reply:
{
"task": "squawk",
"verdict": "refold",
"entries": [
{
"n": 1,
"chapter": 5,
"defect": "Glided nicely across the lawn.",
"action": "No fault found - no adjustment.",
"why": "Nothing to correct."
},
{
"n": 2,
"chapter": 57,
"defect": "Turned left and landed in the wet grass; left wing is soggy.",
"action": "Fold a fresh plane from a new sheet.",
"why": "Torn, soggy or crushed paper cannot hold its folds, so no trim will make it fly true."
},
{
"n": 3,
"chapter": 55,
"defect": "Wobbled and sank fast after drying off.",
"action": "Re-crease every fold sharply, pinch the body closed under the wings and set the wings in a slight upward V.",
"why": "Sharp creases stop the surfaces flexing, and a slight upward V lets the plane roll back level by itself."
}
],
"first_fix": "Fold a fresh plane before anything else: squawk 2's left wing is soggy.",
"next_test": "Throw the fresh plane level, a gentle throw, and write down what it does each time.",
"sign_off": "Not released to service; a fresh plane is needed."
}
One good glide - test-fly
Input:
{
"task": "squawk",
"plane": "Wide-wing glider | paper: US Letter printer paper | where: indoors | tail mark: HALL-7 | throw it: gentle",
"reports": "1. glided straight down the hallway | reads as: good flight | chapter 5\n2. not sure, it hit the lampshade on the way past | reads as: not clear | chapter 5",
"facts": "verdict: test-fly\nsquawks: 2 (read clearly: 1, not clear: 1)\ndamage: none\nfaults: none\ngood flights: 1 (squawk 1)\nfirst fix: none\nwork order: none\nturns both ways: no\nwind: none\nleft out: none"
}
Reply:
{
"task": "squawk",
"verdict": "test-fly",
"entries": [
{
"n": 1,
"chapter": 5,
"defect": "Glided straight down the hallway.",
"action": "No fault found - no adjustment.",
"why": "Nothing to correct."
},
{
"n": 2,
"chapter": 5,
"defect": "Not sure, it hit the lampshade on the way past.",
"action": "No fault code - say what the plane did on this throw.",
"why": "The words do not say what the plane did."
}
],
"first_fix": "No adjustment is due until the reports say how it flies.",
"next_test": "Throw it level, a gentle throw, and write down what it does each time.",
"sign_off": "Awaiting a test flight before release."
}
Three clean flights - cleared
Input:
{
"task": "squawk",
"plane": "Long-wing glider | paper: Thin card | where: outdoors, breezy | tail mark: SKY-2 | throw it: gentle",
"reports": "1. long glide across the park, landed flat | reads as: good flight | chapter 5\n2. flew straight and level for about 15 steps | reads as: good flight | chapter 5\n3. smooth glide again, drifted a bit with the breeze | reads as: good flight | chapter 5",
"facts": "verdict: cleared\nsquawks: 3 (read clearly: 3, not clear: 0)\ndamage: none\nfaults: none\ngood flights: 3 (squawk 1, squawk 2 and squawk 3)\nfirst fix: none\nwork order: none\nturns both ways: no\nwind: breezy\nleft out: none"
}
Reply:
{
"task": "squawk",
"verdict": "cleared",
"entries": [
{
"n": 1,
"chapter": 5,
"defect": "Glided long across the park and landed flat.",
"action": "No fault found - no adjustment.",
"why": "Nothing to correct."
},
{
"n": 2,
"chapter": 5,
"defect": "Flew straight and level for about 15 steps.",
"action": "No fault found - no adjustment.",
"why": "Nothing to correct."
},
{
"n": 3,
"chapter": 5,
"defect": "Glided smoothly again and drifted a bit with the breeze.",
"action": "No fault found - no adjustment.",
"why": "Nothing to correct."
}
],
"first_fix": "No adjustment is due.",
"next_test": "Give it a level, gentle throw and write down what it does each time. Throw it indoors or out of the wind, since a breeze pushes a light plane about.",
"sign_off": "Released to service after 3 good flights."
}
Build the input yourself
Load the engine in node (global.window = global; require("./squawkkit.js"); require("./replykit.js")), then:
const K = SquawkKit, R = ReplyKit;
const built = K.buildInput(K.form({ reg: "FOLD-1", design: "dart", paper: "a4", where: "indoors",
text: "Throw 1: dived nose first into the carpet\nThrow 2: glided straight and level" })); // built.ok is false, with built.note, until there is a throw
const wire = K.toWire(built.input); // POST this to /estimate and /run; built.analysis holds rows, verdict, first, order, bothWays, throwPace, breezy
// then: const reply = R.normalize(R.parseReply(job.output.output));
// const notes = R.check(reply, built).notes; // the page's check: [{ level, text }]
Form keys: reg (the tail mark, up to 12 characters), design (dart, glider, nosebox, longwing or own), paper (a4, letter, half, card, notebook or other), where (indoors, calm or breezy) and text (one throw a line, up to 1,800 characters). Leading markers such as 1., Throw 2:, (3) or - are dropped and blank lines skipped; only the first 8 throws are read, and a line over 200 characters is cut on a word.
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://squawk-sheet.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://squawk-sheet.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"]
// squawk-sheet.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://squawk-sheet.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://squawk-sheet.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
}
// SquawkSheet.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 SquawkSheet {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://squawk-sheet.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://squawk-sheet.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://squawk-sheet.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://squawk-sheet.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":"squawk-sheet"}' | 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": "squawk-sheet"})["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: "squawk-sheet" })).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": "squawk-sheet"}, 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", "squawk-sheet"), 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: "squawk-sheet" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "squawk-sheet"])["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 = "squawk-sheet" }))!["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": "squawk", "plane": "Classic dart | paper: A4 printer paper | where: indoors | tail mark: FOLD-1 | throw it: firm", "reports": "1. dived nose first into the carpet after about 2 m | reads as: dive | chapter 27 ...", "facts": "verdict: trim ...", ...}
# - 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": "squawk", "plane": "Classic dart | paper: A4 printer paper | where: indoors | tail mark: FOLD-1 | throw it: firm", "reports": "1. dived nose first into the carpet after about 2 m | reads as: dive | chapter 27 ...", "facts": "verdict: trim ...", ...}
# - 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: "squawk", plane, reports, facts} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the task and the input: a retry with the same key never bills twice. The page uses squawk-sheet:<task>:<inputHash>:a<attempt> (the one task is squawk), 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="squawk-sheet:squawk:$(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 = "squawk-sheet:" + 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"), reply.get("sign_off"))
for p in reply.get("entries", []):
print(p.get("n"), p.get("defect"), "/", p.get("action"))
import { createHash } from "node:crypto";
const key = `squawk-sheet:${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, reply.sign_off);
for (const p of reply.entries || []) console.log(p.n, p.defect, "/", p.action);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "squawk-sheet:" + 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 }
}
var reply struct {
Verdict string
SignOff string `json:"sign_off"`
Entries []struct { N, Chapter int; Defect, Action, Why string }
}
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply.Verdict, reply.SignOff)
for _, p := range reply.Entries { fmt.Println(p.N, p.Defect, "/", p.Action) }
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "squawk-sheet:" + 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.path("verdict").asText() + " " + reply.path("sign_off").asText());
for (JsonNode p : reply.path("entries")) System.out.println(p.path("n") + " " + p.path("defect").asText() + " / " + p.path("action").asText());
require "digest"
key = "squawk-sheet:#{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"], reply["sign_off"]].inspect
(reply["entries"] || []).each { |p| puts "#{p["n"]} #{p["defect"]} / #{p["action"]}" }
$key = "squawk-sheet:" . $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"] ?? ""), " ", ($reply["sign_off"] ?? ""), "\n";
foreach (($reply["entries"] ?? []) as $p) echo ($p["n"] ?? ""), " ", $p["defect"] ?? "", " / ", $p["action"] ?? "", "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"squawk-sheet:{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?["verdict"]} {reply?["sign_off"]}");
foreach (var p in reply?["entries"]?.AsArray() ?? new JsonArray()) Console.WriteLine($"{p?["n"]} {p?["defect"]} / {p?["action"]}");
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: squawk-sheet:squawk:$(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": "squawk", "plane": "Classic dart | paper: A4 printer paper | where: indoors | tail mark: FOLD-1 | throw it: firm", "reports": "1. dived nose first into the carpet after about 2 m | reads as: dive | chapter 27 ...", "facts": "verdict: trim ...", ...}
# - 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": "squawk-sheet:squawk:" + 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": `squawk-sheet:squawk:${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", "squawk-sheet:squawk:"+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", "squawk-sheet:squawk:" + 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" => "squawk-sheet:squawk:#{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: squawk-sheet:squawk:" . 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", $"squawk-sheet:squawk:{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);