# Gamification: XP, levels, badges, and achievements > Audience: administrators. This page is hidden from members and guests in the sidebar, the search > index, and the documentation export. It is the complete reference for the progression system: > experience points, levels, the full badge catalogue, the activity-tracking engine behind > first-use and usage-tier badges, every place a badge is awarded, and how to extend it. DevPlace rewards participation with three layered mechanics, all centralised in the `devplacepy/utils` package (`utils/rewards.py` for XP and milestones, `utils/badges.py` for the catalogue and tracking) and wired into the existing content, engagement, and feature hooks. Nothing writes badges or XP directly - every award goes through the helpers below so milestone checks and notifications are never skipped. - **XP and levels** measure overall contribution. - **Badges** are one-time, never-revoked awards grouped into themes and shown on the profile. - **Achievements** is the umbrella term for the badge catalogue plus the per-user progress shown in the profile showcase. All reward work is deferred to the background task queue, so it never sits on the request path; in the test suite the queue runs inline so awards are deterministic. ## XP and levels Members earn XP at the existing content and engagement hook points. The amounts are constants in `utils/rewards.py`: | Action | Constant | XP | |--------|----------|----| | Publish a post | `XP_POST` | 10 | | Write a comment | `XP_COMMENT` | 2 | | Publish a project | `XP_PROJECT` | 15 | | Publish a gist | `XP_GIST` | 5 | | Receive an upvote | `XP_UPVOTE` | 5 | | Gain a follower | `XP_FOLLOW` | 5 | Awards for received upvotes and followers go to the content owner or the followed user. The level formula is `level = 1 + xp // 100` (`LEVEL_XP = 100`). `award_xp(user_uid, amount)` clamps XP at zero, recomputes the level, invalidates the user cache, and on a level-up fires a `level` notification and any level badge. `award_rewards(user_uid, amount, first_badge=None)` is the single entry point that chains the optional first-time badge, `award_xp`, and `check_milestone_badges`. ## The badge system: two complementary mechanisms Badges come from two sources, chosen by whether the achievement can be recomputed from existing data: 1. **Source-derived milestones** - counts that already live in a table (posts, comments, projects, gists, stars, followers, following, activity streak). These need no tracking: `check_milestone_badges` recomputes them and is accurate even retroactively. Defined in the `_COUNT_MILESTONES` list and the `LEVEL_BADGES` map. 2. **Activity-tracked badges** - first-time feature use and usage tiers that are *not* derivable from any table (read N documentation pages, ran a DeepSearch, sent a message, used Devii). These are counted in a dedicated activity table and awarded by `track_action`. The four original first-content badges (First Post, First Comment, First Project, First Gist) are a third, simplest case: they are passed as the `first_badge` argument of `award_rewards` at the moment the content is created (in `content.py`). ## Badge catalogue Every badge, its icon, the group it belongs to, and how it is earned. Metadata lives in `BADGE_CATALOG`; the group order is `BADGE_GROUPS`. ### First steps | Icon | Badge | How to earn | |------|-------|-------------| | ✎ | First Post | Publish your first post (`award_rewards` first_badge) | | ❝ | First Comment | Write your first comment (`award_rewards` first_badge) | | ⬢ | First Project | Share your first project (`award_rewards` first_badge) | | ❡ | First Gist | Share your first gist (`award_rewards` first_badge) | | 📎 | First Upload | Upload your first attachment (`upload`, 1) | | 📄 | File Creator | Create a file in a project (`project_file`, 1) | | 📇 | Profiled | Fill in any profile field (`profile`, 1) | ### Explorer | Icon | Badge | How to earn | |------|-------|-------------| | 📖 | Curious | Read 1 documentation page (`docs.read`, distinct) | | 📚 | Studious | Read 5 documentation pages (`docs.read`, distinct) | | 🎓 | Scholar | Read 15 documentation pages (`docs.read`, distinct) | | 🤖 | AI Curious | Talk to Devii for the first time (`devii`, 1) | | ✨ | AI Whisperer | Hold 25 conversations with Devii (`devii`, 25) | | 🍴 | First Fork | Fork a project (`fork`, 1) | | 🌿 | Forker | Fork 5 projects (`fork`, 5) | | 🗜 | Archivist | Download a project archive (`zip`, 1) | | 🔍 | SEO Auditor | Run an SEO diagnostics audit (`seo`, 1) | | 🔬 | Researcher | Run a DeepSearch investigation (`deepsearch`, 1) | | 🌊 | Deep Diver | Run 10 DeepSearch investigations (`deepsearch`, 10) | | 📦 | Container Captain | Create a container instance (`container`, 1) | ### Engagement | Icon | Badge | How to earn | |------|-------|-------------| | 🔖 | Bookmarker | Bookmark your first item (`bookmark`, 1) | | 📌 | Curator | Bookmark 25 items (`bookmark`, 25) | | 💛 | First Reaction | React to content for the first time (`reaction`, 1) | | 🎉 | Cheerleader | React 50 times (`reaction`, 50) | | ⭐ | First Star Given | Star someone's work for the first time (`vote`, 1) | | 👍 | Supporter | Give 50 stars (`vote`, 50) | | 🤝 | Patron | Give 250 stars (`vote`, 250) | | 🗳 | Pollster | Vote in a poll for the first time (`poll`, 1) | ### Content | Icon | Badge | How to earn | |------|-------|-------------| | ✺ | Prolific | Publish 10 posts | | 🖋 | Wordsmith | Publish 50 posts | | 🏆 | Veteran | Publish 100 posts | | 💬 | Conversationalist | Write 25 comments | | 📣 | Debater | Write 100 comments | | 🔧 | Builder | Share 5 projects | | 🏛 | Architect | Share 10 projects | | 📑 | Snippet Collector | Share 5 gists | ### Community | Icon | Badge | How to earn | |------|-------|-------------| | 🔗 | Connector | Follow 10 people | | ✉ | Messenger | Send your first direct message (`message`, 1) | | 📨 | Chatterbox | Send 100 direct messages (`message`, 100) | | 👋 | Friendly | Follow someone for the first time (`follow`, 1) | | 🐛 | Bug Reporter | File your first issue (`issue`, 1) | ### Reputation | Icon | Badge | How to earn | |------|-------|-------------| | ☆ | Rising Star | Earn 25 stars | | ★ | Star Author | Earn 100 stars | | 🌟 | Superstar | Earn 500 stars | | ◎ | Popular | Reach 10 followers | | 📢 | Influencer | Reach 50 followers | | 👑 | Celebrity | Reach 100 followers | ### Dedication | Icon | Badge | How to earn | |------|-------|-------------| | 🔥 | On Fire | Maintain a 7-day activity streak | | 📅 | Dedicated | Maintain a 30-day activity streak | | 🚀 | Unstoppable | Maintain a 100-day activity streak | ### Code Farm See [Code Farm](/docs/code-farm.html) for the game itself. | Icon | Badge | How to earn | |------|-------|-------------| | 🌱 | Green Thumb | Harvest your first build on the Code Farm (`harvest`, 1) | | 🌾 | Master Farmer | Harvest 50 builds on the Code Farm (`harvest`, 50) | | 💧 | Good Neighbor | Water a neighbour's build (`water`, 1) | | 🥷 | Cat Burglar | Steal a ready build from another farm (`harvest_stolen`, 1) | | 🚨 | Robbed | Have a build stolen from your farm (`got_stolen_from`, 1) | ### Levels and membership | Icon | Badge | How to earn | |------|-------|-------------| | ❖ | Level 5 / 10 / 25 / 50 / 100 | Reach the matching level (`LEVEL_BADGES`, group `Levels`) | | ✦ | Member | Join DevPlace (granted at registration, group `Milestones`) | ## The activity-tracking engine Activity-tracked badges are backed by two tables, both created and indexed in `init_db()`: - `user_activity` - a per-user, per-action counter (`count`, `first_at`, `last_at`), unique on `(user_uid, action)`. Used for repeatable actions (votes given, messages sent, forks). - `user_activity_seen` - a per-user set of distinct targets, unique on `(user_uid, action, target)`. Used for "N distinct things" achievements, currently only `docs.read` (distinct documentation slugs), so refreshing the same page does not inflate the count. `database.record_activity(user_uid, action)` upserts the counter and returns the new count; `database.record_unique_activity(user_uid, action, target)` records a distinct target and returns the new distinct count, or `None` if the target was already seen. The single hook the rest of the codebase calls is `utils.track_action(user_uid, action, target=None)`. It returns immediately for an unknown action, otherwise it submits the work to the background queue, which records the activity (counter or distinct, per `UNIQUE_ACTIONS`) and then awards and notifies every threshold crossed in the `ACHIEVEMENTS` registry. It is fully deferred and idempotent, so it is safe to call inline at any feature's success point. The `ACHIEVEMENTS` registry maps an action to its `(threshold, badge)` tiers (threshold 1 is a first-use badge): | Action | Counting | Tiers | |--------|----------|-------| | `docs.read` | distinct | 1 Curious, 5 Studious, 15 Scholar | | `devii` | count | 1 AI Curious, 25 AI Whisperer | | `fork` | count | 1 First Fork, 5 Forker | | `zip` | count | 1 Archivist | | `seo` | count | 1 SEO Auditor | | `deepsearch` | count | 1 Researcher, 10 Deep Diver | | `container` | count | 1 Container Captain | | `message` | count | 1 Messenger, 100 Chatterbox | | `bookmark` | count | 1 Bookmarker, 25 Curator | | `reaction` | count | 1 First Reaction, 50 Cheerleader | | `vote` | count | 1 First Star Given, 50 Supporter, 250 Patron | | `follow` | count | 1 Friendly | | `upload` | count | 1 First Upload | | `project_file` | count | 1 File Creator | | `issue` | count | 1 Bug Reporter | | `poll` | count | 1 Pollster | | `profile` | count | 1 Profiled | | `harvest` | count | 1 Green Thumb, 50 Master Farmer | | `water` | count | 1 Good Neighbor | | `harvest_stolen` | count | 1 Cat Burglar | | `got_stolen_from` | count | 1 Robbed | ## Where each badge is awarded `track_action` is called at the success point of each feature: | Action | Source location | Trigger | |--------|-----------------|---------| | `vote` | `content.apply_vote` | an upvote (not a toggle-off) | | `bookmark` | `content.set_bookmark` | a save (not a removal) | | `reaction` | `routers/reactions.py` | a reaction added | | `follow` | `routers/follow.py` | following someone (follower side) | | `message` | `services/messaging/persist.py` | a message sent (sender) | | `docs.read` | `routers/docs/views.py` | an authenticated doc page view, per slug | | `fork` / `zip` | `routers/projects/index.py` | fork or project-zip enqueue | | `zip` / `project_file` | `routers/projects/files.py` | subtree-zip enqueue, file create | | `seo` | `routers/tools/seo.py` | SEO audit enqueue (signed-in owner) | | `deepsearch` | `routers/tools/deepsearch.py` | DeepSearch enqueue (signed-in owner) | | `container` | `services/containers/api.create_instance` | instance created by a user actor | | `upload` | `routers/uploads.py` | attachment upload | | `issue` | `routers/issues/create.py` | issue filing enqueue | | `poll` | `routers/polls.py` | a poll vote cast | | `profile` | `routers/profile/index.py` | profile saved with a non-empty field | | `devii` | `services/devii/session/core.py` | a completed Devii turn (user owner, non-docs channel) | | `harvest` | `routers/game/index.py` | harvest a ready build on your own farm | | `water` / `harvest_stolen` / `got_stolen_from` | `routers/game/farm.py` | water a neighbour, steal a build (thief), or be stolen from (victim) | Guests are never tracked: the hooks only fire for an authenticated user. The Docii documentation channel is excluded from the `devii` badge. ## Milestone badges (`check_milestone_badges`) `check_milestone_badges(user_uid)` is called by `award_rewards` after every post, comment, project, gist, upvote, and follow. It loads the badges the user already holds, then evaluates only the unheld entries of `_COUNT_MILESTONES`, resolving each metric lazily through a memoised resolver so a metric is queried only when an unheld badge needs it. Metric keys: `posts`, `comments`, `projects`, `gists`, `stars`, `followers`, `following`, `streak`. Streak badges additionally emit a `reward.streak.milestone` audit event. ## Notifications and audit - Earning any badge fires a `badge` notification (`notify_badge`), and a level-up fires a `level` notification. Both carry `related_uid == user_uid`, so they render with the recipient's own avatar. - Every grant records an audit event: `reward.badge.award`, `reward.xp.grant`, `reward.level.up`, and `reward.streak.milestone`. These appear in the admin Audit Log. ## The profile achievements showcase `utils.build_achievements(held_names)` returns the full catalogue grouped by theme, each badge marked earned or locked, with per-group `earned` and `total` counts. The profile route passes it as `achievements` (plus `badge_earned` and `badge_total`) and `profile.html` renders a collapsible **Achievements** panel above the profile tabs: earned badges are highlighted, locked ones are dimmed and show their unlock condition, and the summary shows the overall earned-of-total count. This makes every badge discoverable so there is always a next goal. ## Existing accounts and backfill `_backfill_gamification()` runs at the end of `init_db()`. For accounts still at the default `xp = 0` it computes XP from prior activity, sets the level, and runs `check_milestone_badges`, so historic contributors are not stranded at level 1. Source-derived milestone badges are also awarded on a user's next rewarded action. Activity-tracked badges accrue from the next time each feature is used; they are intentionally not backfilled, because the activity counters do not exist for past actions. ## Extending the system To add a **first-use or usage-tier** badge: 1. Add the badge to `BADGE_CATALOG` with an `icon`, `description`, and `group`. 2. Add an `ACHIEVEMENTS` entry mapping the action to its `(threshold, badge)` tiers. Add the action to `UNIQUE_ACTIONS` if it counts distinct targets. 3. Call `track_action(user_uid, action[, target])` inline at the feature's success point. Do not wrap it in `background.submit` - it defers itself. To add a **source-derived milestone** badge: 1. Add the badge to `BADGE_CATALOG`. 2. Add a row to `_COUNT_MILESTONES` with an existing metric key (extend `_milestone_metrics` if a new metric is needed). It is awarded the next time `check_milestone_badges` runs. Keep all badge logic in these helpers; never insert into the `badges` table or compute thresholds inline elsewhere.