Drive Slow Slow Quick from your own code
The one metered task, routine, is available over HTTP. It takes a song's tempo and beats to a bar, the length you will dance, the dance you picked, who is dancing, their level, the floor, the practice days and the page's tempo facts, and returns a verdict (dance it as is, keep it simpler, or switch the dance), three to ten blocks that fill the music phrase by phrase - each with a figure, its count, how it moves, a cue and the partner's mirror - plus a practice plan and a few watch-outs.
The division of labour first: the page owns the tempo fit, the verdict rule and the phrase budget; the model owns the routine; the page re-checks the reply. All three come from stepkit.js, which you can run yourself (see Build the input yourself). The tempo ranges are approximate competition tempos, treated as estimates.
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":"slow-slow-quick"}), 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, style, music, dancers, level, floor, days, page_facts) 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: writing a routine is metered and needs a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the fit and the verdict
Every run sends task: routine; a missing or different one is still answered as routine. The page first rates your tempo against each dance's range (StepKit.fit): in (inside the range), near (within 10%), stretch (half or double the tempo lands near it) or out (far from it, or the wrong beat: a waltz needs three beats to a bar, the rest four).
The verdict then follows a fixed order, first match wins (StepKit.verdictFor):
- switch - the chosen dance's fit is
out. The routine moves to one of theswitch optionsthe page lists (in range or near, and not a hard dance for a first-timer; the slow sway when none is). - simplify - the fit is
stretch, or the dance is a hard one (Viennese waltz, quickstep, samba, jive, paso doble) and the level isnever. At most three figures. - fits - everything else.
The page sends its verdict in page_facts; the model copies it, and the page flags a reply that disagrees.
The input
Every value is a string (objects are never sent). app.js builds it with StepKit.buildInput(form).input.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always routine. |
style | yes | <id> - <name>, one of sway, waltz, foxtrot, tango, rumba, cha, viennese, quickstep, samba, jive, paso. |
music | yes | Beats per minute, beats to a bar, and the length danced (0:30 to 6:00). |
song_note | no | The person's words about the song (at most 300 characters, the middle cut with [...]), or none given. |
dancers | yes | Starts with couple, solo or group, then a short description in brackets. |
level | yes | Starts with never, lessons or social. |
floor | yes | Starts with small, medium or large. |
occasion, limits | no | The person's words (at most 120 and 200 characters), or none given. |
days | yes | Practice days left, 1 to 60. |
page_facts | yes | The page's lines (StepKit.pageFacts): verdict, the chosen dance with its range and count, tempo fit, hard for this level, switch options (switch only), the phrase size, phrases to fill and the figure limit. |
clip_note | no | What the page shortened, or absent. |
retry_note | no | Sent only on the page's one reformat retry. |
The reply
The reply is one JSON object in job.output.output. The page parses it with StepKit.parseReply and StepKit.normalize (a cut-off stream is closed by StepKit.closeJson and shown as "N of 8 sections recovered").
{"task": "routine", "verdict": "fits | simplify | switch", "style": "rumba", "verdict_reason": "...", "title": "...", "feel": "...",
"blocks": [{"n": 1, "phrases": 2, "figure": "...", "rhythm": "...",
"travel": "in_place | forward | back | side | turn | spin | pose",
"cue": "...", "partner_cue": "...", "why": "..."}],
"practice": [{"session": 1, "minutes": 15, "focus": "...", "drill": "..."}],
"watch_outs": ["..."]}
| Field | Rule |
|---|---|
blocks | 3 to 10 blocks whose phrases add up to exactly phrases to fill; figures (the closing pose aside) within the figure limit; the first block in place or a pose, the last a one-phrase pose. cue and partner_cue at most 20 words (partner_cue empty for solo or group); why at most 18 words, using only the tempo, the beat, the block's place or a moment the song note names. |
practice | As many sessions as practice days, at most 5; at most 20 minutes each for first-timers, 30 otherwise. |
watch_outs | 2 to 4 sentences; the first answers limits when it is given. |
feel, title | One sentence of at most 30 words; at most 8 words naming the dance. |
The page then checks the reply against its own facts (StepKit.check): the verdict and the dance, the phrase total, the figure limit, neighbouring blocks that repeat each other, the closing pose, partner cues, forward or back travel on a small floor, the practice sessions and minutes, lifts, dips, jumps, kicks or flips, and internal field names in the prose.
Worked examples, one per verdict
These are the four free samples on the page: the exact input and a stand-in model's reply, unedited.
A rumba first dance - fits
Input:
{
"task": "routine",
"style": "rumba - Rumba",
"music": "100 beats per minute, four beats to a bar, dancing 2:00 of it",
"song_note": "A slow love song. It goes quiet around 1:10 and builds back up to the end.",
"dancers": "couple (a couple dancing in hold)",
"level": "lessons (had a few lessons)",
"floor": "medium (a party or wedding dance floor)",
"occasion": "Wedding first dance",
"limits": "A long dress - nothing that steps back onto the hem",
"days": "14 practice days before the dance",
"page_facts": "verdict: fits\nchosen dance: rumba - Rumba (four beats to a bar, about 96-104 beats per minute, count: slow quick quick (2 3 4-1))\ntempo fit: in - 100 is inside 96-104\nhard for this level: no\nphrase: 8 counts (two bars), about 4.8 seconds\nphrases to fill: 25 (2:00 of music)\nfigure limit: 6 different figures"
}
Reply:
{
"task": "routine",
"verdict": "fits",
"style": "rumba",
"verdict_reason": "100 beats a minute sits inside the usual 96 to 104 range for a rumba, so this dance fits as chosen.",
"title": "A Simple Rumba For Your Song",
"feel": "This rumba stays calm at the start, builds through turns, softens for the quiet part, then builds again to a clear, simple finish.",
"blocks": [
{
"n": 1,
"phrases": 3,
"figure": "basic in place",
"rhythm": "slow quick quick",
"travel": "in_place",
"cue": "Step your left foot in place, shift weight onto it, then bring your right foot beside it.",
"partner_cue": "Step your right foot in place, shift weight onto it, then bring your left foot beside it.",
"why": "A steady beat to settle into the rhythm before you move together."
},
{
"n": 2,
"phrases": 4,
"figure": "side step",
"rhythm": "slow quick quick",
"travel": "side",
"cue": "Step your left foot to the side, shift your weight onto it, then close your right foot beside it.",
"partner_cue": "Step your right foot to the side, shift your weight onto it, then close your left foot beside it.",
"why": "Moving side to side opens the music up after the calm start."
},
{
"n": 3,
"phrases": 4,
"figure": "forward walks",
"rhythm": "slow quick quick",
"travel": "forward",
"cue": "Walk forward onto your left foot, then your right foot, moving side by side with your partner.",
"partner_cue": "Walk forward onto your right foot, then your left foot, moving side by side with your partner.",
"why": "Walking forward carries the phrase onward as the song keeps building."
},
{
"n": 4,
"phrases": 3,
"figure": "turn together",
"rhythm": "slow quick quick",
"travel": "turn",
"cue": "Turn a quarter to your left as you step, keeping your frame steady and weight changing smoothly.",
"partner_cue": "Turn a quarter to your right as you step, keeping your frame steady and weight changing smoothly.",
"why": "A turn builds contrast right before the song's quiet moment arrives."
},
{
"n": 5,
"phrases": 4,
"figure": "basic in place",
"rhythm": "slow quick quick",
"travel": "in_place",
"cue": "Step your left foot in place, shift weight onto it, then bring your right foot beside it.",
"partner_cue": "Step your right foot in place, shift weight onto it, then bring your left foot beside it.",
"why": "Stepping in place matches the quiet part the song moves through."
},
{
"n": 6,
"phrases": 4,
"figure": "underarm turn",
"rhythm": "slow quick quick",
"travel": "spin",
"cue": "Raise your left hand gently to guide your partner turning under it while you step in place.",
"partner_cue": "Turn under your partner's raised hand in a full circle, keeping your steps small and smooth.",
"why": "An underarm turn matches the music building back up to the end."
},
{
"n": 7,
"phrases": 2,
"figure": "side step",
"rhythm": "slow quick quick",
"travel": "side",
"cue": "Step your left foot to the side, shift your weight onto it, then close your right foot beside it.",
"partner_cue": "Step your right foot to the side, shift your weight onto it, then close your left foot beside it.",
"why": "A side step keeps the energy rising as the ending nears."
},
{
"n": 8,
"phrases": 1,
"figure": "closing pose",
"rhythm": "slow quick quick",
"travel": "pose",
"cue": "Come together into a still, simple pose and hold it for the final beat.",
"partner_cue": "Come together into a still, simple pose and hold it for the final beat.",
"why": "A held pose gives the phrase a clear, simple finish."
}
],
"practice": [
{
"session": 1,
"minutes": 20,
"focus": "the basic in place and the side step (blocks 1 and 2)",
"drill": "Count slow quick quick out loud while doing the basic in place and the side step."
},
{
"session": 2,
"minutes": 20,
"focus": "the forward walks and the turn together (blocks 3 and 4)",
"drill": "Walk the forward pattern without music, then add the quarter turn slowly, counting each step."
},
{
"session": 3,
"minutes": 25,
"focus": "the quiet in place section and the underarm turn (blocks 5 and 6)",
"drill": "Do a slow run-through of the in place step and the underarm turn, counting slow quick quick."
},
{
"session": 4,
"minutes": 25,
"focus": "linking the side step through to the closing pose (blocks 7 and 8)",
"drill": "Do a slow run-through from the side step to the closing pose, holding the final pose."
},
{
"session": 5,
"minutes": 30,
"focus": "the whole routine from start to finish",
"drill": "Do a full run-through of the whole routine with the song, from the start to the closing pose."
}
],
"watch_outs": [
"This routine keeps every step moving forward, in place, or to the side, so you never step back onto your dress hem.",
"The floor is a medium size, so keep your forward steps small and stay aware of where you are dancing.",
"Keep your hold light and your weight balanced on your own feet throughout the turns."
]
}
Quickstep for two first-timers - simplify (a hard dance)
Input:
{
"task": "routine",
"style": "quickstep - Quickstep",
"music": "204 beats per minute, four beats to a bar, dancing 1:00 of it",
"song_note": "A fast big-band tune with a loud horn hit at the very end.",
"dancers": "couple (a couple dancing in hold)",
"level": "never (never danced before)",
"floor": "large (a full ballroom floor)",
"occasion": "Office party dance-off",
"limits": "none given",
"days": "4 practice days before the dance",
"page_facts": "verdict: simplify\nchosen dance: quickstep - Quickstep (four beats to a bar, about 200-208 beats per minute, count: slow quick quick)\ntempo fit: in - 204 is inside 200-208\nhard for this level: yes\nphrase: 8 counts (two bars), about 2.4 seconds\nphrases to fill: 25 (1:00 of music)\nfigure limit: 3 different figures"
}
Reply:
{
"task": "routine",
"verdict": "simplify",
"style": "quickstep",
"verdict_reason": "Quickstep fits the tempo at 204 beats per minute, but is hard for someone who has never danced before, so the routine is simplified.",
"title": "A Simple Quickstep Routine",
"feel": "The routine starts calmly in place, builds through side steps and a turn, then holds still for the loud moment at the end of your song.",
"blocks": [
{
"n": 1,
"phrases": 6,
"figure": "a basic",
"rhythm": "slow quick quick",
"travel": "in_place",
"cue": "Step right in place, close left beside it, step right again, then repeat starting with your left foot.",
"partner_cue": "Mirror with your left: step left in place, close right beside it, step left again, then repeat on the right.",
"why": "A calm start in place lets you settle into the quick four-beat tempo before the routine builds."
},
{
"n": 2,
"phrases": 5,
"figure": "a side step",
"rhythm": "slow quick quick",
"travel": "side",
"cue": "Step side on your right, close your left, step side on your right again to travel across the floor.",
"partner_cue": "Mirror by stepping side on your left, close your right, step side on your left again, following smoothly.",
"why": "The steady four-beat bar carries you into a side step, building gently after the calm opening."
},
{
"n": 3,
"phrases": 4,
"figure": "a basic",
"rhythm": "slow quick quick",
"travel": "in_place",
"cue": "Repeat the in-place basic: step right, close left, step right, then lead with your left.",
"partner_cue": "Mirror the basic: step left, close right, step left, then follow with your right.",
"why": "Returning to the in-place basic gives a short rest before the next turn in the music."
},
{
"n": 4,
"phrases": 5,
"figure": "a quarter turn",
"rhythm": "slow quick quick",
"travel": "turn",
"cue": "Step forward on your right, turn a quarter together, close your left, then quick quick to finish.",
"partner_cue": "Step back on your left, turn with the quarter, close your right, then quick quick to finish.",
"why": "A quarter turn adds contrast partway through, matching the steady quick tempo of the four-beat bar."
},
{
"n": 5,
"phrases": 4,
"figure": "a side step",
"rhythm": "slow quick quick",
"travel": "side",
"cue": "Step side on your right again, close your left, step side once more, staying light on your feet.",
"partner_cue": "Mirror by stepping side on your left again, close your right, step side once more, staying light.",
"why": "A second side step builds energy again as the routine moves toward its finish."
},
{
"n": 6,
"phrases": 1,
"figure": "closing pose",
"rhythm": "hold for the full phrase",
"travel": "pose",
"cue": "Come together and hold still, weight even, facing your partner for the finish.",
"partner_cue": "Hold together with even weight, staying still and facing your partner for the finish.",
"why": "Holding still here meets the loud horn hit at the very end of your song."
}
],
"practice": [
{
"session": 1,
"minutes": 15,
"focus": "the basic in block 1",
"drill": "Walk through block 1's basic slowly without music, counting slow quick quick out loud."
},
{
"session": 2,
"minutes": 20,
"focus": "the side step in blocks 2 and 5",
"drill": "Practice the side step from block 2, counting slow quick quick while moving across the floor."
},
{
"session": 3,
"minutes": 20,
"focus": "the quarter turn in block 4",
"drill": "Walk the quarter turn in block 4 slowly without music, then add the slow quick quick count."
},
{
"session": 4,
"minutes": 20,
"focus": "a full run-through with the start and end",
"drill": "Run the whole routine from block 1 through the closing pose in block 6, slowly."
}
],
"watch_outs": [
"This quickstep is fast, so keep steps small and take your time learning the counts before speeding up.",
"With a large floor, give yourselves room for the side steps and the quarter turn.",
"Keep weight changes clear and controlled rather than rushed, since the tempo is quick.",
"Stop and rest if you feel unsteady, especially while learning the turn together."
]
}
A solo waltz to a fast song - simplify (half time)
Input:
{
"task": "routine",
"style": "waltz - Waltz",
"music": "174 beats per minute, three beats to a bar, dancing 1:00 of it",
"song_note": "A quick song in three-beat time.",
"dancers": "solo (one person dancing alone)",
"level": "lessons (had a few lessons)",
"floor": "small (a living room or a corner, roughly 3 by 3 metres)",
"occasion": "Practising at home between classes",
"limits": "none given",
"days": "3 practice days before the dance",
"page_facts": "verdict: simplify\nchosen dance: waltz - Waltz (three beats to a bar, about 84-90 beats per minute, count: 1 2 3, 1 2 3)\ntempo fit: stretch (half time) - half of 174 is 87, near 84-90\nhard for this level: no\nphrase: 6 counts (two bars), about 2.1 seconds\nphrases to fill: 29 (1:00 of music)\nfigure limit: 3 different figures"
}
Reply:
{
"task": "routine",
"verdict": "simplify",
"style": "waltz",
"verdict_reason": "At 174 beats with three beats to a bar, half of that is 87, a stretch for the waltz's usual speed, so the routine is simplified.",
"title": "A Simple Solo Waltz Routine",
"feel": "The routine stays mostly in place, moving briefly forward and side to raise energy, then settles into a calm closing pose.",
"blocks": [
{
"n": 1,
"phrases": 6,
"figure": "box step",
"rhythm": "1 2 3, 1 2 3 (every other beat)",
"travel": "in_place",
"cue": "Step forward left, side right, close feet. Step back right, side left, close. Repeat, staying in place.",
"partner_cue": "",
"why": "This slow box eases you into the three-beat feel before you move through the space."
},
{
"n": 2,
"phrases": 5,
"figure": "forward walks",
"rhythm": "1 2 3, 1 2 3 (every other beat)",
"travel": "forward",
"cue": "Walk forward on alternating feet, left then right, taking one step every other beat.",
"partner_cue": "",
"why": "The forward walks open up the floor, building energy after the steady opening box."
},
{
"n": 3,
"phrases": 6,
"figure": "box step",
"rhythm": "1 2 3, 1 2 3 (every other beat)",
"travel": "in_place",
"cue": "Return to the box: forward left, side right, close; back right, side left, close.",
"partner_cue": "",
"why": "Coming back to the box gives a moment of rest after moving across the floor."
},
{
"n": 4,
"phrases": 5,
"figure": "side step",
"rhythm": "1 2 3, 1 2 3 (every other beat)",
"travel": "side",
"cue": "Step side left, close right, step side right, close left, keep it smooth and even.",
"partner_cue": "",
"why": "The side steps add contrast in place, a change of direction before the final box."
},
{
"n": 5,
"phrases": 6,
"figure": "box step",
"rhythm": "1 2 3, 1 2 3 (every other beat)",
"travel": "in_place",
"cue": "Finish with the box again: forward left, side right, close; back right, side left, close.",
"partner_cue": "",
"why": "This steady box brings the routine to a calm, clear finish in the three-beat time."
},
{
"n": 6,
"phrases": 1,
"figure": "closing pose",
"rhythm": "hold, no steps",
"travel": "pose",
"cue": "Hold a tall, relaxed pose on the last beat of the phrase.",
"partner_cue": "",
"why": "A short held pose marks a clean, clear ending to the music."
}
],
"practice": [
{
"session": 1,
"minutes": 20,
"focus": "the box step in blocks 1, 3 and 5",
"drill": "Count 1, 2, 3, 1, 2, 3 out loud while slowly walking through the box step without music."
},
{
"session": 2,
"minutes": 25,
"focus": "the forward walks and side step in blocks 2 and 4",
"drill": "Walk block 2's forward steps, then block 4's side steps, counting every other beat with no music."
},
{
"session": 3,
"minutes": 30,
"focus": "a full run-through from the opening box to the closing pose",
"drill": "Run the whole routine once from block 1 through the closing pose with your song playing."
}
],
"watch_outs": [
"Your floor is small, so this routine favors the box step and keeps travel short.",
"The half-time step can feel stretched at first, so keep steps even rather than rushed.",
"With only a few lessons, keep the early sessions slow before adding the full tempo."
]
}
A waltz picked for a four-beat song - switch
Input:
{
"task": "routine",
"style": "waltz - Waltz",
"music": "128 beats per minute, four beats to a bar, dancing 1:15 of it",
"song_note": "Upbeat pop song with a clap on every second beat.",
"dancers": "group (a group dancing side by side, everyone doing the same steps)",
"level": "social (dances socially)",
"floor": "small (a living room or a corner, roughly 3 by 3 metres)",
"occasion": "Living-room dance-off with friends",
"limits": "One of us has a sore knee - no deep bends",
"days": "2 practice days before the dance",
"page_facts": "verdict: switch\nchosen dance: waltz - Waltz (three beats to a bar, about 84-90 beats per minute, count: 1 2 3, 1 2 3)\ntempo fit: out - it needs three beats to a bar\nhard for this level: no\nswitch options: tango - Tango (in; count: slow slow quick quick slow); cha - Cha-cha (in; count: 2 3 4-and-1); sway - Slow sway (in; count: step-touch, step-touch); paso - Paso doble (near; count: 1 2, marching)\nphrase: 8 counts (two bars), about 3.8 seconds\nphrases to fill: 20 (1:15 of music)\nfigure limit: 8 different figures"
}
Reply:
{
"task": "routine",
"verdict": "switch",
"style": "tango",
"verdict_reason": "At 128 beats per minute with four beats to a bar, a waltz needs three beats to a bar and does not fit, so this routine uses a tango instead.",
"title": "A Simple Group Tango For A Small Floor",
"feel": "The routine stays steady and in place to start, moves outward through forward, side, and turning steps, then eases back to a calm, clear finish.",
"blocks": [
{
"n": 1,
"phrases": 5,
"figure": "a basic",
"rhythm": "slow slow quick quick slow",
"travel": "in_place",
"cue": "Step together in place: slow, slow, quick, quick, slow, keeping your weight even and knees soft.",
"partner_cue": "",
"why": "A steady in-place start lets everyone catch the clap on every second beat together."
},
{
"n": 2,
"phrases": 3,
"figure": "walks",
"rhythm": "slow slow quick quick slow",
"travel": "forward",
"cue": "Walk forward together, small steps, changing your weight on each slow or quick count.",
"partner_cue": "",
"why": "The forward walks build energy as the phrase moves forward within the four-beat bar."
},
{
"n": 3,
"phrases": 4,
"figure": "a side step",
"rhythm": "slow slow quick quick slow",
"travel": "side",
"cue": "Step side together, close your feet, then repeat the same pattern to the same side.",
"partner_cue": "",
"why": "The side steps add contrast partway through, keeping movement compact for the small floor."
},
{
"n": 4,
"phrases": 4,
"figure": "a quarter turn",
"rhythm": "slow slow quick quick slow",
"travel": "turn",
"cue": "Turn a quarter together on the slow counts, then step in place on your quicks.",
"partner_cue": "",
"why": "The quarter turns add a change of direction as the routine builds before slowing."
},
{
"n": 5,
"phrases": 3,
"figure": "walks",
"rhythm": "slow slow quick quick slow",
"travel": "back",
"cue": "Walk backward together, small steps, matching your slow, slow, quick, quick, slow count.",
"partner_cue": "",
"why": "The backward walks ease the energy down as the routine moves toward its close."
},
{
"n": 6,
"phrases": 1,
"figure": "closing pose",
"rhythm": "hold still through the final count",
"travel": "pose",
"cue": "Come together and hold your simple standing pose for the final count.",
"partner_cue": "",
"why": "The closing pose gives a clear, calm finish on the last count of the phrase."
}
],
"practice": [
{
"session": 1,
"minutes": 20,
"focus": "the basic in block 1 and the walks in block 2",
"drill": "Count the steps out loud to your song, then walk blocks 1 and 2 without music."
},
{
"session": 2,
"minutes": 25,
"focus": "the full routine from the opening basic to the closing pose",
"drill": "Do a full run-through with the start and end, keeping the turn and the back walks steady."
}
],
"watch_outs": [
"One of you has a sore knee, so keep knees soft and skip any deep bends throughout.",
"The floor is small, so keep forward and backward steps short and stay close together.",
"Keep everyone's steps and turns matched in time so the group moves together smoothly."
]
}
Build the input yourself
Load the script in node (global.window = global; require("./stepkit.js")), then:
const K = StepKit;
const form = K.form({ style: "foxtrot", bpm: "116", meter: "4", length: "2:00", dancers: "couple",
level: "never", floor: "medium", days: "6", occasion: "Anniversary party", limits: "", song_note: "" });
K.fits(K.bpmOf(form), form.meter); // every dance's fit at this tempo: in, near, stretch or out
K.verdictFor(form); // {verdict: "fits", fit, why} - the page's rule
K.budget(form); // beats, phrases, phrase length in counts and seconds
K.sketch(form); // the page's placeholder routine from beginner figures
const built = K.buildInput(form);
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = K.normalize(K.parseReply(job.output.output));
// K.check(reply, built, form).notes // the page's re-check of the reply
// K.md(form, reply) // the routine as Markdown
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://slow-slow-quick.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://slow-slow-quick.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"]
// slow-slow-quick.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://slow-slow-quick.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://slow-slow-quick.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
}
// SlowSlowQuick.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 SlowSlowQuick {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://slow-slow-quick.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://slow-slow-quick.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://slow-slow-quick.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://slow-slow-quick.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":"slow-slow-quick"}' | 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": "slow-slow-quick"})["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: "slow-slow-quick" })).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": "slow-slow-quick"}, 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", "slow-slow-quick"), 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: "slow-slow-quick" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "slow-slow-quick"])["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 = "slow-slow-quick" }))!["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": "routine", "style": "rumba - Rumba", "music": "100 beats per minute, four beats to a bar, dancing 2:00 of it", "song_note": "A slow love song. It goes quiet around 1:10 and builds back up to the end.", "dancers": "couple (a couple dancing in hold)", "level": "lessons (had a few lessons)", "floor": "medium (a party or wedding dance floor)", "occasion": "Wedding first dance", "limits": "A long dress - nothing that steps back onto the hem", "days": "14 practice days before the dance", "page_facts": "verdict: fits\nchosen dance: rumba - Rumba (four beats to a bar, about 96-104 beats per minute, count: slow quick quick (2 3 4-1))\ntempo fit: in - 100 is inside 96-104\nhard for this level: no\nphrase: 8 counts (two bars), about 4.8 seconds\nphrases to fill: 25 (2:00 of music)\nfigure limit: 6 different figures"}
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": "routine", "style": "rumba - Rumba", "music": "100 beats per minute, four beats to a bar, dancing 2:00 of it", "song_note": "A slow love song. It goes quiet around 1:10 and builds back up to the end.", "dancers": "couple (a couple dancing in hold)", "level": "lessons (had a few lessons)", "floor": "medium (a party or wedding dance floor)", "occasion": "Wedding first dance", "limits": "A long dress - nothing that steps back onto the hem", "days": "14 practice days before the dance", "page_facts": "verdict: fits\nchosen dance: rumba - Rumba (four beats to a bar, about 96-104 beats per minute, count: slow quick quick (2 3 4-1))\ntempo fit: in - 100 is inside 96-104\nhard for this level: no\nphrase: 8 counts (two bars), about 4.8 seconds\nphrases to fill: 25 (2:00 of music)\nfigure limit: 6 different figures"}
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: "routine", style, music, song_note, dancers, level, floor, occasion, limits, days, page_facts, clip_note} (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 slow-slow-quick:<task>:<inputHash>:a<attempt> (the one task is routine), 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="slow-slow-quick:routine:$(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 = "slow-slow-quick:" + 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 = `slow-slow-quick:${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 := "slow-slow-quick:" + 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 = "slow-slow-quick:" + 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 = "slow-slow-quick:#{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 = "slow-slow-quick:" . $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 = $"slow-slow-quick:{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: slow-slow-quick:routine:$(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": "routine", "style": "rumba - Rumba", "music": "100 beats per minute, four beats to a bar, dancing 2:00 of it", "song_note": "A slow love song. It goes quiet around 1:10 and builds back up to the end.", "dancers": "couple (a couple dancing in hold)", "level": "lessons (had a few lessons)", "floor": "medium (a party or wedding dance floor)", "occasion": "Wedding first dance", "limits": "A long dress - nothing that steps back onto the hem", "days": "14 practice days before the dance", "page_facts": "verdict: fits\nchosen dance: rumba - Rumba (four beats to a bar, about 96-104 beats per minute, count: slow quick quick (2 3 4-1))\ntempo fit: in - 100 is inside 96-104\nhard for this level: no\nphrase: 8 counts (two bars), about 4.8 seconds\nphrases to fill: 25 (2:00 of music)\nfigure limit: 6 different figures"}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "slow-slow-quick:routine:" + 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": `slow-slow-quick:routine:${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", "slow-slow-quick:routine:"+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", "slow-slow-quick:routine:" + 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" => "slow-slow-quick:routine:#{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: slow-slow-quick:routine:" . 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", $"slow-slow-quick:routine:{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);