Recipe 4's KV leaderboard scans at most 100 keys per call. When your plugin lands on big servers, keep counts in ctx.sql instead — a real table sorts any number of users. Needs the storage:sql capability (staff-reviewed); bootstrap the schema in on_ready, the designed place for per-server init.
@plugin.on_ready
def init_schema(ctx: Context):
ctx.sql.execute(
"CREATE TABLE IF NOT EXISTS xp ("
" user_id TEXT PRIMARY KEY,"
" points BIGINT NOT NULL DEFAULT 0)"
)
@plugin.on_event("message_create")
def award_xp(ctx: Context, event: dict):
if event.get("author_bot"):
return
ctx.sql.execute(
"INSERT INTO xp (user_id, points) VALUES (%s, 5) "
"ON CONFLICT (user_id) DO UPDATE SET points = xp.points + 5",
[event["author_id"]],
)
@plugin.on_slash_command("rank")
def rank(ctx: Context, event: dict):
rows = ctx.sql.query(
"SELECT user_id, points FROM xp ORDER BY points DESC LIMIT 10")
if not rows:
ctx.interaction.respond(content="No XP yet - say something!")
return
lines = [f"{i+1}. <@{r['user_id']}>: {r['points']} XP"
for i, r in enumerate(rows)]
ctx.interaction.respond(content="**XP leaderboard**\n" + "\n".join(lines))