Drive For Services To from your own code
The one metered task, nominate, is available over HTTP. It takes an invented award (its name, what it is for and who it is for), its criteria and the evidence - what the nominee did, one line each - plus the page's cue-word facts. It returns one verdict (rename_the_award, evidence_gap, too_vague or shortlist_ready), every evidence line read as specific or vague and filed under up to 2 criteria, one reading per criterion, a better name when renaming, a citation, a nomination in the nominator's voice, three questions for the nominator and a closing line.
The division of labour first: the page reads the form into numbered lines and cue words; the model reads the meaning; the page then re-checks the reply against the form - one reading per evidence line, criteria that exist, the verdict against the order, the citation's opening, question 1 on what decided the verdict. A playful way to say thank you, not a real awards scheme.
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":"for-services-to"}), 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, award, for_what, nominee, criteria, evidence, 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: the panel's reading is metered and needs a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field and the verdicts
Every run sends task: nominate (the one task is nominate); a missing or different one is still answered as nominate. The verdict field holds one of four ids, by a fixed check order over the reply's own lines that stops at the first that fits: rename_the_award when the lines that fit at least one criterion are fewer than half of all lines; evidence_gap when some criterion is in no line's criteria; too_vague when more than half of the lines that fit a criterion are vague; otherwise shortlist_ready. The page recomputes the verdict from the reply's own fields and flags any mismatch.
Input (every field is a string)
| Field | Required | What it holds |
|---|---|---|
task | yes | Always nominate. |
award | yes | The award's name, up to 60 characters. |
for_what | yes | What the award is for, up to 90 characters, or none given. |
nominee | yes | The name the nominator uses for the nominee, up to 40 characters. |
criteria | yes | 2 to 4 lines as c | criterion, numbered from 1. |
evidence | yes | 3 to 12 lines as n | what the nominee did, numbered from 1, in the nominator's words. |
page_facts | yes | One key | ... per line: nominee, criteria (count), evidence_lines (count), page_cues (line numbers marked detail, praise only or unclear) and verdict_rule. |
clip_note | no | What the page left out or shortened, if anything (criteria past 4, evidence past 12 lines, lines over 220 characters). |
retry_note | no | Only on the page's one reformat retry, after an unusable reply. |
Reply (one JSON object)
{"task": "nominate", "verdict": "shortlist_ready", "verdict_reason": "...",
"lines": [{"n": 1, "criteria": [1], "kind": "specific", "shows": "..."}],
"criteria": [{"c": 1, "reading": "..."}], "better_name": "",
"citation": "For services to ...", "nomination": "I nominate ...",
"questions": [{"line": 2, "criterion": 1, "ask": "...?"}], "closing": "..."}
| Field | What it holds |
|---|---|
verdict, verdict_reason | The verdict id by the check order above; at most 30 words on what decided it. |
lines | Exactly one entry per evidence line: n, criteria (at most 2 criterion numbers, or [] when it fits none), kind (specific or vague) and shows (at most 12 words). |
criteria | Exactly one reading per criterion, in order, at most 20 words, naming only lines filed under it. |
better_name | A new award name of at most 6 words, only for rename_the_award; "" otherwise. |
citation, nomination | The citation (at most 14 words, starting "For services to") and a nomination of at most 110 words in the nominator's voice, built only from specific lines. |
questions, closing | Exactly 3 questions for the nominator, each with the evidence line and criterion it is about (0 for none), at most 20 words, question 1 on what decided the verdict; a closing line of at most 20 words. |
The page then checks the reply (AwardKit.check): the verdict against its own lines, one reading per evidence line with criteria that exist and a kind, one reading per criterion citing only its own lines, a better name exactly when renaming, the citation's opening, the nominee named in the nomination, three questions with question 1 on the deciding moment, pronouns, stray numbers, leaked field ids, "sent" or "won" claims and the word limits. It also reconciles its own cue words: a line that reads like praise only but is called specific, or carries a number or time and an action but is called vague, is flagged for a second look.
Worked examples
These are the four free samples on the page: the exact input and a stand-in model's reply, unedited. Together they cover all four verdicts - an office tea-shelf keeper (shortlist ready), a "best gardener" whose evidence is all bike repairs and borrowed tools (rename the award), a flatmate with nothing yet for cheering everyone up (gap in the evidence), and a neighbour praised as kind and generous but almost never for an act (too vague).
The Golden Kettle - shortlist ready
Input:
{
"task": "nominate",
"award": "The Golden Kettle",
"for_what": "keeping the office tea shelf going",
"nominee": "Dana",
"criteria": "1 | Keeps things running\n2 | Thinks of others\n3 | Does it without being asked",
"evidence": "1 | Fills and boils the kettle before the 9 o'clock meeting every Monday.\n2 | Started a tea shelf with labelled jars and restocks it from the corner shop twice a month.\n3 | Bought decaf for Sam after Sam said caffeine kept them up.\n4 | Put up a sign showing how to descale the kettle, and descales it every Friday.\n5 | Keeps a spare carton of oat milk for the two people who can't have dairy.\n6 | Nobody asked for any of it - Dana noticed the shelf was empty in March and filled it.",
"page_facts": "nominee | Dana\ncriteria | 3\nevidence_lines | 6\npage_cues | detail: 1, 2, 4, 5, 6; praise only: none; unclear: 3\nverdict_rule | rename_the_award when fewer than half of the evidence lines fit any criterion; else evidence_gap when a criterion has no line; else too_vague when more than half of the lines that fit a criterion are vague; else shortlist_ready"
}
Reply:
{
"task": "nominate",
"verdict": "shortlist_ready",
"verdict_reason": "6 of 6 lines fit a criterion, every criterion has a line, and 0 of 6 fitting lines are vague, so shortlist ready.",
"lines": [
{
"n": 1,
"criteria": [
1
],
"kind": "specific",
"shows": "Fills and boils the kettle every Monday before the meeting"
},
{
"n": 2,
"criteria": [
1
],
"kind": "specific",
"shows": "Started a tea shelf with labelled jars, restocks twice a month"
},
{
"n": 3,
"criteria": [
2
],
"kind": "specific",
"shows": "Bought decaf for Sam after Sam said caffeine kept them up"
},
{
"n": 4,
"criteria": [
1
],
"kind": "specific",
"shows": "Put up a descaling sign and descales the kettle every Friday"
},
{
"n": 5,
"criteria": [
2
],
"kind": "specific",
"shows": "Keeps spare oat milk for the two people who can't have dairy"
},
{
"n": 6,
"criteria": [
1,
3
],
"kind": "specific",
"shows": "Noticed the empty shelf in March and filled it, unasked"
}
],
"criteria": [
{
"c": 1,
"reading": "Lines 1, 2, 4 and 6 show Dana keeping the kettle, shelf and descaling routine running every week."
},
{
"c": 2,
"reading": "Lines 3 and 5 show Dana choosing decaf and oat milk for specific colleagues' needs."
},
{
"c": 3,
"reading": "Line 6 shows Dana noticing the empty shelf and filling it without anyone asking."
}
],
"better_name": "",
"citation": "For services to boiling the Monday kettle and restocking the tea shelf.",
"nomination": "I nominate Dana for The Golden Kettle because they fill and boil the kettle every Monday before the meeting, started the tea shelf with labelled jars and restock it from the corner shop twice a month, bought decaf for Sam after Sam said caffeine kept them up, put up a sign on descaling and descale the kettle every Friday, keep a spare carton of oat milk for the two people who can't have dairy, and noticed the shelf was empty in March and filled it without anyone asking.",
"questions": [
{
"line": 6,
"criterion": 3,
"ask": "When did you first notice Dana had seen the shelf was empty in March?"
},
{
"line": 1,
"criterion": 1,
"ask": "What time do you usually see Dana fill and boil the kettle before the meeting?"
},
{
"line": 5,
"criterion": 2,
"ask": "How long have you seen Dana keep the spare carton of oat milk ready?"
}
],
"closing": "Dana's quiet kettle care, tidy tea shelf and thoughtful extras for Sam and others are warm, well-told evidence."
}
Best Gardener on the Street - rename the award
Input:
{
"task": "nominate",
"award": "Best Gardener on the Street",
"for_what": "keeping our street green",
"nominee": "Ola",
"criteria": "1 | Green fingers\n2 | Makes the street look good",
"evidence": "1 | Fixed the chain on a neighbour's bike in ten minutes on the pavement.\n2 | Lent a ladder and a toolbox to three different neighbours this year.\n3 | Jump-started my car on a frozen morning in January.\n4 | Pumped up every flat tyre on the bikes at number 6 before the school run.\n5 | Swept the leaves off the shared path after the storm.",
"page_facts": "nominee | Ola\ncriteria | 2\nevidence_lines | 5\npage_cues | detail: 1, 2, 3, 4; praise only: none; unclear: 5\nverdict_rule | rename_the_award when fewer than half of the evidence lines fit any criterion; else evidence_gap when a criterion has no line; else too_vague when more than half of the lines that fit a criterion are vague; else shortlist_ready"
}
Reply:
{
"task": "nominate",
"verdict": "rename_the_award",
"verdict_reason": "Only 1 of 5 lines fits a criterion, fewer than half, so the verdict is rename the award.",
"lines": [
{
"n": 1,
"criteria": [],
"kind": "specific",
"shows": "Fixed a neighbour's bike chain in ten minutes on the pavement"
},
{
"n": 2,
"criteria": [],
"kind": "specific",
"shows": "Lent a ladder and a toolbox to three different neighbours this year"
},
{
"n": 3,
"criteria": [],
"kind": "specific",
"shows": "Jump-started the nominator's car on a frozen morning in January"
},
{
"n": 4,
"criteria": [],
"kind": "specific",
"shows": "Pumped up every flat bike tyre at number 6 before school run"
},
{
"n": 5,
"criteria": [
2
],
"kind": "specific",
"shows": "Swept leaves off the shared path after the storm"
}
],
"criteria": [
{
"c": 1,
"reading": "No line names a gardening act, so nothing yet shows green fingers."
},
{
"c": 2,
"reading": "Only line 5, sweeping the shared path after a storm, speaks to how the street looks."
}
],
"better_name": "Best Neighbour on the Street",
"citation": "For services to fixing a neighbour's bike and jump-starting a frozen car.",
"nomination": "I nominate Ola for the Best Gardener on the Street because of all the ways Ola helps out. Ola fixed a neighbour's bike chain in ten minutes on the pavement, lent a ladder and toolbox to three different neighbours this year, jump-started my car on a frozen January morning, pumped up every flat bike tyre at number 6 before the school run, and swept leaves off the shared path after that storm. I think a name like Best Neighbour on the Street would fit what Ola actually does better than a gardening award.",
"questions": [
{
"line": 0,
"criterion": 0,
"ask": "What do you really want this street award to stand for?"
},
{
"line": 2,
"criterion": 0,
"ask": "How often have you seen Ola lend tools like that ladder to neighbours?"
},
{
"line": 0,
"criterion": 1,
"ask": "Has Ola ever gardened or grown something you'd call proper green fingers?"
}
],
"closing": "What a lovely list of small favours - it just might belong under a different, better name."
}
Flatmate of the Year - gap in the evidence
Input:
{
"task": "nominate",
"award": "Flatmate of the Year",
"for_what": "making the flat a nicer place to live",
"nominee": "Sam",
"criteria": "1 | Keeps the flat tidy\n2 | Considerate at night\n3 | Cheers everyone up",
"evidence": "1 | Takes the bins out every Tuesday night without being reminded.\n2 | Wears headphones for games after 11pm so nobody is woken.\n3 | Cleaned the oven before the inspection in May.\n4 | Closes the front door gently when coming in late from shifts.",
"page_facts": "nominee | Sam\ncriteria | 3\nevidence_lines | 4\npage_cues | detail: 1, 2, 3; praise only: none; unclear: 4\nverdict_rule | rename_the_award when fewer than half of the evidence lines fit any criterion; else evidence_gap when a criterion has no line; else too_vague when more than half of the lines that fit a criterion are vague; else shortlist_ready"
}
Reply:
{
"task": "nominate",
"verdict": "evidence_gap",
"verdict_reason": "4 of 4 lines fit a criterion, but no line fits criterion 3, so this is a gap in the evidence.",
"lines": [
{
"n": 1,
"criteria": [
1
],
"kind": "specific",
"shows": "Takes bins out every Tuesday night without being reminded"
},
{
"n": 2,
"criteria": [
2
],
"kind": "specific",
"shows": "Wears headphones for games after 11pm so nobody is woken"
},
{
"n": 3,
"criteria": [
1
],
"kind": "specific",
"shows": "Cleaned the oven before the inspection in May"
},
{
"n": 4,
"criteria": [
2
],
"kind": "specific",
"shows": "Closes the front door gently when coming in late from shifts"
}
],
"criteria": [
{
"c": 1,
"reading": "Lines 1 and 3 show tidying acts: taking bins out and cleaning the oven for inspection."
},
{
"c": 2,
"reading": "Lines 2 and 4 show night consideration: headphones after 11pm and closing the door gently."
},
{
"c": 3,
"reading": "No evidence line shows this; nothing describes Sam cheering anyone up."
}
],
"better_name": "",
"citation": "For services to taking the bins out and wearing headphones after 11pm.",
"nomination": "I nominate Sam for Flatmate of the Year because Sam takes the bins out every Tuesday night without being reminded, wears headphones for games after 11pm so nobody is woken, cleaned the oven before the inspection in May, and closes the front door gently when coming in late from shifts. I don't have an example yet of Sam cheering everyone up, which is one of the criteria.",
"questions": [
{
"line": 0,
"criterion": 3,
"ask": "Can you give one example of Sam cheering everyone up?"
},
{
"line": 1,
"criterion": 1,
"ask": "How long have you noticed Sam taking the bins out every Tuesday night?"
},
{
"line": 4,
"criterion": 2,
"ask": "Have you noticed how long Sam has been closing the door gently when coming in late?"
}
],
"closing": "Sam's care with tidying and quiet nights already shines through - one more story would complete it."
}
Neighbour of the Year - too vague
Input:
{
"task": "nominate",
"award": "Neighbour of the Year",
"for_what": "being a good neighbour",
"nominee": "Priya",
"criteria": "1 | Kind\n2 | Generous",
"evidence": "1 | Honestly the kindest neighbour ever.\n2 | So generous with everyone on the street.\n3 | Took in four of my parcels in October while I was away.\n4 | Really kind, a total legend.\n5 | Always generous and lovely.",
"page_facts": "nominee | Priya\ncriteria | 2\nevidence_lines | 5\npage_cues | detail: 3; praise only: 1, 2, 4, 5; unclear: none\nverdict_rule | rename_the_award when fewer than half of the evidence lines fit any criterion; else evidence_gap when a criterion has no line; else too_vague when more than half of the lines that fit a criterion are vague; else shortlist_ready"
}
Reply:
{
"task": "nominate",
"verdict": "too_vague",
"verdict_reason": "4 of 5 lines that fit a criterion are vague, more than half, so too vague.",
"lines": [
{
"n": 1,
"criteria": [
1
],
"kind": "vague",
"shows": "Praises Priya as the kindest neighbour ever; no act named."
},
{
"n": 2,
"criteria": [
2
],
"kind": "vague",
"shows": "Praises Priya as generous with everyone; no act named."
},
{
"n": 3,
"criteria": [
1
],
"kind": "specific",
"shows": "Took in four of the nominator's parcels in October while away."
},
{
"n": 4,
"criteria": [
1
],
"kind": "vague",
"shows": "Praises Priya as really kind, a total legend; no act named."
},
{
"n": 5,
"criteria": [
2
],
"kind": "vague",
"shows": "Praises Priya as always generous and lovely; no act named."
}
],
"criteria": [
{
"c": 1,
"reading": "Lines 1 and 4 only praise Priya as kind; line 3 shows one specific kind act."
},
{
"c": 2,
"reading": "Lines 2 and 5 only praise Priya as generous; no line shows a generous act."
}
],
"better_name": "",
"citation": "For services to taking in four parcels during an October absence.",
"nomination": "I nominate Priya for Neighbour of the Year because Priya took in four of my parcels in October while I was away. I think Priya is the kindest neighbour ever, so generous with everyone on the street, really kind, a total legend, and always generous and lovely, though most of what I've said is praise without a particular act to point to.",
"questions": [
{
"line": 1,
"criterion": 1,
"ask": "What is one specific thing you saw Priya do that felt the kindest?"
},
{
"line": 2,
"criterion": 2,
"ask": "When did you see Priya being generous with everyone on the street?"
},
{
"line": 4,
"criterion": 1,
"ask": "What did you see Priya do to earn being called a total legend?"
}
],
"closing": "Priya clearly matters to this street, and a few more specific stories would make the case shine."
}
Build the input yourself
Load the engine in node (global.window = global; require("./awardkit.js")), then:
const K = AwardKit;
const built = K.buildInput({ award: "The Golden Kettle", nominee: "Dana", for_what: "keeping the tea shelf going",
criteria: "Keeps things running\nThinks of others",
evidence: "Boils the kettle before the 9 o'clock meeting every Monday.\nBought decaf for Sam.\nRestocks the shelf twice a month." });
built.parsed.evidence.length; // 3
built.prescan.cue; // the page's cue word per line: detail, praise or unclear
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.verdictFrom(reply, built.parsed.criteria.length) // the verdict the reply's own lines give
// K.check(reply, built).notes // the page's re-check of the reply
// K.md(built, reply) // the reading 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://for-services-to.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://for-services-to.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"]
// for-services-to.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://for-services-to.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://for-services-to.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
}
// ForServicesTo.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 ForServicesTo {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://for-services-to.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://for-services-to.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://for-services-to.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://for-services-to.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":"for-services-to"}' | 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": "for-services-to"})["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: "for-services-to" })).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": "for-services-to"}, 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", "for-services-to"), 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: "for-services-to" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "for-services-to"])["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 = "for-services-to" }))!["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": "nominate", "award": "The Golden Kettle", "for_what": "...", "nominee": "Dana", "criteria": "1 | ...", "evidence": "1 | ...", "page_facts": "nominee | Dana\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": "nominate", "award": "The Golden Kettle", "for_what": "...", "nominee": "Dana", "criteria": "1 | ...", "evidence": "1 | ...", "page_facts": "nominee | Dana\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: "nominate", award, for_what, nominee, criteria, evidence, 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 for-services-to:<task>:<inputHash>:a<attempt> (the one task is nominate), 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="for-services-to:nominate:$(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 = "for-services-to:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `for-services-to:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "for-services-to:" + 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 = "for-services-to:" + 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 = "for-services-to:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"]].inspect
$key = "for-services-to:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["verdict"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"for-services-to:{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: for-services-to:nominate:$(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": "nominate", "award": "The Golden Kettle", "for_what": "...", "nominee": "Dana", "criteria": "1 | ...", "evidence": "1 | ...", "page_facts": "nominee | Dana\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": "for-services-to:nominate:" + 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": `for-services-to:nominate:${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", "for-services-to:nominate:"+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", "for-services-to:nominate:" + 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" => "for-services-to:nominate:#{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: for-services-to:nominate:" . 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", $"for-services-to:nominate:{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);