Production-grade Discord bot framework for building scalable, maintainable bots with clean, type-safe code.
Slash commands, context menus, modal forms, components with dynamic routing, plugins with versioned config schemas and dependency management, per-guild storage, multi-language i18n, conversation memory, optional AI orchestration, middleware pipeline, lifecycle hooks, and task scheduling—all with zero boilerplate.
29 built-in plugins including levels, economy, moderation, starboard, polls, translation, JuiceWRLD, and more. 1500+ tests, atomic database operations, concurrent plugin safety, proper error isolation, and comprehensive type hints. Deploy with confidence.
- No boilerplate. Decorators do the work. Define commands in two lines, not thirty.
- Type-safe. Full Pyright support. Catch bugs at dev time, not runtime.
- Plugin-native. Modular, testable, reusable. Versioned config schemas with automatic migration. Build plugins in minutes, not hours.
- Optional AI. Includes conversation memory and multi-provider LLM orchestration (9 providers). Use it or ignore it.
- Tested. 1500+ tests covering concurrency, crashes, race conditions, and edge cases.
- Async-first. Proper lock safety, atomic database operations, isolated error handling. Won't silently corrupt state.
pip install "https://github.com/rolling-codes/EasyCord/releases/download/v5.57.0/easycord-5.57.0-py3-none-any.whl"Or scaffold a full project:
easycord new my-bot
cd my-bot && pip install -e ".[dev]"Requirements: Python 3.10+. The only runtime dependency is discord.py>=2.7.1,<3.
import os
from easycord import Bot, Plugin, slash, SQLiteDatabase
from easycord.plugins import LevelsPlugin, ModerationPlugin
class MyPlugin(Plugin):
@slash(description="Server info")
async def info(self, ctx):
await ctx.respond(f"{ctx.guild.name} — {ctx.guild.member_count} members")
bot = Bot(database=SQLiteDatabase("bot.db"))
bot.add_plugins(LevelsPlugin(), ModerationPlugin(), MyPlugin())
bot.run(os.environ["DISCORD_TOKEN"])That's /rank, /leaderboard, /kick, /ban, /timeout, /warn, and /info — from 10 lines.
from easycord.testing import invoke
async def test_info():
ctx = await invoke(bot, "info")
assert "members" in ctx.last_responseNo bot token, no Discord connection. See Testing Commands.
A full-featured community server bot: leveling, tags, polls, moderation, economy, and a custom admin dashboard — all from built-in plugins and one custom Plugin.
import os, discord
from easycord import Bot, Plugin, slash, SQLiteDatabase
from easycord.plugins import ModerationPlugin, EconomyPlugin, ReminderPlugin, StarboardPlugin
class AdminPlugin(Plugin):
@slash(description="Show server overview", guild_only=True, require_admin=True)
async def dashboard(self, ctx):
guild = ctx.guild
embed = discord.Embed(title=f"Admin — {guild.name}", color=discord.Color.gold())
embed.add_field(name="Members", value=str(guild.member_count), inline=True)
embed.add_field(name="Channels", value=str(len(guild.channels)), inline=True)
embed.add_field(name="Roles", value=str(len(guild.roles)), inline=True)
await ctx.respond(embed=embed, ephemeral=True)
bot = Bot(
load_builtin_plugins=True, # WelcomePlugin, TagsPlugin, PollsPlugin, LevelsPlugin
database=SQLiteDatabase("bot.db"),
)
bot.add_plugins(
AdminPlugin(),
ModerationPlugin(),
EconomyPlugin(),
ReminderPlugin(),
StarboardPlugin(),
)
bot.run(os.environ["DISCORD_TOKEN"])What you get: /rank, /leaderboard, /tag, /poll, /welcome, /kick, /ban, /timeout, /warn, /balance, /daily, /transfer, /remind, plus ⭐ starboard pinning and a custom /dashboard — all production-ready with persistent SQLite storage.
Multi-provider AI assistant with tool access. Tries Claude first, falls back to Groq on failure, and exposes a server-info tool the AI can call during reasoning.
import os
from easycord import Bot, Plugin, slash, Orchestrator, FallbackStrategy, RunContext, ai_tool
from easycord.plugins import AnthropicProvider, GroqProvider
class AiPlugin(Plugin):
def __init__(self):
super().__init__()
self._orchestrator = Orchestrator(
strategy=FallbackStrategy([AnthropicProvider(), GroqProvider()]),
)
async def on_load(self):
self._orchestrator.tools = self.bot.tool_registry
@slash(description="Ask the AI a question")
async def ask(self, ctx, question: str):
await ctx.defer()
result = await self._orchestrator.run(
RunContext(messages=[{"role": "user", "content": question}], ctx=ctx, max_steps=3)
)
await ctx.respond(result.text[:2000])
@ai_tool(description="Return this server's name and member count")
async def server_info(self, ctx) -> str:
return f"{ctx.guild.name} has {ctx.guild.member_count} members."
bot = Bot()
bot.add_plugin(AiPlugin())
bot.run(os.environ["DISCORD_TOKEN"])What you get: /ask backed by Claude (falls back to Groq automatically), with a tool the AI can call to look up server info. Swap providers in one line — the command never changes. See AI Features for conversation memory, tool safety levels, and all 9 providers.
A bot that won't corrupt guild config across schema changes, with rate limiting and error isolation from the start.
import os
from easycord import Bot, Plugin, slash, task, SQLiteDatabase
from easycord.config_schema import ConfigSchema
from easycord.middleware import catch_errors, rate_limit, log_middleware
_DEFAULTS = {"xp_enabled": True, "welcome_channel": None, "mod_log": None}
SCHEMA = ConfigSchema(key="server", version=1, defaults=_DEFAULTS)
@SCHEMA.migration(from_version=0)
def _v0_to_v1(section: dict) -> dict:
return {**section, "mod_log": None} # backfill field added in v1
class ServerPlugin(Plugin):
@slash(description="Toggle XP tracking", guild_only=True, require_admin=True)
async def set_xp(self, ctx, enabled: bool):
cfg = await self.config.get_schema(ctx.guild.id, SCHEMA)
cfg["xp_enabled"] = enabled
await self.config.save(ctx.guild.id, cfg)
await ctx.respond(f"XP {'enabled' if enabled else 'disabled'}.", ephemeral=True)
@task(hours=1)
async def health_ping(self):
pass # runs every hour without blocking the event loop
bot = Bot(database=SQLiteDatabase("bot.db"))
bot.use(catch_errors())
bot.use(rate_limit(limit=5, window=10.0))
bot.use(log_middleware())
bot.add_plugin(ServerPlugin())
bot.run(os.environ["DISCORD_TOKEN"])What you get: Per-guild config that survives schema changes — missing keys backfilled, stale keys migrated, all transparently. Heal drifted configs any time with easycord doctor bot:bot --fix-configs. Rate limiting and structured error replies out of the box. See Plugin Config Schemas and Request Lifecycle.
Load the starter set with one call, or cherry-pick:
bot = Bot(load_builtin_plugins=True) # loads Welcome, Tags, Polls, Levels
# or
from easycord.plugins import ModerationPlugin, EconomyPlugin, TicketsPlugin
bot.add_plugins(ModerationPlugin(), EconomyPlugin(), TicketsPlugin())| Plugin | Key Commands | Description |
|---|---|---|
LevelsPlugin |
/rank, /leaderboard, /set_xp_multiplier |
XP, leveling, role rewards, leaderboard caching |
ModerationPlugin |
/kick, /ban, /unban, /timeout, /warn |
Member moderation with reason logging |
EconomyPlugin |
/balance, /daily, /transfer |
Virtual currency with per-guild balances |
TagsPlugin |
/tag get/set/delete/list |
Per-guild text snippets |
PollsPlugin |
/poll |
Reaction-based emoji polls |
WelcomePlugin |
/welcome set |
Configurable join messages |
StarboardPlugin |
(reaction-based) | Pins messages reaching a ⭐ threshold |
TicketsPlugin |
/ticket create/close |
Private support ticket channels |
SuggestionsPlugin |
/suggest |
Community suggestions with voting |
GiveawayPlugin |
/giveaway create/end/roll |
Timed giveaways with role requirements |
ReminderPlugin |
/remind me in X do Y |
User-scheduled reminders |
BirthdayPlugin |
/birthday set/list/next |
Per-guild birthday tracking and announcements |
ReputationPlugin |
/rep give/check |
Member reputation system |
TranslatePlugin |
/translate |
Google Translate, no API key required |
VerificationPlugin |
/verify |
CAPTCHA-style member gate |
WordFilterPlugin |
/filter add/remove/list |
Auto-delete messages matching patterns |
AutoResponderPlugin |
/autoresponder add/remove |
Keyword-triggered auto-replies |
AutoRolePlugin |
/autorole set/remove |
Assign roles automatically on join |
ReactionRolesPlugin |
(reaction-based) | Role assignment via emoji reactions |
ScheduledAnnouncementsPlugin |
/announce schedule |
Recurring channel announcements |
ServerStatsPlugin |
(dynamic channels) | Live member/channel count display channels |
MemberLoggingPlugin |
(event-based) | Logs joins, leaves, nickname and role changes |
InviteTrackerPlugin |
/invites |
Tracks which invite brought each member |
RolePersistencePlugin |
(event-based) | Restores roles when members rejoin |
AIModeratorPlugin |
(event-based) | AI-powered content moderation |
SecurityLabPlugin |
/security |
Audit guild permissions and security posture |
JuiceWRLDPlugin |
/jw_search, /jw_song, /jw_era, /jw_random |
JuiceWRLD discography search and lookup |
OpenClaudePlugin |
/ask |
/ask command backed by Anthropic Claude |
OpenClawPlugin |
/claw |
Lightweight Claude assistant variant |
Full documentation: docs/builtin-plugins.md
+----------------+ +-------------------+ +----------------------+
| Discord.py | <--> | EasyCord (Bot) | <--> | InteractionRegistry |
+----------------+ +---------+---------+ +----------------------+
|
+-----------+-----------+-----------+-----------+
| | | | |
+-----+-----+ +---+-------+ +-+--------+ +-+-------+ +-----------+
| Plugins | | Middleware| | Database | | i18n | | AI Layer |
+-----------+ +-----------+ +----------+ +---------+ +-----------+
InteractionRegistry is the authoritative EasyCord inventory. discord.app_commands.CommandTree remains the Discord sync backend.
my_bot/
├── bot.py # startup, BotConfig, plugin registration
├── plugins/
│ ├── __init__.py
│ ├── fun.py # one Plugin subclass per file
│ └── moderation.py
├── locales/
│ ├── en-US.json
│ └── es-ES.json
├── tests/
│ └── test_commands.py
└── pyproject.toml
- Keep
bot.pyfor startup and wiring only. - Put each feature in its own
Plugin— reloadable, testable, isolated. - Use
db_backend="memory"in tests so runs stay offline and produce no local files.
| What you need | Raw discord.py |
EasyCord |
|---|---|---|
| Slash command with typed args | CommandTree + manual sync |
@slash(description="...") — sync handled |
| Component routing | Match string IDs manually per handler | @component("ticket:close:{id:int}") — typed |
| Reusable feature module | Cog with manual setup/teardown |
Plugin with on_load, on_unload, on_reload |
| Per-guild settings | Hand-rolled storage + migration code | ServerConfigStore + ConfigSchema migrations |
| Request control | Decorator chains on every command | bot.use(catch_errors(), rate_limit(), guild_only()) |
| Error waterfall | Re-raise or duplicate handlers | @command_error → Plugin.on_error → @bot.on_error |
| Offline testing | Mock the entire discord.py client | ctx = await invoke(bot, "kick") |
| AI tool calling | LLM SDK + prompt engineering + glue | @ai_tool + Orchestrator + safety levels |
| Day-one feature set | Zero plugins, all from scratch | 29 built-in plugins, load_builtin_plugins=True |
EasyCord is released under the MIT License.
See pyproject.toml for the canonical license metadata.
Copyright (c) 2026 Rolling Codes.
Docs: Getting Started · Built-in Plugins · AI Features · All guides →