Drive Called It from your own code
The one metered task, call, is available over HTTP. It takes what someone is about to sit through - the thing in their words, its kind (watching, going, a call or a trip), what they already know, the tone (gentle or cheeky), their own squares and a leave-out list - and returns the pool of bingo squares: 30 moments, each tagged with its odds (likely, maybe or longshot) and the elemental genre whose feeling it gives, plus a free centre square, a title, a one-line promise and a house rule.
The division of labour first: the page deals; the model writes. Which squares go on which card, where they sit, the balance of odds along every row, column and diagonal, the main genre of the blend and the line calls are all computed by CardKit - the model never places a square. After the reply, the page checks it against the input.
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": "..." } }
The token is minted for this app (the guest call takes {"slug":"called-it"}), 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, what, kind, tone) 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: writing the squares is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the rules
Every run sends task: call (the one task is call); a missing or different one is still answered as call. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always call. |
what | yes | What the person is about to sit through, in their words (up to 120 characters). |
kind | yes | watch, go, call or trip, followed by a short description, e.g. go - going to an event or a gathering in person. |
known | yes | What the person already knows about it (up to 240 characters), or none. |
tone | yes | gentle or cheeky, followed by a short description. |
own | yes | The person's own squares as numbered lines (1. Someone loses a shoe dancing), up to 12, or none. The page puts these on every card; the reply does not repeat them. |
avoid | yes | Topics to leave out, or none. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
Each square is one moment a player can see or hear happen, at most 32 characters, naming no one (only "someone", "everyone" or a role the input names), with no numbers, days, places or names the input never gave. Squares are about moments, never about a person's looks, age, money, faith, health or love life.
The reply
Exactly one JSON object:
{"task": "call", "title": "...", "promise": "...", "centre": "...", "house_rule": "...", "squares": [{"text": "...", "odds": "likely", "genre": "humor"}]}
| Field | What it holds |
|---|---|
squares[] | 30 objects: 10 likely, then 12 maybe, then 8 longshot. Each has text, odds and genre - one of wonder, idea, adventure, horror, mystery, thriller, humor, relationship, drama, issue, ensemble. |
centre | The free centre square: the near-certain moment. |
title | A name for the game, at most 40 characters. |
promise | One sentence: the feeling this kind of thing promises. |
house_rule | One sentence: when a square counts as marked. |
The page reads the reply with ReplyKit.normalize (unknown odds read as maybe, unknown genres as none), deals the cards with CardKit.dealAll and checks the reply with ReplyKit.check, which lists, under "The page's check", a square count or odds mix other than asked, unknown odds or genres, repeats (shown once on the cards), long squares, contact details (left off the cards), anything from the leave-out list, and numbers, days or capitalised names the input never gave.
Worked examples - every sample
These are the free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover all four kinds, both tones, things already known, the person's own squares and a leave-out list.
Romance film - cheeky tone
Input:
{
"task": "call",
"what": "a feel-good romance film we have both seen before",
"kind": "watch - watching a film, a show or a programme",
"known": "we always cry at the ending",
"tone": "cheeky - playful and teasing about the situation, never about a person",
"own": "none",
"avoid": "none"
}
Reply:
{
"task": "call",
"title": "Seen-It-Before Romance Bingo",
"promise": "Every twist is familiar, and watching it land again is still the fun part.",
"centre": "Everyone cries at the ending",
"house_rule": "A square counts once everyone watching agrees the moment clearly happened.",
"squares": [
{
"text": "The couple have a meet-cute",
"odds": "likely",
"genre": "relationship"
},
{
"text": "A mix-up drives the couple apart",
"odds": "likely",
"genre": "drama"
},
{
"text": "The score swells at a key moment",
"odds": "likely",
"genre": "wonder"
},
{
"text": "A best friend gives blunt advice",
"odds": "likely",
"genre": "relationship"
},
{
"text": "Someone recites a line early",
"odds": "likely",
"genre": "humor"
},
{
"text": "The couple almost kiss then stop",
"odds": "likely",
"genre": "relationship"
},
{
"text": "Someone attempts a grand gesture",
"odds": "likely",
"genre": "wonder"
},
{
"text": "Everyone says the line in unison",
"odds": "likely",
"genre": "ensemble"
},
{
"text": "Someone laughs ahead of a joke",
"odds": "likely",
"genre": "humor"
},
{
"text": "The camera lingers on an embrace",
"odds": "likely",
"genre": "wonder"
},
{
"text": "A rival causes some trouble",
"odds": "maybe",
"genre": "drama"
},
{
"text": "Someone overhears a private talk",
"odds": "maybe",
"genre": "mystery"
},
{
"text": "A note gets slipped to someone",
"odds": "maybe",
"genre": "mystery"
},
{
"text": "A friend plays matchmaker",
"odds": "maybe",
"genre": "humor"
},
{
"text": "The couple share a knowing look",
"odds": "maybe",
"genre": "relationship"
},
{
"text": "Someone turns back to try again",
"odds": "maybe",
"genre": "drama"
},
{
"text": "A door slams during an argument",
"odds": "maybe",
"genre": "drama"
},
{
"text": "Someone apologises at the door",
"odds": "maybe",
"genre": "relationship"
},
{
"text": "A montage covers time passing",
"odds": "maybe",
"genre": "relationship"
},
{
"text": "The couple hold hands at last",
"odds": "maybe",
"genre": "relationship"
},
{
"text": "Everyone cheers at the reunion",
"odds": "maybe",
"genre": "ensemble"
},
{
"text": "A rival loses out in the end",
"odds": "maybe",
"genre": "drama"
},
{
"text": "Someone chases a departing ride",
"odds": "longshot",
"genre": "thriller"
},
{
"text": "A stranger gives perfect advice",
"odds": "longshot",
"genre": "wonder"
},
{
"text": "The credits hide an extra scene",
"odds": "longshot",
"genre": "mystery"
},
{
"text": "A side character steals a scene",
"odds": "longshot",
"genre": "humor"
},
{
"text": "Someone forgives the plot hole",
"odds": "longshot",
"genre": "issue"
},
{
"text": "Someone applauds the final scene",
"odds": "longshot",
"genre": "wonder"
},
{
"text": "Someone re-explains a side plot",
"odds": "longshot",
"genre": "idea"
},
{
"text": "A flashback reveals old history",
"odds": "longshot",
"genre": "mystery"
}
]
}
Wedding reception - gentle tone
Input:
{
"task": "call",
"what": "my cousin's wedding reception",
"kind": "go - going to an event or a gathering in person",
"known": "there will be speeches and a ceilidh band",
"tone": "gentle - fond and kind - nobody is the butt of the joke",
"own": "1. Someone loses a shoe dancing",
"avoid": "exes"
}
Reply:
{
"task": "call",
"title": "Our Cousin's Wedding Bingo",
"promise": "A warm gathering of speeches, ceilidh dancing, and small moments worth catching awaits you.",
"centre": "Someone sways to the music",
"house_rule": "Mark a square the moment everyone present agrees it clearly happened, no earlier.",
"squares": [
{
"text": "A speech gets a laugh",
"odds": "likely",
"genre": "humor"
},
{
"text": "The band strikes up a tune",
"odds": "likely",
"genre": "wonder"
},
{
"text": "Someone claps on the wrong beat",
"odds": "likely",
"genre": "humor"
},
{
"text": "Everyone joins a group dance",
"odds": "likely",
"genre": "ensemble"
},
{
"text": "Someone forgets a dance step",
"odds": "likely",
"genre": "humor"
},
{
"text": "A phone comes out to film it",
"odds": "likely",
"genre": "adventure"
},
{
"text": "Someone is pulled onto the floor",
"odds": "likely",
"genre": "humor"
},
{
"text": "The dance floor fills quickly",
"odds": "likely",
"genre": "ensemble"
},
{
"text": "Someone glances at a watch",
"odds": "likely",
"genre": "thriller"
},
{
"text": "A speech runs long",
"odds": "likely",
"genre": "thriller"
},
{
"text": "The music slows for a moment",
"odds": "maybe",
"genre": "relationship"
},
{
"text": "Someone learns the reel steps",
"odds": "maybe",
"genre": "adventure"
},
{
"text": "Confetti appears for an entrance",
"odds": "maybe",
"genre": "wonder"
},
{
"text": "Someone calls for a photo",
"odds": "maybe",
"genre": "ensemble"
},
{
"text": "A group photo takes a long time",
"odds": "maybe",
"genre": "humor"
},
{
"text": "Someone wipes away a tear",
"odds": "maybe",
"genre": "drama"
},
{
"text": "Someone laughs at an old joke",
"odds": "maybe",
"genre": "humor"
},
{
"text": "A speech loses its place",
"odds": "maybe",
"genre": "drama"
},
{
"text": "Everyone claps along together",
"odds": "maybe",
"genre": "ensemble"
},
{
"text": "Someone steps out for air",
"odds": "maybe",
"genre": "drama"
},
{
"text": "The band takes a short break",
"odds": "maybe",
"genre": "mystery"
},
{
"text": "The microphone squeals loudly",
"odds": "maybe",
"genre": "horror"
},
{
"text": "Someone does the splits dancing",
"odds": "longshot",
"genre": "wonder"
},
{
"text": "The whole room falls silent",
"odds": "longshot",
"genre": "mystery"
},
{
"text": "A singalong breaks out",
"odds": "longshot",
"genre": "ensemble"
},
{
"text": "Someone attempts a dance lift",
"odds": "longshot",
"genre": "adventure"
},
{
"text": "Someone does an impromptu solo",
"odds": "longshot",
"genre": "adventure"
},
{
"text": "The ceilidh band gets an encore",
"odds": "longshot",
"genre": "wonder"
},
{
"text": "A heel gets caught in a gap",
"odds": "longshot",
"genre": "humor"
},
{
"text": "The last dance runs long",
"odds": "longshot",
"genre": "thriller"
}
]
}
All-hands call - cheeky tone
Input:
{
"task": "call",
"what": "the monthly all-hands video call",
"kind": "call - sitting through a call or a meeting",
"known": "it always starts late and the boss shares the slides",
"tone": "cheeky - playful and teasing about the situation, never about a person",
"own": "none",
"avoid": "layoffs"
}
Reply:
{
"task": "call",
"title": "Mute Yourself: All-Hands Bingo",
"promise": "You're about to sit through a call that tends to start late and lean on the slides.",
"centre": "Someone asks can everyone hear",
"house_rule": "A square counts the moment players agree they all just saw or heard it happen.",
"squares": [
{
"text": "Call starts later than planned",
"odds": "likely",
"genre": "humor"
},
{
"text": "The boss shares the slides",
"odds": "likely",
"genre": "idea"
},
{
"text": "Someone is told they are on mute",
"odds": "likely",
"genre": "humor"
},
{
"text": "A long pause follows a question",
"odds": "likely",
"genre": "thriller"
},
{
"text": "The chat fills with greetings",
"odds": "likely",
"genre": "ensemble"
},
{
"text": "Someone asks can you see this",
"odds": "likely",
"genre": "humor"
},
{
"text": "The agenda runs past its time",
"odds": "likely",
"genre": "issue"
},
{
"text": "A slide takes a moment to load",
"odds": "likely",
"genre": "humor"
},
{
"text": "An echo comes through the call",
"odds": "likely",
"genre": "humor"
},
{
"text": "Everyone is thanked at the close",
"odds": "likely",
"genre": "relationship"
},
{
"text": "Someone forgets to unmute first",
"odds": "maybe",
"genre": "humor"
},
{
"text": "A video feed freezes mid word",
"odds": "maybe",
"genre": "humor"
},
{
"text": "The call drops and reconnects",
"odds": "maybe",
"genre": "thriller"
},
{
"text": "No hands go up for the question",
"odds": "maybe",
"genre": "mystery"
},
{
"text": "A join alert sounds mid call",
"odds": "maybe",
"genre": "ensemble"
},
{
"text": "A virtual background glitches",
"odds": "maybe",
"genre": "humor"
},
{
"text": "Reaction emojis pop up in chat",
"odds": "maybe",
"genre": "ensemble"
},
{
"text": "Someone arrives after the start",
"odds": "maybe",
"genre": "humor"
},
{
"text": "Someone reads off a script",
"odds": "maybe",
"genre": "drama"
},
{
"text": "A shoutout mentions a recent win",
"odds": "maybe",
"genre": "ensemble"
},
{
"text": "A poll result appears onscreen",
"odds": "maybe",
"genre": "idea"
},
{
"text": "A camera stays off throughout",
"odds": "maybe",
"genre": "mystery"
},
{
"text": "The call starts exactly on time",
"odds": "longshot",
"genre": "wonder"
},
{
"text": "Every camera stays on throughout",
"odds": "longshot",
"genre": "ensemble"
},
{
"text": "A genuine laugh breaks the quiet",
"odds": "longshot",
"genre": "humor"
},
{
"text": "The slides run without a hitch",
"odds": "longshot",
"genre": "wonder"
},
{
"text": "The connection stays steady",
"odds": "longshot",
"genre": "wonder"
},
{
"text": "Every voice comes through clear",
"odds": "longshot",
"genre": "wonder"
},
{
"text": "A tricky question gets answered",
"odds": "longshot",
"genre": "idea"
},
{
"text": "The chat stays quiet throughout",
"odds": "longshot",
"genre": "mystery"
}
]
}
Train journey - gentle tone
Input:
{
"task": "call",
"what": "a long train journey to visit family",
"kind": "trip - a journey or a day out",
"known": "none",
"tone": "gentle - fond and kind - nobody is the butt of the joke",
"own": "1. A dog boards the train\n2. Someone eats an egg sandwich",
"avoid": "none"
}
Reply:
{
"task": "call",
"title": "The Long Way To Family",
"promise": "A long, gentle ride unfolds, full of small sights, quiet waits, and the slow pull toward family.",
"centre": "Family comes into view at last",
"house_rule": "A square only counts once it clearly happens and everyone playing agrees they saw it too.",
"squares": [
{
"text": "A ticket gets checked again",
"odds": "likely",
"genre": "humor"
},
{
"text": "A bag goes on the overhead rack",
"odds": "likely",
"genre": "adventure"
},
{
"text": "A reserved seat is already taken",
"odds": "likely",
"genre": "thriller"
},
{
"text": "The next stop gets announced",
"odds": "likely",
"genre": "adventure"
},
{
"text": "A window seat is claimed first",
"odds": "likely",
"genre": "humor"
},
{
"text": "Someone naps between stops",
"odds": "likely",
"genre": "humor"
},
{
"text": "The trolley comes down the aisle",
"odds": "likely",
"genre": "humor"
},
{
"text": "A view opens up past the window",
"odds": "likely",
"genre": "wonder"
},
{
"text": "Someone checks how long is left",
"odds": "likely",
"genre": "thriller"
},
{
"text": "A wave goes up on the platform",
"odds": "likely",
"genre": "relationship"
},
{
"text": "A seat gets shared with someone",
"odds": "maybe",
"genre": "relationship"
},
{
"text": "A queue forms outside the toilet",
"odds": "maybe",
"genre": "humor"
},
{
"text": "Someone pulls out a book to read",
"odds": "maybe",
"genre": "idea"
},
{
"text": "The train slows for no reason",
"odds": "maybe",
"genre": "mystery"
},
{
"text": "A headphone leaks a faint sound",
"odds": "maybe",
"genre": "humor"
},
{
"text": "A call drops and is redialed",
"odds": "maybe",
"genre": "thriller"
},
{
"text": "A door sticks between carriages",
"odds": "maybe",
"genre": "humor"
},
{
"text": "Someone rechecks which platform",
"odds": "maybe",
"genre": "thriller"
},
{
"text": "A delay gets announced",
"odds": "maybe",
"genre": "thriller"
},
{
"text": "Someone points out a landmark",
"odds": "maybe",
"genre": "wonder"
},
{
"text": "A charger gets passed along",
"odds": "maybe",
"genre": "relationship"
},
{
"text": "Someone starts packing up early",
"odds": "maybe",
"genre": "adventure"
},
{
"text": "A lost item gets handed back",
"odds": "longshot",
"genre": "relationship"
},
{
"text": "Someone strikes up a long chat",
"odds": "longshot",
"genre": "relationship"
},
{
"text": "The whole carriage goes quiet",
"odds": "longshot",
"genre": "mystery"
},
{
"text": "The train crosses a long bridge",
"odds": "longshot",
"genre": "wonder"
},
{
"text": "Someone spots a familiar face",
"odds": "longshot",
"genre": "mystery"
},
{
"text": "The train arrives ahead of time",
"odds": "longshot",
"genre": "adventure"
},
{
"text": "A round of applause breaks out",
"odds": "longshot",
"genre": "ensemble"
},
{
"text": "A long tunnel swallows the view",
"odds": "longshot",
"genre": "wonder"
}
]
}
Build the input yourself
Load the engine in node (global.window = global; require("./cardkit.js"); require("./replykit.js")), then:
const K = CardKit, R = ReplyKit;
const form = K.form({ what: "the monthly all-hands video call", kind: "call", known: "it always starts late",
tone: "cheeky", own: "", avoid: "", players: "4" });
const built = K.buildInput(form);
// built.ok is false, with built.note, until the thing is named and no square carries contact details
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// K.dealAll(form, reply.squares, reply.centre, 0).cards // one balanced 5x5 card per player
// R.check(reply, { form: form, input: built.input }).notes // the page's check of the reply
Form keys: what (up to 120 characters), kind - watch, go, call or trip - known (up to 240, optional), tone - gentle or cheeky - own (one per line, up to 12, optional), avoid (optional) and players from 1 to 8 (page only; never sent).
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://called-it.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://called-it.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"]
// called-it.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://called-it.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://called-it.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
}
// CalledIt.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 CalledIt {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://called-it.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://called-it.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://called-it.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://called-it.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":"called-it"}' | 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": "called-it"})["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: "called-it" })).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": "called-it"}, 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", "called-it"), 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: "called-it" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "called-it"])["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 = "called-it" }))!["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": "call", "what": "the monthly all-hands video call", "kind": "call - sitting through a call or a meeting", "known": "it always starts late and the boss shares the slides", "tone": "cheeky - ...", "own": "none", "avoid": "layoffs"}
# - 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": "call", "what": "the monthly all-hands video call", "kind": "call - sitting through a call or a meeting", "known": "it always starts late and the boss shares the slides", "tone": "cheeky - ...", "own": "none", "avoid": "layoffs"}
# - 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: "call", what, kind, known, tone, own, avoid} (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 called-it:<task>:<inputHash>:a<attempt> (the one task is call), 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="called-it:call:$(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 = "called-it:" + 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("title"), reply.get("squares"))
import { createHash } from "node:crypto";
const key = `called-it:${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.title, reply.squares);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "called-it:" + 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 = "called-it:" + 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 = "called-it:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"], reply["squares"]].inspect
$key = "called-it:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["title"] ?? ""), " ", ($reply["title"] ?? ""), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"called-it:{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: called-it:call:$(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": "call", "what": "the monthly all-hands video call", "kind": "call - sitting through a call or a meeting", "known": "it always starts late and the boss shares the slides", "tone": "cheeky - ...", "own": "none", "avoid": "layoffs"}
# - 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": "called-it:call:" + 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": `called-it:call:${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", "called-it:call:"+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", "called-it:call:" + 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" => "called-it:call:#{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: called-it:call:" . 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", $"called-it:call:{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);