Drive Liner Clues from your own code
The metered lane is available over HTTP: the clue deck. Send a list of artists for a guess-the-artist round, with optional notes on how each one sounds, and get back one round per artist: a status, a ladder of up to five clues that describe the sound and never the name (hardest first, so an early guess scores more), a one-line description of the sound, short phrases you could hand to an AI music generator, and the clue the host should double-check. A natural use is a quiz-night tool, a party game or a music-club bot that wants fresh rounds without writing clues by hand.
The division of labour first: the page cleans the list and keeps score; the model writes the clues; the page re-checks every clue. The list (cleaned, de-duplicated, capped at eight rounds, sound traits counted in each note, one-word names flagged) and the game (reveal one clue at a time, 5 points down to 1, a scoreboard and a podium) come from cluekit.js, which runs in node as well as in the browser. Its check function holds each reply to the list: a word of the artist's name inside a clue, capitalised words that may be names, numbers that are not decades, internal field names in prose, and the clue count and sources each status allows.
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":"liner-clues"}), 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": {…}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field such as task is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the deck 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
Every run sends task: deck, the only lane. The reply holds one round per artist, each with one status:
| Status | When | Clues |
|---|---|---|
ready | The model knows which act the name means (one-word names count as known unless two or more well-known acts share them). | Exactly 5: rungs 1-4 each from a different dimension (rhythm, harmony, instruments, production, fusion, energy), rung 5 usually era. Every clue is source: "general" (the notes are only hints; the host checks every clue). |
notes_only | The model does not know the act, but the notes describe two or more sound traits. | 2 to 5, never more than the traits in the notes, every one source: "notes". |
need_notes | The model does not know the act (or the name fits several acts) and the notes describe fewer than two sound traits. | None; ask says what to add. |
The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (ClueKit.toWire does this). Every value is a string.
Input fields: deck
| Field | Type | Notes |
|---|---|---|
task | string | deck. |
level | string | casual (a mixed crowd), mixed or nerd (music obsessives: the hardest clue may be a production or technique detail). |
crowd | string | Optional words about who is playing, at most 160 characters. |
artists | string (JSON) | [{n, name, notes, note_traits}] in round order, at most 8, from ClueKit.parseArtists: a name up to 80 characters, notes up to 300 (longer notes are cut on a word with a [...] marker), and the page's rough count of sound traits in the notes. |
page_flags | string (JSON) | [{level, text}] with level fix / warn / info: duplicates dropped, lines past the eighth left out, one-word names with no notes. |
retry_note | string | Only on a reformat retry. |
The reply
The job's output.output is a string holding one JSON object: {task, title, summary, rounds: [{n, name, status, dna, clues: [{rung, dimension, text, source}], prompt_elements, host_check, ask}]}. Parse it with ClueKit.parseReply, then normalize(reply), then check(reply, state), which returns {notes, byRound}: the page's notes (info, warn) shown under "The page's check" and, per round, in the host peek.
Worked examples
Replies produced against the app's prompt for the three sample rounds (real acts appear only as answers to guess; two invented local bands; long fields and lists shortened; the full replies ship in example-result.js). Together they cover every status. The page replays each of them free from its sample buttons.
task: deck - family party - four rounds ready
Input:
{
"task": "deck",
"level": "casual",
"crowd": "three generations, ages 9 to 82",
"artists": "[{\"n\":1,\"name\":\"Louis Armstrong\",\"notes\":\"gravelly singing voice; trumpet solos; New Orleans jazz\",\"note_traits\":3},{\"n\":2,\"name\":\"ABBA\",\"notes\":\"\",\"note_traits\":0},{\"n\":3,\"name\":\"Daft Punk\",\"notes\":\"robot helmets; vocoder vocals; French house\",\"note_traits\":3 ... (333 characters)",
"page_flags": "[{\"level\":\"info\",\"text\":\"\\\"ABBA\\\" is a single word with no notes - if more than one act goes by it, add a note (\\\"the 90s rock band\\\") so the clues hit the righ ... (169 characters)"
}
Reply:
{
"task": "deck",
"title": "Three-Generation Family Record Round",
"summary": "All four rounds are ready for tonight - skim the host-check line on each one before you start so you catch anything worth double-checking.",
"rounds": [
{
"n": 1,
"name": "Louis Armstrong",
"status": "ready",
"dna": "A warm, gravel-edged voice trades lines with virtuoso brass over bright, major-key horn harmonies, all captured in the close, softly hissy warmth of an early studio while a bouncy, strutting rhythm carries the beat underneath.",
"clues": [
{
"rung": 1,
"dimension": "production",
"text": "The recording has a warm, boxed-in closeness, as if the whole band gathered around a single microphone, with a soft hiss humming under every note.",
"source": "general"
},
{
"rung": 2,
"dimension": "harmony",
"text": "Horns stack into bright, cheerful major-key harmonies that trade short phrases back and forth like a friendly conversation.",
"source": "general"
},
"..."
],
"prompt_elements": [
"gravelly, scat-singing voice",
"soaring trumpet solos",
"bright major-key horn harmony",
"..."
],
"host_check": "Check rung 1's claim about the single-microphone recording setup before reading it out.",
"ask": ""
},
{
"n": 2,
"name": "ABBA",
"status": "ready",
"dna": "A pair of richly layered female voices glide over glossy, string-and-synth production and bittersweet major-to-minor harmonies, all riding a polished, dance-ready pulse.",
"clues": [
{
"rung": 1,
"dimension": "production",
"text": "Women's voices are multi-tracked into a thick, glossy stack of harmony, polished with a bright, layered studio sheen.",
"source": "general"
},
{
"rung": 2,
"dimension": "harmony",
"text": "Melodies ride on lush, major-to-minor chord shifts that turn bittersweet in the middle of an otherwise sunny tune.",
"source": "general"
},
"..."
],
"prompt_elements": [
"multi-tracked female vocal harmony",
"glossy, layered pop production",
"bittersweet major-to-minor melodies",
"..."
],
"host_check": "Check rung 3's claim about the shimmering strings and synth flourishes before reading it out.",
"ask": ""
},
{
"n": 3,
"name": "Daft Punk",
"status": "ready",
"dna": "Robotically vocoded vocals ride over a relentless four-on-the-floor dance pulse, filtered disco-funk basslines, and bright, buzzing synthesizer hooks, all polished with crisp, club-ready electronic production.",
"clues": [
{
"rung": 1,
"dimension": "production",
"text": "Vocals are run through a thick, robotic vocoder, turning every lyric into a buzzing, machine-smooth hum.",
"source": "general"
},
{
"rung": 2,
"dimension": "harmony",
"text": "Chords loop in short, hypnotic funk-influenced vamps, repeating and subtly mutating rather than resolving to a big finish.",
"source": "general"
},
"..."
],
"prompt_elements": [
"robotic vocoder vocals",
"four-on-the-floor dance beat",
"filtered disco-funk basslines",
"..."
],
"host_check": "Check rung 1's claim about the vocoder effect before reading it out.",
"ask": ""
},
"... (1 more round)"
]
}
task: deck - friends' bands night - ready, from your notes, needs notes
Input:
{
"task": "deck",
"level": "mixed",
"crowd": "",
"artists": "[{\"n\":1,\"name\":\"Fleetwood Mac\",\"notes\":\"\",\"note_traits\":0},{\"n\":2,\"name\":\"The Upstairs Neighbours\",\"notes\":\"our friends' band; banjo and a brass section; every song is about trains; recorded live in a church hall\",\"note_traits\":5},{\"n\":3,\"name\":\"Glasswing Para ... (346 characters)",
"page_flags": "[{\"level\":\"info\",\"text\":\"\\\"Kraftwerk\\\" is a single word with no notes - if more than one act goes by it, add a note (\\\"the 90s rock band\\\") so the clues hit the ... (174 characters)"
}
Reply:
{
"task": "deck",
"title": "Tonight's Guess-the-Artist Round",
"summary": "Two rounds are ready to play, one is built entirely from your notes, and one still needs a couple of sound details before you can use it.",
"rounds": [
{
"n": 1,
"name": "Fleetwood Mac",
"status": "ready",
"dna": "A bright, fingerpicked guitar line and close three-part vocal harmony ride a steady, unhurried rock pulse, polished with a warm, glossy studio sheen.",
"clues": [
{
"rung": 1,
"dimension": "instruments",
"text": "A bright, fingerpicked guitar line weaves delicate, intricate patterns rather than simply strumming chords, giving the sound a crisp, melodic backbone.",
"source": "general"
},
{
"rung": 2,
"dimension": "harmony",
"text": "Three voices stack into close, bittersweet harmony, trading lead lines between a man and two women.",
"source": "general"
},
"..."
],
"prompt_elements": [
"fingerpicked melodic guitar",
"close three-part vocal harmony",
"steady unhurried rock groove",
"..."
],
"host_check": "Check rung 1's claim about the fingerpicked guitar line - it's truest of the band's most famous songs, so confirm it fits whichever songs get used tonight.",
"ask": ""
},
{
"n": 2,
"name": "The Upstairs Neighbours",
"status": "notes_only",
"dna": "Banjo and brass carry a live, train-themed set recorded in a church hall.",
"clues": [
{
"rung": 1,
"dimension": "instruments",
"text": "A banjo plucks away while a brass section punches in with bright horn hits.",
"source": "notes"
},
{
"rung": 2,
"dimension": "production",
"text": "The whole thing was recorded live in a church hall.",
"source": "notes"
},
"..."
],
"prompt_elements": [
"banjo",
"brass section",
"live church hall recording",
"..."
],
"host_check": "",
"ask": ""
},
{
"n": 3,
"name": "Glasswing Parade",
"status": "need_notes",
"dna": "",
"clues": [],
"prompt_elements": [],
"host_check": "",
"ask": "I don't know this act by name, so I can't tell which band you mean. Could you add two or three sound details - the instruments, the voice, the tempo or mood, or even a decade - or just note which band this is?"
},
"... (1 more round)"
]
}
task: deck - record-shop nerds - a name two acts have gone by
Input:
{
"task": "deck",
"level": "nerd",
"crowd": "record-shop staff after closing",
"artists": "[{\"n\":1,\"name\":\"Radiohead\",\"notes\":\"\",\"note_traits\":0},{\"n\":2,\"name\":\"Nirvana\",\"notes\":\"\",\"note_traits\":0},{\"n\":3,\"name\":\"Talking Heads\",\"notes\":\"jittery funk guitars; big suits\",\"note_traits\":2},{\"n\":4,\"name\":\"Nina Simone\",\"notes\":\"\",\"note_traits\":0}]",
"page_flags": "[{\"level\":\"info\",\"text\":\"\\\"Radiohead\\\" is a single word with no notes - if more than one act goes by it, add a note (\\\"the 90s rock band\\\") so the clues hit the ... (345 characters)"
}
Reply:
{
"task": "deck",
"title": "After-Hours Record Shop Guessing Round",
"summary": "Three rounds are ready to go as written; one needs a quick note from you before it can join the set.",
"rounds": [
{
"n": 1,
"name": "Radiohead",
"status": "ready",
"dna": "Shifting, often irregular rhythms support slowly drifting, unresolved harmonies, with a high, breathy voice woven through cold, occasionally glitch-edited production.",
"clues": [
{
"rung": 1,
"dimension": "production",
"text": "Live drum takes get chopped and reassembled into jittery, non-human loops, then layered under warm synth pads.",
"source": "general"
},
{
"rung": 2,
"dimension": "harmony",
"text": "The chords drift through unresolved, almost modal shapes that rarely resolve to a simple major chord, keeping the mood suspended.",
"source": "general"
},
"..."
],
"prompt_elements": [
"icy electronic textures",
"falsetto vocal floating over layered guitars",
"irregular, shifting time signatures",
"..."
],
"host_check": "Check rung 1's claim about chopped, reassembled drum loops - truer of the band's early-2000s material than its earlier guitar-driven records.",
"ask": ""
},
{
"n": 2,
"name": "Nirvana",
"status": "need_notes",
"dna": "",
"clues": [],
"prompt_elements": [],
"host_check": "",
"ask": "More than one act has recorded under the name Nirvana, so I can't be sure which one you mean. Add two or three sound traits - the instruments, the voice, the tempo or mood, or the decade - or just a word that points to the right act."
},
{
"n": 3,
"name": "Talking Heads",
"status": "ready",
"dna": "Clipped, syncopated funk guitar and a nervy half-sung voice sit atop lean, tightly separated production, with polyrhythmic percussion pulling the harmony toward African-influenced grooves.",
"clues": [
{
"rung": 1,
"dimension": "production",
"text": "Each instrument sits in its own narrow slice of the mix - a clipped guitar line, a rubbery bass and a tight hi-hat - interlocking like separate machine parts.",
"source": "general"
},
{
"rung": 2,
"dimension": "rhythm",
"text": "A clipped, stuttering guitar figure plays slightly behind the beat, locking into a tense, funk-derived groove that never fully relaxes.",
"source": "general"
},
"..."
],
"prompt_elements": [
"jittery funk guitar stabs",
"nervy, half-spoken vocals",
"tight polyrhythmic percussion",
"..."
],
"host_check": "Check rung 4's claim about West African-influenced percussion - true mainly of the band's early-1980s records, less so its first two albums.",
"ask": ""
},
"... (1 more round)"
]
}
Build the input yourself
Load the script in node (global.window = global; require("./cluekit.js")), then:
const K = ClueKit;
const form = { artists: "Nina Simone\nOur cousin's ska band | trombone; fast offbeat guitar; songs about the bus",
players: "Ann, Bo", level: "mixed", crowd: "" };
const st = K.state(form); // the cleaned list (st.artists) and its flags (st.flags)
const wire = K.toWire(K.buildInput(st).input); // POST this to /estimate and /run
// then: const deck = K.normalize(K.parseReply(job.output.output));
// const { notes, byRound } = K.check(deck, st);
// play it: const g = K.newGame(st.players); K.award(g, 1, 0, 2) gives player 0 four points on round 1
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://liner-clues.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://liner-clues.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"]
// liner-clues.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://liner-clues.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://liner-clues.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
}
// LinerClues.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 LinerClues {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://liner-clues.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://liner-clues.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://liner-clues.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://liner-clues.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":"liner-clues"}' | 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": "liner-clues"})["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: "liner-clues" })).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": "liner-clues"}, 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", "liner-clues"), 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: "liner-clues" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "liner-clues"])["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 = "liner-clues" }))!["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": "deck", "level": "casual", "crowd": "three generations, ages 9 to 82", "artists": "[{\"n\":1,\"name\":\"Louis Armstrong\", ...}]", "page_flags": "[...]"}
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": "deck", "level": "casual", "crowd": "three generations, ages 9 to 82", "artists": "[{\"n\":1,\"name\":\"Louis Armstrong\", ...}]", "page_flags": "[...]"}
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: "deck", level, crowd, artists, page_flags} (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 liner-clues:<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="liner-clues:deck:$(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 = "liner-clues:" + 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("verdict"))
import { createHash } from "node:crypto";
const key = `liner-clues:${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 := "liner-clues:" + 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 = "liner-clues:" + 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 = "liner-clues:#{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 = "liner-clues:" . $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 = $"liner-clues:{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: liner-clues:deck:$(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": "deck", "level": "casual", "crowd": "three generations, ages 9 to 82", "artists": "[{\"n\":1,\"name\":\"Louis Armstrong\", ...}]", "page_flags": "[...]"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "liner-clues:deck:" + 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": `liner-clues:deck:${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", "liner-clues:deck:"+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", "liner-clues:deck:" + 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" => "liner-clues:deck:#{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: liner-clues:deck:" . 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", $"liner-clues:deck:{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);