Run Team Forge from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page reads the registrations, picks the size rule, the mechanic, the safety net and the change policy, checks the cut-off and suggests draft teams - all free, in teamkit.js. The run writes the decision set or the scripts; the page then holds the reply to its own picks (teamreply.js).
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"team-forge"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required key (task, event, roster, facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal sign-in. |
| 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
Two lanes - plan (approve before the rule goes into the brief) and script (the words for the day; needs a size rule). A missing or unknown task is answered as plan. Both lanes send the same four fields.
The input
| Field | Required | What |
|---|---|---|
task | yes | plan or script. If it is missing or neither, do plan and set "task": "plan". |
event | yes | the event, its theme and window, the size rule entered, any beginners track, prizes, minors, the people free during the window and the named owners, and the day's clock. |
roster | yes | the registrations: counts, first-timer share, the role field, projects to pitch, registered teams, teams with room, and the suggested draft groupings. |
facts | yes | the page's picks (size rule and wording, mechanic with duration, safety net with anchors and offer order, change policy with cut-off, project supply), the readiness gate, the signals to record and every finding tagged [fix], [check] or [info]. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
- This is the participant-to-participant problem, not participant-to-expert: a person who cannot find a team is in scope; a team that needs help with a technology goes to mentoring. Never set prize amounts; the rules text is printed by the brief.
- Fix the size rule before the mechanic. The strongest default is a stated range including solo - "Teams can be 1-4 people" - which answers the solo question in the same words that set the maximum; one organizer network observes hackers do best in teams of at most 4 (a practitioner observation, no study behind it). A cap alone leaves the solo question to the registration desk; no stated rule is deleted once a brief is published. A rule added after people agreed to hack together disqualifies teams that already exist.
- A composition rule ("at least half the team at their first hackathon") states its fraction before the event. Experience-keyed fractions review cheaply; fractions keyed to age, student status, gender or disability are carve-outs on a protected characteristic that need review before publication - route them, do not word them. Say in advance what happens to a team that drops below the fraction.
- Mechanics: the pitch-and-join round is the default - immediately after the welcome, one minute a pitch, the leader's name, the problem, the solution and the help needed, a named person enforcing the cut-off, and no one pitching an idea they will not work on (unless there are not enough ideas, and everyone knows it). No formal matchmaking is deleted once anyone registers alone. A facilitated mixer only for a mostly first-time cohort with too few projects and a second room, never without a stocked backlog. Pre-event matching only when most register alone and the form already collects a role - suggestions people may ignore, not assignments. A wall or board is the round's artifact, not a substitute.
- The unteamed participant: say "a project of one is okay too" at the opening; a stocked backlog of joinable projects (ready-to-go tasks for newcomers, a build environment up in under 20 minutes); a floor sweep by one named person whose offer runs in this order - a team known to be short-handed, then a stocked project, then solo work as a choice (leading with solo is the failure); a time-boxed escalation for mostly first-timers or minors, anchored to a clock the day already has (end of the pitch round, first meal) - never an invented number of minutes. The ask to a team is private, with a specific person and contribution named, never a room-wide appeal; it ends with the participant choosing, never placed. With minors, the sweep is a safeguarding interaction.
- Change policy: open until a published cut-off then frozen is the default once a safety net exists - a freeze before the sweep cancels the sweep; the cut-off lands before judging opens; open throughout with an attribution rule for multi-day windows; no stated policy is deleted once prizes are awarded against a submission.
- Organizers, volunteers, judges and sponsors do not compete. Only collect registration fields a named person will read. Every gate and signal here is self-set; no other event's figure is a target.
House rules
- Use only the input and the rules above. The page's picks, times and numbers are exact - copy them. Every number in a sentence must appear in the input.
- Label anything that is not one of the rules above, and is not in the input, as your own design.
- Never invent a person, a team, a time or a number of minutes; where someone must be named and is not, give a role. Refer to people by name or as they - never infer anyone's gender from a name.
- Never use backticks, never refer to this input's fields by their key names in a sentence, and never copy this prompt's narrative wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
plan
summary- one sentence, at most 35 words: the size rule, the mechanic and the first thing to settle.size-rule(range,rangeNoSolo,cap,compositionornone, the page's pick),wording(the page's wording, exactly),why(one sentence).composition-fraction(the page's percentage as a number, or null when there is no beginners track),note(one sentence: the kind of fraction and what happens to a team that drops below it, or "" when there is none).mechanic-pick(pitch,mixerornone, the page's pick),minutes(the page's duration as a number, or null when not set),placement(one sentence: when it runs against the welcome and the first line of code),why(one sentence),pre_event(one sentence on the draft suggestions, or "" when the page did not promote them).safety-pick(teamed,solo,backlog,sweeporescalation, the page's pick),owner(the named sweep owner from the input, followed for an escalation by "; escalation:" and the escalation owner; a role when none is named; "" forteamedorsolo),anchors(0 to 2 short strings: the day's clock points the page gave),offer(exactly 3 short strings in the page's order: a short-handed team, a stocked project, solo as a choice;[]forteamedorsolo).change-pick(freeze,cutofforopen, the page's pick),cutoff(the page's wall-clock time, or "" when none),why(one sentence).supply- one sentence: how many solo registrants the pitched projects and teams with room absorb, and the backlog the page wants - the page's figures.signals- 2 or 3 short strings, the page's signals to record, each with what it would change next edition (your design).caveats- at most 4 short strings: every[fix]finding first, then the most important[check]findings while there is room.[]when there are none of either.
{
"task": "plan",
"summary": "one sentence",
"size": { "rule": "range", "wording": "Teams can be 1-3 people", "why": "one sentence" },
"composition": { "fraction": null, "note": "" },
"mechanic": { "pick": "pitch", "minutes": 12, "placement": "one sentence", "why": "one sentence", "pre_event": "" },
"safety": { "pick": "sweep", "owner": "a name or a role", "anchors": [ "a clock point" ], "offer": [ "first", "second", "third" ] },
"change": { "pick": "cutoff", "cutoff": "HH:MM", "why": "one sentence" },
"supply": "one sentence",
"signals": [ "what to record and what it changes" ],
"caveats": [ "a caveat" ]
}
The page holds it to its own picks: the size rule and its exact wording, the fraction, the mechanic and its minutes, the safety net with the offer in its order (a team with room first, solo last), the change policy and its cut-off; no number may appear that the input does not hold.
script
rules- the team rules for the brief, plain sentences: the size rule exactly as the page worded it and its solo answer, the composition fraction if there is one, the change policy with its cut-off time, and that organizers, volunteers, judges and sponsors do not compete for prizes.opening- what the host says at the welcome, a few sentences: the size rule, that a project of one is okay too (as a choice) - only when the size rule allows solo entry; otherwise what happens if a team loses members - and what happens next (the mechanic).pitch- the instructions for the matchmaking mechanic: for a pitch round, what a pitch contains (name, problem, solution, help needed), the one-minute limit and who enforces it, and that nobody pitches an idea they will not work on; for a mixer, its format is your own design - say so; "" when there is no mechanic.sweep- what the sweeper says to someone still looking, with the offers in the page's order (a team with room, then a stocked project, then solo as a choice); "" when the page's safety net has no sweep.escalation- the private ask the escalation owner makes to a team, with [bracketed placeholders] for the specific person and their contribution (the script is a template), ending with the participant choosing; anchored to the page's clock point, never a number of minutes; "" when the page did not pick an escalation.note- one sentence, at most 30 words, for the organizers: what to settle before publishing.
{
"task": "script",
"rules": "Team rules. ...",
"opening": "Welcome ...",
"pitch": "...",
"sweep": "...",
"escalation": "",
"note": "one sentence"
}
The page requires the size rule as worded with its solo answer and the cut-off in the rules text, the one-minute pitch limit and the no-pitching-what-you-won't-build rule, the sweep offering a team with room before solo work, and a private escalation with no invented minutes.
Worked examples - every lane, every sample
These are the page's three free samples (fictional events, people and companies). Each input is exactly what the page sends; each reply is the saved reply the sample replays, shipped as the validating model wrote it; the last line is the page's own check. Every sample has both lanes.
Riverside Civic Hack Day (28 registrations) - plan
28 registered, 18 alone; Teams can be 1-4 people; Pitch-and-join round; A floor sweep plus a time-boxed escalation, with a stocked backlog; Open until a published cut-off, then frozen; 5 of 5 ready.
Input:
{
"task": "plan",
"event": "Event: Riverside Civic Hack Day; open theme; hacking window 8 hours; rooms 1\nSize rule entered: minimum 1, maximum 4; beginners track no\nPrizes against a submission yes; minors participate no; recurring edition yes; rules publish soon no\nPeople free during the window: 2; sweep owner Floor lead (Mina); escalation owner Event lead (Ravi)\nDay clock: opening 09:30, welcome 20 min, first meal 12:30, change cut-off 13:30, judging opens 16:30; stocked project ideas 2",
"roster": "Registrations: 28 - 18 alone, 10 in 3 teams; first hackathon 54%; role field collected; projects to pitch 5\nRegistered teams: Lamp Post (3); Open Ledger (5); Two Bikes (2)\nTeams with room under the cap: Lamp Post +1; Two Bikes +2\nSuggested draft groupings for the solo registrants (suggestions, not assignments):\nDraft A | Bus stop accessibility map | Ana Ruiz (developer), Gus Lind (project manager, first hackathon), Hana Ito (designer), Sana Iqbal (developer, first hackathon)\nDraft B | Rent burden dashboard | Ben Okoro (data scientist), Dara Mills (designer, first hackathon), Jon Abara (developer, first hackathon), Fen Holt (developer)\nDraft C | Food bank volunteer scheduler | Eli Park (domain expert), Ivy Lane (communicator, first hackathon), Noor Sami (developer, first hackathon), Lena Voss (developer)\nDraft D | Permit wait-time tracker | Rafa Sol (government staff), Kit Moreau (data scientist, first hackathon), Chen Wei (developer, first hackathon)\nDraft E | pick from the stocked backlog | Max Feld (advocate, first hackathon), Ola Brandt (designer, first hackathon), Pia Rask (developer, first hackathon)",
"facts": "Size rule: range - A stated range including solo; wording \"Teams can be 1-4 people\"\nMechanic: pitch - Pitch-and-join round; 10 min (1 min a pitch for 5 pitchers plus 5 min, self-set); runs straight after the welcome and ends about 10:00; plus pre-event draft suggestions\nSafety net: escalation - A floor sweep plus a time-boxed escalation, with a stocked backlog; time-box anchored to the end of the pitch round (10:00) or the first meal (12:30); the offer order: a team known to be short-handed, then a stocked project, then solo work as a choice\nChange policy: cutoff - Open until a published cut-off, then frozen; cut-off 13:30\nProject supply: about 15 solo registrants absorbed by pitched projects and teams with room; 3 left over; about 1 stocked ideas wanted, 2 stocked\nReadiness gate (5 of 5): The team-size rule with its solo answer written; The composition fraction, if a track needs one written; The named mechanic with its duration written; The named owner of the safety net written; The change cut-off as a wall-clock time written\nSignals to record: How many registered alone versus as a team (self-set); How many were still unteamed when the sweep ran, and what each did next (self-set); How long the pitch round took against its 10-minute budget (self-set)\nFindings (6):\n[fix] over_cap (size): Open Ledger registered 5 people against a cap of 4 - a rule added after people agreed to hack together disqualifies a team that already exists; settle it with them before publishing.\n[check] capacity (mechanic): The pitched projects and teams with room absorb about 15 of 18 solo registrants at the cap of 4 - 3 still need a stocked project or a team.\n[info] no_second_room (mechanic): Mostly first-timers and too few projects, but only one room - a mixer in the hacking room displaces the hacking, so the pitch round stays.\n[info] pre_event (mechanic): Most registrations are solo and the form collects a role - suggest draft groupings before the day (suggestions people may ignore, not assignments).\n[info] time_box (safety): Anchor the escalation's time-box to a clock the day already has - the end of the pitch round (10:00) or the first meal (12:30) - and never invent a number of minutes; record what it actually took.\n[info] gate (readiness): Readiness: 5 of 5 written before the brief is published."
}
Reply:
{
"task": "plan",
"summary": "Teams can be 1-4 people, formed in a pitch-and-join round right after the welcome, and before publishing organizers must settle Open Ledger's 5-person roster against the 4-person cap.",
"size": {
"rule": "range",
"wording": "Teams can be 1-4 people",
"why": "A stated range including solo sets the maximum and answers whether a solo participant may enter in the same words."
},
"composition": {
"fraction": null,
"note": ""
},
"mechanic": {
"pick": "pitch",
"minutes": 10,
"placement": "The pitch round starts immediately after the welcome ends and runs about 10 minutes, ending near 10:00, when teams begin writing their first code.",
"why": "With only one room, a mixer would displace the hacking itself, so the pitch round stays despite a mostly first-time cohort and too few projects.",
"pre_event": "Because most registrations are solo and the form collects a role, draft groupings are suggested before the day as options participants may ignore, not assignments."
},
"safety": {
"pick": "escalation",
"owner": "Event lead (Ravi)",
"anchors": [
"end of the pitch round (10:00)",
"first meal (12:30)"
],
"offer": [
"a team known to be short-handed",
"a stocked project",
"solo work as a choice"
]
},
"change": {
"pick": "cutoff",
"cutoff": "13:30",
"why": "Registration and team changes stay open until the published 13:30 cut-off, then freeze, landing well before judging opens at 16:30."
},
"supply": "The pitched projects and teams with room absorb about 15 of the 18 solo registrants, leaving 3 who need a stocked project, against 2 ideas stocked and about 1 more wanted.",
"signals": [
"How many registered alone versus as a team - shapes whether next edition leans more on pre-event draft suggestions or on-day matchmaking.",
"How many were still unteamed when the sweep ran, and what each did next - tells organizers whether the stocked backlog size and sweep order need to change.",
"How long the pitch round took against its 10-minute budget - decides whether next edition's pitch round needs more or less time."
],
"caveats": [
"Open Ledger registered 5 people against the 4-person cap; settle with them before publishing, since an existing team cannot be disqualified by a rule added after they agreed to hack together.",
"Pitched projects and teams with room absorb about 15 of 18 solo registrants at the cap of 4; 3 still need a stocked project or a team."
]
}
The page's check: kept - The plan's owner is "Event lead (Ravi)"; the input names Floor lead (Mina).
Riverside Civic Hack Day (28 registrations) - script
28 registered, 18 alone; Teams can be 1-4 people; Pitch-and-join round; A floor sweep plus a time-boxed escalation, with a stocked backlog; Open until a published cut-off, then frozen; 5 of 5 ready.
Input:
{
"task": "script",
"event": "Event: Riverside Civic Hack Day; open theme; hacking window 8 hours; rooms 1\nSize rule entered: minimum 1, maximum 4; beginners track no\nPrizes against a submission yes; minors participate no; recurring edition yes; rules publish soon no\nPeople free during the window: 2; sweep owner Floor lead (Mina); escalation owner Event lead (Ravi)\nDay clock: opening 09:30, welcome 20 min, first meal 12:30, change cut-off 13:30, judging opens 16:30; stocked project ideas 2",
"roster": "Registrations: 28 - 18 alone, 10 in 3 teams; first hackathon 54%; role field collected; projects to pitch 5\nRegistered teams: Lamp Post (3); Open Ledger (5); Two Bikes (2)\nTeams with room under the cap: Lamp Post +1; Two Bikes +2\nSuggested draft groupings for the solo registrants (suggestions, not assignments):\nDraft A | Bus stop accessibility map | Ana Ruiz (developer), Gus Lind (project manager, first hackathon), Hana Ito (designer), Sana Iqbal (developer, first hackathon)\nDraft B | Rent burden dashboard | Ben Okoro (data scientist), Dara Mills (designer, first hackathon), Jon Abara (developer, first hackathon), Fen Holt (developer)\nDraft C | Food bank volunteer scheduler | Eli Park (domain expert), Ivy Lane (communicator, first hackathon), Noor Sami (developer, first hackathon), Lena Voss (developer)\nDraft D | Permit wait-time tracker | Rafa Sol (government staff), Kit Moreau (data scientist, first hackathon), Chen Wei (developer, first hackathon)\nDraft E | pick from the stocked backlog | Max Feld (advocate, first hackathon), Ola Brandt (designer, first hackathon), Pia Rask (developer, first hackathon)",
"facts": "Size rule: range - A stated range including solo; wording \"Teams can be 1-4 people\"\nMechanic: pitch - Pitch-and-join round; 10 min (1 min a pitch for 5 pitchers plus 5 min, self-set); runs straight after the welcome and ends about 10:00; plus pre-event draft suggestions\nSafety net: escalation - A floor sweep plus a time-boxed escalation, with a stocked backlog; time-box anchored to the end of the pitch round (10:00) or the first meal (12:30); the offer order: a team known to be short-handed, then a stocked project, then solo work as a choice\nChange policy: cutoff - Open until a published cut-off, then frozen; cut-off 13:30\nProject supply: about 15 solo registrants absorbed by pitched projects and teams with room; 3 left over; about 1 stocked ideas wanted, 2 stocked\nReadiness gate (5 of 5): The team-size rule with its solo answer written; The composition fraction, if a track needs one written; The named mechanic with its duration written; The named owner of the safety net written; The change cut-off as a wall-clock time written\nSignals to record: How many registered alone versus as a team (self-set); How many were still unteamed when the sweep ran, and what each did next (self-set); How long the pitch round took against its 10-minute budget (self-set)\nFindings (6):\n[fix] over_cap (size): Open Ledger registered 5 people against a cap of 4 - a rule added after people agreed to hack together disqualifies a team that already exists; settle it with them before publishing.\n[check] capacity (mechanic): The pitched projects and teams with room absorb about 15 of 18 solo registrants at the cap of 4 - 3 still need a stocked project or a team.\n[info] no_second_room (mechanic): Mostly first-timers and too few projects, but only one room - a mixer in the hacking room displaces the hacking, so the pitch round stays.\n[info] pre_event (mechanic): Most registrations are solo and the form collects a role - suggest draft groupings before the day (suggestions people may ignore, not assignments).\n[info] time_box (safety): Anchor the escalation's time-box to a clock the day already has - the end of the pitch round (10:00) or the first meal (12:30) - and never invent a number of minutes; record what it actually took.\n[info] gate (readiness): Readiness: 5 of 5 written before the brief is published."
}
Reply:
{
"task": "script",
"rules": "Team rules: Teams can be 1-4 people - since that range starts at one, hacking as a team of one is allowed. Team changes stay open until 13:30, after which rosters are frozen. Organizers, volunteers, judges and sponsors do not compete for the prizes.",
"opening": "Welcome to Riverside Civic Hack Day. Teams can be 1-4 people, and a project of one is okay too - that is a choice you can make, not a fallback. Right after this welcome, we are running a pitch-and-join round so anyone without a team yet can find one.",
"pitch": "A pitch gives the leader's name, the problem, the solution and the help needed. Each pitch gets one minute, and a timekeeper enforces the cut-off. Nobody pitches an idea they will not personally work on, unless there simply are not enough ideas to go around, and everyone in the room knows that.",
"sweep": "If someone is still without a team, Mina first offers a seat on a team known to be short-handed - right now that is Lamp Post or Two Bikes - then offers a stocked project from the backlog, and only then offers working solo, as something the participant chooses for themself, never something they are placed into.",
"escalation": "If someone is still unteamed by the end of the pitch round around 10:00, or by the first meal at 12:30, Ravi has a private word with a team that has room - never a room-wide appeal - naming the participant and the specific contribution they would bring, such as a missing developer or designer. The team hears the ask, and the participant makes the final call on whether to join.",
"note": "Settle Open Ledger's 5-person roster against the 4-person cap with the team directly before publishing the brief."
}
The page's check: kept - nothing to flag
Northgate Campus Hack (beginners track) - plan
30 registered, 26 alone; Teams can be 1-4 people; Facilitated mixer; A floor sweep plus a time-boxed escalation, with a stocked backlog; Open until a published cut-off, then frozen; 4 of 5 ready.
Input:
{
"task": "plan",
"event": "Event: Northgate Campus Hack; themed with organizer problems; hacking window 24 hours; rooms 2\nSize rule entered: minimum 1, maximum 4; beginners track yes (50% of the team at their first hackathon, keyed to experience)\nPrizes against a submission yes; minors participate yes; recurring edition no; rules publish soon no\nPeople free during the window: 1; sweep owner not named; escalation owner not named\nDay clock: opening 10:00, welcome 15 min, first meal 13:00, change cut-off 10:30, judging opens not given; stocked project ideas 1",
"roster": "Registrations: 30 - 26 alone, 4 in 1 team; first hackathon 77%; role field collected; projects to pitch 1\nRegistered teams: Night Owls (4)\nSuggested draft groupings for the solo registrants (suggestions, not assignments):\nDraft A | Study room finder | Student 05 (communicator, first hackathon), Student 10 (designer, first hackathon), Student 06 (developer, first hackathon), Student 22 (developer, first hackathon)\nDraft B | pick from the stocked backlog | Student 13 (communicator, first hackathon), Student 18 (designer, first hackathon), Student 09 (developer, first hackathon), Student 25 (developer, first hackathon)\nDraft C | pick from the stocked backlog | Student 21 (communicator, first hackathon), Student 26 (designer, first hackathon), Student 14 (developer, first hackathon)\nDraft D | pick from the stocked backlog | Student 04 (data scientist), Student 15 (project manager, first hackathon), Student 03 (developer, first hackathon), Student 08 (developer)\nDraft E | pick from the stocked backlog | Student 12 (data scientist), Student 23 (project manager, first hackathon), Student 11 (developer, first hackathon), Student 16 (developer)\nDraft F | pick from the stocked backlog | Student 20 (data scientist), Student 02 (designer, first hackathon), Student 17 (developer, first hackathon), Student 24 (developer)\nDraft G | pick from the stocked backlog | Student 07 (project manager, first hackathon), Student 01 (developer, first hackathon), Student 19 (developer, first hackathon)",
"facts": "Size rule: composition - A range plus a track composition rule; wording \"Teams can be 1-4 people\"\nMechanic: mixer - Facilitated mixer; 45 min, as set by the organizer (no standard mixer format); runs straight after the welcome and ends about 11:00; plus pre-event draft suggestions\nSafety net: escalation - A floor sweep plus a time-boxed escalation, with a stocked backlog; time-box anchored to the end of the mixer (11:00) or the first meal (13:00); the offer order: a team known to be short-handed, then a stocked project, then solo work as a choice\nChange policy: cutoff - Open until a published cut-off, then frozen; cut-off 10:30\nProject supply: about 3 solo registrants absorbed by pitched projects and teams with room; 23 left over; about 6 stocked ideas wanted, 1 stocked; themed ratio wants 8\nReadiness gate (4 of 5): The team-size rule with its solo answer written; The composition fraction, if a track needs one written; The named mechanic with its duration written; The named owner of the safety net MISSING; The change cut-off as a wall-clock time written\nSignals to record: How many registered alone versus as a team (self-set); How many were still unteamed when the sweep ran, and what each did next (self-set); How many teams changed membership after the cut-off would have fallen (self-set)\nFindings (11):\n[info] composition_change (change): Say now what happens to a team that drops below the 50% fraction after someone leaves - deciding it in advance costs a clause, deciding it at judging costs a track.\n[info] mixer (mechanic): A mixer is promoted: mostly first-timers, too few projects to absorb the room, and a second space. It forms social groups that then need a project - never run it without a stocked backlog.\n[info] pre_event (mechanic): Most registrations are solo and the form collects a role - suggest draft groupings before the day (suggestions people may ignore, not assignments).\n[check] no_owner (safety): Name the one person who owns the floor sweep for the window - not \"an organizer\".\n[check] no_escalation_owner (safety): Name the escalation owner - someone who can decide, not only relay (asking a team to take a fifth person under a cap of four is a decision).\n[info] time_box (safety): Anchor the escalation's time-box to a clock the day already has - the end of the mixer (11:00) or the first meal (13:00) - and never invent a number of minutes; record what it actually took.\n[check] minors (safety): Minors participate: the sweep and the escalation are safeguarding interactions - decide who may hold what is observed, for how long, and what is never written down.\n[check] backlog (safety): Stock about 6 joinable project ideas (self-set: one per 4 unplaced solo registrants); 1 stocked. Joinable means ready-to-go tasks for newcomers and a build environment up in under 20 minutes.\n[check] themed_supply (supply): Themed event: about one subject-matter expert + workable project per 4 non-expert participants - 8 needed for 30 non-experts; 2 projects and 0 experts known.\n[fix] freeze_before_sweep (change): The cut-off 10:30 lands at or before the end of the mechanic (11:00) - a freeze before the safety net runs cancels the safety net.\n[check] gate (readiness): Readiness: 4 of 5 written before the brief is published - missing: The named owner of the safety net."
}
Reply:
{
"task": "plan",
"summary": "Teams can be 1-4 people with a 50% first-timer composition rule, formed in a 45-minute mixer after the welcome; settle first that the 10:30 cut-off falls before the mixer ends at 11:00.",
"size": {
"rule": "composition",
"wording": "Teams can be 1-4 people",
"why": "The 1-4 range states the solo answer and the cap together, and the beginners track layers a 50% first-timer composition rule on top."
},
"composition": {
"fraction": 50,
"note": "This is an experience-keyed fraction - at least the published share of the team at their first hackathon - and what happens to a team that drops below it after someone leaves still needs deciding before publishing."
},
"mechanic": {
"pick": "mixer",
"minutes": 45,
"placement": "The mixer runs straight after the welcome ends and continues until about 11:00, when hacking begins.",
"why": "A facilitated mixer fits here because 77% of registrants are first-time hackers, there is only 1 project to pitch, and the event's 2 rooms give it a second space, as long as the stocked backlog stays filled.",
"pre_event": "Draft groupings for the solo registrants were suggested before the day using the role field collected at registration, but they are suggestions participants may ignore, not assignments."
},
"safety": {
"pick": "escalation",
"owner": "the floor sweep owner and the escalation owner, neither named yet",
"anchors": [
"end of the mixer (11:00)",
"first meal (13:00)"
],
"offer": [
"a team known to be short-handed",
"a stocked project",
"solo work as a choice"
]
},
"change": {
"pick": "cutoff",
"cutoff": "10:30",
"why": "A published cut-off that freezes membership is the default once a safety net exists, and here that cut-off is set at 10:30."
},
"supply": "About 3 of the solo registrants are absorbed by the pitched project and the team with room, leaving 23 still needing the backlog, which the page wants stocked with about 6 joinable ideas against the 1 currently ready.",
"signals": [
"How many registered alone versus as a team - a bigger team-registration share next time would support moving to a pitch round instead of a mixer.",
"How many were still unteamed when the sweep ran, and what each did next - a high escalation rate would mean stocking a bigger backlog next edition.",
"How many teams changed membership after the cut-off would have fallen - frequent changes would argue for moving the cut-off later next time."
],
"caveats": [
"The 10:30 change cut-off falls at or before the mixer's end at 11:00, so a freeze before the safety net runs would cancel the safety net.",
"Name the one person who owns the floor sweep for the window, not a generic organizer.",
"Name the escalation owner, someone who can decide, since asking a team to take a fifth person under a cap of four is a decision.",
"Minors are participating, so the sweep and escalation are safeguarding interactions needing a decision on what is observed, held and never written down."
]
}
The page's check: kept - nothing to flag
Northgate Campus Hack (beginners track) - script
30 registered, 26 alone; Teams can be 1-4 people; Facilitated mixer; A floor sweep plus a time-boxed escalation, with a stocked backlog; Open until a published cut-off, then frozen; 4 of 5 ready.
Input:
{
"task": "script",
"event": "Event: Northgate Campus Hack; themed with organizer problems; hacking window 24 hours; rooms 2\nSize rule entered: minimum 1, maximum 4; beginners track yes (50% of the team at their first hackathon, keyed to experience)\nPrizes against a submission yes; minors participate yes; recurring edition no; rules publish soon no\nPeople free during the window: 1; sweep owner not named; escalation owner not named\nDay clock: opening 10:00, welcome 15 min, first meal 13:00, change cut-off 10:30, judging opens not given; stocked project ideas 1",
"roster": "Registrations: 30 - 26 alone, 4 in 1 team; first hackathon 77%; role field collected; projects to pitch 1\nRegistered teams: Night Owls (4)\nSuggested draft groupings for the solo registrants (suggestions, not assignments):\nDraft A | Study room finder | Student 05 (communicator, first hackathon), Student 10 (designer, first hackathon), Student 06 (developer, first hackathon), Student 22 (developer, first hackathon)\nDraft B | pick from the stocked backlog | Student 13 (communicator, first hackathon), Student 18 (designer, first hackathon), Student 09 (developer, first hackathon), Student 25 (developer, first hackathon)\nDraft C | pick from the stocked backlog | Student 21 (communicator, first hackathon), Student 26 (designer, first hackathon), Student 14 (developer, first hackathon)\nDraft D | pick from the stocked backlog | Student 04 (data scientist), Student 15 (project manager, first hackathon), Student 03 (developer, first hackathon), Student 08 (developer)\nDraft E | pick from the stocked backlog | Student 12 (data scientist), Student 23 (project manager, first hackathon), Student 11 (developer, first hackathon), Student 16 (developer)\nDraft F | pick from the stocked backlog | Student 20 (data scientist), Student 02 (designer, first hackathon), Student 17 (developer, first hackathon), Student 24 (developer)\nDraft G | pick from the stocked backlog | Student 07 (project manager, first hackathon), Student 01 (developer, first hackathon), Student 19 (developer, first hackathon)",
"facts": "Size rule: composition - A range plus a track composition rule; wording \"Teams can be 1-4 people\"\nMechanic: mixer - Facilitated mixer; 45 min, as set by the organizer (no standard mixer format); runs straight after the welcome and ends about 11:00; plus pre-event draft suggestions\nSafety net: escalation - A floor sweep plus a time-boxed escalation, with a stocked backlog; time-box anchored to the end of the mixer (11:00) or the first meal (13:00); the offer order: a team known to be short-handed, then a stocked project, then solo work as a choice\nChange policy: cutoff - Open until a published cut-off, then frozen; cut-off 10:30\nProject supply: about 3 solo registrants absorbed by pitched projects and teams with room; 23 left over; about 6 stocked ideas wanted, 1 stocked; themed ratio wants 8\nReadiness gate (4 of 5): The team-size rule with its solo answer written; The composition fraction, if a track needs one written; The named mechanic with its duration written; The named owner of the safety net MISSING; The change cut-off as a wall-clock time written\nSignals to record: How many registered alone versus as a team (self-set); How many were still unteamed when the sweep ran, and what each did next (self-set); How many teams changed membership after the cut-off would have fallen (self-set)\nFindings (11):\n[info] composition_change (change): Say now what happens to a team that drops below the 50% fraction after someone leaves - deciding it in advance costs a clause, deciding it at judging costs a track.\n[info] mixer (mechanic): A mixer is promoted: mostly first-timers, too few projects to absorb the room, and a second space. It forms social groups that then need a project - never run it without a stocked backlog.\n[info] pre_event (mechanic): Most registrations are solo and the form collects a role - suggest draft groupings before the day (suggestions people may ignore, not assignments).\n[check] no_owner (safety): Name the one person who owns the floor sweep for the window - not \"an organizer\".\n[check] no_escalation_owner (safety): Name the escalation owner - someone who can decide, not only relay (asking a team to take a fifth person under a cap of four is a decision).\n[info] time_box (safety): Anchor the escalation's time-box to a clock the day already has - the end of the mixer (11:00) or the first meal (13:00) - and never invent a number of minutes; record what it actually took.\n[check] minors (safety): Minors participate: the sweep and the escalation are safeguarding interactions - decide who may hold what is observed, for how long, and what is never written down.\n[check] backlog (safety): Stock about 6 joinable project ideas (self-set: one per 4 unplaced solo registrants); 1 stocked. Joinable means ready-to-go tasks for newcomers and a build environment up in under 20 minutes.\n[check] themed_supply (supply): Themed event: about one subject-matter expert + workable project per 4 non-expert participants - 8 needed for 30 non-experts; 2 projects and 0 experts known.\n[fix] freeze_before_sweep (change): The cut-off 10:30 lands at or before the end of the mechanic (11:00) - a freeze before the safety net runs cancels the safety net.\n[check] gate (readiness): Readiness: 4 of 5 written before the brief is published - missing: The named owner of the safety net."
}
Reply:
{
"task": "script",
"rules": "Team rules: Teams can be 1-4 people, so hacking solo is allowed. On the beginners track, at least 50% of each team must be at their first hackathon. Team changes are open until 10:30, after which membership is frozen. Organizers, volunteers, judges, and sponsors do not compete for prizes.",
"opening": "Welcome to Northgate Campus Hack. Teams can be 1-4 people, so you're welcome to build solo if you'd rather - a project of one is okay too. Right after this welcome, we'll spend 45 minutes in a mixer to help anyone still looking find a team or a project to join.",
"pitch": "This event runs a facilitated mixer instead of a pitch round - the format below is our own design, since no standard mixer format is set. For 45 minutes right after the welcome, ending at 11:00, organizers circulate using the role each person gave at registration, pointing people toward others who complement their skills and toward the draft groupings already suggested, until everyone has a group or a clear next step.",
"sweep": "If you're still looking, let's sort you out. First, if a team nearby is still short-handed, I'll introduce you to them and what they need. If that's not a fit, here's a project from our stocked backlog, ready to go. And if neither works for you, building solo is a completely good choice - it's yours to decide.",
"escalation": "Privately, the escalation owner asks one specific team to take on one more person, naming that person and the contribution they'd bring - such as a developer or designer the team is missing - never a room-wide ask. The team decides, and the ask is settled by the end of the mixer at 11:00, or by the first meal at 13:00 if it runs long.",
"note": "Before publishing, name the sweep and escalation owners, and move the 10:30 change cut-off to after the mixer ends at 11:00, since a freeze before the safety net cancels it."
}
The page's check: kept - nothing to flag
Harbor Hardware Weekend (all teams) - plan
36 registered, 0 alone; Teams can be 2-5 people; solo entry is not allowed; No formal matchmaking; Everyone is on a team - someone whose team falls apart is handled by the change policy; Open throughout with an attribution rule; 5 of 5 ready.
Input:
{
"task": "plan",
"event": "Event: Harbor Hardware Weekend; open theme; hacking window 40 hours over several days; rooms 1\nSize rule entered: minimum 2, maximum 5; beginners track no\nPrizes against a submission yes; minors participate no; recurring edition yes; rules publish soon yes\nPeople free during the window: 3; sweep owner not named; escalation owner not named\nDay clock: opening 18:00, welcome 30 min, first meal 19:00, change cut-off not set, judging opens not given; stocked project ideas 0",
"roster": "Registrations: 36 - 0 alone, 36 as members of registered teams; first-timer share not collected; role field not collected; projects to pitch 0",
"facts": "Size rule: rangeNoSolo - A stated range without solo; wording \"Teams can be 2-5 people; solo entry is not allowed\"\nMechanic: none - No formal matchmaking\nSafety net: teamed - Everyone is on a team - someone whose team falls apart is handled by the change policy; the offer order: a team known to be short-handed, then a stocked project, then solo work as a choice\nChange policy: open - Open throughout with an attribution rule\nProject supply: about 0 solo registrants absorbed by pitched projects and teams with room; 0 left over; about 0 stocked ideas wanted, 0 stocked\nReadiness gate (5 of 5): The team-size rule with its solo answer written; The composition fraction, if a track needs one written; The named mechanic with its duration written; The named owner of the safety net written; The change cut-off as a wall-clock time written\nSignals to record: How many registered alone versus as a team (self-set); How many were still unteamed when the sweep ran, and what each did next (self-set); How many teams changed membership after the cut-off would have fallen (self-set)\nFindings (5):\n[info] no_solo (size): Solo entry is excluded by the minimum of 2 - say so in the brief, and expect the unteamed protocol to carry more weight.\n[info] big_cap (size): A cap above 4 - one organizer network observes hackers do best in teams of at most 4 (a practitioner observation, no study behind it); civic-tech pitch rounds observe groups of about 2-5.\n[info] all_teamed (mechanic): Everyone registered as a team, so no formal matchmaking costs nothing and covers everyone - the moment anyone registers alone, that rung is deleted.\n[info] open_change (change): A multi-day window: open throughout with an attribution rule, so a team that loses someone on a later day can still submit.\n[info] gate (readiness): Readiness: 5 of 5 written before the brief is published."
}
Reply:
{
"task": "plan",
"summary": "Teams can be 2-5 people with solo entry not allowed, no matchmaking is needed since everyone already registered in teams, and organizers should settle whether to keep the 5-person cap despite the 4-person guideline.",
"size": {
"rule": "rangeNoSolo",
"wording": "Teams can be 2-5 people; solo entry is not allowed",
"why": "The entered minimum of 2 and maximum of 5 forms a stated range that excludes solo entry outright."
},
"composition": {
"fraction": null,
"note": ""
},
"mechanic": {
"pick": "none",
"minutes": null,
"placement": "Nothing runs between the 30-minute welcome and the first line of code since every registrant already arrived on a team.",
"why": "All of the registrants already arrived as team members, so a matchmaking round would serve no one, and the rule is dropped the moment anyone registers alone.",
"pre_event": ""
},
"safety": {
"pick": "teamed",
"owner": "",
"anchors": [],
"offer": []
},
"change": {
"pick": "open",
"cutoff": "",
"why": "With the 40-hour hacking window spanning several days, membership stays open throughout with an attribution rule so a team that loses someone on a later day can still submit."
},
"supply": "There are no solo registrants needing a project: about 0 are absorbed by pitched projects and teams with room, 0 are left over, and the page wants about 0 stocked ideas against the 0 already stocked.",
"signals": [
"How many registered alone versus as a team - if more register alone next time, add a formal matchmaking mechanic.",
"How many were still unteamed when the sweep ran and what each did next - if many stay unteamed, stock a backlog of projects.",
"How many teams changed membership after the cut-off would have fallen - if frequent, set a firmer cut-off before judging."
],
"caveats": []
}
The page's check: kept - nothing to flag
Harbor Hardware Weekend (all teams) - script
36 registered, 0 alone; Teams can be 2-5 people; solo entry is not allowed; No formal matchmaking; Everyone is on a team - someone whose team falls apart is handled by the change policy; Open throughout with an attribution rule; 5 of 5 ready.
Input:
{
"task": "script",
"event": "Event: Harbor Hardware Weekend; open theme; hacking window 40 hours over several days; rooms 1\nSize rule entered: minimum 2, maximum 5; beginners track no\nPrizes against a submission yes; minors participate no; recurring edition yes; rules publish soon yes\nPeople free during the window: 3; sweep owner not named; escalation owner not named\nDay clock: opening 18:00, welcome 30 min, first meal 19:00, change cut-off not set, judging opens not given; stocked project ideas 0",
"roster": "Registrations: 36 - 0 alone, 36 as members of registered teams; first-timer share not collected; role field not collected; projects to pitch 0",
"facts": "Size rule: rangeNoSolo - A stated range without solo; wording \"Teams can be 2-5 people; solo entry is not allowed\"\nMechanic: none - No formal matchmaking\nSafety net: teamed - Everyone is on a team - someone whose team falls apart is handled by the change policy; the offer order: a team known to be short-handed, then a stocked project, then solo work as a choice\nChange policy: open - Open throughout with an attribution rule\nProject supply: about 0 solo registrants absorbed by pitched projects and teams with room; 0 left over; about 0 stocked ideas wanted, 0 stocked\nReadiness gate (5 of 5): The team-size rule with its solo answer written; The composition fraction, if a track needs one written; The named mechanic with its duration written; The named owner of the safety net written; The change cut-off as a wall-clock time written\nSignals to record: How many registered alone versus as a team (self-set); How many were still unteamed when the sweep ran, and what each did next (self-set); How many teams changed membership after the cut-off would have fallen (self-set)\nFindings (5):\n[info] no_solo (size): Solo entry is excluded by the minimum of 2 - say so in the brief, and expect the unteamed protocol to carry more weight.\n[info] big_cap (size): A cap above 4 - one organizer network observes hackers do best in teams of at most 4 (a practitioner observation, no study behind it); civic-tech pitch rounds observe groups of about 2-5.\n[info] all_teamed (mechanic): Everyone registered as a team, so no formal matchmaking costs nothing and covers everyone - the moment anyone registers alone, that rung is deleted.\n[info] open_change (change): A multi-day window: open throughout with an attribution rule, so a team that loses someone on a later day can still submit.\n[info] gate (readiness): Readiness: 5 of 5 written before the brief is published."
}
Reply:
{
"task": "script",
"rules": "Team rules: Teams can be 2-5 people; solo entry is not allowed. Team changes stay open throughout the event, with an attribution rule covering anyone who joins or changes a team partway through, so a team can still submit after a change on a later day. Organizers, volunteers, judges and sponsors do not compete for prizes.",
"opening": "Welcome to Harbor Hardware Weekend. Teams can be 2-5 people; solo entry is not allowed. If your team loses someone during the weekend, you are still covered: registration stays open throughout the event, with an attribution rule for anyone who joins in later, so you can keep working and submit at the end. Since everyone already registered as part of a team, there is no formal matchmaking round - find your team and get started.",
"pitch": "",
"sweep": "",
"escalation": "",
"note": "Everything required for the brief is set; the only open question is whether to reconsider the 5-person cap, since groups above 4 reportedly perform worse."
}
The page's check: kept - nothing to flag
The draft-team CSV for the first sample
draft,project,name,role,first_hackathon
Draft A,Bus stop accessibility map,Ana Ruiz,developer,
Draft A,Bus stop accessibility map,Gus Lind,project manager,yes
Draft A,Bus stop accessibility map,Hana Ito,designer,
Draft A,Bus stop accessibility map,Sana Iqbal,developer,yes
Draft B,Rent burden dashboard,Ben Okoro,data scientist,
Draft B,Rent burden dashboard,Dara Mills,designer,yes
Draft B,Rent burden dashboard,Jon Abara,developer,yes
Draft B,Rent burden dashboard,Fen Holt,developer,
Draft C,Food bank volunteer scheduler,Eli Park,domain expert,
Draft C,Food bank volunteer scheduler,Ivy Lane,communicator,yes
Draft C,Food bank volunteer scheduler,Noor Sami,developer,yes
Draft C,Food bank volunteer scheduler,Lena Voss,developer,
Draft D,Permit wait-time tracker,Rafa Sol,government staff,
Draft D,Permit wait-time tracker,Kit Moreau,data scientist,yes
Draft D,Permit wait-time tracker,Chen Wei,developer,yes
Draft E,pick from the stocked backlog,Max Feld,advocate,yes
Draft E,pick from the stocked backlog,Ola Brandt,designer,yes
Draft E,pick from the stocked backlog,Pia Rask,developer,yes
Build the input yourself
Load the engine in node (global.window = global; require("./teamkit.js"); require("./teamreply.js")), then:
const K = TeamKit, R = TeamReply;
const f = K.form({ name: "My Hack", sizeMin: "1", sizeMax: "4", opening: "09:30", welcome: "20", firstMeal: "12:30", cutoff: "13:30",
sweepOwner: "Floor lead", regs: fs.readFileSync("registrations.csv", "utf8") });
// f.N, f.solo, f.teams, f.room, f.size, f.sizeText, f.mechanic, f.duration, f.mechEnd, f.net, f.change, f.cutoff, f.gate, f.findings
// f.drafts: [{label, project, members: [{name, role, first}], roles, firsts}] - suggestions, not assignments
K.draftCSV(f) // draft, project, name, role, first_hackathon
const b = K.buildInput(f, "plan"); // or "script" (needs a size rule)
const wire = K.toWire(b.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output), "plan");
// R.check(reply, b) // {status: kept or flagged, notes}
Get a token for the steps below from /tokens.html.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://team-forge.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://team-forge.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"]
// team-forge.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://team-forge.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://team-forge.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
}
// TeamForge.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 TeamForge {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://team-forge.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://team-forge.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://team-forge.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://team-forge.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":"team-forge"}' | 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": "team-forge"})["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: "team-forge" })).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": "team-forge"}, 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", "team-forge"), 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: "team-forge" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "team-forge"])["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 = "team-forge" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "plan", "event": "Event: ...; open theme; hacking window 8 hours ...", "roster": "Registrations: 28 - 18 alone, 10 in 3 teams ...", "facts": "Size rule: range - ..."}
# - see the input table
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": "plan", "event": "Event: ...; open theme; hacking window 8 hours ...", "roster": "Registrations: 28 - 18 alone, 10 in 3 teams ...", "facts": "Size rule: range - ..."}
# - see the input table
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: "plan" or "script", event, roster, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses team-forge:<task>:<inputHash>:a<attempt> - the lane is part of the key - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A plan carries summary, size, mechanic, safety, change, supply and caveats; a script carries rules, opening, pitch, sweep, escalation and note. The code below prints the headline parts of either (list items are plain strings).
KEY="team-forge:plan:$(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}'
jq -r '.summary // .format, .note // .supply, ((.signals // .caveats)[] | "\(.signal // .caveat): \(.text // .text)")' reply.json
jq -r '.opening // .note // empty' reply.json # the opening (script) or the note (plan)
import hashlib, time
key = "team-forge:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("summary"))
print(reply.get("note") or reply.get("supply", ""))
for s in reply.get("signals") or reply.get("caveats") or []:
print(s.get("signal", s.get("caveat")), "-", s.get("text", s.get("text")))
if reply.get("opening"):
print("opening:", reply["opening"])
import { createHash } from "node:crypto";
const key = `team-forge:${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.summary);
console.log(reply.note ?? reply.supply);
for (const s of reply.signals || reply.caveats || []) console.log(s.signal ?? s.caveat, "-", s.text ?? s.text);
if (reply.opening) console.log("opening:", reply.opening);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "team-forge:" + 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 }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
items, _ := reply["signals"].([]any) // signals and caveats are plain strings
fmt.Println(j["charged_credits"], j["truncated"], reply["summary"])
fmt.Println(reply["note"], reply["supply"])
for _, x := range items {
c := x.(map[string]any)
fmt.Println(c["signal"], "-", c["status"])
}
if fix, ok := reply["opening"].(string); ok && fix != "" {
fmt.Println("opening:", fix)
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "team-forge:" + 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.get("summary"));
System.out.println(reply.path("note").asText(reply.path("supply").asText()));
for (JsonNode s : reply.has("signals") ? reply.path("signals") : reply.path("caveats")) System.out.println(s.path("signal").asText(s.path("caveat").asText()) + " - " + s.path("status").asText(s.path("text").asText()));
if (!reply.path("opening").asText().isEmpty()) System.out.println("opening: " + reply.get("opening").asText());
require "digest"
key = "team-forge:#{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["summary"]].inspect
puts reply["note"] || reply["supply"]
(reply["signals"] || reply["caveats"] || []).each { |s| puts "#{s["signal"] || s["caveat"]} - #{s["text"] || s["text"]}" }
puts "opening: #{reply["opening"]}" unless reply["opening"].to_s.empty?
$key = "team-forge:" . $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["summary"] ?? ""), "\n";
echo $reply["note"] ?? $reply["supply"] ?? "", "\n";
foreach ($reply["signals"] ?? $reply["caveats"] ?? [] as $s) echo $s["signal"] ?? $s["caveat"], " - ", $s["text"] ?? $s["text"], "\n";
if (!empty($reply["opening"])) echo "opening: ", $reply["opening"], "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"team-forge:{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!["summary"]}");
Console.WriteLine($"{reply["note"] ?? reply["supply"]}");
foreach (var s in (reply["signals"] ?? reply["caveats"]) as JsonArray ?? new JsonArray()) Console.WriteLine($"{s!["signal"] ?? s["caveat"]} - {s["text"] ?? s["text"]}");
if (reply["opening"] is JsonNode fix && fix.GetValue<string>() != "") Console.WriteLine("opening: " + fix);
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: team-forge:plan:$(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": "plan", "event": "Event: ...; open theme; hacking window 8 hours ...", "roster": "Registrations: 28 - 18 alone, 10 in 3 teams ...", "facts": "Size rule: range - ..."}
# - see the input table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "team-forge:plan:" + 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": `team-forge:plan:${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", "team-forge:plan:"+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", "team-forge:plan:" + 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" => "team-forge:plan:#{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: team-forge:plan:" . 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", $"team-forge:plan:{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);