Skip to content

Plugin Reference

Complete reference for every plugin that ships with popii-framework. All plugins are imported from "popii-framework" and passed to the plugins array of popiiClient().

PluginDepends on
sqlitePlugin
mongoosePlugin
webPlugin
uiPlugin
errorHandlerPlugin
permissionGuardPlugin
reloadPlugin
commandLoggerPlugin
commandAnalyticPlugin
telemetryPlugin
activityRotatorPlugin
popiiAiPlugin
autoModPlugin— (AI analysis optionally uses popiiAiPlugin)
levelsPluginsqlitePlugin or mongoosePlugin
economyPluginsqlitePlugin or mongoosePlugin
gamesPluginsqlitePlugin or mongoosePlugin (optional economyPlugin)
moderationPluginsqlitePlugin
voicePlugin
canvasPlugin
deskPluginsqlitePlugin, uiPlugin
lastFmPluginsqlitePlugin, webPlugin
giveawayPluginsqlitePlugin
payPlugin
captchaPlugin
welcomePlugin
autorolePlugin
starboardPluginsqlitePlugin
pollPluginsqlitePlugin
rssPlugin
notifyPluginsqlitePlugin or mongoosePlugin
remindersPluginsqlitePlugin or mongoosePlugin
snapshotsPluginsqlitePlugin or mongoosePlugin
webhookPlugin
gdprPluginsqlitePlugin
sandboxPlugin
pluginManagerPlugin

Injects a Bun SQLite database into every pop context as pop.db. Most built-in data plugins depend on this.

plugins: [
sqlitePlugin({ filename: "mybot.db" }),
]

Options

OptionTypeDefaultDescription
filenamestring"popii.db"SQLite file path, relative to process.cwd().

Context additions

pop.db // Bun.Database

Opens in WAL mode with NORMAL synchronous writes. Creates the custom_commands table on first run. Priority: 100.


Connects Mongoose to MongoDB and exposes the connection on pop.mongoose.

plugins: [
mongoosePlugin({ uri: process.env.MONGODB_URI! }),
]

Peer dependency: bun add mongoose

Options

OptionTypeRequiredDescription
uristringMongoDB connection string.
...restConnectOptionsAny additional Mongoose connect options.

Context additions

pop.mongoose // typeof mongoose

Priority: 100.


XP leveling and virtual currency. Awards XP automatically on every message, with a configurable cooldown.

Requires: sqlitePlugin()

plugins: [
sqlitePlugin(),
economyPlugin({
xpPerMessage: [15, 25],
cooldownMs: 60_000,
currencyName: "Gold",
onLevelUp(userId, newLevel, msg) {
msg.channel.send(`<@${userId}> reached level ${newLevel}!`);
},
}),
]

Options

OptionTypeDefaultDescription
xpPerMessagenumber | [min, max][15, 25]XP per message. A tuple picks a random value in the range.
cooldownMsnumber60_000Minimum ms between XP awards per user.
currencyNamestring"Coins"Display name for the virtual currency.
onLevelUp(userId, level, msg) => voidCallback fired on level-up.

Context additions

pop.economy.currencyName
pop.economy.getProfile(userId) // Promise<{ xp, level, balance, last_daily } | null>
pop.economy.addXp(userId, amount) // Promise<boolean> — true if the user leveled up
pop.economy.addBalance(userId, amount) // Promise<void>

Level formula: threshold for level N = 5N² + 50N + 100

Dashboard settings

KeyDescription
economyBlacklistedChannelsChannel IDs where XP is disabled (comma-separated).
levelUpChannelChannel for level-up announcements.

Slash commands for warn, kick, ban, timeout, and case history. Case records are stored in SQLite.

Requires: sqlitePlugin()

plugins: [
sqlitePlugin(),
moderationPlugin({ defaultReason: "No reason provided." }),
]

Options

OptionTypeDefaultDescription
defaultReasonstring"No reason provided."Reason string used when the moderator omits it.

Commands registered: /warn, /kick, /ban, /unban, /timeout, /cases, /case

Dashboard settings

KeyDescription
modLogChannelChannel where moderation actions are logged.

Wraps every command and snap with error-handling middleware. Turns PopiiError throws into ephemeral user-facing replies and silently drops PopiiSilentError.

import { errorHandlerPlugin, PopiiError, PopiiSilentError } from "popii-framework";
plugins: [ errorHandlerPlugin() ]
// In a command:
throw new PopiiError("You can't do that.", "You need the Moderator role.");
throw new PopiiSilentError(); // no reply sent

Re-applies automatically on hot-reload. Other error types are re-thrown. Priority: 95.


High-level interactive components: paginated embeds, button prompts, modal forms, and multi-step wizards.

plugins: [ uiPlugin({ successColor: 0x57F287 }) ]

Options

OptionTypeDefaultDescription
successColornumber0x57F287Embed color for pop.success().
errorColornumber0xED4245Embed color for pop.error().
infoColornumber0x5865F2Embed color for pop.info().
useSnapsForPaginationbooleanfalseUse persistent Snaps instead of short-lived collectors for pagination buttons.

Context additions

// Status replies
pop.success(message) // ephemeral green embed
pop.error(message) // ephemeral red embed
pop.info(message) // ephemeral blue embed
// Paginate through embeds
pop.paginate(source, timeoutMs?)
// source: EmbedBuilder[] or { fetch: (page) => Promise<EmbedBuilder | null>; totalPages? }
// Button prompt — returns the value of the clicked button, or null on timeout
pop.prompt(embed, buttons, timeoutMs?)
// Yes/no confirmation
pop.confirm(question, timeoutMs?) // Promise<boolean>
// Modal form — returns field values keyed by name, or null on timeout
pop.form(title, fields, timeoutMs?)
// Wait for a follow-up message
pop.awaitMessage(timeoutMs?, promptMessageId?) // Promise<Message | null>
// Multi-step wizard — returns all collected answers, or null if cancelled
pop.wizard(steps, timeoutMs?)
// Per-user session (in-memory, keyed by user + channel)
pop.session.get(key)
pop.session.set(key, value, ttlMs?)
pop.session.del(key)
pop.session.clear()

FormField

interface FormField {
name: string;
label: string;
paragraph?: boolean; // multi-line textarea
required?: boolean;
value?: string; // pre-filled
placeholder?: string;
}

WizardStep

interface WizardStep {
id: string;
prompt: string | EmbedBuilder;
type: "text" | "buttons" | "select";
options?: string[] | { label: string; value: string }[];
}

Consecutive text steps are batched into a single modal (up to 5 fields). Priority: 90.


Starts a Bun HTTP server with a web dashboard, server settings pages, health check, and REST API. Supports Discord OAuth2.

plugins: [
webPlugin({
port: 3000,
publicUrl: "https://mybot.example.com",
dashboard: true,
oauth2: {
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
redirectUri: process.env.REDIRECT_URI!,
},
}),
]

Options

OptionTypeDefaultDescription
portnumber3000HTTP server port.
publicUrlstringPublicly accessible URL (required for OAuth2).
dashboardbooleanfalseEnable the owner dashboard at /dashboard.
dashboardPathstring"/dashboard"Custom dashboard path.
dashboardPasswordstringBasic Auth password as an alternative to OAuth2.
oauth2.clientIdstringDiscord application client ID.
oauth2.clientSecretstringDiscord application client secret.
oauth2.redirectUristringOAuth2 callback URI (register this in the developer portal).
structuredLogsbooleanfalseEmit JSON log lines instead of plain text.

Built-in routes

PathDescription
GET /health{ status, uptime, guilds, ping }
GET /metricsPrometheus-compatible metrics
GET /inviteBot invite link
GET /commandsPublic slash command list
GET /servers/:guildIdPer-server settings (requires Administrator)
GET /dashboardOwner dashboard
GET /membersMember lookup by ID (owners only)

Music bot functionality: YouTube/SoundCloud/Spotify playback, queue management, SponsorBlock, and now-playing card generation.

plugins: [
voicePlugin({
sponsorBlock: true,
defaultVolume: 80,
}),
]

Peer dependencies: bun add @discordjs/voice ffmpeg-static opusscript

Options

OptionTypeDefaultDescription
sponsorBlockbooleanfalseAuto-skip SponsorBlock segments.
defaultVolumenumber100Playback volume (0–200).
maxQueueSizenumber500Maximum tracks in a queue.
leaveOnEmptybooleantrueLeave when the queue finishes.
leaveOnEmptyDelaynumber30_000Delay (ms) before leaving an empty channel.
poTokenstringStatic YouTube po_token for anti-bot bypass.
visitorDatastringvisitor_data string paired with poToken.
poTokenProviderstringURL returning { poToken, visitorData? }. Auto-refreshed.
poTokenRefreshMsnumber3_600_000Refresh interval for poTokenProvider.

Context additions

pop.joinVoice() // Promise<VoiceConnection>
pop.leaveVoice() // Promise<void>
pop.playAudio(urlOrStream) // Promise<{ player, title, metadata? }>
pop.getQueue() // QueuedTrack[]
pop.getNowPlaying() // { title, artist, duration, elapsed, elapsedMs, progress, thumbnail?, requestedBy? } | null
pop.getNowPlayingCard(opts?) // Promise<AttachmentBuilder | null>
pop.sponsorblock.isEnabled()
pop.sponsorblock.setEnabled(enabled)
pop.sponsorblock.getCategories()
pop.sponsorblock.setCategories(cats)
pop.sponsorblock.forceSkip() // Promise<boolean>

Support ticket system. Users click a button to open a private channel; transcripts are saved on close.

Requires: sqlitePlugin(), uiPlugin()

plugins: [
sqlitePlugin(),
uiPlugin(),
deskPlugin({
panelTitle: "🎫 Support",
panelMessage: "Click below to open a ticket.",
}),
]

Options

OptionTypeDescription
categoryIdstringDefault category for ticket channels.
supportRoleIdstringDefault support role.
panelTitlestringDefault panel embed title.
panelMessagestringDefault panel embed body.
transcriptChannelIdstringChannel for closed-ticket transcripts.

Context additions

pop.desk.sendPanel(channelId) // Promise<void> — posts the ticket-open button
pop.desk.closeTicket() // Promise<void> — closes the current ticket channel

Dashboard settings: deskPanelChannelId, deskCategoryId, deskSupportRoleId, deskTranscriptChannelId, deskPanelTitle, deskPanelMessage

One open ticket per user is enforced. Transcripts older than 30 days are pruned automatically.


AI text generation via OpenAI, Google Gemini, Anthropic, or DeepSeek. Required by autoModPlugin.

plugins: [
popiiAiPlugin({
provider: "anthropic",
apiKey: process.env.AI_API_KEY!,
model: "claude-sonnet-4-6",
systemPrompt: "You are a helpful Discord bot assistant.",
}),
]

Options

OptionTypeRequiredDescription
provider"openai" | "gemini" | "anthropic" | "deepseek"AI provider.
apiKeystringAPI key for the chosen provider.
modelstringModel ID. Defaults: gpt-4o-mini / gemini-1.5-flash / claude-sonnet-4-6 / deepseek-chat.
systemPromptstringDefault system prompt. Overridden per-server via the aiPersona dashboard setting.
localestringAppends a language instruction to every request.

Context additions

pop.ai.generate(prompt) // Promise<string>
pop.ai.chat(history) // Promise<string>
// history: { role: "system" | "user" | "assistant"; content: string }[]

Conversation history is capped at 20 non-system messages.


Rule-based message moderation with six configurable filters and optional AI analysis. All rules are configured per-server through the settings dashboard — no hardcoded thresholds.

Requires: nothing (AI analysis is optional and requires popiiAiPlugin() separately)

plugins: [
// Optional: load popii-ai if you want AI-powered analysis too
popiiAiPlugin({ provider: "openai", apiKey: process.env.AI_API_KEY! }),
autoModPlugin(),
]

Options (passed to the factory — for global limits only)

OptionTypeDefaultDescription
maxAiChecksPerMinutenumber10Max AI calls per guild per minute.
maxAiChecksPerUserPerMinutenumber3Max AI calls per user per minute.

Server settings

All rules are toggled and tuned in the server settings dashboard. Available categories:

All settings live in a single AutoMod tab:

SettingDescription
Enable AutoModMaster on/off switch
Log ChannelChannel to post moderation embeds
Exempt Roles / ChannelsComma-separated IDs that bypass all rules
Block Discord InvitesEnable invite filter + action + allowed codes
Enable Word FilterWord blocklist (comma-separated) + action
Enable Spam DetectionMessages-per-5-seconds threshold + action
Enable Mention SpamMax mentions per message + action
Enable Caps FilterCaps % threshold, minimum message length + action
Enable Link FilterBlock all links, allowed domains whitelist + action
Enable AI AnalysisToxicity/spam thresholds + action (requires popii-ai)

Available actions (selectable per rule):

ValueWhat happens
deleteDelete the message
delete_warnDelete and post a warning mentioning the user
delete_muteDelete and timeout the user for 10 minutes
delete_kickDelete and kick the user
delete_banDelete and ban the user

All triggered rules emit an embed to the configured log channel with the user, channel, action, reason, and message content.

Bot messages are always skipped. Rules run top-to-bottom; the first match stops processing further rules for that message.


Handles Patreon and Ko-fi donation webhooks with signature validation.

plugins: [
payPlugin({
patreon: {
webhookSecret: process.env.PATREON_SECRET!,
port: 8080,
onPledgeCreate: async (data, client) => { /* grant a role */ },
},
kofi: {
verificationToken: process.env.KOFI_TOKEN!,
port: 8081,
onPayment: async (data, client) => { /* thank the supporter */ },
},
}),
]

Patreon options: webhookSecret, port, path (/patreon/webhook), onPledgeCreate, onPledgeUpdate, onPledgeDelete

Ko-fi options: verificationToken, port, path (/kofi/webhook), onPayment


Runs an hCaptcha verification page. Users complete the CAPTCHA in their browser and receive a role.

plugins: [
captchaPlugin({
siteKey: process.env.HCAPTCHA_SITE_KEY!,
secretKey: process.env.HCAPTCHA_SECRET_KEY!,
port: 3001,
publicUrl: "https://verify.mybot.example.com",
}),
]

Options

OptionTypeRequiredDescription
siteKeystringhCaptcha site key.
secretKeystringhCaptcha secret key.
portnumberVerification server port.
publicUrlstringPublicly accessible URL (used to build the verify link).

Context additions

pop.captcha.sendVerificationPanel(channelId) // Promise<void>

Dashboard settings: captchaRoleId


Timed giveaways with button-based entry and random winner selection.

Requires: sqlitePlugin()

plugins: [ sqlitePlugin(), giveawayPlugin() ]
// In a command:
await pop.giveaways.start(channelId, "Discord Nitro", 1, 60 * 60 * 1000);

Context additions

pop.giveaways.start(channelId, prize, winnerCount, durationMs) // Promise<void>

Clicking the entry button again removes the entry. Winners are DM’d and announced in the channel.


Last.fm account linking, automatic scrobbling from voicePlugin, and listening stats.

Requires: sqlitePlugin(), webPlugin()

plugins: [
sqlitePlugin(),
webPlugin({ port: 3000, oauth2: { /* ... */ } }),
lastFmPlugin({
apiKey: process.env.LASTFM_API_KEY!,
apiSecret: process.env.LASTFM_API_SECRET!,
}),
]

Options

OptionTypeDefaultDescription
apiKeystringrequiredLast.fm API key.
apiSecretstringrequiredLast.fm API secret.
scrobblebooleantrueScrobble tracks from voicePlugin automatically.
nowPlayingbooleantrueSend “now playing” updates while a track plays.

Context additions

pop.lastfm.getAccount(userId) // Promise<{ username, sessionKey } | null>

Commands registered: /lastfm link, /lastfm unlink, /lastfm profile, /lastfm recent, /lastfm nowplaying, /lastfm toptracks, /lastfm topartists

Scrobble threshold: 50% of track duration or 4 minutes, whichever is less (minimum 30 seconds).


Fluent image generation API built on @napi-rs/canvas.

Peer dependency: bun add @napi-rs/canvas

plugins: [ canvasPlugin() ]
// In a command:
const img = await pop.canvas.create(800, 200)
.then(c => c.fill("#0f0820"))
.then(c => c.text("Hello!", 40, 120, { size: 64, color: "#fff", weight: "bold" }))
.then(c => c.toAttachment("hello.png"));
await pop.reply({ files: [img] });

Options

OptionTypeDefaultDescription
fontPathstringauto-detected.ttf or .otf font path.
fontFamilystring"PopiiFont"Family name to register the font under.

Context additions

pop.canvas.create(width, height) // Promise<CanvasBuilder>
pop.canvas.nowPlayingCard(track, opts?) // Promise<AttachmentBuilder>

CanvasBuilder methods: .fill(color), .rect(x,y,w,h,color,radius?), .outline(...), .text(str,x,y,opts?), .textTruncated(...), .image(src,x,y,w,h,opts?), .gradient(x,y,w,h,stops,dir?), .circle(cx,cy,r,color), .line(...), .save(), .restore(), .raw(), .toBuffer(), .toAttachment(name?)


Cycles the bot’s Discord status through a list of activities.

import { ActivityType } from "discord.js";
plugins: [
activityRotatorPlugin({
intervalMs: 30_000,
activities: [
{ name: "/help", type: ActivityType.Listening },
{ name: c => `${c.discord.guilds.cache.size} servers`, type: ActivityType.Watching },
],
}),
]

Options

OptionTypeDefaultDescription
intervalMsnumber60_000Rotation interval in ms.
activities{ name: string | (client) => string; type: ActivityType }[]requiredActivities to cycle through.

Enforces permissions, allowedRoles, deniedRoles, and allowedChannels fields on commands and snaps.

plugins: [ permissionGuardPlugin() ]

Checks run in order: allowed channels → permissions → allowed roles → denied roles. Failed checks reply with an ephemeral message. Re-applies on hot-reload.


Logs command name, user, guild, and execution time for every invocation.

plugins: [
commandLoggerPlugin({ logExecution: true, logCompletion: true, logErrors: true }),
]

Options

OptionTypeDefaultDescription
logExecutionbooleantrueLog when a command starts.
logCompletionbooleantrueLog when a command finishes (includes duration).
logErrorsbooleantrueLog errors before re-throwing.

Captures command errors with context. Stores the last 10 in client._recentErrors and optionally forwards to an external service.

plugins: [
telemetryPlugin({
onCapture: (error, ctx) => {
Sentry.captureException(error, { extra: ctx });
},
}),
]

ctx contains { user, guild, command, args }. PopiiSilentError is silently ignored. Errors are always re-thrown after capture. Priority: 99.


Counts command usage during the bot’s lifetime. Logs a summary on shutdown.

plugins: [ commandAnalyticPlugin() ]
// Access live counts anywhere:
const stats = client._commandAnalytics; // Map<string, number>

Counts are in-memory only — they reset on restart. For persistence, pair with telemetryPlugin and an external store.


Registers a hidden owner-only /reload command that hot-reloads all commands, events, snaps, tasks, and plugin reload hooks without restarting the process.

plugins: [ reloadPlugin() ]

To suppress or rename it:

pluginCommands: { disable: ["reload"] }

Re-syncs slash commands to Discord after reload. If devGuildId is set, syncs to that guild only.


Sends welcome and goodbye embeds when members join or leave. Fully dashboard-configured.

plugins: [ welcomePlugin() ]

Dashboard settings

KeyDescription
welcomeChannelChannel for welcome messages.
welcomeMessageTemplate. Supports {user}, {server}, {count}.
goodbyeChannelChannel for goodbye messages (falls back to welcomeChannel).
goodbyeMessageGoodbye template.

Assigns a role to every new member automatically. Fully dashboard-configured.

plugins: [ autorolePlugin() ]

Dashboard settings: autoroleId — role ID to assign on join.


Highlights popular messages in a starboard channel when they reach a reaction threshold.

Requires: sqlitePlugin()

plugins: [ sqlitePlugin(), starboardPlugin() ]

Dashboard settings

KeyDescription
starboardChannelChannel where starred messages are posted.
starboardThresholdMinimum ⭐ reactions required (default: 3).

Button-based timed polls with live vote counts. Announces results on expiry.

Requires: sqlitePlugin()

plugins: [ sqlitePlugin(), pollPlugin() ]

Commands registered: /poll — create a poll with up to 5 options and an optional duration.

One vote per user. Clicking a voted option toggles it off.


Polls RSS/Atom feeds on a configurable interval and posts new entries to Discord channels.

plugins: [
rssPlugin({
feeds: [
{
url: "https://github.com/popii-dev/popii/releases.atom",
channelId: "1234567890",
guildId: "0987654321",
interval: 10 * 60 * 1000,
},
],
}),
]

RssFeed fields

FieldTypeDefaultDescription
urlstringrequiredRSS or Atom feed URL.
channelIdstringrequiredDiscord channel to post entries.
guildIdstringrequiredGuild that owns the channel.
intervalnumber300_000Poll interval in ms.

Inbound webhook server with optional HMAC signature validation.

plugins: [
webhookPlugin({
port: 4000,
endpoints: [
{
path: "/github",
secret: process.env.GITHUB_WEBHOOK_SECRET!,
algorithm: "sha256",
async handle(payload, { client }) {
const channel = client.discord.channels.cache.get("123");
if (channel?.isTextBased()) channel.send(`Push to ${payload.repository.name}`);
},
},
],
}),
]

WebhookEndpoint fields

FieldTypeDefaultDescription
pathstringrequiredURL path.
method"GET" | "POST" | "PUT" | "PATCH""POST"HTTP method.
secretstringShared HMAC secret.
algorithm"sha256" | "sha1" | "md5"HMAC preset (reads standard headers).
signatureHeaderstringOverride signature header name.
signaturePrefixstringStrip this prefix from the signature value.
verify(body, headers, secret) => boolean | Promise<boolean>Custom verification function. Return false to reject with 401.
handle(payload, meta) => void | Promise<void>requiredHandler called with the parsed JSON body.

Adds a /gdpr delete command that lets users erase all of their stored data (economy, levels, Last.fm link, moderation cases, etc.).

Requires: sqlitePlugin()

plugins: [ sqlitePlugin(), gdprPlugin() ]

No options. The command confirms before deleting.


Creates an isolated plugin context for staging or testing — runs a subset of plugins without touching production state.

plugins: [
sandboxPlugin({
plugins: [
sqlitePlugin({ filename: "test.db" }),
economyPlugin(),
],
}),
]

Options

OptionTypeDescription
pluginsPopiiPlugin[]Plugins to run inside the isolated context.

Owner-only /plugins commands for listing and reloading plugins at runtime without a process restart.

plugins: [ pluginManagerPlugin() ]

Commands registered: /plugins list, /plugins reload


Per-guild XP leveling system. Each server has independent rankings — XP earned in one server doesn’t carry over to another. Listens to messageCreate and awards XP on a per-user cooldown.

Requires: sqlitePlugin or mongoosePlugin

plugins: [
sqlitePlugin(),
levelsPlugin({
xpPerMessage: [15, 25], // random XP range per message
cooldownMs: 60_000, // minimum gap between XP grants per user
onLevelUp(userId, guildId, newLevel, msg) {
msg.reply(`🎉 Level ${newLevel}!`);
},
}),
]

Options

OptionTypeDefaultDescription
xpPerMessagenumber | [min, max][15, 25]XP awarded per eligible message. Pass a tuple for a random range.
cooldownMsnumber60000Minimum milliseconds between XP grants for the same user in the same guild.
onLevelUp(userId, guildId, newLevel, msg) => voidCalled each time a user levels up.

Context additions

pop.levels.getProfile(guildId, userId) // → { xp, level } | null
pop.levels.addXp(guildId, userId, amount) // → { leveledUp, newLevel }

Commands registered: /rank, /leaderboard

Dashboard settings

KeyDescription
levelsEnabledToggle XP earning on/off for the server
levelUpChannelChannel to post level-up announcements in
xpPerMessageMinMinimum XP per message (overrides plugin option)
xpPerMessageMaxMaximum XP per message (overrides plugin option)
xpCooldownSecondsXP cooldown in seconds (overrides plugin option)
levelScaleMultiplierFloat multiplier for XP thresholds — higher = slower leveling
xpBlacklistedChannelsComma-separated channel IDs where XP is not awarded
levelRolesJSON map of { "level": "roleId" } auto-assigned on reaching that level

Periodically records per-guild message activity (total messages, top users, top channels) to the database. The data is visualised in the web dashboard’s Server Snapshots page and surfaced via /activity.

Requires: sqlitePlugin or mongoosePlugin

plugins: [
sqlitePlugin(),
snapshotsPlugin(),
]

No options — everything is configured per-server from the dashboard.

Commands registered: /activity [period]

Dashboard settings

KeyDescription
snapshotsEnabledToggle snapshot collection on/off for this server
snapshotsIntervalHow often to record a snapshot (5, 10, 15, 30, or 60 minutes)
snapshotsRetentionHow many days of history to keep (7, 14, 30, or 90)

Mini-games with optional economy integration. When economyPlugin is loaded, bets and payouts use the guild’s balance automatically. Without it, games still work but ignore wagers.

Requires: sqlitePlugin or mongoosePlugin · Optional: economyPlugin

plugins: [
sqlitePlugin(),
economyPlugin(),
gamesPlugin({
minBet: 10,
maxBet: 10_000,
cooldownMs: 3_000,
}),
]

Options

OptionTypeDefaultDescription
minBetnumber10Minimum bet amount for wagered games.
maxBetnumber10000Maximum bet amount for wagered games.
cooldownMsnumber3000Cooldown between game commands per user.
slotSymbols[string, string, string, string, string, string]["🍒","🍋","🍊","🍇","⭐","💎"]Six symbols for the slot machine, ordered lowest to highest value.

Commands registered: /coinflip, /slots, /rps, /blackjack


Posts alerts when a Twitch channel goes live or a YouTube channel uploads a new video. Subscriptions and announcement channels are configured per-server from the dashboard.

Requires: sqlitePlugin or mongoosePlugin

plugins: [
sqlitePlugin(),
notifyPlugin({
twitch: {
clientId: process.env.TWITCH_CLIENT_ID!,
clientSecret: process.env.TWITCH_CLIENT_SECRET!,
pollIntervalMs: 60_000, // default
},
youtube: {
pollIntervalMs: 300_000, // default
},
}),
]

Options

OptionTypeDefaultDescription
twitch.clientIdstringrequiredTwitch application client ID from the Twitch Developer Console.
twitch.clientSecretstringrequiredTwitch application client secret.
twitch.pollIntervalMsnumber60000How often to check for live streams.
youtube.pollIntervalMsnumber300000How often to check for new uploads.

Omit twitch or youtube entirely to disable that provider.

Dashboard settings — Twitch

KeyDescription
twitchUsernameTwitch channel login name to watch
twitchNotifyChannelChannel to post go-live alerts in
twitchMentionRoleRole to ping on go-live (leave blank for none)
twitchMessageCustom message template — tokens: {{streamer}}, {{title}}, {{game}}, {{url}}, {{thumbnail}}

Dashboard settings — YouTube

KeyDescription
youtubeChannelIdYouTube channel ID (starts with UC…)
youtubeNotifyChannelChannel to post upload alerts in
youtubeMentionRoleRole to ping on new uploads (leave blank for none)
youtubeMessageCustom message template — tokens: {{title}}, {{url}}, {{channel}}, {{thumbnail}}
youtubeIncludeShortsAlso alert for YouTube Shorts (true/false)

Lets users set one-off or recurring reminders, delivered via DM or a specified channel. Stores up to 25 active reminders per user.

Requires: sqlitePlugin or mongoosePlugin

plugins: [
sqlitePlugin(),
remindersPlugin(),
]

No options.

Commands registered

CommandDescription
/remind <time> <message> [channel] [repeat]Set a reminder. time accepts 10m, 2h, 1d, 3w, 1mo. repeat can be daily, weekly, or monthly.
/reminders listList your active reminders with their IDs.
/reminders cancel <id>Cancel a reminder by ID.