Crea un plugin
La referencia completa del SDK para desarrolladores de plugins — Python en sandbox, entregado como zip, distribuido a través del marketplace.
Los plugins son procesos Python en sandbox. Escribe un handler, declara tus capacidades, envía un zip — lo ejecutamos en un contenedor Docker restringido y transmitimos eventos de Discord mediante JSON-RPC.
Manejo de errores documentado
El SDK lanza excepciones tipadas en lugar de devolver diccionarios de error. Todas heredan de SdkError, por lo que puedes capturarlas de forma amplia o específica.
from yourbot_sdk import (
SdkError, CapabilityError, RateLimitError, DiscordApiError,
SdkPermissionError, ValidationError, KvQuotaError, RpcTimeoutError,
)
@plugin.on_event("message_create")
def on_message(ctx: Context, event: dict):
try:
ctx.discord.send_message(channel_id=event["channel_id"], content="Pong")
except RateLimitError as exc:
ctx.log(f"rate limited; retry in {exc.retry_after}s", level="warning")
except SdkPermissionError as exc:
ctx.log(f"missing Discord permission: {exc.permission}", level="error")
except DiscordApiError as exc:
if exc.status_code == 404:
ctx.log("channel was deleted", level="info")
else:
ctx.log(f"discord {exc.status_code}: {exc}", level="error")
except SdkError as exc:
# Catch-all - logs and keeps the plugin running.
ctx.log(f"unexpected: {exc}", level="error")
Every exception also carries a stable machine-readable .code so you can branch on failures without string-matching: v0.6.1
except SdkError as e:
if e.code == "RATE_LIMITED":
time.sleep(getattr(e, "retry_after", 5))
elif e.code == "CAPABILITY_DENIED":
ctx.log(f"missing capability: {e}", level="error")
Referencia
| Excepción | Se lanza cuando |
|---|---|
| SdkError | Clase base — captura esto para manejar cualquier cosa del SDK. SDK_ERROR |
| CapabilityError | Llamaste a una API que tu plugin no solicitó mediante capabilities_required. CAPABILITY_DENIED |
| RateLimitError | Cuota superada. Tiene .retry_after (segundos). RATE_LIMITED / QUOTA_EXCEEDED |
| DiscordApiError | El REST de Discord devolvió un código no 2xx. Tiene .status_code. DISCORD_API_ERROR |
| SdkPermissionError | Al bot le falta un permiso de guild de Discord. Tiene .permission (p. ej. "manage_channels"). BOT_MISSING_PERMISSION |
| ValidationError | Pasaste argumentos inválidos (channel_id vacío, emoji incorrecto, clave con bytes nulos…). VALIDATION_ERROR |
| KvQuotaError | Hit a KV key-count quota (50k per server or 500k global). An oversized value or key currently surfaces as a generic RPC error instead. KV_QUOTA_EXCEEDED |
| RpcTimeoutError | El ejecutor no respondió dentro del tiempo de espera por llamada. RPC_TIMEOUT |
| PermissionError alias | Alias compatible con versiones anteriores de SdkPermissionError. |
| TimeoutError alias | Alias compatible con versiones anteriores de RpcTimeoutError. |
Pruebas documentado
Las pruebas de plugins funcionan como cualquier otro código Python — sin Docker, sin conexión a plataforma, sin mocks de Discord. Importa desde mmo_maid_sdk.testing:
from yourbot_sdk.testing import MockContext, make_event
def test_ping_replies_pong():
ctx = MockContext()
event = make_event("message_create", content="!ping", channel_id="42")
on_message(ctx, event) # the handler from your __main__.py
assert len(ctx.messages_sent) == 1
sent = ctx.messages_sent[0]
assert sent["channel_id"] == "42"
assert sent["content"] == "Pong!"
def test_kv_counter_increments():
ctx = MockContext()
ctx.kv.increment("hits")
ctx.kv.increment("hits")
assert ctx.kv.get("hits") == 2
def test_capability_gate():
ctx = MockContext(capabilities=["discord:send_message"])
assert ctx.has_capability("discord:send_message")
assert not ctx.has_capability("storage:sql")
MockContext enforces capabilities by default. Calling a gated method without the matching capability raises CapabilityError, exactly like production — so a passing test means a working manifest. With no capabilities= argument you get the full standard set except proxy:websocket; WebSocket tests need MockContext(capabilities=[..., "proxy:websocket"]). capabilities=[] means none. Pass MockContext(strict_capabilities=False) to opt out. v0.6.1
Qué registra MockContext
Cada efecto secundario de Discord se captura para su verificación. Léelos como listas de dicts en el orden en que fueron llamados:
ctx.messages_sent ctx.messages_edited ctx.messages_deleted
ctx.roles_added ctx.roles_removed
ctx.members_banned ctx.members_kicked ctx.modals_sent
ctx.kv_writes ctx.log_lines ctx.interaction.responses
ctx.metrics.recorded ctx.sql.executed
ctx.discord.messages_pinned ctx.discord.messages_unpinned
ctx.discord.reactions_added ctx.discord.members_timed_out ctx.ws.ensured
Everything else your handlers touch is recorded on ctx.discord (webhooks, channels, threads, nicknames, permissions) and ctx.ws (ensured, sent, closed, allowed_hosts).
Deterministic time and canned data: MockContext(clock=MockClock(start=1000.0)) lets you clock.advance(31) for cooldown tests (a bare MockClock() is wall-clock passthrough and cannot be advanced), and ctx.discord.set_messages([…]) feeds get_messages / iter_messages.
Simular HTTP saliente
def test_calls_external_api():
ctx = MockContext()
ctx.http.mock_response(
"api.example.com/status",
status=200,
body='{"online": true}',
)
my_status_handler(ctx, make_event("message_create", content="!status"))
assert ctx.http.requests[0]["url"].startswith("https://api.example.com")
make_event incluye valores predeterminados razonables para los 16 tipos de eventos — pasa sobreescrituras como kwargs.
Capacidades
Every API your plugin uses corresponds to a capability. Declare them in capabilities_required; server admins choose which to grant at install time. The full table of capabilities, tiers and the calls each one unlocks lives in the capability catalog on the Reference tab.
The upload pipeline auto-adds any capability it detects from your code (with file and line attribution in the Dev Portal), plus two manifest implications: slash_commands implies interaction:respond, and a non-empty proxy_domains_requested implies proxy:http. So forgetting a capability does not make calls fail at upload; it silently appears on your consent screen, and if it is a reviewed tier it can send the version to staff review.
Tiers are what customers see at install: Safe, Standard and Dangerous badges on the consent screen. Dangerous-tier calls are also audit-logged at WARNING level.
Llamar a una API que tu plugin no solicitó lanza CapabilityError en tiempo de ejecución — el runner bloquea la llamada, no obtienes daño parcial. El usuario ve un aviso de "necesita permiso" y puede concederlo sin desinstalar.
Sandbox — qué garantiza
Los plugins del marketplace se ejecutan en contenedores Docker aislados. El sandbox no es una limitación impuesta sobre ti — es un conjunto de garantías realizadas en tu nombre.
Lo que el sandbox te garantiza
- Tu plugin no puede filtrar tokens de usuario, DMs de clientes ni secretos de la plataforma. El entorno está vacío — sin variables de entorno, sin credenciales de base de datos, sin tokens de bot de Discord. Si tu código es comprometido, el radio de daño es exactamente lo que el usuario consintió instalar. No estás a una mala dependencia de un incidente de seguridad.
- Tu plugin no puede tumbar la plataforma para otros usuarios. Si tu plugin se descontrola, solo tu plugin muere; el resto del servidor sigue funcionando.
- Los bugs de un plugin no pueden convertirse en los bugs de otro plugin. Cada plugin obtiene su propio contenedor, su propio espacio de nombres KV, su propio esquema SQL. No puedes leer accidentalmente los datos de otro plugin, y otro plugin no puede corromper los tuyos.
- Los clientes no tienen que auditar tu código para confiar en tu plugin. El selector de capacidades en el momento de instalación les muestra exactamente qué puede tocar tu plugin — sin importaciones misteriosas, sin efectos secundarios sorpresa. Eso es lo que hace que el marketplace funcione.
Cómo se aplican esas garantías
- Red:
--network none. Solo la pipe JSON-RPC hacia el runner es accesible. Todo el HTTP pasa a través dectx.httpmediante el proxy de la plataforma — declaras los dominios que necesitas, los clientes los ven en la instalación. - Sistema de archivos: raíz de solo lectura,
/tmpefímero, sin persistencia entre reinicios. El estado vive enctx.kv/ctx.sqldonde las copias de seguridad y auditorías pueden verlo. - Memoria: límite estricto de 64 MB por worker. La fuga de un plugin no derriba a los demás.
- CPU: 0,25 vCPU. El trabajo en segundo plano no dejará sin recursos a otros plugins.
- Procesos: 32 máximo (PIDs). Limita el daño de fork-bomb.
- Usuario: no root (
nobody, uid 65534). Sin rutas de escalada de privilegios. - Secretos: env está vacío. No hay nada que filtrar aunque quisieras.
- Límites de velocidad por (servidor, plugin): 50 eventos/seg, 60 acciones de Discord salientes/min, 30 solicitudes HTTP proxy/min. Un servidor no puede hacer DoS a tu plugin inundándolo.
Si tu plugin supera un límite, recibirá RateLimitError con una pista retry_after — retrocede, no hagas un bucle activo.
Informes de errores
- Quarantine: 5 consecutive crashes within 5 minutes stops event delivery. Restart the plugin from the dashboard to clear it.
Modo bot
Plugins share a container to cut overhead: the platform assigns whole plugins to pools (a deployment-level decision per plugin, not per install), and marketplace plugins run pooled today. Your code runs unchanged; what changes is the runtime shape:
@plugin.on_readyfires per server, not per boot. It runs once per (worker, server) right before that server's first event reaches you, with a tenant-scopedctx— the designed place for per-server init like KV defaults or SQL schema bootstrap.- Schedules run server-side, from your manifest. Declare each
@plugin.crontask in the manifest's"cron"array and the platform fires it per installed server (UTC, 5-minute floor, up to 5 entries; delivery is at-least-once and missed ticks are not replayed, so keep handlers idempotent). Interval-style@plugin.scheduletasks do not run pooled — use cron or the event-driven pattern below. - Module globals are unreliable. The
Contextis rebuilt per event, your plugin runs on more than one worker, and workers restart freely. Anything that must survive belongs inctx.kvorctx.ephemeral. - Tighter limits: 24 MB memory and 0.1 vCPU per plugin (vs 64 MB / 0.25 solo).
Periodic work without schedules
For work tighter than the 5-minute cron floor, or when you'd rather skip the manifest entry, ride on event traffic and throttle with a cooldown — on an active server this runs your job roughly once per interval:
@plugin.on_event("message_create")
def on_message(ctx: Context, event: dict):
# ... your normal handling ...
# Piggyback: roll the daily summary at most once per 24h.
if not ctx.ephemeral.cooldown_check("daily_summary")["active"]:
ctx.ephemeral.cooldown_set("daily_summary", ttl_seconds=86400)
post_daily_summary(ctx)
Quiet servers produce no events, so nothing fires — which is usually what you want (no one is reading the summary anyway). When your feature genuinely needs wall-clock delivery, declare it in the manifest "cron" array instead: those fire on schedule whether or not the server is chatting.
Publicar & revisar
Upload your zip on the Dev Portal (or link a GitHub repo and pull versions from it). Run yourbot validate first — it applies the same checks locally. The reviewer checks:
- El manifiesto es válido: campos obligatorios presentes, capacidades legales, comandos slash bien formados.
- El código no importa ningún módulo no permitido (el sandbox los bloquea de todos modos, pero la verificación previa lo detecta antes de que los usuarios vean errores).
- Sin SQL sin parametrizar, sin interpolación de f-string en
ctx.sql.execute. - No surprises — every submission from a new developer gets human review, and a Dangerous-tier request (like
discord:ban_members) is exactly what reviewers weigh hardest. Established developers with public repos publish without the queue. - Slash command names are legal: no names reserved by built-in plugins (see the definitive list), no duplicates within your manifest.
yourbot validatechecks this locally.
El tiempo de respuesta es de 1–3 días hábiles. Si se deniega, verás comentarios específicos y accionables en el portal de desarrolladores.
After you publish: updates and rollback
- Auto-updates: installs default to
auto_updateon, so servers pick up your new published version automatically. Include a changelog with every version; it shows on the marketplace page and in update notifications. - Pinning is rollback: server owners can pin any published version; pinning disables auto-update for that server. There is no separate revert button.
- Capability changes: adding any new capability or proxy domain, even a Safe-tier one, pauses auto-update on each install until the admin re-consents — a harmless-looking new capability fragments your install base across versions. Removing one is silent.
- Slash commands propagate per install: Discord sees the commands of each server's installed version, not your latest published one. A renamed option only reaches a server after it updates.
Nombra tu plugin
A price is optional: free plugins are welcome and many of the most-installed ones are free. When you do charge, the platform handles checkout, invoicing, refunds and payouts.
Crea tu primera pregunta frecuente
- Accept the Developer Agreement (you'll be prompted in the Dev Portal).
- Complete Stripe Connect onboarding from Earnings — that's where your money lands. Checkout stays disabled for your paid plans until Stripe enables charges on your account, so buyers can never pay into a void.
planes de pago
Plans are set per plugin in the Dev Portal: monthly, yearly or one-time billing, with optional per-seat quantities. You can run limited-time sales, offer bundles of your plugins and buyers can gift purchases to a server.
Lo que tú haces
| Your trailing 30-day gross | te quedas |
|---|---|
| Up to $1,000 | 70% |
| $1,000+ | 75% |
| $5,000+ | 80% |
| $10,000+ | 85% |
The tier is computed from your last 30 days of sales net of refunds, so a good month makes every plugin you sell cheaper to run. Your current tier and progress to the next one show on Earnings.
Payouts, refunds and disputes
- Payouts are automatic. Each sale's funds release about 30 days after payment, then pay out to your connected bank through Stripe. No invoices to send, no payout button to press.
- Refunds don't cost you the platform fee. When a sale is refunded the buyer is made whole, your share of that sale is reversed and the platform returns its fee share to you — a fully refunded sale nets you zero, not negative.
- Buyers can escalate refund requests. You get 7 days to respond before staff decide. Disputes (chargebacks) pause the buyer's access and claw back the sale while under review.
SDK v0.8.4 · Última actualización: julio de 2026 · Volver al Portal de desarrolladores