Overview

Build loyalty programs that reward user engagement through:

  • Quest-based point accumulation
  • Leaderboard-driven competition
  • User progress tracking via rankings
  • Sprint-specific campaigns

Understanding Loyalty Points

Quest Points System

Each quest can award loyalty points to participants:

query GetQuestPoints($questId: ID!) {
  quest(id: $questId) {
    id
    name
    loyaltyPoints
    status
    space {
      id
      name
    }
  }
}

Response:

{
  "data": {
    "quest": {
      "id": "GChdWUjXX3",
      "name": "Introduction to Web3!",
      "loyaltyPoints": 100,
      "status": "Active",
      "space": {
        "id": "40",
        "name": "BNB Chain"
      }
    }
  }
}

Get User Points via Leaderboard

query GetLeaderboard($spaceId: Int!, $cursorAfter: String) {
  space(id: $spaceId) {
    id
    name
    loyaltyPointsRanks(cursorAfter: $cursorAfter) {
      totalCount
      pageInfo {
        hasNextPage
        endCursor
      }
      list {
        rank
        points
        address {
          username
          avatar
          address
        }
      }
    }
  }
}

Basic Implementation

Find User Points

async function findUserPoints(spaceId, userAddress, accessToken) {
  let cursorAfter = null;
  let pageCount = 0;
  const maxPages = 20; // Reasonable search limit
  
  while (pageCount < maxPages) {
    const response = await fetch('https://graphigo-business.prd.galaxy.eco/query', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'access-token': accessToken
      },
      body: JSON.stringify({
        query: `query GetLeaderboard($spaceId: Int!, $cursorAfter: String) {
          space(id: $spaceId) {
            loyaltyPointsRanks(cursorAfter: $cursorAfter) {
              totalCount
              pageInfo { hasNextPage endCursor }
              list {
                rank
                points
                address { address username avatar }
              }
            }
          }
        }`,
        variables: { spaceId, cursorAfter }
      })
    });
    
    const data = await response.json();
    const ranks = data.data.space.loyaltyPointsRanks;
    
    // Search current page
    const userRank = ranks.list.find(item => 
      item.address.address.toLowerCase() === userAddress.toLowerCase()
    );
    
    if (userRank) {
      return {
        found: true,
        points: userRank.points,
        rank: userRank.rank,
        username: userRank.address.username,
        totalParticipants: ranks.totalCount,
        searchPages: pageCount + 1
      };
    }
    
    if (!ranks.pageInfo.hasNextPage) break;
    
    cursorAfter = ranks.pageInfo.endCursor;
    pageCount++;
    
    // Rate limiting
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  
  return { 
    found: false, 
    points: 0, 
    rank: null,
    totalParticipants: pageCount > 0 ? ranks.totalCount : 0,
    searchPages: pageCount 
  };
}

Get Quest Points Information

async function getQuestPointValue(questId, accessToken) {
  const response = await fetch('https://graphigo-business.prd.galaxy.eco/query', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'access-token': accessToken
    },
    body: JSON.stringify({
      query: `query GetQuestPoints($questId: ID!) {
        quest(id: $questId) {
          id
          name
          loyaltyPoints
          status
          space { id name }
        }
      }`,
      variables: { questId }
    })
  });
  
  const data = await response.json();
  return data.data.quest;
}

Sprint-Based Programs

Sprint vs Overall Comparison

query GetSprintLeaderboard($spaceId: Int!, $sprintId: Int!, $cursorAfter: String) {
  space(id: $spaceId) {
    loyaltyPointsRanks(
      sprintId: $sprintId
      cursorAfter: $cursorAfter
    ) {
      totalCount
      list {
        rank
        points
        address {
          username
          address
        }
      }
    }
  }
}

Sprint Management: Sprint information must come from external sources as sprint management is not available through this API.

Compare User Progress

async function compareUserProgress(spaceId, userAddress, sprintId, accessToken) {
  const [overall, sprint] = await Promise.all([
    findUserPoints(spaceId, userAddress, accessToken),
    findUserPointsInSprint(spaceId, userAddress, sprintId, accessToken)
  ]);
  
  return {
    overall: {
      points: overall.points,
      rank: overall.rank,
      found: overall.found
    },
    sprint: {
      points: sprint.points,
      rank: sprint.rank,
      found: sprint.found,
      sprintId
    },
    comparison: {
      pointsDifference: sprint.points - overall.points,
      rankImprovement: overall.rank && sprint.rank 
        ? overall.rank - sprint.rank 
        : null,
      sprintPerformance: sprint.rank && overall.rank
        ? sprint.rank < overall.rank ? 'better' : 'worse'
        : 'unknown'
    }
  };
}

// Similar to findUserPoints but with sprintId parameter
async function findUserPointsInSprint(spaceId, userAddress, sprintId, accessToken) {
  // Implementation follows same pattern as findUserPoints
  // but includes sprintId in the loyaltyPointsRanks query
}

User Dashboard Pattern

Create User Dashboard

async function createUserDashboard(spaceId, userAddress, accessToken) {
  const userData = await findUserPoints(spaceId, userAddress, accessToken);
  
  if (!userData.found) {
    return {
      user: {
        address: userAddress,
        points: 0,
        rank: null,
        isRanked: false,
        message: 'User not found in top rankings'
      },
      leaderboard: {
        totalParticipants: userData.totalParticipants,
        searchedPages: userData.searchPages
      }
    };
  }
  
  // Calculate percentile
  const percentile = userData.totalParticipants 
    ? ((userData.totalParticipants - userData.rank) / userData.totalParticipants) * 100 
    : 0;
  
  return {
    user: {
      address: userAddress,
      username: userData.username,
      points: userData.points,
      rank: userData.rank,
      isRanked: true,
      percentile: Math.round(percentile)
    },
    leaderboard: {
      totalParticipants: userData.totalParticipants,
      searchedPages: userData.searchPages
    },
    spaceInfo: {
      id: spaceId
    }
  };
}

Quest Integration

Track Quest Completion

async function trackQuestCompletion(questId, userAddress, accessToken) {
  const quest = await getQuestPointValue(questId, accessToken);
  
  if (!quest) {
    return { error: 'Quest not found' };
  }
  
  if (quest.loyaltyPoints === 0) {
    return { message: 'Quest does not award loyalty points' };
  }
  
  const userPoints = await findUserPoints(
    parseInt(quest.space.id), 
    userAddress, 
    accessToken
  );
  
  return {
    quest: {
      id: quest.id,
      name: quest.name,
      points: quest.loyaltyPoints,
      status: quest.status,
      space: quest.space
    },
    user: {
      currentPoints: userPoints.points,
      currentRank: userPoints.rank,
      projectedPoints: userPoints.points + quest.loyaltyPoints,
      isCurrentlyRanked: userPoints.found
    },
    impact: {
      pointsGain: quest.loyaltyPoints
    }
  };
}

Common Integration Patterns

Points-Based Access Control

async function checkPointsRequirement(spaceId, userAddress, requiredPoints, accessToken) {
  const userPoints = await findUserPoints(spaceId, userAddress, accessToken);
  
  return {
    hasAccess: userPoints.points >= requiredPoints,
    userPoints: userPoints.points,
    requiredPoints,
    deficit: Math.max(0, requiredPoints - userPoints.points),
    rank: userPoints.rank,
    isRanked: userPoints.found
  };
}

Achievement Thresholds

function calculateAchievements(userPoints) {
  const thresholds = [
    { name: 'Newcomer', points: 10 },
    { name: 'Explorer', points: 100 },
    { name: 'Adventurer', points: 500 },
    { name: 'Champion', points: 1000 },
    { name: 'Legend', points: 5000 }
  ];
  
  const achieved = thresholds.filter(t => userPoints >= t.points);
  const nextThreshold = thresholds.find(t => userPoints < t.points);
  
  return {
    current: achieved[achieved.length - 1] || { name: 'Beginner', points: 0 },
    next: nextThreshold,
    progress: nextThreshold 
      ? (userPoints - achieved[achieved.length - 1]?.points || 0) / (nextThreshold.points - (achieved[achieved.length - 1]?.points || 0))
      : 1,
    totalAchieved: achieved.length
  };
}

Usage Examples

Basic User Profile

// Get user loyalty program status
const accessToken = process.env.GALXE_ACCESS_TOKEN;
const spaceId = 40; // BNB Chain space
const userAddress = '0x1234567890123456789012345678901234567890';

const dashboard = await createUserDashboard(spaceId, userAddress, accessToken);

console.log('User Points:', dashboard.user.points);
console.log('User Rank:', dashboard.user.rank);
console.log('Percentile:', dashboard.user.percentile + '%');
console.log('Total Participants:', dashboard.leaderboard.totalParticipants);

Quest Rewards Preview

// Show potential rewards before quest participation
async function showQuestRewards(questId, userAddress, accessToken) {
  const tracking = await trackQuestCompletion(questId, userAddress, accessToken);
  
  if (tracking.error || tracking.message) {
    console.log(tracking.error || tracking.message);
    return;
  }
  
  console.log(`Quest: ${tracking.quest.name}`);
  console.log(`Potential Points: +${tracking.quest.points}`);
  console.log(`Current Points: ${tracking.user.currentPoints}`);
  console.log(`After Completion: ${tracking.user.projectedPoints}`);
  console.log(`Current Rank: ${tracking.user.currentRank || 'Unranked'}`);
}

Achievement System

// Check user achievements
const userPoints = await findUserPoints(spaceId, userAddress, accessToken);
const achievements = calculateAchievements(userPoints.points);

console.log(`Current Level: ${achievements.current.name}`);
if (achievements.next) {
  console.log(`Next Level: ${achievements.next.name} (${achievements.next.points} points)`);
  console.log(`Progress: ${Math.round(achievements.progress * 100)}%`);
}
console.log(`Total Achievements: ${achievements.totalAchieved}/5`);

Best Practices

  1. Search Optimization: Limit user search to 20-50 pages max for UI responsiveness
  2. Caching: Cache user positions for 5-10 minutes to reduce API calls
  3. Error Handling: Handle cases where users are not found in rankings
  4. Performance: Consider showing approximate positions rather than exact searches
  5. User Experience: Clearly indicate when users are not yet ranked

Limitations & Workarounds

Current Limitations

  1. No Direct User Lookup: Must search through leaderboard pages
  2. Search Performance: Finding users can require multiple API calls
  3. No Sprint Management: Sprint IDs come from external sources
  1. Efficient Search: Implement reasonable search limits
  2. Caching Strategy: Cache user positions for short periods
  3. User Communication: Set expectations about search time
  4. Achievement Systems: Define point thresholds instead of exact rankings

Points Integration Patterns

  • New User Onboarding: Show potential points from available quests
  • Progress Tracking: Regular user position checks (respect rate limits)
  • Achievement Systems: Define point thresholds (100, 500, 1000, etc.)
  • Access Control: Points-based feature unlocking
  • Quest Recommendations: Suggest high-value quests based on current points

Next Steps