vote-tracker
v8.0.1
Published
Track, reward, and query Top.gg votes for Discord bots
Maintainers
Readme
Vote Tracker
A professional Top.gg vote system for Discord.js bots. Receive signed vote events, reward members, send polished Discord cards, expose leaderboards, query the Top.gg API, and automatically post bot statistics.
Contents
- Features
- Requirements
- Installation
- Quick start with Top.gg v1
- Top.gg dashboard setup
- Discord interface
- Vote rewards and reminders
- Vote statistics
- Top.gg v1 API methods
- Automatic bot-stat posting
- Legacy v0 webhooks
- Configuration reference
- Events
- Public methods
- TypeScript
- Security
- Troubleshooting
- Contributing
- License
Features
Current Top.gg support
- Top.gg v1
vote.createandwebhook.testevents - HMAC SHA-256 verification over the untouched request body
- Constant-time signature comparison
- Configurable replay-protection window
- Nested v1 payload normalization
- Legacy v0 Authorization-header webhooks through
@top-gg/sdk - Automatic selection between v1 and v0 when
webhookVersionis omitted - Duplicate v1 delivery protection using the Top.gg vote ID
- Weekend and multiplier vote weights
- Exact
created_atandexpires_athandling - Immediate webhook acknowledgement followed by background Discord processing
Professional Discord interface
- Branded vote cards using Discord embeds
- Special gold weekend-boost presentation
- Voter avatar, vote power, streak, support points, and next-vote timestamp
- Native Vote on Top.gg and View bot profile buttons
- Optional Support server button
- Optional banner image, custom colors, title, and footer
- Safe mention defaults that prevent unexpected notification pings
- Channel or Discord webhook delivery
- Optional voter mention
- Optional
/votecommand center - Ephemeral command responses by default
- Optional dynamic Discord presence with live vote totals
Rewards and community features
- Optional voter role assignment
- Automatic role removal when the vote expires
- Vote-again direct-message reminders
- Exact v1 reminder timing from Top.gg
- Configurable legacy reminder delay
- Weighted per-user totals
- Vote streak tracking
- Local leaderboard
- Overall and per-user HTTP statistics
- Optional Bearer protection for statistics routes
API and operations
- Fetch the authenticated Top.gg project
- Check a Discord or Top.gg user's active vote
- Read cursor-paginated vote history
- Automatically post Discord server and shard counts
- Health endpoint
- Custom Express application support
- Custom HTTP server support
- Configurable host, port, webhook route, and JSON body limit
- Graceful shutdown
- CommonJS and TypeScript declarations
Requirements
- Node.js 18.18.2 or newer
- Discord.js 14
- A Discord bot with the
Guildsintent - The
GuildMembersintent when assigning or removing voter roles - A public HTTPS URL that Top.gg can reach
Installation
npm install vote-trackerSecrets should be stored in environment variables:
DISCORD_TOKEN=your-discord-bot-token
TOPGG_WEBHOOK_SECRET=whs_your_webhook_secret
TOPGG_API_TOKEN=your_topgg_api_token
STATS_TOKEN=a_private_token_for_stats_routesQuick start with Top.gg v1
const { Client, GatewayIntentBits } = require("discord.js");
const VoteTracker = require("vote-tracker");
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
],
});
const tracker = new VoteTracker(client, {
guildId: "YOUR_GUILD_ID",
channelId: "YOUR_VOTE_LOG_CHANNEL_ID",
roleId: "OPTIONAL_VOTER_ROLE_ID",
webhookVersion: "v1",
webhookSecret: process.env.TOPGG_WEBHOOK_SECRET,
webhookPath: "/dblwebhook",
port: 3000,
apiToken: process.env.TOPGG_API_TOKEN,
reminder: true,
stats: {
enabled: true,
endpoint: "/stats",
authToken: process.env.STATS_TOKEN,
leaderboardSize: 10,
streakWindowHours: 14,
},
discordUi: {
accentColor: "#5865F2",
weekendColor: "#F0B232",
title: "A new supporter appeared!",
footer: "Powered by Top.gg - Thank you for supporting us",
supportUrl: "https://discord.gg/your-server",
commands: {
enabled: true,
ephemeral: true,
leaderboardSize: 10,
},
presence: {
enabled: true,
text: "{votes} votes from {voters} supporters - /vote",
status: "online",
},
},
});
tracker.on("ready", ({ port, webhookPath }) => {
console.log(`Vote webhook ready on port ${port}${webhookPath}`);
});
tracker.on("vote", ({ user, vote, stats }) => {
console.log(`${user.username}'s vote counted ${vote.weight}x`, stats);
});
tracker.on("webhookTest", ({ traceId }) => {
console.log("Verified Top.gg test event:", traceId);
});
tracker.on("commandsReady", ({ command }) => {
console.log(`Registered /${command.name}`);
});
tracker.on("statsPosted", () => {
console.log("Posted bot statistics to Top.gg");
});
tracker.on("error", console.error);
client.once("ready", () => {
tracker.init();
});
client.login(process.env.DISCORD_TOKEN);Top.gg dashboard setup
- Open the project dashboard on Top.gg.
- Open Webhooks.
- Add
https://your-domain.example/dblwebhook. - Subscribe to
vote.createandwebhook.test. - Copy the generated secret beginning with
whs_. - Store it as
TOPGG_WEBHOOK_SECRET. - Send a test event from the dashboard.
- Listen for
webhookTestto confirm the signed request arrived.
The URL path must match webhookPath. The server must be accessible over public HTTPS; localhost cannot be called by Top.gg.
Discord interface
Vote cards
Every accepted vote produces a Discord message containing:
- The voter and their avatar
- Standard or weekend-boost vote power
- Weighted lifetime support recorded by this process
- The current streak
- A Discord relative timestamp for the next eligible vote
- Direct links to vote and view the bot on Top.gg
- An optional support-server link
The interface is enabled by default. Disable it with:
discordUi: falseBranding
discordUi: {
accentColor: "#5865F2",
weekendColor: "#F0B232",
title: "Thanks for voting!",
footer: "Your Community - Powered by Top.gg",
bannerUrl: "https://example.com/banner.png",
supportUrl: "https://discord.gg/example",
mentionVoter: false,
showAvatar: true,
showStats: true,
buttons: true
}mentionVoter: false still displays the voter inside the embed. Vote messages use allowedMentions so embed text does not accidentally notify members.
Channel mode
Channel mode sends the vote card through the bot:
postmode: "channel",
channelId: "VOTE_LOG_CHANNEL_ID"Webhook mode
Webhook mode sends the same card and link buttons through a Discord webhook:
postmode: "webhook",
webhook: "https://discord.com/api/webhooks/..."The webhook name and avatar are updated to match the bot's vote tracker branding.
Slash-command vote center
Slash commands are opt-in:
discordUi: {
commandName: "vote",
commands: true
}Or configure them:
discordUi: {
commands: {
enabled: true,
ephemeral: true,
leaderboardSize: 10
}
}The package registers one guild command:
/vote dashboarddisplays the caller's vote eligibility, points, streak, and voting link./vote leaderboarddisplays the highest local weighted vote totals./vote stats [user]displays a selected member's vote dashboard.
When apiToken is configured, dashboard and member-stat responses also check the current vote through the Top.gg v1 API. Commands require the applications.commands scope, which is normally included when a Discord bot is invited.
Dynamic presence
discordUi: {
presence: {
enabled: true,
text: "{votes} votes from {voters} supporters - /vote",
status: "online"
}
}Available placeholders:
| Placeholder | Value |
| --- | --- |
| {votes} | Weighted vote total |
| {voters} | Unique local voter count |
The presence updates at startup and after each processed vote. A Discord.js ActivityType may be passed with type.
Vote rewards and reminders
Voter role
Set roleId to assign a guild role when a vote is processed:
roleId: "VOTER_ROLE_ID"The bot must have Manage Roles, and its highest role must be above the voter role. The role is removed when the reminder timer expires, whether or not direct-message reminders are enabled.
Vote reminders
reminder: trueFor v1 events, the timer uses the exact Top.gg expires_at value. For v0 events, it uses reminderDelayHours, which defaults to 12 hours.
At expiry:
- The package attempts to send the voter a direct-message reminder when
reminderis enabled. - The configured voter role is removed.
Users may block direct messages; failures are logged without crashing the tracker.
Vote statistics
Statistics are kept in memory. They reset whenever the Node.js process restarts.
Weighted totals
Top.gg's weight is respected:
- A standard vote adds
1tototalVotesand the user'scount. - A double weekend vote adds
2. voteEventsand the user'seventsalways count deliveries, not vote weight.
Streaks
A vote continues a streak when it arrives inside stats.streakWindowHours, which defaults to 14 hours.
HTTP endpoints
stats: {
enabled: true,
endpoint: "/stats",
authToken: process.env.STATS_TOKEN,
leaderboardSize: 10,
streakWindowHours: 14
}The following routes are registered:
| Route | Response |
| --- | --- |
| GET /stats | Overall totals and unique voter count |
| GET /stats/leaderboard?limit=10 | Top voters; limit is capped at 100 |
| GET /stats/users/:id | One user's local statistics |
When authToken is configured:
Authorization: Bearer YOUR_STATS_TOKENOverall response:
{
"totalVotes": 42,
"voteEvents": 37,
"uniqueVoters": 21
}User response:
{
"userId": "123456789012345678",
"count": 5,
"events": 4,
"lastVoteAt": 1783072800000,
"streak": 3
}Health endpoint
GET /health is always available:
{
"status": "ok",
"webhookVersion": "v1",
"uptime": 120.5
}Top.gg v1 API methods
Set apiToken to enable the Bearer-authenticated API client:
apiToken: process.env.TOPGG_API_TOKENFetch the project
const project = await tracker.getProject();
console.log(project.name, project.votes, project.votes_total);Check a vote
Discord user ID:
const vote = await tracker.getVote("DISCORD_USER_ID");Top.gg user ID:
const vote = await tracker.getVote("TOPGG_USER_ID", "topgg");getVote() returns null when the user has no active vote.
Read vote history
The first request requires a date no more than one year in the past:
const page = await tracker.getVotes({
startDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
});Use the returned cursor for the next page:
const nextPage = await tracker.getVotes({
cursor: page.cursor,
});Failed API requests throw TopggApiError with:
message: Top.gg's human-readable errorstatus: HTTP statusdetails: parsed problem response
Automatic bot-stat posting
Enable server and shard count posting:
apiToken: process.env.TOPGG_API_TOKEN,
autoPost: trueOr configure the current topgg-autoposter package:
autoPost: {
interval: 30 * 60 * 1000,
postOnStart: true,
startPosting: true
}Top.gg requires an interval of at least 15 minutes. Successful posts emit statsPosted; failures emit error.
Legacy v0 webhooks
Existing v0 integrations remain supported:
const tracker = new VoteTracker(client, {
guildId: "YOUR_GUILD_ID",
channelId: "YOUR_CHANNEL_ID",
webhookVersion: "v0",
password: process.env.TOPGG_LEGACY_WEBHOOK_PASSWORD
});For legacy webhooks, password must match the Authorization value configured in the Top.gg dashboard.
When webhookVersion is omitted:
webhookSecretselects v1.- A
passwordbeginning withwhs_selects v1. - Any other
passwordselects v0.
Configuration reference
Main options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| guildId | string | required | Discord guild that receives rewards and commands |
| channelId | string | none | Vote-log channel |
| roleId | string | none | Temporary voter role |
| postmode | "channel" \| "webhook" | "channel" | Vote-card delivery mode |
| webhook | string | none | Discord webhook URL for webhook post mode |
| color | ColorResolvable | #333333 | Legacy color and Discord UI accent fallback |
| port | number | 3000 | HTTP port; use 0 to select a free port |
| host | string | system default | HTTP listen address |
| reminder | boolean | true | Send a vote-again DM |
| reminderDelayHours | number | 12 | Legacy v0 reminder delay |
| app | Express Application | new app | Existing Express app |
| server | HTTP Server | new server | Existing HTTP server |
Webhook options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| webhookVersion | "v0" \| "v1" | auto | Top.gg webhook generation |
| webhookSecret | string | none | v1 whs_... signing secret |
| password | string | none | Legacy v0 Authorization secret; accepted as a v1 fallback |
| webhookPath | string | /dblwebhook | Incoming Top.gg route |
| signatureToleranceSeconds | number | 300 | Maximum accepted v1 signature age; Infinity disables the age check |
| bodyLimit | string | 100kb | Express request-body limit |
API and auto-post options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| apiToken | string | none | Top.gg API token |
| apiBaseUrl | string | https://top.gg/api/v1 | API base URL, mainly for testing or proxies |
| fetch | function | global fetch | Custom fetch-compatible implementation |
| autoPost | boolean \| object | false | Server/shard count auto-poster |
Statistics options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| stats.enabled | boolean | true | Collect local statistics |
| stats.leaderboardSize | number | 10 | Default leaderboard size |
| stats.streakWindowHours | number | 14 | Maximum gap that continues a streak |
| stats.endpoint | string | none | Enables statistics HTTP routes |
| stats.authToken | string | none | Protects statistics routes with Bearer authentication |
Discord UI options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| discordUi.enabled | boolean | true | Enable professional Discord cards |
| discordUi.accentColor | ColorResolvable | #5865F2 | Standard card color |
| discordUi.weekendColor | ColorResolvable | #F0B232 | Multiplier card color |
| discordUi.title | string | package title | Vote-card title |
| discordUi.footer | string | package footer | Card footer |
| discordUi.bannerUrl | string | none | Vote-card image |
| discordUi.supportUrl | string | none | Optional support-server button |
| discordUi.mentionVoter | boolean | false | Add voter mention content without generating a ping |
| discordUi.showAvatar | boolean | true | Show voter thumbnail |
| discordUi.showStats | boolean | true | Show points and streak |
| discordUi.buttons | boolean | true | Show link buttons |
| discordUi.commandName | string | vote | Slash-command name |
| discordUi.commands | boolean \| object | false | Enable and configure slash commands |
| discordUi.presence | boolean \| object | false | Enable and configure live presence |
Events
ready
The HTTP server is listening:
tracker.on("ready", ({ port, webhookPath }) => {});vote
A verified vote was normalized, its user was fetched, and statistics were recorded:
tracker.on("vote", ({ user, guild, vote, stats }) => {});Normalized vote fields:
{
id,
user,
bot,
type,
isWeekend,
weight,
createdAt,
expiresAt,
sourceVersion,
raw
}webhookTest
A signed Top.gg v1 test event was accepted:
tracker.on("webhookTest", ({ payload, traceId }) => {});commandsReady
The optional guild slash command was registered:
tracker.on("commandsReady", ({ command }) => {});statsPosted
The auto-poster successfully submitted server statistics:
tracker.on("statsPosted", (data) => {});error
Discord processing, command registration, Top.gg API, or auto-poster work failed:
tracker.on("error", (error) => {
console.error(error);
});Without an error listener, Vote Tracker logs operational errors to the console.
Public methods
| Method | Result |
| --- | --- |
| init() | Registers routes/features, starts auto-posting, and listens; returns the tracker |
| stop() | Stops auto-posting and reminders, removes the interaction listener, and closes the HTTP server |
| getStats() | Returns overall in-memory statistics |
| getUserStats(userId) | Returns one user's statistics or null |
| getLeaderboard(limit?) | Returns ranked local user statistics |
| getProject() | Fetches the authenticated Top.gg project |
| getVote(userId, source?) | Returns active vote information or null |
| getVotes({ startDate }) | Fetches the first vote-history page |
| getVotes({ cursor }) | Fetches the next vote-history page |
| startAutoPoster() | Starts or returns the configured Top.gg auto-poster |
Graceful shutdown:
async function shutdown() {
await tracker.stop();
client.destroy();
}
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);TypeScript
The package includes declarations for options, events, normalized votes, statistics, and Top.gg API responses.
import { Client, GatewayIntentBits } from "discord.js";
import VoteTracker, {
TopggApiError,
VoteTrackerOptions,
} from "vote-tracker";
const client = new Client({
intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers],
});
const options: VoteTrackerOptions = {
guildId: process.env.GUILD_ID!,
webhookVersion: "v1",
webhookSecret: process.env.TOPGG_WEBHOOK_SECRET!,
};
const tracker = new VoteTracker(client, options);
try {
await tracker.getProject();
} catch (error) {
if (error instanceof TopggApiError) {
console.error(error.status, error.details);
}
}Security
- V1 signatures are verified before JSON parsing.
- The signed value is
{timestamp}.{rawBody}. - HMAC SHA-256 signatures use constant-time comparison.
- Requests outside the configured timestamp tolerance are rejected.
- Duplicate v1 vote IDs are ignored during their in-memory retention window.
- Statistics routes can require Bearer authentication.
- Discord output disables automatic mention parsing.
- Tokens and secrets should never be committed.
- Production deployments should use HTTPS and a reverse proxy with appropriate request limits.
Security vulnerabilities should not be posted publicly. Follow SECURITY.md for responsible reporting.
Troubleshooting
Top.gg returns an invalid signature response
- Confirm that
webhookSecretis the v1 secret beginning withwhs_. - Do not use the API token as the webhook secret.
- Ensure a proxy is not rewriting the request body.
- Check that the server clock is synchronized.
No vote card appears
- Confirm the bot can view and send messages in
channelId. - Confirm
postmodematches the configured channel or Discord webhook. - Add an
errorlistener. - Check that the Discord client is ready before calling
init().
The voter role is not assigned
- Enable the
GuildMembersintent. - Grant Manage Roles.
- Move the bot role above the voter role.
- Confirm
roleIdbelongs toguildId.
Slash commands do not appear
- Enable
discordUi.commands. - Ensure the bot was invited with
applications.commands. - Confirm the configured
guildIdis correct. - Listen for
commandsReadyanderror.
The Top.gg API methods throw immediately
getProject(), getVote(), and getVotes() require apiToken.
Contributing
Contributions are welcome. Read CONTRIBUTING.md for:
- Development setup
- Bug and feature proposal workflow
- Code and test expectations
- Pull-request requirements
- Security reporting
Use the repository's GitHub issue forms for bugs and feature requests.
License
Vote Tracker is released under the MIT License.
