Authentication & Setup

Basic GraphQL Client

const executeQuery = async (query, variables = {}) => {
  const response = await fetch('https://graphigo-business.prd.galaxy.eco/query', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'access-token': process.env.GALXE_ACCESS_TOKEN
    },
    body: JSON.stringify({ query, variables })
  });

  const data = await response.json();
  if (data.errors) throw new Error(data.errors[0].message);
  return data.data;
};

Error Handling with Retry

const retryQuery = async (query, variables, maxRetries = 3) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await executeQuery(query, variables);
    } catch (error) {
      const isRetryable = error.message.includes('rate limit') || 
                         error.message.includes('timeout');
      
      if (isRetryable && attempt < maxRetries) {
        await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
        continue;
      }
      throw error;
    }
  }
};

Quest Patterns

Quest Status Validation

query QuestStatus($id: ID!) {
  quest(id: $id) {
    status
    startTime
    endTime
  }
}
const isQuestActive = (quest) => {
  const now = Date.now() / 1000;
  return quest.status === 'Active' && 
         now >= quest.startTime && 
         now <= quest.endTime;
};

const getQuestPhase = (quest) => {
  const now = Date.now() / 1000;
  if (now < quest.startTime) return 'upcoming';
  if (now <= quest.endTime) return 'active';
  return 'ended';
};

User Eligibility Check

query CheckEligibility($questId: ID!, $address: String!) {
  quest(id: $questId) {
    credentialGroups(address: $address) {
      conditions { eligible }
      rewards { eligible rewardType }
    }
  }
}
const checkEligibility = async (questId, address) => {
  const data = await executeQuery(`
    query CheckEligibility($questId: ID!, $address: String!) {
      quest(id: $questId) {
        credentialGroups(address: $address) {
          conditions { eligible }
          rewards { eligible }
        }
      }
    }
  `, { questId, address });

  return data.quest.credentialGroups.some(group => 
    group.conditions.some(c => c.eligible) && 
    group.rewards.some(r => r.eligible)
  );
};

Pagination Patterns

Standard Pagination Query

query PaginatedQuery($first: Int!, $after: String) {
  items(first: $first, after: $after) {
    pageInfo {
      hasNextPage
      endCursor
    }
    edges {
      node {
        # your fields here
      }
    }
  }
}

Fetch All Pages

const fetchAllPages = async (query, variables = {}, extractPath) => {
  let allItems = [];
  let hasNextPage = true;
  let cursor = null;

  while (hasNextPage) {
    const data = await executeQuery(query, { 
      ...variables, 
      first: 100, 
      after: cursor 
    });
    
    const result = extractPath(data);
    allItems.push(...result.edges.map(edge => edge.node));
    
    hasNextPage = result.pageInfo.hasNextPage;
    cursor = result.pageInfo.endCursor;

    // Rate limiting
    await new Promise(resolve => setTimeout(resolve, 100));
  }

  return allItems;
};

// Usage example
const allQuests = await fetchAllPages(
  `query GetQuests($spaceId: ID!, $first: Int!, $after: String) {
    quests(input: {spaceId: $spaceId, first: $first, after: $after}) {
      pageInfo { hasNextPage endCursor }
      list { id name status }
    }
  }`,
  { spaceId: "40" },
  data => data.quests
);

Leaderboard Patterns

No Direct User Lookup: The B2B API does not provide direct user position lookup. You must search through leaderboard pages to find a specific user.

const findUserPosition = async (spaceId, userAddress) => {
  let cursorAfter = null;
  let pageCount = 0;
  const maxPages = 20; // Reasonable search limit
  
  while (pageCount < maxPages) {
    const data = await executeQuery(`
      query SearchUser($spaceId: Int!, $cursorAfter: String) {
        space(id: $spaceId) {
          loyaltyPointsRanks(cursorAfter: $cursorAfter) {
            pageInfo { hasNextPage endCursor }
            list {
              rank
              points
              address { address username }
            }
          }
        }
      }
    `, { spaceId, cursorAfter });

    const userRank = data.space.loyaltyPointsRanks.list.find(item => 
      item.address.address.toLowerCase() === userAddress.toLowerCase()
    );
    
    if (userRank) {
      return { found: true, rank: userRank.rank, points: userRank.points };
    }
    
    if (!data.space.loyaltyPointsRanks.pageInfo.hasNextPage) break;
    cursorAfter = data.space.loyaltyPointsRanks.pageInfo.endCursor;
    pageCount++;
    
    await new Promise(resolve => setTimeout(resolve, 100)); // Rate limiting
  }
  
  return { found: false, rank: null, points: 0 };
};

const getLeaderboardWithUser = async (spaceId, userAddress) => {
  const [leaderboardData, userPosition] = await Promise.all([
    executeQuery(`
      query GetLeaderboard($spaceId: Int!) {
        space(id: $spaceId) {
          loyaltyPointsRanks(first: 10) {
            list {
              rank
              points
              address { username }
            }
          }
        }
      }
    `, { spaceId }),
    findUserPosition(spaceId, userAddress)
  ]);

  return {
    leaderboard: leaderboardData.space.loyaltyPointsRanks.list,
    userPosition
  };
};

Credential Patterns

Batch Eligibility Check

const checkBatchEligibility = async (credId, addresses) => {
  const promises = addresses.map(address => 
    executeQuery(`
      query CheckEligibility($credId: ID!, $address: String!) {
        credential(id: $credId) { eligible(address: $address) }
      }
    `, { credId, address })
      .then(data => ({ address, eligible: data.credential.eligible === 1 }))
      .catch(() => ({ address, eligible: false }))
  );

  return Promise.all(promises);
};

Credential Validation

const validateCredential = async (credId) => {
  const data = await executeQuery(`
    query ValidateCredential($credId: ID!) {
      credential(id: $credId) {
        syncStatus
        itemCount
        lastUpdate
      }
    }
  `, { credId });

  const cred = data.credential;
  return {
    isValid: cred.syncStatus === 'SYNCED',
    hasHolders: cred.itemCount > 0,
    lastSynced: new Date(cred.lastUpdate * 1000)
  };
};

Real-time Monitoring

Simple Polling Monitor

const createMonitor = (queryFn, interval = 30000) => {
  let intervalId = null;
  
  const start = (callback) => {
    intervalId = setInterval(async () => {
      try {
        const data = await queryFn();
        callback({ type: 'update', data });
      } catch (error) {
        callback({ type: 'error', error: error.message });
      }
    }, interval);
  };

  const stop = () => {
    if (intervalId) {
      clearInterval(intervalId);
      intervalId = null;
    }
  };

  return { start, stop };
};

// Usage
const leaderboardMonitor = createMonitor(
  () => getLeaderboardWithUser(40, userAddress),
  60000 // 1 minute
);

leaderboardMonitor.start((update) => {
  if (update.type === 'update') {
    console.log('Leaderboard updated:', update.data);
  }
});

Rate Limiting

Simple Rate Limiter

class RateLimiter {
  constructor(requestsPerSecond = 10) {
    this.requests = [];
    this.maxRequests = requestsPerSecond;
  }

  async execute(fn) {
    const now = Date.now();
    this.requests = this.requests.filter(time => now - time < 1000);

    if (this.requests.length >= this.maxRequests) {
      const waitTime = 1000 - (now - this.requests[0]);
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }

    this.requests.push(now);
    return fn();
  }
}

// Usage
const limiter = new RateLimiter(5); // 5 requests per second

const data = await limiter.execute(() => 
  executeQuery(query, variables)
);

Data Transformation

Standardize User Objects

const standardizeUser = (userObject) => {
  // Handle different user object formats
  if (userObject.address) {
    return {
      id: userObject.address.id || userObject.address.address,
      username: userObject.address.username,
      address: userObject.address.address
    };
  }
  
  return {
    id: userObject.id,
    username: userObject.username,
    address: userObject.address
  };
};

Format Points and Rankings

const formatRanking = (rankingData) => ({
  rank: parseInt(rankingData.rank),
  points: parseInt(rankingData.points),
  user: standardizeUser(rankingData),
  rankMovement: rankingData.delta || 0
});

const formatLeaderboard = (leaderboardData) => ({
  totalCount: leaderboardData.totalCount,
  rankings: leaderboardData.edges.map(edge => formatRanking(edge.node)),
  hasNextPage: leaderboardData.pageInfo.hasNextPage
});

Utility Functions

Time Helpers

const timeHelpers = {
  toUnix: (date) => Math.floor(date.getTime() / 1000),
  fromUnix: (timestamp) => new Date(timestamp * 1000),
  isAfter: (timestamp) => Date.now() / 1000 > timestamp,
  formatDuration: (seconds) => {
    const days = Math.floor(seconds / 86400);
    const hours = Math.floor((seconds % 86400) / 3600);
    const minutes = Math.floor((seconds % 3600) / 60);
    return `${days}d ${hours}h ${minutes}m`;
  }
};

Validation Helpers

const validators = {
  isValidAddress: (address) => /^0x[a-fA-F0-9]{40}$/.test(address),
  isValidQuestId: (id) => typeof id === 'string' && id.length > 0,
  isValidSpaceId: (id) => Number.isInteger(id) && id > 0
};

Best Practices Summary

  1. Always validate inputs before API calls
  2. Implement retry logic for network failures
  3. Use pagination for large datasets
  4. Cache frequently accessed data (30-60 seconds)
  5. Handle rate limits with exponential backoff
  6. Monitor for errors and log appropriately
  7. Use environment variables for sensitive data

Quick Reference

Essential Queries

  • Quest status: quest(id) { status startTime endTime }
  • User eligibility: quest(id) { credentialGroups(address) { conditions { eligible } } }
  • Leaderboard: space(id) { loyaltyPointsRanks(cursorAfter) { list { rank points address { username } } } }
  • User search: Must paginate through leaderboard to find specific users

Common Response Patterns

  • Paginated: { pageInfo { hasNextPage endCursor } edges { node { ... } } }
  • User objects: { address { username address } } or { username address }
  • Error format: { errors [{ message extensions { code category } }] }

Next Steps