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().
Quick reference
Section titled “Quick reference”| Plugin | Depends on |
|---|---|
| sqlitePlugin | — |
| mongoosePlugin | — |
| webPlugin | — |
| uiPlugin | — |
| errorHandlerPlugin | — |
| permissionGuardPlugin | — |
| reloadPlugin | — |
| commandLoggerPlugin | — |
| commandAnalyticPlugin | — |
| telemetryPlugin | — |
| activityRotatorPlugin | — |
| popiiAiPlugin | — |
| autoModPlugin | — (AI analysis optionally uses popiiAiPlugin) |
| levelsPlugin | sqlitePlugin or mongoosePlugin |
| economyPlugin | sqlitePlugin or mongoosePlugin |
| gamesPlugin | sqlitePlugin or mongoosePlugin (optional economyPlugin) |
| moderationPlugin | sqlitePlugin |
| voicePlugin | — |
| canvasPlugin | — |
| deskPlugin | sqlitePlugin, uiPlugin |
| lastFmPlugin | sqlitePlugin, webPlugin |
| giveawayPlugin | sqlitePlugin |
| payPlugin | — |
| captchaPlugin | — |
| welcomePlugin | — |
| autorolePlugin | — |
| starboardPlugin | sqlitePlugin |
| pollPlugin | sqlitePlugin |
| rssPlugin | — |
| notifyPlugin | sqlitePlugin or mongoosePlugin |
| remindersPlugin | sqlitePlugin or mongoosePlugin |
| snapshotsPlugin | sqlitePlugin or mongoosePlugin |
| webhookPlugin | — |
| gdprPlugin | sqlitePlugin |
| sandboxPlugin | — |
| pluginManagerPlugin | — |
sqlitePlugin
Section titled “sqlitePlugin”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
| Option | Type | Default | Description |
|---|---|---|---|
filename | string | "popii.db" | SQLite file path, relative to process.cwd(). |
Context additions
pop.db // Bun.DatabaseOpens in WAL mode with NORMAL synchronous writes. Creates the custom_commands table on first run. Priority: 100.
mongoosePlugin
Section titled “mongoosePlugin”Connects Mongoose to MongoDB and exposes the connection on pop.mongoose.
plugins: [ mongoosePlugin({ uri: process.env.MONGODB_URI! }),]Peer dependency: bun add mongoose
Options
| Option | Type | Required | Description |
|---|---|---|---|
uri | string | ✓ | MongoDB connection string. |
...rest | ConnectOptions | Any additional Mongoose connect options. |
Context additions
pop.mongoose // typeof mongoosePriority: 100.
economyPlugin
Section titled “economyPlugin”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
| Option | Type | Default | Description |
|---|---|---|---|
xpPerMessage | number | [min, max] | [15, 25] | XP per message. A tuple picks a random value in the range. |
cooldownMs | number | 60_000 | Minimum ms between XP awards per user. |
currencyName | string | "Coins" | Display name for the virtual currency. |
onLevelUp | (userId, level, msg) => void | — | Callback fired on level-up. |
Context additions
pop.economy.currencyNamepop.economy.getProfile(userId) // Promise<{ xp, level, balance, last_daily } | null>pop.economy.addXp(userId, amount) // Promise<boolean> — true if the user leveled uppop.economy.addBalance(userId, amount) // Promise<void>Level formula: threshold for level N = 5N² + 50N + 100
Dashboard settings
| Key | Description |
|---|---|
economyBlacklistedChannels | Channel IDs where XP is disabled (comma-separated). |
levelUpChannel | Channel for level-up announcements. |
moderationPlugin
Section titled “moderationPlugin”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
| Option | Type | Default | Description |
|---|---|---|---|
defaultReason | string | "No reason provided." | Reason string used when the moderator omits it. |
Commands registered: /warn, /kick, /ban, /unban, /timeout, /cases, /case
Dashboard settings
| Key | Description |
|---|---|
modLogChannel | Channel where moderation actions are logged. |
errorHandlerPlugin
Section titled “errorHandlerPlugin”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 sentRe-applies automatically on hot-reload. Other error types are re-thrown. Priority: 95.
uiPlugin
Section titled “uiPlugin”High-level interactive components: paginated embeds, button prompts, modal forms, and multi-step wizards.
plugins: [ uiPlugin({ successColor: 0x57F287 }) ]Options
| Option | Type | Default | Description |
|---|---|---|---|
successColor | number | 0x57F287 | Embed color for pop.success(). |
errorColor | number | 0xED4245 | Embed color for pop.error(). |
infoColor | number | 0x5865F2 | Embed color for pop.info(). |
useSnapsForPagination | boolean | false | Use persistent Snaps instead of short-lived collectors for pagination buttons. |
Context additions
// Status repliespop.success(message) // ephemeral green embedpop.error(message) // ephemeral red embedpop.info(message) // ephemeral blue embed
// Paginate through embedspop.paginate(source, timeoutMs?)// source: EmbedBuilder[] or { fetch: (page) => Promise<EmbedBuilder | null>; totalPages? }
// Button prompt — returns the value of the clicked button, or null on timeoutpop.prompt(embed, buttons, timeoutMs?)
// Yes/no confirmationpop.confirm(question, timeoutMs?) // Promise<boolean>
// Modal form — returns field values keyed by name, or null on timeoutpop.form(title, fields, timeoutMs?)
// Wait for a follow-up messagepop.awaitMessage(timeoutMs?, promptMessageId?) // Promise<Message | null>
// Multi-step wizard — returns all collected answers, or null if cancelledpop.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.
webPlugin
Section titled “webPlugin”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
| Option | Type | Default | Description |
|---|---|---|---|
port | number | 3000 | HTTP server port. |
publicUrl | string | — | Publicly accessible URL (required for OAuth2). |
dashboard | boolean | false | Enable the owner dashboard at /dashboard. |
dashboardPath | string | "/dashboard" | Custom dashboard path. |
dashboardPassword | string | — | Basic Auth password as an alternative to OAuth2. |
oauth2.clientId | string | — | Discord application client ID. |
oauth2.clientSecret | string | — | Discord application client secret. |
oauth2.redirectUri | string | — | OAuth2 callback URI (register this in the developer portal). |
structuredLogs | boolean | false | Emit JSON log lines instead of plain text. |
Built-in routes
| Path | Description |
|---|---|
GET /health | { status, uptime, guilds, ping } |
GET /metrics | Prometheus-compatible metrics |
GET /invite | Bot invite link |
GET /commands | Public slash command list |
GET /servers/:guildId | Per-server settings (requires Administrator) |
GET /dashboard | Owner dashboard |
GET /members | Member lookup by ID (owners only) |
voicePlugin
Section titled “voicePlugin”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
| Option | Type | Default | Description |
|---|---|---|---|
sponsorBlock | boolean | false | Auto-skip SponsorBlock segments. |
defaultVolume | number | 100 | Playback volume (0–200). |
maxQueueSize | number | 500 | Maximum tracks in a queue. |
leaveOnEmpty | boolean | true | Leave when the queue finishes. |
leaveOnEmptyDelay | number | 30_000 | Delay (ms) before leaving an empty channel. |
poToken | string | — | Static YouTube po_token for anti-bot bypass. |
visitorData | string | — | visitor_data string paired with poToken. |
poTokenProvider | string | — | URL returning { poToken, visitorData? }. Auto-refreshed. |
poTokenRefreshMs | number | 3_600_000 | Refresh 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? } | nullpop.getNowPlayingCard(opts?) // Promise<AttachmentBuilder | null>
pop.sponsorblock.isEnabled()pop.sponsorblock.setEnabled(enabled)pop.sponsorblock.getCategories()pop.sponsorblock.setCategories(cats)pop.sponsorblock.forceSkip() // Promise<boolean>deskPlugin
Section titled “deskPlugin”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
| Option | Type | Description |
|---|---|---|
categoryId | string | Default category for ticket channels. |
supportRoleId | string | Default support role. |
panelTitle | string | Default panel embed title. |
panelMessage | string | Default panel embed body. |
transcriptChannelId | string | Channel for closed-ticket transcripts. |
Context additions
pop.desk.sendPanel(channelId) // Promise<void> — posts the ticket-open buttonpop.desk.closeTicket() // Promise<void> — closes the current ticket channelDashboard settings: deskPanelChannelId, deskCategoryId, deskSupportRoleId, deskTranscriptChannelId, deskPanelTitle, deskPanelMessage
One open ticket per user is enforced. Transcripts older than 30 days are pruned automatically.
popiiAiPlugin
Section titled “popiiAiPlugin”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
| Option | Type | Required | Description |
|---|---|---|---|
provider | "openai" | "gemini" | "anthropic" | "deepseek" | ✓ | AI provider. |
apiKey | string | ✓ | API key for the chosen provider. |
model | string | — | Model ID. Defaults: gpt-4o-mini / gemini-1.5-flash / claude-sonnet-4-6 / deepseek-chat. |
systemPrompt | string | — | Default system prompt. Overridden per-server via the aiPersona dashboard setting. |
locale | string | — | Appends 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.
autoModPlugin
Section titled “autoModPlugin”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)
| Option | Type | Default | Description |
|---|---|---|---|
maxAiChecksPerMinute | number | 10 | Max AI calls per guild per minute. |
maxAiChecksPerUserPerMinute | number | 3 | Max 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:
| Setting | Description |
|---|---|
| Enable AutoMod | Master on/off switch |
| Log Channel | Channel to post moderation embeds |
| Exempt Roles / Channels | Comma-separated IDs that bypass all rules |
| Block Discord Invites | Enable invite filter + action + allowed codes |
| Enable Word Filter | Word blocklist (comma-separated) + action |
| Enable Spam Detection | Messages-per-5-seconds threshold + action |
| Enable Mention Spam | Max mentions per message + action |
| Enable Caps Filter | Caps % threshold, minimum message length + action |
| Enable Link Filter | Block all links, allowed domains whitelist + action |
| Enable AI Analysis | Toxicity/spam thresholds + action (requires popii-ai) |
Available actions (selectable per rule):
| Value | What happens |
|---|---|
delete | Delete the message |
delete_warn | Delete and post a warning mentioning the user |
delete_mute | Delete and timeout the user for 10 minutes |
delete_kick | Delete and kick the user |
delete_ban | Delete 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.
payPlugin
Section titled “payPlugin”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
captchaPlugin
Section titled “captchaPlugin”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
| Option | Type | Required | Description |
|---|---|---|---|
siteKey | string | ✓ | hCaptcha site key. |
secretKey | string | ✓ | hCaptcha secret key. |
port | number | — | Verification server port. |
publicUrl | string | — | Publicly accessible URL (used to build the verify link). |
Context additions
pop.captcha.sendVerificationPanel(channelId) // Promise<void>Dashboard settings: captchaRoleId
giveawayPlugin
Section titled “giveawayPlugin”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.
lastFmPlugin
Section titled “lastFmPlugin”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
| Option | Type | Default | Description |
|---|---|---|---|
apiKey | string | required | Last.fm API key. |
apiSecret | string | required | Last.fm API secret. |
scrobble | boolean | true | Scrobble tracks from voicePlugin automatically. |
nowPlaying | boolean | true | Send “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).
canvasPlugin
Section titled “canvasPlugin”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
| Option | Type | Default | Description |
|---|---|---|---|
fontPath | string | auto-detected | .ttf or .otf font path. |
fontFamily | string | "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?)
activityRotatorPlugin
Section titled “activityRotatorPlugin”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
| Option | Type | Default | Description |
|---|---|---|---|
intervalMs | number | 60_000 | Rotation interval in ms. |
activities | { name: string | (client) => string; type: ActivityType }[] | required | Activities to cycle through. |
permissionGuardPlugin
Section titled “permissionGuardPlugin”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.
commandLoggerPlugin
Section titled “commandLoggerPlugin”Logs command name, user, guild, and execution time for every invocation.
plugins: [ commandLoggerPlugin({ logExecution: true, logCompletion: true, logErrors: true }),]Options
| Option | Type | Default | Description |
|---|---|---|---|
logExecution | boolean | true | Log when a command starts. |
logCompletion | boolean | true | Log when a command finishes (includes duration). |
logErrors | boolean | true | Log errors before re-throwing. |
telemetryPlugin
Section titled “telemetryPlugin”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.
commandAnalyticPlugin
Section titled “commandAnalyticPlugin”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.
reloadPlugin
Section titled “reloadPlugin”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.
welcomePlugin
Section titled “welcomePlugin”Sends welcome and goodbye embeds when members join or leave. Fully dashboard-configured.
plugins: [ welcomePlugin() ]Dashboard settings
| Key | Description |
|---|---|
welcomeChannel | Channel for welcome messages. |
welcomeMessage | Template. Supports {user}, {server}, {count}. |
goodbyeChannel | Channel for goodbye messages (falls back to welcomeChannel). |
goodbyeMessage | Goodbye template. |
autorolePlugin
Section titled “autorolePlugin”Assigns a role to every new member automatically. Fully dashboard-configured.
plugins: [ autorolePlugin() ]Dashboard settings: autoroleId — role ID to assign on join.
starboardPlugin
Section titled “starboardPlugin”Highlights popular messages in a starboard channel when they reach a reaction threshold.
Requires: sqlitePlugin()
plugins: [ sqlitePlugin(), starboardPlugin() ]Dashboard settings
| Key | Description |
|---|---|
starboardChannel | Channel where starred messages are posted. |
starboardThreshold | Minimum ⭐ reactions required (default: 3). |
pollPlugin
Section titled “pollPlugin”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.
rssPlugin
Section titled “rssPlugin”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
| Field | Type | Default | Description |
|---|---|---|---|
url | string | required | RSS or Atom feed URL. |
channelId | string | required | Discord channel to post entries. |
guildId | string | required | Guild that owns the channel. |
interval | number | 300_000 | Poll interval in ms. |
webhookPlugin
Section titled “webhookPlugin”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
| Field | Type | Default | Description |
|---|---|---|---|
path | string | required | URL path. |
method | "GET" | "POST" | "PUT" | "PATCH" | "POST" | HTTP method. |
secret | string | — | Shared HMAC secret. |
algorithm | "sha256" | "sha1" | "md5" | — | HMAC preset (reads standard headers). |
signatureHeader | string | — | Override signature header name. |
signaturePrefix | string | — | Strip 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> | required | Handler called with the parsed JSON body. |
gdprPlugin
Section titled “gdprPlugin”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.
sandboxPlugin
Section titled “sandboxPlugin”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
| Option | Type | Description |
|---|---|---|
plugins | PopiiPlugin[] | Plugins to run inside the isolated context. |
pluginManagerPlugin
Section titled “pluginManagerPlugin”Owner-only /plugins commands for listing and reloading plugins at runtime without a process restart.
plugins: [ pluginManagerPlugin() ]Commands registered: /plugins list, /plugins reload
levelsPlugin
Section titled “levelsPlugin”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
| Option | Type | Default | Description |
|---|---|---|---|
xpPerMessage | number | [min, max] | [15, 25] | XP awarded per eligible message. Pass a tuple for a random range. |
cooldownMs | number | 60000 | Minimum milliseconds between XP grants for the same user in the same guild. |
onLevelUp | (userId, guildId, newLevel, msg) => void | — | Called each time a user levels up. |
Context additions
pop.levels.getProfile(guildId, userId) // → { xp, level } | nullpop.levels.addXp(guildId, userId, amount) // → { leveledUp, newLevel }Commands registered: /rank, /leaderboard
Dashboard settings
| Key | Description |
|---|---|
levelsEnabled | Toggle XP earning on/off for the server |
levelUpChannel | Channel to post level-up announcements in |
xpPerMessageMin | Minimum XP per message (overrides plugin option) |
xpPerMessageMax | Maximum XP per message (overrides plugin option) |
xpCooldownSeconds | XP cooldown in seconds (overrides plugin option) |
levelScaleMultiplier | Float multiplier for XP thresholds — higher = slower leveling |
xpBlacklistedChannels | Comma-separated channel IDs where XP is not awarded |
levelRoles | JSON map of { "level": "roleId" } auto-assigned on reaching that level |
snapshotsPlugin
Section titled “snapshotsPlugin”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
| Key | Description |
|---|---|
snapshotsEnabled | Toggle snapshot collection on/off for this server |
snapshotsInterval | How often to record a snapshot (5, 10, 15, 30, or 60 minutes) |
snapshotsRetention | How many days of history to keep (7, 14, 30, or 90) |
gamesPlugin
Section titled “gamesPlugin”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
| Option | Type | Default | Description |
|---|---|---|---|
minBet | number | 10 | Minimum bet amount for wagered games. |
maxBet | number | 10000 | Maximum bet amount for wagered games. |
cooldownMs | number | 3000 | Cooldown 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
notifyPlugin
Section titled “notifyPlugin”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
| Option | Type | Default | Description |
|---|---|---|---|
twitch.clientId | string | required | Twitch application client ID from the Twitch Developer Console. |
twitch.clientSecret | string | required | Twitch application client secret. |
twitch.pollIntervalMs | number | 60000 | How often to check for live streams. |
youtube.pollIntervalMs | number | 300000 | How often to check for new uploads. |
Omit twitch or youtube entirely to disable that provider.
Dashboard settings — Twitch
| Key | Description |
|---|---|
twitchUsername | Twitch channel login name to watch |
twitchNotifyChannel | Channel to post go-live alerts in |
twitchMentionRole | Role to ping on go-live (leave blank for none) |
twitchMessage | Custom message template — tokens: {{streamer}}, {{title}}, {{game}}, {{url}}, {{thumbnail}} |
Dashboard settings — YouTube
| Key | Description |
|---|---|
youtubeChannelId | YouTube channel ID (starts with UC…) |
youtubeNotifyChannel | Channel to post upload alerts in |
youtubeMentionRole | Role to ping on new uploads (leave blank for none) |
youtubeMessage | Custom message template — tokens: {{title}}, {{url}}, {{channel}}, {{thumbnail}} |
youtubeIncludeShorts | Also alert for YouTube Shorts (true/false) |
remindersPlugin
Section titled “remindersPlugin”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
| Command | Description |
|---|---|
/remind <time> <message> [channel] [repeat] | Set a reminder. time accepts 10m, 2h, 1d, 3w, 1mo. repeat can be daily, weekly, or monthly. |
/reminders list | List your active reminders with their IDs. |
/reminders cancel <id> | Cancel a reminder by ID. |