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.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, quirks) 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: both tasks are metered and need 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, 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:
- broken - two or more nerfs.
- needs_patch - at least one nerf or buff.
- 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
| Field | Type | Notes |
|---|---|---|
task | string | forge or tune. Required. |
name | string | What you call the person, up to 40 characters. May be empty (the forge picks a nickname from the quirks). |
style, style_label | string | rushdown, zoner, grappler or all_rounder, and its plain label. |
tone, tone_label | string | forge only: kind or cheeky, and its plain label. |
quirks | string (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. |
figures | string (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. |
moves | string (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_note | string | What 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"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://quarter-circle.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"]
// quarter-circle.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://quarter-circle.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://quarter-circle.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
}
// QuarterCircle.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 QuarterCircle {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://quarter-circle.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://quarter-circle.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://quarter-circle.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://quarter-circle.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":"quarter-circle"}' | 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": "quarter-circle"})["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: "quarter-circle" })).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": "quarter-circle"}, 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", "quarter-circle"), 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: "quarter-circle" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "quarter-circle"])["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 = "quarter-circle" }))!["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": "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}'
inp = json.load(open("input.json")) # {"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\"}"}
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: "forge", name, style, tone, quirks, figures, ... (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 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}'
import hashlib, time
key = "quarter-circle:" + 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 reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("title"))
import { createHash } from "node:crypto";
const key = `quarter-circle:${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.title);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "quarter-circle:" + 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 = "quarter-circle:" + 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 = "quarter-circle:#{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 = "quarter-circle:" . $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 = $"quarter-circle:{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: quarter-circle:forge:$(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": "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\"}"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "quarter-circle:forge:" + 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": `quarter-circle:forge:${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", "quarter-circle:forge:"+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", "quarter-circle:forge:" + 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" => "quarter-circle:forge:#{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: quarter-circle:forge:" . 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", $"quarter-circle:forge:{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);