← Liner Clues
Liner Clues API
Tokens

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": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field such as task is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the deck is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Every run sends task: deck, the only lane. The reply holds one round per artist, each with one status:

StatusWhenClues
readyThe 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_onlyThe 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_notesThe 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

FieldTypeNotes
taskstringdeck.
levelstringcasual (a mixed crowd), mixed or nerd (music obsessives: the hardest clue may be a production or technique detail).
crowdstringOptional words about who is playing, at most 160 characters.
artistsstring (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_flagsstring (JSON)[{level, text}] with level fix / warn / info: duplicates dropped, lines past the eighth left out, one-word names with no notes.
retry_notestringOnly 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"}
}

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

Step 3 — GET /me

Who the token is (subject_type: user or guest) and the credit balance.

api GET /me | jq '.data | {subject_type, credits}' 

Step 4 — POST /estimate

Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.

# input.json: the run input object - {"task": "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}' 

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

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