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

  1. Earn Points: Complete actions like trading, logging in, creating tokens, referring friends

  2. Accumulate: Points are added to your total permanently (no decay)

  3. Level Up: Every 1,000 points equals one level

  4. Unlock Badges: Earn special badges at levels 5, 10, 20, and 50

  5. Track Progress: View point history and progress to next level

  6. Recognition: Display your level and badges on your profile

Database Schema

user_points

Main table storing user point totals and progression.

Column
Type
Nullable
Default
Description

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_wallet

  • Check: total_points >= 0

  • Check: 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

Historical record of all point-earning activities.

Column
Type
Nullable
Default
Description

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 bonus

  • trade_completed - Completed a trade

  • token_created - Created a new token

  • referral - Referred a friend

  • comment - Left a comment

  • stream_started - Started a livestream

  • achievement - 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

Action
Points
Description

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:

Badge
Level Required
Points Required
Icon

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:

  1. Recalculates level

  2. Updates progress bar

  3. Checks for new badges

  4. Refreshes transaction history

  5. Shows toast notification for level-ups and badges

Frontend Components

usePoints Hook

Location: /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

Location: /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 history

Flow 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 2

Flow 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 dashboard

Flow 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 history

Best 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:

  1. Is wallet connected?

  2. Does user_points record exist?

  3. Check browser console for errors

  4. Verify database connection

  5. Check RLS policies

Level Not Updating

Check:

  1. Verify calculation: floor(totalPoints / 1000) + 1

  2. Check if database was updated

  3. Verify real-time subscription is active

  4. Try manual refresh

Badges Not Appearing

Check:

  1. Is user at required level?

  2. Check badges array in database

  3. Verify LEVEL_BADGES constant

  4. 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?