Drive No Further Questions from your own code
Both metered lanes are available over HTTP. presser takes a game sheet - who is at the podium and in what role, both sides, what was played, the score and the result, the moments (each marked contested, highlight, miss or note), a persona and a room - and returns a verdict, hot-seat, post-mortem or easy-room, with an opening statement, four to six questions from named reporters and their answers, a headline, a pull quote and an exit line. podium takes the same sheet plus one reporter's question and the answer you wrote, and returns on-the-record, rephrase or walk-it-back, with media coach notes, a better answer, tomorrow's headline and the best soundbite. A natural use is a league newsletter or a team chat bot that turns the week's results into a mock presser.
The division of labour first: the page owns the sheet, the score and the hold-back; the model runs the room; the page re-checks the reply. The sheet reader, the hold-back and the clipping come from presskit.js, which runs in node as well as in the browser. Run PressKit.buildInput on anything you send: it works out the result from the score and drops every moment that touches an off-limits topic, so the model never sees it. PressKit.check holds each reply to the sheet: the question count and skeptic count match the room, a hot seat opens on the contested moment, every question cites moments that were sent, the pull quote and soundbite are word for word, and no dig at a person, swear word, off-limits word, capitalised name, number or pronoun turns up that the sheet does not hold.
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":"no-further-questions"}), 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 such as task, speaker or moments 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: both lanes are metered and need 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: presser or podium; a missing or unknown task is treated as presser. Each lane decides its verdict by three ordered, non-overlapping rules - the first that applies wins:
| Lane | Order | Verdict | When |
|---|---|---|---|
| presser | 1 | hot-seat | At least one moment is contested. Q1 asks about the first contested moment and a later question follows up on it. |
| presser | 2 | post-mortem | The result is a loss. Q1 asks what went wrong; a question covers a miss if there is one. |
| presser | 3 | easy-room | Otherwise. Q1 asks about the best highlight, or the result. |
| podium | 1 | walk-it-back | The answer takes a dig at a person, touches an off-limits topic, or swears. A different answer comes back; no soundbite. |
| podium | 2 | rephrase | Over 60 words, a dodge, a score or game event the sheet does not hold, or three or more stock phrases with nothing specific. A tighter answer keeps the point. |
| podium | 3 | on-the-record | None of the above: no issues, no rewrite. |
A friendly room gets 4 questions and no skeptic, a mixed room 5 with exactly one skeptic question, a tough room 6 with at least two.
The input fields
The declared input schema carries scalars only, so the array-valued off_limits and moments fields travel JSON-encoded as strings (PressKit.toWire does this). Every value is a string.
| Field | Type | Meaning |
|---|---|---|
task | string | presser or podium. Required. |
speaker | string | Who is at the podium, up to 30 characters. Required. |
role | string | coach, captain, player or organizer. |
side, opponent | string | The two sides' names as typed; either may be empty. |
activity | string | What was played, up to 80 characters. |
score_us, score_them, result | string | Whole numbers, or both empty; result is win, loss, draw or empty, worked out by the page. |
persona | string | gracious, deadpan, cliche, fiery or rookie. |
room, question_count | string | friendly, mixed or tough, and the matching count "4", "5" or "6". |
off_limits | string (JSON) | Topics as typed, up to 10. Moments touching one are already removed. |
held_back | string | How many moments the page withheld. |
moments | string (JSON) | [{id, kind, text}] - ids M1.., up to 12 moments of up to 200 characters; kind is contested, highlight, miss or note, from a "call:", "good:" or "miss:" mark. Required. |
question, draft | string | podium only: the reporter's question (up to 240 characters) and the answer to judge (up to 900). |
draft_words, cliche_count | string | podium only: the answer's length in words and the number of stock phrases, both counted by the page. |
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. Both lanes carry task, verdict, summary and headline. presser adds opening, questions ([{id, reporter, ask, answer, moments, move}]; ids Q1..; reporter is beat, radio, stats, student, podcast or skeptic; move is straight, credit, deflect, cliche or joke), pull_quote and exit_line. podium adds title, issues ([{rule, note}]; rule is dig, off-limits, swearing, too-long, dodge, ungrounded or all-cliche), rewrite and soundbite. Parse with PressKit.parseReply, then normalize(reply, task), then check(reply, form), which returns the page's notes.
Worked examples
Replies produced against the app's prompt for the six sample runs (made-up people and games; long fields shortened; the full replies ship in example-result.js). Together they cover every verdict of both lanes.
presser - beer-league softball, a disputed tag at home, tough room: hot-seat
Input:
{
"task": "presser",
"speaker": "Robin",
"role": "captain",
"side": "Late Swings",
"opponent": "Gravel Pit",
"activity": "Tuesday night beer-league softball",
"score_us": "11",
"score_them": "12",
"result": "loss",
"persona": "fiery",
"room": "tough",
"question_count": "6",
"off_limits": "[\"Theo's knee\"]",
"held_back": "1",
"moments": "[{\"id\":\"M1\",\"kind\":\"contested\",\"text\":\"Dana was called out at home on a tag the whole bench saw miss\"},{\"id\":\"M2\",\"kind\":\"highlight\",\"text\":\"Priya hit two triples\"},{\"id\":\"M3\",\"kind\":\"miss\",\"text\":\"we left the bases loaded in the last inning\"},{\"id\":\"M4\",\"kind\":\"highlight\",\"text\":\"Theo turned a double play\"},{\"id\":\"M5\",\"kind\":\"note\",\"text\":\"the game ran so long the field lights ... (391 characters)"
}
Reply:
{
"task": "presser",
"verdict": "hot-seat",
"headline": "Late Swings Captain Stands Firm on Disputed Call",
"summary": "Robin fronted a tough room that returned twice to the disputed tag at home, pressing the call again after first raising it. Reporters also asked about Priya's two triples, Theo's double play, the bases left loaded in the last inning and the lights coming on.",
"opening": "I'll say it straight: that tag at home never should have gone against Dana, and I don't think this team has anything to be ashamed of. We came up short against Gravel Pit, but I know how hard this team competed.",
"questions": [
{
"id": "Q1",
"reporter": "skeptic",
"ask": "Did that tag at home really get Dana out?",
"answer": "That tag missed, plain and simple, and everyone on our bench saw it miss. Dana should still be standing on that plate, and I'll say that every time someone asks.",
"moments": [
"M1"
],
"move": "straight"
},
{
"id": "Q2",
"reporter": "beat",
"ask": "What did you think of Priya's two triples?",
"answer": "Priya hit two triples and that bat was the best thing we had going. That's the kind of swing this team needs more of.",
"moments": [
"M2"
],
"move": "credit"
},
"... (6 in all)"
],
"pull_quote": "Dana was safe, and I'll keep saying it as long as anyone asks.",
"exit_line": "I've got nothing to apologize for, and that's that."
}
presser - under-10 soccer, a 1-4 loss, friendly room: post-mortem
Input:
{
"task": "presser",
"speaker": "Jess",
"role": "coach",
"side": "Green Comets",
"opponent": "Riverside",
"activity": "under-10 Saturday soccer",
"score_us": "1",
"score_them": "4",
"result": "loss",
"persona": "gracious",
"room": "friendly",
"question_count": "4",
"off_limits": "[]",
"held_back": "0",
"moments": "[{\"id\":\"M1\",\"kind\":\"miss\",\"text\":\"we gave up two goals from corner kicks\"},{\"id\":\"M2\",\"kind\":\"highlight\",\"text\":\"Mateo scored a first-ever goal\"},{\"id\":\"M3\",\"kind\":\"highlight\",\"text\":\"our keeper Ana made a diving save\"},{\"id\":\"M4\",\"kind\":\"note\",\"text\":\"everyone got equal minutes\"}]"
}
Reply:
{
"task": "presser",
"verdict": "post-mortem",
"headline": "Green Comets Fall to Riverside Despite Mateo's First Goal",
"summary": "Jess answered questions about the loss to Riverside, starting with what went wrong on the two goals conceded from corner kicks. Credit went to Mateo's goal and Ana's save, and the room stayed friendly throughout.",
"opening": "Credit to Riverside, they were the better side. I'm proud of the Green Comets, and credit to Mateo and Ana for what they did.",
"questions": [
{
"id": "Q1",
"reporter": "beat",
"ask": "What went wrong for the Comets in this loss?",
"answer": "We gave up two goals from corner kicks, and that's on us to clean up. Credit to Riverside for that.",
"moments": [
"M1"
],
"move": "credit"
},
{
"id": "Q2",
"reporter": "radio",
"ask": "What can you say about Mateo's goal?",
"answer": "Mateo scored a first-ever goal. Credit to Mateo for that.",
"moments": [
"M2"
],
"move": "credit"
},
"... (4 in all)"
],
"pull_quote": "Mateo scored a first-ever goal.",
"exit_line": "Thanks, everyone."
}
presser - pub trivia, a 52-47 win, mixed room: easy-room
Input:
{
"task": "presser",
"speaker": "Sam",
"role": "captain",
"side": "The Know-It-Ales",
"opponent": "Table Six",
"activity": "Thursday pub trivia",
"score_us": "52",
"score_them": "47",
"result": "win",
"persona": "cliche",
"room": "mixed",
"question_count": "5",
"off_limits": "[]",
"held_back": "0",
"moments": "[{\"id\":\"M1\",\"kind\":\"highlight\",\"text\":\"we swept the music round\"},{\"id\":\"M2\",\"kind\":\"highlight\",\"text\":\"Lee knew a capital city nobody else in the bar knew\"},{\"id\":\"M3\",\"kind\":\"miss\",\"text\":\"we wrote the right answer on the picture round, then crossed it out\"},{\"id\":\"M4\",\"kind\":\"note\",\"text\":\"the host lost the answer sheet for ten minutes\"}]"
}
Reply:
{
"task": "presser",
"verdict": "easy-room",
"headline": "The Know-It-Ales Sweep Music Round, Win Pub Trivia",
"summary": "Sam fielded five questions after The Know-It-Ales' win over Table Six, covering the music round sweep, Lee's capital city answer, the crossed-out picture round answer and the host's lost answer sheet. One question pressed harder, asking whether the win was deserved.",
"opening": "We take it one game at a time. The Know-It-Ales got the win over Table Six.",
"questions": [
{
"id": "Q1",
"reporter": "beat",
"ask": "What made the music round sweep the highlight of the win?",
"answer": "We control what we can control, and sweeping the music round was a big one for us.",
"moments": [
"M1"
],
"move": "cliche"
},
{
"id": "Q2",
"reporter": "stats",
"ask": "How did the picture round answer end up crossed out?",
"answer": "It is what it is - we wrote the right answer on the picture round, then crossed it out.",
"moments": [
"M3"
],
"move": "cliche"
},
"... (5 in all)"
],
"pull_quote": "We control what we can control, and sweeping the music round was a big one for us",
"exit_line": "It is what it is, and thanks for coming out."
}
podium - the call at home, owned: on-the-record
Input:
{
"task": "podium",
"speaker": "Robin",
"role": "captain",
"side": "Late Swings",
"opponent": "Gravel Pit",
"activity": "Tuesday night beer-league softball",
"score_us": "11",
"score_them": "12",
"result": "loss",
"persona": "fiery",
"room": "tough",
"question_count": "6",
"off_limits": "[\"Theo's knee\"]",
"held_back": "1",
"moments": "[{\"id\":\"M1\",\"kind\":\"contested\",\"text\":\"Dana was called out at home on a tag the whole bench saw miss\"},{\"id\":\"M2\",\"kind\":\"highlight\",\"text\":\"Priya hit two triples\"},{\"id\":\"M3\",\"kind\":\"miss\",\"text\":\"we left the bases loaded in the last inning\"},{\"id\":\"M4\",\"kind\":\"highlight\",\"text\":\"Theo turned a double play\"},{\"id\":\"M5\",\"kind\":\"note\",\"text\":\"the game ran so long the field lights ... (391 characters)",
"question": "Robin, walk us through the call at home plate.",
"draft": "From where I stood the tag missed, and our whole bench saw it the same way. But that's one call in a long game - we still left the bases loaded in the last inning, and that's on us, not the umpire.",
"draft_words": "42",
"cliche_count": "0"
}
Reply:
{
"task": "podium",
"verdict": "on-the-record",
"title": "The Answer On The Home Plate Call",
"summary": "Robin's answer about the call at home plate holds up as given. Robin calls the tag missed, says the bench saw it the same way, then owns the bases-loaded inning rather than blaming the umpire.",
"issues": [],
"rewrite": "",
"headline": "Robin Calls Tag Missed, Owns Bases-Loaded Finish",
"soundbite": "that's on us, not the umpire"
}
podium - the corner kicks, all coachspeak and 81 words: rephrase
Input:
{
"task": "podium",
"speaker": "Jess",
"role": "coach",
"side": "Green Comets",
"opponent": "Riverside",
"activity": "under-10 Saturday soccer",
"score_us": "1",
"score_them": "4",
"result": "loss",
"persona": "gracious",
"room": "friendly",
"question_count": "4",
"off_limits": "[]",
"held_back": "0",
"moments": "[{\"id\":\"M1\",\"kind\":\"miss\",\"text\":\"we gave up two goals from corner kicks\"},{\"id\":\"M2\",\"kind\":\"highlight\",\"text\":\"Mateo scored a first-ever goal\"},{\"id\":\"M3\",\"kind\":\"highlight\",\"text\":\"our keeper Ana made a diving save\"},{\"id\":\"M4\",\"kind\":\"note\",\"text\":\"everyone got equal minutes\"}]",
"question": "Coach, what went wrong on the corner kicks?",
"draft": "Look, at the end of the day it is what it is, credit to them, they came to play and we have to learn from it and take it one game at a time, and honestly we control what we can control and we left it all out there, so we will regroup, go back to the drawing board and focus on the process, because that is soccer and we will be back next week stronger than ever as a team.",
"draft_words": "81",
"cliche_count": "13"
}
Reply:
{
"task": "podium",
"verdict": "rephrase",
"title": "The Answer On The Corner Kicks",
"summary": "Jess's answer runs long and leans on stock coachspeak without ever getting to the corner kicks the question asked about. It names nothing from the game, so the cliches stand without anything specific behind them. The rewrite keeps Jess's point but trims it down and answers the question directly.",
"issues": [
{
"rule": "too-long",
"note": "The answer runs well past the 60-word limit for a draft response."
},
{
"rule": "dodge",
"note": "The answer never addresses what went wrong on the corner kicks, the question that was asked."
},
{
"rule": "all-cliche",
"note": "The answer relies on stock phrases throughout and does not name anything from the moments of the game."
}
],
"rewrite": "Credit to Riverside, they scored two of their goals off corner kicks and we did not defend them well enough. That is on us, and we will keep working on it. I am proud of how hard the team competed everywhere else, and we will keep building from here.",
"headline": "Jess Dodges Corner Kick Question With Cliches",
"soundbite": ""
}
podium - a jab at the umpire: walk-it-back
Input:
{
"task": "podium",
"speaker": "Robin",
"role": "captain",
"side": "Late Swings",
"opponent": "Gravel Pit",
"activity": "Tuesday night beer-league softball",
"score_us": "11",
"score_them": "12",
"result": "loss",
"persona": "fiery",
"room": "tough",
"question_count": "6",
"off_limits": "[\"Theo's knee\"]",
"held_back": "1",
"moments": "[{\"id\":\"M1\",\"kind\":\"contested\",\"text\":\"Dana was called out at home on a tag the whole bench saw miss\"},{\"id\":\"M2\",\"kind\":\"highlight\",\"text\":\"Priya hit two triples\"},{\"id\":\"M3\",\"kind\":\"miss\",\"text\":\"we left the bases loaded in the last inning\"},{\"id\":\"M4\",\"kind\":\"highlight\",\"text\":\"Theo turned a double play\"},{\"id\":\"M5\",\"kind\":\"note\",\"text\":\"the game ran so long the field lights ... (391 characters)",
"question": "Was that the right call at home?",
"draft": "Absolutely not. That umpire is blind and probably had money on Gravel Pit. Put that in the paper.",
"draft_words": "18",
"cliche_count": "0"
}
Reply:
{
"task": "podium",
"verdict": "walk-it-back",
"title": "The Answer On The Home Call",
"summary": "Robin's answer on the call at home needs a rewrite before it goes out. It calls the umpire blind and says the umpire probably had money on Gravel Pit, which questions the umpire's honesty rather than describing the call.",
"issues": [
{
"rule": "dig",
"note": "Robin's draft calls the umpire blind, which mocks the official."
},
{
"rule": "dig",
"note": "Robin's draft says the umpire probably had money on Gravel Pit, which questions the official's honesty."
}
],
"rewrite": "No, I don't think that was the right call. The whole bench saw that tag miss at home.",
"headline": "Robin Says Umpire Blind, Probably Had Money On Gravel Pit",
"soundbite": ""
}
Build the input yourself
Load the script in node (global.window = global; require("./presskit.js")), then:
const K = PressKit;
const form = K.form({ speaker: "Robin", role: "captain", side: "Late Swings", opponent: "Gravel Pit", activity: "beer-league softball",
score_us: "11", score_them: "12", persona: "fiery", room: "tough", off_limits: "Theo's knee",
moments: "call: Dana was called out at home\ngood: Priya hit two triples\nTheo's knee was sore",
question: "Was that the right call?", draft: "From where I stood the tag missed." });
console.log(K.flags("presser", form), K.leanPresser(form), K.moments(form).held); // the flags, the page's lean, the held moments
const wire = K.toWire(K.buildInput("presser", form).input); // POST this to /estimate and /run ("podium" adds the answer)
// then: const presser = K.normalize(K.parseReply(job.output.output), "presser");
// const { notes } = K.check(presser, form); // the page's re-check of the reply
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://no-further-questions.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://no-further-questions.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"]
// no-further-questions.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://no-further-questions.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://no-further-questions.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
}
// NoFurtherQuestions.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 NoFurtherQuestions {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://no-further-questions.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://no-further-questions.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://no-further-questions.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://no-further-questions.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":"no-further-questions"}' | 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": "no-further-questions"})["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: "no-further-questions" })).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": "no-further-questions"}, 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", "no-further-questions"), 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: "no-further-questions" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "no-further-questions"])["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 = "no-further-questions" }))!["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": "presser", "speaker": "Robin", "role": "captain", "side": "Late Swings", "opponent": "Gravel Pit", "activity": "Tuesday night beer-league softball", "score_us": "11", "score_them": "12", "result": "loss", "persona": "fiery", "room": "tough", "question_count": "6", "off_limits": "[\"Theo's knee\"]", "held_back": "1", "moments": "[{\"id\":\"M1\",\"kind\":\"contested\",\"text\":\"Dana was called out at home on a tag the whole bench saw miss\"},{\"id\":\"M2\",\"kind\":\"highlight\",\"text\":\"Priya hit two triples\"}, ...]"}
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": "presser", "speaker": "Robin", "role": "captain", "side": "Late Swings", "opponent": "Gravel Pit", "activity": "Tuesday night beer-league softball", "score_us": "11", "score_them": "12", "result": "loss", "persona": "fiery", "room": "tough", "question_count": "6", "off_limits": "[\"Theo's knee\"]", "held_back": "1", "moments": "[{\"id\":\"M1\",\"kind\":\"contested\",\"text\":\"Dana was called out at home on a tag the whole bench saw miss\"},{\"id\":\"M2\",\"kind\":\"highlight\",\"text\":\"Priya hit two triples\"}, ...]"}
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: "presser", speaker, role, activity, score_us, score_them, persona, room, moments, ... (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 no-further-questions:<task>:<inputHash>:a<attempt> (the lanes are presser and podium), 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="no-further-questions:presser:$(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 = "no-further-questions:" + 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 = `no-further-questions:${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 := "no-further-questions:" + 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 = "no-further-questions:" + 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 = "no-further-questions:#{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["headline"]].inspect
$key = "no-further-questions:" . $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["headline"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"no-further-questions:{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: no-further-questions:presser:$(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": "presser", "speaker": "Robin", "role": "captain", "side": "Late Swings", "opponent": "Gravel Pit", "activity": "Tuesday night beer-league softball", "score_us": "11", "score_them": "12", "result": "loss", "persona": "fiery", "room": "tough", "question_count": "6", "off_limits": "[\"Theo's knee\"]", "held_back": "1", "moments": "[{\"id\":\"M1\",\"kind\":\"contested\",\"text\":\"Dana was called out at home on a tag the whole bench saw miss\"},{\"id\":\"M2\",\"kind\":\"highlight\",\"text\":\"Priya hit two triples\"}, ...]"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "no-further-questions:presser:" + 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": `no-further-questions:presser:${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", "no-further-questions:presser:"+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", "no-further-questions:presser:" + 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" => "no-further-questions:presser:#{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: no-further-questions:presser:" . 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", $"no-further-questions:presser:{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);