← Shelf Showdown
API
Tokens

Drive Shelf Showdown from your own code

Both metered steps - the blurb read and the referee - are available over HTTP. A natural use is a reading tracker or a book club tool that already holds someone's to-read list: your code sends the books with their blurbs and the reader's mood, and shows the quoted tags or the head-to-head call.

The split first: the page does the parsing, the prescan and the seeding; the model reads the blurbs; the page checks the reading. Parsing, the keyword prescan, benching, seeding and the bracket come from shelfkit.js, which runs in node as well as in the browser. Its reconcile function then checks that every trope quote appears in that book's blurb, that a hard no never comes back as a match, that blurbs under 25 words read as thin, that heat and darkness estimates over the reader's limits are not matches, and that the referee's swap check agrees with its verdict.

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":"shelf-showdown"}), 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 task or reader is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: both tasks are metered and need a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Every run sends task, which routes the one system prompt to one of two contracts:

taskWhat it returnsVerdict field
tagA title and summary; books, one per input book in order: {id, verdict, tropes[{id, quote}], channels{transportation, aesthetic, social, flow, curiosity}, channel_note, heat, dark, pace, mood[], fit, hard_no_hits[]}; 1–2 caveats. Channels, heat and dark are 0–3.per book verdict: match, maybe, hard_no, thin
refA title and summary; 2–5 criteria [{name, left, right, note}] with levels strong, some, none, unknown; swap_check {first_pass, second_pass, consistent}; case_left, case_right, deciding_factor; caveats.verdict: left, right, toss_up, neither

Trope ids come from a fixed list of 28 tropes and 5 content flags (love-triangle, cheating, cliffhanger, animal-harm, grief); ShelfKit.TROPE_IDS holds them. Heat runs closed door, warm, steamy, explicit; darkness runs light, some shadows, dark, very dark - both are estimates from the blurb, never from the book.

The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (ShelfKit.toWire does this). task and reader are required.

Input fields

FieldTypeNotes
taskstringtag or ref.
readerstring (JSON){craving, wants[], hard_nos[], heat_max, dark_max} from ShelfKit.normReader; the craving is clipped to 600 characters.
booksstring (JSON)tag only: [{id, title, author?, blurb, words}], up to 16; each blurb over 1,200 characters keeps its start and end around a marker.
left, rightstring (JSON)ref only: two books in the same shape. left is the higher bracket slot.
prescanstring (JSON)Per book {id, words, thin, hits[{trope, phrase}], heat_hint, dark_hint} from ShelfKit.prescan.
tagsstring (JSON)ref only, optional: {left, right} from an earlier blurb read.
notestringOptional; over 500 characters keeps the start and the end.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with ShelfKit.parseReply, then normalize, then check it with reconcile(reply, {books, reader, pair, read}). Seeds are never asked of the model: seedOrder(books, reader, prescan, read) ranks matches, then maybes, then thin blurbs, benches hard nos, and resolve(seeds, picks) builds the bracket.

Worked examples

Replies produced against the app's prompt for three made-up piles - every title, author and blurb was invented for the page (long fields shortened; the full replies ship in example-result.js). The page replays each of them free from its sample buttons.

task: tag - the autumn romantasy pile (verdicts: hard_no, match, maybe, thin)

Input:

{
 "task": "tag",
 "reader": "{\"craving\":\"Slow-burn enemies to lovers with a court full of schemers. Nothing darker than a few shadows, nothing past steamy. No cheating, please.\",\"wants\":[\"enemies-to-lovers\",\"slow-burn\",\"court-intrigue\"],\"hard_nos\":[\"cheating\"],\"heat_max\":2,\"dark_max\":1}",
 "books": "[{\"id\":\"B1\",\"title\":\"The Thorn Regent\",\"blurb\":\"Wren has spent ten years despising Cassian Vale, the fae heir who took her mother's seat o...\",\"words\":83,\"author\":\"Ilse Marrow\"},{\"id\":\"B2\",\"title\":\"Salt and Sovereign\",\"blurb\":\"Two rival harbour kingdoms, one treaty, and a political marriage neither heir asked for. P...\",\"words\":73,\"author\":\"Dara Quill\"},\"... 5 more in the same shape\"]",
 "prescan": "[{\"id\":\"B1\",\"words\":83,\"thin\":false,\"hits\":[{\"trope\":\"enemies-to-lovers\",\"phrase\":\"despis\"},{\"trope\":\"slow-burn\",\"phrase\":\"slow burn\"},{\"trope\":\"court-intrigue\",\"phrase\":\"court\"},{\"trope\":\"fae\",\"phrase\":\"fae\"}],\"heat_hint\":null,\"dark_hint\":0},{\"id\":\"B2\",\"words\":73,\"thin\":false,\"hits\":[{\"trope\":\"rivals\",\"phrase\":\"rival\"},{\"trope\":\"marriage-of-convenience\",\"phrase\":\"political marriage\"},{\"trope\":\"court-intrigue\",\"phrase\":\"prince\"}],\"heat_hint\":null,\"dark_hint\":0},\"... 5 more in the same shape\"]"
}

Reply:

{
 "task": "tag",
 "title": "Your Pile, Sorted for Slow-Burn Court Schemers",
 "summary": "The Thorn Regent and Salt and Sovereign both deliver your slow-burn, scheming-court craving within your limits - one a true decade-long enmity, the other a gradual marriage-of-convenience. Velvet Oath has the court intrigue but turns on an affair (the cheating you ruled out), and A Map of Burning Wings and Ninefold Night both run darker than you want. Hollow Crown Bakery is a charming but different cozy mood, and The Glass Orchard's blurb is too short to judge.",
 "books": [
  {
   "id": "B1",
   "verdict": "match",
   "tropes": [
    {
     "id": "enemies-to-lovers",
     "quote": "ten years despising Cassian Vale"
    },
    {
     "id": "slow-burn",
     "quote": "The slow burn between them is the most dangerous scheme"
    },
    {
     "id": "court-intrigue",
     "quote": "court is a nest of knives"
    },
    {
     "id": "fae",
     "quote": "fae heir"
    }
   ],
   "channels": {
    "transportation": 2,
    "aesthetic": 1,
    "social": 3,
    "flow": 2,
    "curiosity": 2
   },
   "channel_note": "Promises social intrigue most - a court of rivals and courtiers betting on who breaks first.",
   "heat": 1,
   "dark": 1,
   "pace": "steady",
   "mood": [
    "tense",
    "scheming"
   ],
   "fit": "Delivers exactly what you asked for: a decade of enmity turning into a political slow burn in a knife-edge court, within your heat and dark limits.",
   "hard_no_hits": []
  },
  {
   "id": "B2",
   "verdict": "match",
   "tropes": [
    {
     "id": "rivals",
     "quote": "Two rival harbour kingdoms"
    },
    {
     "id": "marriage-of-convenience",
     "quote": "a political marriage neither heir asked for"
    },
    {
     "id": "slow-burn",
     "quote": "one careful step at a time"
    },
    {
     "id": "court-intrigue",
     "quote": "a council that wants the treaty to fail"
    }
   ],
   "channels": {
    "transportation": 3,
    "aesthetic": 2,
    "social": 2,
    "flow": 2,
    "curiosity": 2
   },
   "channel_note": "Promises transportation and aesthetic pleasure together - a sweeping harbour-court world in lyrical lines.",
   "heat": 1,
   "dark": 0,
   "pace": "steady",
   "mood": [
    "sweeping",
    "wary"
   ],
   "fit": "A marriage-of-convenience slow burn in a scheming harbour court - not outright enemies, but it hits the court-intrigue and gradual-trust craving within your limits.",
   "hard_no_hits": []
  },
  "... one entry per book, same shape"
 ],
 "caveats": [
  "Heat and darkness are estimates from each blurb's own words, not the full book.",
  "A thin blurb (like The Glass Orchard) can't be judged fairly - more back-cover text would help."
 ]
}

task: ref - the autumn romantasy pile, B1 vs B2 (left)

Input:

{
 "task": "ref",
 "reader": "{\"craving\":\"Slow-burn enemies to lovers with a court full of schemers. Nothing darker than a few shadows, nothing past steamy. No cheating, please.\",\"wants\":[\"enemies-to-lovers\",\"slow-burn\",\"court-intrigue\"],\"hard_nos\":[\"cheating\"],\"heat_max\":2,\"dark_max\":1}",
 "left": "{\"id\":\"B1\",\"title\":\"The Thorn Regent\",\"blurb\":\"Wren has spent ten years despising Cassian Vale, the fae heir who took her mother's seat o...\",\"words\":83,\"author\":\"Ilse Marrow\"}",
 "right": "{\"id\":\"B2\",\"title\":\"Salt and Sovereign\",\"blurb\":\"Two rival harbour kingdoms, one treaty, and a political marriage neither heir asked for. P...\",\"words\":73,\"author\":\"Dara Quill\"}",
 "prescan": "[{\"id\":\"B1\",\"words\":83,\"thin\":false,\"hits\":[{\"trope\":\"enemies-to-lovers\",\"phrase\":\"despis\"},{\"trope\":\"slow-burn\",\"phrase\":\"slow burn\"},{\"trope\":\"court-intrigue\",\"phrase\":\"court\"},{\"trope\":\"fae\",\"phrase\":\"fae\"}],\"heat_hint\":null,\"dark_hint\":0},{\"id\":\"B2\",\"words\":73,\"thin\":false,\"hits\":[{\"trope\":\"rivals\",\"phrase\":\"rival\"},{\"trope\":\"marriage-of-convenience\",\"phrase\":\"political marriage\"},{\"trope\":\"court-intrigue\",\"phrase\":\"prince\"}],\"heat_hint\":null,\"dark_hint\":0},\"... 0 more in the same shape\"]",
 "tags": "{\"left\":{...},\"right\":{...}}"
}

Reply:

{
 "task": "ref",
 "verdict": "left",
 "title": "The Thorn Regent wins on enemies-to-lovers and slow burn",
 "summary": "Both courts are full of schemers and both stay inside the reader's heat and dark limits, but The Thorn Regent is built on real, declared enmity and names its own slow burn outright. Salt and Sovereign is a warmer, wary-strangers treaty marriage - closer to rivals easing into trust than true enemies. Neither blurb shows any cheating.",
 "criteria": [
  {
   "name": "enemies-to-lovers friction",
   "left": "strong",
   "right": "some",
   "note": "left: ten years despising Cassian Vale and swore to ruin him; right: starts as wary strangers, not declared enemies"
  },
  {
   "name": "slow burn pacing",
   "left": "strong",
   "right": "some",
   "note": "left names it outright, 'The slow burn between them'; right builds trust 'one careful step at a time' but never calls it a burn"
  },
  "..."
 ],
 "swap_check": {
  "first_pass": "left",
  "second_pass": "left",
  "consistent": true
 },
 "case_left": "Wren and Cassian carry ten years of real hatred - she 'swore to ruin' him - into an alliance the blurb calls 'the most dangerous scheme in the palace,' with every courtier betting on who breaks first.",
 "case_right": "Odile and Rafe are forced into a treaty marriage neither wanted, and between masked balls, forged letters, and a council rooting for the treaty to fail, they learn to trust each other 'one careful step at a time' into what the blurb calls 'a sweeping, warm romance.'",
 "deciding_factor": "Both deliver a schemer-filled court inside the reader's limits, but only The Thorn Regent pairs that with true enemies-to-lovers and an explicit slow burn - the craving's two leading wants - while Salt and Sovereign reads closer to wary strangers warming up.",
 "caveats": [
  "Heat and dark levels are estimates from the blurb text only.",
  "Salt and Sovereign's leads start as suspicious strangers rather than declared enemies, so its enemies-to-lovers read is only a partial match."
 ]
}

task: ref - the autumn romantasy pile, B3 vs B6 (right)

Input:

{
 "task": "ref",
 "reader": "{\"craving\":\"Slow-burn enemies to lovers with a court full of schemers. Nothing darker than a few shadows, nothing past steamy. No cheating, please.\",\"wants\":[\"enemies-to-lovers\",\"slow-burn\",\"court-intrigue\"],\"hard_nos\":[\"cheating\"],\"heat_max\":2,\"dark_max\":1}",
 "left": "{\"id\":\"B3\",\"title\":\"A Map of Burning Wings\",\"blurb\":\"Ash-born and penniless, Kit wins a place at the Skyreach Academy by accident and is bonded...\",\"words\":75,\"author\":\"Teo Valcourt\"}",
 "right": "{\"id\":\"B6\",\"title\":\"Hollow Crown Bakery\",\"blurb\":\"When Nell inherits a crumbling bakery in a small town where the fae still trade in honey a...\",\"words\":58,\"author\":\"June Ashdown\"}",
 "prescan": "[{\"id\":\"B3\",\"words\":75,\"thin\":false,\"hits\":[{\"trope\":\"found-family\",\"phrase\":\"ragtag\"},{\"trope\":\"academy\",\"phrase\":\"academy\"},{\"trope\":\"dragons\",\"phrase\":\"dragon\"}],\"heat_hint\":null,\"dark_hint\":1},{\"id\":\"B6\",\"words\":58,\"thin\":false,\"hits\":[{\"trope\":\"grumpy-sunshine\",\"phrase\":\"grump\"},{\"trope\":\"found-family\",\"phrase\":\"found family\"},{\"trope\":\"fae\",\"phrase\":\"fae\"},{\"trope\":\"small-town\",\"phrase\":\"small town\"},{\"trope\":\"cozy\",\"phrase\":\"bakery\"}],\"heat_hint\":null,\"dark_hint\":0},\"... 0 more in the same shape\"]",
 "tags": "{\"left\":{...},\"right\":{...}}"
}

Reply:

{
 "task": "ref",
 "verdict": "right",
 "title": "A Map of Burning Wings vs. Hollow Crown Bakery",
 "summary": "Left is a fast dragon-and-academy war story with no romance in sight, and its 'blood-soaked campaign' reads darker than this reader's few-shadows limit. Right is a cozy fae-village bakery story with a grumpy-sunshine spark, safely within the limits but still short of the slow-burn, court-intrigue romance craved. Right wins this matchup by elimination, not by being a true match.",
 "criteria": [
  {
   "name": "slow-burn enemies-to-lovers chemistry",
   "left": "none",
   "right": "some",
   "note": "left has no romance at all; right hints a grumpy/sunshine spark, not confirmed enemies-to-lovers or slow burn"
  },
  {
   "name": "court full of schemers",
   "left": "none",
   "right": "none",
   "note": "left is a war-torn dragon academy, right a fae village bakery - neither has a court"
  },
  "..."
 ],
 "swap_check": {
  "first_pass": "right",
  "second_pass": "right",
  "consistent": true
 },
 "case_left": "Kit's 'ragtag crew of misfits' and her bond with 'the oldest dragon in the aerie' promise a fast, high-stakes season of trials for a reader open to war over romance.",
 "case_right": "Nell's grumpy hob landlord and her sunshine optimism promise a gentle slow build, and the blurb is explicitly 'a cozy, low-stakes story' that stays safely within this reader's limits.",
 "deciding_factor": "Left's 'blood-soaked campaign' and border war read darker than the reader's shadows-only limit, so right wins by elimination even though neither book shows the court-intrigue enemies-to-lovers romance craved.",
 "caveats": [
  "Heat and darkness are estimates from the blurbs' own words only.",
  "Right is the safer pick, not a true match - it offers grumpy-sunshine warmth, not the enemies-to-lovers court intrigue craved."
 ]
}

task: ref - the autumn romantasy pile, B4 vs B7 (neither)

Input:

{
 "task": "ref",
 "reader": "{\"craving\":\"Slow-burn enemies to lovers with a court full of schemers. Nothing darker than a few shadows, nothing past steamy. No cheating, please.\",\"wants\":[\"enemies-to-lovers\",\"slow-burn\",\"court-intrigue\"],\"hard_nos\":[\"cheating\"],\"heat_max\":2,\"dark_max\":1}",
 "left": "{\"id\":\"B4\",\"title\":\"Velvet Oath\",\"blurb\":\"Queen Seraphine has a crown, a cold king and a kingdom on the edge of revolt. What she doe...\",\"words\":70,\"author\":\"Mina Corrow\"}",
 "right": "{\"id\":\"B7\",\"title\":\"Ninefold Night\",\"blurb\":\"He is the villain of every story she was raised on. The Night King rules a realm of tortur...\",\"words\":58,\"author\":\"Ezra Kell\"}",
 "prescan": "[{\"id\":\"B4\",\"words\":70,\"thin\":false,\"hits\":[{\"trope\":\"court-intrigue\",\"phrase\":\"queen\"},{\"trope\":\"cheating\",\"phrase\":\"affair\"}],\"heat_hint\":null,\"dark_hint\":0},{\"id\":\"B7\",\"words\":58,\"thin\":false,\"hits\":[{\"trope\":\"morally-grey\",\"phrase\":\"villain\"},{\"trope\":\"court-intrigue\",\"phrase\":\"king\"}],\"heat_hint\":2,\"dark_hint\":2},\"... 0 more in the same shape\"]",
 "tags": "{\"left\":{...},\"right\":{...}}"
}

Reply:

{
 "task": "ref",
 "verdict": "neither",
 "title": "Velvet Oath vs. Ninefold Night: neither clears your list",
 "summary": "Velvet Oath offers a forbidden court romance, but its central relationship is framed as an affair - your stated hard no. Ninefold Night calls itself 'Explicit' and lingers on torture and massacre, well past both your heat and darkness limits. Both are ruled out, not just one.",
 "criteria": [
  {
   "name": "slow-burn enemies-to-lovers",
   "left": "none",
   "right": "some",
   "note": "left: she already loves him, no enemy spark shown; right: 'a desire she cannot explain' toward the villain hints at it, but no burn is shown"
  },
  {
   "name": "court full of schemers",
   "left": "some",
   "right": "none",
   "note": "left: a queen, a cold king and 'a kingdom on the edge of revolt'; right: just a king and a castle, no scheming shown"
  },
  "..."
 ],
 "swap_check": {
  "first_pass": "tie",
  "second_pass": "tie",
  "consistent": true
 },
 "case_left": "A queen risking everything for the one person who truly sees her, with rebels closing on the capital for real stakes - if the affair itself isn't the dealbreaker.",
 "case_right": "A classic villain's-captive setup - 'the villain of every story she was raised on' - with an inexplicable pull across the enemy line, for a reader who wants immersive, high-stakes darkness.",
 "deciding_factor": "Velvet Oath's romance is explicitly an affair (your cheating hard no); Ninefold Night calls itself 'Explicit' and dwells on torture and massacre (over both your heat and darkness limits) - so both are out, not just one. Look instead for a blurb that builds from real enmity into romance, inside a scheming court, with no affair and no on-page violence.",
 "caveats": [
  "Heat and darkness are estimates from the blurbs' own words, not the full books.",
  "A detail the back cover leaves out (an open marriage, or off-page violence) could change this read."
 ]
}

task: tag - the rainy-night mystery stack (verdicts: hard_no, match)

Input:

{
 "task": "tag",
 "reader": "{\"craving\":\"A cozy whodunit for rainy evenings: a clever sleuth, a small cast, low gore. I lost my dad this year, so go easy on grief storylines.\",\"wants\":[\"whodunit\",\"cozy\",\"closed-circle\"],\"hard_nos\":[\"grief\",\"animal-harm\"],\"heat_max\":1,\"dark_max\":1}",
 "books": "[{\"id\":\"B1\",\"title\":\"Murder at the Marmalade Inn\",\"blurb\":\"Innkeeper Polly Fairweather has six guests, one snowed-in country inn and a ginger cat wit...\",\"words\":70,\"author\":\"Hattie Grove\"},{\"id\":\"B2\",\"title\":\"The Quiet Ledger\",\"blurb\":\"Retired accountant Edwin Marsh moved to the village for peace. Then the treasurer of the s...\",\"words\":75,\"author\":\"Arthur Penhallow\"},\"... 4 more in the same shape\"]",
 "prescan": "[{\"id\":\"B1\",\"words\":70,\"thin\":false,\"hits\":[{\"trope\":\"whodunit\",\"phrase\":\"murdered\"},{\"trope\":\"cozy\",\"phrase\":\"cat\"}],\"heat_hint\":null,\"dark_hint\":0},{\"id\":\"B2\",\"words\":75,\"thin\":false,\"hits\":[{\"trope\":\"small-town\",\"phrase\":\"village\"}],\"heat_hint\":null,\"dark_hint\":1},\"... 4 more in the same shape\"]"
}

Reply:

{
 "task": "tag",
 "title": "Picks for a Cozy Rainy-Evening Whodunit",
 "summary": "Three of six books are snowed-in or storm-locked whodunits that match this craving almost perfectly (B1, B5, B6); a fourth (B2) is a warm, gore-free village puzzle without the closed-circle lock. Two miss outright: B4 centers a grief storyline you asked to skip, and B3's torture and brutality run far past the darkness limit.",
 "books": [
  {
   "id": "B1",
   "verdict": "match",
   "tropes": [
    {
     "id": "whodunit",
     "quote": "the killer is still at breakfast"
    },
    {
     "id": "cozy",
     "quote": "Gentle, clever and full of marmalade"
    },
    {
     "id": "closed-circle",
     "quote": "one snowed-in country inn...the road is closed"
    }
   ],
   "channels": {
    "transportation": 2,
    "aesthetic": 2,
    "social": 2,
    "flow": 1,
    "curiosity": 3
   },
   "channel_note": "Curiosity leads - the puzzle of who's lying promises the clever-sleuth payoff this craving wants most.",
   "heat": 0,
   "dark": 0,
   "pace": "steady",
   "mood": [
    "cozy",
    "gentle",
    "wry"
   ],
   "fit": "A snowed-in inn, a gentle tone and a small locked-in guest list hit cozy, whodunit and closed-circle at once, with no gore described.",
   "hard_no_hits": []
  },
  {
   "id": "B2",
   "verdict": "match",
   "tropes": [
    {
     "id": "whodunit",
     "quote": "find which of the committee would kill"
    },
    {
     "id": "cozy",
     "quote": "A warm, witty puzzle with a small cast and no gore"
    }
   ],
   "channels": {
    "transportation": 1,
    "aesthetic": 2,
    "social": 2,
    "flow": 1,
    "curiosity": 2
   },
   "channel_note": "Curiosity and the warm, witty voice promise the puzzle-solving pleasure a cozy whodunit needs most.",
   "heat": 0,
   "dark": 0,
   "pace": "steady",
   "mood": [
    "warm",
    "witty",
    "cozy"
   ],
   "fit": "A warm, witty, gore-free village puzzle meets the cozy whodunit craving, though nothing in the blurb seals the cast in.",
   "hard_no_hits": []
  },
  "... one entry per book, same shape"
 ],
 "caveats": [
  "Heat and dark ratings are estimates from the blurb text alone, not the full book.",
  "Verdicts judge only what each blurb states; a book's actual tone may differ."
 ]
}

task: ref - the rainy-night mystery stack, B1 vs B2 (left)

Input:

{
 "task": "ref",
 "reader": "{\"craving\":\"A cozy whodunit for rainy evenings: a clever sleuth, a small cast, low gore. I lost my dad this year, so go easy on grief storylines.\",\"wants\":[\"whodunit\",\"cozy\",\"closed-circle\"],\"hard_nos\":[\"grief\",\"animal-harm\"],\"heat_max\":1,\"dark_max\":1}",
 "left": "{\"id\":\"B1\",\"title\":\"Murder at the Marmalade Inn\",\"blurb\":\"Innkeeper Polly Fairweather has six guests, one snowed-in country inn and a ginger cat wit...\",\"words\":70,\"author\":\"Hattie Grove\"}",
 "right": "{\"id\":\"B2\",\"title\":\"The Quiet Ledger\",\"blurb\":\"Retired accountant Edwin Marsh moved to the village for peace. Then the treasurer of the s...\",\"words\":75,\"author\":\"Arthur Penhallow\"}",
 "prescan": "[{\"id\":\"B1\",\"words\":70,\"thin\":false,\"hits\":[{\"trope\":\"whodunit\",\"phrase\":\"murdered\"},{\"trope\":\"cozy\",\"phrase\":\"cat\"}],\"heat_hint\":null,\"dark_hint\":0},{\"id\":\"B2\",\"words\":75,\"thin\":false,\"hits\":[{\"trope\":\"small-town\",\"phrase\":\"village\"}],\"heat_hint\":null,\"dark_hint\":1},\"... 0 more in the same shape\"]",
 "tags": "{\"left\":{...},\"right\":{...}}"
}

Reply:

{
 "task": "ref",
 "verdict": "left",
 "title": "Marmalade Inn vs. Quiet Ledger",
 "summary": "Both are gentle, small-cast whodunits that keep the violence and romance off the page. The Marmalade Inn adds the closed-circle isolation this reader asked for - a snowed-in inn with the road shut - while the Quiet Ledger's open village never traps its suspects. For a cozy, rainy-evening puzzle with nowhere to run, the inn is the stronger pick.",
 "criteria": [
  {
   "name": "isolated, closed-circle suspects",
   "left": "strong",
   "right": "none",
   "note": "The inn is \"one snowed-in country inn\" with \"the road is closed\"; the village in the ledger has no stated isolation."
  },
  {
   "name": "cozy warmth, low gore",
   "left": "strong",
   "right": "strong",
   "note": "Both say so outright: \"Gentle, clever and full of marmalade\" and \"a small cast and no gore.\""
  },
  "..."
 ],
 "swap_check": {
  "first_pass": "left",
  "second_pass": "left",
  "consistent": true
 },
 "case_left": "Six guests, a snowed-in inn and a closed road give real closed-circle stakes, and the blurb stays gentle and clever throughout.",
 "case_right": "A warm, witty puzzle that promises outright \"a small cast and no gore,\" with a clever retired-accountant sleuth chasing a cooked ledger.",
 "deciding_factor": "Closed-circle is one of the reader's named wants, and only the Marmalade Inn delivers it - the Quiet Ledger's open village never isolates its suspects.",
 "caveats": [
  "Heat and darkness are estimates from the blurbs' own words only.",
  "Neither blurb mentions grief, so that hard no does not separate them."
 ]
}

task: ref - the rainy-night mystery stack, B1 vs B6 (toss_up)

Input:

{
 "task": "ref",
 "reader": "{\"craving\":\"A cozy whodunit for rainy evenings: a clever sleuth, a small cast, low gore. I lost my dad this year, so go easy on grief storylines.\",\"wants\":[\"whodunit\",\"cozy\",\"closed-circle\"],\"hard_nos\":[\"grief\",\"animal-harm\"],\"heat_max\":1,\"dark_max\":1}",
 "left": "{\"id\":\"B1\",\"title\":\"Murder at the Marmalade Inn\",\"blurb\":\"Innkeeper Polly Fairweather has six guests, one snowed-in country inn and a ginger cat wit...\",\"words\":70,\"author\":\"Hattie Grove\"}",
 "right": "{\"id\":\"B6\",\"title\":\"The Snowbound Supper Club\",\"blurb\":\"Retired schoolteacher Agnes Pike hosts six old friends for supper at her farmhouse. When a...\",\"words\":66,\"author\":\"Wren Ashby\"}",
 "prescan": "[{\"id\":\"B1\",\"words\":70,\"thin\":false,\"hits\":[{\"trope\":\"whodunit\",\"phrase\":\"murdered\"},{\"trope\":\"cozy\",\"phrase\":\"cat\"}],\"heat_hint\":null,\"dark_hint\":0},{\"id\":\"B6\",\"words\":66,\"thin\":false,\"hits\":[{\"trope\":\"cozy\",\"phrase\":\"cosy\"}],\"heat_hint\":null,\"dark_hint\":0},\"... 0 more in the same shape\"]",
 "tags": "{\"left\":{...},\"right\":{...}}"
}

Reply:

{
 "task": "ref",
 "verdict": "toss_up",
 "title": "Murder at the Marmalade Inn vs. The Snowbound Supper Club",
 "summary": "Both blurbs promise the same thing: a gentle whodunit sealed off by weather, a sharp-eyed host turned sleuth, and a houseful of old acquaintances to suspect. Neither crosses your heat or darkness limits, and neither leans into grief on the page, so the craving is met equally well by both.",
 "criteria": [
  {
   "name": "clever whodunit sleuthing",
   "left": "strong",
   "right": "strong",
   "note": "Polly has \"a guest ledger and a nose for a fib\"; Agnes has \"a notebook of everyone's old grudges to unmask the culprit\""
  },
  {
   "name": "cozy, gentle tone",
   "left": "strong",
   "right": "strong",
   "note": "both blurbs say it outright: \"Gentle, clever and full of marmalade\" vs \"Gentle, clever and cosy as a quilt\""
  },
  "..."
 ],
 "swap_check": {
  "first_pass": "tie",
  "second_pass": "tie",
  "consistent": true
 },
 "case_left": "Murder at the Marmalade Inn gives a snowed-in inn, a guest ledger of suspects, and the blurb's own promise: \"Gentle, clever and full of marmalade.\"",
 "case_right": "The Snowbound Supper Club offers the same shape - a blizzard-sealed farmhouse, a notebook of everyone's old grudges, and a tone the blurb calls \"cosy as a quilt.\"",
 "deciding_factor": "Nothing in either blurb breaks the tie - both are equally gentle, closed-circle whodunits within your limits. Read the first page of each and let the voice (innkeeper's scones vs. retired teacher's cocoa) decide it.",
 "caveats": [
  "Heat and darkness are estimates from the blurbs only, not the full books.",
  "Neither blurb dwells on grief, but both involve the death of someone known to the group - skim the opening page if that balance matters to you."
 ]
}

task: tag - the book club shortlist (verdicts: hard_no, match, thin)

Input:

{
 "task": "tag",
 "reader": "{\"craving\":\"Our club meets in three weeks. We want something character-driven that we can argue about, under 400 pages, and complete in one volume.\",\"wants\":[\"unreliable-narrator\",\"dual-timeline\"],\"hard_nos\":[\"cliffhanger\"],\"heat_max\":3,\"dark_max\":2}",
 "books": "[{\"id\":\"B1\",\"title\":\"The Salt Archivist\",\"blurb\":\"In 1953 a lighthouse keeper's wife begins writing letters she never sends. Seventy years l...\",\"words\":71,\"author\":\"Noor Halvorsen\"},{\"id\":\"B2\",\"title\":\"Every Version of June\",\"blurb\":\"June wakes up on the morning of her tenth wedding anniversary for the fourth time. Each lo...\",\"words\":73,\"author\":\"Callum Reyes\"},\"... 2 more in the same shape\"]",
 "prescan": "[{\"id\":\"B1\",\"words\":71,\"thin\":false,\"hits\":[{\"trope\":\"dual-timeline\",\"phrase\":\"two timelines\"},{\"trope\":\"small-town\",\"phrase\":\"village\"}],\"heat_hint\":null,\"dark_hint\":0},{\"id\":\"B2\",\"words\":73,\"thin\":false,\"hits\":[{\"trope\":\"unreliable-narrator\",\"phrase\":\"unreliable\"}],\"heat_hint\":null,\"dark_hint\":0},\"... 2 more in the same shape\"]",
 "note": "Twelve members, mixed tastes. Last month's pick was too slow for half the group."
}

Reply:

{
 "task": "tag",
 "title": "Dual timelines and an unreliable narrator carry this pile - skip the cliffhanger",
 "summary": "Two books hit your craving squarely: The Salt Archivist pairs a dual-timeline structure with a self-contained mystery about memory and truth, and Every Version of June builds its whole hook - and a debatable ending - around an unreliable narrator, both built for a group that likes to argue. Kingdom of Small Hours is a trilogy opener whose own blurb promises a cliffhanger, so it breaks your no-cliffhanger rule. Paper Lanterns's blurb is only 13 words, too thin to judge - ask whoever suggested it to paste the full back cover.",
 "books": [
  {
   "id": "B1",
   "verdict": "match",
   "tropes": [
    {
     "id": "dual-timeline",
     "quote": "Told across two timelines"
    }
   ],
   "channels": {
    "transportation": 3,
    "aesthetic": 2,
    "social": 2,
    "flow": 1,
    "curiosity": 2
   },
   "channel_note": "Promises transportation most: a vivid lighthouse-to-archive setting carrying a lyrical, truth-chasing mystery.",
   "heat": 0,
   "dark": 1,
   "pace": "slow",
   "mood": [
    "quiet",
    "luminous",
    "secretive"
   ],
   "fit": "Delivers your dual-timeline want in a self-contained, discussable mystery about memory and truth, with no romance heat and only mild shadows.",
   "hard_no_hits": []
  },
  {
   "id": "B2",
   "verdict": "match",
   "tropes": [
    {
     "id": "unreliable-narrator",
     "quote": "love an unreliable narrator"
    },
    {
     "id": "time-loop",
     "quote": "Each loop, her husband tells her"
    }
   ],
   "channels": {
    "transportation": 1,
    "aesthetic": 1,
    "social": 2,
    "flow": 2,
    "curiosity": 3
   },
   "channel_note": "Promises curiosity most: a twisty who's-lying hook built for an ending readers will argue over.",
   "heat": 0,
   "dark": 1,
   "pace": "fast",
   "mood": [
    "tense",
    "funny",
    "devastating"
   ],
   "fit": "Delivers your unreliable-narrator want and is built around an ending made to argue about - exactly this club's ask.",
   "hard_no_hits": []
  },
  "... one entry per book, same shape"
 ],
 "caveats": [
  "Heat and darkness are estimates from the blurbs' own words only, not the full books.",
  "Page count and series status beyond what each blurb states aren't confirmed - worth a quick check before the meeting."
 ]
}

task: ref - the book club shortlist, B1 vs B2 (right)

Input:

{
 "task": "ref",
 "reader": "{\"craving\":\"Our club meets in three weeks. We want something character-driven that we can argue about, under 400 pages, and complete in one volume.\",\"wants\":[\"unreliable-narrator\",\"dual-timeline\"],\"hard_nos\":[\"cliffhanger\"],\"heat_max\":3,\"dark_max\":2}",
 "left": "{\"id\":\"B1\",\"title\":\"The Salt Archivist\",\"blurb\":\"In 1953 a lighthouse keeper's wife begins writing letters she never sends. Seventy years l...\",\"words\":71,\"author\":\"Noor Halvorsen\"}",
 "right": "{\"id\":\"B2\",\"title\":\"Every Version of June\",\"blurb\":\"June wakes up on the morning of her tenth wedding anniversary for the fourth time. Each lo...\",\"words\":73,\"author\":\"Callum Reyes\"}",
 "prescan": "[{\"id\":\"B1\",\"words\":71,\"thin\":false,\"hits\":[{\"trope\":\"dual-timeline\",\"phrase\":\"two timelines\"},{\"trope\":\"small-town\",\"phrase\":\"village\"}],\"heat_hint\":null,\"dark_hint\":0},{\"id\":\"B2\",\"words\":73,\"thin\":false,\"hits\":[{\"trope\":\"unreliable-narrator\",\"phrase\":\"unreliable\"}],\"heat_hint\":null,\"dark_hint\":0},\"... 0 more in the same shape\"]",
 "tags": "{\"left\":{...},\"right\":{...}}",
 "note": "Twelve members, mixed tastes. Last month's pick was too slow for half the group."
}

Reply:

{
 "task": "ref",
 "verdict": "right",
 "title": "Every Version of June edges The Salt Archivist",
 "summary": "Both blurbs hand you one of your two wanted tropes and both stay inside your heat and dark limits, so this comes down to what each blurb itself promises your club. June's blurb names an unreliable narrator and promises, almost in your own words, 'an ending they will argue about' - and its 'tense, funny' pace is a safer bet after last month's slow pick.",
 "criteria": [
  {
   "name": "dual-timeline (a want)",
   "left": "strong",
   "right": "none",
   "note": "left splits 1953 letters from a present-day search; right loops one day, not two eras"
  },
  {
   "name": "unreliable narrator (a want)",
   "left": "none",
   "right": "strong",
   "note": "right names the trope outright; left's narrators aren't framed as untrustworthy"
  },
  "..."
 ],
 "swap_check": {
  "first_pass": "right",
  "second_pass": "right",
  "consistent": true
 },
 "case_left": "The Salt Archivist delivers your dual-timeline want outright and tells you up front it is a complete, single-volume read - real insurance for a three-week deadline - in a voice it calls 'quiet, luminous'.",
 "case_right": "Every Version of June hands you the unreliable-narrator want by name and bills itself, almost in your own words, as a novel with 'an ending they will argue about', with a 'tense, funny' pace that is the opposite of last month's too-slow pick.",
 "deciding_factor": "June's blurb echoes your craving almost exactly ('an ending they will argue about') and confirms the other want on your list; weighed against your note that half the group found last month's pick too slow, that outweighs Archivist's edge on being a confirmed single volume.",
 "caveats": [
  "Heat and dark are estimates from these two blurbs only; neither signals past warm or light.",
  "Neither blurb states a page count, so 'under 400 pages' can't be confirmed from the text - check that before you commit."
 ]
}

Build the input yourself

Load the script in node (global.window = {}; require("./shelfkit.js")), then:

const K = ShelfKit;
const { books, warnings } = K.parsePile(pileText);          // "Title by Author" + blurb blocks, or a CSV export
const reader = { craving: "...", wants: ["slow-burn"], hard_nos: ["cheating"], heat_max: 2, dark_max: 1 };
const { input } = K.buildInput("tag", { books, reader });
const wire = K.toWire(input);                               // POST this to /estimate and /run
// later: const read = K.normalize(K.parseReply(job.output.output));
// K.reconcile(read, { books, reader }) -> [{level, code, text}]
// const { seeds, bench } = K.seedOrder(books, reader, K.prescan(books), read);
// const bracket = K.resolve(seeds, {});                    // picks: { "0-1": "B3", ... }
// referee: K.buildInput("ref", { books, reader, pair: ["B1", "B2"], read })

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://shelf-showdown.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":"shelf-showdown"}' | 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": "tag", "reader": "{...}", "books": "[...]", "prescan": "[...]"}
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 shelf-showdown:<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="shelf-showdown:tag:$(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: shelf-showdown:tag:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json