Skip to content

Creating Tournaments

This guide shows you how to create and manage tournaments in your game using Funtico’s tournament system. We’ll cover tournament creation, player management, score submission, and prize distribution.

Before implementing tournaments, ensure you have:

  • Funtico Account - Developer account with tournament access enabled
  • API Credentials - Your API Key ID and Secret for Basic authentication
  • Auth Integration - Users must be authenticated via Login with Funtico
  • SDK Installation - Latest version supporting tournament operations
Terminal window
npm install @pillarex/funtico-sdk@latest

Initialize the SDK with your credentials. For tournaments, you’ll need API credentials for Basic authentication:

import { FunticoSDK } from '@pillarex/funtico-sdk';
// Initialize SDK with API credentials for tournament operations
const sdk = new FunticoSDK({
apiKeyId: process.env.FUNTICO_API_KEY_ID,
apiKeySecret: process.env.FUNTICO_API_KEY_SECRET,
env: process.env.NODE_ENV === 'production' ? 'production' : 'staging'
});

Create a simple tournament with essential configuration:

// Backend: Create a basic tournament
async function createBasicTournament() {
try {
const tournament = await sdk.createTournament({
name: "Weekly Puzzle Challenge",
description: "Compete in our weekly puzzle tournament for TICO prizes!",
starts_at: "2024-12-01T10:00:00Z",
ends_at: "2024-12-07T22:00:00Z",
max_players: 100,
score_mode: "descending", // Higher scores win
score_type: "points", // Best single score
entry_fee_type: "free",
entry_limit: 1, // One entry per player
is_kyc_required: false,
is_published: false, // Create as draft first
metadata: {
difficulty: "medium",
game_mode: "puzzle"
}
});
console.log('Tournament created:', tournament.id);
return tournament;
} catch (error) {
console.error('Failed to create tournament:', error);
throw error;
}
}

Create a tournament with entry fees and prize distribution:

// Backend: Create tournament with prizes
async function createPrizeTournament() {
try {
const tournament = await sdk.createTournament({
name: "Championship Battle Royale",
description: "Ultimate battle royale tournament with massive TICO prizes!",
cover_image_url: "https://your-game.com/images/tournament-cover.jpg",
starts_at: "2024-12-15T18:00:00Z",
ends_at: "2024-12-15T22:00:00Z",
max_players: 64,
score_mode: "descending",
score_type: "points",
entry_fee_type: "tico",
entry_fee_amount: 10, // 10 TICO entry fee
entry_limit: 3, // Up to 3 entries per player
is_kyc_required: true, // Required for high-value tournaments
is_published: false,
metadata: {
game_mode: "battle_royale",
difficulty: "expert",
duration_minutes: 240
},
prize_distributions: [
{
type: "specific_place",
place: 1,
rewards: [{ type: "tico", amount: 300 }] // 1st place: 300 TICO
},
{
type: "place_range",
place_range_start: 2,
place_range_end: 3,
rewards: [{ type: "tico", amount: 100 }] // 2nd-3rd: 100 TICO each
},
{
type: "percentage_range",
percentage_range: 10, // Top 10%
rewards: [{ type: "tico", amount: 25 }] // Top 10%: 25 TICO each
}
]
});
return tournament;
} catch (error) {
console.error('Failed to create prize tournament:', error);
throw error;
}
}

Create backend endpoints for tournament operations:

import express from 'express';
const app = express();
// Middleware for Basic authentication
function requireApiAuth(req, res, next) {
// Validate Basic auth credentials from your game server
const authHeader = req.headers['authorization'];
if (!authHeader || !authHeader.startsWith('Basic ')) {
return res.status(401).json({ error: 'Missing or invalid authorization header' });
}
const base64Credentials = authHeader.split(' ')[1];
const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
const [keyId, keySecret] = credentials.split(':');
if (!isValidApiCredentials(keyId, keySecret)) {
return res.status(401).json({ error: 'Invalid API credentials' });
}
next();
}
// Create tournament endpoint
app.post('/api/tournaments', requireApiAuth, async (req, res) => {
try {
const tournamentData = req.body;
const tournament = await sdk.createTournament(tournamentData);
res.status(201).json({ tournament });
} catch (error) {
console.error('Tournament creation failed:', error);
res.status(500).json({ error: 'Failed to create tournament' });
}
});
// Get tournaments endpoint
app.get('/api/tournaments', requireApiAuth, async (req, res) => {
try {
const tournaments = await sdk.getTournaments();
res.json({ tournaments });
} catch (error) {
console.error('Failed to fetch tournaments:', error);
res.status(500).json({ error: 'Failed to fetch tournaments' });
}
});
// Publish tournament endpoint
app.post('/api/tournaments/:id/publish', requireApiAuth, async (req, res) => {
try {
const { id } = req.params;
await sdk.publishTournament(id);
res.json({ success: true });
} catch (error) {
console.error('Failed to publish tournament:', error);
res.status(500).json({ error: 'Failed to publish tournament' });
}
});

Handle tournament registration with dual authentication:

// Backend: Tournament join endpoint (requires both Basic auth and access token)
app.post('/api/tournaments/:id/join', requireApiAuth, async (req, res) => {
try {
const { id } = req.params;
const { password, user_ip } = req.body;
// Get access token from secure cookie
const accessToken = req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({ error: 'User not authenticated' });
}
// Join tournament with dual authentication
const result = await sdk.joinTournament(id, {
accessToken, // User authentication
password, // Optional tournament password
user_ip // Optional for analytics
});
res.json({ success: true, data: result });
} catch (error) {
console.error('Failed to join tournament:', error);
if (error.error_code === 'tournament_full') {
res.status(400).json({ error: 'Tournament is full' });
} else if (error.error_code === 'insufficient_balance') {
res.status(400).json({ error: 'Insufficient TICO balance for entry fee' });
} else {
res.status(500).json({ error: 'Failed to join tournament' });
}
}
});

Submit player scores with validation:

// Backend: Score submission endpoint
app.post('/api/tournaments/:id/score', requireApiAuth, async (req, res) => {
try {
const { id } = req.params;
const { score, user_ip } = req.body;
// Get access token from secure cookie
const accessToken = req.cookies.access_token;
if (!accessToken) {
return res.status(401).json({ error: 'User not authenticated' });
}
// Validate score on your server before submission
if (!isValidScore(score, req.user)) {
return res.status(400).json({ error: 'Invalid score' });
}
// Submit score with dual authentication
const result = await sdk.submitTournamentScore(id, {
score,
accessToken, // User authentication
user_ip // Optional for analytics
});
res.json({ success: true, data: result });
} catch (error) {
console.error('Failed to submit score:', error);
if (error.error_code === 'tournament_ended') {
res.status(400).json({ error: 'Tournament has ended' });
} else if (error.error_code === 'not_joined') {
res.status(400).json({ error: 'Player has not joined this tournament' });
} else {
res.status(500).json({ error: 'Failed to submit score' });
}
}
});
// Server-side score validation example
function isValidScore(score, user) {
// Implement your game-specific score validation logic
// Check for reasonable score ranges, rate limiting, etc.
if (score < 0 || score > 1000000) {
return false;
}
// Add additional validation based on your game mechanics
return true;
}

Provide real-time leaderboard data:

// Backend: Get tournament leaderboard
app.get('/api/tournaments/:id/leaderboard', requireApiAuth, async (req, res) => {
try {
const { id } = req.params;
const leaderboard = await sdk.getTournamentLeaderboard(id);
res.json({ leaderboard });
} catch (error) {
console.error('Failed to get leaderboard:', error);
res.status(500).json({ error: 'Failed to get leaderboard' });
}
});

Show available tournaments to players:

// Frontend: Tournament list component
interface Tournament {
id: string;
name: string;
description: string;
cover_image_url?: string;
starts_at: string;
ends_at: string;
max_players: number;
entry_fee_type: 'free' | 'tico';
entry_fee_amount: number;
prize_distributions: PrizeDistribution[];
}
async function loadTournaments(): Promise<Tournament[]> {
try {
const response = await fetch('/api/tournaments', {
credentials: 'include'
});
if (response.ok) {
const { tournaments } = await response.json();
return tournaments;
} else {
throw new Error('Failed to load tournaments');
}
} catch (error) {
console.error('Error loading tournaments:', error);
return [];
}
}
// React component example
function TournamentList() {
const [tournaments, setTournaments] = useState<Tournament[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadTournaments()
.then(setTournaments)
.finally(() => setLoading(false));
}, []);
if (loading) return <div>Loading tournaments...</div>;
return (
<div className="tournament-list">
{tournaments.map(tournament => (
<TournamentCard
key={tournament.id}
tournament={tournament}
onJoin={handleJoinTournament}
/>
))}
</div>
);
}

Handle player actions:

// Frontend: Tournament interaction functions
async function joinTournament(tournamentId: string, password?: string) {
try {
const response = await fetch(`/api/tournaments/${tournamentId}/join`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
password,
user_ip: await getUserIP() // Optional
})
});
if (response.ok) {
showSuccess('Successfully joined tournament!');
// Refresh tournament data
await loadTournamentDetails(tournamentId);
} else {
const error = await response.json();
showError(error.error || 'Failed to join tournament');
}
} catch (error) {
showError('Network error. Please try again.');
}
}
async function submitScore(tournamentId: string, score: number) {
try {
const response = await fetch(`/api/tournaments/${tournamentId}/score`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
score,
user_ip: await getUserIP() // Optional
})
});
if (response.ok) {
showSuccess(`Score ${score} submitted successfully!`);
// Refresh leaderboard
await loadLeaderboard(tournamentId);
} else {
const error = await response.json();
showError(error.error || 'Failed to submit score');
}
} catch (error) {
showError('Network error. Please try again.');
}
}
async function loadLeaderboard(tournamentId: string) {
try {
const response = await fetch(`/api/tournaments/${tournamentId}/leaderboard`, {
credentials: 'include'
});
if (response.ok) {
const { leaderboard } = await response.json();
updateLeaderboardDisplay(leaderboard);
}
} catch (error) {
console.error('Failed to load leaderboard:', error);
}
}
// Handle tournament-specific errors
function handleTournamentError(error: any, operation: string) {
if (error.error_code) {
switch (error.error_code) {
case 'tournament_not_found':
return 'Tournament not found. It may have been cancelled.';
case 'tournament_full':
return 'Tournament is full. Try joining another tournament.';
case 'tournament_ended':
return 'Tournament has ended. Scores can no longer be submitted.';
case 'insufficient_balance':
return 'Insufficient TICO balance for entry fee. Please add funds.';
case 'already_joined':
return 'You have already joined this tournament.';
case 'entry_limit_reached':
return 'You have reached the maximum entries for this tournament.';
case 'invalid_password':
return 'Invalid tournament password. Please check and try again.';
case 'kyc_required':
return 'KYC verification required to join this tournament.';
default:
return `Tournament ${operation} failed. Please try again.`;
}
}
return 'An unexpected error occurred. Please try again.';
}
// Usage in error handling
try {
await joinTournament(tournamentId);
} catch (error) {
const message = handleTournamentError(error, 'join');
showError(message);
}
  • Dual Authentication - Always use both Basic auth and Access Token for player operations
  • Score Validation - Implement server-side score validation before submission
  • Rate Limiting - Prevent abuse of tournament operations
  • Input Sanitization - Validate all tournament parameters
  • Caching - Cache tournament lists and leaderboards appropriately
  • Real-time Updates - Use WebSockets for live leaderboard updates
  • Pagination - Handle large tournament lists efficiently
  • Lazy Loading - Load tournament details on demand
  • Clear Status - Show tournament phases and user participation status
  • Entry Requirements - Display entry fees, requirements, and restrictions clearly
  • Prize Information - Transparently show prize structures and distributions
  • Real-time Feedback - Provide immediate feedback for all actions