Drive Staggered Start from your own code
The one metered task, level, is available over HTTP. It takes the game, how a game is won, an optional target, how many games were played together, player_count and players - one numbered line per player with name, wins and note, exactly as the page writes it. It returns one entry per player (head-start size, a handicap in the game's own terms and the reason), a verdict for the table, one table rule and, when a gap is too wide, another way to play - or a polite decline when money rides on the game.
The division of labour first: the page works out its own head-start size per player by a fixed rule; the model fits a handicap to the game and writes the words; the page then re-checks every size, the verdict, every amount (a share of the target, rounded) and the word limits. Build players the same way (see "Build the input yourself") so the lines match.
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":"staggered-start"}), 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, game, win_rule, target, games_played, player_count, players) 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: levelling a game is metered and needs a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the status and the verdicts
Every run sends task: level (the one task is level); a missing or different one is still answered as level. status is declined when the game, the win rule or any note mentions a bet, wagers, stakes, gambling or money riding on the result. Otherwise each player gets a tier from r = (leader's wins + 1) / (player's wins + 1): none below 1.5, small (10%) below 2.5, medium (20%) below 4, large (30%) below 7, beyond at 7 or more. The table's verdict is checked in this order, stopping at the first match: switch (any player beyond), stagger (any large), nudge (any small or medium), level (everyone none).
Input (every field is a string)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always level. |
game | yes | The game, up to 40 characters. |
win_rule | yes | How a game is won, up to 80 characters. |
target | yes | The score or rounds needed to win as a whole number, or not given. |
games_played | yes | How many games the group played together, 1 to 99. |
player_count | yes | How many players, 2 to 6, as text. |
players | yes | One numbered line per player, fields separated by | : the name (the person's own words, up to 24 characters, or Player N), wins: (a whole number) and note: (up to 80 characters, or none), worded exactly as the page writes them. |
retry_note | no | Only on the page's one reformat retry, after an unusable reply. |
Reply (one JSON object)
{"task": "level", "status": "ok", "reason": "", "verdict": "nudge",
"players": [{"n": 1, "tier": "none", "handicap": "", "why": "..."}, {"n": 2, "tier": "small", "handicap": "...", "why": "..."}, ...],
"house_rule": "...", "switch_idea": "", "closing": "..."}
| Field | What it holds |
|---|---|
status, reason | ok or declined; for declined, one sentence of at most 25 words, players empty and every other field empty. |
verdict | level, nudge, stagger or switch, by the check order above. |
players | Exactly player_count entries in input order. Each: n, tier (none, small, medium, large or beyond), handicap (at most 20 words, empty for none and beyond; with a target it names the amount - that percentage of the target, rounded to a whole number, never below 1) and why (the player's and the leader's wins, at most 22 words). |
house_rule | One catch-up or turn-order rule for the table, at most 28 words. |
switch_idea | Only for switch: another way to play the same game, at most 28 words; otherwise empty. |
closing | One line of at most 18 words that agrees with the verdict. |
The page then checks the reply (StartKit.check) against its own size for each player, the verdict, the count, every amount and the word limits, and lists any disagreement under "The page's check".
Worked examples
These are the five free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover every outcome - darts (nudge), chess (stagger), table tennis (switch), a word-tile game (level) and pool with money on it (declined).
Darts, three players - nudge
Input:
{
"task": "level",
"game": "Darts",
"win_rule": "first to take 501 down to exactly zero, finishing on a double",
"target": "501",
"games_played": "10",
"player_count": "3",
"players": "1. Priya | wins: 6 | note: none\n2. Tom | wins: 3 | note: throws left-handed\n3. Ana | wins: 1 | note: new to finishing on a double"
}
Reply:
{
"task": "level",
"status": "ok",
"reason": "",
"verdict": "nudge",
"players": [
{
"n": 1,
"tier": "none",
"handicap": "",
"why": "Priya holds the lead with 6 wins and has no head start."
},
{
"n": 2,
"tier": "small",
"handicap": "Give Tom a 50 head start, so Tom needs only 451 to finish.",
"why": "Tom has 3 wins against Priya's 6 wins and gets a small head start."
},
{
"n": 3,
"tier": "medium",
"handicap": "Give Ana a 100 head start, so Ana needs only 401 to finish.",
"why": "Ana has 1 win against Priya's 6 wins and gets a medium head start."
}
],
"house_rule": "Let Ana throw first in each game, since Ana has the fewest wins.",
"switch_idea": "",
"closing": "This table needs small head starts for two players, not a bigger fix."
}
Chess, two players - stagger
Input:
{
"task": "level",
"game": "Chess",
"win_rule": "checkmate the other king",
"target": "not given",
"games_played": "10",
"player_count": "2",
"players": "1. Sam | wins: 9 | note: none\n2. Lee | wins: 1 | note: prefers quick games"
}
Reply:
{
"task": "level",
"status": "ok",
"reason": "",
"verdict": "stagger",
"players": [
{
"n": 1,
"tier": "none",
"handicap": "",
"why": "Sam holds the lead with 9 wins and has no head start."
},
{
"n": 2,
"tier": "large",
"handicap": "Play this game with Sam's queen removed, to help Lee.",
"why": "Lee has 1 win against Sam's 9 wins and gets a large head start."
}
],
"house_rule": "Let Lee choose which color to play in this game.",
"switch_idea": "",
"closing": "This matchup needs a large head start, not a small adjustment."
}
Table tennis, four players - switch
Input:
{
"task": "level",
"game": "Table tennis",
"win_rule": "first to 11 points, two clear",
"target": "11",
"games_played": "12",
"player_count": "4",
"players": "1. Maya | wins: 11 | note: none\n2. Jon | wins: 1 | note: none\n3. Kit | wins: 0 | note: only just learned to serve\n4. Player 4 | wins: 0 | note: none"
}
Reply:
{
"task": "level",
"status": "ok",
"reason": "",
"verdict": "switch",
"players": [
{
"n": 1,
"tier": "none",
"handicap": "",
"why": "Maya holds the lead with 11 wins and has no head start."
},
{
"n": 2,
"tier": "large",
"handicap": "Start Jon's score at 3 points before this game begins.",
"why": "Jon has 1 win against Maya's 11 wins and gets a large head start."
},
{
"n": 3,
"tier": "beyond",
"handicap": "",
"why": "Kit has 0 wins against Maya's 11 wins, too wide for a head start."
},
{
"n": 4,
"tier": "beyond",
"handicap": "",
"why": "Player 4 has 0 wins against Maya's 11 wins, too wide for a head start."
}
],
"house_rule": "Give the player with the fewest wins the serve first each game.",
"switch_idea": "Play doubles, pairing Kit and Player 4 against Maya and Jon for a real contest.",
"closing": "Two players' gaps are too wide for a head start; this table needs a different format."
}
A word-tile game - level
Input:
{
"task": "level",
"game": "Word-tile board game",
"win_rule": "highest total score when the tiles run out",
"target": "not given",
"games_played": "8",
"player_count": "3",
"players": "1. Rosa | wins: 3 | note: none\n2. Ben | wins: 3 | note: none\n3. Ivy | wins: 2 | note: likes long words"
}
Reply:
{
"task": "level",
"status": "ok",
"reason": "",
"verdict": "level",
"players": [
{
"n": 1,
"tier": "none",
"handicap": "",
"why": "Rosa shares the lead with 3 wins and has no head start."
},
{
"n": 2,
"tier": "none",
"handicap": "",
"why": "Ben shares the lead with 3 wins and has no head start."
},
{
"n": 3,
"tier": "none",
"handicap": "",
"why": "Ivy has 2 wins while the lead sits at 3 wins, and has no head start."
}
],
"house_rule": "Let Ivy draw tiles first each game, since Ivy has the fewest wins.",
"switch_idea": "",
"closing": "No player here needs a head start right now."
}
Pool with money on it - declined
Input:
{
"task": "level",
"game": "Pool",
"win_rule": "pot your seven balls, then the black",
"target": "not given",
"games_played": "6",
"player_count": "2",
"players": "1. Dev | wins: 4 | note: none\n2. Max | wins: 1 | note: loser pays £5 a frame"
}
Reply:
{
"task": "level",
"status": "declined",
"reason": "This page only levels games played for fun and does not size bets.",
"verdict": "",
"players": [],
"house_rule": "",
"switch_idea": "",
"closing": ""
}
Build the input yourself
Load the engine in node (global.window = global; require("./staggered.js"); require("./track.js")), then:
const K = StartKit, T = StartTrack;
const built = K.buildInput({ game: "Darts", win_rule: "first to take 501 down to zero", target: "501", games: "10",
players: [ { name: "Priya", wins: "6", note: "" }, { name: "Tom", wins: "3", note: "" } ] });
built.parsed.rows.map(r => r.tier); // the page's own sizes: ["none", "small"]
built.parsed.rows.map(r => r.amount); // [0, 50] - 10% of 501, rounded
built.prescan.verdict; // "nudge"
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the reply
// T.svg({ rows: built.parsed.rows }) // the track drawing
// K.md(built, reply) // the levelled game 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://staggered-start.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://staggered-start.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"]
// staggered-start.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://staggered-start.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://staggered-start.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
}
// StaggeredStart.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 StaggeredStart {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://staggered-start.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://staggered-start.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://staggered-start.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://staggered-start.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":"staggered-start"}' | 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": "staggered-start"})["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: "staggered-start" })).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": "staggered-start"}, 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", "staggered-start"), 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: "staggered-start" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "staggered-start"])["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 = "staggered-start" }))!["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": "level", "game": "...", "player_count": "2", "players": "1. ...
2. ..."} - 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": "level", "game": "...", "player_count": "2", "players": "1. ...
2. ..."} - 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: "level", game, win_rule, target, games_played, player_count, players} (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 staggered-start:<task>:<inputHash>:a<attempt> (the one task is level), 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="staggered-start:level:$(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 = "staggered-start:" + 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("status"))
import { createHash } from "node:crypto";
const key = `staggered-start:${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.status);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "staggered-start:" + 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 = "staggered-start:" + 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 = "staggered-start:#{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["status"]].inspect
$key = "staggered-start:" . $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["status"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"staggered-start:{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: staggered-start:level:$(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": "level", "game": "...", "player_count": "2", "players": "1. ...
2. ..."} - 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": "staggered-start:level:" + 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": `staggered-start:level:${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", "staggered-start:level:"+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", "staggered-start:level:" + 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" => "staggered-start:level:#{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: staggered-start:level:" . 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", $"staggered-start:level:{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);