Drive Byes and Brackets from your own code
Everything the page does is available over HTTP. A natural use is a club website or a league spreadsheet that already holds the fixtures and results: a script rebuilds the event, asks for a round report after each match night, and posts it to the noticeboard.
One thing to be clear about first: the model never computes the draw or the table. The caller builds the bracket or fixtures, the schedule, the standings with their tiebreakers, the draw checks and the clinch statuses, and sends them as facts. The model returns an assessment with swaps, or a round report with scenarios and picks; the caller replays each swap against its own checker and simulates each scenario before showing it. The page's own engine ships as plain scripts you can load in node — see the end of the examples.
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":"byes-and-brackets"}), so no slug header is needed afterwards. 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.
Errors:
| 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: runs are metered and need a personal token. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
One system prompt serves three tasks. task picks the contract; anything else is read from the state (no results: review; results: round) and the reply says so in notes.
| task | When | Extra fields |
|---|---|---|
review | The draw before play: seeding, byes, rest gaps, groups, schedule. Proposes swaps. | None. |
rereview | After the organiser accepted or declined swaps, or changed the draw by hand. | previous and changes; the reply adds one previous entry per earlier swap. |
round | Results are in: the round report, callouts, qualification scenarios and picks. | None; status.clinch carries the browser's enumeration. |
Every run body is a JSON object of string fields (the declared input schema; task, title, format, settings, entrants, matches, standings, findings and status are required, and /estimate warns when one is missing). Ids are E1… for entrants (seed order) and M1… for matches.
| Field | What it holds |
|---|---|
task | "review", "rereview" or "round". |
title, kind, notes | The event's name, what kind of event it is, and the organiser's notes (cut in the middle past 1,500 characters). |
format | single, double, roundrobin, swiss or groups. |
settings | JSON string: {"format","courts":[...],"start","slot_minutes","rest_slots","points":{"w","d","l"},"tiebreaks":["points",...,"seed"],"advance"?,"swiss_rounds"?,"groups"?,"legs","bracket_reset"?,"score_unit"}. |
entrants | JSON string: [{"id":"E1","name","seed":1,"club"?,"from"?}], at most 64. |
matches | JSON string: [{"id":"M2","st":"W1","rd":"Round 1","a":"E2","b":"E9" or "W:M3" | "L:M3" | "A#1","slot","time","court","score"?:"3-1","state":"done|ready|waiting|tied"}]. Walkovers are not listed here. Past 140 matches only the latest played ones are sent and clipped says so. |
byes | JSON string: [{"match":"M1","st":"W1","to":"E1" or "A#1"}], the walkovers. |
standings | JSON string: [{"key","title","advance","complete","rows":[{"rank","id","P","W","D","L","F","A","diff","pts","buchholz"?,"sb"?,"tb"?}]}]. |
findings | JSON string: [{"key":"seed-half:E1|E2","rule","sev":"error|warn","msg"}], from the caller's draw checker. |
status | JSON string: {"played","remaining","champion","to_play":[...],"clinch":{"E3":{"status":"top|clinched|out|alive","best","worst"}}}. |
clipped | Optional JSON string: what was left out. |
changes | rereview only. JSON string: [{"type":"swap","a","b","via":"accepted swap 1 | by hand"}, {"type":"reseeded|settings|rescheduled|regenerated",...}]. |
previous | rereview only. JSON string: {"headline","swaps":[{"ref":1,"a","b","reason","you":"accepted|declined|open"}]}. |
The reply text is one JSON object. For review and rereview:
{
"task": "review | rereview", "title": "...", "headline": "...",
"assessment": [{ "area": "seeding|byes|rest|availability|balance|clubs|schedule|courts|format|fairness", "verdict": "good|fix|watch", "note", "entrants": ["E2"], "matches": ["M2"] }],
"swaps": [{ "a": "E2", "b": "E8", "reason", "addresses": ["seed-half:E1|E2"] }],
"schedule": [...], "notes": [...],
"previous": [{ "ref": 1, "status": "done|still-needed|dropped|superseded", "note" }]
}
For round:
{
"task": "round", "title": "...", "round": 3, "headline": "...", "report": ["paragraph", ...],
"callouts": [{ "match": "M7", "kind": "upset|thriller|rout|milestone|note", "text" }],
"scenarios": [{ "entrant": "E3", "outcome": "qualifies|eliminated|wins|advances", "if": [{ "match": "M14", "winner": "E3" or "draw" }], "text" }],
"predictions": [{ "match": "M15", "pick": "E2", "confidence": "low|medium|high", "upset": false, "why" }],
"notes": [...]
}
A swap exchanges two entrants' places everywhere in the draw. Replay it on a copy and re-run your checks before trusting it; the page marks one that adds a finding as "would break". A scenario is a sufficient condition: the page enumerates every remaining result consistent with if and marks it verified only when the outcome holds in all of them.
Worked examples
These are the bundled examples on the page: the exact run body the page builds and a reply produced against the shipped prompt (the page replays the same replies for free).
review: a club pickleball cup, 11 pairs, single elimination with byes and three hand-placement problems
Input:
{
"task": "review",
"title": "Riverside Pickleball Autumn Cup",
"kind": "Club doubles cup",
"notes": "Club doubles cup, games to 11, one game per match. Three courts from 09:00. The Hale and Brooks pair cannot be there before 10:00. Northgate sent two pairs and would rather not play each other first up.",
"format": "single",
"settings": "(a JSON string - decoded below)",
"entrants": "(a JSON string - decoded below)",
"matches": "(a JSON string - decoded below)",
"byes": "(a JSON string - decoded below)",
"standings": "(a JSON string - decoded below)",
"findings": "(a JSON string - decoded below)",
"status": "(a JSON string - decoded below)"
}
settings, decoded:
{
"format": "Single elimination",
"courts": [
"Court 1",
"Court 2",
"Court 3"
],
"start": "2026-10-17 09:00",
"slot_minutes": 25,
"rest_slots": 1,
"points": {
"w": 3,
"d": 1,
"l": 0
},
"tiebreaks": [
"points",
"h2h",
"diff",
"for",
"seed"
],
"legs": 1,
"score_unit": "points"
}
entrants, decoded:
[
{
"id": "E1",
"name": "Patel / Okafor",
"seed": 1
},
{
"id": "E2",
"name": "Moreau / Shah",
"seed": 2,
"club": "Northgate"
},
{
"id": "E3",
"name": "Lindqvist / Adeyemi",
"seed": 3
},
{
"id": "E4",
"name": "Kowalski / Reyes",
"seed": 4
},
{
"id": "E5",
"name": "Byrne / Tanaka",
"seed": 5
},
{
"id": "E6",
"name": "Haddad / Nilsen",
"seed": 6
},
{
"id": "E7",
"name": "Ferreira / Cole",
"seed": 7
},
{
"id": "E8",
"name": "Ito / Brennan",
"seed": 8
},
{
"id": "E9",
"name": "Novak / Achebe",
"seed": 9,
"club": "Northgate"
},
{
"id": "E10",
"name": "Duarte / Quinn",
"seed": 10
},
{
"id": "E11",
"name": "Hale / Brooks",
"seed": 11,
"from": "10:00"
}
]
matches, decoded:
[
{
"id": "M2",
"st": "W1",
"rd": "Round 1",
"a": "E2",
"b": "E9",
"slot": 0,
"time": "2026-10-17 09:00",
"court": "Court 1",
"state": "ready"
},
{
"id": "M6",
"st": "W1",
"rd": "Round 1",
"a": "E7",
"b": "E10",
"slot": 0,
"time": "2026-10-17 09:00",
"court": "Court 2",
"state": "ready"
},
{
"id": "M8",
"st": "W1",
"rd": "Round 1",
"a": "E6",
"b": "E11",
"slot": 3,
"time": "2026-10-17 10:15",
"court": "Court 1",
"state": "ready"
},
{
"id": "M9",
"st": "W2",
"rd": "Quarter-finals",
"a": "E1",
"b": "W:M2",
"slot": 2,
"time": "2026-10-17 09:50",
"court": "Court 1",
"state": "waiting"
},
{
"id": "M10",
"st": "W2",
"rd": "Quarter-finals",
"a": "E4",
"b": "E5",
"slot": 0,
"time": "2026-10-17 09:00",
"court": "Court 3",
"state": "ready"
},
{
"id": "M11",
"st": "W2",
"rd": "Quarter-finals",
"a": "E8",
"b": "W:M6",
"slot": 2,
"time": "2026-10-17 09:50",
"court": "Court 2",
"state": "waiting"
},
{
"id": "M12",
"st": "W2",
"rd": "Quarter-finals",
"a": "E3",
"b": "W:M8",
"slot": 5,
"time": "2026-10-17 11:05",
"court": "Court 1",
"state": "waiting"
},
{
"id": "M13",
"st": "W3",
"rd": "Semi-finals",
"a": "W:M9",
"b": "W:M10",
"slot": 4,
"time": "2026-10-17 10:40",
"court": "Court 1",
"state": "waiting"
},
{
"id": "M14",
"st": "W3",
"rd": "Semi-finals",
"a": "W:M11",
"b": "W:M12",
"slot": 7,
"time": "2026-10-17 11:55",
"court": "Court 1",
"state": "waiting"
},
{
"id": "M15",
"st": "W4",
"rd": "Final",
"a": "W:M13",
"b": "W:M14",
"slot": 9,
"time": "2026-10-17 12:45",
"court": "Court 1",
"state": "waiting"
}
]
byes, decoded:
[
{
"match": "M1",
"st": "W1",
"to": "E1"
},
{
"match": "M3",
"st": "W1",
"to": "E4"
},
{
"match": "M4",
"st": "W1",
"to": "E5"
},
{
"match": "M5",
"st": "W1",
"to": "E8"
},
{
"match": "M7",
"st": "W1",
"to": "E3"
}
]
standings, decoded:
[]
findings, decoded:
[
{
"key": "seed-half:E1|E2",
"rule": "seed-half",
"sev": "warn",
"msg": "Seeds 1 and 2 (Patel / Okafor, Moreau / Shah) are in the same half, so they can meet before the final."
},
{
"key": "bye:E8",
"rule": "bye",
"sev": "warn",
"msg": "Ito / Brennan (seed 8) has a first-round bye, but the 5 byes belong to seeds 1-5; Moreau / Shah (seed 2) has to play instead."
},
{
"key": "club:M2",
"rule": "club",
"sev": "warn",
"msg": "Moreau / Shah and Novak / Achebe are both from Northgate and meet in the first round (M2)."
}
]
status, decoded:
{
"played": 0,
"remaining": 10,
"champion": null,
"to_play": [
"M2",
"M6",
"M10",
"M8"
],
"clinch": {}
}
Reply:
{
"task": "review",
"title": "Autumn Cup draw review",
"headline": "Not ready yet: Moreau / Shah (E2) are in the wrong slot - they play a Northgate clubmate at 09:00 and could meet the top seeds in the quarter-final, while seed 8 has a bye. One swap, E2 with E8, fixes all three.",
"assessment": [
{
"area": "seeding",
"verdict": "fix",
"note": "Seed 2 Moreau / Shah (E2) sit in Patel / Okafor's (E1) half: if E2 wins M2 they meet E1 in quarter-final M9 at 09:50, so the two strongest pairs could meet in the quarter-finals, not the final. Swapping E2 with E8 moves E2 into the bottom half, so the semis would be E1 v E4/E5 (M13) and E2 v E3 (M14).",
"entrants": [
"E1",
"E2"
],
"matches": [
"M9",
"M13"
]
},
{
"area": "byes",
"verdict": "fix",
"note": "The five byes should go to seeds 1-5, but Ito / Brennan (E8, seed 8) have one (M5) and seed 2 E2 has to play M2. After the E2-E8 swap the byes sit on E1, E2, E3, E4 and E5, and E8 plays M2 at 09:00.",
"entrants": [
"E8",
"E2"
],
"matches": [
"M2",
"M5"
]
},
{
"area": "clubs",
"verdict": "fix",
"note": "M2 puts the two Northgate pairs, E2 and Novak / Achebe (E9), against each other first up, which the notes ask to avoid. After the swap E2 and E9 are in opposite halves and could only meet in the final (M15).",
"entrants": [
"E2",
"E9"
],
"matches": [
"M2",
"M15"
]
},
{
"area": "availability",
"verdict": "good",
"note": "Hale / Brooks (E11) cannot arrive before 10:00, and their only possible first match, M8 against E6, is at 10:15 - the first slot after they arrive. The swap leaves this match where it is.",
"entrants": [
"E11",
"E6"
],
"matches": [
"M8"
]
},
{
"area": "fairness",
"verdict": "watch",
"note": "After the swap, the likeliest upsets are between close seeds: M2 (E8 v E9, seeds 8 and 9) is close to a coin-flip, and quarter-final M10 (E4 v E5) is the closest match between two pairs with byes. In one game to 11, a single bad run can decide either. M6 (E7 v E10) and M8 (E6 v E11) favour the higher seed, but less so - E11 come straight in after arriving. The hardest path is E2's: E7/E10, then probably E3 in semi-final M14, then the final.",
"entrants": [
"E8",
"E9",
"E4",
"E5",
"E2"
],
"matches": [
"M2",
"M10",
"M6",
"M8",
"M14"
]
}
],
"swaps": [
{
"a": "E2",
"b": "E8",
"reason": "Seed 2 takes seed 8's bye and bottom-half slot, and seed 8 plays Novak / Achebe in M2. This keeps seeds 1 and 2 apart until the final, puts the byes on seeds 1-5, and separates the two Northgate pairs, without touching E11's 10:15 match.",
"addresses": [
"seed-half:E1|E2",
"bye:E8",
"club:M2"
]
},
{
"a": "E3",
"b": "E4",
"reason": "Optional: many draws place seeds 3 and 4 by lot rather than fixing their halves. Swapping them would give Lindqvist / Adeyemi (E3) a 09:00 quarter-final (M10) instead of waiting until 11:05 (M12), with E4 facing the E6/E11 winner later. The draw is fair without this swap.",
"addresses": []
}
],
"schedule": [
"Tell Hale / Brooks (E11) that M8 starts at 10:15, only 15 minutes after they can arrive. It cannot move earlier, because the 09:50 slot is before 10:00.",
"With the swap, Ito / Brennan (E8) now play at 09:00 in M2, and Moreau / Shah (E2) start in quarter-final M11 at 09:50 on Court 2. Let both pairs know before the morning.",
"No match can be played at 09:25 on any court, because every quarter-final needs a round-1 winner plus one rest slot. Use that time for warm-ups and for announcing the quarter-finals.",
"After 10:15, only Court 1 is used (M13, M12, M14, M15), and every one of those matches is already at its earliest legal slot. Courts 2 and 3 are free for practice or a plate event. The final, M15 at 12:45, gives the M14 winner exactly one rest slot."
],
"notes": [
"All three checker findings come from where E2 was placed in the draw, so the one swap clears them all."
]
}
review: a school sports day, groups then knockout, with a group of death
Input:
{
"task": "review",
"title": "Hillcrest Primary Sports Day: 5-a-side",
"kind": "School sports day",
"notes": "Year 5 and 6 forms, 5-a-side, 12-minute games on three pitches from 09:30. Lunch is at 12:15 and nothing should be scheduled after 12:00. Seeds from last year's finishing order.",
"format": "groups",
"settings": "(a JSON string - decoded below)",
"entrants": "(a JSON string - decoded below)",
"matches": "(a JSON string - decoded below)",
"byes": "(a JSON string - decoded below)",
"standings": "(a JSON string - decoded below)",
"findings": "(a JSON string - decoded below)",
"status": "(a JSON string - decoded below)"
}
settings, decoded:
{
"format": "Groups, then knockout",
"courts": [
"Pitch 1",
"Pitch 2",
"Pitch 3"
],
"start": "2026-07-10 09:30",
"slot_minutes": 15,
"rest_slots": 1,
"points": {
"w": 3,
"d": 1,
"l": 0
},
"tiebreaks": [
"points",
"h2h",
"diff",
"for",
"seed"
],
"advance": 2,
"groups": 3,
"legs": 1,
"score_unit": "goals"
}
entrants, decoded:
[
{
"id": "E1",
"name": "6 Oak",
"seed": 1
},
{
"id": "E2",
"name": "6 Ash",
"seed": 2
},
{
"id": "E3",
"name": "5 Rowan",
"seed": 3
},
{
"id": "E4",
"name": "6 Elm",
"seed": 4
},
{
"id": "E5",
"name": "5 Birch",
"seed": 5
},
{
"id": "E6",
"name": "5 Hazel",
"seed": 6
},
{
"id": "E7",
"name": "6 Pine",
"seed": 7
},
{
"id": "E8",
"name": "5 Willow",
"seed": 8
},
{
"id": "E9",
"name": "6 Cedar",
"seed": 9
},
{
"id": "E10",
"name": "5 Maple",
"seed": 10
},
{
"id": "E11",
"name": "6 Holly",
"seed": 11
},
{
"id": "E12",
"name": "5 Alder",
"seed": 12
}
]
matches, decoded:
[
{
"id": "M1",
"st": "A1",
"rd": "Group A · R1",
"a": "E1",
"b": "E12",
"slot": 0,
"time": "2026-07-10 09:30",
"court": "Pitch 1",
"state": "ready"
},
{
"id": "M2",
"st": "A1",
"rd": "Group A · R1",
"a": "E2",
"b": "E7",
"slot": 0,
"time": "2026-07-10 09:30",
"court": "Pitch 2",
"state": "ready"
},
{
"id": "M3",
"st": "A2",
"rd": "Group A · R2",
"a": "E7",
"b": "E1",
"slot": 2,
"time": "2026-07-10 10:00",
"court": "Pitch 1",
"state": "ready"
},
{
"id": "M4",
"st": "A2",
"rd": "Group A · R2",
"a": "E12",
"b": "E2",
"slot": 2,
"time": "2026-07-10 10:00",
"court": "Pitch 2",
"state": "ready"
},
{
"id": "M5",
"st": "A3",
"rd": "Group A · R3",
"a": "E1",
"b": "E2",
"slot": 4,
"time": "2026-07-10 10:30",
"court": "Pitch 1",
"state": "ready"
},
{
"id": "M6",
"st": "A3",
"rd": "Group A · R3",
"a": "E7",
"b": "E12",
"slot": 4,
"time": "2026-07-10 10:30",
"court": "Pitch 2",
"state": "ready"
},
{
"id": "M7",
"st": "B1",
"rd": "Group B · R1",
"a": "E6",
"b": "E11",
"slot": 0,
"time": "2026-07-10 09:30",
"court": "Pitch 3",
"state": "ready"
},
{
"id": "M8",
"st": "B1",
"rd": "Group B · R1",
"a": "E5",
"b": "E8",
"slot": 1,
"time": "2026-07-10 09:45",
"court": "Pitch 1",
"state": "ready"
},
{
"id": "M9",
"st": "B2",
"rd": "Group B · R2",
"a": "E8",
"b": "E6",
"slot": 3,
"time": "2026-07-10 10:15",
"court": "Pitch 1",
"state": "ready"
},
{
"id": "M10",
"st": "B2",
"rd": "Group B · R2",
"a": "E11",
"b": "E5",
"slot": 3,
"time": "2026-07-10 10:15",
"court": "Pitch 2",
"state": "ready"
},
{
"id": "M11",
"st": "B3",
"rd": "Group B · R3",
"a": "E6",
"b": "E5",
"slot": 5,
"time": "2026-07-10 10:45",
"court": "Pitch 1",
"state": "ready"
},
{
"id": "M12",
"st": "B3",
"rd": "Group B · R3",
"a": "E8",
"b": "E11",
"slot": 5,
"time": "2026-07-10 10:45",
"court": "Pitch 2",
"state": "ready"
},
{
"id": "M13",
"st": "C1",
"rd": "Group C · R1",
"a": "E3",
"b": "E10",
"slot": 1,
"time": "2026-07-10 09:45",
"court": "Pitch 2",
"state": "ready"
},
{
"id": "M14",
"st": "C1",
"rd": "Group C · R1",
"a": "E4",
"b": "E9",
"slot": 1,
"time": "2026-07-10 09:45",
"court": "Pitch 3",
"state": "ready"
},
{
"id": "M15",
"st": "C2",
"rd": "Group C · R2",
"a": "E9",
"b": "E3",
"slot": 3,
"time": "2026-07-10 10:
... (shortened here)
byes, decoded:
[
{
"match": "M19",
"st": "KO1",
"to": "A#1"
},
{
"match": "M21",
"st": "KO1",
"to": "B#1"
}
]
standings, decoded:
[
{
"key": "A",
"title": "Group A",
"advance": 2,
"complete": false,
"rows": [
{
"rank": 1,
"id": "E1",
"P": 0,
"W": 0,
"D": 0,
"L": 0,
"F": 0,
"A": 0,
"diff": 0,
"pts": 0
},
{
"rank": 2,
"id": "E2",
"P": 0,
"W": 0,
"D": 0,
"L": 0,
"F": 0,
"A": 0,
"diff": 0,
"pts": 0,
"tb": "seed"
},
{
"rank": 3,
"id": "E7",
"P": 0,
"W": 0,
"D": 0,
"L": 0,
"F": 0,
"A": 0,
"diff": 0,
"pts": 0,
"tb": "seed"
},
{
"rank": 4,
"id": "E12",
"P": 0,
"W": 0,
"D": 0,
"L": 0,
"F": 0,
"A": 0,
"diff": 0,
"pts": 0,
"tb": "seed"
}
]
},
{
"key": "B",
"title": "Group B",
"advance": 2,
"complete": false,
"rows": [
{
"rank": 1,
"id": "E5",
"P": 0,
"W": 0,
"D": 0,
"L": 0,
"F": 0,
"A": 0,
"diff": 0,
"pts": 0
},
{
"rank": 2,
"id": "E6",
"P": 0,
"W": 0,
"D": 0,
"L": 0,
"F": 0,
"A": 0,
"diff": 0,
"pts": 0,
"tb": "seed"
},
{
"rank": 3,
"id": "E8",
"P": 0,
"W": 0,
"D": 0,
"L": 0,
"F": 0,
"A": 0,
"diff": 0,
"pts": 0,
"tb": "seed"
},
{
"rank": 4,
"id": "E11",
"P": 0,
"W": 0,
"D": 0,
"L": 0,
"F": 0,
"A": 0,
"diff": 0,
"pts": 0,
"tb": "seed"
}
]
},
{
"key": "C",
"title": "Group C",
"advance": 2,
"complete": false,
"rows": [
{
"rank": 1,
"id": "E3",
"P": 0,
"W": 0,
"D": 0,
"L": 0,
"F": 0,
"A": 0,
"diff": 0,
"pts": 0
},
{
"rank": 2,
"id": "E4",
"P": 0,
"W": 0,
"D": 0,
"L": 0,
"F": 0,
"A": 0,
"diff": 0,
"pts": 0,
"tb": "seed"
},
{
"rank": 3,
"id": "E9",
"P": 0,
"W": 0,
"D": 0,
"L": 0,
"F": 0,
"A": 0,
"diff": 0,
"pts": 0,
"tb": "seed"
},
{
"rank": 4,
"id": "E10",
"P": 0,
"W": 0,
"D": 0,
"L": 0,
"F": 0,
"A": 0,
"diff": 0,
"pts": 0,
"tb": "seed"
}
]
}
]
findings, decoded:
[
{
"key": "death:A",
"rule": "death",
"sev": "warn",
"msg": "Group A has 2 top-pot seeds (6 Oak, 6 Ash): a group of death."
},
{
"key": "pot:B|2",
"rule": "pot",
"sev": "warn",
"msg": "Group B has 2 entrants from pot 2 (5 Hazel, 5 Birch)."
}
]
status, decoded:
{
"played": 0,
"remaining": 23,
"champion": null,
"to_play": [
"M1",
"M2",
"M7",
"M8",
"M13",
"M14",
"M3",
"M4",
"M9",
"M10",
"M15",
"M5",
"M6",
"M16",
"M11",
"M12",
"M17",
"M18"
],
"clinch": {}
}
Reply:
{
"task": "review",
"title": "Hillcrest 5-a-side draw review",
"headline": "The groups are uneven as drawn: Group A holds both 6 Oak (E1) and 6 Ash (E2), while Group B has two pot-2 sides. One swap, 6 Ash (E2) for 5 Hazel (E6), fixes both - but the bigger problem is the clock: the semi-finals start at 12:00 and the final (M25) at 12:30, past the 12:00 cut-off.",
"assessment": [
{
"area": "balance",
"verdict": "fix",
"note": "Group A is the group of death: seeds 1 and 2, 6 Oak (E1) and 6 Ash (E2), meet in M5 at 10:30 and one of last year's top two can finish no better than second, while Group B (E5, E6, E8, E11) has no top-pot side at all. Seed totals are 22 in A, 30 in B and 26 in C.",
"entrants": [
"E1",
"E2",
"E5",
"E6"
],
"matches": [
"M5"
]
},
{
"area": "seeding",
"verdict": "fix",
"note": "Group B has two pot-2 sides, 5 Birch (E5) and 5 Hazel (E6), who meet in M11 at 10:45. Swapping 6 Ash (E2) with 5 Hazel (E6) gives every group one side from each pot and an identical seed total of 26.",
"entrants": [
"E5",
"E6",
"E2"
],
"matches": [
"M11"
]
},
{
"area": "schedule",
"verdict": "fix",
"note": "The notes say nothing after 12:00 with lunch at 12:15, but the semi-finals M23 and M24 start at 12:00 (ending about 12:12) and the final M25 is at 12:30. The final cannot happen before lunch as scheduled.",
"matches": [
"M23",
"M24",
"M25"
]
},
{
"area": "rest",
"verdict": "good",
"note": "Every side gets at least one free slot between group games: Group A plays slots 0, 2 and 4, Group B and Group C stagger across slots 0-6, and the quarter-finals M20 and M22 at 11:30 leave a full slot after Group C's last games, M17 and M18, at 11:00.",
"matches": [
"M17",
"M18",
"M20",
"M22"
]
},
{
"area": "fairness",
"verdict": "watch",
"note": "Seeds come from last year's finishing order, so treat them as a soft guide; in 12-minute games upsets between close seeds are very possible. The likeliest are 6 Oak v 6 Ash (M5), 5 Birch v 5 Hazel (M11, or 6 Ash v 5 Birch after the swap) and 5 Rowan v 6 Elm (M17, seeds 3 v 4). Group C (E3, E4, E9, E10) is already balanced, one side from each pot.",
"entrants": [
"E3",
"E4"
],
"matches": [
"M5",
"M11",
"M17"
]
},
{
"area": "byes",
"verdict": "watch",
"note": "The two quarter-final byes, M19 and M21, go to the winners of Groups A and B (A#1, B#1), who go straight into the semi-finals M23 and M24. Group C's winner gets no bye and must also win M22 against B#2, so topping Group C (5 Rowan E3, 6 Elm E4, 6 Cedar E9, 5 Maple E10) is worth less than topping A or B. After the E2-E6 swap the three groups are equally strong, so which group ends up with the harder path comes down to the draw, not seeding.",
"entrants": [
"E3",
"E4"
],
"matches": [
"M19",
"M21",
"M22",
"M23",
"M24"
]
}
],
"swaps": [
{
"a": "E2",
"b": "E6",
"reason": "Moves 6 Ash (seed 2) into Group B and 5 Hazel (seed 6) into Group A, so 6 Oak and 6 Ash can only meet in the knockout and each group has one side per pot (seed totals 26/26/26). The two play in the same slots (0, 2/3, 4/5), so rest gaps do not change.",
"addresses": [
"death:A",
"pot:B|2"
]
}
],
"schedule": [
"The final M25 at 12:30 and the semi-finals M23 and M24 at 12:00 break the 12:00 cut-off. Even with every pitch full, the 18 group games need six 15-minute slots (09:30-10:59), and one rest slot before each knockout round puts the final at 12:15 at the earliest. Either play the final after lunch or agree to drop the rest slot in the knockout.",
"Pitch 3 is idle at 10:00 (slot of M3/M4), 10:45 (M11/M12) and 11:00 (M17/M18), and no match is scheduled at 11:15. Group C finishing at 11:00 (M17, M18) is what pushes the quarter-finals M20 and M22 to 11:30.",
"If the rest slot is waived for the knockout only, the quarter-finals M20 and M22 can start at 11:15 and the semis M23 and M24 at 11:30, leaving the final M25 at 11:45, before 12:00."
],
"notes": [
"Waiving the rest slot or moving the final af
... (shortened here; the page shows it whole)
rereview: the cup after one swap accepted, one declined and a pair moved by hand
Input (previous lists each earlier swap with what the organiser did, changes what moved since):
{
"task": "rereview",
"title": "Riverside Pickleball Autumn Cup",
"kind": "Club doubles cup",
"notes": "Club doubles cup, games to 11, one game per match. Three courts from 09:00. The Hale and Brooks pair cannot be there before 10:00. Northgate sent two pairs and would rather not play each other first up.",
"format": "single",
"settings": "(a JSON string - decoded below)",
"entrants": "(a JSON string - decoded below)",
"matches": "(a JSON string - decoded below)",
"byes": "(a JSON string - decoded below)",
"standings": "(a JSON string - decoded below)",
"findings": "(a JSON string - decoded below)",
"status": "(a JSON string - decoded below)",
"previous": "(a JSON string - decoded below)",
"changes": "(a JSON string - decoded below)"
}
settings, decoded:
{
"format": "Single elimination",
"courts": [
"Court 1",
"Court 2",
"Court 3"
],
"start": "2026-10-17 09:00",
"slot_minutes": 25,
"rest_slots": 1,
"points": {
"w": 3,
"d": 1,
"l": 0
},
"tiebreaks": [
"points",
"h2h",
"diff",
"for",
"seed"
],
"legs": 1,
"score_unit": "points"
}
entrants, decoded:
[
{
"id": "E1",
"name": "Patel / Okafor",
"seed": 1
},
{
"id": "E2",
"name": "Moreau / Shah",
"seed": 2,
"club": "Northgate"
},
{
"id": "E3",
"name": "Lindqvist / Adeyemi",
"seed": 3
},
{
"id": "E4",
"name": "Kowalski / Reyes",
"seed": 4
},
{
"id": "E5",
"name": "Byrne / Tanaka",
"seed": 5
},
{
"id": "E6",
"name": "Haddad / Nilsen",
"seed": 6
},
{
"id": "E7",
"name": "Ferreira / Cole",
"seed": 7
},
{
"id": "E8",
"name": "Ito / Brennan",
"seed": 8
},
{
"id": "E9",
"name": "Novak / Achebe",
"seed": 9,
"club": "Northgate"
},
{
"id": "E10",
"name": "Duarte / Quinn",
"seed": 10
},
{
"id": "E11",
"name": "Hale / Brooks",
"seed": 11,
"from": "10:00"
}
]
matches, decoded:
[
{
"id": "M2",
"st": "W1",
"rd": "Round 1",
"a": "E8",
"b": "E9",
"slot": 0,
"time": "2026-10-17 09:00",
"court": "Court 1",
"state": "ready"
},
{
"id": "M6",
"st": "W1",
"rd": "Round 1",
"a": "E7",
"b": "E11",
"slot": 0,
"time": "2026-10-17 09:00",
"court": "Court 2",
"state": "ready"
},
{
"id": "M8",
"st": "W1",
"rd": "Round 1",
"a": "E6",
"b": "E10",
"slot": 3,
"time": "2026-10-17 10:15",
"court": "Court 1",
"state": "ready"
},
{
"id": "M9",
"st": "W2",
"rd": "Quarter-finals",
"a": "E1",
"b": "W:M2",
"slot": 2,
"time": "2026-10-17 09:50",
"court": "Court 1",
"state": "waiting"
},
{
"id": "M10",
"st": "W2",
"rd": "Quarter-finals",
"a": "E4",
"b": "E5",
"slot": 0,
"time": "2026-10-17 09:00",
"court": "Court 3",
"state": "ready"
},
{
"id": "M11",
"st": "W2",
"rd": "Quarter-finals",
"a": "E2",
"b": "W:M6",
"slot": 2,
"time": "2026-10-17 09:50",
"court": "Court 2",
"state": "waiting"
},
{
"id": "M12",
"st": "W2",
"rd": "Quarter-finals",
"a": "E3",
"b": "W:M8",
"slot": 5,
"time": "2026-10-17 11:05",
"court": "Court 1",
"state": "waiting"
},
{
"id": "M13",
"st": "W3",
"rd": "Semi-finals",
"a": "W:M9",
"b": "W:M10",
"slot": 4,
"time": "2026-10-17 10:40",
"court": "Court 1",
"state": "waiting"
},
{
"id": "M14",
"st": "W3",
"rd": "Semi-finals",
"a": "W:M11",
"b": "W:M12",
"slot": 7,
"time": "2026-10-17 11:55",
"court": "Court 1",
"state": "waiting"
},
{
"id": "M15",
"st": "W4",
"rd": "Final",
"a": "W:M13",
"b": "W:M14",
"slot": 9,
"time": "2026-10-17 12:45",
"court": "Court 1",
"state": "waiting"
}
]
byes, decoded:
[
{
"match": "M1",
"st": "W1",
"to": "E1"
},
{
"match": "M3",
"st": "W1",
"to": "E4"
},
{
"match": "M4",
"st": "W1",
"to": "E5"
},
{
"match": "M5",
"st": "W1",
"to": "E2"
},
{
"match": "M7",
"st": "W1",
"to": "E3"
}
]
standings, decoded:
[]
findings, decoded:
[
{
"key": "avail:E11|M6",
"rule": "avail",
"sev": "error",
"msg": "Hale / Brooks is available from 10:00 but M6 starts at 09:00."
}
]
status, decoded:
{
"played": 0,
"remaining": 10,
"champion": null,
"to_play": [
"M2",
"M6",
"M10",
"M8"
],
"clinch": {}
}
changes, decoded:
[
{
"type": "swap",
"a": "E2",
"b": "E8",
"via": "accepted swap 1"
},
{
"type": "swap",
"a": "E10",
"b": "E11",
"via": "by hand"
}
]
previous, decoded:
{
"headline": "Not ready yet: Moreau / Shah (E2) are in the wrong slot - they play a Northgate clubmate at 09:00 and could meet the top seeds in the quarter-final, while seed 8 has a bye. One swap, E2 with E8, fixes all three.",
"swaps": [
{
"ref": 1,
"a": "E2",
"b": "E8",
"reason": "Seed 2 takes seed 8's bye and bottom-half slot, and seed 8 plays Novak / Achebe in M2. This keeps seeds 1 and 2 apart until the final, puts the byes on seeds 1…",
"you": "accepted"
},
{
"ref": 2,
"a": "E3",
"b": "E4",
"reason": "Optional: many draws place seeds 3 and 4 by lot rather than fixing their halves. Swapping them would give Lindqvist / Adeyemi (E3) a 09:00 quarter-final (M10) …",
"you": "declined"
}
]
}
Reply:
{
"task": "rereview",
"title": "Autumn Cup draw - second look",
"headline": "Nearly ready: the seed-2 swap worked, but the by-hand move of Hale / Brooks (E11) into the 09:00 match M6 puts them on court an hour before they can arrive. Swap E10 and E11 back and the draw is clean.",
"assessment": [
{
"area": "availability",
"verdict": "fix",
"note": "The organiser's by-hand swap of Duarte / Quinn (E10) and Hale / Brooks (E11) moved E11 into M6 against E7 at 09:00, but the notes say E11 cannot be there before 10:00 (finding avail:E11|M6). Their old slot, M8 at 10:15, suited them; E10 has no time limit, so undoing the move costs nothing.",
"entrants": [
"E11",
"E10"
],
"matches": [
"M6",
"M8"
]
},
{
"area": "seeding",
"verdict": "good",
"note": "After accepted swap 1, Patel / Okafor (E1) and Moreau / Shah (E2) sit in opposite halves (M9/M13 and M11/M14) and can only meet in the final M15. Seeds 3 and 4 are split across the halves too.",
"entrants": [
"E1",
"E2",
"E3",
"E4"
],
"matches": [
"M13",
"M14",
"M15"
]
},
{
"area": "byes",
"verdict": "good",
"note": "The five byes (M1, M3, M4, M5, M7) now go to the top five seeds E1, E4, E5, E2 and E3.",
"entrants": [
"E1",
"E2",
"E3",
"E4",
"E5"
],
"matches": []
},
{
"area": "clubs",
"verdict": "good",
"note": "The two Northgate pairs, E2 and Novak / Achebe (E9), are now in opposite halves, so they cannot meet before the final - better than the organiser's 'not first up' wish.",
"entrants": [
"E2",
"E9"
],
"matches": [
"M2",
"M11"
]
},
{
"area": "fairness",
"verdict": "watch",
"note": "Closest first-round match on seeds is M2, Ito / Brennan (E8) against Novak / Achebe (E9): a near coin-flip, so the safe call is only a slight lean to E8. M10, Kowalski / Reyes (E4) against Byrne / Tanaka (E5), is also close and is a quarter-final. With E10 and E11 back in place, M6 is seed 7 against 10 and M8 seed 6 against 11, the usual pairing.",
"entrants": [
"E8",
"E9",
"E4",
"E5"
],
"matches": [
"M2",
"M10"
]
}
],
"swaps": [
{
"a": "E10",
"b": "E11",
"reason": "Undo the by-hand move: Hale / Brooks go back to M8 at 10:15, after their 10:00 arrival, and Duarte / Quinn take the 09:00 match M6. Both keep at least one free slot before their quarter-finals (M11 at 09:50, M12 at 11:05).",
"addresses": [
"avail:E11|M6"
]
}
],
"schedule": [
"From 10:15 every match (M8, M13, M12, M14, M15) is on Court 1 while Courts 2 and 3 sit idle after M11 at 09:50; if Court 1 runs late, move the next match to Court 2 rather than pushing the times back.",
"E3 does not play until M12 at 11:05, then M14 at 11:55 and possibly the final M15 at 12:45 - tell Lindqvist / Adeyemi their first start time in advance.",
"The final M15 is at 12:45 on Court 1, one free slot after M14; keep M14 on time so the rest gap holds."
],
"notes": [
"If the by-hand move was made for a reason not in the notes (for example Duarte / Quinn arriving late), say so and a different fix can be found; as it stands, reverting it is the smallest change."
],
"previous": [
{
"ref": 1,
"status": "done",
"note": "Accepted: E2 now has seed 8's bye and bottom-half slot, and seeds 1 and 2 can only meet in the final."
},
{
"ref": 2,
"status": "dropped",
"note": "Declined, and that is fine: it was optional, there is no finding behind it, and seeds 3 and 4 are already in opposite halves."
}
]
}
round: a pub darts league three weeks in, four teams level on points
Input:
{
"task": "round",
"title": "Thursday Darts League, autumn",
"kind": "Pub darts league",
"notes": "Six pub teams, one 12-leg match each a week across three pubs. Two points for a win, one for a 6-6 draw. The top two meet in the final night at the Swan.",
"format": "roundrobin",
"settings": "(a JSON string - decoded below)",
"entrants": "(a JSON string - decoded below)",
"matches": "(a JSON string - decoded below)",
"byes": "(a JSON string - decoded below)",
"standings": "(a JSON string - decoded below)",
"findings": "(a JSON string - decoded below)",
"status": "(a JSON string - decoded below)"
}
settings, decoded:
{
"format": "Round robin",
"courts": [
"The Swan",
"Red Lion",
"Kings Arms"
],
"start": "2026-09-03 20:00",
"slot_minutes": 10080,
"rest_slots": 0,
"points": {
"w": 2,
"d": 1,
"l": 0
},
"tiebreaks": [
"points",
"diff",
"h2h",
"for",
"seed"
],
"advance": 2,
"legs": 1,
"score_unit": "legs"
}
entrants, decoded:
[
{
"id": "E1",
"name": "The Swan A",
"seed": 1
},
{
"id": "E2",
"name": "Red Lion Arrows",
"seed": 2
},
{
"id": "E3",
"name": "Kings Arms",
"seed": 3
},
{
"id": "E4",
"name": "The Swan B",
"seed": 4
},
{
"id": "E5",
"name": "Railway Tavern",
"seed": 5
},
{
"id": "E6",
"name": "The Plough",
"seed": 6
}
]
matches, decoded:
[
{
"id": "M1",
"st": "RR1",
"rd": "Round 1",
"a": "E1",
"b": "E6",
"slot": 0,
"time": "2026-09-03 20:00",
"court": "The Swan",
"score": "8-4",
"state": "done"
},
{
"id": "M2",
"st": "RR1",
"rd": "Round 1",
"a": "E2",
"b": "E5",
"slot": 0,
"time": "2026-09-03 20:00",
"court": "Red Lion",
"score": "5-7",
"state": "done"
},
{
"id": "M3",
"st": "RR1",
"rd": "Round 1",
"a": "E3",
"b": "E4",
"slot": 0,
"time": "2026-09-03 20:00",
"court": "Kings Arms",
"score": "6-6",
"state": "done"
},
{
"id": "M4",
"st": "RR2",
"rd": "Round 2",
"a": "E5",
"b": "E1",
"slot": 1,
"time": "2026-09-10 20:00",
"court": "The Swan",
"score": "7-5",
"state": "done"
},
{
"id": "M5",
"st": "RR2",
"rd": "Round 2",
"a": "E6",
"b": "E4",
"slot": 1,
"time": "2026-09-10 20:00",
"court": "Red Lion",
"score": "9-3",
"state": "done"
},
{
"id": "M6",
"st": "RR2",
"rd": "Round 2",
"a": "E2",
"b": "E3",
"slot": 1,
"time": "2026-09-10 20:00",
"court": "Kings Arms",
"score": "6-6",
"state": "done"
},
{
"id": "M7",
"st": "RR3",
"rd": "Round 3",
"a": "E1",
"b": "E4",
"slot": 2,
"time": "2026-09-17 20:00",
"court": "The Swan",
"score": "10-2",
"state": "done"
},
{
"id": "M8",
"st": "RR3",
"rd": "Round 3",
"a": "E5",
"b": "E3",
"slot": 2,
"time": "2026-09-17 20:00",
"court": "Red Lion",
"score": "4-8",
"state": "done"
},
{
"id": "M9",
"st": "RR3",
"rd": "Round 3",
"a": "E6",
"b": "E2",
"slot": 2,
"time": "2026-09-17 20:00",
"court": "Kings Arms",
"score": "7-5",
"state": "done"
},
{
"id": "M10",
"st": "RR4",
"rd": "Round 4",
"a": "E3",
"b": "E1",
"slot": 3,
"time": "2026-09-24 20:00",
"court": "The Swan",
"state": "ready"
},
{
"id": "M11",
"st": "RR4",
"rd": "Round 4",
"a": "E4",
"b": "E2",
"slot": 3,
"time": "2026-09-24 20:00",
"court": "Red Lion",
"state": "ready"
},
{
"id": "M12",
"st": "RR4",
"rd": "Round 4",
"a": "E5",
"b": "E6",
"slot": 3,
"time": "2026-09-24 20:00",
"court": "Kings Arms",
"state": "ready"
},
{
"id": "M13",
"st": "RR5",
"rd": "Round 5",
"a": "E1",
"b": "E2",
"slot": 4,
"time": "2026-10-01 20:00",
"court": "The Swan",
"state": "ready"
},
{
"id": "M14",
"st": "RR5",
"rd": "Round 5",
"a": "E3",
"b": "E6",
"slot": 4,
"time": "2026-10-01 20:00",
"court": "Red Lion",
"state": "ready"
},
{
"i
... (shortened here)
byes, decoded:
[]
standings, decoded:
[
{
"key": "RR",
"title": "League table",
"advance": 2,
"complete": false,
"rows": [
{
"rank": 1,
"id": "E1",
"P": 3,
"W": 2,
"D": 0,
"L": 1,
"F": 23,
"A": 13,
"diff": 10,
"pts": 4
},
{
"rank": 2,
"id": "E3",
"P": 3,
"W": 1,
"D": 2,
"L": 0,
"F": 20,
"A": 16,
"diff": 4,
"pts": 4,
"tb": "diff"
},
{
"rank": 3,
"id": "E6",
"P": 3,
"W": 2,
"D": 0,
"L": 1,
"F": 20,
"A": 16,
"diff": 4,
"pts": 4,
"tb": "seed"
},
{
"rank": 4,
"id": "E5",
"P": 3,
"W": 2,
"D": 0,
"L": 1,
"F": 18,
"A": 18,
"diff": 0,
"pts": 4,
"tb": "diff"
},
{
"rank": 5,
"id": "E2",
"P": 3,
"W": 0,
"D": 1,
"L": 2,
"F": 16,
"A": 20,
"diff": -4,
"pts": 1
},
{
"rank": 6,
"id": "E4",
"P": 3,
"W": 0,
"D": 1,
"L": 2,
"F": 11,
"A": 25,
"diff": -14,
"pts": 1,
"tb": "diff"
}
]
}
]
findings, decoded:
[]
status, decoded:
{
"played": 9,
"remaining": 6,
"champion": null,
"to_play": [
"M10",
"M11",
"M12",
"M13",
"M14",
"M15"
],
"clinch": {
"E1": {
"status": "alive",
"best": 1,
"worst": 6
},
"E3": {
"status": "alive",
"best": 1,
"worst": 6
},
"E6": {
"status": "alive",
"best": 1,
"worst": 6
},
"E5": {
"status": "alive",
"best": 1,
"worst": 6
},
"E2": {
"status": "alive",
"best": 2,
"worst": 6
},
"E4": {
"status": "alive",
"best": 2,
"worst": 6
}
}
}
Reply:
{
"task": "round",
"title": "Thursday Darts League - week 3 report",
"round": 3,
"headline": "Four teams locked on four points after three weeks, with The Swan A top on leg difference after a 10-2 win over The Swan B.",
"report": [
"Three weeks in, the Thursday Darts League could hardly be tighter. The Swan A, Kings Arms, The Plough and Railway Tavern all sit on four points, separated only by leg difference, while Red Lion Arrows and The Swan B have one point each from a draw apiece.",
"Week 3 belonged to the hosts at The Swan, where The Swan A beat The Swan B 10-2, the biggest margin of the season so far, to go top on a leg difference of +10. Kings Arms won 8-4 at Railway Tavern for their first win of the campaign and stay the only unbeaten side after two 6-6 draws, against The Swan B in week 1 and Red Lion Arrows in week 2. The Plough beat Red Lion Arrows 7-5, adding to their 9-3 win over The Swan B in week 2.",
"Railway Tavern have been the early giant-killers, beating Red Lion Arrows 7-5 on the opening night and then The Swan A 7-5 in week 2. Red Lion Arrows, second seeds, are still looking for a first win.",
"With two rounds left and the top two going through to the final night at the Swan, every match still matters. Week 4 sends Kings Arms to The Swan to face The Swan A, and week 5 brings The Swan A against Red Lion Arrows and Kings Arms against The Plough."
],
"callouts": [
{
"match": "M2",
"kind": "upset",
"text": "Fifth seeds Railway Tavern opened the season by beating second seeds Red Lion Arrows 7-5."
},
{
"match": "M4",
"kind": "upset",
"text": "Railway Tavern did it again in week 2, beating top seeds The Swan A 7-5, the only defeat The Swan A have taken so far."
},
{
"match": "M6",
"kind": "thriller",
"text": "Red Lion Arrows and Kings Arms shared the points at 6-6, the second draw in two weeks for Kings Arms."
},
{
"match": "M7",
"kind": "rout",
"text": "The Swan A beat The Swan B 10-2, the season's biggest win, which lifted them to the top on leg difference."
},
{
"match": "M9",
"kind": "upset",
"text": "Sixth seeds The Plough beat second seeds Red Lion Arrows 7-5, their second win, leaving the Arrows still without one."
}
],
"scenarios": [
{
"entrant": "E1",
"outcome": "qualifies",
"if": [
{
"match": "M10",
"winner": "E1"
},
{
"match": "M13",
"winner": "E1"
}
],
"text": "If The Swan A beat Kings Arms and Red Lion Arrows they finish on 8 points. Railway Tavern and The Plough play each other, so only one of them can also reach 8, and The Swan A are in the final."
},
{
"entrant": "E6",
"outcome": "qualifies",
"if": [
{
"match": "M12",
"winner": "E6"
},
{
"match": "M14",
"winner": "E6"
}
],
"text": "If The Plough beat Railway Tavern and Kings Arms they reach 8 points. Only The Swan A could then match that, so The Plough finish in the top two."
},
{
"entrant": "E3",
"outcome": "wins",
"if": [
{
"match": "M10",
"winner": "E3"
},
{
"match": "M14",
"winner": "E3"
},
{
"match": "M12",
"winner": "draw"
}
],
"text": "If Kings Arms win both of their games and Railway Tavern draw with The Plough, Kings Arms finish on 8 points and nobody else can get past 7, so Kings Arms top the table."
},
{
"entrant": "E4",
"outcome": "eliminated",
"if": [
{
"match": "M11",
"winner": "draw"
}
],
"text": "A draw with Red Lion Arrows leaves The Swan B on 2 points with at most 4 to come. The Swan A v Kings Arms and Railway Tavern v The Plough each send at least one side to 5 or more, so The Swan B cannot finish in the top two."
},
{
"entrant": "E2",
"outcome": "eliminated",
"if": [
{
"match": "M11",
"winner": "E4"
}
],
"text": "If Red Lion Arrows lose to The Swan B they can reach no more than 3 points, while four teams already have 4, so the Arrows are out."
}
],
"predictions": [
{
"match": "M10",
"pick": "E1",
"confidence": "low",
"upset": false,
"why": "The top seeds lead the table and
... (shortened here; the page shows it whole)
round: a chess club rapid Swiss before its last round
Input:
{
"task": "round",
"title": "Castle Chess Club Rapid Swiss",
"kind": "Chess club rapid",
"notes": "Club rapid night, 15+10, four rounds. One point for a win, a half for a draw; the odd player out gets a one-point bye. Prizes for the top three; tiebreaks Buchholz, then Sonneborn-Berger.",
"format": "swiss",
"settings": "(a JSON string - decoded below)",
"entrants": "(a JSON string - decoded below)",
"matches": "(a JSON string - decoded below)",
"byes": "(a JSON string - decoded below)",
"standings": "(a JSON string - decoded below)",
"findings": "(a JSON string - decoded below)",
"status": "(a JSON string - decoded below)"
}
settings, decoded:
{
"format": "Swiss",
"courts": [
"Board 1",
"Board 2",
"Board 3",
"Board 4"
],
"start": "2026-10-08 19:00",
"slot_minutes": 40,
"rest_slots": 0,
"points": {
"w": 1,
"d": 0.5,
"l": 0
},
"tiebreaks": [
"points",
"buchholz",
"sb",
"h2h",
"seed"
],
"advance": 3,
"swiss_rounds": 4,
"legs": 1,
"score_unit": "points"
}
entrants, decoded:
[
{
"id": "E1",
"name": "A. Okonkwo (1840)",
"seed": 1
},
{
"id": "E2",
"name": "M. Fischer (1795)",
"seed": 2
},
{
"id": "E3",
"name": "R. Iyer (1760)",
"seed": 3
},
{
"id": "E4",
"name": "S. Laurent (1712)",
"seed": 4
},
{
"id": "E5",
"name": "T. Byrne (1655)",
"seed": 5
},
{
"id": "E6",
"name": "J. Mensah (1610)",
"seed": 6
},
{
"id": "E7",
"name": "K. Sato (1580)",
"seed": 7
},
{
"id": "E8",
"name": "L. Varga (1525)",
"seed": 8
},
{
"id": "E9",
"name": "P. Doyle (1490)",
"seed": 9
}
]
matches, decoded:
[
{
"id": "M1",
"st": "SW1",
"rd": "Round 1",
"a": "E1",
"b": "E5",
"slot": 0,
"time": "2026-10-08 19:00",
"court": "Board 1",
"score": "1-0",
"state": "done"
},
{
"id": "M2",
"st": "SW1",
"rd": "Round 1",
"a": "E2",
"b": "E6",
"slot": 0,
"time": "2026-10-08 19:00",
"court": "Board 2",
"score": "1-0",
"state": "done"
},
{
"id": "M3",
"st": "SW1",
"rd": "Round 1",
"a": "E3",
"b": "E7",
"slot": 0,
"time": "2026-10-08 19:00",
"court": "Board 3",
"score": "0-1",
"state": "done"
},
{
"id": "M4",
"st": "SW1",
"rd": "Round 1",
"a": "E4",
"b": "E8",
"slot": 0,
"time": "2026-10-08 19:00",
"court": "Board 4",
"score": "1-0",
"state": "done"
},
{
"id": "M6",
"st": "SW2",
"rd": "Round 2",
"a": "E1",
"b": "E4",
"slot": 1,
"time": "2026-10-08 19:40",
"court": "Board 1",
"score": "0.5-0.5",
"state": "done"
},
{
"id": "M7",
"st": "SW2",
"rd": "Round 2",
"a": "E2",
"b": "E7",
"slot": 1,
"time": "2026-10-08 19:40",
"court": "Board 2",
"score": "1-0",
"state": "done"
},
{
"id": "M8",
"st": "SW2",
"rd": "Round 2",
"a": "E9",
"b": "E3",
"slot": 1,
"time": "2026-10-08 19:40",
"court": "Board 3",
"score": "0-1",
"state": "done"
},
{
"id": "M9",
"st": "SW2",
"rd": "Round 2",
"a": "E5",
"b": "E6",
"slot": 1,
"time": "2026-10-08 19:40",
"court": "Board 4",
"score": "1-0",
"state": "done"
},
{
"id": "M11",
"st": "SW3",
"rd": "Round 3",
"a": "E2",
"b": "E1",
"slot": 2,
"time": "2026-10-08 20:20",
"court": "Board 1",
"score": "0-1",
"state": "done"
},
{
"id": "M12",
"st": "SW3",
"rd": "Round 3",
"a": "E4",
"b": "E3",
"slot": 2,
"time": "2026-10-08 20:20",
"court": "Board 2",
"score": "0-1",
"state": "done"
},
{
"id": "M13",
"st": "SW3",
"rd": "Round 3",
"a": "E5",
"b": "E8",
"slot": 2,
"time": "2026-10-08 20:20",
"court": "Board 3",
"score": "1-0",
"state": "done"
},
{
"id": "M14",
"st": "SW3",
"rd": "Round 3",
"a": "E7",
"b": "E9",
"slot": 2,
"time": "2026-10-08 20:20",
"court": "Board 4",
"score": "1-0",
"state": "done"
},
{
"id": "M16",
"st": "SW4",
"rd": "Round 4",
"a": "E1",
"b": "E3",
"slot": 3,
"time": "2026-10-08 21:00",
"court": "Board 1",
"state": "ready"
},
{
"id": "M17",
"st": "SW4",
"rd": "Round 4",
"a": "E2",
"b": "E5",
"slot": 3,
"time": "2026-10-08 21:00",
"court": "Board 2
... (shortened here)
byes, decoded:
[
{
"match": "M5",
"st": "SW1",
"to": "E9"
},
{
"match": "M10",
"st": "SW2",
"to": "E8"
},
{
"match": "M15",
"st": "SW3",
"to": "E6"
},
{
"match": "M20",
"st": "SW4",
"to": "E4"
}
]
standings, decoded:
[
{
"key": "SW",
"title": "Swiss standings",
"advance": 3,
"complete": false,
"rows": [
{
"rank": 1,
"id": "E1",
"P": 3,
"W": 2,
"D": 1,
"L": 0,
"F": 2.5,
"A": 0.5,
"diff": 2,
"pts": 2.5,
"buchholz": 6.5,
"sb": 5.25
},
{
"rank": 2,
"id": "E4",
"P": 4,
"W": 2,
"D": 1,
"L": 1,
"F": 1.5,
"A": 1.5,
"diff": 0,
"pts": 2.5,
"buchholz": 5.5,
"sb": 2.25,
"tb": "buchholz"
},
{
"rank": 3,
"id": "E3",
"P": 3,
"W": 2,
"D": 0,
"L": 1,
"F": 2,
"A": 1,
"diff": 1,
"pts": 2,
"buchholz": 5.5,
"sb": 3.5
},
{
"rank": 4,
"id": "E2",
"P": 3,
"W": 2,
"D": 0,
"L": 1,
"F": 2,
"A": 1,
"diff": 1,
"pts": 2,
"buchholz": 5.5,
"sb": 3,
"tb": "sb"
},
{
"rank": 5,
"id": "E7",
"P": 3,
"W": 2,
"D": 0,
"L": 1,
"F": 2,
"A": 1,
"diff": 1,
"pts": 2,
"buchholz": 5,
"sb": 3,
"tb": "buchholz"
},
{
"rank": 6,
"id": "E5",
"P": 3,
"W": 2,
"D": 0,
"L": 1,
"F": 2,
"A": 1,
"diff": 1,
"pts": 2,
"buchholz": 4.5,
"sb": 2,
"tb": "buchholz"
},
{
"rank": 7,
"id": "E8",
"P": 3,
"W": 1,
"D": 0,
"L": 2,
"F": 0,
"A": 2,
"diff": -2,
"pts": 1,
"buchholz": 4.5,
"sb": 0
},
{
"rank": 8,
"id": "E6",
"P": 3,
"W": 1,
"D": 0,
"L": 2,
"F": 0,
"A": 2,
"diff": -2,
"pts": 1,
"buchholz": 4,
"sb": 0,
"tb": "buchholz"
},
{
"rank": 9,
"id": "E9",
"P": 3,
"W": 1,
"D": 0,
"L": 2,
"F": 0,
"A": 2,
"diff": -2,
"pts": 1,
"buchholz": 4,
"sb": 0,
"tb": "seed"
}
]
}
]
findings, decoded:
[]
status, decoded:
{
"played": 12,
"remaining": 4,
"champion": null,
"to_play": [
"M16",
"M17",
"M18",
"M19"
],
"clinch": {
"E1": {
"status": "alive",
"best": 1,
"worst": 6
},
"E4": {
"status": "alive",
"best": 2,
"worst": 6
},
"E3": {
"status": "alive",
"best": 1,
"worst": 8
},
"E2": {
"status": "alive",
"best": 1,
"worst": 8
},
"E7": {
"status": "alive",
"best": 1,
"worst": 8
},
"E5": {
"status": "alive",
"best": 1,
"worst": 8
},
"E8": {
"status": "out",
"best": 4,
"worst": 9
},
"E6": {
"status": "out",
"best": 4,
"worst": 9
},
"E9": {
"status": "out",
"best": 4,
"worst": 9
}
}
}
Reply:
{
"task": "round",
"title": "Castle Rapid Swiss: Round 3 report",
"round": 3,
"headline": "Okonkwo beats Fischer to lead on 2.5 going into the last round, where he meets Iyer on Board 1 with first place at stake.",
"report": [
"Round 3 decided the top boards. A. Okonkwo beat M. Fischer 1-0 on Board 1 to move to 2.5/3, unbeaten after his round-2 draw with S. Laurent. R. Iyer beat Laurent 1-0 on Board 2, his second win since losing to K. Sato in round 1. T. Byrne beat L. Varga 1-0 and Sato beat P. Doyle 1-0, so four players now sit on 2 points behind the leader.",
"Laurent is level with Okonkwo on 2.5, but his total includes the round-4 bye, so his tournament is over and he waits to see how many players pass him. He was the only player on 1.5 after round 3, and Doyle, Varga and J. Mensah had each already taken a bye in rounds 1 to 3, so the bye could not go lower down the table. Mensah, Varga and Doyle are on 1 point and can no longer reach the top three.",
"Round 4 pairs Okonkwo against Iyer on Board 1, Fischer against Byrne on Board 2, Sato against Mensah on Board 3 and Varga against Doyle on Board 4. A win for Okonkwo takes first prize outright. Any other result leaves first and the other prize places open, and players level on points will be split by Buchholz, then Sonneborn-Berger."
],
"callouts": [
{
"match": "M3",
"kind": "upset",
"text": "Seventh seed K. Sato beat third seed R. Iyer 1-0 in round 1, the only win by a lower seed so far."
},
{
"match": "M6",
"kind": "thriller",
"text": "Fourth seed S. Laurent held top seed A. Okonkwo to a 0.5-0.5 draw in round 2, the only half-point the leader has given up."
},
{
"match": "M11",
"kind": "milestone",
"text": "Okonkwo beat second seed M. Fischer 1-0 to stay unbeaten and take the lead alone on 2.5 games."
},
{
"match": "M12",
"kind": "note",
"text": "Iyer beat Laurent 1-0 to set up the Board 1 meeting with Okonkwo, two wins in a row after his round-1 loss."
}
],
"scenarios": [
{
"entrant": "E1",
"outcome": "wins",
"if": [
{
"match": "M16",
"winner": "E1"
}
],
"text": "If Okonkwo beats Iyer he finishes on 3.5. Nobody else can pass 3, so he wins first prize outright."
},
{
"entrant": "E3",
"outcome": "wins",
"if": [
{
"match": "M16",
"winner": "E3"
},
{
"match": "M17",
"winner": "draw"
},
{
"match": "M18",
"winner": "E6"
}
],
"text": "If Iyer beats Okonkwo, Fischer and Byrne draw, and Sato loses to Mensah, Iyer is alone on 3 and wins outright. A win alone guarantees him a prize, but if Sato or the Board 2 winner also reaches 3, Buchholz decides first place."
},
{
"entrant": "E2",
"outcome": "qualifies",
"if": [
{
"match": "M17",
"winner": "E2"
}
],
"text": "If Fischer beats Byrne he reaches 3, and at most two players can finish level with or above him, so he wins a prize. The same holds for Byrne if he wins."
},
{
"entrant": "E7",
"outcome": "qualifies",
"if": [
{
"match": "M18",
"winner": "E7"
}
],
"text": "If Sato beats Mensah he reaches 3 and is certain of a top-three place, whatever happens on Boards 1 and 2."
},
{
"entrant": "E4",
"outcome": "qualifies",
"if": [
{
"match": "M16",
"winner": "E1"
},
{
"match": "M17",
"winner": "E2"
},
{
"match": "M18",
"winner": "E6"
}
],
"text": "Laurent stays on 2.5. If Okonkwo and Fischer win and Sato loses, only two players pass him and he takes third. A drawn Board 2 or Board 3 would leave him level on 2.5, and then Buchholz would decide."
},
{
"entrant": "E5",
"outcome": "eliminated",
"if": [
{
"match": "M17",
"winner": "E2"
}
],
"text": "A loss leaves Byrne on 2, behind Fischer, Laurent and at least one of the Board 1 players, so he misses the prizes. The same applies to every 2-point player who loses."
}
],
"predictions": [
{
"match": "M16",
"pick": "E1",
"confidence": "medium",
"upset": false,
"why": "Okonkwo is the top seed by 80 points and unbeaten here,
... (shortened here; the page shows it whole)
round: a six-team double-elimination LAN bracket, part-way
Input:
{
"task": "round",
"title": "Saturday LAN: Rocket League 3v3",
"kind": "Esports LAN",
"notes": "Best of three, two stations, 30-minute slots. Grand final with a bracket reset. Station 2 has the slower PC, so the winners' final should be on Station 1.",
"format": "double",
"settings": "(a JSON string - decoded below)",
"entrants": "(a JSON string - decoded below)",
"matches": "(a JSON string - decoded below)",
"byes": "(a JSON string - decoded below)",
"standings": "(a JSON string - decoded below)",
"findings": "(a JSON string - decoded below)",
"status": "(a JSON string - decoded below)"
}
settings, decoded:
{
"format": "Double elimination",
"courts": [
"Station 1",
"Station 2"
],
"start": "2026-11-14 12:00",
"slot_minutes": 30,
"rest_slots": 1,
"points": {
"w": 3,
"d": 1,
"l": 0
},
"tiebreaks": [
"points",
"h2h",
"diff",
"for",
"seed"
],
"legs": 1,
"bracket_reset": true,
"score_unit": "games"
}
entrants, decoded:
[
{
"id": "E1",
"name": "Orbit",
"seed": 1
},
{
"id": "E2",
"name": "Night Shift",
"seed": 2
},
{
"id": "E3",
"name": "Blue Static",
"seed": 3
},
{
"id": "E4",
"name": "Kickoff Crew",
"seed": 4
},
{
"id": "E5",
"name": "Turbo Llamas",
"seed": 5
},
{
"id": "E6",
"name": "Paper Planes",
"seed": 6
}
]
matches, decoded:
[
{
"id": "M2",
"st": "W1",
"rd": "Winners' quarter-finals",
"a": "E4",
"b": "E5",
"slot": 0,
"time": "2026-11-14 12:00",
"court": "Station 1",
"score": "1-2",
"state": "done"
},
{
"id": "M4",
"st": "W1",
"rd": "Winners' quarter-finals",
"a": "E3",
"b": "E6",
"slot": 0,
"time": "2026-11-14 12:00",
"court": "Station 2",
"score": "2-0",
"state": "done"
},
{
"id": "M5",
"st": "W2",
"rd": "Winners' semi-finals",
"a": "E1",
"b": "E5",
"slot": 2,
"time": "2026-11-14 13:00",
"court": "Station 1",
"score": "2-0",
"state": "done"
},
{
"id": "M6",
"st": "W2",
"rd": "Winners' semi-finals",
"a": "E2",
"b": "E3",
"slot": 2,
"time": "2026-11-14 13:00",
"court": "Station 2",
"score": "1-2",
"state": "done"
},
{
"id": "M7",
"st": "W3",
"rd": "Winners' final",
"a": "E1",
"b": "E3",
"slot": 4,
"time": "2026-11-14 14:00",
"court": "Station 1",
"state": "ready"
},
{
"id": "M10",
"st": "L2",
"rd": "Losers' R2",
"a": "E4",
"b": "E2",
"slot": 4,
"time": "2026-11-14 14:00",
"court": "Station 2",
"score": "0-2",
"state": "done"
},
{
"id": "M11",
"st": "L2",
"rd": "Losers' R2",
"a": "E6",
"b": "E5",
"slot": 5,
"time": "2026-11-14 14:30",
"court": "Station 1",
"state": "ready"
},
{
"id": "M12",
"st": "L3",
"rd": "Losers' R3",
"a": "E2",
"b": "W:M11",
"slot": 7,
"time": "2026-11-14 15:30",
"court": "Station 1",
"state": "waiting"
},
{
"id": "M13",
"st": "L4",
"rd": "Losers' final",
"a": "W:M12",
"b": "L:M7",
"slot": 9,
"time": "2026-11-14 16:30",
"court": "Station 1",
"state": "waiting"
},
{
"id": "M14",
"st": "GF",
"rd": "Grand final",
"a": "W:M7",
"b": "W:M13",
"slot": 11,
"time": "2026-11-14 17:30",
"court": "Station 1",
"state": "waiting"
},
{
"id": "M15",
"st": "GF2",
"rd": "Grand final reset",
"a": "W:M7",
"b": "W:M13",
"slot": 13,
"time": "2026-11-14 18:30",
"court": "Station 1",
"state": "waiting"
}
]
byes, decoded:
[
{
"match": "M1",
"st": "W1",
"to": "E1"
},
{
"match": "M3",
"st": "W1",
"to": "E2"
},
{
"match": "M8",
"st": "L1",
"to": "E4"
},
{
"match": "M9",
"st": "L1",
"to": "E6"
}
]
standings, decoded:
[]
findings, decoded:
[]
status, decoded:
{
"played": 5,
"remaining": 6,
"champion": null,
"to_play": [
"M7",
"M11"
],
"clinch": {}
}
Reply:
{
"task": "round",
"title": "Saturday LAN: winners' semi-finals and losers' round 2",
"round": 2,
"headline": "Blue Static knock out second seed Night Shift 2-1 to set up a winners' final against Orbit, while Kickoff Crew are the first team out.",
"report": [
"The opening round gave us an early upset: fifth seed Turbo Llamas edged fourth seed Kickoff Crew 2-1 on Station 1, while third seed Blue Static swept Paper Planes 2-0 on Station 2. Top seeds Orbit and Night Shift came in on byes.",
"In the winners' semi-finals Orbit were clinical, beating Turbo Llamas 2-0. The tie of the day was next door: Blue Static took second seed Night Shift to three games and won it 2-1, sending Night Shift into the losers' bracket. Night Shift answered straight away, beating Kickoff Crew 2-0 in losers' round 2, and with two defeats Kickoff Crew are out of the event.",
"Where everyone sits: Orbit and Blue Static are the last two unbeaten teams and meet in the winners' final (M7). Night Shift, Turbo Llamas and Paper Planes each have one loss and are alive in the losers' bracket. Kickoff Crew are eliminated.",
"Next up: the winners' final at 14:00 on Station 1, then Paper Planes against Turbo Llamas at 14:30. In double elimination the winners' final loser is not finished - they drop to the losers' final - but the M11 loser takes a second defeat and goes home."
],
"callouts": [
{
"match": "M6",
"kind": "upset",
"text": "Third seed Blue Static beat second seed Night Shift 2-1, the closest and biggest result so far, and earned a spot in the winners' final."
},
{
"match": "M2",
"kind": "upset",
"text": "Fifth seed Turbo Llamas beat fourth seed Kickoff Crew 2-1 in the first match of the day - a seed gap of one, the classic upset spot."
},
{
"match": "M5",
"kind": "rout",
"text": "Top seed Orbit did not drop a game against Turbo Llamas, winning 2-0 to reach the winners' final."
},
{
"match": "M10",
"kind": "milestone",
"text": "Night Shift bounced back with a 2-0 win over Kickoff Crew; that second loss eliminates Kickoff Crew, the first team out."
}
],
"scenarios": [
{
"entrant": "E1",
"outcome": "advances",
"if": [
{
"match": "M7",
"winner": "E1"
}
],
"text": "If Orbit beat Blue Static in the winners' final, Orbit go to the grand final unbeaten. Losing would not knock them out - they would drop to the losers' final (M13)."
},
{
"entrant": "E3",
"outcome": "advances",
"if": [
{
"match": "M7",
"winner": "E3"
}
],
"text": "If Blue Static beat Orbit, Blue Static reach the grand final from the winners' side. A loss sends them to the losers' final with one life left."
},
{
"entrant": "E5",
"outcome": "advances",
"if": [
{
"match": "M11",
"winner": "E5"
}
],
"text": "If Turbo Llamas beat Paper Planes they stay alive and face Night Shift in losers' round 3 (M12). A loss is their second and ends their day."
},
{
"entrant": "E6",
"outcome": "advances",
"if": [
{
"match": "M11",
"winner": "E6"
}
],
"text": "If Paper Planes beat Turbo Llamas they play Night Shift in M12. Losing M11 would be their second defeat, which eliminates them."
}
],
"predictions": [
{
"match": "M7",
"pick": "E1",
"confidence": "medium",
"upset": false,
"why": "Orbit are the top seed and have not dropped a game, but Blue Static have won both their series and just beat the second seed, so this is far from settled."
},
{
"match": "M11",
"pick": "E5",
"confidence": "low",
"upset": false,
"why": "Turbo Llamas are the higher seed and won a series today, while Paper Planes were swept 2-0, but five versus six is a narrow gap in a best of three."
}
],
"notes": [
"The winners' final (M7) is on Station 1 as the organiser wanted, given Station 2 has the slower PC.",
"The M11 winner plays Night Shift in M12 at 15:30 on Station 1, with a free slot after M11 to rest.",
"Kickoff Crew are out after losses in M2 and M10."
]
}
The page computes, replays and simulates; the API does not
Over the API you get the reply as JSON. The draw, the tables and the checks are yours to compute, and the page's own engine is plain scripts you can load in node after global.window = {}, in this order: drawkit.js (DrawKit.normalizeEvent, DrawKit.generate(event) for any format, DrawKit.compute(event) for results, walkovers and tables), rules.js (DrawRules.check(event), DrawRules.clinch(event, computed), DrawRules.inputFor(event, {task}) for this exact run body, DrawRules.toIcs and toCsv) and review.js (Review.normalize(reply, task), Review.reconcile(result, event, computed, findings, statuses) to replay every swap and simulate every scenario, and Review.compact(result, statuses) for the next previous).
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://byes-and-brackets.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://byes-and-brackets.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"]
// byes-and-brackets.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://byes-and-brackets.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://byes-and-brackets.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
}
// ByesAndBrackets.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 ByesAndBrackets {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://byes-and-brackets.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://byes-and-brackets.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://byes-and-brackets.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://byes-and-brackets.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":"byes-and-brackets"}' | 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": "byes-and-brackets"})["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: "byes-and-brackets" })).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": "byes-and-brackets"}, 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", "byes-and-brackets"), 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: "byes-and-brackets" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "byes-and-brackets"])["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 = "byes-and-brackets" }))!["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": "round", "title": "...", "format": "...", "settings": "{...}", "entrants": "[...]", "matches": "[...]", ...}
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": "round", "title": "...", "format": "...", "settings": "{...}", "entrants": "[...]", "matches": "[...]", ...}
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, title, format, settings, entrants, matches, ...}
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 byes-and-brackets:<task>:<inputHash>:a<attempt>. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="byes-and-brackets:review:$(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 = "byes-and-brackets:" + 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 = `byes-and-brackets:${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 := "byes-and-brackets:" + 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 = "byes-and-brackets:" + 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 = "byes-and-brackets:#{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["headline"]].inspect
$key = "byes-and-brackets:" . $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["headline"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"byes-and-brackets:{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: byes-and-brackets:review:$(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": "round", "title": "...", "format": "...", "settings": "{...}", "entrants": "[...]", "matches": "[...]", ...}
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "byes-and-brackets:review:" + 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": `byes-and-brackets:review:${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", "byes-and-brackets:review:"+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", "byes-and-brackets:review:" + 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" => "byes-and-brackets:review:#{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: byes-and-brackets:review:" . 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", $"byes-and-brackets:review:{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);