Drive the rhythm coach from your own code
The measuring happens in the browser: the page plays the click, records each tap and matches it to the beat grid. The coach over HTTP takes the result of that - a timing profile - and returns a diagnosis and a practice set. A natural use is your own practice app, a drum-teacher dashboard or a MIDI tool that already has tap times: compute the same profile (the page's engine.js does it, and runs in node) and send it.
The page never trusts the model's numbers. Every statistic a reply cites is looked up in the browser's own measurements and marked verified, mismatched or not checkable, and every exercise must name a pattern from the library in a meter it exists in. Over the API nothing has checked the reply yet - see the end of the examples for the same checks in node.
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":"beat-offset"}). Send it as Authorization: Bearer …. The run body is the input object: never wrap it as {"input": {…}} - that returns 200, warns unknown field 'input', and hides every field from the model.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object. |
| 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: the coach 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 comes first
| task | When | What comes back |
|---|---|---|
diagnose | After a take. | 2-5 findings each citing one statistic, 0-4 callouts pinned to bars, a practice set of 3-5 exercises (warm-up, skill, performance, cool-down), a next step. progress is empty. |
recheck | After practising that set: a new take, plus previous (the earlier reply, the diagnosed take's numbers and what was practised). | The same shape, plus progress: each statistic before and after with a verdict. |
The declared input schema is scalar-only, so every object or list travels as a JSON string. task is required.
| Field | Type | Meaning |
|---|---|---|
task | string | diagnose or recheck |
pattern_id, pattern_name | string | A pattern from the library (library lists them) |
meter | string | 3/4, 4/4, 5/4 or 6/8 (the 6/8 beat is the dotted quarter) |
level, tempo, bars, latency_ms | number | Pattern level 1-5, BPM of the beat, bars recorded, latency already subtracted |
click_mode | string | full, dropout (2 on, 2 off), backbeat, countin |
subdiv, audio | boolean | Subdivision clicks; whether the click was audible (false = visual only) |
stats | JSON string | Session numbers: hit_rate, pocket_pct, mean_ms, spread_ms, drift_ms_per_bar, felt_bpm, clicked/silent, ... |
positions, bar_stats | JSON string | Per position (beat1..beat7, e, and, a, trip, let) and per bar |
taps, missed, clip_note | JSON string / string | The clipped tap list (first 24 in order + 12 worst), up to 16 missed notes, and what was left out |
library | JSON string | The patterns the coach may prescribe: {id, name, level, meters} |
learner_note | string | Optional goal or instrument |
previous | JSON string | recheck only |
retry_note | string | Set by the page when it retries an unreadable reply once |
The reply
One JSON object: {"task", "headline", "findings":[{"finding","stat","position","bar","value","severity"}], "callouts":[{"bar","text"}], "prescription":[{"phase","title","pattern","meter","tempo","bars","click","minutes","cue","why"}], "progress":[{"stat","position","before","after","verdict"}], "next_step"}. stat is a session statistic (pocket_pct, mean_ms, ...), position_mean_ms/position_spread_ms with a position, or bar_mean_ms/bar_spread_ms with a bar.
Worked examples
The three examples on the page, with their real run inputs and the saved replies the page replays.
diagnose: straight eighths at 96 with the click dropping out, rushing the &
Input:
{
"task": "diagnose",
"pattern_id": "eighths",
"pattern_name": "Straight eighths",
"meter": "4/4",
"level": 1,
"tempo": 96,
"bars": 8,
"click_mode": "dropout",
"subdiv": false,
"audio": true,
"latency_ms": 0,
"stats": "(a JSON string - decoded below)",
"positions": "(a JSON string - decoded below)",
"bar_stats": "(a JSON string - decoded below)",
"taps": "(a JSON string - decoded below)",
"missed": "(a JSON string - decoded below)",
"clip_note": "measured 62 taps, sent 36 (the first 24 in order and the 12 furthest off the grid). Every aggregate covers every tap.",
"library": "(a JSON string - decoded below)",
"learner_note": "I play drums in a covers band; the singer says I speed up in the verses."
}
stats, decoded:
{
"expected": 64,
"hits": 62,
"missed": 2,
"extra": 1,
"hit_rate": 96.9,
"pocket_pct": 64.1,
"mean_ms": -16.1,
"spread_ms": 17.3,
"median_ms": -14,
"mean_abs_ms": 18.8,
"p90_abs_ms": 39.7,
"early": 21,
"late": 0,
"drift_ms_per_bar": -1.3,
"drift_r2": 0.03,
"felt_bpm": 96.1,
"first_half_ms": -15.7,
"second_half_ms": -16.5,
"clicked": {
"n": 31,
"mean_ms": -10.8,
"spread_ms": 16.7
},
"silent": {
"n": 31,
"mean_ms": -21.4,
"spread_ms": 16.6
},
"tendency": "rushing"
}
positions, decoded:
[
{
"key": "beat1",
"label": "1",
"expected": 8,
"n": 8,
"mean_ms": -2.2,
"spread_ms": 10.2
},
{
"key": "and",
"label": "&",
"expected": 32,
"n": 31,
"mean_ms": -29.6,
"spread_ms": 12.2
},
{
"key": "beat2",
"label": "2",
"expected": 8,
"n": 7,
"mean_ms": -1.1,
"spread_ms": 9
},
{
"key": "beat3",
"label": "3",
"expected": 8,
"n": 8,
"mean_ms": -5.9,
"spread_ms": 8.9
},
{
"key": "beat4",
"label": "4",
"expected": 8,
"n": 8,
"mean_ms": -0.9,
"spread_ms": 9.7
}
]
bar_stats, decoded:
[
{
"bar": 1,
"clicked": true,
"expected": 8,
"n": 8,
"mean_ms": -15.8,
"spread_ms": 15.6
},
{
"bar": 2,
"clicked": true,
"expected": 8,
"n": 7,
"mean_ms": -8.3,
"spread_ms": 18.2
},
{
"bar": 3,
"clicked": false,
"expected": 8,
"n": 8,
"mean_ms": -13.1,
"spread_ms": 17.3
},
{
"bar": 4,
"clicked": false,
"expected": 8,
"n": 8,
"mean_ms": -24.6,
"spread_ms": 15.8
},
{
"bar": 5,
"clicked": true,
"expected": 8,
"n": 8,
"mean_ms": -10.4,
"spread_ms": 21.1
},
{
"bar": 6,
"clicked": true,
"expected": 8,
"n": 8,
"mean_ms": -8.2,
"spread_ms": 13.2
},
{
"bar": 7,
"clicked": false,
"expected": 8,
"n": 7,
"mean_ms": -22,
"spread_ms": 16
},
{
"bar": 8,
"clicked": false,
"expected": 8,
"n": 8,
"mean_ms": -26,
"spread_ms": 17.2
}
]
taps, decoded:
[
{
"n": 1,
"bar": 1,
"pos": "beat1",
"off": -6.2,
"why": "first"
},
{
"n": 2,
"bar": 1,
"pos": "and",
"off": -31.6,
"why": "first"
},
{
"n": 3,
"bar": 1,
"pos": "beat2",
"off": -1,
"why": "first"
},
{
"n": 4,
"bar": 1,
"pos": "and",
"off": -43.9,
"why": "first"
},
{
"n": 5,
"bar": 1,
"pos": "beat3",
"off": -4.5,
"why": "first"
},
{
"n": 6,
"bar": 1,
"pos": "and",
"off": -18,
"why": "first"
},
{
"n": 7,
"bar": 1,
"pos": "beat4",
"off": -1.7,
"why": "first"
},
{
"n": 8,
"bar": 1,
"pos": "and",
"off": -19.8,
"why": "first"
},
{
"n": 9,
"bar": 2,
"pos": "beat1",
"off": 1.3,
"why": "first"
},
{
"n": 10,
"bar": 2,
"pos": "and",
"off": -32.8,
"why": "first"
},
{
"n": 11,
"bar": 2,
"pos": "beat2",
"off": 10.5,
"why": "first"
},
{
"n": 12,
"bar": 2,
"pos": "and",
"off": -32.1,
"why": "first"
},
{
"n": 13,
"bar": 2,
"pos": "beat3",
"off": 10.6,
"why": "first"
},
{
"n": 15,
"bar": 2,
"pos": "beat4",
"off": -5.8,
"why": "first"
},
{
"n": 16,
"bar": 2,
"pos": "and",
"off": -10,
"why": "first"
},
{
"n": 17,
"bar": 3,
"pos": "beat1",
"off": 3.9,
"why": "first"
},
{
"n": 18,
"bar": 3,
"pos": "and",
"off": -22.5,
"why": "first"
},
{
"n": 19,
"bar": 3,
"pos": "beat2",
"off": -7,
"why": "first"
},
{
"n": 20,
"bar": 3,
"pos": "and",
"off": -39.7,
"why": "first"
},
{
"n": 21,
"bar": 3,
"pos": "beat3",
"off": 0.2,
"why": "first"
},
{
"n": 22,
"bar": 3,
"pos": "and",
"off": -28.8,
"why": "first"
},
{
"n": 23,
"bar": 3,
"pos": "beat4",
"off": 9.3,
"why": "first"
},
{
"n": 24,
"bar": 3,
"pos": "and",
"off": -19.9,
"why": "first"
},
{
"n": 25,
"bar": 4,
"pos": "beat1",
"off": -17.5,
"why": "first"
},
{
"n": 64,
"bar": 8,
"pos": "and",
"off": -53.5,
"why": "worst"
},
{
"n": 26,
"bar": 4,
"pos": "and",
"off": -49.6,
"why": "worst"
},
{
"n": 56,
"bar": 7,
"pos": "and",
"off": -41.8,
"why": "worst"
},
{
"n": 52,
"bar": 7,
"pos": "and",
"off": -40.5,
"why": "worst"
},
{
"n": 60,
"bar": 8,
"pos": "and",
"off": -39.7,
"why": "worst"
},
{
"n": 58,
"bar": 8,
"pos": "and",
"off": -37.5,
"why": "worst"
},
{
"n": 32,
"bar": 4,
"pos": "and",
"off": -37.3,
"why": "worst"
},
{
"n": 34,
"bar": 5,
"pos": "and",
"off": -37.1,
"why": "worst"
},
{
"n": 38,
"bar": 5,
"pos": "and",
"off": -36.5,
"why": "worst"
}
... (shortened here)
missed, decoded:
[
{
"n": 14,
"bar": 2,
"pos": "and"
},
{
"n": 51,
"bar": 7,
"pos": "beat2"
}
]
library, decoded:
[
{
"id": "pulse",
"name": "Quarter pulse",
"level": 1,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "eighths",
"name": "Straight eighths",
"level": 1,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "backbeat",
"name": "Backbeat 2 and 4",
"level": 1,
"meters": [
"4/4"
]
},
{
"id": "offbeats",
"name": "Off-beats (the &)",
"level": 2,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "triplets",
"name": "Eighth triplets",
"level": 2,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "swing",
"name": "Swung eighths",
"level": 2,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "charleston",
"name": "Charleston",
"level": 2,
"meters": [
"4/4"
]
},
{
"id": "sixteenths",
"name": "Straight sixteenths",
"level": 3,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "gallop",
"name": "Gallop (8th + two 16ths)",
"level": 3,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "reverse-gallop",
"name": "Reverse gallop (two 16ths + 8th)",
"level": 3,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "tresillo",
"name": "Tresillo 3-3-2",
"level": 3,
"meters": [
"4/4"
]
},
{
"id": "skip-triplet",
"name": "Triplet skip (trip-let)",
"level": 3,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "e-and-a",
"name": "Sixteenths without the beat",
"level": 4,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "the-a",
"name": "Only the 'a'",
"level": 4,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "hemiola",
"name": "Hemiola (two over three)",
"level": 4,
"meters": [
"3/4"
]
},
{
"id": "poly-3-2",
"name": "Three over two",
"level": 5,
"meters": [
"4/4"
]
},
{
"id": "jig-pulse",
"name": "6/8 pulse",
"level": 1,
"meters": [
"6/8"
]
},
{
"id": "jig-eighths",
"name": "6/8 eighths",
"level": 2,
"meters": [
"6/8"
]
},
{
"id": "jig-bell",
"name": "6/8 bell",
"level": 4,
"meters": [
"6/8"
]
}
]
Reply:
{
"task": "diagnose",
"headline": "Your beats sit right on the click, but every \"and\" comes early, and when the click drops out the whole groove slides ahead - that is the speeding-up your singer hears.",
"findings": [
{
"finding": "The \"and\" between the beats is rushed: it lands about 30 ms early on average, while beats 1-4 sit within a few ms of the click.",
"stat": "position_mean_ms",
"position": "and",
"bar": 0,
"value": -29.6,
"severity": "high"
},
{
"finding": "When the click goes silent you push further ahead: silent bars average -21.4 ms against -10.8 ms in clicked bars.",
"stat": "silent_mean_ms",
"position": "",
"bar": 0,
"value": -21.4,
"severity": "high"
},
{
"finding": "Overall the take leans early - 21 notes early and none late - so only 64.1% of notes are in the pocket.",
"stat": "pocket_pct",
"position": "",
"bar": 0,
"value": 64.1,
"severity": "medium"
},
{
"finding": "The last silent bar is the furthest ahead of the take.",
"stat": "bar_mean_ms",
"position": "",
"bar": 8,
"value": -26,
"severity": "medium"
},
{
"finding": "Two notes were missed (an \"and\" in bar 2 and beat 2 in bar 7), and one extra tap crept in.",
"stat": "missed",
"position": "",
"bar": 0,
"value": 2,
"severity": "low"
}
],
"callouts": [
{
"bar": 4,
"text": "Click off: taps slide early, averaging -24.6 ms."
},
{
"bar": 7,
"text": "Click off again: early slide returns and beat 2 is missed."
},
{
"bar": 8,
"text": "Furthest ahead of the take: -26 ms."
}
],
"prescription": [
{
"phase": "warmup",
"title": "Settle the pulse",
"pattern": "pulse",
"meter": "4/4",
"tempo": 76,
"bars": 8,
"click": "full",
"minutes": 3,
"cue": "Let each beat land, breathe out on the click.",
"why": "Starts from what already works - your on-beats are close to the click."
},
{
"phase": "skill",
"title": "Wait for the &",
"pattern": "offbeats",
"meter": "4/4",
"tempo": 76,
"bars": 8,
"click": "full",
"minutes": 5,
"cue": "Count one-AND out loud and hit only on the AND, late in your mind.",
"why": "Isolates the rushed \"and\" (-29.6 ms) at a tempo where it can sit in the middle."
},
{
"phase": "skill",
"title": "Eighths, slow and even",
"pattern": "eighths",
"meter": "4/4",
"tempo": 82,
"bars": 8,
"click": "full",
"minutes": 4,
"cue": "Make the gap after the beat as long as the gap before it.",
"why": "Puts the \"and\" back between the beats so the eighths are even."
},
{
"phase": "performance",
"title": "Eighths with the click dropping out",
"pattern": "eighths",
"meter": "4/4",
"tempo": 96,
"bars": 16,
"click": "dropout",
"minutes": 5,
"cue": "When the click stops, keep counting it in your head - don't fill the space.",
"why": "Answers the early slide in silent bars (-21.4 ms) - the verse with no click to lean on."
},
{
"phase": "cooldown",
"title": "Easy pulse",
"pattern": "pulse",
"meter": "4/4",
"tempo": 76,
"bars": 8,
"click": "backbeat",
"minutes": 2,
"cue": "Lazy and heavy, sit back on every beat.",
"why": "Ends on a relaxed, laid-back feel to counter the overall rush."
}
],
"progress": [],
"next_step": "Re-take Straight eighths at 96 with the dropout click and see whether the \"and\" and the silent bars have moved toward zero."
}
diagnose: a clean take of sixteenths at 72
Input:
{
"task": "diagnose",
"pattern_id": "sixteenths",
"pattern_name": "Straight sixteenths",
"meter": "4/4",
"level": 3,
"tempo": 72,
"bars": 8,
"click_mode": "full",
"subdiv": false,
"audio": true,
"latency_ms": 0,
"stats": "(a JSON string - decoded below)",
"positions": "(a JSON string - decoded below)",
"bar_stats": "(a JSON string - decoded below)",
"taps": "(a JSON string - decoded below)",
"missed": "[]",
"clip_note": "measured 128 taps, sent 36 (the first 24 in order and the 12 furthest off the grid). Every aggregate covers every tap.",
"library": "(a JSON string - decoded below)",
"learner_note": ""
}
stats, decoded:
{
"expected": 128,
"hits": 128,
"missed": 0,
"extra": 0,
"hit_rate": 100,
"pocket_pct": 96.9,
"mean_ms": 6.9,
"spread_ms": 9.1,
"median_ms": 7.2,
"mean_abs_ms": 9.5,
"p90_abs_ms": 18,
"early": 0,
"late": 4,
"drift_ms_per_bar": -0.6,
"drift_r2": 0.02,
"felt_bpm": 72,
"first_half_ms": 8,
"second_half_ms": 5.9,
"clicked": {
"n": 128,
"mean_ms": 6.9,
"spread_ms": 9.1
},
"silent": {
"n": 0,
"mean_ms": null,
"spread_ms": null
},
"tendency": "centred"
}
positions, decoded:
[
{
"key": "beat1",
"label": "1",
"expected": 8,
"n": 8,
"mean_ms": 7.3,
"spread_ms": 12.5
},
{
"key": "e",
"label": "e",
"expected": 32,
"n": 32,
"mean_ms": 6.9,
"spread_ms": 8.9
},
{
"key": "and",
"label": "&",
"expected": 32,
"n": 32,
"mean_ms": 7.6,
"spread_ms": 9.8
},
{
"key": "a",
"label": "a",
"expected": 32,
"n": 32,
"mean_ms": 6.6,
"spread_ms": 8.9
},
{
"key": "beat2",
"label": "2",
"expected": 8,
"n": 8,
"mean_ms": 3.5,
"spread_ms": 9.5
},
{
"key": "beat3",
"label": "3",
"expected": 8,
"n": 8,
"mean_ms": 11.8,
"spread_ms": 5.7
},
{
"key": "beat4",
"label": "4",
"expected": 8,
"n": 8,
"mean_ms": 4.1,
"spread_ms": 6.8
}
]
bar_stats, decoded:
[
{
"bar": 1,
"clicked": true,
"expected": 16,
"n": 16,
"mean_ms": 8.7,
"spread_ms": 9.4
},
{
"bar": 2,
"clicked": true,
"expected": 16,
"n": 16,
"mean_ms": 7.7,
"spread_ms": 9
},
{
"bar": 3,
"clicked": true,
"expected": 16,
"n": 16,
"mean_ms": 7.1,
"spread_ms": 8.9
},
{
"bar": 4,
"clicked": true,
"expected": 16,
"n": 16,
"mean_ms": 8.7,
"spread_ms": 9.8
},
{
"bar": 5,
"clicked": true,
"expected": 16,
"n": 16,
"mean_ms": 5.8,
"spread_ms": 9.6
},
{
"bar": 6,
"clicked": true,
"expected": 16,
"n": 16,
"mean_ms": 10.1,
"spread_ms": 6.7
},
{
"bar": 7,
"clicked": true,
"expected": 16,
"n": 16,
"mean_ms": 4.3,
"spread_ms": 8.1
},
{
"bar": 8,
"clicked": true,
"expected": 16,
"n": 16,
"mean_ms": 3.3,
"spread_ms": 10.7
}
]
taps, decoded:
[
{
"n": 1,
"bar": 1,
"pos": "beat1",
"off": 25.9,
"why": "first"
},
{
"n": 2,
"bar": 1,
"pos": "e",
"off": 2.7,
"why": "first"
},
{
"n": 3,
"bar": 1,
"pos": "and",
"off": 9.3,
"why": "first"
},
{
"n": 4,
"bar": 1,
"pos": "a",
"off": 17.1,
"why": "first"
},
{
"n": 5,
"bar": 1,
"pos": "beat2",
"off": 12.1,
"why": "first"
},
{
"n": 6,
"bar": 1,
"pos": "e",
"off": 8.1,
"why": "first"
},
{
"n": 7,
"bar": 1,
"pos": "and",
"off": 28.1,
"why": "first"
},
{
"n": 8,
"bar": 1,
"pos": "a",
"off": 0.1,
"why": "first"
},
{
"n": 9,
"bar": 1,
"pos": "beat3",
"off": 13.3,
"why": "first"
},
{
"n": 10,
"bar": 1,
"pos": "e",
"off": -1,
"why": "first"
},
{
"n": 11,
"bar": 1,
"pos": "and",
"off": 6.4,
"why": "first"
},
{
"n": 12,
"bar": 1,
"pos": "a",
"off": 6.7,
"why": "first"
},
{
"n": 13,
"bar": 1,
"pos": "beat4",
"off": 9.8,
"why": "first"
},
{
"n": 14,
"bar": 1,
"pos": "e",
"off": 1.8,
"why": "first"
},
{
"n": 15,
"bar": 1,
"pos": "and",
"off": -8.1,
"why": "first"
},
{
"n": 16,
"bar": 1,
"pos": "a",
"off": 6.3,
"why": "first"
},
{
"n": 17,
"bar": 2,
"pos": "beat1",
"off": 10,
"why": "first"
},
{
"n": 18,
"bar": 2,
"pos": "e",
"off": 9.5,
"why": "first"
},
{
"n": 19,
"bar": 2,
"pos": "and",
"off": 7.7,
"why": "first"
},
{
"n": 20,
"bar": 2,
"pos": "a",
"off": -9.2,
"why": "first"
},
{
"n": 21,
"bar": 2,
"pos": "beat2",
"off": 10.5,
"why": "first"
},
{
"n": 22,
"bar": 2,
"pos": "e",
"off": 2.1,
"why": "first"
},
{
"n": 23,
"bar": 2,
"pos": "and",
"off": 18.6,
"why": "first"
},
{
"n": 24,
"bar": 2,
"pos": "a",
"off": -8,
"why": "first"
},
{
"n": 67,
"bar": 5,
"pos": "and",
"off": 27.5,
"why": "worst"
},
{
"n": 46,
"bar": 3,
"pos": "e",
"off": 26.5,
"why": "worst"
},
{
"n": 55,
"bar": 4,
"pos": "and",
"off": 23.5,
"why": "worst"
},
{
"n": 57,
"bar": 4,
"pos": "beat3",
"off": 23.4,
"why": "worst"
},
{
"n": 87,
"bar": 6,
"pos": "and",
"off": 21.8,
"why": "worst"
},
{
"n": 30,
"bar": 2,
"pos": "e",
"off": 20.8,
"why": "worst"
},
{
"n": 60,
"bar": 4,
"pos": "a",
"off": 20.6,
"why": "worst"
},
{
"n": 114,
"bar": 8,
"pos": "e",
"off": 20.5,
"why": "worst"
},
{
"n": 108,
"bar": 7,
"pos": "a",
"off": 20.1,
"why": "worst"
},
{
"n": 76,
"bar": 5,
"pos": "a",
"off": 18,
"why": "worst"
... (shortened here)
library, decoded:
[
{
"id": "pulse",
"name": "Quarter pulse",
"level": 1,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "eighths",
"name": "Straight eighths",
"level": 1,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "backbeat",
"name": "Backbeat 2 and 4",
"level": 1,
"meters": [
"4/4"
]
},
{
"id": "offbeats",
"name": "Off-beats (the &)",
"level": 2,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "triplets",
"name": "Eighth triplets",
"level": 2,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "swing",
"name": "Swung eighths",
"level": 2,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "charleston",
"name": "Charleston",
"level": 2,
"meters": [
"4/4"
]
},
{
"id": "sixteenths",
"name": "Straight sixteenths",
"level": 3,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "gallop",
"name": "Gallop (8th + two 16ths)",
"level": 3,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "reverse-gallop",
"name": "Reverse gallop (two 16ths + 8th)",
"level": 3,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "tresillo",
"name": "Tresillo 3-3-2",
"level": 3,
"meters": [
"4/4"
]
},
{
"id": "skip-triplet",
"name": "Triplet skip (trip-let)",
"level": 3,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "e-and-a",
"name": "Sixteenths without the beat",
"level": 4,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "the-a",
"name": "Only the 'a'",
"level": 4,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "hemiola",
"name": "Hemiola (two over three)",
"level": 4,
"meters": [
"3/4"
]
},
{
"id": "poly-3-2",
"name": "Three over two",
"level": 5,
"meters": [
"4/4"
]
},
{
"id": "jig-pulse",
"name": "6/8 pulse",
"level": 1,
"meters": [
"6/8"
]
},
{
"id": "jig-eighths",
"name": "6/8 eighths",
"level": 2,
"meters": [
"6/8"
]
},
{
"id": "jig-bell",
"name": "6/8 bell",
"level": 4,
"meters": [
"6/8"
]
}
]
Reply:
{
"task": "diagnose",
"headline": "This is a clean, steady take - every note hit and 96.9% in the pocket - so it is time to move on, not to fix anything.",
"findings": [
{
"finding": "Every one of the sixteenths was played, with nothing missed and no extra taps.",
"stat": "hit_rate",
"position": "",
"bar": 0,
"value": 100,
"severity": "low"
},
{
"finding": "Nearly all notes sit inside the pocket.",
"stat": "pocket_pct",
"position": "",
"bar": 0,
"value": 96.9,
"severity": "low"
},
{
"finding": "The spread is tight - your notes are consistent with each other.",
"stat": "spread_ms",
"position": "",
"bar": 0,
"value": 9.1,
"severity": "low"
},
{
"finding": "You sit a touch behind the click on average - a relaxed feel, not a problem.",
"stat": "mean_ms",
"position": "",
"bar": 0,
"value": 6.9,
"severity": "low"
},
{
"finding": "No drift: the tempo you kept matches the click.",
"stat": "felt_bpm",
"position": "",
"bar": 0,
"value": 72,
"severity": "low"
}
],
"callouts": [],
"prescription": [
{
"phase": "warmup",
"title": "Sixteenths, home tempo",
"pattern": "sixteenths",
"meter": "4/4",
"tempo": 66,
"bars": 8,
"click": "full",
"minutes": 3,
"cue": "Small, even strokes, same height every note.",
"why": "Warms up the pattern you already play cleanly."
},
{
"phase": "skill",
"title": "Gallop",
"pattern": "gallop",
"meter": "4/4",
"tempo": 66,
"bars": 8,
"click": "full",
"minutes": 4,
"cue": "Say one-and-a, and keep the gap where the e was.",
"why": "Progression: a level-3 pattern that removes notes from the grid you now own."
},
{
"phase": "skill",
"title": "Sixteenths without the beat",
"pattern": "e-and-a",
"meter": "4/4",
"tempo": 60,
"bars": 8,
"click": "full",
"minutes": 5,
"cue": "Feel the beat in your foot and play only e-and-a.",
"why": "Progression: a level-4 step that tests whether your even sixteenths hold without the downbeat."
},
{
"phase": "performance",
"title": "Sixteenths, click drops out",
"pattern": "sixteenths",
"meter": "4/4",
"tempo": 72,
"bars": 16,
"click": "dropout",
"minutes": 5,
"cue": "Keep the click alive in your head through the silent bars.",
"why": "This take had the full click throughout; the next test is holding the 72 BPM you kept without it."
}
],
"progress": [],
"next_step": "Once the dropout take stays this clean, raise the tempo a few BPM or take Sixteenths without the beat up to 72."
}
recheck: the eighths again after practising the prescribed set
Input:
{
"task": "recheck",
"pattern_id": "eighths",
"pattern_name": "Straight eighths",
"meter": "4/4",
"level": 1,
"tempo": 96,
"bars": 8,
"click_mode": "dropout",
"subdiv": false,
"audio": true,
"latency_ms": 0,
"stats": "(a JSON string - decoded below)",
"positions": "(a JSON string - decoded below)",
"bar_stats": "(a JSON string - decoded below)",
"taps": "(a JSON string - decoded below)",
"missed": "(a JSON string - decoded below)",
"clip_note": "measured 63 taps, sent 36 (the first 24 in order and the 12 furthest off the grid). Every aggregate covers every tap.",
"library": "(a JSON string - decoded below)",
"learner_note": "",
"previous": "(a JSON string - decoded below)"
}
stats, decoded:
{
"expected": 64,
"hits": 63,
"missed": 1,
"extra": 0,
"hit_rate": 98.4,
"pocket_pct": 95.3,
"mean_ms": -5.5,
"spread_ms": 11.2,
"median_ms": -6.7,
"mean_abs_ms": 10.2,
"p90_abs_ms": 18.5,
"early": 1,
"late": 1,
"drift_ms_per_bar": -0.1,
"drift_r2": 0,
"felt_bpm": 96,
"first_half_ms": -6.6,
"second_half_ms": -4.3,
"clicked": {
"n": 31,
"mean_ms": -3.4,
"spread_ms": 11.7
},
"silent": {
"n": 32,
"mean_ms": -7.5,
"spread_ms": 10.5
},
"tendency": "centred"
}
positions, decoded:
[
{
"key": "beat1",
"label": "1",
"expected": 8,
"n": 8,
"mean_ms": 5.9,
"spread_ms": 9.2
},
{
"key": "and",
"label": "&",
"expected": 32,
"n": 31,
"mean_ms": -12.3,
"spread_ms": 8.7
},
{
"key": "beat2",
"label": "2",
"expected": 8,
"n": 8,
"mean_ms": 2.4,
"spread_ms": 13
},
{
"key": "beat3",
"label": "3",
"expected": 8,
"n": 8,
"mean_ms": -4.7,
"spread_ms": 5.4
},
{
"key": "beat4",
"label": "4",
"expected": 8,
"n": 8,
"mean_ms": 1.1,
"spread_ms": 6.6
}
]
bar_stats, decoded:
[
{
"bar": 1,
"clicked": true,
"expected": 8,
"n": 8,
"mean_ms": -2.2,
"spread_ms": 8
},
{
"bar": 2,
"clicked": true,
"expected": 8,
"n": 8,
"mean_ms": -8,
"spread_ms": 7.9
},
{
"bar": 3,
"clicked": false,
"expected": 8,
"n": 8,
"mean_ms": -10.5,
"spread_ms": 12.2
},
{
"bar": 4,
"clicked": false,
"expected": 8,
"n": 8,
"mean_ms": -5.6,
"spread_ms": 12.2
},
{
"bar": 5,
"clicked": true,
"expected": 8,
"n": 8,
"mean_ms": -1.9,
"spread_ms": 12.4
},
{
"bar": 6,
"clicked": true,
"expected": 8,
"n": 7,
"mean_ms": -1.1,
"spread_ms": 17.9
},
{
"bar": 7,
"clicked": false,
"expected": 8,
"n": 8,
"mean_ms": -7.9,
"spread_ms": 11.2
},
{
"bar": 8,
"clicked": false,
"expected": 8,
"n": 8,
"mean_ms": -5.9,
"spread_ms": 7.2
}
]
taps, decoded:
[
{
"n": 1,
"bar": 1,
"pos": "beat1",
"off": 12.2,
"why": "first"
},
{
"n": 2,
"bar": 1,
"pos": "and",
"off": -10.4,
"why": "first"
},
{
"n": 3,
"bar": 1,
"pos": "beat2",
"off": -9.2,
"why": "first"
},
{
"n": 4,
"bar": 1,
"pos": "and",
"off": 5.4,
"why": "first"
},
{
"n": 5,
"bar": 1,
"pos": "beat3",
"off": -4.6,
"why": "first"
},
{
"n": 6,
"bar": 1,
"pos": "and",
"off": -6.7,
"why": "first"
},
{
"n": 7,
"bar": 1,
"pos": "beat4",
"off": 2.5,
"why": "first"
},
{
"n": 8,
"bar": 1,
"pos": "and",
"off": -6.7,
"why": "first"
},
{
"n": 9,
"bar": 2,
"pos": "beat1",
"off": 3.3,
"why": "first"
},
{
"n": 10,
"bar": 2,
"pos": "and",
"off": -8.9,
"why": "first"
},
{
"n": 11,
"bar": 2,
"pos": "beat2",
"off": -13.6,
"why": "first"
},
{
"n": 12,
"bar": 2,
"pos": "and",
"off": -17.3,
"why": "first"
},
{
"n": 13,
"bar": 2,
"pos": "beat3",
"off": 3.3,
"why": "first"
},
{
"n": 14,
"bar": 2,
"pos": "and",
"off": -15.2,
"why": "first"
},
{
"n": 15,
"bar": 2,
"pos": "beat4",
"off": -5.6,
"why": "first"
},
{
"n": 16,
"bar": 2,
"pos": "and",
"off": -10.2,
"why": "first"
},
{
"n": 17,
"bar": 3,
"pos": "beat1",
"off": -11.3,
"why": "first"
},
{
"n": 18,
"bar": 3,
"pos": "and",
"off": 1.5,
"why": "first"
},
{
"n": 19,
"bar": 3,
"pos": "beat2",
"off": 2.3,
"why": "first"
},
{
"n": 20,
"bar": 3,
"pos": "and",
"off": -24.5,
"why": "first"
},
{
"n": 21,
"bar": 3,
"pos": "beat3",
"off": -2.8,
"why": "first"
},
{
"n": 22,
"bar": 3,
"pos": "and",
"off": -10,
"why": "first"
},
{
"n": 23,
"bar": 3,
"pos": "beat4",
"off": -6.8,
"why": "first"
},
{
"n": 24,
"bar": 3,
"pos": "and",
"off": -32.1,
"why": "first"
},
{
"n": 43,
"bar": 6,
"pos": "beat2",
"off": 26.2,
"why": "worst"
},
{
"n": 46,
"bar": 6,
"pos": "and",
"off": -24.5,
"why": "worst"
},
{
"n": 28,
"bar": 4,
"pos": "and",
"off": -19.9,
"why": "worst"
},
{
"n": 54,
"bar": 7,
"pos": "and",
"off": -19.7,
"why": "worst"
},
{
"n": 44,
"bar": 6,
"pos": "and",
"off": -18.5,
"why": "worst"
},
{
"n": 56,
"bar": 7,
"pos": "and",
"off": -18.2,
"why": "worst"
},
{
"n": 64,
"bar": 8,
"pos": "and",
"off": -18.1,
"why": "worst"
},
{
"n": 52,
"bar": 7,
"pos": "and",
"off": -17.8,
"why": "worst"
},
{
"n": 26,
"bar": 4,
"pos": "and",
"off": -17.7,
"why": "worst"
}
... (shortened here)
missed, decoded:
[
{
"n": 42,
"bar": 6,
"pos": "and"
}
]
library, decoded:
[
{
"id": "pulse",
"name": "Quarter pulse",
"level": 1,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "eighths",
"name": "Straight eighths",
"level": 1,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "backbeat",
"name": "Backbeat 2 and 4",
"level": 1,
"meters": [
"4/4"
]
},
{
"id": "offbeats",
"name": "Off-beats (the &)",
"level": 2,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "triplets",
"name": "Eighth triplets",
"level": 2,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "swing",
"name": "Swung eighths",
"level": 2,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "charleston",
"name": "Charleston",
"level": 2,
"meters": [
"4/4"
]
},
{
"id": "sixteenths",
"name": "Straight sixteenths",
"level": 3,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "gallop",
"name": "Gallop (8th + two 16ths)",
"level": 3,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "reverse-gallop",
"name": "Reverse gallop (two 16ths + 8th)",
"level": 3,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "tresillo",
"name": "Tresillo 3-3-2",
"level": 3,
"meters": [
"4/4"
]
},
{
"id": "skip-triplet",
"name": "Triplet skip (trip-let)",
"level": 3,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "e-and-a",
"name": "Sixteenths without the beat",
"level": 4,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "the-a",
"name": "Only the 'a'",
"level": 4,
"meters": [
"3/4",
"4/4",
"5/4"
]
},
{
"id": "hemiola",
"name": "Hemiola (two over three)",
"level": 4,
"meters": [
"3/4"
]
},
{
"id": "poly-3-2",
"name": "Three over two",
"level": 5,
"meters": [
"4/4"
]
},
{
"id": "jig-pulse",
"name": "6/8 pulse",
"level": 1,
"meters": [
"6/8"
]
},
{
"id": "jig-eighths",
"name": "6/8 eighths",
"level": 2,
"meters": [
"6/8"
]
},
{
"id": "jig-bell",
"name": "6/8 bell",
"level": 4,
"meters": [
"6/8"
]
}
]
previous, decoded:
{
"headline": "Your beats sit right on the click, but every \"and\" comes early, and when the click drops out the whole groove slides ahead - that is the speeding-up your singer hears.",
"findings": [
{
"finding": "The \"and\" between the beats is rushed: it lands about 30 ms early on average, while beats 1-4 sit within a few ms of the click.",
"stat": "position_mean_ms",
"position": "and",
"bar": 0,
"value": -29.6
},
{
"finding": "When the click goes silent you push further ahead: silent bars average -21.4 ms against -10.8 ms in clicked bars.",
"stat": "silent_mean_ms",
"position": "",
"bar": 0,
"value": -21.4
},
{
"finding": "Overall the take leans early - 21 notes early and none late - so only 64.1% of notes are in the pocket.",
"stat": "pocket_pct",
"position": "",
"bar": 0,
"value": 64.1
},
{
"finding": "The last silent bar is the furthest ahead of the take.",
"stat": "bar_mean_ms",
"position": "",
"bar": 8,
"value": -26
},
{
"finding": "Two notes were missed (an \"and\" in bar 2 and beat 2 in bar 7), and one extra tap crept in.",
"stat": "missed",
"position": "",
"bar": 0,
"value": 2
}
],
"prescription": [
{
"id": "rx1",
"title": "Settle the pulse",
"pattern": "pulse",
"meter": "4/4",
"tempo": 76,
"click": "full"
},
{
"id": "rx2",
"title": "Wait for the &",
"pattern": "offbeats",
"meter": "4/4",
"tempo": 76,
"click": "full"
},
{
"id": "rx3",
"title": "Eighths, slow and even",
"pattern": "eighths",
"meter": "4/4",
"tempo": 82,
"click": "full"
},
{
"id": "rx4",
"title": "Eighths with the click dropping out",
"pattern": "eighths",
"meter": "4/4",
"tempo": 96,
"click": "dropout"
},
{
"id": "rx5",
"title": "Easy pulse",
"pattern": "pulse",
"meter": "4/4",
"tempo": 76,
"click": "backbeat"
}
],
"before": {
"pattern_id": "eighths",
"meter": "4/4",
"tempo": 96,
"stats": {
"expected": 64,
"hits": 62,
"missed": 2,
"extra": 1,
"hit_rate": 96.9,
"pocket_pct": 64.1,
"mean_ms": -16.1,
"spread_ms": 17.3,
"median_ms": -14,
"mean_abs_ms": 18.8,
"p90_abs_ms": 39.7,
"early": 21,
"late": 0,
"drift_ms_per_bar": -1.3,
"drift_r2": 0.03,
"felt_bpm": 96.1,
"first_half_ms": -15.7,
"second_half_ms": -16.5,
"clicked": {
"n": 31,
"mean_ms": -10.8,
"spread_ms": 16.7
},
"silent": {
"n": 31,
"mean_ms": -21.4,
"spread_ms": 16.6
},
"tendency": "rushing"
},
"positions": [
... (shortened here)
Reply:
{
"task": "recheck",
"headline": "Big step forward - you practised all five exercises, the silent bars no longer run away and 95.3% of notes are now in the pocket; the \"and\" is still a little early.",
"findings": [
{
"finding": "The \"and\" still lands ahead of the beats, though far less than before.",
"stat": "position_mean_ms",
"position": "and",
"bar": 0,
"value": -12.3,
"severity": "medium"
},
{
"finding": "The silent bars now stay close to the clicked ones - the slide when the click drops out is mostly gone.",
"stat": "silent_mean_ms",
"position": "",
"bar": 0,
"value": -7.5,
"severity": "low"
},
{
"finding": "Almost every note is in the pocket now.",
"stat": "pocket_pct",
"position": "",
"bar": 0,
"value": 95.3,
"severity": "low"
},
{
"finding": "One note was missed - an \"and\" in bar 6 - and no extra taps.",
"stat": "missed",
"position": "",
"bar": 0,
"value": 1,
"severity": "low"
}
],
"callouts": [
{
"bar": 6,
"text": "One \"and\" missed here."
}
],
"prescription": [
{
"phase": "warmup",
"title": "Eighths, easy",
"pattern": "eighths",
"meter": "4/4",
"tempo": 82,
"bars": 8,
"click": "full",
"minutes": 3,
"cue": "Make the gap after the beat as long as the gap before it.",
"why": "Re-settles the even eighths that improved most."
},
{
"phase": "skill",
"title": "Wait for the & again",
"pattern": "offbeats",
"meter": "4/4",
"tempo": 80,
"bars": 8,
"click": "full",
"minutes": 5,
"cue": "Count one-AND out loud and let the AND arrive, don't reach for it.",
"why": "The \"and\" is still early (-12.3 ms); this drill had the lowest best pocket of the set."
},
{
"phase": "performance",
"title": "Eighths, backbeat click only",
"pattern": "eighths",
"meter": "4/4",
"tempo": 96,
"bars": 16,
"click": "backbeat",
"minutes": 5,
"cue": "Feel 1 and 3 yourself; the click only answers on 2 and 4.",
"why": "Keeps the silent-bar gain while giving you less click to lean on, like a real verse."
}
],
"progress": [
{
"stat": "position_mean_ms",
"position": "and",
"before": -29.6,
"after": -12.3,
"verdict": "better"
},
{
"stat": "silent_mean_ms",
"position": "",
"before": -21.4,
"after": -7.5,
"verdict": "better"
},
{
"stat": "pocket_pct",
"position": "",
"before": 64.1,
"after": 95.3,
"verdict": "better"
},
{
"stat": "mean_ms",
"position": "",
"before": -16.1,
"after": -5.5,
"verdict": "better"
},
{
"stat": "spread_ms",
"position": "",
"before": 17.3,
"after": 11.2,
"verdict": "better"
},
{
"stat": "missed",
"position": "",
"before": 2,
"after": 1,
"verdict": "better"
}
],
"next_step": "Re-take Straight eighths at 96 with the dropout click and check that the \"and\" moves closer to the beats."
}
The page re-checks every reply; over the API, so should you
The page's own code is plain scripts you can load in node after global.window = {}, in this order: patterns.js (the library, notation and click plan), engine.js (Engine.session turns tap times into the full profile; Engine.clipTaps is the clipping), coach.js (Coach.buildInput builds these exact run bodies; Coach.normalize and Coach.reconcile check a reply against the take: every cited number within tolerance, every exercise a real pattern in a real meter, every callout on a bar that exists).
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://beat-offset.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://beat-offset.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"]
// beat-offset.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://beat-offset.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://beat-offset.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
}
// BeatOffset.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 BeatOffset {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://beat-offset.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://beat-offset.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://beat-offset.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://beat-offset.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":"beat-offset"}' | 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": "beat-offset"})["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: "beat-offset" })).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": "beat-offset"}, 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", "beat-offset"), 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: "beat-offset" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "beat-offset"])["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 = "beat-offset" }))!["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 (not wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model reads gpt-5.6-terra and model_alias gpt-terra.
# input.json: the run input object - {"task": "diagnose", "pattern_id": "eighths", "meter": "4/4", "tempo": 96, "stats": "...", ...}
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": "diagnose", "pattern_id": "eighths", "meter": "4/4", "tempo": 96, "stats": "...", ...}
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, pattern_id, meter, tempo, stats, ...}
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 task and the input: a retry with the same key never bills twice. The page uses beat-offset:<task>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="beat-offset:diagnose:$(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 = "beat-offset:" + 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 lane's JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("headline"))
import { createHash } from "node:crypto";
const key = `beat-offset:${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.headline);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "beat-offset:" + 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 = "beat-offset:" + 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 = "beat-offset:#{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["task"]].inspect
$key = "beat-offset:" . $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["task"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"beat-offset:{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: beat-offset:diagnose:$(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": "diagnose", "pattern_id": "eighths", "meter": "4/4", "tempo": 96, "stats": "...", ...}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "beat-offset:diagnose:" + 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": `beat-offset:diagnose:${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", "beat-offset:diagnose:"+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", "beat-offset:diagnose:" + 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" => "beat-offset:diagnose:#{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: beat-offset:diagnose:" . 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", $"beat-offset:diagnose:{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);