Drive Tag Yourself from your own code
The one metered step - building a test - is available over HTTP. A natural use is a group-chat bot: it collects the group's running jokes, builds a test, and posts the share link so everyone plays in the browser for free.
The split first: the page does the bookkeeping; the model writes the test; the page checks it. Material parsing, private-detail scrubbing, the name and sensitive-subject prescan, scoring, the random-answer odds and the share links come from tagkit.js, which runs in node as well as in the browser. Its reconcile function then checks every structure rule: 3 axes with six distinct pole letters, the requested question count balanced across the axes, 3-4 options per question with both poles present, exactly 8 types covering every key with unique codes and valid best/worst matches, real material citations, no real name from the material, and that both poles of every axis are reachable.
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":"tag-yourself"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field 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: building 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 comes first
Every run sends task. This app has one task:
| task | What it returns | Verdict field |
|---|---|---|
build | title, tagline, summary; axes [{id, name, left:{letter, label}, right:{letter, label}}] (3); questions [{id, axis, text, options[{text, pole, weight}], uses[]}] (9 or 12, pole left or right, weight 1 or 2); types [{key, code, name, line, traits[3], best_with, worst_with, uses[]}] (8, keys LLL..RRR); missing; reframe; caveats. | ready (four or more specific bits of material), thin (a full but more generic test plus missing: what would sharpen it), declined (targets one private person or a protected trait: no axes, questions or types, and a reframe idea instead). |
A type key has one letter per axis in order, L for the left pole and R for the right. Scoring: each axis sums -weight for a left answer and +weight for a right one; a tie goes to the pole of the last answered option on that axis (TagKit.score).
The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (TagKit.toWire does this). task, group and material are required.
Input fields
| Field | Type | Notes |
|---|---|---|
task | string | build. |
group | string (JSON) | {name, kind, spice, questions}; kind office, group_chat, friends, team, club, class, fandom, family or other; spice mild, medium or spicy; questions 9 or 12. |
material | string (JSON) | [{id, text}], ids M1.., at most 40 lines of 220 characters, private details already replaced with [removed] (TagKit.parseMaterial). |
prescan | string (JSON) | {material_count, names[], removed_private, sensitive[{id, topic}]} from TagKit.prescan. |
note | string | Optional wishes; over 600 characters keeps the start and the end around a marker. |
retry_note | string | Only on a reformat retry. |
The reply
The job's output.output is a string holding one JSON object. Parse it with TagKit.parseReply, then normalize, then check it with reconcile(reply, {group, material, prescan}). playable(reply) says whether it can be played; encodeShare(reply) returns the fragment for https://tag-yourself.skillsafe.ai/#t=…, a link that plays the whole test with no account.
Worked examples
Replies produced against the app's prompt for the three sample groups (invented; lists shortened; the full replies ship in example-result.js), one per verdict. The page replays each of them free from its sample buttons.
Office floor - verdict ready
Input:
{
"task": "build",
"group": "{\"name\":\"The fourth-floor product team\",\"kind\":\"office\",\"spice\":\"medium\",\"questions\":12}",
"material": "[{\"id\":\"M1\",\"text\":\"The fridge has a shelf of yogurts nobody will admit to owning\"},{\"id\":\"M2\",\"text\":\"Standup is supposed to be 10 minutes and has never once been 10 minutes\"},{\"id\":\"M3\",\"text\":\"Someone always replies-all with just a thumbs up\"},\"... 7 more lines\"]",
"prescan": "{\"material_count\":10,\"names\":[],\"removed_private\":0,\"sensitive\":[]}"
}
Reply:
{
"task": "build",
"verdict": "ready",
"title": "The Fourth-Floor Personality Test",
"tagline": "Standup overruns, mystery yogurt, and the thermostat cold war finally get you typed.",
"summary": "Built straight from your team's own flashpoints: the standup that never ends on time, the unclaimed yogurt shelf, the window-vs-hallway thermostat war, and Friday demo-day crunch. Medium spice keeps it an affectionate roast people will actually want to share.",
"axes": [
{
"id": "A1",
"name": "Meeting Time Philosophy",
"left": {
"letter": "O",
"label": "Overtimer"
},
"right": {
"letter": "C",
"label": "Clock-Cutter"
}
},
{
"id": "A2",
"name": "Snack & Supply Instinct",
"left": {
"letter": "H",
"label": "Hoarder"
},
"right": {
"letter": "G",
"label": "Giver"
}
},
{
"id": "A3",
"name": "Office Diplomacy",
"left": {
"letter": "W",
"label": "Window Warrior"
},
"right": {
"letter": "P",
"label": "Peacemaker"
}
}
],
"questions": [
{
"id": "Q1",
"axis": "A1",
"text": "Standup was supposed to end eight minutes ago. You...",
"options": [
{
"text": "Keep talking, this is the good part",
"pole": "left",
"weight": 2
},
{
"text": "Start a side thread so standup can actually end",
"pole": "right",
"weight": 2
},
{
"text": "Say 'last thing' and mean it this time... maybe",
"pole": "left",
"weight": 1
},
{
"text": "Mute yourself and text the real update later",
"pole": "right",
"weight": 1
}
],
"uses": [
"M2"
]
},
{
"id": "Q2",
"axis": "A2",
"text": "The snack drawer just got restocked. It's Monday morning. You...",
"options": [
{
"text": "Grab a stash for the whole week before anyone notices",
"pole": "left",
"weight": 2
},
{
"text": "Take one and leave plenty for everyone else",
"pole": "right",
"weight": 2
},
{
"text": "Peek twice before actually taking anything",
"pole": "left",
"weight": 1
},
{
"text": "Mention the restock in the group chat so others know",
"pole": "right",
"weight": 1
}
],
"uses": [
"M10"
]
},
"... 10 more"
],
"types": [
{
"key": "LLL",
"code": "DRAGN",
"name": "The Snack Dragon",
"line": "Guards the last yogurt like a dragon hoard and will absolutely let standup run twenty minutes over to make a point about the thermostat.",
"traits": [
"Guards the yogurt shelf",
"Never says 'last thing' and means it",
"Has a thermostat opinion"
],
"best_with": "RRR",
"worst_with": "RLL",
"uses": [
"M2",
"M1",
"M8"
]
},
{
"key": "LLR",
"code": "SQRL",
"name": "The Quiet Hoarder",
"line": "Talks through the whole meeting, has a secret snack stash nobody's found, and still refuses to pick a side in the thermostat war.",
"traits": [
"Secret snack stash",
"Talks past the meeting's end time",
"Stays out of office wars"
],
"best_with": "RRL",
"worst_with": "LRL",
"uses": [
"M7",
"M3"
]
},
"... 6 more"
],
"missing": [],
"reframe": "",
"caveats": [
"All 10 submitted bits read as ordinary workplace habits, not personal attacks, so every one was treated as fair game for jokes.",
"Assumed 'Friday demo day' and the Monday snack restock are weekly recurring rituals based on how the material is phrased."
]
}
Sunday pickup - verdict thin
Input:
{
"task": "build",
"group": "{\"name\":\"Sunday pickup soccer\",\"kind\":\"team\",\"spice\":\"mild\",\"questions\":9}",
"material": "[{\"id\":\"M1\",\"text\":\"We play on Sundays\"},{\"id\":\"M2\",\"text\":\"Some people are late\"}]",
"prescan": "{\"material_count\":2,\"names\":[],\"removed_private\":0,\"sensitive\":[]}"
}
Reply:
{
"task": "build",
"verdict": "thin",
"title": "The Sunday Pickup Soccer Type Test",
"tagline": "For the team that argues about kickoff time more than tactics.",
"summary": "This leans on your two running bits - the Sunday ritual and the chronic lateness - and stretches them across punctuality, weather commitment, and competitiveness. Only two concrete details came through, so the weather and intensity axes are built from generic pickup-soccer tropes rather than this team's own habits; a bit more detail would make it sharper.",
"axes": [
{
"id": "A1",
"name": "Punctuality",
"left": {
"letter": "E",
"label": "Early bird"
},
"right": {
"letter": "L",
"label": "Fashionably late"
}
},
{
"id": "A2",
"name": "Weather Commitment",
"left": {
"letter": "A",
"label": "All-weather"
},
"right": {
"letter": "F",
"label": "Fair-weather flake"
}
},
{
"id": "A3",
"name": "Competitive Intensity",
"left": {
"letter": "C",
"label": "Casual kickabout"
},
"right": {
"letter": "W",
"label": "Win-at-all-costs"
}
}
],
"questions": [
{
"id": "Q1",
"axis": "A1",
"text": "Kickoff is 11:00 sharp this Sunday. It's 10:55 and you are...",
"options": [
{
"text": "Already laced up, warming up",
"pole": "left",
"weight": 2
},
{
"text": "Just pulling into the lot",
"pole": "left",
"weight": 1
},
{
"text": "Still hunting for parking at 11:02",
"pole": "right",
"weight": 1
},
{
"text": "Texting 'omw' from bed",
"pole": "right",
"weight": 2
}
],
"uses": [
"M2"
]
},
{
"id": "Q2",
"axis": "A2",
"text": "Forecast says cold rain for Sunday's game.",
"options": [
{
"text": "Pack extra layers, see you there",
"pole": "left",
"weight": 2
},
{
"text": "Show up grumbling but show up",
"pole": "left",
"weight": 1
},
{
"text": "Suddenly very busy that day",
"pole": "right",
"weight": 1
},
{
"text": "Already drafting the group chat excuse",
"pole": "right",
"weight": 2
}
],
"uses": []
},
"... 7 more"
],
"types": [
{
"key": "LLL",
"code": "GRND",
"name": "The Groundskeeper",
"line": "Rain, shine, or 2-0 down, you're just happy to be out there twenty minutes before everyone else.",
"traits": [
"First one stretching on the grass",
"Shows up rain or shine",
"Couldn't tell you the score"
],
"best_with": "RLR",
"worst_with": "RRR",
"uses": [
"M2"
]
},
{
"key": "LLR",
"code": "KPTN",
"name": "The Captain",
"line": "You're there before kickoff, playing through the drizzle, and absolutely keeping score in your head.",
"traits": [
"Organizes the warm-up",
"Never misses for weather",
"Remembers every scoreline"
],
"best_with": "RLL",
"worst_with": "LRL",
"uses": [
"M2"
]
},
"... 6 more"
],
"missing": [
"what actually happens when someone's late - sub-in chaos, a group-chat roast, a running tally?",
"whether this team skips games for rain or plays through anything short of lightning",
"the one argument that always breaks out mid-game (offside calls, uneven teams, etc.)",
"any nicknames, positions, or rituals beyond the Sunday slot itself"
],
"reframe": "",
"caveats": [
"Weather-flaking and win-at-all-costs axes are generic pickup-soccer tropes, not drawn from this group's own material - only two concrete bits came through.",
"No names appeared in material or prescan, so none were used; no sensitive lines were present to avoid."
]
}
A test about my ex - verdict declined
Input:
{
"task": "build",
"group": "{\"name\":\"My ex Jordan\",\"kind\":\"other\",\"spice\":\"spicy\",\"questions\":9}",
"material": "[{\"id\":\"M1\",\"text\":\"Jordan never paid back the concert money\"},{\"id\":\"M2\",\"text\":\"Jordan cried at the dentist\"},{\"id\":\"M3\",\"text\":\"Which of Jordan's flaws are you - make people pick the worst one\"},\"... 1 more lines\"]",
"prescan": "{\"material_count\":4,\"names\":[\"Jordan\"],\"removed_private\":0,\"sensitive\":[{\"id\":\"M4\",\"topic\":\"body\"}]}",
"note": "Make it brutal, I want to send it to everyone we both know."
}
Reply:
{
"task": "build",
"verdict": "declined",
"title": "Which Kind of Breakup Survivor Are You?",
"tagline": "A lighthearted post-breakup-habits quiz for you and your friends - no one named.",
"summary": "This isn't a group personality test - it's a roast of one identifiable real person (your ex), built from private details about them and meant to be sent to people who know them. The material also leans on a joke about their body after the breakup, which is a protected-trait line I won't use even inside a 'funny' quiz. I can't build this one, but here's a version that keeps the humor and loses the target.",
"axes": [],
"questions": [],
"types": [],
"missing": [],
"reframe": "Build 'Which Kind of Breakup Survivor Are You?' for you and your friends instead - a lighthearted quiz about post-breakup habits everyone recognizes (the 3am playlist purge, the petty item-return, the group-chat play-by-play, the revenge haircut), scored for your own friend group with no one named or targeted.",
"caveats": [
"Declined: the subject is one identifiable private person (named ex), not a group.",
"Material includes a body/weight joke, which is a protected-trait line regardless of spice level.",
"The stated goal (send it to everyone we both know) is harassment framing, not a shareable group test."
]
}
Build the input yourself
Load the script in node (global.window = {}; require("./tagkit.js")), then:
const K = TagKit;
const { input } = K.buildInput("build", { group: { name: "The night shift", kind: "office", spice: "medium", questions: 9 }, text: "Coffee is a food group\n..." });
const wire = K.toWire(input); // POST this to /estimate and /run
// later: const test = K.normalize(K.parseReply(job.output.output));
// K.reconcile(test, { group: input.group, material: input.material, prescan: input.prescan }) -> [{level, code, text}]
// K.score(test, [0, 2, 1, ...]).key -> "LRL"; K.typeOdds(test) -> {LLL: 0.125, ...}
// "https://tag-yourself.skillsafe.ai/#t=" + K.encodeShare(test)
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://tag-yourself.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://tag-yourself.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"]
// tag-yourself.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://tag-yourself.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://tag-yourself.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
}
// CueScroll.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 CueScroll {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://tag-yourself.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://tag-yourself.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://tag-yourself.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://tag-yourself.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":"tag-yourself"}' | 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": "tag-yourself"})["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: "tag-yourself" })).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": "tag-yourself"}, 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", "tag-yourself"), 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: "tag-yourself" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "tag-yourself"])["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 = "tag-yourself" }))!["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": "build", "group": "{...}", "material": "[...]", "prescan": "{...}"}
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": "build", "group": "{...}", "material": "[...]", "prescan": "{...}"}
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, group, material, prescan, note}
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 tag-yourself:<task>:<inputHash>:a<attempt>, 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="tag-yourself:build:$(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 = "tag-yourself:" + 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 lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `tag-yourself:${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 := "tag-yourself:" + 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 = "tag-yourself:" + 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 = "tag-yourself:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"]].inspect
$key = "tag-yourself:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["verdict"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"tag-yourself:{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: tag-yourself:build:$(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": "build", "group": "{...}", "material": "[...]", "prescan": "{...}"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "tag-yourself:build:" + 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": `tag-yourself:build:${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", "tag-yourself:build:"+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", "tag-yourself:build:" + 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" => "tag-yourself:build:#{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: tag-yourself:build:" . 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", $"tag-yourself:build:{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);