Drive Steal or Open from your own code
The one metered task, rules-check, is available over HTTP. It takes the number of players, the house rules as numbered lines, the page's word-list reading of them and its simulated figures, and returns a verdict - rewrite, patch or ready - with the lines that clash, loopholes walked step by step, the topics that are missing, a numbered table card (fixes marked as suggestions) and a host script to read aloud. A natural use is a party-planning tool that checks the rules a host typed before the invitations go out, or a club newsletter that prints a reviewed card every year.
The division of labour first: the page owns the counting and the simulation; the model owns the judgement and the words; the page re-checks the reply. Splitting the rules into lines, the topic reading, the steal limit, the steal-back ban, the per-turn cap, the first player's final turn and 400 seeded games come from swapkit.js, which runs in node as well as in the browser. The model does no arithmetic: SwapKit.buildInput hands it the figures. SwapKit.check holds each reply to the lines: every cited line real, the verdict following the reply's own lists, the card keeping the rules' price and steal limit, no unmarked number the rules lack, and no line id in prose.
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":"steal-or-open"}), 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, players, rules) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the review 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 verdicts
Every run sends task: rules-check (the only task; a missing or unknown one is treated as rules-check). The verdict follows a fixed check order - the first that applies wins:
| Order | Verdict | When |
|---|---|---|
| 1 | rewrite | Two or more lines clash, or no line says what a player does on a turn. |
| 2 | patch | Otherwise, a core topic is missing (turn order, price cap, steal limit, how it ends) or there is at least one loophole. |
| 3 | ready | Otherwise. Missing extras (stealing straight back, steals per turn, wrapping, showing opened gifts, after the game) are allowed. |
The declared input schema carries scalars only, so the structured fields (rules, page_reading, page_sim) travel JSON-encoded as strings (SwapKit.toWire does this). Every value is a string.
Input fields
| Field | Type | Notes |
|---|---|---|
task | string | rules-check. Required. |
players | string | How many people will play, 3 to 40. Required. |
rules | string (JSON) | [{id, text}] - ids R1.., one per line typed, at most 30 lines of at most 280 characters. Required. |
page_reading | string (JSON) | {covered, missing_core, steal_limits_found, steal_back_barred, turn_cap, first_player_final_turn, price, endless_chain} - the page's word-list reading, a hint the model judges for itself. |
page_sim | string (JSON) | {games, players, median_minutes, p90_minutes, median_steals, p90_steals, median_longest_turn, max_longest_turn, mean_frozen, runaway_games, assumptions} - simulated figures; minutes are estimates. |
clip_note, retry_note | string | What the page cut, when it cut something; and only on a reformat retry, what was wrong. |
The reply
The job's output.output is a string holding one JSON object: task, verdict, title, summary, clashes ([{refs, why, fix}]), loopholes ([{refs, scenario, fix}]), gaps ([{topic, why, fix}]), card ([{text, refs, suggested}], at most 10) and host_script. Parse with SwapKit.parseReply, then normalize(reply), then check(reply, built) where built = SwapKit.buildInput(form); md(reply, built) writes the Markdown and cardAsRules(reply) turns the card into new rules for a second check.
Worked examples
Replies produced against the app's prompt for the three sample rule sets (made up for this app; long fields shortened; the full replies ship in example-result.js). Together they cover all three verdicts.
sixteen players, two lines with different steal limits - rewrite
Input:
{
"task": "rules-check",
"players": "16",
"rules": "[{\"id\":\"R1\",\"text\":\"Office Gift Swap - Friday after lunch\"},{\"id\":\"R2\",\"text\":\"Bring one wrapped gift, $25 max.\"},{\"id\":\"R3\",\"text\":\"Everyone draws a number from the hat; number 1 goes first.\"},{\"id\":\"R4\",\"text\":\"On your turn, open a wrapped gift from the pile or steal one somebody already opened.\"},{\"id\":\"R5\",\"text\":\"If your gift gets stolen, you can open a new one or steal from someone else.\"},{\"id\":\"R6\",\"text\":\"A ... (683 characters)",
"page_reading": "{\"covered\":[{\"topic\":\"turn\",\"refs\":[\"R4\",\"R5\"]},{\"topic\":\"order\",\"refs\":[\"R3\"]},{\"topic\":\"price\",\"refs\":[\"R2\"]},{\"topic\":\"steal_limit\",\"refs\":[\"R6\",\"R8\"]},{\"topic\":\"ending\",\"refs\":[\"R9\"]},{\"topic\":\"steal_back\",\"refs\":[\"R7\"]},{\"topic\":\"wrapping\",\"refs\":[\"R2\"]}],\"missing_core\":[],\"steal_limits_found\":[{\"steals\":2,\"ref\":\"R6\"},{\"steals\":3,\"ref\":\"R8\"}],\"steal_back_barred\":true,\"turn_cap\":\"not stated\",\"first_player_final_t ... (474 characters)",
"page_sim": "{\"games\":400,\"players\":16,\"appetite\":\"usual\",\"median_minutes\":21,\"p90_minutes\":23,\"median_steals\":11,\"p90_steals\":17,\"median_longest_turn\":3,\"max_longest_turn\":9,\"mean_frozen\":4.1,\"runaway_games\":0,\"assumptions\":\"estimates only: about 60 seconds per unwrap and 25 seconds per steal; a player steals 45% of the time when a steal is allowed\"}"
}
Reply:
{
"task": "rules-check",
"verdict": "rewrite",
"title": "Two different steal limits conflict",
"summary": "Two lines set different steal limits for the same gift, so a host must guess which one to enforce. Keep the first one listed and drop the other before play. A game runs about 21 minutes, the page's estimate.",
"clashes": [
{
"refs": [
"R6",
"R8"
],
"why": "One line freezes a gift after it's stolen twice; another says a third steal happens before locking it. A host cannot follow both rules at once.",
"fix": "Keep the rule that freezes a gift after it's stolen twice (the one listed first); remove the third-steal rule."
}
],
"loopholes": [],
"gaps": [],
"card": [
{
"text": "Bring one wrapped gift, $25 max.",
"refs": [
"R2"
],
"suggested": false
},
{
"text": "Draw a number from the hat; number 1 goes first.",
"refs": [
"R3"
],
"suggested": false
},
{
"text": "On your turn, open a wrapped gift from the pile, or steal one that's already opened.",
"refs": [
"R4"
],
"suggested": false
},
{
"text": "If your gift is stolen, open a new gift or steal from someone else right away.",
"refs": [
"R5"
],
"suggested": false
},
"... (7 card rules in all)"
],
"host_script": "Each of you brought one wrapped gift, $25 max. Draw a number from the hat - number 1 goes first. On your turn, open a wrapped gift from the pile, or steal one that's already been opened. If your gift gets stolen, take your turn right away: open a new gift or steal from someone else. You can't steal back the exact gift that was just taken from you. Once a gift has been stolen twice, it freezes - no one can take it again. The game ends the moment the last wrapped gift is opened."
}
nine players, no price cap and no ending - patch
Input:
{
"task": "rules-check",
"players": "9",
"rules": "[{\"id\":\"R1\",\"text\":\"Our family gift swap\"},{\"id\":\"R2\",\"text\":\"Everyone brings one wrapped present.\"},{\"id\":\"R3\",\"text\":\"Youngest goes first, then we go around the circle to the left.\"},{\"id\":\"R4\",\"text\":\"When it's your turn you can unwrap a present or steal one that's already open.\"},{\"id\":\"R5\",\"text\":\"If someone steals yours, you get to go again right away.\"},{\"id\":\"R6\",\"text\":\"Each present can only be stolen 3 time ... (542 characters)",
"page_reading": "{\"covered\":[{\"topic\":\"turn\",\"refs\":[\"R4\"]},{\"topic\":\"order\",\"refs\":[\"R3\"]},{\"topic\":\"steal_limit\",\"refs\":[\"R6\"]},{\"topic\":\"wrapping\",\"refs\":[\"R2\"]},{\"topic\":\"reveal\",\"refs\":[\"R7\"]}],\"missing_core\":[\"price\",\"ending\"],\"steal_limits_found\":[{\"steals\":3,\"ref\":\"R6\"}],\"steal_back_barred\":\"not stated\",\"turn_cap\":\"not stated\",\"first_player_final_turn\":\"not stated\",\"price\":\"not found\",\"endless_chain\":false}",
"page_sim": "{\"games\":400,\"players\":9,\"appetite\":\"greedy\",\"median_minutes\":14,\"p90_minutes\":17,\"median_steals\":13,\"p90_steals\":20,\"median_longest_turn\":4,\"max_longest_turn\":14,\"mean_frozen\":3.5,\"runaway_games\":0,\"assumptions\":\"estimates only: about 60 seconds per unwrap and 25 seconds per steal; a player steals 65% of the time when a steal is allowed\"}"
}
Reply:
{
"task": "rules-check",
"verdict": "patch",
"title": "No price cap, no ending rule, and an order gap",
"summary": "The rules cover a turn, order, and freezing, but never say when the game ends or set a spending cap, and don't say who goes next after an immediate extra turn. About 14 minutes per game, the page's estimate.",
"clashes": [],
"loopholes": [
{
"refs": [
"R3",
"R5"
],
"scenario": "A player out of normal turn order steals a gift; the player who lost it immediately takes another turn, as the line about going again says; once that turn ends, nothing says whether the circle resumes after the original stealer or elsewhere.",
"fix": "After an extra turn like this, play returns to whoever comes next after the player who took that extra turn, continuing the usual circle order."
}
],
"gaps": [
{
"topic": "price",
"why": "No line sets a spending cap, a homemade-only rule, or any other limit on what a brought gift can be.",
"fix": "Agree on a spending limit for gifts and note it on the card."
},
{
"topic": "ending",
"why": "No line says when the game is over, such as after the last wrapped gift is opened or a final round.",
"fix": "The game ends once every wrapped gift has been unwrapped; whoever is holding a gift at that point keeps it."
},
{
"topic": "steal_back",
"why": "No line says whether a player can immediately steal back the very gift that was just taken from them.",
"fix": "Add a line that a player may not immediately steal back the gift just taken from them on the same turn."
},
{
"topic": "turn_cap",
"why": "No line limits how many times steals can chain together in a row before order returns to the circle.",
"fix": "Cap chained steals at 3 in a row before the extra turns stop and normal order resumes."
}
],
"card": [
{
"text": "Everyone brings one wrapped gift.",
"refs": [
"R2"
],
"suggested": false
},
{
"text": "Agree on a spending limit for gifts, and note it on the card.",
"refs": [],
"suggested": true
},
{
"text": "The youngest player goes first, then play moves left around the circle.",
"refs": [
"R3"
],
"suggested": false
},
{
"text": "On your turn, unwrap a gift or steal one that's already open.",
"refs": [
"R4"
],
"suggested": false
},
"... (10 card rules in all)"
],
"host_script": "Each of you brought one wrapped gift, so let's get started. The youngest player goes first, then we move left around the circle. On your turn, unwrap a gift or steal one that's already open. If your gift gets stolen, you immediately take another turn - but you may not steal that same gift straight back. Chained steals like that stop after 3 in a row, then we go back to the usual order. Each gift can be stolen up to three times; after the third steal, it freezes with whoever is holding it. Hold your gift up so everyone can see what's open. We'll keep going until every wrapped gift has been opened."
}
fourteen players, every core topic covered - ready
Input:
{
"task": "rules-check",
"players": "14",
"rules": "[{\"id\":\"R1\",\"text\":\"Neighbourhood gift swap - house rules\"},{\"id\":\"R2\",\"text\":\"Bring one wrapped gift worth no more than $20. Wrap it so the shape doesn't give it away.\"},{\"id\":\"R3\",\"text\":\"Draw numbered slips from a bowl; play goes in number order, starting with number 1.\"},{\"id\":\"R4\",\"text\":\"On your turn, either unwrap a gift from the pile or steal any opened gift that is not frozen.\"},{\"id\":\"R5\",\"text\":\"If your gi ... (1302 characters)",
"page_reading": "{\"covered\":[{\"topic\":\"turn\",\"refs\":[\"R4\",\"R5\"]},{\"topic\":\"order\",\"refs\":[\"R3\"]},{\"topic\":\"price\",\"refs\":[\"R2\"]},{\"topic\":\"steal_limit\",\"refs\":[\"R4\",\"R7\",\"R10\"]},{\"topic\":\"ending\",\"refs\":[\"R10\",\"R11\"]},{\"topic\":\"steal_back\",\"refs\":[\"R6\"]},{\"topic\":\"turn_cap\",\"refs\":[\"R8\"]},{\"topic\":\"wrapping\",\"refs\":[\"R2\"]},{\"topic\":\"reveal\",\"refs\":[\"R9\"]},{\"topic\":\"after\",\"refs\":[\"R11\"]}],\"missing_core\":[],\"steal_limits_found\":[{\"ste ... (545 characters)",
"page_sim": "{\"games\":400,\"players\":14,\"appetite\":\"usual\",\"median_minutes\":18,\"p90_minutes\":20,\"median_steals\":10,\"p90_steals\":14,\"median_longest_turn\":3,\"max_longest_turn\":3,\"mean_frozen\":1.4,\"runaway_games\":0,\"assumptions\":\"estimates only: about 60 seconds per unwrap and 25 seconds per steal; a player steals 45% of the time when a steal is allowed\"}"
}
Reply:
{
"task": "rules-check",
"verdict": "ready",
"title": "Ready for the table",
"summary": "The turn, order, price, freezing, and ending rules are all clear and consistent for 14 players, with no clashes or stalls. Expect about 18 minutes, the page's estimate, to play through.",
"clashes": [],
"loopholes": [],
"gaps": [],
"card": [
{
"text": "Bring one wrapped gift worth no more than $20, wrapped so its shape doesn't give it away.",
"refs": [
"R2"
],
"suggested": false
},
{
"text": "Draw a numbered slip to set play order, starting with the first player.",
"refs": [
"R3"
],
"suggested": false
},
{
"text": "On a turn, a player unwraps a new gift from the pile or steals an opened gift that isn't frozen.",
"refs": [
"R4"
],
"suggested": false
},
{
"text": "A player whose gift is stolen immediately takes a turn: unwrap a new gift or steal a different opened gift, unless the per-turn steal cap has been reached.",
"refs": [
"R5"
],
"suggested": false
},
"... (10 card rules in all)"
],
"host_script": "Each of you brought one wrapped gift worth no more than $20, wrapped so its shape doesn't give it away. Draw a numbered slip for play order, starting with the first player. On your turn, unwrap a new gift or steal an unfrozen opened gift. If you're stolen from, take your turn right away: unwrap, or steal a different gift, unless the steal cap is reached. You can't steal back the gift taken from you that turn. A gift freezes after its third steal. Only three steals chain before the next player must unwrap. Once the last gift is unwrapped, the first player keeps their gift or swaps for any unfrozen one, then the game ends. No trades after."
}
Build the input yourself
Load the script in node (global.window = global; require("./swapkit.js")), then:
const K = SwapKit;
const form = K.form({ players: "10", appetite: "usual",
rules: "Bring one wrapped gift, $15 max.\nOn your turn, open a gift or steal an opened one.\nA gift freezes after two steals." });
const r = K.read(form); // numbered lines, topic reading, 400 simulated games
const built = K.buildInput(form);
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the reply
// K.cardAsRules(reply) // the card as new rules, for a second check
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://steal-or-open.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://steal-or-open.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"]
// steal-or-open.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://steal-or-open.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://steal-or-open.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
}
// StealOrOpen.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 StealOrOpen {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://steal-or-open.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://steal-or-open.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://steal-or-open.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://steal-or-open.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":"steal-or-open"}' | 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": "steal-or-open"})["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: "steal-or-open" })).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": "steal-or-open"}, 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", "steal-or-open"), 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: "steal-or-open" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "steal-or-open"])["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 = "steal-or-open" }))!["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": "rules-check", "players": "9", "rules": "[{\"id\":\"R1\",\"text\":\"Our family gift swap\"},{\"id\":\"R2\",\"text\":\"Everyone brings one wrapped present.\"}, ...]", "page_reading": "{...}", "page_sim": "{...}"}
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": "rules-check", "players": "9", "rules": "[{\"id\":\"R1\",\"text\":\"Our family gift swap\"},{\"id\":\"R2\",\"text\":\"Everyone brings one wrapped present.\"}, ...]", "page_reading": "{...}", "page_sim": "{...}"}
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: "rules-check", players, rules, page_reading, page_sim, ... (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 steal-or-open:<task>:<inputHash>:a<attempt> (the one task is rules-check), 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="steal-or-open:rules-check:$(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 = "steal-or-open:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `steal-or-open:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "steal-or-open:" + 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 = "steal-or-open:" + 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 = "steal-or-open:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "steal-or-open:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"steal-or-open:{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: steal-or-open:rules-check:$(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": "rules-check", "players": "9", "rules": "[{\"id\":\"R1\",\"text\":\"Our family gift swap\"},{\"id\":\"R2\",\"text\":\"Everyone brings one wrapped present.\"}, ...]", "page_reading": "{...}", "page_sim": "{...}"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "steal-or-open:rules-check:" + 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": `steal-or-open:rules-check:${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", "steal-or-open:rules-check:"+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", "steal-or-open:rules-check:" + 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" => "steal-or-open:rules-check:#{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: steal-or-open:rules-check:" . 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", $"steal-or-open:rules-check:{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);