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 urlHealth 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);