← No Further Questions
API
Tokens

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.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field such as task, speaker or moments is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both lanes are metered and need a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. 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:

LaneOrderVerdictWhen
presser1hot-seatAt least one moment is contested. Q1 asks about the first contested moment and a later question follows up on it.
presser2post-mortemThe result is a loss. Q1 asks what went wrong; a question covers a miss if there is one.
presser3easy-roomOtherwise. Q1 asks about the best highlight, or the result.
podium1walk-it-backThe answer takes a dig at a person, touches an off-limits topic, or swears. A different answer comes back; no soundbite.
podium2rephraseOver 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.
podium3on-the-recordNone 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.

FieldTypeMeaning
taskstringpresser or podium. Required.
speakerstringWho is at the podium, up to 30 characters. Required.
rolestringcoach, captain, player or organizer.
side, opponentstringThe two sides' names as typed; either may be empty.
activitystringWhat was played, up to 80 characters.
score_us, score_them, resultstringWhole numbers, or both empty; result is win, loss, draw or empty, worked out by the page.
personastringgracious, deadpan, cliche, fiery or rookie.
room, question_countstringfriendly, mixed or tough, and the matching count "4", "5" or "6".
off_limitsstring (JSON)Topics as typed, up to 10. Moments touching one are already removed.
held_backstringHow many moments the page withheld.
momentsstring (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, draftstringpodium only: the reporter's question (up to 240 characters) and the answer to judge (up to 900).
draft_words, cliche_countstringpodium only: the answer's length in words and the number of stock phrases, both counted by the page.
clip_note, retry_notestringWhat 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"}
}

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')

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}' 

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}' 

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}' 

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