Drive Stick the Landing from your own code
The one metered task, routine, is available over HTTP. It takes a tone (deadpan, warm or stern), an optional title and the routine, three to eight moves as n | move with the landing last. It returns every move's demands, value and a reason, the faults (move, size, lens, an exact quote and a note), the landing call with its quote and reason, a remark from each of the five judges, one tidy-up and a sign-off.
The division of labour first: the page numbers the moves and lists rough demand and mishap word hints; the model reads every move for its demands, marks faults and calls the landing; the page then does all the arithmetic (values, the connection bonus, five judges with the high and low dropped, the total), works each judge's move and the tidy-up move again, draws the beam and the cards, and re-checks the reply. The model never writes a score.
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":"stick-the-landing"}), 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, tone, title, moves, page_facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: calling the panel 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 status
Every run sends task: routine (the one task is routine); a missing or different one is still answered as routine. The status field holds one of two ids, checked in this order: declined when a move names a specific real person, the feat is risky or unlawful (riding or driving while doing something else, no hands on a bike, heights, fire, knives, alcohol, drugs, anything that risks a person or an animal), a baby, a child or an animal is part of the feat, or it is sexual, hateful or about self-harm; otherwise ok.
For ok, every move gets its demands from five ids - carry, footing, both (at the same time, or a heel, hip, elbow, knee, teeth or chin doing a job), clock, aim - and a value from their count: none is A, one B, two C, three D, four or five E. Faults go only on moves before the landing, at most one per move, sized in order fall, large, medium, small, with a judge's lens (precision, form, rhythm, composure, else amplitude) and an exact quote. The landing is the first that fits the last move: fall, hop, step, else stuck.
The five judges sit in order E1 form, E2 rhythm, E3 precision, E4 composure, E5 commitment (amplitude); each judge's line is the lowest move with a fault in that lens, else 0. The tidy-up's line is the last move when the landing is not stuck, otherwise the move with the biggest fault (lowest move on a tie), otherwise 0 with an empty tip.
Input (every field is a string)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always routine. |
tone | yes | deadpan, warm or stern. |
title | yes | The person's own name for the feat, or "" (then the model names it from the moves' words). |
moves | yes | 3 to 8 lines as n | move, numbered from 1; the last is the landing. Moves are cut at 100 characters, repeats dropped, leading bullets and numbers removed and pipes become slashes. |
page_facts | yes | One key | value per line: moves, landing_line, then the hints demand_hints, mishap_words, harm_words and capitalised_words. |
clip_note | no | What the page left out, if anything. |
retry_note | no | Only on the page's one reformat retry, after an unusable reply. |
Reply (one JSON object)
{"task": "routine", "status": "ok", "reason": "", "title": "...",
"elements": [{"line": 1, "demands": ["carry"], "value": "B", "why": "..."}, ...],
"faults": [{"line": 2, "size": "medium", "lens": "precision", "quote": "...", "note": "..."}, ...],
"landing": "step", "landing_quote": "...", "landing_why": "...",
"judges": [{"seat": "E1", "line": 0, "remark": "..."}, ...],
"tidy": {"line": 6, "tip": "..."}, "closing": "..."}
| Field | What it holds |
|---|---|
status, reason | The status by the order above; for declined, at most 30 words (empty for ok). |
title | The input title word for word, or 2 to 5 words whose longer words come from the moves. |
elements | One per move ([] when declined): line, demands, value and why (at most 14 words). |
faults | In move order: line, size, lens, an exact quote from that move and a note (at most 14 words). |
landing, landing_quote, landing_why | The call, an exact quote from the last move ("" when stuck) and at most 20 words of reason. |
judges | Exactly 5, seats E1 to E5: line by the rule above and a remark of at most 12 words. |
tidy, closing | The tidy-up's move and tip (at most 20 words), and a sign-off of at most 16 words. |
The reply carries no scores. The page computes them (LandKit.score): D is the sum of the values (A 0.1 to E 0.5) plus 0.1 for each neighbouring pair both valued C or more with no fault on either; each judge takes off 0.1, 0.3, 0.5 or 1.0 per fault by size, 0.1 more on a fault in their own lens (not on a fall), plus the landing (0, 0.1, 0.3 or 1.0); the highest and the lowest judge are dropped and E is 10 minus the mean of the other three. Then it checks the reply (LandKit.check): one element per move, each value against its demand count, faults only before the landing and quoted from their own move, the landing quote, every judge's move, the tidy-up move, the word limits, no score talk, no number the moves never give, no he or she and no internal field id in the prose. Anything that disagrees is listed under the reply; nothing is silently corrected.
Worked examples
Each example is the exact input the page sends for one of its five samples, and the saved reply it replays for free.
A tray of mugs - stuck
Input:
{
"task": "routine",
"tone": "warm",
"title": "",
"moves": "1 | picked up the tray with four full mugs of tea\n2 | pushed the kitchen door open with my hip\n3 | stepped over a shoe in the hallway\n4 | one mug sloshed onto the tray but nothing reached the floor\n5 | lowered the tray onto the low table and let go",
"page_facts": "moves | 5\nlanding_line | 5\ndemand_hints | 1: carry; 2: both; 3: footing; 4: carry; 5: carry\nmishap_words | 1: none; 2: none; 3: none; 4: sloshed; 5: none\nharm_words | none\ncapitalised_words | none"
}
Reply:
{
"task": "routine",
"status": "ok",
"reason": "",
"title": "The Full Tray Lowered",
"elements": [
{
"line": 1,
"demands": [
"carry"
],
"value": "B",
"why": "You picked up the tray with four full mugs - that's carrying."
},
{
"line": 2,
"demands": [
"both"
],
"value": "B",
"why": "Using your hip, not your hand, to push the kitchen door open."
},
{
"line": 3,
"demands": [
"footing"
],
"value": "B",
"why": "Stepping over the shoe in the hallway is the footing moment."
},
{
"line": 4,
"demands": [],
"value": "A",
"why": "This move's words name no demand - just the mug that sloshed."
},
{
"line": 5,
"demands": [
"carry"
],
"value": "B",
"why": "Lowering the tray onto the low table is still carrying it."
}
],
"faults": [
{
"line": 4,
"size": "medium",
"lens": "precision",
"quote": "sloshed onto the tray",
"note": "The mug sloshed onto the tray, and nothing reached the floor."
}
],
"landing": "stuck",
"landing_quote": "",
"landing_why": "The tray is lowered onto the low table and let go, with no mishap named.",
"judges": [
{
"seat": "E1",
"line": 0,
"remark": "This seat takes nothing off for body control."
},
{
"seat": "E2",
"line": 0,
"remark": "This seat takes nothing off for the rhythm."
},
{
"seat": "E3",
"line": 4,
"remark": "Move 4 shows a mug that sloshed onto the tray."
},
{
"seat": "E4",
"line": 0,
"remark": "This seat takes nothing off for staying calm."
},
{
"seat": "E5",
"line": 0,
"remark": "This seat takes nothing off for commitment."
}
],
"tidy": {
"line": 4,
"tip": "Keep the tray flatter so the mug moves less."
},
"closing": "That tray made it all the way to the low table."
}
The shopping in one trip - a step
Input:
{
"task": "routine",
"tone": "deadpan",
"title": "",
"moves": "1 | hooked six shopping bags over both arms at the car boot\n2 | shut the boot with my elbow\n3 | climbed the three front steps without stopping\n4 | fished the keys out of my pocket while holding every bag\n5 | the bags swung and I wobbled on the doormat\n6 | set every bag on the kitchen counter and took one small step back",
"page_facts": "moves | 6\nlanding_line | 6\ndemand_hints | 1: carry; 2: both; 3: footing; 4: carry, both; 5: carry; 6: carry\nmishap_words | 1: none; 2: none; 3: none; 4: none; 5: wobbled; 6: none\nharm_words | none\ncapitalised_words | none"
}
Reply:
{
"task": "routine",
"status": "ok",
"reason": "",
"title": "Bags Over Boot",
"elements": [
{
"line": 1,
"demands": [
"carry"
],
"value": "B",
"why": "The bags hooked over both arms show carrying."
},
{
"line": 2,
"demands": [
"both"
],
"value": "B",
"why": "Shutting the boot with an elbow, not a hand."
},
{
"line": 3,
"demands": [
"footing"
],
"value": "B",
"why": "Climbing three front steps without stopping needs footing."
},
{
"line": 4,
"demands": [
"carry",
"both"
],
"value": "C",
"why": "Holding every bag while fishing keys shows carrying at the same time."
},
{
"line": 5,
"demands": [
"carry"
],
"value": "B",
"why": "The bags swung on the doormat, showing carrying."
},
{
"line": 6,
"demands": [
"carry"
],
"value": "B",
"why": "Setting every bag on the kitchen counter shows carrying."
}
],
"faults": [
{
"line": 5,
"size": "medium",
"lens": "form",
"quote": "I wobbled",
"note": "The bags swung and you wobbled on the doormat."
}
],
"landing": "step",
"landing_quote": "one small step back",
"landing_why": "Setting every bag on the kitchen counter ends with one small step back.",
"judges": [
{
"seat": "E1",
"line": 5,
"remark": "This seat has the doormat wobble in its sights."
},
{
"seat": "E2",
"line": 0,
"remark": "This seat takes nothing off for rhythm."
},
{
"seat": "E3",
"line": 0,
"remark": "This seat takes nothing off for precision."
},
{
"seat": "E4",
"line": 0,
"remark": "This seat takes nothing off for composure."
},
{
"seat": "E5",
"line": 0,
"remark": "This seat takes nothing off for commitment."
}
],
"tidy": {
"line": 6,
"tip": "Step back slowly once every bag is down on the kitchen counter."
},
"closing": "This one lands as a step, says the panel."
}
A duvet cover change - a hop
Input:
{
"task": "routine",
"tone": "stern",
"title": "Duvet cover change",
"moves": "1 | turned the duvet cover inside out\n2 | reached inside and took hold of the two far corners\n3 | shook the duvet down in one go\n4 | had to stop halfway and start the shake again\n5 | flapped it out over the bed and staggered two steps back",
"page_facts": "moves | 5\nlanding_line | 5\ndemand_hints | 1: none; 2: carry; 3: none; 4: none; 5: footing\nmishap_words | 1: none; 2: none; 3: none; 4: had to stop; 5: staggered\nharm_words | none\ncapitalised_words | none"
}
Reply:
{
"task": "routine",
"status": "ok",
"reason": "",
"title": "Duvet cover change",
"elements": [
{
"line": 1,
"demands": [],
"value": "A",
"why": "Turning the duvet cover inside out states no demand."
},
{
"line": 2,
"demands": [
"carry"
],
"value": "B",
"why": "Taking hold of the two far corners states a clear carry."
},
{
"line": 3,
"demands": [],
"value": "A",
"why": "Shaking the duvet down in one go states no demand."
},
{
"line": 4,
"demands": [],
"value": "A",
"why": "Stopping and starting the shake again states no demand here."
},
{
"line": 5,
"demands": [
"footing"
],
"value": "B",
"why": "Staggering two steps back states a footing demand."
}
],
"faults": [
{
"line": 4,
"size": "large",
"lens": "rhythm",
"quote": "had to stop halfway and start the shake again",
"note": "You stopped halfway through the shake and started it again."
}
],
"landing": "hop",
"landing_quote": "staggered two steps back",
"landing_why": "Flapping it out and staggering two steps back is more than a small slip.",
"judges": [
{
"seat": "E1",
"line": 0,
"remark": "This seat takes nothing off for form."
},
{
"seat": "E2",
"line": 4,
"remark": "Restarting the shake at move 4 broke the flow."
},
{
"seat": "E3",
"line": 0,
"remark": "This seat takes nothing off for precision."
},
{
"seat": "E4",
"line": 0,
"remark": "This seat takes nothing off for composure."
},
{
"seat": "E5",
"line": 0,
"remark": "This seat takes nothing off for commitment."
}
],
"tidy": {
"line": 5,
"tip": "Check where your feet are before stepping back after flapping it out."
},
"closing": "The stagger back at the end stands as the call."
}
Recycling boxes down the stairs - a fall
Input:
{
"task": "routine",
"tone": "deadpan",
"title": "",
"moves": "1 | stacked three recycling boxes up to my chin\n2 | went down the stairs sideways\n3 | paused halfway down to shift my grip\n4 | the top box tipped and I caught it with my knee\n5 | at the bottom the whole stack slid out of my arms onto the hall floor",
"page_facts": "moves | 5\nlanding_line | 5\ndemand_hints | 1: carry, both; 2: footing; 3: none; 4: carry, both, aim; 5: carry\nmishap_words | 1: none; 2: none; 3: paused; 4: tipped; 5: slid out, slid\nharm_words | none\ncapitalised_words | none"
}
Reply:
{
"task": "routine",
"status": "ok",
"reason": "",
"title": "Boxes Down The Stairs",
"elements": [
{
"line": 1,
"demands": [
"carry",
"both"
],
"value": "C",
"why": "Carrying three stacked boxes, braced under your chin as well."
},
{
"line": 2,
"demands": [
"footing"
],
"value": "B",
"why": "Stairs taken sideways, nothing else stated in this move."
},
{
"line": 3,
"demands": [],
"value": "A",
"why": "This move's own words add nothing further here."
},
{
"line": 4,
"demands": [
"carry",
"both",
"aim"
],
"value": "D",
"why": "Caught the tipping box with your knee, holding it at the same time."
},
{
"line": 5,
"demands": [
"carry"
],
"value": "B",
"why": "The stack was held until it slid from your arms."
}
],
"faults": [
{
"line": 3,
"size": "small",
"lens": "rhythm",
"quote": "paused halfway down",
"note": "A pause halfway down to adjust your grip."
},
{
"line": 4,
"size": "medium",
"lens": "precision",
"quote": "box tipped",
"note": "The top box tipped before being caught with your knee."
}
],
"landing": "fall",
"landing_quote": "onto the hall floor",
"landing_why": "The whole stack left your arms and ended up on the hall floor.",
"judges": [
{
"seat": "E1",
"line": 0,
"remark": "Nothing taken off here; the landing call remains a fall."
},
{
"seat": "E2",
"line": 3,
"remark": "Move 3 shows a hitch partway down while resetting your grip."
},
{
"seat": "E3",
"line": 4,
"remark": "Move 4 needed your knee to stop the top box tipping."
},
{
"seat": "E4",
"line": 0,
"remark": "No reaction-based faults here; the landing call remains a fall."
},
{
"seat": "E5",
"line": 0,
"remark": "Nothing held back here; the landing call remains a fall."
}
],
"tidy": {
"line": 5,
"tip": "Wrap both arms fully around the stack instead of balancing it loosely."
},
"closing": "The stack ending up on the hall floor is the call."
}
A no-hands bike ride - declined
Input:
{
"task": "routine",
"tone": "warm",
"title": "",
"moves": "1 | rode my bike down the high street with no hands\n2 | texted my sister while steering with my knees\n3 | coasted through the lights as they turned red",
"page_facts": "moves | 3\nlanding_line | 3\ndemand_hints | 1: none; 2: both; 3: none\nmishap_words | 1: none; 2: none; 3: none\nharm_words | steering, no hands\ncapitalised_words | none"
}
Reply:
{
"task": "routine",
"status": "declined",
"reason": "This panel only takes on low-risk, everyday moments, so this one's out of bounds - try sending a calmer routine instead.",
"title": "",
"elements": [],
"faults": [],
"landing": "",
"landing_quote": "",
"landing_why": "",
"judges": [],
"tidy": {
"line": 0,
"tip": ""
},
"closing": "No worries - bring the panel something steadier and it'll gladly take a look."
}
Build the input yourself
Load the engine in node (global.window = global; require("./landkit.js"); require("./beamdraw.js")), then:
const K = LandKit, D = BeamDraw;
const built = K.buildInput({ tone: "deadpan", title: "",
moves: "hooked six bags over both arms
shut the boot with my elbow
set every bag on the counter and let go" });
built.parsed.lines; // [{n: 1, text: "hooked six bags over both arms"}, ...]
built.prescan.demands; // [["carry"], ["both"], ["carry"]]
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.score(K.modelOf(reply)) // D, E, the five judges (high and low dropped) and the total
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply, reply.title) // the scorecard as Markdown
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://stick-the-landing.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://stick-the-landing.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"]
// stick-the-landing.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://stick-the-landing.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://stick-the-landing.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
}
// StickTheLanding.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 StickTheLanding {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://stick-the-landing.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://stick-the-landing.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://stick-the-landing.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://stick-the-landing.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":"stick-the-landing"}' | 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": "stick-the-landing"})["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: "stick-the-landing" })).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": "stick-the-landing"}, 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", "stick-the-landing"), 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: "stick-the-landing" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "stick-the-landing"])["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 = "stick-the-landing" }))!["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": "routine", "tone": "deadpan", "title": "", "moves": "1 | move\n2 | move\n3 | landing", "page_facts": "moves | 3\n..."} - 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": "routine", "tone": "deadpan", "title": "", "moves": "1 | move\n2 | move\n3 | landing", "page_facts": "moves | 3\n..."} - 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: "routine", tone, title, moves, page_facts} (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 stick-the-landing:<task>:<inputHash>:a<attempt> (the one task is routine), 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="stick-the-landing:routine:$(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 = "stick-the-landing:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("status"))
import { createHash } from "node:crypto";
const key = `stick-the-landing:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.status);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "stick-the-landing:" + 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 = "stick-the-landing:" + 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 = "stick-the-landing:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["status"]].inspect
$key = "stick-the-landing:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["status"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"stick-the-landing:{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: stick-the-landing:routine:$(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": "routine", "tone": "deadpan", "title": "", "moves": "1 | move\n2 | move\n3 | landing", "page_facts": "moves | 3\n..."} - 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": "stick-the-landing:routine:" + 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": `stick-the-landing:routine:${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", "stick-the-landing:routine:"+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", "stick-the-landing:routine:" + 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" => "stick-the-landing:routine:#{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: stick-the-landing:routine:" . 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", $"stick-the-landing:routine:{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);