Drive On Twos from your own code
The one metered task, pose, is available over HTTP. It takes move - one everyday movement in the person's own words - with seconds, fps (12 or 24), target_frames (seconds times fps) and page_actions (the page's quick count of separate actions). It returns a verdict - one_shot, missing_key or split_it - a shot name and summary, 3 to 8 numbered poses (each a key, breakdown or hold, with a puppet shape and a frame count; together they fill target_frames exactly), 2 to 4 of the twelve classic principles tied to pose numbers, what is missing or how to split the move, and one shooting tip.
The division of labour first: the page works out the frame count and makes its own quick word read; the model reads meaning, poses the move and writes the words; the page then re-checks the reply: the frame total against target_frames, pose numbering and kinds, the puppet shapes, principle ids and pose references and their order, the mid-air cap, the verdict-specific lists and the word limits. Build the input the same way (see "Build the input yourself") so target_frames always matches.
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":"on-twos"}), 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, move, seconds, fps, target_frames, page_actions) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: a breakdown 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: pose (the one task is pose); a missing or different one is still answered as pose. The verdict is checked in this order, first match wins: split_it (two or more separate actions, each with its own goal - a wind-up, bounce or settle is part of its action), missing_key (one action, but the words leave out how the body starts or how it ends), one_shot (one action with its start and end stated). For split_it the poses cover only the first action.
Input (every field is a string)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always pose. |
move | yes | One everyday movement in the person's words, 12 to 400 characters, at least three words. |
seconds | yes | The shot length, 0.5 to 10 in steps of 0.5, as text. |
fps | yes | 12 (on twos) or 24 (on ones). |
target_frames | yes | Seconds times fps, rounded - the exact total the poses must fill. |
page_actions | yes | The page's quick count of clauses joined by words like "then" (at least 1). Only a hint. |
retry_note | no | Only on the page's one reformat retry, after an unusable reply. |
Reply (one JSON object)
{"task": "pose", "verdict": "one_shot", "shot": "...", "summary": "...",
"poses": [{"n": 1, "label": "...", "kind": "key", "preset": "stand", "frames": 4, "note": "..."}, ...],
"principles": [{"id": "anticipation", "poses": [2], "why": "..."}, ...],
"missing": [], "splits": [], "tip": "..."}
| Field | What it holds |
|---|---|
verdict, shot, summary | one_shot, missing_key or split_it; a shot name of at most 6 words; a summary of at most 24 words that agrees with the verdict. |
poses | 3 to 8, numbered from 1 in time order. Each: label (at most 6 words), kind (key, breakdown or hold; the first and last are keys; a hold has 2 or more frames), preset (one of stand, step, crouch, lean_forward, lean_back, reach_up, stretch_tall, reach_forward, arms_out, twist, jump, land_squash, fall, sit, lie, slump), frames (whole numbers summing to target_frames; a jump or fall pose gets at most a quarter of it) and note (at most 18 words). |
principles | 2 to 4, each id once, from squash_stretch, anticipation, staging, straight_ahead_pose_to_pose, follow_through, slow_in_out, arcs, secondary_action, timing, exaggeration, solid_drawing, appeal; with the pose numbers where it shows and why (at most 24 words). Ordered by lowest pose number, ties in that list's order. |
missing, splits | For missing_key only: 1 or 2 questions naming the start or end the words leave out. For split_it only: 2 to 5 short shot names in order. Otherwise empty arrays. |
tip | One practical way to shoot it with a bendable figure on a table, at most 24 words, using table-sized stand-ins for anything the move names. |
The page then checks the reply (TwosKit.check) and lists any disagreement under "The page's check". The puppet plays each pose's shape on its first frame and eases toward the next pose; a hold stays still.
Worked examples
These are the three free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover every verdict - flopping onto the sofa (one shot), a balloon pops (missing a key) and getting out of bed (split it).
Flopping onto the sofa - one shot
Input:
{
"task": "pose",
"move": "Standing next to the sofa, the figure lets the knees go, drops backwards onto the cushions with arms flung wide, bounces once and settles into a slump.",
"seconds": "2",
"fps": "12",
"target_frames": "24",
"page_actions": "1"
}
Reply:
{
"task": "pose",
"verdict": "one_shot",
"shot": "Drop backward onto the sofa",
"summary": "The figure stands, drops backward onto the sofa with arms wide, bounces once, and settles into a slump.",
"poses": [
{
"n": 1,
"label": "Standing beside the sofa",
"kind": "key",
"preset": "stand",
"frames": 2,
"note": "The figure stands upright next to the sofa before the knees give way."
},
{
"n": 2,
"label": "Knees let go",
"kind": "breakdown",
"preset": "crouch",
"frames": 2,
"note": "The knees bend and release, starting the drop backward toward the sofa."
},
{
"n": 3,
"label": "Drops back, arms flung wide",
"kind": "breakdown",
"preset": "fall",
"frames": 4,
"note": "The body drops backward through the air with arms flung wide."
},
{
"n": 4,
"label": "Lands on the cushions",
"kind": "breakdown",
"preset": "land_squash",
"frames": 2,
"note": "The body lands on the cushions, weight sinking in on impact."
},
{
"n": 5,
"label": "Bounces once off cushions",
"kind": "breakdown",
"preset": "jump",
"frames": 2,
"note": "The body lifts slightly off the cushions in one small bounce."
},
{
"n": 6,
"label": "Settles into a slump",
"kind": "key",
"preset": "slump",
"frames": 12,
"note": "The figure settles down into a slump on the cushions."
}
],
"principles": [
{
"id": "anticipation",
"poses": [
1,
2
],
"why": "Before dropping backward, the knees let go first, a small downward move opposite the backward fall to come."
},
{
"id": "secondary_action",
"poses": [
3
],
"why": "Flinging the arms wide as the body drops backward is a smaller move supporting the fall."
},
{
"id": "squash_stretch",
"poses": [
4,
5
],
"why": "The body sinks into the cushions on landing, then springs up slightly in the bounce."
},
{
"id": "slow_in_out",
"poses": [
5,
6
],
"why": "The bounce passes quickly while the final slump holds far longer, slowing into the settle."
}
],
"missing": [],
"splits": [],
"tip": "Fold a cloth into a low pad, prop the figure on a wire rig as it tips back and bounces, then photograph each change."
}
A balloon pops - missing a key
Input:
{
"task": "pose",
"move": "Jumping in surprise when a balloon pops behind them.",
"seconds": "1.5",
"fps": "24",
"target_frames": "36",
"page_actions": "1"
}
Reply:
{
"task": "pose",
"verdict": "missing_key",
"shot": "Jump of surprise at the pop",
"summary": "Four poses show the figure flinching, pushing off, and rising to the jump's peak; start and finish positions aren't stated.",
"poses": [
{
"n": 1,
"label": "Flinching down at the pop",
"kind": "key",
"preset": "crouch",
"frames": 11,
"note": "The figure drops low, bracing in surprise just after the balloon pops behind it."
},
{
"n": 2,
"label": "Pushing off the ground",
"kind": "breakdown",
"preset": "stretch_tall",
"frames": 7,
"note": "The body stretches upward, legs driving against the ground to launch into the jump."
},
{
"n": 3,
"label": "Rising fast off the ground",
"kind": "breakdown",
"preset": "jump",
"frames": 9,
"note": "The figure leaves the ground, rising quickly as the jump carries it upward."
},
{
"n": 4,
"label": "At the peak of the jump",
"kind": "key",
"preset": "jump",
"frames": 9,
"note": "The figure hangs at its highest point, caught mid-air in surprise from the pop."
}
],
"principles": [
{
"id": "squash_stretch",
"poses": [
1,
2
],
"why": "The figure compresses low in the dip, then stretches tall as it pushes off the ground."
},
{
"id": "anticipation",
"poses": [
1
],
"why": "The downward dip at the pop moves opposite the upward jump that follows it."
},
{
"id": "slow_in_out",
"poses": [
1,
4
],
"why": "More frames sit on the startled dip and the peak, fewer on the quick push and rise."
}
],
"missing": [
"What position is the figure in just before the balloon pops?",
"Where does the figure land, and what happens after the jump?"
],
"splits": [],
"tip": "Place a small ball behind the figure to mark the balloon's spot, and hold it aloft on a wire or putty-stick for the peak."
}
Getting out of bed - split it
Input:
{
"task": "pose",
"move": "Getting out of bed: throw off the duvet, swing both legs to the floor, stand up and stretch, then shuffle to the window and open the curtains.",
"seconds": "3",
"fps": "12",
"target_frames": "36",
"page_actions": "2"
}
Reply:
{
"task": "pose",
"verdict": "split_it",
"shot": "Getting out of bed",
"summary": "The description includes several separate actions; this sequence shows only the first, getting out of bed.",
"poses": [
{
"n": 1,
"label": "Lying under the duvet",
"kind": "key",
"preset": "lie",
"frames": 6,
"note": "The figure lies still under the duvet before the action begins."
},
{
"n": 2,
"label": "Throwing off the duvet",
"kind": "breakdown",
"preset": "arms_out",
"frames": 4,
"note": "The figure throws the duvet up and off the body."
},
{
"n": 3,
"label": "Swinging legs to the floor",
"kind": "breakdown",
"preset": "sit",
"frames": 5,
"note": "The figure sits up and swings both legs over onto the floor."
},
{
"n": 4,
"label": "Pushing up to stand",
"kind": "breakdown",
"preset": "lean_forward",
"frames": 9,
"note": "Weight shifts forward and the body rises up from sitting."
},
{
"n": 5,
"label": "Standing and stretching",
"kind": "key",
"preset": "stretch_tall",
"frames": 12,
"note": "Fully upright, the body stretches upward before holding still."
}
],
"principles": [
{
"id": "staging",
"poses": [
1,
5
],
"why": "Lying under the duvet and standing stretched both read clearly as the start and end of the action."
},
{
"id": "timing",
"poses": [
2,
3,
4
],
"why": "Short counts make tossing off the duvet and swinging the legs feel quick, while rising takes longer."
},
{
"id": "follow_through",
"poses": [
4,
5
],
"why": "After the body finishes rising, it keeps moving upward into the stretch."
}
],
"missing": [],
"splits": [
"Getting out of bed",
"Shuffling to the window",
"Opening the curtains"
],
"tip": "Drape a folded cloth over your figure as the duvet, then nudge its legs slightly between photos as it sits up and stands."
}
Build the input yourself
Load the engine in node (global.window = global; require("./twos.js"); require("./puppet.js")), then:
const K = TwosKit, A = TwosArt;
const built = K.buildInput({ move: "Sitting on a stool, the figure reaches up to a shelf and steps down onto the floor.", seconds: 2, fps: 12 });
built.input.target_frames; // "24" - 2 s at 12 fps
built.prescan.verdict; // the page's quick word read
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.sheet(reply.poses, 12) // one row per frame, the exposure sheet
// A.stage(reply.poses, 1) // the puppet stage at frame 1, as SVG
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://on-twos.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://on-twos.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"]
// on-twos.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://on-twos.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://on-twos.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
}
// OnTwos.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 OnTwos {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://on-twos.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://on-twos.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://on-twos.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://on-twos.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":"on-twos"}' | 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": "on-twos"})["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: "on-twos" })).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": "on-twos"}, 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", "on-twos"), 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: "on-twos" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "on-twos"])["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 = "on-twos" }))!["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": "pose", "move": "...", "seconds": "2", "fps": "12", "target_frames": "24", "page_actions": "1"} - 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": "pose", "move": "...", "seconds": "2", "fps": "12", "target_frames": "24", "page_actions": "1"} - 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: "pose", move, seconds, fps, target_frames, page_actions} (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 on-twos:<task>:<inputHash>:a<attempt> (the one task is pose), 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="on-twos:pose:$(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 = "on-twos:" + 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 = `on-twos:${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 := "on-twos:" + 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 = "on-twos:" + 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 = "on-twos:#{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 = "on-twos:" . $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 = $"on-twos:{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: on-twos:pose:$(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": "pose", "move": "...", "seconds": "2", "fps": "12", "target_frames": "24", "page_actions": "1"} - 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": "on-twos:pose:" + 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": `on-twos:pose:${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", "on-twos:pose:"+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", "on-twos:pose:" + 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" => "on-twos:pose:#{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: on-twos:pose:" . 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", $"on-twos:pose:{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);