A modern Discord bot framework for production bots. No AI required. Commands, events, moderation, leveling, per-guild configuration, and optional AI orchestration — all with minimal boilerplate. Start simple with slash commands. Add bundled plugins for features (moderation, roles, logging, leveling). Optionally add intelligent agents with multi-provider LLM support and permission-gated tool calling.
- Install the latest release:
pip install "easycord @ git+https://github.com/rolling-codes/EasyCord.git@v4.5.0-beta.2" - Create a bot with one slash command.
- Split features into plugins once the bot grows.
from easycord import Bot
bot = Bot()
bot.load_builtin_plugins()
@bot.slash(description="Ping the bot")
async def ping(ctx):
await ctx.respond("Pong!")
bot.run("YOUR_TOKEN")Want to see a production bot without AI? Open examples/core-bot.py.
For the shortest path to a working bot, open docs/getting-started.md.
Platform-grade localization infrastructure:
- Locale auto-detection with intelligent fallback chains (user → guild → system → default)
- Regional fallback support (pt-BR → pt → en-US) for partial translations
- Three diagnostic modes: SILENT (production, zero overhead), WARN (development, deduplicated), STRICT (CI/testing)
- Translation completeness validation with coverage metrics
- Debug-only locale resolution tracing with zero overhead when disabled
- Optional metrics tracking for cache hits, fallback frequency, and locale distribution
- Deterministic normalization guarantees (idempotent, centralized, safe for caching)
- All 193 localization and core tests passing
- Production-ready under stress with measurable performance characteristics
Earlier in v4.3.1:
- Fixed localization auto-translator source priority to ensure canonical translations are used consistently across language chains
- Fixed type checking infrastructure for slash command groups (Ruff F821 undefined-name error)
Earlier in v4.3:
- Current package line re-cut with the EasyCord helper utilities from
easycord/ - Runtime fixes for webhook retries, emoji upload validation, SQLite decoding, and limiter cleanup
- Automatic release-label handling for pull requests
- PR governance now creates missing release labels and normalizes duplicates
Create paginated help/results in one line:
from easycord import Paginator
@bot.slash(description="Show commands")
async def help(ctx):
lines = [f"/cmd{i}" for i in range(1, 37)]
await Paginator.from_lines(lines, per_page=10, title="Command List").send(ctx)Or paginate existing embeds:
from easycord import Paginator
embeds = [embed_page_1, embed_page_2, embed_page_3]
await Paginator.from_embeds(embeds).send(ctx)Use status templates for common bot responses:
from easycord import EasyEmbed
await ctx.respond(embed=EasyEmbed.success("Operation complete!"))
await ctx.respond(embed=EasyEmbed.error("Something went wrong."))
await ctx.respond(embed=EasyEmbed.info("Update available."))
await ctx.respond(embed=EasyEmbed.warning("Double-check this setting."))Start with a safer default stack in one line:
from easycord import FrameworkManager
bot = (
FrameworkManager.build_bot(
builtin_plugins=True,
guild_only=True,
)
)pip install "easycord @ git+https://github.com/rolling-codes/EasyCord.git@v4.4.0"git clone https://github.com/rolling-codes/EasyCord.git
cd EasyCord
pip install .pip install -e ".[dev]"Build bots that speak your server's language:
# Define translations in a locale file (en.json)
{
"commands": {
"ping": {
"response": "Pong!"
}
}
}
# Use in your command
@bot.slash()
async def ping(ctx):
await ctx.respond(ctx.t("commands.ping.response"))Initialize the bot with localization:
from easycord import Bot, LocalizationManager
locales = LocalizationManager()
locales.register("en", "locales/en.json")
locales.register("es", "locales/es.json")
bot = Bot(localization=locales, default_locale="en")Translations fallback gracefully: user locale → guild locale → default locale → English. See docs/localization.md for the full guide.
EasyCord core works great without AI. If you want intelligent agents, add them optionally.
from easycord import Bot
from easycord.plugins import OpenClaudePlugin
bot = Bot()
bot.add_plugin(OpenClaudePlugin(api_key="sk-ant-...")) # or ANTHROPIC_API_KEY env var
bot.run("YOUR_TOKEN")Members use /ask "your question" to query Claude API. Responses are automatically truncated to Discord's 2000-char limit, requests are rate limited per user, and the waiting message can be localized with openclaude.thinking.
For custom commands, configure a shared provider and call it through context:
from easycord.plugins import OpenAIProvider
bot = Bot(ai_provider=OpenAIProvider(api_key="sk-..."))
@bot.slash(description="Ask AI")
async def ask(ctx, prompt: str):
response = await ctx.ai(prompt, model="gpt-4o")
await ctx.respond(response[:2000])Setup: Install anthropic SDK and set ANTHROPIC_API_KEY environment variable.
See docs/examples.md for examples with OpenAI, Gemini, Groq, Ollama, and custom providers.
Let AI safely call into your bot via @ai_tool decorator:
from easycord import Plugin, ai_tool, ToolSafety
from datetime import timedelta
class ModToolsPlugin(Plugin):
@ai_tool(description="Check if user is a member of the server")
async def is_member(self, ctx, user_id: int):
try:
await ctx.guild.fetch_member(user_id)
return "User is a member"
except:
return "User is not a member"
@ai_tool(
description="Timeout a user from the server",
safety=ToolSafety.CONTROLLED,
require_admin=True,
parameters={
"type": "object",
"properties": {
"user_id": {"type": "integer"},
"seconds": {"type": "integer"}
}
}
)
async def timeout_user(self, ctx, user_id: int, seconds: int = 3600):
member = await ctx.guild.fetch_member(user_id)
await member.timeout(timedelta(seconds=seconds))
return f"Timed out {member.name} for {seconds}s"Tools are categorized by safety:
- SAFE — read-only (queries, lookups, member info)
- CONTROLLED — validated actions (moderation, database writes, role changes)
- RESTRICTED — never expose to AI (admin-only, destructive operations)
Each tool can require require_admin=True, specific allowed_roles, or allowed_users.
Use the orchestration layer for intelligent provider selection with fallback chains:
from easycord import Bot, Plugin, slash, Orchestrator, FallbackStrategy, RunContext
from easycord.plugins import AnthropicProvider, GroqProvider, OpenAIProvider
bot = Bot()
# Create orchestrator with fallback chain
orchestrator = Orchestrator(
strategy=FallbackStrategy([
AnthropicProvider(), # Try first
GroqProvider(), # Fallback
OpenAIProvider(), # Last resort
]),
tools=bot.tool_registry, # Auto-includes @ai_tool methods
)
class AIPlugin(Plugin):
@slash(description="Ask AI with tool access")
async def ask_with_tools(self, ctx, prompt: str):
await ctx.defer()
response = await orchestrator.run(
RunContext(
messages=[{"role": "user", "content": prompt}],
ctx=ctx,
max_steps=5, # Max tool calls before returning
)
)
await ctx.respond(response.text[:2000])
bot.add_plugin(AIPlugin())
bot.run("YOUR_TOKEN")The orchestrator:
- Routes intelligently: tries best provider first, falls back if it fails
- Detects tool calls: when AI requests a function call
- Executes safely: checks permissions, enforces timeouts, handles exceptions
- Loops: feeds tool results back to AI, continues until final response
- Respects constraints: admin-only, role-gated, and user-allowlisted tools
Bot Framework (complete lifecycle management):
- Slash commands, context menus, buttons, select menus, modals — all with decorators
- Event handlers (
@on) for member joins, message updates, reactions, etc. - Per-guild configuration and persistent storage (SQLite or in-memory)
- Plugins: reusable feature bundles with lifecycle hooks (
on_load,on_ready,on_unload) - 10+ bundled plugins: moderation, reaction roles, leveling, member logging, auto-responder, starboard, invite tracking, welcome, polls, tags
- Rate limiting per-user, per-tool, or per-guild
- Permission checks (built-in or custom via middleware)
- Localization: user/guild/default locale fallback
- Conversation memory for multi-turn context
Moderation & Server Management (built-in):
- Manual moderation: kick, ban, unban, timeout, warn, mute/unmute
- AI-powered moderation: message analysis with configurable confidence thresholds
- Member audit logging: track joins, leaves, nickname changes, role changes
- Reaction roles: auto-assign/revoke roles via emoji reactions
- Starboard: archive popular messages
- Invite tracking: see which invite brought each member
Developer Experience:
- Minimal boilerplate — decorators handle registration
- Middleware for cross-cutting concerns (logging, auth, rate limits)
- Fluent builder (
Composer) for declarative bot setup - Context object with shortcuts for common operations
- Embed helpers with buttons/selects built-in
- Helper libraries for common tasks (EmbedBuilder, ConfigHelpers, ContextHelpers, ToolHelpers, RateLimitHelpers)
AI & Orchestration:
- 9 LLM providers: Anthropic (Claude), OpenAI (GPT), Google (Gemini), Groq, Mistral, HuggingFace, Together.ai, Ollama (local), LiteLLM (proxy)
- Multi-provider routing: fallback chain (try Anthropic → Groq → OpenAI if first fails)
- Tool registration: expose bot commands and custom functions to AI via
@ai_tooldecorator - Permission-gated tools: SAFE (read-only), CONTROLLED (validated), RESTRICTED (never expose) — each tool can require admin/roles/users
- Tool execution loop: AI detects function calls, executes with timeout + exception handling, feeds results back
- Conversation memory: maintain context across multi-turn interactions
- Smart truncation: responses auto-fit Discord's 2000-char limit
Build bots that speak your server's language:
# Define translations in a locale file (en.json)
{
"commands": {
"ping": {
"response": "Pong!"
}
}
}
# Use in your command
@bot.slash()
async def ping(ctx):
await ctx.respond(ctx.t("commands.ping.response"))Initialize the bot with localization:
from easycord import Bot, LocalizationManager
locales = LocalizationManager()
locales.register("en", "locales/en.json")
locales.register("es", "locales/es.json")
bot = Bot(localization=locales, default_locale="en")Translations fallback gracefully: user locale → guild locale → default locale → English. See docs/localization.md for the full guide.
Build bots that speak your server's language:
# Define translations in a locale file (en.json)
{
"commands": {
"ping": {
"response": "Pong!"
}
}
}
# Use in your command
@bot.slash()
async def ping(ctx):
await ctx.respond(ctx.t("commands.ping.response"))Initialize the bot with localization:
from easycord import Bot, LocalizationManager
locales = LocalizationManager()
locales.register("en", "locales/en.json")
locales.register("es", "locales/es.json")
bot = Bot(localization=locales, default_locale="en")Translations fallback gracefully: user locale → guild locale → default locale → English. See docs/localization.md for the full guide.
Built for the moment a bot stops being a weekend project and becomes production infrastructure.
EasyCord started as a way to eliminate repetitive Discord bot boilerplate. It evolved into something deeper: a framework that removes architectural decisions you'd otherwise have to make.
With discord.py, you decide:
- How to structure commands (app_commands, prefixed, cogs?)
- How to handle permissions (decorators, checks, middleware?)
- How to rate limit (custom tracking, cooldowns, both?)
- How to organize features (cogs, blueprints, file layout?)
- How to configure per-guild (JSON files, database, cache?)
With EasyCord, those are answered. One way. Designed for production.
AI is optional. You can build fully-featured bots with zero AI dependencies. If you want intelligent agents, the framework has you covered—but you don't need it.
That's worth more than "less code"—it's fewer design questions.
| Task | Raw discord.py |
This framework |
|---|---|---|
| Slash commands | Build command tree, sync manually | @bot.slash(...) |
| Permission checks | Repeat in each command | Declare on decorator |
| Cooldowns | Track timestamps yourself | cooldown=... |
| Components | Wire interaction handlers by ID | @bot.component(...) |
| Middleware | Write custom decorators | bot.use(log_middleware()) |
| Plugins | Custom Cog wiring |
Plugin + lifecycle |
| AI integration | Build from discord.py + LLM SDK | Orchestrator + ToolRegistry |
| Tool calling | Manual prompt engineering | @ai_tool + routing |
my_bot/
├── bot.py
├── plugins/
│ ├── fun.py
│ └── moderation.py
└── pyproject.toml
- Keep
bot.pyfor startup and wiring. - Put each feature in its own plugin.
- Move shared config into
ServerConfigStorewhen you need it.
Commands & Interaction:
Botfor slash commands, events, components, and plugin loading@slash,@on,@component,@modal,@taskdecoratorsSlashGroupfor command namespacesContextfor replies, DMs, embeds, moderationEmbedCardand themed embed helpers
Plugins & Configuration:
Pluginfor reusable feature bundles withon_load()/on_unload()Bot.dbfor guild-scoped storage (SQLite or in-memory)ServerConfigStorefor per-guild settings without a databaseComposerfor fluent declarative setup
Middleware & Utilities:
- Middleware for logging, error handling, rate limiting, permission guards
- Built-in:
guild_only,admin_only,allowed_roles,has_permission,boost_only LocalizationManagerfor multi-language support
AI & Orchestration:
- 9
AIProviderimplementations (Anthropic, OpenAI, Gemini, Groq, Mistral, HuggingFace, Together, Ollama, LiteLLM) Orchestratorfor provider routing + tool execution loopsToolRegistryfor explicit tool registration with permission gates@ai_tooldecorator for AI-callable functionsFallbackStrategyfor multi-provider resilience
- Read
docs/getting-started.mdto make your first bot. - Read
docs/concepts.mdto understand the pieces. - Copy
examples/basic_bot.pyand make one change. - Move a command into a plugin once the file starts feeling crowded.
examples/core-bot.py: production bot with zero AI dependencies (commands, events, logging, permissions)examples/basic_bot.py: the smallest practical starter botexamples/plugin_bot.py: a feature split across pluginsexamples/group_bot.py: grouped slash commands withSlashGroupdocs/index.md: documentation homedocs/getting-started.md: 5-minute walkthrough to a working botdocs/quickstart-production.md: complete bot from scratch showing plugins, events, AI, error handling (canonical pattern)docs/api.md: complete API reference with examplesdocs/examples.md: patterns and snippetsdocs/fork-and-expand.md: how to grow a real bot projectdocs/migration-from-discord.py.md: side-by-side comparison, "delete after migrating" checklistdocs/security-best-practices.md: token management, permissions, AI safety pipeline, prompt injection preventiondocs/performance-tuning.md: optimize latency, memory, throughputdocs/troubleshooting.md: common issues and solutionsdocs/stability-and-scope.md: API stability guarantees, intentional gaps, extension surface, upgrade safetyserver_commands/__init__.py: one place to load the bundled plugins
This project started as a way to cut down the repetitive work of Discord bot development for a school server. That original goal still drives the project: make the first command easy, then make the second and third commands feel just as simple.
EasyCord is currently released under the MIT License.
- See
pyproject.tomlfor the canonical package license metadata (license = "MIT"). - Any future licensing experiments (including dual-license models) are not part of this release line.
Copyright (c) 2026 Rolling Codes