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

chesscom-sdk

v1.0.1

Published

Unofficial TypeScript SDK for the Chess.com Published-Data API. Not affiliated with Chess.com.

Readme

chesscom-sdk

npm version license types

Unofficial TypeScript SDK for the Chess.com Published-Data API. Not affiliated with, endorsed by, or sponsored by Chess.com.

A clean, typed, isomorphic client for the public Chess.com API — with built-in rate limiting, ETag caching, runtime response validation, and lazy pagination.

  • ✅ Typed responses, validated at runtime with zod
  • ✅ Serial rate limiting + backoff (respects the Chess.com "be serial" rule)
  • ✅ Transparent ETag caching (304 Not Modified aware)
  • ✅ Lazy async iteration over monthly game archives
  • ✅ Isomorphic — native fetch, runs in Node 22+, Deno, Bun, the browser
  • ✅ One dependency (zod)

✅ Stable since 1.0.0. The public API follows semver — breaking changes ship only in a new major version.

Install

npm install chesscom-sdk

Quickstart

import { ChessComClient } from "chesscom-sdk";

const client = new ChessComClient({
  // Required by Chess.com — include an app name and a contact.
  userAgent: "myapp/1.0 ([email protected])",
});

const profile = await client.getPlayer("hikaru");
const stats = await client.getPlayerStats("hikaru");

// Lazily stream a player's games across monthly archives:
for await (const game of client.streamPlayerGames("hikaru", {
  since: "2024-01",
})) {
  console.log(game.url, game.pgn);
}

Why a userAgent is required

Chess.com rejects requests without a descriptive User-Agent (HTTP 403) and asks that you include a way to contact you. The client therefore requires it at construction. Use "<app>/<version> (<contact>)", e.g. "my-bot/1.0 ([email protected])".

Note: in browsers, fetch ignores a custom User-Agent (it is a forbidden header). The option only takes effect on Node, Deno, and Bun.

API

All methods return validated, fully typed results.

| Method | Returns | Endpoint | | --------------------------------------- | ---------------------- | ------------------------------------------ | | getPlayer(username) | PlayerProfile | /player/{username} | | getPlayerStats(username) | PlayerStats | /player/{username}/stats | | getPlayerArchives(username) | string[] | /player/{username}/games/archives | | getPlayerGames(username, year, month) | Game[] | /player/{username}/games/{YYYY}/{MM} | | getPlayerGamesPgn(user, year, month) | string (raw PGN) | /player/{username}/games/{YYYY}/{MM}/pgn | | streamPlayerGames(username, options?) | AsyncGenerator<Game> | iterates over the monthly archives | | getPlayerGamesToMove(username) | ToMoveGame[] | /player/{username}/games/to-move | | getPlayerClubs(username) | PlayerClub[] | /player/{username}/clubs | | getPlayerTournaments(username) | PlayerTournaments | /player/{username}/tournaments | | getClub(urlId) | ClubProfile | /club/{url-id} | | getClubMembers(urlId) | ClubMembers | /club/{url-id}/members | | getTournament(urlId) | Tournament | /tournament/{url-id} | | getTournamentRound(urlId, round) | TournamentRound | /tournament/{url-id}/{round} | | getTournamentRoundGroup(urlId, r, g) | TournamentRoundGroup | /tournament/{url-id}/{round}/{group} | | getLeaderboards() | Leaderboards | /leaderboards | | getStreamers() | Streamer[] | /streamers | | getDailyPuzzle() | Puzzle | /puzzle | | getRandomPuzzle() | Puzzle | /puzzle/random | | getCountry(iso) | Country | /country/{iso} | | getCountryPlayers(iso) | string[] | /country/{iso}/players | | getCountryClubs(iso) | string[] | /country/{iso}/clubs | | getPlayerMatches(username) | PlayerMatches | /player/{username}/matches | | getClubMatches(urlId) | ClubMatches | /club/{url-id}/matches | | getMatch(id) | Match | /match/{id} | | getMatchBoard(id, board) | MatchBoard | /match/{id}/{board} | | getTitledPlayers(title) | string[] | /titled/{title} |

Each method also accepts a final options object with an AbortSignal:

const controller = new AbortController();
const profile = await client.getPlayer("hikaru", { signal: controller.signal });

Return conventions

The API wraps most collections in an envelope. Two rules keep returns predictable:

  • Single-key array envelopes are unwrapped. Endpoints whose payload is just { games: [...] }, { players: [...] }, { clubs: [...] }, etc. return the array directly (Game[], string[], …) — the wrapper carries no extra meaning.
  • Multi-key structures are returned as-is. When the grouping is the data — { finished, in_progress, registered } (matches, tournaments), { weekly, monthly, all_time } (club members), the leaderboard categories — the object is returned whole (PlayerMatches, ClubMembers, …).

Numeric path parameters

year/month are typed number (they are validated — month must be 1–12). Opaque path ids — a match id, a board, a tournament round/group — accept number | string, since they are passed through verbatim and can exceed Number.MAX_SAFE_INTEGER.

streamPlayerGames

Hides the monthly pagination: it lists the archives, then fetches one month at a time (lazily) and yields game by game. The rate limiter and cache apply per month, so re-runs are fast and polite.

for await (const game of client.streamPlayerGames("hikaru", {
  since: "2024-01", // YYYY-MM, inclusive
  until: "2024-12", // YYYY-MM, inclusive
  order: "newest-first", // or "oldest-first" (default: newest-first)
  timeClass: "blitz", // keep only blitz games
  rated: true, // keep only rated games
})) {
  // …
}

Months outside the since/until window are never requested.

Parsing PGN

Games expose their raw PGN as a string — this SDK does not parse moves, so you can pair it with whatever chess library you prefer. For example, with chess.js:

import { Chess } from "chess.js";

const games = await client.getPlayerGames("hikaru", 2024, 1);
const game = games[0];

if (game?.pgn) {
  const chess = new Chess();
  chess.loadPgn(game.pgn);
  console.log(chess.history()); // ["e4", "c5", "Nf3", …]
  console.log(chess.header()); // { White, Black, Result, ECO, … }
}

(Header fields like white, black, time_control, eco, and end_time are also available as structured fields on the Game object, no parsing needed.)

Configuration

new ChessComClient({
  userAgent: "myapp/1.0 ([email protected])", // required
  fetch, // custom fetch (default: global fetch)
  cache, // custom CacheStore (default: in-memory Map)
  timeout: 10_000, // per-request timeout in ms (default: none)
  baseUrl: "https://api.chess.com/pub", // default
  onValidationError: "throw", // "throw" | "warn" | "ignore" (default: "throw")
  onRateLimit: (info) => console.warn("rate limited", info),
});

Error handling

Every error thrown by the SDK extends ChessComError and carries a discriminant kind. Branch with instanceof or switch (err.kind).

import { ChessComError, NotFoundError } from "chesscom-sdk";

try {
  await client.getPlayer("does-not-exist");
} catch (err) {
  if (err instanceof NotFoundError) {
    // …
  } else if (err instanceof ChessComError) {
    console.error(err.kind, err.status, err.url);
  }
}

| Error | kind | When | | ----------------- | ------------ | ---------------------------------------------- | | NotFoundError | not_found | HTTP 404 / 410 | | RateLimitError | rate_limit | HTTP 429 after retries are exhausted | | ForbiddenError | forbidden | HTTP 403 (often a missing/rejected User-Agent) | | ServerError | server | HTTP 5xx or an unexpected status | | ValidationError | validation | A response did not match its schema | | NetworkError | network | The request never produced a response |

Validation

Responses are validated against zod schemas. If the API drifts from the expected shape, onValidationError decides what happens:

  • "throw" (default) — throw a ValidationError.
  • "warn" — log a warning and return the raw data.
  • "ignore" — return the raw data silently.

Rate limiting

Chess.com asks clients to make requests serially (parallel requests get a 429). By default the client funnels all requests through a serial queue and retries a 429 with exponential backoff, honoring the server's Retry-After. This is per-client instance; share one client to share the queue.

Caching

The client revalidates with ETags (If-None-Match) and serves the cached body on 304 Not Modified. The default store is an in-memory Map. Plug in your own by implementing CacheStore:

import type { CacheStore, CacheEntry } from "chesscom-sdk";

class RedisCacheStore implements CacheStore {
  constructor(private redis: import("ioredis").Redis) {}

  async get(key: string): Promise<CacheEntry | undefined> {
    const raw = await this.redis.get(key);
    return raw ? (JSON.parse(raw) as CacheEntry) : undefined;
  }

  async set(key: string, value: CacheEntry): Promise<void> {
    await this.redis.set(key, JSON.stringify(value));
  }
}

const client = new ChessComClient({
  userAgent: "myapp/1.0 ([email protected])",
  cache: new RedisCacheStore(redis),
});

Requirements

  • The published library runs on Node 22+, Deno, Bun, and browsers (anything with a global fetch).
  • Contributing to this repo requires Node 22+ (the dev toolchain).

Contributing

Contributions are welcome — see CONTRIBUTING.md.

Documentation

License

MIT