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

@potenfyrstudios/discord-botlists

v1.0.3

Published

Universal multi-botlist SDK for Discord bots: post stats to 27 verified live lists, realtime vote webhooks, ratings and a universal data parser with full TypeScript types

Downloads

523

Readme

npm npm downloads Website Discord GitHub View

CI status license

Typing SVG

@potenfyrstudios/discord-botlists: the universal multi-botlist SDK for Discord bots: post stats to 27 verified live lists, get votes in realtime, parse every API into one shape.

Docs · Examples · npm · Issues · Live status


Why discord-botlists

Posting your bot's stats to every list and handling every list's webhook format yourself is weeks of glue code. This package does all of it with zero runtime dependencies:

  • Stats posting to 27 verified live lists, each with its correct wire format, auth header and endpoint, learned from every list's own docs plus the BotBlock directory. Dead lists are pruned automatically by the hourly status sync.
  • Realtime vote, comment and review events through a built-in webhook server. No polling, no delay, no express dependency.
  • Universal parser that normalizes any list's bot data into one UniversalBot object so you never juggle server_count vs guildCount vs guilds vs count again.
  • Status tracking: automated hourly probes detect deprecated or shut down lists, with latency and HTTP state, synced into this README and the docs site.
  • Fully typed: strict TypeScript, generic event emitter, autocomplete for every option.
  • Lightweight: zero dependencies, works on Node 18+ and Bun.

The full documentation lives at botlists.docs.potenfyr.in: API reference, examples and the live status board. This README mirrors the same content.

Installation

npm install @potenfyrstudios/discord-botlists
# or
bun add @potenfyrstudios/discord-botlists
# or
pnpm add @potenfyrstudios/discord-botlists

Requirements: Node.js 18+ or Bun 1.1+. No peer dependencies.

Works with every Discord framework, or none at all:

| Framework | Auto stats collection | | --- | --- | | discord.js | pass client - reads guilds.cache.size, shard info | | Eris | pass client - reads the guilds Map | | Oceanic | pass client - reads the guilds Map | | Any other client | pass client if it exposes guilds | | Anything else / no framework | use statsProvider or pass stats explicitly |

// framework-less usage: stats provided by you
const lists = new Botlists({
  statsProvider: () => ({
    serverCount: shardManager.totalGuilds,
    shardCount: shardManager.shardCount,
  }),
});

// or per call
await lists.postStats({ serverCount: myGuildCount });

The webhook server, parser, status checks and every other feature are framework independent - they use plain HTTP and Node builtins.

Quick start

import { Client } from 'discord.js';
import { Botlists } from '@potenfyrstudios/discord-botlists';

const client = new Client({ intents: ['Guilds'] });

const lists = new Botlists({
  client,
  tokens: {
    'top.gg': process.env.TOPGG_TOKEN,
    'discordbotlist.com': process.env.DBL_TOKEN,
  },
  webhook: {
    port: 8080,
    path: '/discord-botlists',
    secret: process.env.WEBHOOK_SECRET,
  },
});

client.once('ready', async () => {
  // 1. post stats to every list you gave a token for
  const report = await lists.postStats();
  console.log(`posted to ${report.posted} lists`);

  // 2. start realtime vote webhooks
  await lists.startWebhooks();
});

// 3. react to votes the moment they happen
lists.on('vote', (vote) => {
  console.log(`${vote.voterId} voted on ${vote.listName} (x${vote.weight})`);
  // give the voter a bonus role here
});

lists.on('review', (review) => {
  console.log(`new review on ${review.listName}: ${review.content}`);
});

client.login(process.env.DISCORD_TOKEN);

Point each botlist's webhook URL at https://your-domain:8080/discord-botlists/<list-id> and votes arrive as typed events instantly. Per-list wire formats, env variable names and more live examples: docs · examples.

top.gg v1 signed webhooks

top.gg no longer sends the shared password in the Authorization header. Every top.gg v1 delivery carries x-topgg-signature: t=<unix seconds>,v1=<hex>, the HMAC-SHA256 of <t>.<rawBody> keyed with your whs_... webhook secret from the top.gg dashboard. The SDK verifies this automatically - just keep the secret configured for top.gg (the same field you already had). The legacy Authorization header and other signature schemes keep working for every other list.

The v1 payload is wrapped ({"vote":{"userId":..,"botId":..,"type":"vote"|"test"}}); the SDK flattens it so your vote handler is unchanged, preserves the original body on vote.raw, and routes dashboard test deliveries to the test event with isTest: true. Feeding bodies through lists.webhook.ingest() from your own framework route? Pass the delivery headers and ingest enforces the signature check for you (returns null on failure); omit them and it trusts your framework's auth.

Announce votes to Discord in realtime (text / embed / embed-v2)

Disabled by default - opt in and every incoming vote is broadcast to your Discord channel webhooks (and optionally external endpoints) through the raw Discord execute-webhook REST API, no Discord library required:

const lists = new Botlists({
  client,
  announcer: {
    enabled: true,
    format: 'embed-v2', // 'text' ({placeholder} template) | 'embed' | 'embed-v2' (Components V2)
    webhooks: [process.env.VOTE_WEBHOOK_URL!],          // Discord channel webhooks
    external: ['https://api.example.com/hooks/votes'],  // JSON: { source, event, list, vote }
    links: [{ label: 'Vote Again', url: 'https://top.gg/bot/YOUR_BOT/vote', emoji: '🗳️' }],
  },
});

Rate-limit safe by default: ≥ 1 s spacing per target (minIntervalMs), one polite Retry-After wait on 429, bounded queues (maxQueueSize, oldest dropped - a stalled webhook can never grow memory) and unref'd timers that only exist while a delivery is pending. Full control via customize(vote, payload), identity overrides via username / avatarUrl / botToken, and a standalone VoteAnnouncer class for custom pipelines.

Full API

class Botlists

| Method | Returns | Description | | --- | --- | --- | | postStats(stats?, opts?) | Promise<PostReport> | Post to every list you have a token for. opts.only / opts.skip filter by list id. | | postStatsTo(list, stats?) | Promise<PostResult> | Post to one list. | | postViaBotBlock(stats?) | Promise<PostReport> | One request to botblock.org that fans out to all lists. | | startWebhooks(port?) | Promise<string> | Start the realtime webhook server, returns its address. | | stopWebhooks() | Promise<void> | Stop the webhook server. | | ingestWebhook(list, body, isTest?) | void | Feed a webhook body from your own express/fastify app. | | fetchBot(list, botId?) | Promise<UniversalBot> | Fetch and normalize a bot from one list. | | fetchVotes(list, botId?) | Promise<number \| null> | Current vote count on one list. | | hasVoted(list, userId) | Promise<boolean \| null> | Check if a user voted (lists that expose it). | | searchBots(list, query, limit?) | Promise<UniversalBot[]> | Search a list's directory. | | refreshStatus(force?) | Promise<StatusBoard> | Probe all lists: state, latency, HTTP. | | checkAndReportStatus(force?) | Promise<StatusBoard> | Probe + console table + issue links. | | widgetUrl(list, botId?) | string \| null | Widget image URL on a list. | | viewBotUrl(list, botId?) | string \| null | Public bot page on a list. | | startAutoPost(intervalMs?, stats?) | void | Re-post stats periodically (default 30 min). | | stopAutoPost() | void | Stop the auto poster. |

A list can be referenced by id ('top.gg'), name ('Discord Bots'), hostname ('discord.bots.gg') or shorthand ('topgg', 'voidbots', 'radarcord').

Also exported: UniversalParser, StatusChecker, ConsoleReport, BotlistsError, EVENTS, and the subpath exports @potenfyrstudios/discord-botlists/webhooks (VoteWebhookServer) and /lists (the raw registry).

Realtime events

| Event | Payload | Fired when | | --- | --- | --- | | vote | UniversalVote | A user votes for your bot on any list. | | review | UniversalComment | A review is posted. | | comment | UniversalComment | A comment is posted. | | rating | UniversalComment | A rating is posted. | | test | UniversalVote | A list dashboard sends a webhook test. | | statsPosted | PostReport | After each postStats fan-out. | | status | StatusBoard | After each status refresh. | | raw | ParsedWebhook | Every parsed webhook, for custom handling. | | request | { method, url, status, listId } | Every HTTP request the server receives. | | error | Error | Bad auth, invalid JSON, internal errors. |

lists.on('vote', (vote) => {
  vote.listId;      // 'top.gg'
  vote.voterId;     // '160105994217586689'
  vote.voterName;   // 'someuser'
  vote.weight;      // 2 on weekend multiplier lists
  vote.weekend;     // true
  vote.isTest;      // false
  vote.query;       // { ref: 'partner' } if the vote link had query params
  vote.raw;         // the untouched original body
});

UniversalBot: one shape for every list

Whatever list you call fetchBot on, you always get:

interface UniversalBot {
  listId: string;          // 'top.gg'
  listName: string;        // 'Discord Bot List'
  id: string;              // '432161800760442880'
  name: string;            // 'Rythm'
  avatar: string | null;
  description: string | null;
  owners: string[];        // ['160105994217586689']
  serverCount: number | null;
  shardCount: number | null;
  votes: number | null;         // total votes / points
  monthlyVotes: number | null;
  certified: boolean | null;
  website: string | null;
  github: string | null;
  supportServer: string | null;
  invite: string | null;
  prefix: string | null;
  library: string | null;
  tags: string[];
  ratings: { average: number | null; count: number | null };
  raw: unknown;            // original payload, always kept
  fetchedAt: number;       // unix ms
}

Webhook security

Anyone who discovers your webhook URL could POST fake votes. The server defends against that by default:

  • Secret required: POSTs without a valid secret are rejected with 401. The server refuses to start if you never configure a secret (override with security.requireSecret = false for local tests only).
  • Per-list secrets: secret: { 'top.gg': '...', 'botlist.me': '...' } - each list only passes with its own value.
  • HMAC-SHA256 payload signing: for lists that sign payloads, pass security.hmac keys and the server verifies the signature from x-signature-256 / x-hub-signature-256 / x-signature headers. A wrong signature is rejected even if the secret matches.
  • Per-IP rate limiting: default 30 requests/minute, extra requests get 429 + Retry-After.
  • Brute-force lockout: 10 consecutive auth failures bans the IP for 15 minutes (403).
  • List allowlist: security.allowedLists restricts which lists may POST at all.
  • Body limit: payloads over 512 KB are dropped (413).
  • security.trustProxy = true if you run behind nginx/Cloudflare so rate limits key off the real client IP.
const lists = new Botlists({
  webhook: {
    port: 8080,
    secret: {
      'top.gg': 'shared-secret-for-topgg',
      'botlist.me': 'another-secret',
    },
    security: {
      // sign keys per list, checked against x-signature-256 etc.
      hmac: { 'top.gg': 'webhook-signing-key' },
      rateLimit: { max: 30, windowMs: 60_000 },
      banAfterFailures: 10,
      allowedLists: ['top.gg', 'botlist.me', 'discordbotlist.com'],
      trustProxy: true, // behind a reverse proxy
    },
  },
});

Tokens: three ways, pick what fits

# 1. env vars (recommended). Pattern: DBL_<LISTID-UPPERCASED>
DBL_TOP.GG=eyJ...            # top.gg token
DBL_DISCORDBOTLIST.COM=...   # discordbotlist.com token
DBL_VOIDBOTS.NET=...

Env keys are mapped onto list ids automatically (DBL_TOPGG / DBL_TOP.GGtop.gg), so you never have to hand-translate names. Dots, underscores and casing don't matter.

// 2. constructor map
const lists = new Botlists({ tokens: { 'top.gg': 'eyJ...' } });

// 3. mixed: env is the base, the map overrides per key

list.tokenEnvKey tells you the env name for every list at runtime. See .env.example for the full key list.

Posting stats, including shards

await lists.postStats({
  serverCount: client.guilds.cache.size,
  shardCount: client.shard?.count,
  shards: await client.shard?.fetchClientValues('guilds.cache.size'),
});

// only some lists
await lists.postStats({ serverCount: 100 }, { only: ['top.gg', 'botlist.me'] });

// one list
await lists.postStatsTo('radarcord', { serverCount: 100 });

// single BotBlock request for all lists (counts as 1 request to botblock)
await lists.postViaBotBlock({ serverCount: 100 });

Every list gets its correct body shape automatically: server_count for top.gg, guildCount for discord.bots.gg, guilds for discordbotlist.com, servers for disforge, and so on for all 27.

Rate limit safety

  • Posts are throttled with a 250 ms gap per list; HTTP layer retries 408/429/5xx with backoff, and strict requests honour Retry-After (≤ 30 s) once before failing.
  • BotBlock mode sends one request total (their own limit is 1 per 120 s, the SDK will not retry it faster).
  • Status probes use 8 parallel HEAD requests max (GET fallback for hosts that reject HEAD), browser UA, board cached 5 minutes.
  • fetchBot/searchBots add your token only when required, public endpoints stay unauthenticated.

Custom / self-hosted lists

const lists = new Botlists({
  lists: [{
    id: 'my-list.dev',
    name: 'My Self Hosted List',
    website: 'https://my-list.dev',
    apiPost: 'https://api.my-list.dev/bots/:id/stats',
    postField: 'server_count',
    postMethod: 'POST',
    authHeader: 'Authorization',
    apiDocs: null, apiGet: null, viewBot: null, widget: null,
    shardField: null, shardIdField: null, shardsArrayField: null,
    tokenEnvKey: 'DBL_MYLIST',
    webhook: { header: 'Authorization', voterField: 'user_id', eventField: null },
    supports: { post: true, get: false, widget: false, webhook: true },
  }],
});

Custom lists work everywhere built-in ones do: posting, webhooks, parsing, status.

Supported lists (27 verified live)

The generated registry (src/data/lists.generated.ts, reproducible from the BotBlock snapshot) only contains lists that answered during the latest status audit. Shutdown and deprecated lists are removed automatically. The September 2026 docs audit pruned 8 domains that now serve registrar parking or squatter pages despite answering HTTP 200 (blist.xyz, botlist.co, botsdatabase.com, discord.services, discordbot.world, motiondevelopment.top, space-bot-list.xyz, topcord.xyz) and added topbot.gg and discordforge.org:

top.gg | discordbotlist.com | discord.bots.gg | botlist.me | discords.com | voidbots.net | vcodes.xyz | radarcord.net | discordlist.gg | disforge.com | disq.ink | dlist.space | cybralist.com | discord.rovelstars.com | yabl.xyz | justdiscord.org | omniplex.gg | discover.fluxpoint.dev | bots.discordlabs.org | discordbotlist.xyz | stellarbotlist.com | carbonitex.net | discord.place | topbot.gg | discordforge.org

Each record carries: endpoint URLs, wire field names, shard field names, auth header, widget and view URLs, webhook format hint and env var key.

Missing a list? Open a list request with its name, API docs link and a maintainer contact: live lists get added within days.

Live status

Last sync: 2026-09-21 | 🟢 27 live | 🟡 0 deprecated | 🔴 0 shutdown | ⚪ 0 unknown

| List | Status | Latency | HTTP | Last checked (UTC) | | --- | --- | --- | --- | --- | | Botlist.me | 🟢 live | 899 ms | 200 | 2026-09-21 02:32 | | Discord Labs | 🟢 live | 530 ms | 200 | 2026-09-21 02:32 | | Bots on Discord | 🟢 live | 1593 ms | 200 | 2026-09-21 02:32 | | Carbonitex | 🟢 live | 1140 ms | 200 | 2026-09-21 02:32 | | Cybralist | 🟢 live | 554 ms | 200 | 2026-09-21 02:32 | | Discord Bots | 🟢 live | 747 ms | 200 | 2026-09-21 02:32 | | discord.place | 🟢 live | 157 ms | 403 | 2026-09-21 02:32 | | Rovel Discord List | 🟢 live | 1546 ms | 200 | 2026-09-21 02:32 | | Discord Bot List | 🟢 live | 671 ms | 200 | 2026-09-21 02:32 | | Discord Bot List XYZ | 🟢 live | 814 ms | 200 | 2026-09-21 02:32 | | Discord Extreme List | 🟢 live | 1225 ms | 200 | 2026-09-21 02:32 | | DiscordForge | 🟢 live | 524 ms | 200 | 2026-09-21 02:32 | | dlist.gg | 🟢 live | 473 ms | 200 | 2026-09-21 02:32 | | Bots for Discord | 🟢 live | 463 ms | 200 | 2026-09-21 02:32 | | Fluxpoint Discover | 🟢 live | 1072 ms | 200 | 2026-09-21 02:32 | | Disforge | 🟢 live | 2384 ms | 200 | 2026-09-21 02:33 | | DisQ | 🟢 live | 1225 ms | 200 | 2026-09-21 02:32 | | DList.Space | 🟢 live | 235 ms | 200 | 2026-09-21 02:32 | | JustDiscord | 🟢 live | 966 ms | 200 | 2026-09-21 02:32 | | Omniplex | 🟢 live | 1156 ms | 200 | 2026-09-21 02:32 | | Radarcord | 🟢 live | 1226 ms | 200 | 2026-09-21 02:32 | | Stellar Bot List | 🟢 live | 971 ms | 200 | 2026-09-21 02:32 | | Discord Bot List | 🟢 live | 158 ms | 403 | 2026-09-21 02:32 | | TopBot | 🟢 live | 707 ms | 200 | 2026-09-21 02:32 | | vCodes | 🟢 live | 931 ms | 200 | 2026-09-21 02:32 | | Void Bots | 🟢 live | 571 ms | 200 | 2026-09-21 02:32 | | Yet Another Bot List | 🟢 live | 167 ms | 200 | 2026-09-21 02:32 |

The table above is regenerated hourly by scripts/status-sync.ts (workflow: status-sync.yml). A list is marked:

  • 🟢 live: website answered with HTTP < 500.
  • 🟡 deprecated: superseded or announced end of life.
  • 🔴 shutdown: unreachable, or domain is parked/dead.

When a list turns deprecated or shutdown, the workflow opens a pull request that removes it from the post registry, so a human always approves registry changes. Pure latency/uptime refreshes land on main directly.

Testing

bun install
bun test                    # unit tests, no network
bun scripts/test-live.ts    # live integration, reads .env (optional)

Everything also runs in Docker (the pinned oven/bun:1 image - no local toolchain needed):

docker compose -f docker-compose.test.yml run --rm tests   # typecheck + full suite
docker compose -f docker-compose.test.yml run --rm build   # tsc build
TOPGG_TOKEN=... TOPGG_WEBHOOK_SECRET=... \
  docker compose -f docker-compose.test.yml run --rm live  # real top.gg round-trip

For the live test: cp .env.example .env, fill in tokens for the lists you use (all optional), and run bun scripts/test-live.ts. It fetches real data, posts real stats (server count 1) and simulates a vote webhook, printing PASS/FAIL per action. Lists without tokens are skipped, nothing hard fails. scripts/verify-topgg.mjs is the production-style check: real stats POST + round-trip, a genuinely signed top.gg v1 delivery through the ingest path, and announcer validation - TOPGG_TOKEN=... TOPGG_WEBHOOK_SECRET=... node scripts/verify-topgg.mjs <serverCount>.

Registry maintenance

# refresh the BotBlock snapshot and regenerate the registry
curl -s https://botblock.org/api/lists > scripts/snapshot/botblock-lists.json
python3 scripts/snapshot/build_lists.py
bun test   # verify nothing broke

Contributing

PRs welcome; see CONTRIBUTING.md for setup, commands and conventions. In short:

  1. Add or fix list data in scripts/snapshot/build_lists.py (manual overrides), not the generated file.
  2. Run bun test and bun run lint.
  3. Keep the zero-dependency promise: no new runtime deps.

Found a vulnerability? Please report privately: see SECURITY.md.

Docs & links

License

Licensed under the Apache License 2.0 with the Commons Clause: free to fork, modify, use, and build around; not to be sold as a product. See LICENSE; the LICENSE file is authoritative. Botlist names and trademarks belong to their respective owners; see NOTICE.md.


Built by PotenFYR Studios · potenfyr.in · Part of the PotenFYR Studios open-source ecosystem.


⭐ Star History

Every public PotenFYR Studios repository on one live chart, served by star-history.com.

Contributing

Contributions make the open-source community such an amazing place to learn, inspire and create. Any contributions you make are greatly appreciated - see CONTRIBUTING.md and the good first issues. Security concerns: please use SECURITY.md (private vulnerability reporting), not public issues.