Skip to Content
BackendAchievement System

Achievement System

The achievement system is a data-driven engine — admins create achievements and rules via the admin panel with zero code changes. When users perform actions in the app, events are published to NATS JetStream, and a background worker evaluates achievement progress.

Architecture

User Action (login, trade, deposit, etc.) UseCase fires SendEvent() ──► NATS JetStream stream: ACHIEVEMENTS subject: achievements.events.<userID> Achievement Worker (durable pull consumer) CheckAchievementActivity() ├── calculateSignalData() ──► DB queries for all metrics ├── loadActiveAchievements() ──► DB: active, non-expired ├── evalProgress() ──► comparator-based evaluation ├── upsertUserAchievement() ──► update progress % ├── grantRewards() ──► apply consumable rewards └── sendNotification() ──► in-app notification

Events

Every significant user action fires an achievement event. The achievement worker recalculates ALL metrics from the database on each event — events are triggers, not data carriers.

All Event Types

EventConstantFired WhenSource File
match_closeEventMatchCloseMatch finishes and rewards distributedgame_usecase_close.go
position_createEventPositionCreatePlayer opens a new positiongame_usecase.go
position_closeEventPositionClosePosition closed (manual, SL/TP, liquidation)game_usecase_calc.go
position_updatedEventPositionUpdatedPlayer updates SL/TP on positiongame_usecase.go
match_joinEventMatchJoinPlayer joins a match roomgame_usecase.go
match_startEventMatchStartMatch transitions to “open” statusgame_usecase.go
match_canceledEventMatchCanceledAdmin cancels a matchgame_usecase.go
loginEventLoginUser logs in (email OTP or password)authentication_usecase.go
profile_updateEventProfileUpdateUser updates profile (nickname/avatar)authentication_usecase.go
depositEventDepositFunds deposited to walletwallet_usecase.go
withdrawalEventWithdrawalWithdrawal request createdwallet_usecase.go
level_upEventLevelUpUser gains XP (via consumable or match)consumable_usecase.go
friend_request_sentEventFriendRequestSentUser sends a friend requestfriendship_usecase.go
friend_request_acceptedEventFriendRequestAcceptedFriend request acceptedfriendship_usecase.go
store_purchaseEventStorePurchaseUser purchases a store itemstore_usecase.go

Metrics

The achievement engine computes 13 metrics from the database at evaluation time. Adding a new achievement rule using any of these metrics requires NO code changes — just create it in the admin panel.

MetricTypeDescriptionDB Source
matchesuint64Total matches playedGameMatchParticipant count
winsuint64Total matches wonGameMatch where winner_id = userID
lossesuint64Total matches lostmatches - wins
positionsuint64Total positions openedSum of positions across all participations
wins_in_rowint64Current consecutive win streakLast 20 closed matches
total_volumefloat64Cumulative position sizeSum of GameMatchPosition.Size
total_profitfloat64Cumulative realized PnLSum of GameMatchPosition.Profit
inviteint64Accepted friend countFriendship where status=accepted
leveluint8Current user levelUser.Level
depositsuint64Total deposit transactionsTransaction where type=deposit
withdrawalsuint64Total withdrawal transactionsTransaction where type=withdraw
profile_completedfloat641.0 if nickname+avatar set, 0 otherwiseUser.NickName + User.Avatar
trades_closeduint64Total closed positionsGameMatchPosition where status=closed

Rule Comparators

Each achievement rule specifies a comparator that determines how progress is calculated:

ComparatorBehaviorExample
gteProgress = current / target (0-100%). Completes when current >= target”Win 5 matches” → 3/5 = 60%
gtCompletes when current > target. Shows progress ratio until then”Score above 1000”
lteBinary: 100% if current <= target, 0% otherwise”Keep losses under 5”
ltBinary: 100% if current < target, 0% otherwise”Fewer than 3 liquidations”
eqBinary: 100% only when current == target exactly”Reach exactly level 10”
boolBinary: 100% if current != 0, 0% if current == 0”Complete your profile”

Weighted Multi-Rule Achievements

Achievements can have multiple rules with weights that sum to 100:

Achievement: "Trading Master" ├── Rule 1: wins gte 10, weight 50 → 50% of progress ├── Rule 2: total_profit gte 500, weight 30 → 30% of progress └── Rule 3: trades_closed gte 100, weight 20 → 20% of progress Total progress = sum(each rule's progress × weight) Completed when total >= 100

Reward Disbursement

When an achievement is completed for the first time:

  1. The rewards edge is loaded (links to Consumable items)
  2. Each consumable is applied to the user via ApplyConsumable
  3. An in-app notification is sent (ACHIEVEMENT_UNLOCKED)
  4. The UserAchievement record is marked is_completed = true with completed_at timestamp

A double-grant guard ensures rewards are only granted once — if CheckAchievementActivity runs again for an already-completed achievement, it skips reward granting and notification.

Worker Deployment

The achievement worker is a separate binary:

go run . achievement

It runs a NATS JetStream durable pull consumer on stream ACHIEVEMENTS with subjects achievements.events.>. Messages are processed by a semaphore-bounded goroutine pool (max 50 concurrent workers).

Scaling: Run multiple instances of the achievement worker — JetStream automatically load-balances messages across consumers with the same durable name.

Creating Achievements (Admin Panel)

Step 1: Create Achievement Rules

In the admin panel, navigate to Achievement Rules and create rules:

FieldValue
Name”Win 5 matches”
Fieldwins (dropdown)
Comparatorgte (dropdown)
Target5
Weight100

Step 2: Create Achievement

Navigate to Achievements and create:

FieldValue
Name”Rising Star”
Keyrising_star (unique slug)
Description”Win your first 5 matches”
AvatarUpload badge image
Is Activetrue
RulesSelect the rule(s) created in step 1
RewardsSelect consumable reward(s) (optional)

Step 3: Test

  1. Log in as a test user
  2. Play and win 5 matches
  3. Check achievement progress via UserAchievement query
  4. On 5th win, the achievement completes automatically

No deployment or code changes needed.

GraphQL API

Query User Achievements

query { profile { achievements { edges { node { id progress isCompleted completedAt achievement { name key avatar description } } } } } }

Admin: Seed Permissions

mutation { seedPermissions seedAchievements }

Seed Achievements (Built-in)

The following achievements are seeded via seedAchievements mutation:

AchievementRulesReward
First Winwins >= 150 XP
Win Streakwins_in_row >= 3100 XP
Trading Veterantrades_closed >= 50
High Rollertotal_volume >= 10000
Profile Completeprofile_completed = true (bool)25 XP
Championwins >= 10 (w:50) + total_profit >= 500 (w:50)
First Depositdeposits >= 1
Social Butterflyinvite >= 3
Last updated on