Drive Pretty Messy from your own code
The one metered task, mess-weigh, is available over HTTP. It takes 1 lovely-but-messy thing or habit, what was already tried, and up to 8 rows - each a delight or a mess with its size, how often it happens and its weight - plus the page's verdict, and returns 1 problem, 3 ways out (sweep and keep, prune back, replant) and 1 recommendation.
The division of labour first: a fixed rule weighs the rows and picks the verdict; the model frames the problem and the ways out. After a run the page checks that the verdict matches its weighing, that all 3 ways come back in order, that the recommendation names the main delight and the worst mess, that every number comes from the rows 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":"pretty-messy"}), 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, thing, tried, rows, 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: a reading 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 verdict
Every run sends task: mess-weigh (the one task is mess-weigh); a missing or different one is still answered as mess-weigh. Each row's weight is its size (tiny 1 to huge 5) times how often (rarely 1, now and then 2, most weeks 3, most days 4). The rule, in order: 4 x delights >= 5 x messes sweep; 5 x delights >= 4 x messes prune; otherwise replant. The main delight and the worst mess are the heaviest row on each side, the earlier row on a tie.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always mess-weigh. |
thing | yes | The thing or habit in the person's words, up to 60 characters. |
tried | yes | What was already tried, up to 100 characters, or nothing yet. |
rows | yes | 1 line per row: item 1 | delight | size: huge | how often: most days | weight: 20 | says: .... |
facts | yes | The page's lines: delight total:, mess total:, verdict:, main delight:, worst mess:. |
The reply
job.output.output is a JSON string:
{"task": "mess-weigh", "verdict": "sweep", "problem": "...", "options": [{"kind": "sweep", "title": "...", "keeps": "...", "costs": "...", "first_step": "..."}, {"kind": "prune", ...}, {"kind": "replant", ...}], "recommendation": "...", "closing": "..."}
| Field | What it holds |
|---|---|
verdict | The page's verdict, copied: sweep, prune or replant. |
problem | 1 sentence naming the main delight and the worst mess, at most 30 words. |
options | Exactly 3, in order sweep, prune, replant: kind, title (at most 8 words), keeps and costs (at most 20 words each) and first_step (at most 30 words). |
recommendation | Starts with the verdict's way name, gives the 2 totals and names the main delight and worst mess as item numbers, at most 40 words. |
closing | 1 light sentence, at most 20 words. |
The page then checks the reply (MessKit.check) and lists any disagreement under "The page's check".
Worked examples - every verdict
These are the 3 free samples on the page: the exact input the page sends and the reply that replays for free.
White linen sofa - delights 32 against messes 16: sweep and keep
Input:
{
"task": "mess-weigh",
"thing": "white linen sofa",
"tried": "nothing yet",
"rows": "item 1 | delight | size: huge | how often: most days | weight: 20 | says: sinking into it every evening\nitem 2 | delight | size: medium | how often: most days | weight: 12 | says: the whole room feels bright\nitem 3 | mess | size: big | how often: most weeks | weight: 12 | says: every coffee drip shows\nitem 4 | mess | size: tiny | how often: most days | weight: 4 | says: cushions slump out of shape",
"facts": "delight total: 32 (2 delights)\nmess total: 16 (2 messes)\nverdict: sweep (delights at least 1.25 times the messes)\nmain delight: item 1 (weight 20)\nworst mess: item 3 (weight 12)"
}
Reply:
{
"task": "mess-weigh",
"verdict": "sweep",
"problem": "How do you keep sinking into it every evening without every coffee drip showing on the white linen sofa?",
"options": [
{
"kind": "sweep",
"title": "A daily drip check",
"keeps": "You keep sinking into it every evening and the whole room feels bright, with nothing about it altered.",
"costs": "Cushions slump out of shape still, plus the effort of keeping up a new routine for every coffee drip.",
"first_step": "Keep a small tray or cloth nearby so you can wipe up every coffee drip the moment it happens."
},
{
"kind": "prune",
"title": "Sink in less often",
"keeps": "You still keep sinking into it, though on fewer evenings than before.",
"costs": "You get fewer evenings of sinking into it, and cushions can still slump out of shape a little.",
"first_step": "Pick some evenings to relax somewhere else, so the white linen sofa meets fewer coffee drips."
},
{
"kind": "replant",
"title": "Try a chair instead",
"keeps": "You still get the feeling of sinking into it, just somewhere that is not the white linen sofa.",
"costs": "The whole room no longer feels as bright, since the white linen sofa itself is gone.",
"first_step": "Try sitting and relaxing in a plain armchair or on a floor cushion for a few evenings to see how it feels."
}
],
"recommendation": "Sweep and keep, since delights reach 32 against messes at 16, item 1 is the heart of it, and item 3 is what a routine should target.",
"closing": "Keep sinking in and keep a cloth close by, small habits can protect the white linen sofa too."
}
Jigsaw on the dining table - delights 22 against messes 25: prune back
Input:
{
"task": "mess-weigh",
"thing": "1000-piece jigsaw on the dining table",
"tried": "nothing yet",
"rows": "item 1 | delight | size: big | how often: most days | weight: 16 | says: a calm break after work\nitem 2 | mess | size: big | how often: most days | weight: 16 | says: no room to eat at the table\nitem 3 | delight | size: small | how often: most weeks | weight: 6 | says: the cheer when a section clicks\nitem 4 | mess | size: medium | how often: most weeks | weight: 9 | says: pieces end up under the sofa",
"facts": "delight total: 22 (2 delights)\nmess total: 25 (2 messes)\nverdict: prune (delights and messes close to even)\nmain delight: item 1 (weight 16)\nworst mess: item 2 (weight 16)"
}
Reply:
{
"task": "mess-weigh",
"verdict": "prune",
"problem": "How do you keep a calm break after work without losing room to eat at the dining table?",
"options": [
{
"kind": "sweep",
"title": "Clear space for meals",
"keeps": "You keep a calm break after work and the cheer when a section clicks, with nothing about the jigsaw changed.",
"costs": "Pieces can still end up under the sofa, plus the effort of clearing the table before each meal.",
"first_step": "Set a tray or box to hold the puzzle pieces so you can clear a space to eat at the table before each meal."
},
{
"kind": "prune",
"title": "Shrink its table footprint",
"keeps": "You still get a calm break after work, just with the jigsaw taking up less of the table.",
"costs": "Pieces can still end up under the sofa, and you get less table space for the puzzle itself.",
"first_step": "Set out a board or tray that limits the jigsaw to a smaller section of the table, leaving the rest free to eat at."
},
{
"kind": "replant",
"title": "A break away from the table",
"keeps": "You still get a calm break after work, just somewhere other than at the dining table.",
"costs": "You give up the cheer when a section clicks, since the jigsaw itself is no longer there.",
"first_step": "Try taking a calm break after work with a short walk or a book instead of the jigsaw."
}
],
"recommendation": "Prune back, because delights sit at 22 while messes reach 25, keeping item 1 going but easing item 2 by giving the jigsaw a smaller claim on the table.",
"closing": "A smaller table claim could still let that calm break happen, worth a first try."
}
Glitter nails at home, a tray already tried - delights 12 against messes 22: replant
Input:
{
"task": "mess-weigh",
"thing": "glitter nail art at home",
"tried": "a tray under my hands while I paint",
"rows": "item 1 | delight | size: big | how often: now and then | weight: 8 | says: sparkly nails for nights out\nitem 2 | delight | size: small | how often: now and then | weight: 4 | says: trying a new look\nitem 3 | mess | size: big | how often: most days | weight: 16 | says: glitter on every surface for days\nitem 4 | mess | size: medium | how often: now and then | weight: 6 | says: removal takes ages",
"facts": "delight total: 12 (2 delights)\nmess total: 22 (2 messes)\nverdict: replant (messes well over the delights)\nmain delight: item 1 (weight 8)\nworst mess: item 3 (weight 16)"
}
Reply:
{
"task": "mess-weigh",
"verdict": "replant",
"problem": "How do you get sparkly nails for nights out without glitter ending up on every surface for days?",
"options": [
{
"kind": "sweep",
"title": "Expand the tray routine",
"keeps": "You keep sparkly nails for nights out and keep trying a new look, with nothing about it changed.",
"costs": "Removal still takes ages, plus the extra effort of wiping down surfaces beyond the tray each time.",
"first_step": "Beyond the tray you already use, add a cloth to wipe nearby surfaces right after you finish painting your nails."
},
{
"kind": "prune",
"title": "Glitter on fewer nails",
"keeps": "You still get sparkly nails for nights out, just with glitter on fewer nails each time.",
"costs": "Removal still takes ages for the glitter nails you do keep, and some glitter can still spread nearby.",
"first_step": "Try glitter on fewer nails next time, keeping the tray under your hands as you already do."
},
{
"kind": "replant",
"title": "Shimmer without the spread",
"keeps": "You can still get sparkly nails for nights out using a shimmer polish without any loose glitter.",
"costs": "You give up some chances of trying a new look, since the loose glitter nail art itself is gone.",
"first_step": "Try a shimmer polish without loose glitter for your next nights out instead of the glitter nail art."
}
],
"recommendation": "Replant, because delights only reach 12 against messes at 22, with item 3 far outweighing item 1, so a different way to get the sparkle fits better.",
"closing": "A shimmer polish might give you that sparkle for nights out without the days of cleanup after."
}
Build the input yourself
Load the engine in node (global.window = global; require("./messkit.js")), then:
const K = MessKit;
const built = K.buildInput(K.form({ thing: "white linen sofa", tried: "",
items: [{ side: "bloom", text: "sinking into it every evening", size: 5, often: "daily" },
{ side: "petal", text: "every coffee drip shows", size: 4, often: "weekly" }] }));
built.input.facts; // the page's lines: totals, verdict, main delight, worst mess
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, "my sofa") // the weighing 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://pretty-messy.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://pretty-messy.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"]
// pretty-messy.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://pretty-messy.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://pretty-messy.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://pretty-messy.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://pretty-messy.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://pretty-messy.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://pretty-messy.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":"pretty-messy"}' | 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": "pretty-messy"})["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: "pretty-messy" })).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": "pretty-messy"}, 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", "pretty-messy"), 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: "pretty-messy" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "pretty-messy"])["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 = "pretty-messy" }))!["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": "mess-weigh", "thing": "white linen sofa", "tried": "nothing yet", "rows": "item 1 | delight | size: huge | ...", "facts": "delight total: 32 ..."}
# - 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": "mess-weigh", "thing": "white linen sofa", "tried": "nothing yet", "rows": "item 1 | delight | size: huge | ...", "facts": "delight total: 32 ..."}
# - 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: "mess-weigh", thing, tried, rows, facts} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses pretty-messy:<task>:<inputHash>:a<attempt> (the one task is mess-weigh), 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="pretty-messy:mess-weigh:$(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 = "pretty-messy:" + 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("order"))
import { createHash } from "node:crypto";
const key = `pretty-messy:${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.order);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "pretty-messy:" + 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 = "pretty-messy:" + 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 = "pretty-messy:#{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["order"]].inspect
$key = "pretty-messy:" . $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"], " ", json_encode($reply["order"]), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"pretty-messy:{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: pretty-messy:mess-weigh:$(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": "mess-weigh", "thing": "white linen sofa", "tried": "nothing yet", "rows": "item 1 | delight | size: huge | ...", "facts": "delight total: 32 ..."}
# - 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": "pretty-messy:mess-weigh:" + 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": `pretty-messy:mess-weigh:${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", "pretty-messy:mess-weigh:"+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", "pretty-messy:mess-weigh:" + 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" => "pretty-messy:mess-weigh:#{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: pretty-messy:mess-weigh:" . 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", $"pretty-messy:mess-weigh:{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);