Points System
Overview
The Med.Fun points system is a gamification layer that rewards users for platform engagement. Users earn points for various activities, level up as they accumulate points, and unlock badges at milestone levels.
How It Works
Earn Points: Complete actions like trading, logging in, creating tokens, referring friends
Accumulate: Points are added to your total permanently (no decay)
Level Up: Every 1,000 points equals one level
Unlock Badges: Earn special badges at levels 5, 10, 20, and 50
Track Progress: View point history and progress to next level
Recognition: Display your level and badges on your profile
Database Schema
user_points
user_pointsMain table storing user point totals and progression.
id
uuid
No
gen_random_uuid()
Primary key
user_wallet
text
No
-
User's wallet address
total_points
integer
No
0
Cumulative points
level
integer
No
1
Current level
badges
jsonb
No
[]
Array of earned badges
created_at
timestamptz
No
now()
Account creation
updated_at
timestamptz
No
now()
Last point change
Constraints:
Unique index on
user_walletCheck:
total_points >= 0Check:
level >= 1
RLS Policies:
Users can view their own points
Users can create their initial points record
Users can update their own points
System can insert/update points
point_transactions
point_transactionsHistorical record of all point-earning activities.
id
uuid
No
gen_random_uuid()
Primary key
user_wallet
text
No
-
User's wallet address
points
integer
No
-
Points awarded (can be negative)
action_type
text
No
-
Type of action
description
text
Yes
null
Human-readable description
metadata
jsonb
Yes
{}
Additional context data
created_at
timestamptz
No
now()
Transaction timestamp
Common Action Types:
daily_login- Daily login bonustrade_completed- Completed a tradetoken_created- Created a new tokenreferral- Referred a friendcomment- Left a commentstream_started- Started a livestreamachievement- Unlocked an achievement
Constraints:
Foreign key to
user_points(user_wallet)(conceptual)
RLS Policies:
Users can view their own transactions
System can insert transactions
Point Earning Activities
Default Point Values
Daily Login
+10
First login each day
Complete Trade
+50
Buy or sell tokens
Create Token
+200
Launch a new token
Refer Friend
+100
Friend signs up with your code
Leave Comment
+5
Comment on a token
Start Stream
+150
Go live on a token
Unlock Achievement
Varies
Special achievements
Custom Point Awards
The system supports custom point awards with metadata:
await addPoints(
500, // Points amount
'special_event', // Action type
'Participated in launch event', // Description
{ eventId: '123', prize: 'gold' } // Metadata
);Negative Points
While not commonly used, the system supports negative point values for penalties:
await addPoints(
-25,
'violation',
'Community guideline violation',
{ violationType: 'spam' }
);Level System
Level Calculation
Formula: Level = floor(totalPoints / 1000) + 1
Examples:
0-999 points = Level 1
1,000-1,999 points = Level 2
2,000-2,999 points = Level 3
10,000-10,999 points = Level 11
Points Per Level
const POINTS_PER_LEVEL = 1000;Each level requires 1,000 points. This creates a linear progression system that's easy to understand and predict.
Level Progress Tracking
Calculate current level:
const level = Math.floor(totalPoints / POINTS_PER_LEVEL) + 1;Calculate progress within current level:
const currentLevelPoints = (level - 1) * POINTS_PER_LEVEL;
const pointsInCurrentLevel = totalPoints - currentLevelPoints;
const progress = (pointsInCurrentLevel / POINTS_PER_LEVEL) * 100;Calculate points to next level:
const pointsToNextLevel = POINTS_PER_LEVEL - pointsInCurrentLevel;Level Benefits
While the current system is primarily recognition-based, levels can be used to unlock features:
Potential Level Benefits:
Access to exclusive channels
Custom profile badges
Priority customer support
Early access to new features
Reduced trading fees
Enhanced referral rates
Badge System
Badge Definitions
const LEVEL_BADGES = [
{ level: 5, name: 'Bronze Explorer', icon: '🥉' },
{ level: 10, name: 'Silver Trader', icon: '🥈' },
{ level: 20, name: 'Gold Master', icon: '🥇' },
{ level: 50, name: 'Diamond Legend', icon: '💎' },
];Badge Unlocking
Badges are automatically awarded when users reach the required level:
Bronze Explorer
5
4,000+
🥉
Silver Trader
10
9,000+
🥈
Gold Master
20
19,000+
🥇
Diamond Legend
50
49,000+
💎
Badge Storage
Badges are stored as a JSONB array in the user_points table:
[
{ \"level\": 5, \"name\": \"Bronze Explorer\", \"icon\": \"🥉\", \"earned_at\": \"2025-01-15T10:30:00Z\" },
{ \"level\": 10, \"name\": \"Silver Trader\", \"icon\": \"🥈\", \"earned_at\": \"2025-02-20T14:45:00Z\" }
]Badge Display
Badges can be displayed in:
User profiles
Leaderboards
Chat messages
Point dashboard
Point Transactions
Transaction Structure
Each transaction represents a single point-earning (or losing) event:
interface PointTransaction {
id: string;
user_wallet: string;
points: number; // Amount of points (positive or negative)
action_type: string; // Type of action
description: string | null; // Human-readable description
metadata: any; // Additional context
created_at: string; // ISO timestamp
}Transaction History
The system maintains a complete history of all point transactions, limited to the most recent 50 entries per user:
const { data: transactions } = await supabase
.from('point_transactions')
.select('*')
.eq('user_wallet', userWallet)
.order('created_at', { ascending: false })
.limit(50);Metadata Usage
Metadata can store contextual information about point awards:
// Trade completion
metadata: {
coinId: 'abc-123',
tradeType: 'buy',
amount: 100.50
}
// Referral
metadata: {
referralCode: 'CRYPTO123',
referredUser: 'wallet_address'
}
// Achievement
metadata: {
achievementId: 'first_trade',
achievementName: 'First Trade Ever'
}Real-time Updates
Supabase Subscriptions
Points Updates:
const pointsChannel = supabase
.channel('user_points_changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'user_points',
filter: `user_wallet=eq.${userWallet}`
},
() => fetchPointsData()
)
.subscribe();Transaction Updates:
const transactionsChannel = supabase
.channel('point_transactions_changes')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'point_transactions',
filter: `user_wallet=eq.${userWallet}`
},
() => fetchPointsData()
)
.subscribe();Auto-refresh
When points change, the UI automatically:
Recalculates level
Updates progress bar
Checks for new badges
Refreshes transaction history
Shows toast notification for level-ups and badges
Frontend Components
usePoints Hook
usePoints HookLocation: /src/hooks/usePoints.tsx
Purpose: Manages all point-related data and operations
State Management:
const {
userPoints, // Full user_points record
pointTransactions, // Array of recent transactions
totalPoints, // Total points earned
level, // Current level
badges, // Array of earned badges
pointsToNextLevel, // Points needed for next level
levelProgress, // Progress percentage (0-100)
loading, // Loading state
addPoints, // Function to add points
refreshData // Function to refresh data
} = usePoints(userWallet);Key Functions:
addPoints:
async function addPoints(
points: number,
actionType: string,
description?: string,
metadata?: any
): Promise<void> {
if (!userWallet) {
toast.error('Please connect your wallet first');
return;
}
try {
// 1. Add point transaction
await supabase
.from('point_transactions')
.insert({
user_wallet: userWallet,
points,
action_type: actionType,
description: description || null,
metadata: metadata || {}
});
// 2. Calculate new totals
const newTotalPoints = totalPoints + points;
const newLevel = Math.floor(newTotalPoints / POINTS_PER_LEVEL) + 1;
const leveledUp = newLevel > level;
// 3. Check for new badges
const newBadges = [...badges];
for (const badge of LEVEL_BADGES) {
if (newLevel >= badge.level && !badges.some(b => b.name === badge.name)) {
newBadges.push({
...badge,
earned_at: new Date().toISOString()
});
}
}
// 4. Update user points
await supabase
.from('user_points')
.update({
total_points: newTotalPoints,
level: newLevel,
badges: newBadges
})
.eq('user_wallet', userWallet);
// 5. Show notifications
if (leveledUp) {
toast.success(`🎉 Level up! You're now level ${newLevel}!`);
}
if (newBadges.length > badges.length) {
const earnedBadge = newBadges[newBadges.length - 1];
toast.success(`🏆 New badge earned: ${earnedBadge.icon} ${earnedBadge.name}!`);
}
// 6. Refresh data
await fetchPointsData();
} catch (error: any) {
console.error('Error adding points:', error);
toast.error(error.message || 'Failed to add points');
}
}fetchPointsData:
async function fetchPointsData() {
if (!userWallet) {
setLoading(false);
return;
}
try {
// 1. Fetch or create user points
let { data: pointsData, error: pointsError } = await supabase
.from('user_points')
.select('*')
.eq('user_wallet', userWallet)
.single();
if (pointsError && pointsError.code !== 'PGRST116') {
throw pointsError;
}
// 2. Create initial points if they don't exist
if (!pointsData) {
const { data: newPoints, error: createError } = await supabase
.from('user_points')
.insert({
user_wallet: userWallet,
total_points: 0,
level: 1,
badges: []
})
.select()
.single();
if (createError) throw createError;
pointsData = newPoints;
}
// 3. Update state
setUserPoints(pointsData);
setTotalPoints(pointsData.total_points);
setLevel(pointsData.level);
setBadges(Array.isArray(pointsData.badges) ? pointsData.badges : []);
// 4. Calculate progress
const currentLevelPoints = (pointsData.level - 1) * POINTS_PER_LEVEL;
const pointsInCurrentLevel = pointsData.total_points - currentLevelPoints;
const progress = (pointsInCurrentLevel / POINTS_PER_LEVEL) * 100;
setLevelProgress(Math.min(100, progress));
setPointsToNextLevel(Math.max(0, POINTS_PER_LEVEL - pointsInCurrentLevel));
// 5. Fetch transactions
const { data: transactionsData, error: transactionsError } = await supabase
.from('point_transactions')
.select('*')
.eq('user_wallet', userWallet)
.order('created_at', { ascending: false })
.limit(50);
if (transactionsError) throw transactionsError;
setPointTransactions(transactionsData || []);
} catch (error) {
console.error('Error fetching points data:', error);
} finally {
setLoading(false);
}
}PointsDashboard Component
PointsDashboard ComponentLocation: /src/components/PointsDashboard.tsx
Features:
Current level and progress display
Points to next level
Badge showcase
Point transaction history
Animated progress bar
Mobile-responsive design
UI Structure:
<Dialog>
<DialogContent>
<DialogHeader>
<DialogTitle>Points & Levels</DialogTitle>
</DialogHeader>
{/* Level & Progress Section */}
<div className="space-y-4">
<div>
<h3>Level {level}</h3>
<p>{totalPoints} total points</p>
</div>
<Progress value={levelProgress} />
<p>{pointsToNextLevel} points to level {level + 1}</p>
</div>
{/* Badges Section */}
<div>
<h3>Badges</h3>
<div className="flex gap-2">
{badges.map(badge => (
<div key={badge.name}>
<span>{badge.icon}</span>
<p>{badge.name}</p>
</div>
))}
</div>
</div>
{/* Transaction History */}
<ScrollArea>
<h3>Recent Activity</h3>
{pointTransactions.map(transaction => (
<div key={transaction.id}>
<p>{transaction.description}</p>
<span>{transaction.points > 0 ? '+' : ''}{transaction.points} points</span>
<time>{formatDate(transaction.created_at)}</time>
</div>
))}
</ScrollArea>
</DialogContent>
</Dialog>Integration Points
Award Points on Trade
// In your trade completion handler
async function handleTradeComplete(tradeData: TradeData) {
// ... existing trade logic ...
// Award points
await addPoints(
50,
'trade_completed',
`Completed ${tradeData.type} trade`,
{
coinId: tradeData.coinId,
amount: tradeData.amount,
tradeType: tradeData.type
}
);
}Award Points on Token Creation
async function handleTokenCreate(tokenData: TokenData) {
// ... existing creation logic ...
await addPoints(
200,
'token_created',
`Created ${tokenData.name}`,
{
tokenId: tokenData.id,
symbol: tokenData.symbol
}
);
}Daily Login Bonus
async function checkDailyLogin(userWallet: string) {
const today = new Date().toISOString().split('T')[0];
// Check if already logged in today
const { data: todayLogin } = await supabase
.from('point_transactions')
.select('*')
.eq('user_wallet', userWallet)
.eq('action_type', 'daily_login')
.gte('created_at', `${today}T00:00:00Z`)
.single();
if (!todayLogin) {
await addPoints(
10,
'daily_login',
'Daily login bonus',
{ date: today }
);
}
}Referral Bonus
async function handleReferralComplete(
referrerWallet: string,
referredWallet: string
) {
await addPoints(
100,
'referral',
'Friend joined using your referral code',
{
referredWallet,
timestamp: new Date().toISOString()
}
);
}User Flows
Flow 1: First Time User
1. User connects wallet
2. System creates user_points record (0 points, level 1)
3. User completes first trade
4. System awards 50 points
5. Level remains 1 (need 1000 for level 2)
6. Transaction appears in historyFlow 2: Level Up
1. User has 950 points (level 1)
2. User creates a token
3. System awards 200 points
4. Total becomes 1,150 points
5. Level automatically updates to 2
6. Toast notification: "🎉 Level up! You're now level 2!"
7. Progress bar resets to show progress in level 2Flow 3: Badge Unlock
1. User reaches level 5 (4,000+ points)
2. System checks LEVEL_BADGES array
3. Bronze Explorer badge criteria met
4. Badge added to user_points.badges array
5. Toast notification: "🏆 New badge earned: 🥉 Bronze Explorer!"
6. Badge appears in dashboardFlow 4: View History
1. User opens points dashboard
2. System fetches last 50 transactions
3. Transactions displayed chronologically
4. Each shows: action, points, description, date
5. User can scroll through historyBest Practices
For Developers
1. Always validate point amounts:
if (points < 0 && Math.abs(points) > totalPoints) {
throw new Error("Cannot deduct more points than user has");
}2. Use descriptive action types:
// Good
addPoints(50, 'trade_completed', 'Completed buy trade');
// Bad
addPoints(50, 'trade', 'trade');3. Include metadata for context:
// Good - includes context
addPoints(200, 'token_created', 'Created DOGE token', {
tokenId: '123',
symbol: 'DOGE'
});
// Acceptable - simple actions may not need metadata
addPoints(10, 'daily_login', 'Daily login bonus');4. Handle errors gracefully:
try {
await addPoints(50, 'trade_completed');
} catch (error) {
console.error('Failed to award points:', error);
// Don't block user flow if points fail
}For Product
1. Point values should feel rewarding
Small actions: 5-10 points
Medium actions: 50-100 points
Large actions: 150-200 points
2. Level progression should be achievable
1,000 points per level = 20 trades per level
First badge at level 5 = ~80-100 actions
Keeps users engaged without feeling impossible
3. Badges should be meaningful
Tied to significant milestones
Not too easy or too hard to obtain
Visual recognition is important
4. Transaction history builds trust
Users can verify point awards
Transparency in gamification
Helps debug issues
Troubleshooting
Points Not Adding
Check:
Is wallet connected?
Does user_points record exist?
Check browser console for errors
Verify database connection
Check RLS policies
Level Not Updating
Check:
Verify calculation:
floor(totalPoints / 1000) + 1Check if database was updated
Verify real-time subscription is active
Try manual refresh
Badges Not Appearing
Check:
Is user at required level?
Check badges array in database
Verify LEVEL_BADGES constant
Check badge comparison logic
Duplicate Transactions
Prevent:
// Use idempotency key
await addPoints(50, 'trade_completed', 'Trade', {
idempotencyKey: `trade_${tradeId}`
});
// Check for duplicates
const existing = await supabase
.from('point_transactions')
.select('*')
.eq('metadata->idempotencyKey', `trade_${tradeId}`)
.single();
if (existing.data) {
return; // Already awarded
}Performance Considerations
Database Indexes
CREATE INDEX idx_user_points_wallet ON user_points(user_wallet);
CREATE INDEX idx_point_transactions_wallet ON point_transactions(user_wallet);
CREATE INDEX idx_point_transactions_created ON point_transactions(created_at DESC);Query Optimization
Limit transaction history to 50 most recent
Use single query for user points (don't aggregate transactions)
Cache badge definitions in application layer
Use database triggers for automatic level calculation
Caching Strategy
// Cache user points for 1 minute
const cachedPoints = useMemo(() => userPoints, [userPoints]);
// Only refetch on explicit actions
const refreshData = useCallback(() => {
fetchPointsData();
}, [userWallet]);Future Enhancements
Potential Features
Leaderboards: Top point earners displayed publicly
Daily Challenges: Bonus points for specific actions
Point Multipliers: 2x points during events
Point Shop: Spend points on perks
Achievement System: Complex multi-step achievements
Seasonal Badges: Limited-time special badges
Point Decay: Points expire after inactivity (optional)
Technical Improvements
Database trigger for automatic level calculation
Point expiration system
Batch point awards for efficiency
Analytics dashboard for point distribution
A/B testing framework for point values
Last updated
Was this helpful?