@team-gauntlet/client
v1.0.0
Published
TypeScript client for the Gauntlet tournament bracket API.
Maintainers
Readme
@team-gauntlet/client
TypeScript client for the Gauntlet tournament bracket API. Single elimination, double elimination, round robin and Swiss, with live updates over SSE.
Zero runtime dependencies. Needs a runtime with global fetch: Node 18+, Deno, Bun, browsers, workers.
npm install @team-gauntlet/clientRequests go to https://gauntletbrackets.com.
Usage
import { GauntletClient } from "@team-gauntlet/client";
const client = new GauntletClient({
token: process.env.GAUNTLET_API_KEY, // gt_live_...
});
const tournament = await client.createTournament({
name: "Spring Invitational",
game: "Rocket League",
format: "double_elim",
});
// Double elimination needs at least 3; single elimination is the format for 2.
const entrants = await client.addParticipants(tournament.id, [
"Team Vortex",
"Team Halcyon",
"Team Meridian",
]);
// Each entrant gets a magic link. Send it to them; listParticipants returns it again.
for (const entrant of entrants) {
console.log(entrant.name, `https://gauntletbrackets.com/p/${entrant.accessToken}`);
}
await client.generateBracket(tournament.id); // pending -> ready, roster locked
const bracket = await client.open(tournament.id); // ready -> underway, results now acceptedLifecycle
pending --generateBracket--> ready --open--> underway --(final decided)--> complete
^ |
+----------reset--------------+Generating the bracket does not start play. Results are refused with 409 until open runs, which
is the deliberate go-live step. reset discards the bracket and reopens registration, and is
permitted from ready only: once a tournament is underway nothing may destroy a reported result.
archive is a flag, not a state, so it composes with all of the above. An archived tournament stays
readable and embeddable but rejects every write until unarchive.
Matches
pending --(both teams known)--> ready --setMatchUnderway--> underway --reportResult--> completeready is "both teams known, nobody has started". underway is "being played right now", set by an
organiser, and it is what tells a scoreboard the difference. Reporting a result clears it either way,
so setMatchUnderway(id, false) is only for the match that never actually started.
getBracket returns the matches that are part of the event as it stands — playing, playable or
decided — and leaves out pending and void, the empty later rounds with no teams in them yet:
const live = await client.getBracket(id, { state: ["underway"] }); // a now-playing board
const full = await client.getBracket(id, { matches: "all" }); // to draw the whole bracketReporting results safely
Pass the version you read as expectedVersion to make the write optimistic. A co-organiser who
reported first causes a conflict instead of a silent overwrite:
import { GauntletError } from "@team-gauntlet/client";
const match = bracket.matches.find((m) => m.state === "ready")!;
try {
await client.reportResult(match.id, { winnerId: match.p1Id!, expectedVersion: match.version });
} catch (error) {
if (error instanceof GauntletError && error.isVersionConflict) {
// Someone else got there first. Refetch and decide what to do.
}
}Live updates
const controller = new AbortController();
for await (const event of client.watch(tournament.id, { signal: controller.signal })) {
if (event.event !== "bracket.updated") continue;
const { matches } = await client.getBracket(tournament.id, { state: ["underway"] });
console.log("on now:", matches.map((match) => match.label).join(", ") || "nothing");
}Frames carry the reason for a change, not the new state, so one code path renders the first load and
every update. watch does not reconnect on its own: it returns when the connection drops. Wrap it in
a retry loop if the subscription must outlive a network blip.
Errors
Every non-2xx response throws a GauntletError with status, code, message and details.
Branch on code, never on message.
| code | When |
| --- | --- |
| bad_request | Malformed body, or an unknown field |
| unauthorized | No credential where one is required |
| forbidden | Authenticated, but not permitted |
| not_found | Missing, or not readable by this caller. Both answer identically on purpose |
| conflict | Wrong lifecycle state, archived, or a lost expectedVersion race |
| unprocessable | Valid request the domain refuses, e.g. a roster over the 256 cap |
| invalid_bracket | The format engine refused, e.g. too few entrants for the format |
| rate_limited | Read error.retryAfterSeconds |
Credentials
| Token | Prefix | Gets you |
| --- | --- | --- |
| API key | gt_live_ | Everything you own. Mint one at /settings |
| Participant token | gtp_ | Reporting your own match, via submitResult |
Both travel as Authorization: Bearer; the server tells them apart by prefix. Omit token entirely
to read public tournaments anonymously.
API key management (/api/v1/keys) is deliberately not wrapped: those endpoints accept an
interactive session only, and refuse bearer credentials, so a key can never mint another key.
License
MIT
