discord-botlists.

copy · paste · ship

Examples

Real recipes against the real API surface. Every option shown here exists in the shipped types: check the full reference for the rest.

Reward voters instantly

Give a role the second someone votes on any list. vote.voterId is normalized across all 33 registries.

reward-voters.ts

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

// inside your discord.js client setup
const lists = new Botlists({
  client,
  webhook: { port: 8080, secret: process.env.WEBHOOK_SECRET },
});

await lists.startWebhooks();

lists.on('vote', async (vote) => {
  if (vote.isTest) return;            // dashboard test button
  const member = await guild.members.fetch(vote.voterId!).catch(() => null);
  if (member) await member.roles.add('VOTER_ROLE_ID');
  if (vote.weekend) {
    // weekend multiplier: vote.weight tells you how many entries to grant
    await member?.roles.add('DOUBLE_VOTER_ROLE_ID');
  }
});

Post stats without discord.js

statsProvider gives full control: any framework, custom sharding, or a plain cron job with no client at all.

framework-less.ts

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

const lists = new Botlists({
  botId: '432161800760442880',
  tokens: {
    'top.gg': process.env.TOPGG_TOKEN,
    'botlist.me': process.env.BOTLISTME_TOKEN,
  },
  statsProvider: async () => ({
    serverCount: await shardManager.fetchTotalGuilds(),
    shardCount: shardManager.count,
    shards: await shardManager.fetchPerShardGuilds(),
  }),
});

// every 30 minutes, minimum 60s, timer is unref'd
lists.startAutoPost(30 * 60 * 1000);

lists.on('statsPosted', (report) => {
  console.log(`posted to ${report.posted}, failed ${report.failed}, skipped ${report.skipped}`);
});

Webhooks inside Express or Fastify

Already run an HTTP server? Skip the built-in one and feed bodies straight into ingestWebhook.

express-ingest.ts

import express from 'express';
import { Botlists } from '@potenfyrstudios/discord-botlists';

const lists = new Botlists({
  botId: process.env.BOT_ID,
  webhook: { secret: process.env.WEBHOOK_SECRET }, // never started, used for parsing
});

const app = express();
app.use(express.json());

app.post('/webhooks/:list', (req, res) => {
  // the last segment identifies the list: /webhooks/top.gg
  lists.ingestWebhook(req.params.list, req.body);
  res.sendStatus(200);
});

app.listen(3000);

Per-list posting with filters

Post to every tokened list, or slice it with only/skip. Each PostResult carries the HTTP status and Retry-After.

post-report.ts

const report = await lists.postStats(
  { serverCount: client.guilds.cache.size },
  { skip: ['bots.ondiscord.xyz'] },   // or { only: ['top.gg', 'botlist.me'] }
);

for (const result of report.results) {
  if (!result.ok && !result.error?.startsWith('no token')) {
    console.error(`${result.listName}: ${result.error}`);
    if (result.retryAfter) console.log(`  retry after ${result.retryAfter}s`);
  }
}

// one list by id, name, hostname or shorthand ('radarcord', 'voidbots', ...)
await lists.postStatsTo('radarcord', { serverCount: 120 });

// one request to botblock.org fans out to every list (their limit: 1 per 120s)
await lists.postViaBotBlock({ serverCount: 120 });

Read normalized data from any list

fetchBot returns UniversalBot everywhere. Votes, ratings, invite, prefix: same shape on every list, raw payload kept.

read-data.ts

const bot = await lists.fetchBot('discord.bots.gg', '557628352828014614');
bot.name;         // 'Ticket Tool'
bot.serverCount;  // number | null
bot.votes;        // number | null
bot.ratings;      // { average, count }
bot.raw;          // the untouched API response

const votes = await lists.fetchVotes('top.gg');        // number | null
const voted = await lists.hasVoted('top.gg', userId);  // boolean | null
const found = await lists.searchBots('botlist.me', 'music', 5);

lists.widgetUrl('top.gg');   // widget image url
lists.viewBotUrl('top.gg');  // listing page url

Health cron for the registry

Probe every list, print the unicode status table, and get prefilled GitHub issue URLs for dead lists.

status-cron.ts

const board = await lists.checkAndReportStatus();
board.summary;   // { live: 33, deprecated: 0, shutdown: 0, unknown: 0 }
board.entries;   // [{ listId, state, httpStatus, latencyMs, ... }]

lists.on('status', (b) => {
  if (b.summary.shutdown > 0) {
    // dead lists are skipped automatically by postStats
    notifyChannel(`${b.summary.shutdown} list(s) unreachable`);
  }
});

Self-hosted or missing list

One BotlistRecord and a custom list works everywhere built-in ones do: posting, webhooks, parsing, probing.

custom-list.ts

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

Typed event wiring

One EventEmitter carries every realtime signal. EVENTS is exported if you want exhaustiveness checks.

events.ts

import { EVENTS } from '@potenfyrstudios/discord-botlists';

lists.on('comment', (c) => console.log(c.userName, 'commented:', c.content));
lists.on('review', (r) => console.log(`review ${r.rating}/5 on ${r.listName}`));
lists.on('rating', (r) => track(r.listId, r.rating));
lists.on('test', (v) => console.log('test vote from', v.listId));
lists.on('raw', (p) => console.log(p.listId, p.event));
lists.on('error', (err) => console.error(err.message));

// 'vote' | 'comment' | 'review' | 'rating' | 'test' | 'statsPosted'
// 'status' | 'error' | 'request' | 'raw'
console.log(EVENTS);