Drive A Hill To Die On from your own code
Everything the page asks the writer is available over HTTP. A natural use is a content calendar: a script sends a week of takes, gets back timed scripts and hooks, and drops the captions straight into an editor as SRT.
Be clear about the split first: the model writes; the caller times and checks. The page computes the word budget from the clip length and conviction (2.2 + 0.12 × conviction words a second), times each beat, and checks every reply against the clip, the no-hedging rule, the 16-word line cap and the take's own words. That code ships as a plain script — takekit.js (TakeKit.prescan, timeBeats, reconcile, toSrt) — you can load in node.
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":"hill-to-die-on"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}} — the declared input schema warns unknown field 'input' and the model sees nothing.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the writer is metered and needs a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
One system prompt serves two tasks. task picks the contract; send exactly one of:
| task | What comes back |
|---|---|
defend | The timed defense: beats (open, claim, evidence, escalation, callback, closer), one gesture per beat, 2–3 comment-section comebacks, the joke's anatomy, a caption and hashtags. |
hooks | Five opening hooks (contrarian, curiosity, callout, confession, stakes), each with on-screen text, the first frame, a 1–10 score and a critique, plus the best pick and what to avoid. Send the defense's lines as script_lines so the hooks lead into it. |
Input fields
| Field | Type | Notes |
|---|---|---|
task | string, required | defend or hooks. |
take | string, required | The opinion, at most 280 characters. |
tone | string | villa, closing, documentary or coach. |
conviction | number | 1 (reasonable) to 5 (unhinged). |
seconds | number | 15, 30, 45 or 60 - the clip length. |
word_budget | number | round(seconds × (2.2 + 0.12 × conviction)). |
prescan | string | JSON-encoded {category, spice, hedges, absolutes, is_question} from TakeKit.prescan. The schema carries scalars only, so objects travel encoded. |
script_lines | string | hooks only, optional: JSON-encoded array of the defense's lines, in order. |
retry_note | string | Only on a reformat retry. |
The reply
The job's output.output is a string holding one JSON object. Gestures are one of point, count, chest, pinch, shrug, palms, clap, lean, pause. Parse it, then check it — the page never trusts the reply's own arithmetic.
Worked examples
Real replies, produced against the app's prompt. The page replays each of them free from its Try buttons.
defend: "Cereal is a soup" - closing argument, conviction 4, 30 seconds
Input (as sent):
{
"task": "defend",
"take": "Cereal is a soup",
"tone": "closing",
"conviction": 4,
"seconds": 30,
"word_budget": 80,
"prescan": "{\"category\":\"food\",\"spice\":48,\"hedges\":[],\"absolutes\":[],\"is_question\":false}"
}
Reply:
{
"lane": "defend",
"title": "Closing Argument: Cereal Is Soup",
"take_restated": "Cereal is soup: cold, grain-based, and legally defensible.",
"beats": [
{
"kind": "open",
"line": "Members of the jury, the defense never doubted this case.",
"gesture": "point"
},
{
"kind": "claim",
"line": "Cereal is soup. Cold, grain-based, undeniable soup.",
"gesture": "chest"
},
{
"kind": "evidence",
"line": "Exhibit A: a bowl. Exhibit B: a spoon. Case closed.",
"gesture": "count"
},
{
"kind": "evidence",
"line": "Soup is solid food suspended in liquid. Milk is liquid.",
"gesture": "count"
},
{
"kind": "evidence",
"line": "Gazpacho is cold soup, and nobody calls it cereal.",
"gesture": "pinch"
},
{
"kind": "escalation",
"line": "If cereal is not soup, then soup is not real.",
"gesture": "lean"
},
{
"kind": "callback",
"line": "Exhibit A, the bowl, never once objected to this truth.",
"gesture": "point"
},
{
"kind": "closer",
"line": "The defense rests, and so should your spoon.",
"gesture": "pause"
}
],
"closer": "The defense rests, and so should your spoon.",
"counterpoints": [
{
"objection": "Soup is hot. Cereal is cold.",
"comeback": "Gazpacho is served cold and still called soup."
},
{
"objection": "You don't call cereal soup on a diner menu.",
"comeback": "Diners also call hot dogs sandwiches. Menus lie constantly."
},
{
"objection": "Milk isn't broth.",
"comeback": "Broth is just milk that chose a savory life."
}
],
"joke": {
"premise": "Soup is defined by its format, solid food in liquid in a bowl, not by temperature or flavor.",
"mechanism": "category error argued by strict definition, applying soup's structural rules while ignoring its cultural assumptions.",
"misdirection": "The audience expects the lawyer to concede cereal is just breakfast, and instead he cross-examines soup itself.",
"callback": "Exhibit A, the bowl, returns in the second-to-last beat as a silent witness.",
"why_it_lands": "It argues a silly breakfast question with real courtroom gravity, so the mismatch in stakes is the joke."
},
"caption": "Closing arguments: cereal was soup this whole time, and the bowl knew it.",
"hashtags": [
"twofishes",
"hottake",
"cerealisoup",
"courtroomdrama",
"breakfastcourt"
],
"notes": ""
}
defend: a hedged take on a 15-second clip
The take arrives with "I think"; the prescan lists it, and the reply's take_restated drops it. At conviction 5 the 42-word budget times at a little over 15 seconds, so the page warns if the beats overrun by more than 15%.
{
"task": "defend",
"take": "I think voicemails should be illegal",
"tone": "villa",
"conviction": 5,
"seconds": 15,
"word_budget": 42,
"prescan": "{\"category\":\"etiquette\",\"spice\":45,\"hedges\":[\"i think\"],\"absolutes\":[\"illegal\"],\"is_question\":false}"
}
Reply:
{
"lane": "defend",
"title": "The Case Against Voicemail",
"take_restated": "Voicemails should be illegal.",
"beats": [
{
"kind": "open",
"line": "I am genuinely so hurt right now.",
"gesture": "chest"
},
{
"kind": "claim",
"line": "Voicemails should be illegal. Full stop.",
"gesture": "point"
},
{
"kind": "evidence",
"line": "You had my number. You had texting.",
"gesture": "count"
},
{
"kind": "evidence",
"line": "Instead you made me do audio homework.",
"gesture": "pinch"
},
{
"kind": "escalation",
"line": "I will go to actual prison over this.",
"gesture": "lean"
},
{
"kind": "closer",
"line": "Delete your voicemail and keep our friendship.",
"gesture": "palms"
}
],
"closer": "Delete your voicemail and keep our friendship.",
"counterpoints": [
{
"objection": "Some people just hate texting.",
"comeback": "Then they can send a pigeon like a normal person."
},
{
"objection": "Voicemail-to-text makes it easy now.",
"comeback": "That just proves the voicemail was never necessary."
},
{
"objection": "What if it's an emergency?",
"comeback": "Then call back. Twice. That's what phones are for."
}
],
"joke": {
"premise": "That any message slower than a text is a personal betrayal.",
"mechanism": "Category error - a minor inconvenience prosecuted with full legal seriousness.",
"misdirection": "The audience expects the speaker to admit it's a small thing, and instead she asks for jail time.",
"callback": "",
"why_it_lands": "The gap between the tiny annoyance and the courtroom-grade outrage is the whole joke."
},
"caption": "Voicemails should be illegal. That's not my opinion, that's the law now.",
"hashtags": [
"twofishes",
"hottake",
"voicemail",
"etiquette",
"phoneproblems"
],
"notes": ""
}
hooks: handed off from the cereal defense
{
"task": "hooks",
"take": "Cereal is a soup",
"tone": "closing",
"conviction": 4,
"seconds": 30,
"word_budget": 80,
"prescan": "{\"category\":\"food\",\"spice\":48,\"hedges\":[],\"absolutes\":[],\"is_question\":false}",
"script_lines": "[\"Members of the jury, the defense never doubted this case.\",\"Cereal is soup. Cold, grain-based, undeniable soup.\",\"Exhibit A: a bowl. Exhibit B: a spoon. Case closed.\",\"Soup is solid food suspended in liquid. Milk is liquid.\",\"Gazpacho is cold soup, and nobody calls it cereal.\",\"If cereal is not soup, then soup is not real.\",\"Exhibit A, the bowl, never once objected to this truth.\",\"The defense rests, and so should your spoon.\"]"
}
Reply:
{
"lane": "hooks",
"hooks": [
{
"type": "contrarian",
"text": "Everyone's wrong: cereal was legally a soup the whole time.",
"on_screen_text": "The verdict nobody wanted",
"first_frame": "Speaker in a blazer bangs a toy gavel on the kitchen table.",
"score": 7,
"critique": "Leads with a meta-claim about being right instead of a visual image, so it lands a beat slower than it should."
},
{
"type": "curiosity",
"text": "There's a legal reason cereal has always been soup.",
"on_screen_text": "Wait... cereal is WHAT?",
"first_frame": "Speaker leans toward the camera, raising one finger before speaking.",
"score": 8,
"critique": "The vague tease only pays off if the reveal comes fast, or it reads as empty clickbait."
},
{
"type": "callout",
"text": "If you've ever grabbed a spoon for cereal, you're guilty too.",
"on_screen_text": "You already proved my case",
"first_frame": "Speaker points directly at the lens like a prosecutor accusing the jury.",
"score": 7,
"critique": "Only lands for viewers who already eat cereal with a spoon and milk, narrowing who feels called out."
},
{
"type": "confession",
"text": "I'll admit it: I've always known cereal was soup.",
"on_screen_text": "My shameful cereal secret",
"first_frame": "Speaker exhales, hand on heart, before confessing to the jury.",
"score": 6,
"critique": "The confession framing softens the courtroom's unshakable conviction before the gavel even drops."
},
{
"type": "stakes",
"text": "If cereal isn't soup, the entire food pyramid collapses tonight.",
"on_screen_text": "Breakfast as we know it",
"first_frame": "Speaker slams both palms on the table, eyes wide with alarm.",
"score": 8,
"critique": "The food-pyramid metaphor is a half-step removed from the bowl itself, costing a beat of clarity."
}
],
"best_index": 1,
"why_best": "The curiosity hook opens a specific legal loop that only a courtroom verdict can close, pulling scrollers straight into the jury address that follows.",
"avoid": [
"A dictionary-definition hook ('cereal, noun...') reads like trivia, not an accusation, and won't survive the first second of a scroll.",
"A hedge-softened opener that leaves room for doubt kills the courtroom's total conviction before the gavel falls.",
"A generic 'you won't believe this' tease that never names cereal or soup fails the hook contract and wastes the first frame."
]
}
The page checks the reply; the API does not
Load takekit.js in node (global.window = {}; require("./takekit.js")) and call TakeKit.normalize(TakeKit.parseResult(text), {task}) then TakeKit.reconcile(res, {take, seconds, conviction}). It returns notes for an overrun clip, hedging in a line, lines past 16 words, a defense that never names the take, hooks past 12 words or on-screen text past 7, and missing hook types. TakeKit.toSrt(TakeKit.timeBeats(res.beats, conviction)) gives captions.
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://hill-to-die-on.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://hill-to-die-on.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"]
// hill-to-die-on.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://hill-to-die-on.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://hill-to-die-on.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
}
// HillToDieOn.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 HillToDieOn {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://hill-to-die-on.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://hill-to-die-on.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://hill-to-die-on.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://hill-to-die-on.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":"hill-to-die-on"}' | 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": "hill-to-die-on"})["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: "hill-to-die-on" })).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": "hill-to-die-on"}, 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", "hill-to-die-on"), 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: "hill-to-die-on" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "hill-to-die-on"])["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 = "hill-to-die-on" }))!["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": "defend", "take": "Cereal is a soup", "tone": "closing", "conviction": 4, "seconds": 30, "word_budget": 80, "prescan": "{...}"}
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": "defend", "take": "Cereal is a soup", "tone": "closing", "conviction": 4, "seconds": 30, "word_budget": 80, "prescan": "{...}"}
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, take, tone, conviction, seconds, word_budget, prescan, script_lines?}
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 hill-to-die-on:<task>:<inputHash>:a<attempt>, 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="hill-to-die-on:defend:$(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 = "hill-to-die-on:" + 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 lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("closer"))
import { createHash } from "node:crypto";
const key = `hill-to-die-on:${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.closer);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "hill-to-die-on:" + 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 = "hill-to-die-on:" + 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 = "hill-to-die-on:#{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["closer"]].inspect
$key = "hill-to-die-on:" . $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["closer"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"hill-to-die-on:{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: hill-to-die-on:defend:$(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": "defend", "take": "Cereal is a soup", "tone": "closing", "conviction": 4, "seconds": 30, "word_budget": 80, "prescan": "{...}"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "hill-to-die-on:defend:" + 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": `hill-to-die-on:defend:${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", "hill-to-die-on:defend:"+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", "hill-to-die-on:defend:" + 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" => "hill-to-die-on:defend:#{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: hill-to-die-on:defend:" . 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", $"hill-to-die-on:defend:{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);