Drive Left at the Scarecrow from your own code
The one metered task, passport, is available over HTTP. It takes a fact list (one fact per line), a theme, who will walk the maze and how many checkpoint posts it has, and returns a verdict (ready to print, short of facts, or no usable facts), one question per post - each with the right answer taken from a fact line and two wrong answers - the lines it left out with a reason, any spare facts, a maze name and the entrance and exit signs.
The division of labour first: the page owns the maze, the posts, the turns and the line count; the model owns which lines are facts and the words; the page pins each right answer to its post's right turn and re-checks the reply. All of it comes from mazekit.js, which you can run yourself (see Build the input yourself). The model never sees the maze, so one reply fits any maze with the same number of posts.
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":"left-at-the-scarecrow"}), 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, ages, posts, 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: writing a passport 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 and the verdict
Every run sends task: passport; a missing or different one is still answered as passport. The model sorts every line into usable facts and skipped lines (opinions, instructions, greetings, questions, vague lines, repeats, private details, unkind lines). With U usable lines and P posts, the verdict is checked in a fixed order and the first that applies wins:
- unusable - U is 0. No questions; every post prints as a blank sign.
- short - U is less than P. One question per usable line, in line order; the remaining posts print blank.
- ready - otherwise. Exactly P questions from the first P usable lines; the rest go in
spare_lines.
The page recomputes the rule from the reply's own count (MazeKit.check) and flags a reply that disagrees.
Input fields
Every value is a string (objects are never sent). app.js builds it with MazeKit.buildInput(form).input.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always passport. |
theme | no | What the maze is about, at most 80 characters, or (no theme given). |
ages | yes | 5-7, 8-11 or 12+ - sets the question and answer lengths. |
posts | yes | The number of checkpoint posts, 3 to 10 (at most 8 on the small field). |
facts | yes | Numbered lines, 1. ..., one fact per line. At most 24 lines of at most 240 characters are sent; bullets and old numbers are stripped and the lines renumbered. |
page_count | no | fact lines L; posts P - the page's own count. |
clip_note | no | Present only when lines were dropped or cut. |
retry_note | no | Sent only on the one automatic reformat retry. |
The reply contract
The reply is one JSON object in job.output.output. The page parses it with MazeKit.parseReply and MazeKit.normalize.
{"task": "passport", "verdict": "ready | short | unusable", "verdict_reason": "...", "maze_name": "...", "welcome": "...",
"stations": [{"line": 1, "question": "...", "answer": "...", "wrong": ["...", "..."], "reveal": "..."}],
"spare_lines": [7], "skipped_lines": [{"line": 2, "why": "an opinion"}], "finish": "..."}
| Field | Rule |
|---|---|
stations | Station 1 goes on post 1, and so on. The question uses only its fact line; the answer keeps the line's words; two wrong answers, different from each other and from the answer, and plainly false. No turns or directions - the page adds them. |
spare_lines, skipped_lines | Together with the stations' line values, every line from 1 to L appears exactly once. |
maze_name, welcome, finish | Built only from the theme and the facts - no invented names, places, dates or prizes. |
The page then pins each station to its post (MazeKit.signs): the right answer on the turn that leads on, the wrong answers on the dead-end turns in order (a two-way fork uses only the first wrong answer). It checks the reply (MazeKit.check): the verdict against the reply's own count, every line accounted for exactly once, line numbers in range, two different wrong answers per post, a right answer that shares a word with its fact line, no talk of turns, and no internal field names in the wording.
Worked examples - one per verdict
These are the app's three free samples: the exact input app.js sends and a reply to it, unedited.
Pumpkin patch, ages 8-11 - ready
Input:
{
"task": "passport",
"theme": "Pumpkin patch",
"ages": "8-11",
"posts": "5",
"facts": "1. Pumpkins are a kind of squash.\n2. Pumpkin patches are the most fun place to be!\n3. A pumpkin is about 90 percent water.\n4. Pumpkins grow on long vines that spread along the ground.\n5. Pumpkin flowers are bright yellow.\n6. Bees carry pollen between pumpkin flowers so the fruit can grow.\n7. Pumpkin seeds can be roasted and eaten.",
"page_count": "fact lines 7; posts 5"
}
Reply:
{
"task": "passport",
"verdict": "ready",
"verdict_reason": "Six of the seven lines are facts with one clear answer, enough for all five posts.",
"maze_name": "Pumpkin Patch Maze",
"welcome": "Welcome to the Pumpkin Patch maze, where every right answer about pumpkins shows you the way on!",
"stations": [
{
"line": 1,
"question": "A pumpkin is a kind of what?",
"answer": "Squash",
"wrong": [
"Melon",
"Cucumber"
],
"reveal": "Pumpkins are a kind of squash."
},
{
"line": 3,
"question": "About what percent of a pumpkin is water?",
"answer": "90 percent",
"wrong": [
"50 percent",
"75 percent"
],
"reveal": "A pumpkin is about 90 percent water."
},
{
"line": 4,
"question": "What do pumpkins grow on?",
"answer": "Long vines",
"wrong": [
"Short stems",
"Tall trees"
],
"reveal": "Pumpkins grow on long vines that spread along the ground."
},
{
"line": 5,
"question": "What color are pumpkin flowers?",
"answer": "Bright yellow",
"wrong": [
"Deep red",
"Soft blue"
],
"reveal": "Pumpkin flowers are bright yellow."
},
{
"line": 6,
"question": "What do bees carry between pumpkin flowers?",
"answer": "Pollen",
"wrong": [
"Leaves",
"Sand"
],
"reveal": "Bees carry pollen between pumpkin flowers so the fruit can grow."
}
],
"spare_lines": [
7
],
"skipped_lines": [
{
"line": 2,
"why": "an opinion"
}
],
"finish": "You made it through the Pumpkin Patch maze - great job answering all about pumpkins!"
}
Grandad's 70th birthday - short
Input:
{
"task": "passport",
"theme": "Grandad's 70th birthday",
"ages": "12+",
"posts": "6",
"facts": "1. Grandad was born in a village called Ashby.\n2. His first car was a green van.\n3. What cake should we get?\n4. He has kept bees for 30 years.\n5. Grandad's first car was a van painted green.\n6. Grandad is the best grandad in the world.\n7. Remember to bring the balloons.",
"page_count": "fact lines 7; posts 6"
}
Reply:
{
"task": "passport",
"verdict": "short",
"verdict_reason": "Three of the seven lines are facts with one clear answer, but there are six posts, so the extra posts print as blank signs.",
"maze_name": "Grandad's 70th Birthday Maze",
"welcome": "Step into Grandad's 70th birthday maze, where every right answer shows you the way on.",
"stations": [
{
"line": 1,
"question": "Which village was Grandad born in?",
"answer": "Ashby",
"wrong": [
"Millbrook",
"Oakford"
],
"reveal": "Grandad was born in the village of Ashby."
},
{
"line": 2,
"question": "What was Grandad's first car?",
"answer": "A green van",
"wrong": [
"A red car",
"A blue truck"
],
"reveal": "Grandad's first car was a green van."
},
{
"line": 4,
"question": "How many years has Grandad kept bees?",
"answer": "30 years",
"wrong": [
"20 years",
"15 years"
],
"reveal": "Grandad has kept bees for 30 years."
}
],
"spare_lines": [],
"skipped_lines": [
{
"line": 3,
"why": "a question, not a fact"
},
{
"line": 5,
"why": "repeats line 2"
},
{
"line": 6,
"why": "an opinion"
},
{
"line": 7,
"why": "an instruction"
}
],
"finish": "Well done, you've made it through Grandad's birthday maze!"
}
Farm open day - unusable
Input:
{
"task": "passport",
"theme": "Farm open day",
"ages": "5-7",
"posts": "4",
"facts": "1. Please wear your boots.\n2. Our apple juice is the tastiest around.\n3. Have a wonderful day!\n4. Which animal do you like best?",
"page_count": "fact lines 4; posts 4"
}
Reply:
{
"task": "passport",
"verdict": "unusable",
"verdict_reason": "None of the four lines are facts with one clear answer, so all four posts will print as blank signs.",
"maze_name": "Farm Open Day Maze",
"welcome": "Welcome to the farm open day maze, come on in!",
"stations": [],
"spare_lines": [],
"skipped_lines": [
{
"line": 1,
"why": "an instruction"
},
{
"line": 2,
"why": "an opinion"
},
{
"line": 3,
"why": "a greeting"
},
{
"line": 4,
"why": "a question, not a fact"
}
],
"finish": "Well done, you found your way through the farm open day maze!"
}
Build the input yourself
Load the script in node (global.window = global; require("./mazekit.js")), then:
const K = MazeKit;
const form = K.form({ theme: "Apple orchard", ages: "5-7", posts: 4, size: "small", seed: "orchard",
facts: "Apples grow on trees.\nApple blossoms are pink and white.\nBring a basket!" });
const lay = K.layout(form.size, form.seed, form.posts); // the maze, its one true path, the forks and the posts
lay.posts.map(p => [p.no, p.turn, p.turns]); // [[1, "right", ["straight", "right"]], [2, "right", ["left", "right"]], ...] - post, right turn, turns
const built = K.buildInput(form); // built.L = 3 lines, built.P = 4 posts
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.signs(lay, reply) // each post's options, the right answer on the right turn
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, lay, reply) // the passport and answer key 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://left-at-the-scarecrow.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://left-at-the-scarecrow.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"]
// left-at-the-scarecrow.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://left-at-the-scarecrow.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://left-at-the-scarecrow.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
}
// LeftAtTheScarecrow.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 LeftAtTheScarecrow {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://left-at-the-scarecrow.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://left-at-the-scarecrow.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://left-at-the-scarecrow.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://left-at-the-scarecrow.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":"left-at-the-scarecrow"}' | 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": "left-at-the-scarecrow"})["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: "left-at-the-scarecrow" })).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": "left-at-the-scarecrow"}, 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", "left-at-the-scarecrow"), 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: "left-at-the-scarecrow" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "left-at-the-scarecrow"])["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 = "left-at-the-scarecrow" }))!["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": "passport", "theme": "Pumpkin patch", "ages": "8-11", "posts": "5", "facts": "1. Pumpkins are a kind of squash.\n2. Pumpkin patches are the most fun place to be!\n3. A pumpkin is about 90 percent water.\n4. Pumpkins grow on long vines that spread along the ground.\n5. Pumpkin flowers are bright yellow.\n6. Bees carry pollen between pumpkin flowers so the fruit can grow.\n7. Pumpkin seeds can be roasted and eaten.", "page_count": "fact lines 7; posts 5"}
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": "passport", "theme": "Pumpkin patch", "ages": "8-11", "posts": "5", "facts": "1. Pumpkins are a kind of squash.\n2. Pumpkin patches are the most fun place to be!\n3. A pumpkin is about 90 percent water.\n4. Pumpkins grow on long vines that spread along the ground.\n5. Pumpkin flowers are bright yellow.\n6. Bees carry pollen between pumpkin flowers so the fruit can grow.\n7. Pumpkin seeds can be roasted and eaten.", "page_count": "fact lines 7; posts 5"}
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: "passport", theme, ages, posts, facts, page_count, 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 left-at-the-scarecrow:<task>:<inputHash>:a<attempt> (the one task is passport), 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="left-at-the-scarecrow:passport:$(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 = "left-at-the-scarecrow:" + 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("maze_name"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `left-at-the-scarecrow:${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.maze_name, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "left-at-the-scarecrow:" + 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 = "left-at-the-scarecrow:" + 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 = "left-at-the-scarecrow:#{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["maze_name"]].inspect
$key = "left-at-the-scarecrow:" . $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["maze_name"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"left-at-the-scarecrow:{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: left-at-the-scarecrow:passport:$(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": "passport", "theme": "Pumpkin patch", "ages": "8-11", "posts": "5", "facts": "1. Pumpkins are a kind of squash.\n2. Pumpkin patches are the most fun place to be!\n3. A pumpkin is about 90 percent water.\n4. Pumpkins grow on long vines that spread along the ground.\n5. Pumpkin flowers are bright yellow.\n6. Bees carry pollen between pumpkin flowers so the fruit can grow.\n7. Pumpkin seeds can be roasted and eaten.", "page_count": "fact lines 7; posts 5"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "left-at-the-scarecrow:passport:" + 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": `left-at-the-scarecrow:passport:${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", "left-at-the-scarecrow:passport:"+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", "left-at-the-scarecrow:passport:" + 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" => "left-at-the-scarecrow:passport:#{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: left-at-the-scarecrow:passport:" . 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", $"left-at-the-scarecrow:passport:{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);