Drive They Served from your own code
The metered reply is available over HTTP. It takes what a family knows about someone who served - the name, the relationship, the pronoun, where the tribute goes, its length and tone, every fact with the pile the page sorted it into, and the page's counts - and returns one JSON object: a verdict, a title, a line for the plaque, the tribute, a note on how each fact was used, the questions to ask and what to check before sharing.
The division of labour first: the page owns the sorting and the rule; the model owns the words; the page re-checks the reply. The piles, the pronoun and the verdict rule come from servekit.js, which you can load in node to build the input exactly as the page does (see Build the input yourself).
Get a token on the token page (step 2 below also shows how to mint a guest token in code).
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "status": 402, "details": { ... } } }
The token is minted for this app (the guest call takes {"slug":"they-served"}), 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 (name, pronouns, words_min, words_max, facts, figures) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the reply 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 verdict rule
There is one task, so no task field is sent. The verdict is decided by a fixed rule on the page's counts, checked in this order - the first that applies wins:
check_first- nothing is on record (figures.recordsis"0").mostly_stories- family stories outnumber the facts on record.on_record- everything else.
A fact's pile is the page's reading of its wording: a question, or a line that says nobody knows, is unknown; a line where a document says something ("discharge papers say...") is record; a line that says who told it or hedges ("Grandma said", "apparently", "Dad thinks") is story; a line that names papers, a photo, a medal or a letter is record; anything else is treated as story. A tag at the end of a line (", record", ", story", ", unknown") overrides it. The declared input schema carries scalars only, so the array- and object-valued fields are JSON-encoded strings.
Input fields
| Field | Type | Notes |
|---|---|---|
name, relation | string | What the family calls the person (required) and who they are to you. |
pronouns | string | he, she, they or name only. Required. |
purpose, purpose_label | string | post, school, program or family, and its plain label. |
length, words_min, words_max | string | short (50-80), standard (100-150) or long (170-230); the bounds as strings. Required. |
tone | string | plain, warm or formal. |
facts | string (JSON) | Up to 10 facts, in order: [{fact, source, source_label}]; source is record, story or unknown. Required. |
figures | string (JSON) | {verdict, records, stories, unknowns, total}, every value a string. Required. |
wishes | string | Your note (something to leave out, or how it will be read), or empty; past 400 characters the opening and closing are kept with the middle marked [...]. |
clip_note, retry_note | string | What the page cut, when it cut something; and only on a reformat retry, what was wrong. |
The reply
The job's output.output is a string holding one JSON object: verdict (check_first | mostly_stories | on_record), title, plaque (one line, at most 12 words), tribute (between words_min and words_max words, paragraphs separated by a blank line), facts (one {fact, use, how} per input fact, in order, the fact copied exactly; use is stated, framed or left_out), questions (up to 6) and before_sharing. The page re-checks every reply: the verdict against the rule, one note per fact in order, no not-known fact used and no family story stated as plain fact, the tribute's length, every number and year from the facts, no hero, sacrifice or battle words the facts do not use, no added frequencies or feelings, the pronoun, and no internal field names in the prose.
Worked examples
Replies produced against the app's prompt for the four samples (every name and fact is made up; the full replies ship in example-result.js). Together they cover every verdict and every pile.
Grandad - on record, four facts on record, two family stories and one question
Input:
{
"name": "Grandad Joe",
"relation": "my grandfather",
"pronouns": "he",
"purpose": "post",
"purpose_label": "a short post for 11 November",
"length": "standard",
"words_min": "100",
"words_max": "150",
"tone": "warm",
"facts": "[{\"fact\":\"Discharge papers say he served in the Royal Navy from 1943 to 1946\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"A photo of him in uniform, dated 1944 on the back\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"His service medals are in a tin in Mum's wardrobe\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"His record card lists him as a radio operator\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"Grandma said he never talked about the war, only about the food\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"He used to whistle the same tune while washing up\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"Which ship was he on?\",\"source\":\"unknown\",\"source_label\":\"not known (a gap or a question)\"}]",
"figures": "{\"verdict\":\"on_record\",\"records\":\"4\",\"stories\":\"2\",\"unknowns\":\"1\",\"total\":\"7\"}",
"wishes": ""
}
Reply:
{
"verdict": "on_record",
"title": "Remembering Grandad Joe",
"plaque": "Royal Navy radio operator, 1943 to 1946",
"tribute": "My grandfather, Grandad Joe, served in the Royal Navy from 1943 to 1946, which is what his discharge papers say. There is a photo of him in uniform, dated 1944 on the back. His record card lists him as a radio operator, and his service medals are in a tin in Mum's wardrobe.\n\nAlongside what is on record, the family also keeps stories about him, each one told rather than checked. Grandma said he never talked about the war, only about the food. He also used to whistle the same tune while he was washing up, or so it is remembered.",
"facts": [
{
"fact": "Discharge papers say he served in the Royal Navy from 1943 to 1946",
"use": "stated",
"how": "Opens the tribute, stated plainly as the record shows."
},
{
"fact": "A photo of him in uniform, dated 1944 on the back",
"use": "stated",
"how": "Stated plainly, placed right after the service dates."
},
{
"fact": "His service medals are in a tin in Mum's wardrobe",
"use": "stated",
"how": "Stated plainly alongside the record card detail."
},
{
"fact": "His record card lists him as a radio operator",
"use": "stated",
"how": "Stated plainly, giving his role on record."
},
{
"fact": "Grandma said he never talked about the war, only about the food",
"use": "framed",
"how": "Framed as what Grandma said, kept as her story."
},
{
"fact": "He used to whistle the same tune while washing up",
"use": "framed",
"how": "Framed as a family memory, closing the tribute."
},
{
"fact": "Which ship was he on?",
"use": "left_out",
"how": "Not known, so it becomes a question instead."
}
],
"questions": [
"Do the discharge papers, or any other paperwork, say which ship he was on?",
"Does anyone keep a letter or note that might mention the tune he used to whistle?"
],
"before_sharing": "The main facts are on record, so before sharing, check the names and years against the discharge papers, photo, and record card to be sure they all agree."
}
Great-aunt - mostly family stories, short and formal for a service programme
Input:
{
"name": "Great-Aunt Rosa",
"relation": "my great-aunt",
"pronouns": "she",
"purpose": "program",
"purpose_label": "a few lines for a service programme or memorial page",
"length": "short",
"words_min": "50",
"words_max": "80",
"tone": "formal",
"facts": "[{\"fact\":\"Her obituary in the local paper says she served as an Army nurse\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"The family says she nursed soldiers in England before D-Day\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"Mom remembers her saying the night shifts were the hardest part\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"She apparently met her husband at a dance near the base\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"Not sure which hospital she was posted to\",\"source\":\"unknown\",\"source_label\":\"not known (a gap or a question)\"},{\"fact\":\"Did she stay in the service after the war?\",\"source\":\"unknown\",\"source_label\":\"not known (a gap or a question)\"}]",
"figures": "{\"verdict\":\"mostly_stories\",\"records\":\"1\",\"stories\":\"3\",\"unknowns\":\"2\",\"total\":\"6\"}",
"wishes": "It will be read aloud, so keep the sentences short."
}
Reply:
{
"verdict": "mostly_stories",
"title": "Remembering Great-Aunt Rosa",
"plaque": "Served as an Army nurse",
"tribute": "My great-aunt Rosa's obituary in the local paper says she served as an Army nurse.\n\nThe family says she nursed soldiers in England before D-Day. Mom remembers her saying the night shifts were the hardest part.\n\nAs the story goes, she met her husband at a dance near the base.",
"facts": [
{
"fact": "Her obituary in the local paper says she served as an Army nurse",
"use": "stated",
"how": "Used directly as the opening line about her role as a nurse."
},
{
"fact": "The family says she nursed soldiers in England before D-Day",
"use": "framed",
"how": "Kept as a family story, worded with 'the family says' in the tribute."
},
{
"fact": "Mom remembers her saying the night shifts were the hardest part",
"use": "framed",
"how": "Kept as Mom's memory, word for word, in the second paragraph."
},
{
"fact": "She apparently met her husband at a dance near the base",
"use": "framed",
"how": "Kept as a family story, introduced with 'as the story goes' before the detail."
},
{
"fact": "Not sure which hospital she was posted to",
"use": "left_out",
"how": "Not known, so turned into one of the questions to ask."
},
{
"fact": "Did she stay in the service after the war?",
"use": "left_out",
"how": "Not known, so turned into a question about her later service."
}
],
"questions": [
"Could the official service records show which hospital she was posted to?",
"Do the official service records show whether she stayed in the service after the war?",
"Is there a letter or paper from her time nursing in England that an older relative might have kept?",
"Does anyone have a photo from the dance where she met her husband?"
],
"before_sharing": "Most of this rests on family stories, so keep them described as stories rather than as checked facts when the programme is shared."
}
Great-grandpa - check before sharing, nothing on record, for a school assignment
Input:
{
"name": "Great-Grandpa Walter",
"relation": "my great-grandfather",
"pronouns": "he",
"purpose": "school",
"purpose_label": "a school assignment about someone who served",
"length": "standard",
"words_min": "100",
"words_max": "150",
"tone": "plain",
"facts": "[{\"fact\":\"The family story is that he lied about his age to join up\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"Dad thinks he was in the army in France in the First World War\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"He supposedly kept a diary, but nobody knows where it went\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"He came home and worked on the railways for forty years\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"What regiment was he in?\",\"source\":\"unknown\",\"source_label\":\"not known (a gap or a question)\"},{\"fact\":\"Is there a photo of him in uniform anywhere?\",\"source\":\"unknown\",\"source_label\":\"not known (a gap or a question)\"}]",
"figures": "{\"verdict\":\"check_first\",\"records\":\"0\",\"stories\":\"4\",\"unknowns\":\"2\",\"total\":\"6\"}",
"wishes": ""
}
Reply:
{
"verdict": "check_first",
"title": "Remembering Great-Grandpa Walter",
"plaque": "Remembered by the family",
"tribute": "My great-grandfather, Great-Grandpa Walter, is known to our family mostly through what is remembered and believed, rather than through papers we can point to. As the family tells it, he lied about his age to join up, and Dad thinks he was in the army in France in the First World War, though nobody has confirmed either part of it. The family also says he kept a diary, but nobody knows where it went, so that part of the story has no paper to back it up either.\n\nWhat the family remembers, too, is this: he came home and worked on the railways for forty years.",
"facts": [
{
"fact": "The family story is that he lied about his age to join up",
"use": "framed",
"how": "Used in the tribute as a family story about joining up."
},
{
"fact": "Dad thinks he was in the army in France in the First World War",
"use": "framed",
"how": "Used in the tribute as what Dad thinks, not as a certainty."
},
{
"fact": "He supposedly kept a diary, but nobody knows where it went",
"use": "framed",
"how": "Used in the tribute as a family story about a lost diary."
},
{
"fact": "He came home and worked on the railways for forty years",
"use": "framed",
"how": "Used in the tribute as a family story about his years on the railways."
},
{
"fact": "What regiment was he in?",
"use": "left_out",
"how": "Not known, so turned into a question about his regiment."
},
{
"fact": "Is there a photo of him in uniform anywhere?",
"use": "left_out",
"how": "Not known, so turned into a question about a photo in uniform."
}
],
"questions": [
"Do the official service records show what regiment he was in?",
"Does anyone in the family have a photo of him in uniform?",
"Could Dad say more about how he knows Walter served in France in the First World War?",
"Does anyone know if the diary he is said to have kept still exists, and where?"
],
"before_sharing": "Nothing here is on record yet: check with an older relative or the official service records before sharing this beyond the family, keep the stories described as family memory, and note in the assignment which parts are family stories."
}
A living parent - on record, the name only, one fact left out as asked
Input:
{
"name": "Sam",
"relation": "my parent",
"pronouns": "name only",
"purpose": "family",
"purpose_label": "a message for the family",
"length": "short",
"words_min": "50",
"words_max": "80",
"tone": "warm",
"facts": "[{\"fact\":\"Sam's discharge certificate shows six years in the Air Force, 1998 to 2004\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"Sam still keeps the old ID badge from the base in a drawer\",\"source\":\"record\",\"source_label\":\"on record (the family holds papers, a photo, a medal or a letter for it)\"},{\"fact\":\"Sam deployed overseas twice\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"},{\"fact\":\"Sam says the best part was the friends made on base\",\"source\":\"story\",\"source_label\":\"a family story (told, remembered or believed, not checked)\"}]",
"figures": "{\"verdict\":\"on_record\",\"records\":\"2\",\"stories\":\"2\",\"unknowns\":\"0\",\"total\":\"4\"}",
"wishes": "Sam does not like talking about the deployments - leave them out."
}
Reply:
{
"verdict": "on_record",
"title": "A Family Message for Sam",
"plaque": "Air Force service, 1998 to 2004",
"tribute": "My parent, Sam, served six years in the Air Force, from 1998 to 2004, as the discharge certificate shows. Sam still keeps the old ID badge from the base in a drawer.\n\nAlong with what is on record, Sam also says that the best part was the friends made on base.",
"facts": [
{
"fact": "Sam's discharge certificate shows six years in the Air Force, 1998 to 2004",
"use": "stated",
"how": "Stated plainly, opening the message with the service record."
},
{
"fact": "Sam still keeps the old ID badge from the base in a drawer",
"use": "stated",
"how": "Stated plainly right after the service years."
},
{
"fact": "Sam deployed overseas twice",
"use": "left_out",
"how": "Left out, as Sam asked, since it is about deployments."
},
{
"fact": "Sam says the best part was the friends made on base",
"use": "framed",
"how": "Framed as what Sam says, closing the message."
}
],
"questions": [
"Is there a photo or letter from Sam's time on base that shows some of those friends?"
],
"before_sharing": "The main facts are on record, so before sharing, check the years against the discharge certificate, and keep the friends story framed as what Sam says, not as confirmed record."
}
Build the input yourself
Load the script in node (global.window = global; require("./servekit.js")), then:
const K = ServeKit;
const form = K.form({ name: "Grandad Joe", relation: "my grandfather", pronouns: "auto", purpose: "post", length: "standard", tone: "warm",
facts: "Discharge papers say he served from 1943 to 1946\nGrandma said he never talked about it\nWhich ship was he on?", wishes: "" });
const r = K.read(form); // r.facts (each with its pile), r.pronoun, r.fig.verdict
const built = K.buildInput(form);
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built).notes // the page's re-check of the reply
// K.csv(built, reply), K.md(reply, built, form) // the fact ledger and the Markdown export
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://they-served.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://they-served.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"]
// they-served.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://they-served.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://they-served.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
}
// TheyServed.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 TheyServed {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://they-served.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://they-served.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://they-served.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://they-served.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":"they-served"}' | 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": "they-served"})["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: "they-served" })).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": "they-served"}, 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", "they-served"), 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: "they-served" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "they-served"])["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 = "they-served" }))!["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 - {"name": "Grandad Joe", "relation": "my grandfather", "pronouns": "he", "purpose": "post", "purpose_label": "a short post for 11 November", "length": "standard", "words_min": "100", "words_max": "150", "tone": "warm", "facts": "[...]", "figures": "{...}", "wishes": ""}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"name": "Grandad Joe", "relation": "my grandfather", "pronouns": "he", "purpose": "post", "purpose_label": "a short post for 11 November", "length": "standard", "words_min": "100", "words_max": "150", "tone": "warm", "facts": "[...]", "figures": "{...}", "wishes": ""}
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")); // {name, relation, pronouns, purpose, length, words_min, words_max, tone, facts, figures, wishes, ... (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 input: a retry with the same key never bills twice. The page uses they-served:tribute:<inputHash>:a<attempt> (the one task is tribute), 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="they-served:tribute:$(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 = "they-served:tribute:" + 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"))
import { createHash } from "node:crypto";
const key = `they-served:tribute:${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);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "they-served:tribute:" + 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 = "they-served:tribute:" + 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 = "they-served:tribute:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "they-served:tribute:" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"they-served:tribute:{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: they-served:tribute:$(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: {"name": "Grandad Joe", "relation": "my grandfather", "pronouns": "he", "purpose": "post", "purpose_label": "a short post for 11 November", "length": "standard", "words_min": "100", "words_max": "150", "tone": "warm", "facts": "[...]", "figures": "{...}", "wishes": ""}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "they-served:tribute:" + 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": `they-served:tribute:${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", "they-served:tribute:"+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", "they-served:tribute:" + 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" => "they-served:tribute:#{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: they-served:tribute:" . 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", $"they-served:tribute:{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);