← Quarter Circle
API
Tokens

Drive Quarter Circle from your own code

Two metered tasks are available over HTTP. forge takes a person's quirks, with the page's own reading of each one (its kind and the input reserved for it), and returns a made-up fighting-game character: a name card, five stats adding up to 15, one special per quirk (up to four) and a super, each with startup, active and recovery frames, on-block advantage, damage, what it does and its tell, plus a weakness. tune takes a move list with the page's four flags per move and returns keep, nerf or buff per move with a patch, the tell and the counterplay, plus a game plan and a verdict by a fixed rule.

The division of labour first: the page owns the reading and the arithmetic; the model owns the character and the words; the page re-checks the reply. Reading the quirks, the reserved inputs, the fair ranges, the flags and the calls come from qckit.js, which runs in node as well as in the browser: QcKit.buildInput(form, task, ctx) builds exactly the object the page sends, and QcKit.check(reply, built) is the page's own re-check.

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":"quarter-circle"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or a required field (task, quirks) 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, the flags and the verdict rule

Every run sends task first: forge or tune (a missing or unknown one is treated as forge). Frame data is at 60 frames a second; every number is invented for a made-up character. Inputs use numpad notation (6 forward, 4 back, 2 down; brackets mean hold): specials 236, 214, 623, 41236, 63214, [4]6, [2]8, 22, 63214789; supers 236236, 214214; buttons P and K.

The four flags the page computes per move: too safe (not a grab, on block +3 or more), too quick (startup 4 or less and damage 100 or more), too strong (damage 200 or more; a super 400) and too slow (startup 30 or more, or 70 frames in all; a super at startup 20). A move with too safe, too quick or too strong is a nerf; one that is only too slow a buff; otherwise keep.

tune, checked in this order - the first that applies wins:

  1. broken - two or more nerfs.
  2. needs_patch - at least one nerf or buff.
  3. tournament_ready - everything else.

forge has no verdict: it keeps every move inside the fair ranges (special: startup 5-28, active 1-8, recovery 8-40, on block -20 to +2, damage 60-180, 65 frames at most; super: startup 5-15, active 1-10, recovery 20-60, on block -40 to -10, damage 250-380; a grab is 0 on block), so a forged list balance-checks as tournament ready until you edit it.

The declared input schema carries scalars only, so the array- and object-valued fields are JSON-encoded strings.

Input fields

FieldTypeNotes
taskstringforge or tune. Required.
namestringWhat you call the person, up to 40 characters. May be empty (the forge picks a nickname from the quirks).
style, style_labelstringrushdown, zoner, grappler or all_rounder, and its plain label.
tone, tone_labelstringforge only: kind or cheeky, and its plain label.
quirksstring (JSON)[{id, text}], ids Q1.., 2 to 8 quirks of up to 140 characters. forge adds cue (grab, projectile, rush, counter, other), type, motion and button - the page's reading and the input it reserved. Required.
figuresstring (JSON)forge: {quirks, specials, stat_total, super_id}. tune: {moves, flags, calls, punish, nerfs, buffs} - flags, calls and punish are maps from move id. Required.
movesstring (JSON)tune only: [{id, name, quirk, type, motion, button, super, startup, active, recovery, on_block, damage, does, tell}], ids M1.., up to 5 moves, super "yes" or "no".
clip_note, retry_notestringWhat the page cut, when it cut something; and only on a reformat retry, what was wrong.

The replies

The job's output.output is a string holding one JSON object. forge: task, title, fighter ({name, epithet, style, bio, intro, win_quote}), stats ({power, speed, defense, range, combo}), moves ([{id, name, quirk, type, motion, button, super, startup, active, recovery, on_block, damage, does, tell}], the specials then the super), weakness. tune: task, verdict, title, summary, moves ([{id, call, patch: {startup, active, recovery, on_block, damage}, why, tell, counter}], one per input move), game_plan ({as, against}).

Worked examples

Replies produced against the app's prompt for the six samples - three made-up people to forge and three made-up move lists to tune (the full replies ship in example-result.js). Together they cover every tune verdict and every call.

forge: Dana, a cheeky rushdown with five quirks

Input:

{
 "task": "forge",
 "name": "Dana",
 "style": "rushdown",
 "style_label": "rushdown - fast and up close",
 "tone": "cheeky",
 "tone_label": "cheeky - gentle teasing about the habits listed",
 "quirks": "[{\"id\":\"Q1\",\"text\":\"Always ten minutes late but sprints for the bus\",\"cue\":\"rush\",\"type\":\"strike\",\"motion\":\"[4]6\",\"button\":\"K\"},{\"id\":\"Q2\",\"text\":\"Sends voice notes that are four minutes long\",\"cue\":\"projectile\",\"type\":\"projectile\",\"motion\":\"236\",\"button\":\"P\"},{\"id\":\"Q3\",\"text\":\"Says technically before correcting you\",\"cue\":\"counter\",\"type\":\"strike\",\"motion\":\"214\",\"button\":\"P\"},{\"id\":\"Q4\",\"text\":\"Hugs everyone way too hard\",\"cue\":\"grab\",\"type\":\"grab\",\"motion\":\"63214789\",\"button\":\"P\"},{\"id\":\"Q5\",\"text\":\"Falls asleep on any sofa within minutes\",\"cue\":\"other\",\"type\":\"strike\",\"motion\":\"623\",\"button\":\"P\"}]",
 "figures": "{\"quirks\":\"5\",\"specials\":\"4\",\"stat_total\":\"15\",\"super_id\":\"M5\"}",
 "clip_note": ""
}

Reply:

{
 "task": "forge",
 "title": "Dana, the Ten-Minutes-Late Rushdown Hugger",
 "fighter": {
  "name": "Dana",
  "epithet": "Fashionably Late, Full-Strength Hugs",
  "style": "rushdown",
  "bio": "Always ten minutes late but sprinting hard to catch the bus, firing off four-minute voice notes, correcting people with a technically, hugging too hard, and dozing off on any sofa within minutes.",
  "intro": "Sorry I'm late, sprinted the whole way, so let's hug this out hard.",
  "win_quote": "Technically, I win, now hold still for a victory hug."
 },
 "stats": {
  "power": 3,
  "speed": 5,
  "defense": 3,
  "range": 1,
  "combo": 3
 },
 "moves": [
  {
   "id": "M1",
   "name": "Bus Sprint",
   "quirk": "Q1",
   "type": "strike",
   "motion": "[4]6",
   "button": "K",
   "super": false,
   "startup": 10,
   "active": 3,
   "recovery": 14,
   "on_block": -2,
   "damage": 90,
   "does": "Dana bursts forward at a dead sprint, the same frantic dash used to catch the bus after running ten minutes behind.",
   "tell": "A held-back stance, like someone running late and bracing to dash."
  },
  {
   "id": "M2",
   "name": "Four-Minute Voice Note",
   "quirk": "Q2",
   "type": "projectile",
   "motion": "236",
   "button": "P",
   "super": false,
   "startup": 20,
   "active": 6,
   "recovery": 28,
   "on_block": -6,
   "damage": 75,
   "does": "Dana lobs a long voice note across the stage, a message so long it keeps droning on for the whole four minutes.",
   "tell": "A deep breath before launching into another impossibly long message."
  },
  {
   "id": "M3",
   "name": "The Technically Jab",
   "quirk": "Q3",
   "type": "strike",
   "motion": "214",
   "button": "P",
   "super": false,
   "startup": 8,
   "active": 2,
   "recovery": 16,
   "on_block": -4,
   "damage": 95,
   "does": "Dana snaps out a sharp correction of a strike, punctuated the moment someone gets a detail wrong.",
   "tell": "A raised finger and a knowing pause before the correction lands."
  },
  {
   "id": "M4",
   "name": "Bear Hug",
   "quirk": "Q4",
   "type": "grab",
   "motion": "63214789",
   "button": "P",
   "super": false,
   "startup": 12,
   "active": 1,
   "recovery": 20,
   "on_block": 0,
   "damage": 150,
   "does": "Dana wraps both arms around the opponent in an inescapable hug, squeezing far harder than anyone asked for.",
   "tell": "Wide-open arms telegraph the incoming hug from a step away."
  },
  {
   "id": "M5",
   "name": "Sofa Nap Slam",
   "quirk": "Q5",
   "type": "strike",
   "motion": "236236",
   "button": "P",
   "super": true,
   "startup": 10,
   "active": 4,
   "recovery": 35,
   "on_block": -20,
   "damage": 300,
   "does": "Dana sprints in, then drops into a sudden doze mid-air, crashing down onto the opponent like any sofa within minutes.",
   "tell": "Eyelids drooping and a slow stumble before the sudden drop."
  }
 ],
 "weakness": "Wait out the long voice note; its finish leaves Dana wide open to a hard counter before the next move."
}

forge: no name, a kind zoner with three quirks

Input:

{
 "task": "forge",
 "name": "",
 "style": "zoner",
 "style_label": "zoner - keeps you out at range",
 "tone": "kind",
 "tone_label": "kind - celebrate the quirks",
 "quirks": "[{\"id\":\"Q1\",\"text\":\"Throws a frisbee further than anyone at the park\",\"cue\":\"projectile\",\"type\":\"projectile\",\"motion\":\"236\",\"button\":\"P\"},{\"id\":\"Q2\",\"text\":\"Texts back with one word replies\",\"cue\":\"projectile\",\"type\":\"projectile\",\"motion\":\"236\",\"button\":\"K\"},{\"id\":\"Q3\",\"text\":\"Finishes the crossword in pen every morning\",\"cue\":\"other\",\"type\":\"strike\",\"motion\":\"623\",\"button\":\"P\"}]",
 "figures": "{\"quirks\":\"3\",\"specials\":\"3\",\"stat_total\":\"15\",\"super_id\":\"M4\"}",
 "clip_note": ""
}

Reply:

{
 "task": "forge",
 "title": "Crossword Frisbee - Keeps You at a Distance",
 "fighter": {
  "name": "Crossword Frisbee",
  "epithet": "Long Throw, Short Reply",
  "style": "zoner",
  "bio": "They throw a frisbee further than anyone at the park, text back with one word replies, and finish the crossword in pen every morning.",
  "intro": "I'll keep my distance and let my throw do the talking.",
  "win_quote": "Another long throw, another win - short and sweet."
 },
 "stats": {
  "power": 2,
  "speed": 3,
  "defense": 3,
  "range": 4,
  "combo": 3
 },
 "moves": [
  {
   "id": "M1",
   "name": "Frisbee Fling",
   "quirk": "Q1",
   "type": "projectile",
   "motion": "236",
   "button": "P",
   "super": false,
   "startup": 18,
   "active": 2,
   "recovery": 20,
   "on_block": -2,
   "damage": 100,
   "does": "They hurl a frisbee across the screen, further than anyone at the park, flying straight toward the opponent.",
   "tell": "A long wind-up before the frisbee leaves their hand."
  },
  {
   "id": "M2",
   "name": "Quick Reply",
   "quirk": "Q2",
   "type": "projectile",
   "motion": "236",
   "button": "K",
   "super": false,
   "startup": 10,
   "active": 2,
   "recovery": 14,
   "on_block": -4,
   "damage": 70,
   "does": "They flick out a short, clipped projectile, as curt and to the point as a one word reply.",
   "tell": "A brief, silent pause, like a quick text, before the short throw."
  },
  {
   "id": "M3",
   "name": "Pen Strike",
   "quirk": "Q3",
   "type": "strike",
   "motion": "623",
   "button": "P",
   "super": false,
   "startup": 14,
   "active": 3,
   "recovery": 22,
   "on_block": -6,
   "damage": 110,
   "does": "They strike with the confident precision of finishing a crossword in pen, no hesitation, no mistakes.",
   "tell": "A calm, steady pause, like filling in a crossword answer, right before the strike."
  },
  {
   "id": "M4",
   "name": "Frisbee Finale",
   "quirk": "Q1",
   "type": "projectile",
   "motion": "236236",
   "button": "P",
   "super": true,
   "startup": 12,
   "active": 4,
   "recovery": 35,
   "on_block": -20,
   "damage": 300,
   "does": "They wind up and launch their biggest frisbee throw yet, sailing the full length of the screen at the opponent.",
   "tell": "A long, deliberate wind-up as they plant their feet before the big throw."
  }
 ],
 "weakness": "Close the gap fast - the crossword strike is their slowest, most punishable move, so block it and strike back before they reset."
}

forge: a grappler whose quirks hold no grab - the super is the grab

Input:

{
 "task": "forge",
 "name": "Rob",
 "style": "grappler",
 "style_label": "grappler - slow, but the grabs hurt",
 "tone": "kind",
 "tone_label": "kind - celebrate the quirks",
 "quirks": "[{\"id\":\"Q1\",\"text\":\"Tells the same fishing story at every dinner\",\"cue\":\"other\",\"type\":\"strike\",\"motion\":\"623\",\"button\":\"P\"},{\"id\":\"Q2\",\"text\":\"Laughs so loud the whole room turns around\",\"cue\":\"other\",\"type\":\"strike\",\"motion\":\"623\",\"button\":\"K\"},{\"id\":\"Q3\",\"text\":\"Fixes every squeaky door with one tap of a hammer\",\"cue\":\"other\",\"type\":\"strike\",\"motion\":\"22\",\"button\":\"K\"}]",
 "figures": "{\"quirks\":\"3\",\"specials\":\"3\",\"stat_total\":\"15\",\"super_id\":\"M4\"}",
 "clip_note": ""
}

Reply:

{
 "task": "forge",
 "title": "Rob the Storytelling Grappler",
 "fighter": {
  "name": "Rob",
  "epithet": "The Loudest Laugh in the Room",
  "style": "grappler",
  "bio": "They tell the same fishing story at every dinner, laugh so loud the whole room turns around, and fix every squeaky door with one tap of a hammer.",
  "intro": "Get ready, that fishing story is about to turn into a grab.",
  "win_quote": "Told you the big one wouldn't get away, and neither did you."
 },
 "stats": {
  "power": 5,
  "speed": 1,
  "defense": 4,
  "range": 3,
  "combo": 2
 },
 "moves": [
  {
   "id": "M1",
   "name": "The Big Catch",
   "quirk": "Q1",
   "type": "strike",
   "motion": "623",
   "button": "P",
   "super": false,
   "startup": 14,
   "active": 3,
   "recovery": 18,
   "on_block": -4,
   "damage": 110,
   "does": "They act out reeling in a big catch, swinging an arm wide before snapping it forward into a solid strike.",
   "tell": "The wind-up as they start telling the story again."
  },
  {
   "id": "M2",
   "name": "Room-Turner",
   "quirk": "Q2",
   "type": "strike",
   "motion": "623",
   "button": "K",
   "super": false,
   "startup": 10,
   "active": 4,
   "recovery": 20,
   "on_block": -3,
   "damage": 95,
   "does": "They throw their head back in a huge laugh, the shockwave of sound knocking the opponent off balance.",
   "tell": "The deep breath before the laugh erupts."
  },
  {
   "id": "M3",
   "name": "One-Tap Fix",
   "quirk": "Q3",
   "type": "strike",
   "motion": "22",
   "button": "K",
   "super": false,
   "startup": 16,
   "active": 2,
   "recovery": 22,
   "on_block": -6,
   "damage": 125,
   "does": "They swing one sharp hammer tap downward, the same motion that silences a squeaky door, landing hard.",
   "tell": "The hammer raised high before it comes down."
  },
  {
   "id": "M4",
   "name": "The Big One",
   "quirk": "Q1",
   "type": "grab",
   "motion": "236236",
   "button": "P",
   "super": true,
   "startup": 10,
   "active": 3,
   "recovery": 30,
   "on_block": 0,
   "damage": 300,
   "does": "They reel the opponent in close like landing the big one, then pull them off their feet.",
   "tell": "The sudden step in and open arms as if casting a line."
  }
 ],
 "weakness": "The hammer-tap strike is slow to land and leaves them wide open if you block it, so step back then punish."
}

tune: every move fair - tournament_ready

Input:

{
 "task": "tune",
 "name": "Mo",
 "style": "all_rounder",
 "style_label": "all-rounder - a bit of everything",
 "quirks": "[{\"id\":\"Q1\",\"text\":\"Always first on the dance floor at weddings\"},{\"id\":\"Q2\",\"text\":\"Throws paper planes across the office\"},{\"id\":\"Q3\",\"text\":\"Answers every question with another question\"}]",
 "moves": "[{\"id\":\"M1\",\"name\":\"Dance Floor Dash\",\"quirk\":\"Q1\",\"type\":\"strike\",\"motion\":\"[4]6\",\"button\":\"K\",\"super\":\"no\",\"startup\":11,\"active\":4,\"recovery\":19,\"on_block\":-4,\"damage\":100,\"does\":\"Slides in on one knee and kicks on the beat.\",\"tell\":\"Taps a foot twice, counting in the song.\"},{\"id\":\"M2\",\"name\":\"Paper Plane\",\"quirk\":\"Q2\",\"type\":\"projectile\",\"motion\":\"236\",\"button\":\"P\",\"super\":\"no\",\"startup\":15,\"active\":3,\"recovery\":30,\"on_block\":-3,\"damage\":80,\"does\":\"Folds and throws a paper plane that glides across the screen.\",\"tell\":\"Licks a finger to test the wind.\"},{\"id\":\"M3\",\"name\":\"Question Back\",\"quirk\":\"Q3\",\"type\":\"strike\",\"motion\":\"214\",\"button\":\"P\",\"super\":\"no\",\"startup\":8,\"active\":5,\"recovery\":24,\"on_block\":-9,\"damage\":120,\"does\":\"Answers your attack with a raised finger and a counter jab.\",\"tell\":\"\"},{\"id\":\"M4\",\"name\":\"Last Song\",\"quirk\":\"Q1\",\"type\":\"strike\",\"motion\":\"236236\",\"button\":\"P\",\"super\":\"yes\",\"startup\":8,\"active\":5,\"recovery\":42,\"on_block\":-22,\"damage\":320,\"does\":\"The lights drop and a full dance routine hits five times.\",\"tell\":\"The music cuts out for a beat.\"}]",
 "figures": "{\"moves\":\"4\",\"flags\":{\"M1\":\"\",\"M2\":\"\",\"M3\":\"\",\"M4\":\"\"},\"calls\":{\"M1\":\"keep\",\"M2\":\"keep\",\"M3\":\"keep\",\"M4\":\"keep\"},\"punish\":{\"M1\":\"minus\",\"M2\":\"minus\",\"M3\":\"punishable\",\"M4\":\"punishable\"},\"nerfs\":\"0\",\"buffs\":\"0\"}",
 "clip_note": ""
}

Reply:

{
 "task": "tune",
 "verdict": "tournament_ready",
 "title": "Mo's Moves Hold Up As Is",
 "summary": "Every one of the four moves already plays fair, so none needs a nerf or a buff and nothing about them is a problem. With nothing to change, next try sharpening the tell on the counter jab so it reads as clearly as the others.",
 "moves": [
  {
   "id": "M1",
   "call": "keep",
   "patch": {
    "startup": 11,
    "active": 4,
    "recovery": 19,
    "on_block": -4,
    "damage": 100
   },
   "why": "Startup 11 and -4 on block keep it hard to punish, and 100 damage fits a strike without over-performing.",
   "tell": "Taps a foot twice, counting in the song.",
   "counter": "Block the dash and take the first move back before Mo can set up the next hit."
  },
  {
   "id": "M2",
   "call": "keep",
   "patch": {
    "startup": 15,
    "active": 3,
    "recovery": 30,
    "on_block": -3,
    "damage": 80
   },
   "why": "Startup 15 and -3 on block make the plane safe to toss, and 80 damage keeps a projectile from dominating neutral.",
   "tell": "Licks a finger to test the wind.",
   "counter": "Block the plane and take your turn, or jump over it and close the distance before another throw."
  },
  {
   "id": "M3",
   "call": "keep",
   "patch": {
    "startup": 8,
    "active": 5,
    "recovery": 24,
    "on_block": -9,
    "damage": 120
   },
   "why": "Startup 8 and -9 on block punish answering it directly, and 120 damage stays reasonable for a counter strike.",
   "tell": "A raised eyebrow and a pause, like another question is already forming.",
   "counter": "Block the counter jab and strike back immediately, since the opening afterward is wide enough to punish."
  },
  {
   "id": "M4",
   "call": "keep",
   "patch": {
    "startup": 8,
    "active": 5,
    "recovery": 42,
    "on_block": -22,
    "damage": 320
   },
   "why": "Startup 8 and -22 on block give room to punish the super, and 320 damage matches a full routine landing clean.",
   "tell": "The music cuts out for a beat.",
   "counter": "Block the full routine and hit back hard, since there is plenty of time before Mo can act again."
  }
 ],
 "game_plan": {
  "as": "Zone with the paper plane throw, bait a swing into the counter jab, and close with the dance floor dash before cashing out with the full routine super.",
  "against": "Watch for the twice tap before the dash and punish on reaction, stay out of plane range, and respect the long recovery after the full routine finishes."
 }
}

tune: one move too safe, one too slow - needs_patch

Input:

{
 "task": "tune",
 "name": "Jo",
 "style": "zoner",
 "style_label": "zoner - keeps you out at range",
 "quirks": "[{\"id\":\"Q1\",\"text\":\"Shouts across the car park instead of walking over\"},{\"id\":\"Q2\",\"text\":\"Takes forever to choose from a menu\"},{\"id\":\"Q3\",\"text\":\"Corrects everyone's grammar in the group chat\"}]",
 "moves": "[{\"id\":\"M1\",\"name\":\"Car Park Shout\",\"quirk\":\"Q1\",\"type\":\"projectile\",\"motion\":\"236\",\"button\":\"P\",\"super\":\"no\",\"startup\":13,\"active\":3,\"recovery\":22,\"on_block\":4,\"damage\":80,\"does\":\"A shout travels the whole screen as a wave of sound.\",\"tell\":\"Cups both hands around the mouth.\"},{\"id\":\"M2\",\"name\":\"Menu Deliberation\",\"quirk\":\"Q2\",\"type\":\"strike\",\"motion\":\"22\",\"button\":\"K\",\"super\":\"no\",\"startup\":34,\"active\":4,\"recovery\":38,\"on_block\":-12,\"damage\":150,\"does\":\"Stares at a giant menu, then slams it shut on the opponent.\",\"tell\":\"Opens a menu and hums.\"},{\"id\":\"M3\",\"name\":\"Red Pen\",\"quirk\":\"Q3\",\"type\":\"strike\",\"motion\":\"214\",\"button\":\"P\",\"super\":\"no\",\"startup\":9,\"active\":4,\"recovery\":20,\"on_block\":-6,\"damage\":110,\"does\":\"Underlines the opponent with a red pen stroke.\",\"tell\":\"Clicks the pen.\"},{\"id\":\"M4\",\"name\":\"Group Chat Storm\",\"quirk\":\"Q3\",\"type\":\"projectile\",\"motion\":\"236236\",\"button\":\"K\",\"super\":\"yes\",\"startup\":10,\"active\":6,\"recovery\":45,\"on_block\":-20,\"damage\":300,\"does\":\"A storm of message bubbles fills the screen.\",\"tell\":\"A phone buzzes three times.\"}]",
 "figures": "{\"moves\":\"4\",\"flags\":{\"M1\":\"too_safe\",\"M2\":\"too_slow\",\"M3\":\"\",\"M4\":\"\"},\"calls\":{\"M1\":\"nerf\",\"M2\":\"buff\",\"M3\":\"keep\",\"M4\":\"keep\"},\"punish\":{\"M1\":\"plus\",\"M2\":\"punishable\",\"M3\":\"punishable\",\"M4\":\"punishable\"},\"nerfs\":\"1\",\"buffs\":\"1\"}",
 "clip_note": ""
}

Reply:

{
 "task": "tune",
 "verdict": "needs_patch",
 "title": "Jo's Zoner Kit: One Nerf, One Buff",
 "summary": "This moveset needs patching: the shout stayed plus on block and the menu slam was slow to start and recover, so one move gets a nerf and one gets a buff. Fix the menu slam's startup first, since its long windup is the easiest tell to punish.",
 "moves": [
  {
   "id": "M1",
   "call": "nerf",
   "patch": {
    "startup": 13,
    "active": 3,
    "recovery": 22,
    "on_block": 2,
    "damage": 80
   },
   "why": "Plus four on block let the shout keep its turn. The patch drops it to plus two so the opponent can act first.",
   "tell": "Cups both hands around the mouth.",
   "counter": "Block the shout and wait for an opening, or jump over it or close the distance before it lands."
  },
  {
   "id": "M2",
   "call": "buff",
   "patch": {
    "startup": 20,
    "active": 4,
    "recovery": 38,
    "on_block": -12,
    "damage": 150
   },
   "why": "Thirty four frames of startup made the slam too easy to see and block. The patch cuts it to twenty so Jo can land it.",
   "tell": "Opens a menu and hums.",
   "counter": "Block the slam and punish immediately since it leaves Jo open afterward."
  },
  {
   "id": "M3",
   "call": "keep",
   "patch": {
    "startup": 9,
    "active": 4,
    "recovery": 20,
    "on_block": -6,
    "damage": 110
   },
   "why": "The pen stays quick and only slightly minus on block, so Jo keeps it safe to throw without being free for a full punish.",
   "tell": "Clicks the pen.",
   "counter": "Block the pen strike and punish right away, since Jo is left open after it lands."
  },
  {
   "id": "M4",
   "call": "keep",
   "patch": {
    "startup": 10,
    "active": 6,
    "recovery": 45,
    "on_block": -20,
    "damage": 300
   },
   "why": "The chat storm stays within the super's own limits for speed and damage, so it keeps real risk when it misses or is blocked.",
   "tell": "A phone buzzes three times.",
   "counter": "Block the message storm and punish straight after, or jump over it or close the distance to avoid it."
  }
 ],
 "game_plan": {
  "as": "Keep Jo at range with the red pen poke and the patched shout, building toward the chat storm super once an opening to end the round appears.",
  "against": "Press forward to dodge the shout, punish the slow menu slam on reaction, and stay ready to jump or step away from the chat storm super."
 }
}

tune: three moves out of line - broken

Input:

{
 "task": "tune",
 "name": "Sam",
 "style": "rushdown",
 "style_label": "rushdown - fast and up close",
 "quirks": "[{\"id\":\"Q1\",\"text\":\"Grabs the last slice of pizza before anyone moves\"},{\"id\":\"Q2\",\"text\":\"Speed-walks everywhere\"},{\"id\":\"Q3\",\"text\":\"Never stops talking during films\"}]",
 "moves": "[{\"id\":\"M1\",\"name\":\"Last Slice\",\"quirk\":\"Q1\",\"type\":\"grab\",\"motion\":\"63214789\",\"button\":\"P\",\"super\":\"no\",\"startup\":3,\"active\":2,\"recovery\":30,\"on_block\":0,\"damage\":170,\"does\":\"Snatches the opponent like the last slice and folds them in half.\",\"tell\":\"Eyes go wide at the box.\"},{\"id\":\"M2\",\"name\":\"Speed Walk\",\"quirk\":\"Q2\",\"type\":\"strike\",\"motion\":\"[4]6\",\"button\":\"K\",\"super\":\"no\",\"startup\":6,\"active\":4,\"recovery\":14,\"on_block\":3,\"damage\":90,\"does\":\"Power-walks through the opponent, elbows pumping.\",\"tell\":\"Checks a watch.\"},{\"id\":\"M3\",\"name\":\"Film Talk\",\"quirk\":\"Q3\",\"type\":\"projectile\",\"motion\":\"236\",\"button\":\"P\",\"super\":\"no\",\"startup\":12,\"active\":3,\"recovery\":26,\"on_block\":-4,\"damage\":90,\"does\":\"A speech bubble of plot spoilers flies forward.\",\"tell\":\"Leans in and whispers.\"},{\"id\":\"M4\",\"name\":\"Credits Roll\",\"quirk\":\"Q3\",\"type\":\"strike\",\"motion\":\"214214\",\"button\":\"P\",\"super\":\"yes\",\"startup\":4,\"active\":6,\"recovery\":30,\"on_block\":-14,\"damage\":420,\"does\":\"Talks through the whole ending while landing a flurry of hits.\",\"tell\":\"\"}]",
 "figures": "{\"moves\":\"4\",\"flags\":{\"M1\":\"too_quick\",\"M2\":\"too_safe\",\"M3\":\"\",\"M4\":\"too_quick,too_strong\"},\"calls\":{\"M1\":\"nerf\",\"M2\":\"nerf\",\"M3\":\"keep\",\"M4\":\"nerf\"},\"punish\":{\"M1\":\"grab\",\"M2\":\"plus\",\"M3\":\"minus\",\"M4\":\"punishable\"},\"nerfs\":\"3\",\"buffs\":\"0\"}",
 "clip_note": ""
}

Reply:

{
 "task": "tune",
 "verdict": "broken",
 "title": "Sam's Fighter Is Broken And Needs Three Nerfs",
 "summary": "Sam's fighter is broken because three of the four moves need a nerf, flagged too quick, too safe, or too strong before patching. Fix the super's old startup and damage first, since it was both too quick and too strong.",
 "moves": [
  {
   "id": "M1",
   "call": "nerf",
   "patch": {
    "startup": 5,
    "active": 2,
    "recovery": 30,
    "on_block": 0,
    "damage": 170
   },
   "why": "Too quick at 3 startup with 170 damage; startup now 5 frames, still a fair, hard-hitting grab.",
   "tell": "Eyes go wide at the box.",
   "counter": "The grab cannot be blocked, so jump or step back out of its range before it lands."
  },
  {
   "id": "M2",
   "call": "nerf",
   "patch": {
    "startup": 6,
    "active": 4,
    "recovery": 14,
    "on_block": 2,
    "damage": 90
   },
   "why": "This strike was too safe on block at 3; lowered to 2 so the opponent gets a fair window back.",
   "tell": "Checks a watch.",
   "counter": "Block it and wait for a real opening before swinging back."
  },
  {
   "id": "M3",
   "call": "keep",
   "patch": {
    "startup": 12,
    "active": 3,
    "recovery": 26,
    "on_block": -4,
    "damage": 90
   },
   "why": "Startup and recovery are modest, the damage is moderate, and the on block number lets the other fighter act first, a fair ranged poke.",
   "tell": "Leans in and whispers.",
   "counter": "Block it and take your turn right after, or jump over it or close the distance before it is thrown."
  },
  {
   "id": "M4",
   "call": "nerf",
   "patch": {
    "startup": 5,
    "active": 6,
    "recovery": 30,
    "on_block": -14,
    "damage": 380
   },
   "why": "This super was too quick at 4 startup and too strong at 420 damage; startup is now 5 and damage 380, still a heavy finisher.",
   "tell": "They start narrating the next scene before the flurry lands.",
   "counter": "Block it and hit back immediately before the flurry can be followed up."
  }
 ],
 "game_plan": {
  "as": "Speed-walk in, bait with the projectile, then grab when they freeze, saving the super as a big punish after you have an opening.",
  "against": "Stay out of grab range, punish the risky super hard on block, and challenge the gap after the strike since it is no longer fully safe."
 }
}

Build the input yourself

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

const K = QcKit;
const form = K.form({ name: "Dana", style: "rushdown", tone: "cheeky",
  quirks: "Always ten minutes late but sprints for the bus\nSends voice notes that are four minutes long\nHugs everyone way too hard" });
const r = K.read(form);                                   // r.quirks: each with its cue and reserved input
const forgeIn = K.toWire(K.buildInput(form, "forge").input);       // POST this to /estimate and /run
// then: const fighter = K.normalize(K.parseReply(job.output.output), "forge");
const moves = K.movesFrom(fighter);                       // edit any number here
moves[0].on_block = 4;                                    // now too safe on block
const built = K.buildInput(form, "tune", { moves });
const tuneIn = K.toWire(built.input);                     // built.input.figures.calls says which moves need a nerf or buff
// K.check(tuneReply, built).notes                        // the page's re-check of a reply
// K.csv(moves, tuneReply)                                // the frame data, one row per move

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://quarter-circle.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":"quarter-circle"}' | 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": "forge", "name": "Dana", "style": "rushdown", "tone": "cheeky", "quirks": "[{\"id\":\"Q1\",\"text\":\"Always ten minutes late but sprints for the bus\",\"cue\":\"rush\",\"type\":\"strike\",\"motion\":\"[4]6\",\"button\":\"K\"},{\"id\":\"Q2\",\"text\":\"Sends voice notes that are four minutes long\",\"cue\":\"projectile\",\"type\":\"projectile\",\"motion\":\"236\",\"button\":\"P\"}, ...]", "figures": "{\"quirks\":\"5\",\"specials\":\"4\",\"stat_total\":\"15\",\"super_id\":\"M5\"}"}
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 quarter-circle:<task>:<inputHash>:a<attempt> (the tasks are forge and tune), 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="quarter-circle:forge:$(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: quarter-circle:forge:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json