Skip to content

Accessing user data

After users authenticate with Login with Funtico, you can access their profile information and TICO balance. This guide shows you how to retrieve and manage user data securely using a backend approach with secure HTTP-only cookies.

Funtico provides access to user data through authenticated API endpoints:

  • User Profile - Basic user information (email, username, etc.)
  • TICO Balance - Current cryptocurrency balance

Before accessing user data, ensure you have:

  • Valid Access Token - Obtained through Login with Funtico flow
  • Required Scopes - profile, email for user info; balance:read, transactions:read, tournaments:read, progression:read for extended data access
  • SDK Initialization - Properly configured Funtico SDK
  • Backend Setup - Secure token management with HTTP-only cookies

Initialize the SDK with your auth client credentials:

import { FunticoSDK } from '@pillarex/funtico-sdk';
const sdk = new FunticoSDK({
authClientId: process.env.FUNTICO_AUTH_CLIENT_ID,
authClientSecret: process.env.FUNTICO_AUTH_CLIENT_SECRET,
env: process.env.NODE_ENV === 'production' ? 'production' : 'staging'
});

Create a secure backend endpoint to get the user’s TICO balance:

// Backend: GET /api/user/balance
app.get('/api/user/balance', async (req, res) => {
try {
const accessToken = req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({ error: 'Not authenticated' });
}
const balance = await sdk.getUserBalance({
accessToken: accessToken
});
res.json({
balance: balance, // Returns balance in smallest unit (e.g., 1000 = 10.00 TICO)
formattedBalance: (balance / 100).toFixed(2) // Display format
});
} catch (error) {
console.error('Failed to get user balance:', error);
res.status(500).json({ error: 'Failed to retrieve balance' });
}
});

Your frontend would call the balance endpoint:

// Frontend: Get user balance
async function getUserBalance() {
try {
const response = await fetch('/api/user/balance', {
credentials: 'include' // Include cookies
});
if (response.ok) {
const { balance, formattedBalance } = await response.json();
console.log(`User balance: ${formattedBalance} TICO`);
return { balance, formattedBalance };
} else {
throw new Error('Failed to get balance');
}
} catch (error) {
console.error('Failed to get user balance:', error);
throw error;
}
}

Implement robust error handling in your backend endpoint:

// Backend: GET /api/user/balance with comprehensive error handling
app.get('/api/user/balance', async (req, res) => {
try {
const accessToken = req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({
error: 'Authentication required. Please log in again.',
code: 'AUTH_REQUIRED'
});
}
const balance = await sdk.getUserBalance({
accessToken: accessToken
});
res.json({
success: true,
balance: balance,
formattedBalance: (balance / 100).toFixed(2) // Convert to TICO with 2 decimal places
});
} catch (error) {
if (isSDKError(error)) {
switch (error.name) {
case 'invalid_token':
res.status(401).json({
error: 'Authentication required. Please log in again.',
code: 'AUTH_REQUIRED'
});
break;
case 'insufficient_scope':
res.status(403).json({
error: 'Insufficient permissions to access balance.',
code: 'INSUFFICIENT_SCOPE'
});
break;
default:
res.status(500).json({
error: 'Failed to retrieve balance. Please try again.',
code: 'UNKNOWN_ERROR'
});
}
} else {
res.status(500).json({
error: 'Network error. Please check your connection.',
code: 'NETWORK_ERROR'
});
}
}
});

Create a secure backend endpoint to get user profile information:

// Backend: GET /api/user/profile
app.get('/api/user/profile', async (req, res) => {
try {
const accessToken = req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({ error: 'Not authenticated' });
}
const userInfo = await sdk.getUserInfo({
accessToken: accessToken
});
res.json({ profile: userInfo });
} catch (error) {
console.error('Failed to get user profile:', error);
res.status(500).json({ error: 'Failed to retrieve profile' });
}
});

Retrieve both profile and balance in a single backend endpoint:

// Backend: GET /api/user/data
app.get('/api/user/data', async (req, res) => {
try {
const accessToken = req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({ error: 'Not authenticated' });
}
// Fetch profile and balance in parallel
const [userInfo, balance] = await Promise.all([
sdk.getUserInfo({ accessToken }),
sdk.getUserBalance({ accessToken })
]);
res.json({
profile: userInfo,
balance: balance,
formattedBalance: (balance / 100).toFixed(2)
});
} catch (error) {
console.error('Failed to get user data:', error);
res.status(500).json({ error: 'Failed to retrieve user data' });
}
});

Your frontend would call these endpoints:

// Frontend: Get complete user data
async function getCompleteUserData() {
try {
const response = await fetch('/api/user/data', {
credentials: 'include' // Include cookies
});
if (response.ok) {
const userData = await response.json();
console.log(`Welcome, ${userData.profile.name}!`);
console.log(`Your balance: ${userData.formattedBalance} TICO`);
return userData;
} else {
throw new Error('Failed to get user data');
}
} catch (error) {
console.error('Failed to get user data:', error);
throw error;
}
}

The balance endpoint returns:

// Balance response format
{
balance: number // Balance in smallest currency unit (e.g., 1000 = 10.00 TICO)
}

The user profile contains standard OIDC claims:

// User profile response format
{
sub: string, // Unique user identifier
email: string, // User's email address
email_verified: boolean, // Whether email is verified
name: string, // User's display name
preferred_username: string, // User's chosen username
image: string, // User's avatar image URL
frame: string // User's avatar frame URL
}
// Backend: Handle different error types in user data endpoints
app.get('/api/user/data', requireAuth, async (req, res) => {
try {
const [userInfo, balance] = await Promise.all([
sdk.getUserInfo({ accessToken: req.accessToken }),
sdk.getUserBalance({ accessToken: req.accessToken })
]);
res.json({ success: true, profile: userInfo, balance });
} catch (error) {
if (isSDKError(error)) {
switch (error.name) {
case 'invalid_token':
res.status(401).json({
error: 'Authentication required. Please log in again.',
code: 'INVALID_TOKEN'
});
break;
case 'insufficient_scope':
res.status(403).json({
error: 'Token missing required scope',
code: 'INSUFFICIENT_SCOPE'
});
break;
case 'rate_limit_exceeded':
res.status(429).json({
error: 'Rate limit exceeded, please wait',
code: 'RATE_LIMITED'
});
break;
default:
res.status(500).json({
error: 'Unknown SDK error',
code: 'SDK_ERROR'
});
}
} else {
res.status(500).json({
error: 'Network or server error',
code: 'SERVER_ERROR'
});
}
}
})
// Frontend: Handle API errors with user-friendly messages
function getErrorMessage(errorCode: string): string {
switch (errorCode) {
case 'INVALID_TOKEN':
case 'NO_TOKEN':
return 'Your session has expired. Please log in again.';
case 'INSUFFICIENT_SCOPE':
return 'Unable to access this data. Please contact support.';
case 'RATE_LIMITED':
return 'Too many requests. Please try again in a moment.';
case 'SDK_ERROR':
return 'Unable to load user data. Please try again.';
case 'SERVER_ERROR':
return 'Server error. Please check your connection and try again.';
default:
return 'An unexpected error occurred. Please try again.';
}
}
// Frontend: Usage in UI
async function loadUserData() {
try {
const response = await fetch('/api/user/data', {
credentials: 'include'
});
if (response.ok) {
const userData = await response.json();
setUserData(userData);
} else {
const error = await response.json();
const message = getErrorMessage(error.code || 'UNKNOWN');
showErrorNotification(message);
}
} catch (error) {
showErrorNotification('Network error. Please check your connection.');
}
}

Cache user data to reduce API calls:

// Simple caching implementation
class UserDataCache {
private cache = new Map<string, { data: any; timestamp: number }>();
private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
async getUserBalance(accessToken: string): Promise<number> {
const cacheKey = `balance_${accessToken}`;
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.CACHE_DURATION) {
return cached.data;
}
try {
const balance = await sdk.getUserBalance({ accessToken });
// Cache the result
this.cache.set(cacheKey, {
data: balance,
timestamp: Date.now()
});
return balance;
} catch (error) {
// Return cached data if available, even if expired
if (cached) {
console.warn('Using cached balance due to API error');
return cached.data;
}
throw error;
}
}
// Clear cache for a specific user
clearUserCache(accessToken: string) {
const cacheKey = `balance_${accessToken}`;
this.cache.delete(cacheKey);
}
}
// Usage
const userCache = new UserDataCache();
const balance = await userCache.getUserBalance(accessToken);

Implement polling for real-time balance updates:

// Polling for balance updates
class BalancePoller {
private intervalId: NodeJS.Timeout | null = null;
private lastBalance: number | null = null;
private onBalanceChange: ((balance: number) => void) | null = null;
startPolling(onBalanceChange: (balance: number) => void) {
this.onBalanceChange = onBalanceChange;
this.intervalId = setInterval(async () => {
try {
const response = await fetch('/api/user/balance', {
credentials: 'include'
});
if (response.ok) {
const { balance } = await response.json();
if (this.lastBalance !== null && this.lastBalance !== balance) {
this.onBalanceChange?.(balance);
}
this.lastBalance = balance;
}
} catch (error) {
console.error('Failed to poll balance:', error);
}
}, 30000); // Poll every 30 seconds
}
stopPolling() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
}
// Usage in React component
function BalanceDisplay() {
const [balance, setBalance] = useState<number | null>(null);
const pollerRef = useRef<BalancePoller | null>(null);
useEffect(() => {
pollerRef.current = new BalancePoller();
pollerRef.current.startPolling(setBalance);
return () => {
pollerRef.current?.stopPolling();
};
}, []);
return (
<div>
{balance !== null ? (
<p>Balance: {(balance / 100).toFixed(2)} TICO</p>
) : (
<p>Loading balance...</p>
)}
</div>
);
}

Backend middleware automatically validates tokens from cookies:

// Backend: Token validation middleware with error handling
function requireAuth(req, res, next) {
const accessToken = req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({
error: 'Authentication required',
code: 'NO_TOKEN'
});
}
req.accessToken = accessToken;
next();
}
// Backend: Enhanced validation with token refresh attempt
async function requireAuthWithRefresh(req, res, next) {
const accessToken = req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({
error: 'Authentication required',
code: 'NO_TOKEN'
});
}
try {
// Validate token by making a test call
await sdk.getUserInfo({ accessToken });
req.accessToken = accessToken;
next();
} catch (error) {
if (isSDKError(error) && error.name === 'invalid_token') {
return res.status(401).json({
error: 'Token expired',
code: 'TOKEN_EXPIRED'
});
}
throw error;
}
}

Store tokens securely using HTTP-only cookies on the backend:

// Backend: Secure token management
app.post('/auth/refresh', async (req, res) => {
try {
const refreshToken = req.cookies.refresh_token;
if (!refreshToken) {
return res.status(401).json({ error: 'No refresh token' });
}
const { accessToken, refreshToken: newRefreshToken } = await sdk.refreshTokens({
refreshToken
});
// Update tokens in secure cookies
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 1000 // 1 hour
});
res.cookie('refresh_token', newRefreshToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.json({ success: true });
} catch (error) {
res.status(401).json({ error: 'Failed to refresh token' });
}
});
// Backend: Token validation middleware
function requireAuth(req, res, next) {
const accessToken = req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({ error: 'Authentication required' });
}
req.accessToken = accessToken;
next();
}
// Usage
app.get('/api/user/balance', requireAuth, async (req, res) => {
try {
const balance = await sdk.getUserBalance({ accessToken: req.accessToken });
res.json({ balance });
} catch (error) {
res.status(500).json({ error: 'Failed to get balance' });
}
});

Here’s a complete example with backend endpoints and frontend integration:

// Complete backend user data service
import express from 'express';
import cookieParser from 'cookie-parser';
import { FunticoSDK } from '@pillarex/funtico-sdk';
const app = express();
app.use(cookieParser());
const sdk = new FunticoSDK({
authClientId: process.env.FUNTICO_AUTH_CLIENT_ID!,
authClientSecret: process.env.FUNTICO_AUTH_CLIENT_SECRET!,
env: 'staging'
});
// In-memory cache (use Redis in production)
const cache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
// Authentication middleware
function requireAuth(req, res, next) {
const accessToken = req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({ error: 'Authentication required' });
}
req.accessToken = accessToken;
next();
}
// Get complete user data with caching
app.get('/api/user/data', requireAuth, async (req, res) => {
try {
const cacheKey = `user_data_${req.accessToken}`;
const cached = cache.get(cacheKey);
// Return cached data if still valid
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return res.json(cached.data);
}
// Fetch fresh data
const [userInfo, balance] = await Promise.all([
sdk.getUserInfo({ accessToken: req.accessToken }),
sdk.getUserBalance({ accessToken: req.accessToken })
]);
const userData = {
profile: userInfo,
balance: balance,
formattedBalance: (balance / 100).toFixed(2),
lastUpdated: new Date().toISOString()
};
// Cache the result
cache.set(cacheKey, {
data: userData,
timestamp: Date.now()
});
res.json(userData);
} catch (error) {
console.error('Failed to get user data:', error);
res.status(500).json({ error: 'Failed to retrieve user data' });
}
});
// Refresh user data (bypass cache)
app.post('/api/user/data/refresh', requireAuth, async (req, res) => {
try {
const cacheKey = `user_data_${req.accessToken}`;
cache.delete(cacheKey); // Clear cache
// Fetch fresh data
const [userInfo, balance] = await Promise.all([
sdk.getUserInfo({ accessToken: req.accessToken }),
sdk.getUserBalance({ accessToken: req.accessToken })
]);
const userData = {
profile: userInfo,
balance: balance,
formattedBalance: (balance / 100).toFixed(2),
lastUpdated: new Date().toISOString()
};
res.json(userData);
} catch (error) {
console.error('Failed to refresh user data:', error);
res.status(500).json({ error: 'Failed to refresh user data' });
}
});