Rewards System

Overview

The Med.Fun rewards system provides cashback on trading fees through a tier-based multiplier system. Users earn cashback on every trade, with higher tiers receiving increased multipliers based on their trading volume.

How It Works

  1. Trade Tokens: Execute buy or sell trades on the platform

  2. Earn Cashback: Receive percentage of trading fees back

  3. Tier Multipliers: Cashback multiplied by your current tier (1x to 3x)

  4. Track Volume: Trading volume determines your tier

  5. Accumulate: Cashback accumulates as "pending"

  6. Claim: Withdraw accumulated cashback (minimum $1.00)

Database Schema

reward_tiers

Defines tier structure and multipliers.

Column
Type
Nullable
Default
Description

id

uuid

No

gen_random_uuid()

Primary key

tier_name

text

No

-

Tier display name

tier_level

integer

No

-

Tier number (1-5)

min_volume

numeric

No

0

Min volume required

max_volume

numeric

Yes

null

Max volume (null = unlimited)

cashback_multiplier

numeric

No

1.0

Multiplier for cashback

tier_color

text

No

-

UI color code

benefits

jsonb

No

{}

Additional tier benefits

created_at

timestamptz

No

now()

Creation timestamp

Default Tiers:

Tier
Level
Min Volume
Max Volume
Multiplier
Color

Intern

1

$0

$1,000

1.0x

Gray

Resident

2

$1,000

$10,000

1.5x

Blue

Chief Surgeon

3

$10,000

$50,000

2.0x

Purple

Surgical Trader

4

$50,000

$250,000

2.5x

Gold

Doctor of Degeneracy

5

$250,000

3.0x

Rainbow

Constraints:

  • Unique on tier_level

  • Check: cashback_multiplier > 0

RLS Policies:

  • Anyone can view tiers

  • System can manage tiers

user_trading_stats

Tracks user trading activity and current tier.

Column
Type
Nullable
Default
Description

id

uuid

No

gen_random_uuid()

Primary key

user_wallet

text

No

-

User's wallet address

total_volume

numeric

No

0

Lifetime trading volume

total_trades

integer

No

0

Total trade count

current_tier_id

uuid

Yes

null

Current tier ID

lifetime_cashback

numeric

No

0

Total cashback earned

created_at

timestamptz

No

now()

Stats creation

updated_at

timestamptz

No

now()

Last update

Constraints:

  • Unique index on user_wallet

  • Foreign key to reward_tiers(id)

  • Check: total_volume >= 0

  • Check: total_trades >= 0

RLS Policies:

  • Users can view their own stats

  • Users can create their own stats

  • Users can update their own stats

  • System can manage all stats

cashback_earnings

Records individual cashback transactions.

Column
Type
Nullable
Default
Description

id

uuid

No

gen_random_uuid()

Primary key

user_wallet

text

No

-

Earner's wallet

trade_id

uuid

Yes

null

Related trade ID

trade_volume

numeric

No

-

Trade amount

cashback_amount

numeric

No

-

Cashback earned

multiplier

numeric

No

-

Tier multiplier applied

status

text

No

'pending'

pending/claimed

created_at

timestamptz

No

now()

Earned timestamp

claimed_at

timestamptz

Yes

null

Claimed timestamp

Constraints:

  • Foreign key to user_trading_stats(user_wallet)

  • Check: cashback_amount >= 0

  • Check: multiplier > 0

RLS Policies:

  • Users can view their own earnings

  • Users can update their own earnings (claim)

  • System can insert earnings

Reward Tiers

Tier Definitions

Tier 1: Intern

  • Min Volume: $0

  • Max Volume: $999.99

  • Multiplier: 1.0x (base rate)

  • Color: #6B7280 (Gray)

  • Benefits:

    • Access to basic cashback

    • Transaction history

    • Standard support

Tier 2: Resident

  • Min Volume: $1,000

  • Max Volume: $9,999.99

  • Multiplier: 1.5x (+50% boost)

  • Color: #3B82F6 (Blue)

  • Benefits:

    • All Tier 1 benefits

    • Priority transaction processing

    • Email support

Tier 3: Chief Surgeon

  • Min Volume: $10,000

  • Max Volume: $49,999.99

  • Multiplier: 2.0x (+100% boost)

  • Color: #8B5CF6 (Purple)

  • Benefits:

    • All Tier 2 benefits

    • Advanced analytics

    • Custom dashboard

    • Priority support

Tier 4: Surgical Trader

  • Min Volume: $50,000

  • Max Volume: $249,999.99

  • Multiplier: 2.5x (+150% boost)

  • Color: #F59E0B (Gold)

  • Benefits:

    • All Tier 3 benefits

    • API access

    • Custom alerts

    • VIP support channel

Tier 5: Doctor of Degeneracy

  • Min Volume: $250,000+

  • Max Volume: Unlimited

  • Multiplier: 3.0x (+200% boost)

  • Color: linear-gradient(rainbow) (Rainbow)

  • Benefits:

    • All Tier 4 benefits

    • Dedicated account manager

    • Custom fee structures

    • Early access to features

    • Exclusive community access

Tier Progression

Automatic Tier Assignment:

function determineTier(totalVolume: number, tiers: RewardTier[]): RewardTier {
  // Sort tiers by min_volume descending
  const sortedTiers = [...tiers].sort((a, b) => b.min_volume - a.min_volume);
  
  // Find first tier where user qualifies
  for (const tier of sortedTiers) {
    if (totalVolume >= tier.min_volume) {
      if (tier.max_volume === null || totalVolume <= tier.max_volume) {
        return tier;
      }
    }
  }
  
  // Default to Tier 1
  return tiers.find(t => t.tier_level === 1)!;
}

Progress Calculation:

function calculateTierProgress(
  currentVolume: number,
  currentTier: RewardTier,
  nextTier: RewardTier | null
): number {
  if (!nextTier) return 100; // Max tier reached
  
  const tierVolumeRange = nextTier.min_volume - currentTier.min_volume;
  const volumeInTier = currentVolume - currentTier.min_volume;
  
  return (volumeInTier / tierVolumeRange) * 100;
}

Cashback Calculation

Base Cashback Formula

// Trading fee is typically 1% of trade volume
const tradingFee = tradeVolume * 0.01;

// Base cashback (before multiplier)
const baseCashback = tradingFee * BASE_CASHBACK_RATE; // e.g., 10% of fee

// Apply tier multiplier
const finalCashback = baseCashback * tierMultiplier;

Example Calculations

Example 1: Intern (1.0x multiplier)

Trade Volume: $1,000
Trading Fee (1%): $10.00
Base Cashback (10%): $1.00
Tier Multiplier: 1.0x
Final Cashback: $1.00

Example 2: Chief Surgeon (2.0x multiplier)

Trade Volume: $5,000
Trading Fee (1%): $50.00
Base Cashback (10%): $5.00
Tier Multiplier: 2.0x
Final Cashback: $10.00

Example 3: Doctor of Degeneracy (3.0x multiplier)

Trade Volume: $10,000
Trading Fee (1%): $100.00
Base Cashback (10%): $10.00
Tier Multiplier: 3.0x
Final Cashback: $30.00

Cashback Tracking

async function recordTradeCashback(
  userWallet: string,
  tradeId: string,
  tradeVolume: number,
  tradeFee: number
) {
  // 1. Get user's current tier
  const { data: stats } = await supabase
    .from('user_trading_stats')
    .select('*, reward_tiers!inner(*)')
    .eq('user_wallet', userWallet)
    .single();
  
  if (!stats) return;
  
  // 2. Calculate cashback
  const baseCashback = tradeFee * 0.10; // 10% of fee
  const cashbackAmount = baseCashback * stats.reward_tiers.cashback_multiplier;
  
  // 3. Record earning
  await supabase
    .from('cashback_earnings')
    .insert({
      user_wallet: userWallet,
      trade_id: tradeId,
      trade_volume: tradeVolume,
      cashback_amount: cashbackAmount,
      multiplier: stats.reward_tiers.cashback_multiplier,
      status: 'pending'
    });
  
  // 4. Update stats
  await supabase
    .from('user_trading_stats')
    .update({
      total_volume: stats.total_volume + tradeVolume,
      total_trades: stats.total_trades + 1,
      lifetime_cashback: stats.lifetime_cashback + cashbackAmount
    })
    .eq('user_wallet', userWallet);
}

Trading Stats Tracking

Stats Initialization

When a user makes their first trade:

async function initializeTradingStats(userWallet: string) {
  // Get Tier 1 (Intern)
  const { data: tier1 } = await supabase
    .from('reward_tiers')
    .select('*')
    .eq('tier_level', 1)
    .single();
  
  // Create stats record
  await supabase
    .from('user_trading_stats')
    .insert({
      user_wallet: userWallet,
      total_volume: 0,
      total_trades: 0,
      current_tier_id: tier1.id,
      lifetime_cashback: 0
    });
}

Stats Updates

After each trade:

async function updateTradingStats(
  userWallet: string,
  tradeVolume: number,
  cashbackEarned: number
) {
  // 1. Update volume and trade count
  const { data: updatedStats } = await supabase
    .from('user_trading_stats')
    .update({
      total_volume: stats.total_volume + tradeVolume,
      total_trades: stats.total_trades + 1,
      lifetime_cashback: stats.lifetime_cashback + cashbackEarned,
      updated_at: new Date().toISOString()
    })
    .eq('user_wallet', userWallet)
    .select()
    .single();
  
  // 2. Check for tier upgrade
  await checkTierUpgrade(userWallet, updatedStats.total_volume);
}

Tier Upgrade Check

async function checkTierUpgrade(
  userWallet: string,
  totalVolume: number
) {
  // 1. Get all tiers
  const { data: tiers } = await supabase
    .from('reward_tiers')
    .select('*')
    .order('min_volume', { ascending: false });
  
  // 2. Determine appropriate tier
  const newTier = tiers.find(tier => 
    totalVolume >= tier.min_volume &&
    (tier.max_volume === null || totalVolume <= tier.max_volume)
  );
  
  if (!newTier) return;
  
  // 3. Get current tier
  const { data: currentStats } = await supabase
    .from('user_trading_stats')
    .select('current_tier_id')
    .eq('user_wallet', userWallet)
    .single();
  
  // 4. Update if tier changed
  if (currentStats.current_tier_id !== newTier.id) {
    await supabase
      .from('user_trading_stats')
      .update({ current_tier_id: newTier.id })
      .eq('user_wallet', userWallet);
    
    // 5. Notify user
    toast.success(`🎉 Tier upgraded to ${newTier.tier_name}!`);
  }
}

Claiming Cashback

Claim Requirements

  • Minimum Amount: $1.00 USD

  • Status: Must be 'pending'

  • User Action: Manual claim via dashboard

Claim Process

async function claimCashback(userWallet: string) {
  // 1. Check pending balance
  const { data: earnings } = await supabase
    .from('cashback_earnings')
    .select('cashback_amount')
    .eq('user_wallet', userWallet)
    .eq('status', 'pending');
  
  const pendingAmount = earnings?.reduce(
    (sum, e) => sum + parseFloat(e.cashback_amount),
    0
  ) || 0;
  
  if (pendingAmount < 1.00) {
    throw new Error('Minimum $1.00 required to claim');
  }
  
  // 2. Update earnings status
  const { error } = await supabase
    .from('cashback_earnings')
    .update({
      status: 'claimed',
      claimed_at: new Date().toISOString()
    })
    .eq('user_wallet', userWallet)
    .eq('status', 'pending');
  
  if (error) throw error;
  
  // 3. Process withdrawal (implementation specific)
  await processWithdrawal(userWallet, pendingAmount);
  
  // 4. Show success
  toast.success(`Successfully claimed $${pendingAmount.toFixed(2)}!`);
}

Claim History

Users can view claimed cashback history:

const { data: claimedEarnings } = await supabase
  .from('cashback_earnings')
  .select('*')
  .eq('user_wallet', userWallet)
  .eq('status', 'claimed')
  .order('claimed_at', { ascending: false });

Real-time Updates

Supabase Subscriptions

Trading Stats Updates:

const statsChannel = supabase
  .channel('user_trading_stats_changes')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'user_trading_stats',
      filter: `user_wallet=eq.${userWallet}`
    },
    () => fetchRewardsData()
  )
  .subscribe();

Cashback Earnings Updates:

const earningsChannel = supabase
  .channel('cashback_earnings_changes')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'cashback_earnings',
      filter: `user_wallet=eq.${userWallet}`
    },
    () => fetchRewardsData()
  )
  .subscribe();

Frontend Components

useRewards Hook

Location: /src/hooks/useRewards.tsx

Purpose: Manages all rewards-related data and operations

State Management:

const {
  tiers,                 // All reward tiers
  userStats,             // User's trading stats
  cashbackEarnings,      // Earnings history
  totalCashback,         // Lifetime cashback
  pendingCashback,       // Unclaimed amount
  claimedCashback,       // Claimed amount
  currentTier,           // Current tier object
  nextTier,              // Next tier object
  tierProgress,          // Progress percentage
  volumeToNextTier,      // Volume needed
  loading,               // Loading state
  claiming,              // Claiming state
  claimCashback,         // Function
  refreshData            // Function
} = useRewards(userWallet);

Key Functions:

fetchRewardsData:

async function fetchRewardsData() {
  if (!userWallet) {
    setLoading(false);
    return;
  }
  
  try {
    // 1. Fetch all tiers
    const { data: tiersData } = await supabase
      .from('reward_tiers')
      .select('*')
      .order('tier_level');
    
    setTiers(tiersData || []);
    
    // 2. Fetch or create user stats
    let { data: statsData } = await supabase
      .from('user_trading_stats')
      .select('*, reward_tiers!inner(*)')
      .eq('user_wallet', userWallet)
      .single();
    
    if (!statsData) {
      // Create initial stats
      const tier1 = tiersData?.find(t => t.tier_level === 1);
      const { data: newStats } = await supabase
        .from('user_trading_stats')
        .insert({
          user_wallet: userWallet,
          total_volume: 0,
          total_trades: 0,
          current_tier_id: tier1?.id,
          lifetime_cashback: 0
        })
        .select('*, reward_tiers!inner(*)')
        .single();
      
      statsData = newStats;
    }
    
    setUserStats(statsData);
    setCurrentTier(statsData.reward_tiers);
    
    // 3. Calculate next tier and progress
    const nextTierData = tiersData?.find(
      t => t.tier_level === statsData.reward_tiers.tier_level + 1
    );
    setNextTier(nextTierData || null);
    
    if (nextTierData) {
      const progress = 
        ((statsData.total_volume - statsData.reward_tiers.min_volume) /
        (nextTierData.min_volume - statsData.reward_tiers.min_volume)) * 100;
      setTierProgress(Math.min(100, Math.max(0, progress)));
      setVolumeToNextTier(nextTierData.min_volume - statsData.total_volume);
    }
    
    // 4. Fetch cashback earnings
    const { data: earningsData } = await supabase
      .from('cashback_earnings')
      .select('*')
      .eq('user_wallet', userWallet)
      .order('created_at', { ascending: false });
    
    setCashbackEarnings(earningsData || []);
    
    // 5. Calculate totals
    const total = earningsData?.reduce(
      (sum, e) => sum + parseFloat(e.cashback_amount),
      0
    ) || 0;
    const pending = earningsData
      ?.filter(e => e.status === 'pending')
      .reduce((sum, e) => sum + parseFloat(e.cashback_amount), 0) || 0;
    const claimed = earningsData
      ?.filter(e => e.status === 'claimed')
      .reduce((sum, e) => sum + parseFloat(e.cashback_amount), 0) || 0;
    
    setTotalCashback(total);
    setPendingCashback(pending);
    setClaimedCashback(claimed);
  } catch (error) {
    console.error('Error fetching rewards data:', error);
  } finally {
    setLoading(false);
  }
}

TradingRewardsDashboard Component

Location: /src/components/TradingRewardsDashboard.tsx

Features:

  • Tabbed interface (Overview, History, Tiers)

  • Current tier display with color coding

  • Cashback summary (pending/claimed)

  • Trading stats (volume, trades)

  • Tier progress bar

  • Claim button

  • Earnings history list

  • All tiers overview

  • Mobile-responsive

UI Structure:

<Tabs defaultValue="overview">
  <TabsList>
    <TabsTrigger value="overview">Overview</TabsTrigger>
    <TabsTrigger value="history">History</TabsTrigger>
    <TabsTrigger value="tiers">Tiers</TabsTrigger>
  </TabsList>
  
  <TabsContent value="overview">
    {/* Tier badge, cashback amounts, stats, progress */}
  </TabsContent>
  
  <TabsContent value="history">
    {/* Cashback earnings list */}
  </TabsContent>
  
  <TabsContent value="tiers">
    {/* All tiers with requirements and benefits */}
  </TabsContent>
</Tabs>

RewardsCard Component

Location: /src/components/RewardsCard.tsx

Purpose: Summary card for quick access

Displayed Data:

  • Current tier name and icon

  • Pending cashback amount

  • Total earned amount

  • "View Details" button

Integration Points

Record Trade Cashback

// After trade execution
async function handleTradeComplete(trade: TradeData) {
  // 1. Calculate fee
  const tradeFee = trade.volume * 0.01; // 1%
  
  // 2. Get user tier
  const { data: stats } = await supabase
    .from('user_trading_stats')
    .select('*, reward_tiers!inner(*)')
    .eq('user_wallet', trade.userWallet)
    .single();
  
  if (!stats) {
    await initializeTradingStats(trade.userWallet);
    return;
  }
  
  // 3. Calculate cashback
  const baseCashback = tradeFee * 0.10;
  const cashback = baseCashback * stats.reward_tiers.cashback_multiplier;
  
  // 4. Record earning
  await supabase
    .from('cashback_earnings')
    .insert({
      user_wallet: trade.userWallet,
      trade_id: trade.id,
      trade_volume: trade.volume,
      cashback_amount: cashback,
      multiplier: stats.reward_tiers.cashback_multiplier,
      status: 'pending'
    });
  
  // 5. Update stats
  await updateTradingStats(
    trade.userWallet,
    trade.volume,
    cashback
  );
  
  // 6. Check tier upgrade
  await checkTierUpgrade(
    trade.userWallet,
    stats.total_volume + trade.volume
  );
}

User Flows

Flow 1: First Trade & Cashback

1. User connects wallet
2. User makes first trade ($100)
3. System creates trading_stats record (Tier 1)
4. Trading fee calculated: $1.00
5. Base cashback: $0.10 (10% of fee)
6. Tier multiplier applied: 1.0x
7. Final cashback: $0.10
8. Cashback recorded as 'pending'
9. Stats updated: volume = $100, trades = 1

Flow 2: Tier Upgrade

1. User has $9,500 volume (Tier 1: Intern)
2. User makes trade for $1,000
3. New total volume: $10,500
4. System checks tier eligibility
5. Qualifies for Tier 3: Chief Surgeon ($10k+)
6. Tier upgraded in database
7. Multiplier increases: 1.0x → 2.0x
8. Toast: "🎉 Tier upgraded to Chief Surgeon!"
9. Future cashback uses 2.0x multiplier

Flow 3: Claim Cashback

1. User accumulates $5.50 pending cashback
2. User opens rewards dashboard
3. Sees "$5.50 Available to Claim"
4. Clicks "Claim Cashback" button
5. System validates minimum ($1.00) ✓
6. Status updated: pending → claimed
7. claimed_at timestamp recorded
8. Withdrawal processed
9. Balance updates in UI
10. Toast: "Successfully claimed $5.50!"

Best Practices

For Developers

1. Always use transactions for multi-step operations:

// Use Supabase RPC for atomic operations
await supabase.rpc('record_trade_with_cashback', {
  p_user: userWallet,
  p_volume: tradeVolume,
  p_fee: tradeFee
});

2. Handle tier upgrades gracefully:

// Check tier after every stats update
if (newVolume >= nextTier.min_volume) {
  await upgradeTier(userWallet, nextTier.id);
}

3. Validate cashback calculations:

// Never allow negative cashback
if (cashbackAmount < 0) {
  throw new Error("Invalid cashback calculation");
}

// Cap at reasonable maximum
const MAX_CASHBACK = tradeFee * 0.50; // 50% of fee max
cashbackAmount = Math.min(cashbackAmount, MAX_CASHBACK);

For Product

1. Tier thresholds encourage growth

  • Each tier ~10x previous tier volume

  • Creates clear progression goals

  • Rewards high-volume traders

2. Minimum claim prevents micro-transactions

  • $1.00 minimum balances network costs

  • Encourages accumulation

  • Reduces support burden

3. Real-time updates build engagement

  • Instant feedback on earnings

  • Progress bars show advancement

  • Tier upgrades feel rewarding

Troubleshooting

Cashback Not Recording

Check:

  1. Does user_trading_stats record exist?

  2. Is current_tier_id set correctly?

  3. Verify cashback calculation logic

  4. Check database triggers/functions

Tier Not Upgrading

Check:

  1. Is total_volume updating correctly?

  2. Verify tier min_volume thresholds

  3. Check tier upgrade logic

  4. Confirm current_tier_id updates

Cannot Claim Cashback

Check:

  1. Is pending balance >= $1.00?

  2. Are earnings in 'pending' status?

  3. Verify wallet connection

  4. Check RLS policies

Performance Considerations

Database Indexes

CREATE INDEX idx_trading_stats_wallet ON user_trading_stats(user_wallet);
CREATE INDEX idx_cashback_earnings_wallet ON cashback_earnings(user_wallet);
CREATE INDEX idx_cashback_earnings_status ON cashback_earnings(status);
CREATE INDEX idx_reward_tiers_level ON reward_tiers(tier_level);

Caching Strategy

  • Cache tier definitions (rarely change)

  • Fetch user stats on demand

  • Use real-time for automatic updates

  • Aggregate earnings in application layer

Future Enhancements

Potential Features

  • Bonus Multipliers: Limited-time cashback boosts

  • VIP Tiers: Exclusive invite-only tiers

  • Cashback Tokens: Earn native platform tokens

  • Leaderboards: Top earners by tier

  • Referral Bonuses: Extra cashback for referrals

  • Auto-Claim: Automatic claiming at threshold

Technical Improvements

  • Database function for atomic cashback recording

  • Scheduled job for batch tier upgrades

  • Analytics dashboard for cashback trends

  • A/B testing framework for multipliers

Last updated

Was this helpful?