Drive Line Score from your own code
Both metered lanes are available over HTTP. outline takes a sports podcast episode's settings, the hosts' talking points and listener questions, and returns a full rundown: segments with minutes, a lead host, beats, takes, both sides of each debate and the listener question each mail segment answers, plus a cold open, three title ideas, a fact-check list and show notes. tighten takes a rundown and returns one keep / trim / cut / move call per segment, the new total and a verdict. A natural use is a show-prep bot in a hosts' group chat, or a producer's script that checks every episode against the same house rules before recording.
The division of labour first: the page owns the board, the clock and the house checks; the model does the editorial work; the page re-checks the reply. The line score, start times, talk-time split and every house check come from scorekit.js, which runs in node as well as in the browser. Its check function holds each reply to the board and the notes: one call per segment, minutes that add up, a verdict that follows the calls, a draft that passes the house checks, listener questions that were really asked, and no number in prose that is not in the notes.
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":"line-score"}), 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, target_minutes or hosts 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 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
Every run sends task: outline or tighten. One system prompt routes on it; a missing or unknown task is treated as outline. A tighten verdict is decided by three ordered, non-overlapping rules over the calls; the first that matches wins:
| Order | Verdict | When |
|---|---|---|
| 1 | restructure | Any call is move - a structural house check was broken (open not first, close not last, an ad before 8:00 or last, two ads under 8 minutes apart). |
| 2 | tighten | Otherwise, any call is trim or cut - the total was more than 10% over the target, or a segment ran past 15 minutes. |
| 3 | ready | Every call is keep. |
The declared input schema carries scalars only, so array-valued fields travel JSON-encoded as strings (ScoreKit.toWire does this). Every value is a string.
Input fields
| Field | Type | Notes |
|---|---|---|
task | string | outline or tighten. |
show, occasion | string | The show's name and the game or night; may be empty. |
sport | string | baseball, basketball, football, hockey, soccer or other. |
kind | string | preview, recap, debate or mailbag. |
target_minutes | string | The episode length to aim for, 10-180. Required. |
hosts | string (JSON) | Array of up to four host names. Required. |
ad_breaks | string | How many ad breaks the episode needs, 0-4. |
notes | string | Talking points, one per line. Past 3,000 characters the middle lines are cut with a marker. |
mail | string | Listener questions, one per line; past 1,500 characters the later questions are left out with a count. |
rundown | string (JSON) | tighten only: [{n, start, title, kind, minutes, lead, take, sides, question, beats}] in running order; kind is open, topic, debate, take, mail, ad or close. |
page_total, page_flags | string, string (JSON) | tighten only: the rundown's minutes and the house checks it fails. |
retry_note | string | Only on a reformat retry. |
The replies
The job's output.output is a string holding one JSON object. For outline: task, title, titles (three ideas), cold_open, segments ([{title, kind, minutes, lead, beats, take, sides, question}]), verify (facts from the notes to check) and show_notes. For tighten: task, verdict, title, summary, calls ([{segment, call, minutes, to, why}], one per segment; to only on move) and new_total. Parse with ScoreKit.parseReply, then normalize(reply, task), then check(reply, form, rundown), which returns the page's notes (and, for tighten, the verdict by the calls and the rundown after them).
Worked examples
Replies produced against the app's prompt for the four sample shows (made-up shows, teams, players and notes; long fields and lists shortened; the full replies ship in example-result.js). Together they cover both lanes and every verdict.
outline - title-series Game 1 preview, drafted from talking points
Input:
{
"task": "outline",
"show": "Two Outs, Rally",
"sport": "baseball",
"kind": "preview",
"occasion": "Title series, Game 1 preview - Harbor City Gulls vs Prairie Comets",
"target_minutes": "45",
"hosts": "[\"Dee\",\"Marco\"]",
"ad_breaks": "2",
"notes": "Neither team has ever won a title - somebody's drought ends this week.\nGulls swept the semifinal 4-0. Comets needed all 7 games and won Game ... (723 characters)",
"mail": "Kim from Eastport: who's the X-factor nobody is talking about?\nJay: should the Gulls start ... (176 characters)"
}
Reply:
{
"task": "outline",
"title": "Title Series Game 1: Legs vs. Arms",
"titles": [
"Someone's Drought Ends This Week",
"Sweep vs. Game 7: The Opener",
"Legs vs. Arms: Previewing Game 1"
],
"cold_open": "Neither the Gulls nor the Comets has ever won a title - that drought ends this week. We're breaking down Game 1, Harbor City Gulls vs. Prairie Comets, before first pitch.",
"segments": [
{
"title": "Open: No One's Ever Won This",
"kind": "open",
"minutes": 3,
"lead": "all",
"beats": [
"Say it straight: neither the Gulls nor the Comets has ever won a title - someone's drought ends this week.",
"Set the stage: Game 1 of the title series, Harbor City Gulls vs. Prairie Comets.",
"Tease the rundown: how each team got here, the Game 1 starters, the stolen-base vs. bullpen fight, and your picks."
]
},
{
"title": "How They Got Here",
"kind": "topic",
"minutes": 6,
"lead": "Marco",
"beats": [
"Gulls swept the semifinal 4-0.",
"Comets needed all 7 games and won Game 7 on a walk-off.",
"Ask what that contrast - a clean sweep vs. a grind to Game 7 - says about each team walking into Game 1."
]
},
{
"title": "Game 1 on the Mound",
"kind": "topic",
"minutes": 7,
"lead": "Dee",
"beats": [
"Gulls send Rafa Ortiz, who has a 2.41 ERA in three playoff starts.",
"Comets counter with lefty Nate Pryor on three days' rest.",
"Gulls haven't played in six days - debate whether that's rest or rust."
]
},
"... (11 in all)"
],
"verify": [
"Gulls swept the semifinal 4-0; Comets won Game 7 on a walk-off.",
"Rafa Ortiz has a 2.41 ERA in three playoff starts; Nate Pryor goes on three days' rest.",
"... (5 in all)"
],
"show_notes": "Game 1 of the title series is here - Harbor City Gulls vs. Prairie Comets - and one franchise's drought ends this week. Dee and Marco break down the semifinal runs (a Gulls sweep vs. a Comets Game 7 walk-off), the Ortiz-Pryor pitching matchup, and whether steals or bullpens decide the series. Plus listener mail on the X-factor nobody's talking about and a short-rest question for Game 4."
}
tighten - opener recap, ready
Input:
{
"task": "tighten",
"show": "Full Court Press Box",
"sport": "basketball",
"kind": "recap",
"occasion": "Opening night recap - the Riverside Owls' first game of the season",
"target_minutes": "40",
"hosts": "[\"Priya\",\"Sam\"]",
"ad_breaks": "1",
"notes": "Owls won the opener by 9. New point guard Theo Banks had 14 assists in his debut. Bench scored only 18 points. Rookie Malik Osei played 11 m ... (189 characters)",
"mail": "Dana: is Theo Banks already the best passer we've had?",
"rundown": "[{\"n\":1,\"start\":\"0:00\",\"title\":\"Opening night, first impressions\",\"kind\":\"open\",\"minutes\":3,\"lead\":\"all\",\"beats\":[\"The win in one sentence each\",\"What surprised us\"]},{\"n\":2,\"start\":\"3:00\",\"title\":\"Theo Banks runs the show\",\"kind\":\"topic\",\"minutes\":10,\"lead\":\" ... (1110 characters)",
"page_total": "40",
"page_flags": "[]"
}
Reply:
{
"task": "tighten",
"verdict": "ready",
"title": "Owls Opening Night Recap: Ready To Go",
"summary": "Your rundown already lands right on your target, and every check the board runs comes back clean. The open leads off, the close wraps things up, your ad break sits well clear of both edges, and Priya and Sam split the talk time evenly. Nothing here needs trimming, cutting, or moving - you're ready to record.",
"calls": [
{
"segment": 1,
"call": "keep",
"minutes": 3,
"why": "The open comes in short and sits first, exactly where it needs to be."
},
{
"segment": 2,
"call": "keep",
"minutes": 10,
"why": "Ten minutes keeps this well under your 15-minute limit, so no time fix is needed."
},
{
"segment": 3,
"call": "keep",
"minutes": 2,
"why": "Your ad break already starts well past 8 minutes and runs a clean two minutes, so it stays put."
},
{
"segment": 4,
"call": "keep",
"minutes": 10,
"why": "Ten minutes keeps the debate under the limit with both sides already covered."
},
"... (7 in all)"
],
"new_total": 40
}
tighten - rivalry recap 17 minutes long, tighten
Input:
{
"task": "tighten",
"show": "Blue Line Banter",
"sport": "hockey",
"kind": "recap",
"occasion": "Rivalry night recap - Northport Pike lose 5-3 after leading 3-1",
"target_minutes": "45",
"hosts": "[\"Jo\",\"Kev\",\"Tash\"]",
"ad_breaks": "2",
"notes": "Pike led 3-1 after two periods and gave up four in the third. Starting goalie pulled after the fourth goal. Coach has lost five straight to ... (284 characters)",
"mail": "",
"rundown": "[{\"n\":1,\"start\":\"0:00\",\"title\":\"Cold open: what just happened\",\"kind\":\"open\",\"minutes\":4,\"lead\":\"all\"},{\"n\":2,\"start\":\"4:00\",\"title\":\"The third-period collapse, shift by shift\",\"kind\":\"topic\",\"minutes\":18,\"lead\":\"Jo\",\"beats\":[\"Goal by goal through the third\",\" ... (1274 characters)",
"page_total": "62",
"page_flags": "[\"The rundown runs 62 min against a 45-minute target - 17 min over.\",\"Inning at 4:00 (The third-period collapse, shift by shift) runs 18 min - past 15, split it ... (174 characters)"
}
Reply:
{
"task": "tighten",
"verdict": "tighten",
"title": "Rivalry Night Recap: Trimmed And Cut",
"summary": "The third-period collapse segment ran past the 15-minute cap and was most of why the show ran so far over your target, so it gets trimmed down. The around-the-league segment is cut outright - your own notes say you didn't watch those games closely, which makes it the easiest minutes to lose rather than cutting into the collapse, the goalie question, the coach debate, or Tash's take. Everything else stays as you built it; the show now lands close to your target.",
"calls": [
{
"segment": 1,
"call": "keep",
"minutes": 4,
"why": "The open is already short and doesn't need to change."
},
{
"segment": 2,
"call": "trim",
"minutes": 10,
"why": "This was the longest segment and ran past the 15-minute cap; trimming it is what brings the show back within your target."
},
{
"segment": 3,
"call": "keep",
"minutes": 2,
"why": "This ad break is already the right length and well placed."
},
{
"segment": 4,
"call": "keep",
"minutes": 12,
"why": "Not driving the overage, so it stays as is."
},
"... (9 in all)"
],
"new_total": 49
}
tighten - solo mailbag that opens on an ad, restructure
Input:
{
"task": "tighten",
"show": "Half-Time Oranges",
"sport": "soccer",
"kind": "mailbag",
"occasion": "Monday mailbag after the derby draw",
"target_minutes": "30",
"hosts": "[\"Lena\"]",
"ad_breaks": "1",
"notes": "Solo episode. Three listener questions this week. My take: the cup run matters more than the league this season.",
"mail": "Ollie: is the new striker a bust already?\nMei: best away kit this season?\nSandeep: should ... (126 characters)",
"rundown": "[{\"n\":1,\"start\":\"0:00\",\"title\":\"Sponsor read\",\"kind\":\"ad\",\"minutes\":2},{\"n\":2,\"start\":\"2:00\",\"title\":\"Welcome to the mailbag\",\"kind\":\"open\",\"minutes\":2,\"lead\":\"Lena\"},{\"n\":3,\"start\":\"4:00\",\"title\":\"Is the new striker a bust?\",\"kind\":\"mail\",\"minutes\":7,\"lead\":\" ... (892 characters)",
"page_total": "30",
"page_flags": "[\"The first inning is not an open - listeners decide in the first minute whether to stay.\",\"The last inning is not a close - end on a sign-off, not mid-topic.\", ... (228 characters)"
}
Reply:
{
"task": "tighten",
"verdict": "restructure",
"title": "Derby Mailbag Gets A Restructure",
"summary": "Your ad break sits before the open and your sign-off gets stranded in the middle of the rundown, so this sends the ad back to start at 9:00, after your first mailbag question, and moves the sign-off to the very end. Everything else already fits your target, so every other segment keeps its spot and its time.",
"calls": [
{
"segment": 1,
"call": "move",
"minutes": 2,
"to": 3,
"why": "Moves the ad break so it starts at 9:00, after the 8:00 minimum, and leaves the welcome segment first."
},
{
"segment": 2,
"call": "keep",
"minutes": 2,
"why": "Already first once the ad break clears out, and it's paced fine."
},
{
"segment": 3,
"call": "keep",
"minutes": 7,
"why": "Runs fine at its current length."
},
{
"segment": 4,
"call": "move",
"minutes": 3,
"to": 7,
"why": "Moves the sign-off to the last spot so the show ends on a close, not mid-question."
},
"... (7 in all)"
],
"new_total": 30
}
Build the input yourself
Load the script in node (global.window = global; require("./scorekit.js")), then:
const K = ScoreKit;
const form = { show: "Two Outs, Rally", sport: "baseball", kind: "preview", occasion: "Game 1 preview",
target: "45", hosts: "Dee, Marco", ads: "2", notes: "Gulls swept the semifinal 4-0.\nMarco's pick: Comets in 6.", mail: "" };
const rundown = [{ title: "Open", kind: "open", min: 3, lead: "all" }, { title: "How they got here", kind: "topic", min: 10, lead: "Marco" },
{ title: "Ad break", kind: "ad", min: 2 }, { title: "Picks", kind: "take", min: 5, lead: "Marco", take: "Comets in 6" },
{ title: "Close", kind: "close", min: 3, lead: "all" }];
console.log(K.flags(form, rundown)); // the house checks
console.log(K.lineScore(form, rundown).rows); // minutes per inning per host, R H E
const wire = K.toWire(K.buildInput("tighten", form, rundown).input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output), "tighten");
// const { notes, verdict, after } = K.check(reply, form, rundown); // the page's re-check, and the board after the calls
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://line-score.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://line-score.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"]
// line-score.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://line-score.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://line-score.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
}
// LineScore.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 LineScore {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://line-score.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://line-score.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://line-score.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://line-score.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":"line-score"}' | 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": "line-score"})["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: "line-score" })).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": "line-score"}, 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", "line-score"), 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: "line-score" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "line-score"])["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 = "line-score" }))!["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": "outline", "show": "Two Outs, Rally", "sport": "baseball", "kind": "preview", "occasion": "Title series, Game 1 preview ...", "target_minutes": "45", "hosts": "[\"Dee\",\"Marco\"]", "ad_breaks": "2", "notes": "Neither team has ever won a title ...", "mail": "Kim from Eastport: ..."}
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": "outline", "show": "Two Outs, Rally", "sport": "baseball", "kind": "preview", "occasion": "Title series, Game 1 preview ...", "target_minutes": "45", "hosts": "[\"Dee\",\"Marco\"]", "ad_breaks": "2", "notes": "Neither team has ever won a title ...", "mail": "Kim from Eastport: ..."}
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: "outline", hosts, notes, mail, ... (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 line-score:<task>:<inputHash>:a<attempt> (the lanes are outline and tighten), 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="line-score:outline:$(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 = "line-score:" + 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 read as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
import { createHash } from "node:crypto";
const key = `line-score:${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 := "line-score:" + 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 = "line-score:" + 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 = "line-score:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["title"]].inspect
$key = "line-score:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["title"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"line-score:{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: line-score:outline:$(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": "outline", "show": "Two Outs, Rally", "sport": "baseball", "kind": "preview", "occasion": "Title series, Game 1 preview ...", "target_minutes": "45", "hosts": "[\"Dee\",\"Marco\"]", "ad_breaks": "2", "notes": "Neither team has ever won a title ...", "mail": "Kim from Eastport: ..."}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "line-score:outline:" + 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": `line-score:outline:${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", "line-score:outline:"+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", "line-score:outline:" + 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" => "line-score:outline:#{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: line-score:outline:" . 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", $"line-score:outline:{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);