npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@team-gauntlet/client

v1.0.0

Published

TypeScript client for the Gauntlet tournament bracket API.

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/client

Requests 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 accepted

Lifecycle

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-->  complete

ready 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 bracket

Reporting 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