# Levels and XP

> Give members XP for chatting, announce level-ups, and add /rank and /leaderboard commands.

You will build a level system:

- Members get 15 to 25 **XP** (experience points) for chatting. At most once a minute, so spam does not help.
- The bot posts a message when someone reaches a new level.
- `/rank` shows a member's level and XP.
- `/leaderboard` shows the top 10.

> **Premium needed:** XP for chatting needs a **Message** event. Message events run only on servers with a premium plan. `/rank` and `/leaderboard` work on every plan.

## What you need

- A premium plan on the server.
- About 10 minutes.

The XP is saved in a **Mongo database**. That is a place to keep data that lasts, with one row (called a *document*) per member. You do not need to set it up. It is made the first time the code saves something. Learn more in [Saving data](https://ccbot.app/docs/concepts/saving-data).

## How levels work

A member's level comes from their XP:

| Level | XP needed |
|---|---|
| 1 | 100 |
| 2 | 400 |
| 3 | 900 |
| 5 | 2,500 |
| 10 | 10,000 |

The rule is: level = square root of (XP ÷ 100). Each level takes a bit longer than the last.

## 1. Give XP for messages

On the dashboard, open **Create → Event**.

| Field | Value |
|---|---|
| Name | `XP for messages` |
| Type | `MESSAGE (Pro Only)` |

Paste this in **Code**:

```python
def level_of(xp):
    return int((xp / 100) ** 0.5)

# Bots and webhooks do not earn XP.
if user and not user.is_bot:
    levels = SlashMongo("levels")
    uid = str(user.id)
    now = int(unix_timestamp)
    doc = await levels.find_one({"_id": uid}) or {"xp": 0, "last": 0}

    # One reward per minute.
    if now - doc.get("last", 0) >= 60:
        old_xp = doc.get("xp", 0)
        new_xp = old_xp + randomNumber(15, 25)
        await levels.update_one(
            {"_id": uid},
            {"$set": {"xp": new_xp, "last": now}},
            upsert=True,
        )

        if level_of(new_xp) > level_of(old_xp):
            send_message(
                channel,
                f"{user.mention} reached **level {level_of(new_xp)}**!",
            )
```

Press save.

What the code does:

1. Skips bots.
2. Loads the member's row. A new member starts at 0 XP.
3. Stops if they got XP less than 60 seconds ago.
4. Adds 15 to 25 XP and saves. `upsert=True` makes the row if it is not there yet.
5. Posts a message when the level goes up.

> **Note:** Database functions like `find_one` and `update_one` need `await` in front. Most other functions do not. See [Storage](https://ccbot.app/docs/reference/storage).

## 2. Make /rank

Open **Create → Slash Command**.

| Field | Value |
|---|---|
| Command Name | `rank` |
| Description | `Show your level` |

Under **Arguments**, add a **Member** argument:

| Field | Value |
|---|---|
| Name | `member` |
| Description | `Whose rank to show` |
| Required | Off |

Paste this in **Pre-hook**:

```python
def level_of(xp):
    return int((xp / 100) ** 0.5)

target = member or user
doc = await SlashMongo("levels").find_one({"_id": str(target.id)})
xp = doc["xp"] if doc else 0
level = level_of(xp)
next_xp = (level + 1) ** 2 * 100

set_content(
    f"**{target.display_name}** is level **{level}**\n"
    f"XP: {intcomma(xp)} / {intcomma(next_xp)}"
)
```

Leave the **Response** box empty. `set_content` fills the reply.

When nobody is picked, `member` is empty, so `member or user` falls back to the person who ran the command.

## 3. Make /leaderboard

Make another slash command named `leaderboard`. Paste this in **Pre-hook**:

```python
def level_of(xp):
    return int((xp / 100) ** 0.5)

top = await SlashMongo("levels").find({}, sort=[("xp", -1)], limit=10)

lines = []
for place, doc in enumerate(top, start=1):
    lines.append(
        f"**{place}.** <@{doc['_id']}> — level {level_of(doc['xp'])} "
        f"({intcomma(doc['xp'])} XP)"
    )

set_embed(Message(
    title=f"Top members in {server.name}",
    description="\n".join(lines) or "Nobody has XP yet. Start chatting!",
    color="#5865F2",
))
```

`sort=[("xp", -1)]` puts the highest XP first. `limit=10` keeps only 10 rows, so the command stays fast on big servers.

Mentions inside an embed do not ping anyone, so the leaderboard is quiet.

## 4. Test it

1. Send a message in your server.
2. Run `/rank`. You have 15 to 25 XP.
3. Send another message right away. Run `/rank` again. Nothing changed: that is the one-minute wait.
4. Run `/leaderboard`.

Not working? Open **Logs → Error Logs** on the dashboard. Errors from your code show there.

## Ideas

- **Level roles.** In the event, after the level-up message, give a role: `if level_of(new_xp) == 5: add_roles(user, "Level 5")`.
- **No XP in some channels.** Add `and channel.name != "bot-spam"` to the first `if`.
- **Image rank card.** Design one in **Image Templates** and send it with `render_template`. See [Web and files](https://ccbot.app/docs/reference/web-and-files).
- **Reset everyone.** Make a staff-only command that runs `await SlashMongo("levels").delete_many({})`.
