Custom Development Guide
Build Custom Games & Applications
Create your own unique games, applications, and experiences while leveraging MCAP's powerful infrastructure for wallet management, payments, and settlement. This approach gives you complete creative control while handling the complex smart contract interactions.
What You Build vs. What MCAP Provides
🎨 What You Build (Full Creative Control)
Custom Game Mechanics
- Unique gameplay logic tailored to your vision
- Specialized betting and reward systems
- Custom user interfaces that match your brand
- Platform-specific features that differentiate your offering
- Proprietary algorithms for games, matching, or trading
Your User Experience
- Complete UI/UX design freedom
- Custom user flows and navigation
- Branded visual identity throughout
- Personalized onboarding experience
- Platform-specific social features
Business Logic
- Custom prize structures and payout logic
- Specialized tournament formats
- Unique loyalty and reward systems
- Platform-specific compliance rules
- Custom analytics and reporting
🏗️ What MCAP Provides (Infrastructure & Security)
Wallet & Authentication
- Seamless Web3 wallet connection (MetaMask, WalletConnect, etc.)
- Secure user authentication with JWT tokens and signature verification
- Multi-wallet support for user convenience
- Account recovery and security features
Financial Infrastructure
- Multi-token deposit system supporting any ERC-20 token
- Instant withdrawal processing with status tracking
- Real-time balance management across multiple tokens
- Automated on-chain settlement and payout systems
- Transaction history and record keeping
Session & Security
- Secure session management with automatic renewal
- Fraud detection and prevention
- Compliance monitoring and reporting
- Emergency controls and multi-sig circuit breakers
Blockchain Integration
- Smart contract interactions abstracted and secured
- Gas optimization and transaction batching
- Event monitoring and blockchain state synchronization
- Cross-chain support (future roadmap)
SDK Setup
For custom development, you'll use the MCAP Development SDK which provides low-level access to all MCAP infrastructure features.
📖 Complete SDK Setup: See the MCAP SDK Guide for detailed installation, configuration, and usage examples.
Key Provider API Endpoints
When building custom games, your server will interact with these essential MCAP provider endpoints. For complete API documentation including request/response examples, see MCAP API Documentation.
Provider Wallet Balance
POST /v1/providers/wallets/balance - API Reference
Check a user's current balance for a specific session. Returns both available balance (funds that can be bet) and locked balance (funds currently tied up in pending bets). Use this before allowing bets or displaying funds in your game UI.
Place Bet
POST /v1/providers/bets/place-bet - API Reference
Initiate any game action that involves risking user funds. This endpoint locks the specified amount from the user's available balance and creates a pending bet record with atomic smart contract transaction handling. Call this at the start of every game round to secure funds before executing game logic.
Bet Outcome
POST /v1/providers/bets/bet-outcome - API Reference
Finalize the result of a game action and process winnings or losses. This endpoint unlocks the originally bet amount and applies the payout (zero for losses, positive for wins), updating the user's balance accordingly. Essential for completing the betting lifecycle.
Void Bet
POST /v1/providers/bets/void - API Reference
Cancel a pending bet and return locked funds to the user's available balance. Use this to handle error conditions, game interruptions, or technical issues gracefully without processing any payout.
Get Session Details
GET /v1/providers/sessions/{session_id} - API Reference
Retrieve session information including user wallet details, token type, and session expiration. Use this to validate session authenticity and get user context for customizing game behavior based on session parameters.
Complete Slots Game Integration Example
Here's a comprehensive example of integrating a Slots game using the provider endpoints:
Backend Implementation (Node.js/Express)
// slots-game-server.js
import express from 'express';
import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
const app = express();
app.use(express.json());
const MCAP_API_BASE = process.env.MCAP_API_BASE || 'https://mcap-api.mcap.meme';
const MCAP_API_KEY = process.env.MCAP_API_KEY;
// MCAP API client
class MCAPClient {
constructor(apiKey, baseURL) {
this.apiKey = apiKey;
this.baseURL = baseURL;
}
async makeRequest(method, endpoint, data = null) {
try {
const response = await axios({
method,
url: `${this.baseURL}${endpoint}`,
data,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
});
return response.data;
} catch (error) {
console.error('MCAP API Error:', error.response?.data || error.message);
throw error;
}
}
async getSessionBalance(sessionId) {
return this.makeRequest('POST', '/v1/providers/wallets/balance', {
session_id: sessionId
});
}
async placeBet(betId, sessionId, amount, betData) {
return this.makeRequest('POST', '/v1/providers/bets/place-bet', {
bet_id: betId,
session_id: sessionId,
amount: amount.toString(), // Amount in wei
bet_data: betData
});
}
async settleBet(betId, sessionId, payout, betData) {
return this.makeRequest('POST', '/v1/providers/bets/bet-outcome', {
bet_id: betId,
session_id: sessionId,
payout: payout.toString(), // Payout in wei
bet_data: betData
});
}
async voidBet(betId) {
return this.makeRequest('POST', '/v1/providers/bets/void', {
bet_id: betId
});
}
async getSession(sessionId) {
return this.makeRequest('GET', `/v1/providers/sessions/${sessionId}`);
}
}
const mcap = new MCAPClient(MCAP_API_KEY, MCAP_API_BASE);
// Slots game logic
class SlotsGame {
constructor() {
this.symbols = [
{ name: 'cherry', weight: 20, payouts: { 3: 10, 4: 25, 5: 100 } },
{ name: 'lemon', weight: 18, payouts: { 3: 15, 4: 40, 5: 150 } },
{ name: 'orange', weight: 15, payouts: { 3: 20, 4: 60, 5: 200 } },
{ name: 'plum', weight: 12, payouts: { 3: 30, 4: 80, 5: 300 } },
{ name: 'bell', weight: 10, payouts: { 3: 50, 4: 150, 5: 500 } },
{ name: 'bar', weight: 8, payouts: { 3: 100, 4: 300, 5: 1000 } },
{ name: 'seven', weight: 5, payouts: { 3: 200, 4: 500, 5: 2000 } },
{ name: 'diamond', weight: 2, payouts: { 3: 500, 4: 1500, 5: 5000 } }
];
this.reels = 5;
this.rows = 3;
this.paylines = [
[1, 1, 1, 1, 1], // Top row
[2, 2, 2, 2, 2], // Middle row
[3, 3, 3, 3, 3], // Bottom row
[1, 2, 3, 2, 1], // V shape
[3, 2, 1, 2, 3], // Inverse V
// Add more paylines as needed
];
}
// Generate weighted random symbol
getRandomSymbol() {
const totalWeight = this.symbols.reduce((sum, symbol) => sum + symbol.weight, 0);
let random = Math.random() * totalWeight;
for (const symbol of this.symbols) {
random -= symbol.weight;
if (random <= 0) {
return symbol.name;
}
}
return this.symbols[0].name; // Fallback
}
// Generate reel results
spin() {
const results = [];
for (let reel = 0; reel < this.reels; reel++) {
const reelResults = [];
for (let row = 0; row < this.rows; row++) {
reelResults.push(this.getRandomSymbol());
}
results.push(reelResults);
}
return results;
}
// Calculate winnings
calculateWinnings(reelResults, betPerLine) {
let totalWinnings = 0;
const winningLines = [];
for (let lineIndex = 0; lineIndex < this.paylines.length; lineIndex++) {
const payline = this.paylines[lineIndex];
const lineSymbols = payline.map((rowIndex, reelIndex) =>
reelResults[reelIndex][rowIndex - 1]
);
// Check for winning combinations
const { isWinning, count, symbol } = this.checkWinningLine(lineSymbols);
if (isWinning) {
const symbolData = this.symbols.find(s => s.name === symbol);
const multiplier = symbolData.payouts[count] || 0;
const lineWinning = BigInt(betPerLine) * BigInt(multiplier);
totalWinnings += Number(lineWinning);
winningLines.push({
line: lineIndex + 1,
symbol,
count,
multiplier,
winnings: lineWinning.toString()
});
}
}
return {
totalWinnings: totalWinnings.toString(),
winningLines,
isWin: winningLines.length > 0
};
}
// Check if a payline is winning
checkWinningLine(lineSymbols) {
let consecutiveCount = 1;
const firstSymbol = lineSymbols[0];
// Count consecutive symbols from left to right
for (let i = 1; i < lineSymbols.length; i++) {
if (lineSymbols[i] === firstSymbol) {
consecutiveCount++;
} else {
break;
}
}
// Need at least 3 consecutive symbols to win
const isWinning = consecutiveCount >= 3;
return {
isWinning,
count: consecutiveCount,
symbol: firstSymbol
};
}
}
const slotsGame = new SlotsGame();
// API Routes
// Get current balance for session
app.get('/api/slots/balance/:sessionId', async (req, res) => {
try {
const { sessionId } = req.params;
const balanceData = await mcap.getSessionBalance(sessionId);
res.json({
success: true,
balance: balanceData.balance
});
} catch (error) {
res.status(500).json({
success: false,
error: error.response?.data?.error || 'Failed to get balance'
});
}
});
// Spin the slots
app.post('/api/slots/spin', async (req, res) => {
try {
const { sessionId, betAmount, paylines } = req.body;
// Validate input
if (!sessionId || !betAmount || !paylines) {
return res.status(400).json({
success: false,
error: 'Missing required fields: sessionId, betAmount, paylines'
});
}
const betId = uuidv4();
const betPerLine = Math.floor(parseInt(betAmount) / paylines);
const totalBet = (betPerLine * paylines).toString();
// Step 1: Place bet with MCAP
const placeBetResponse = await mcap.placeBet(betId, sessionId, totalBet, {
game_type: 'slots',
paylines: paylines,
bet_per_line: betPerLine.toString(),
total_bet: totalBet
});
// Step 2: Generate spin results
const reelResults = slotsGame.spin();
const winResults = slotsGame.calculateWinnings(reelResults, betPerLine);
// Step 3: Settle bet with results
const settlementData = {
result: winResults.isWin ? 'win' : 'loss',
reel_results: reelResults,
winning_lines: winResults.winningLines,
total_winnings: winResults.totalWinnings,
rtp_percentage: calculateRTP(totalBet, winResults.totalWinnings)
};
const settleResponse = await mcap.settleBet(
betId,
sessionId,
winResults.totalWinnings,
settlementData
);
// Step 4: Return results to client
res.json({
success: true,
bet_id: betId,
spin_results: {
reels: reelResults,
winning_lines: winResults.winningLines,
total_winnings: winResults.totalWinnings,
is_win: winResults.isWin
},
balance: settleResponse.balance
});
} catch (error) {
console.error('Slots spin error:', error);
// If we placed a bet but failed to settle, try to void it
if (error.betId) {
try {
await mcap.voidBet(error.betId);
} catch (voidError) {
console.error('Failed to void bet:', voidError);
}
}
res.status(500).json({
success: false,
error: error.response?.data?.error || 'Spin failed'
});
}
});
// Get session info
app.get('/api/slots/session/:sessionId', async (req, res) => {
try {
const { sessionId } = req.params;
const sessionData = await mcap.getSession(sessionId);
res.json({
success: true,
session: sessionData
});
} catch (error) {
res.status(500).json({
success: false,
error: error.response?.data?.error || 'Failed to get session'
});
}
});
// Calculate RTP (Return to Player) percentage
function calculateRTP(betAmount, winAmount) {
const bet = parseInt(betAmount);
const win = parseInt(winAmount);
if (bet === 0) return 0;
return ((win / bet) * 100).toFixed(2);
}
// Error handling middleware
app.use((error, req, res, next) => {
console.error('Server error:', error);
res.status(500).json({
success: false,
error: 'Internal server error'
});
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Slots game server running on port ${PORT}`);
});
Frontend Implementation (React)
// SlotsGame.jsx
import React, { useState, useEffect } from 'react';
import { useSession } from '@mcap/sdk-react';
const SlotsGame = ({ sessionId }) => {
const [balance, setBalance] = useState(null);
const [betAmount, setBetAmount] = useState('100000000000000000'); // 0.1 token
const [paylines, setPaylines] = useState(25);
const [reelResults, setReelResults] = useState([]);
const [winnings, setWinnings] = useState(null);
const [isSpinning, setIsSpinning] = useState(false);
const [gameHistory, setGameHistory] = useState([]);
// Fetch initial balance
useEffect(() => {
if (sessionId) {
fetchBalance();
}
}, [sessionId]);
const fetchBalance = async () => {
try {
const response = await fetch(`/api/slots/balance/${sessionId}`);
const data = await response.json();
if (data.success) {
setBalance(data.balance);
} else {
console.error('Failed to fetch balance:', data.error);
}
} catch (error) {
console.error('Balance fetch error:', error);
}
};
const spin = async () => {
if (!sessionId || isSpinning) return;
setIsSpinning(true);
setWinnings(null);
try {
const response = await fetch('/api/slots/spin', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId,
betAmount,
paylines
})
});
const data = await response.json();
if (data.success) {
// Animate reels spinning
animateReels();
// Show results after animation
setTimeout(() => {
setReelResults(data.spin_results.reels);
setWinnings(data.spin_results);
setBalance(data.balance);
// Add to history
setGameHistory(prev => [{
id: data.bet_id,
bet: betAmount,
win: data.spin_results.total_winnings,
timestamp: new Date().toISOString()
}, ...prev.slice(0, 9)]); // Keep last 10 games
setIsSpinning(false);
}, 2000);
} else {
console.error('Spin failed:', data.error);
alert('Spin failed: ' + data.error);
setIsSpinning(false);
}
} catch (error) {
console.error('Spin error:', error);
alert('Network error occurred');
setIsSpinning(false);
}
};
const animateReels = () => {
// Simple animation - in production you'd use more sophisticated animations
const symbols = ['🍒', '🍋', '🍊', '🍇', '🔔', '⭐', '💎', '7️⃣'];
for (let i = 0; i < 20; i++) {
setTimeout(() => {
const animatedResults = Array(5).fill().map(() =>
Array(3).fill().map(() => symbols[Math.floor(Math.random() * symbols.length)])
);
setReelResults(animatedResults);
}, i * 100);
}
};
const formatAmount = (amount) => {
const formatted = (parseInt(amount) / 1e18).toFixed(4);
return parseFloat(formatted).toString();
};
const getSymbolEmoji = (symbol) => {
const emojiMap = {
cherry: '🍒',
lemon: '🍋',
orange: '🍊',
plum: '🍇',
bell: '🔔',
bar: '⭐',
seven: '7️⃣',
diamond: '💎'
};
return emojiMap[symbol] || symbol;
};
const canSpin = balance && parseInt(balance.available_balance) >= parseInt(betAmount) && !isSpinning;
return (
<div className="slots-game">
<div className="game-header">
<h2>🎰 MCAP Slots</h2>
{balance && (
<div className="balance-info">
<div>Available: {formatAmount(balance.available_balance)} tokens</div>
<div>Locked: {formatAmount(balance.locked_balance)} tokens</div>
</div>
)}
</div>
<div className="game-controls">
<div className="bet-controls">
<label>
Bet Amount:
<input
type="number"
value={formatAmount(betAmount)}
onChange={(e) => setBetAmount((parseFloat(e.target.value) * 1e18).toString())}
min="0.001"
step="0.001"
disabled={isSpinning}
/>
</label>
<label>
Paylines:
<select
value={paylines}
onChange={(e) => setPaylines(parseInt(e.target.value))}
disabled={isSpinning}
>
<option value={1}>1</option>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={25}>25</option>
</select>
</label>
<div className="total-bet">
Total Bet: {formatAmount((parseInt(betAmount) / paylines * paylines).toString())} tokens
</div>
</div>
<button
onClick={spin}
disabled={!canSpin}
className={`spin-button ${isSpinning ? 'spinning' : ''}`}
>
{isSpinning ? '🎰 SPINNING...' : '🎰 SPIN'}
</button>
</div>
<div className="reels-container">
{reelResults.map((reel, reelIndex) => (
<div key={reelIndex} className="reel">
{reel.map((symbol, symbolIndex) => (
<div key={symbolIndex} className="symbol">
{getSymbolEmoji(symbol)}
</div>
))}
</div>
))}
</div>
{winnings && (
<div className="results">
{winnings.is_win ? (
<div className="win-message">
🎉 YOU WON! 🎉
<div className="win-amount">
{formatAmount(winnings.total_winnings)} tokens
</div>
{winnings.winning_lines.map((line, index) => (
<div key={index} className="winning-line">
Line {line.line}: {line.count}x {getSymbolEmoji(line.symbol)} = {formatAmount(line.winnings)} tokens
</div>
))}
</div>
) : (
<div className="loss-message">
Better luck next time! 🍀
</div>
)}
</div>
)}
<div className="game-history">
<h3>Recent Games</h3>
{gameHistory.map((game) => (
<div key={game.id} className="history-item">
<span>Bet: {formatAmount(game.bet)}</span>
<span className={parseInt(game.win) > 0 ? 'win' : 'loss'}>
{parseInt(game.win) > 0 ? `Won: ${formatAmount(game.win)}` : 'Lost'}
</span>
</div>
))}
</div>
</div>
);
};
export default SlotsGame;
CSS Styling
/* SlotsGame.css */
.slots-game {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.game-header {
text-align: center;
margin-bottom: 20px;
}
.balance-info {
display: flex;
justify-content: space-around;
background: #f0f0f0;
padding: 10px;
border-radius: 8px;
margin: 10px 0;
}
.game-controls {
background: #fff;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.bet-controls {
display: flex;
gap: 20px;
align-items: center;
margin-bottom: 15px;
flex-wrap: wrap;
}
.bet-controls label {
display: flex;
flex-direction: column;
gap: 5px;
}
.bet-controls input,
.bet-controls select {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
width: 120px;
}
.total-bet {
font-weight: bold;
color: #333;
}
.spin-button {
width: 100%;
padding: 15px;
font-size: 18px;
font-weight: bold;
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.spin-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0,0,0,0.2);
}
.spin-button:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
.spin-button.spinning {
animation: pulse 1s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.reels-container {
display: flex;
justify-content: center;
gap: 10px;
background: #2c3e50;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
}
.reel {
background: white;
border: 3px solid #gold;
border-radius: 8px;
padding: 10px 5px;
min-width: 80px;
}
.symbol {
text-align: center;
font-size: 24px;
padding: 5px;
border-bottom: 1px solid #eee;
}
.symbol:last-child {
border-bottom: none;
}
.results {
text-align: center;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.win-message {
background: linear-gradient(45deg, #2ecc71, #27ae60);
color: white;
animation: celebration 0.5s ease;
}
.loss-message {
background: #95a5a6;
color: white;
}
@keyframes celebration {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.win-amount {
font-size: 24px;
font-weight: bold;
margin: 10px 0;
}
.winning-line {
font-size: 14px;
margin: 5px 0;
}
.game-history {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
}
.history-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.history-item:last-child {
border-bottom: none;
}
.history-item .win {
color: #27ae60;
font-weight: bold;
}
.history-item .loss {
color: #e74c3c;
}
/* Responsive design */
@media (max-width: 768px) {
.bet-controls {
flex-direction: column;
align-items: stretch;
}
.bet-controls label {
flex-direction: row;
justify-content: space-between;
}
.reels-container {
flex-wrap: wrap;
}
.balance-info {
flex-direction: column;
gap: 5px;
}
}
This comprehensive example demonstrates:
- Complete API Integration: Using all key provider endpoints
- Real Game Logic: Actual slots game with paylines, symbols, and payouts
- Proper Error Handling: Bet voiding on failures, user feedback
- Balance Management: Real-time balance updates
- User Experience: Animations, history, responsive design
- Production Ready: Proper validation, security considerations
The example shows how providers can build engaging games while leveraging MCAP's infrastructure for all the complex blockchain and financial operations.
Core SDK Features Integration
For complete implementation examples and code samples, see the MCAP SDK Guide.
1. Wallet Connection & Authentication - SDK Reference
Handle Web3 wallet connection and MCAP authentication flow. The SDK provides React hooks for connecting wallets, generating authentication nonces, requesting wallet signatures, and exchanging signatures for JWT tokens. Includes automatic authentication state management and error handling.
2. Deposit Management - SDK Reference
Enable users to deposit ERC20 tokens into your provider's vault contract. The SDK handles token approval workflows, deposit transactions, balance checking, and deposit status monitoring. Provides real-time balance updates and transaction confirmation handling.
3. Withdrawal System - SDK Reference
Allow users to withdraw their available balance back to their wallet. The SDK manages withdrawal requests, balance validation, transaction processing, and status polling. Includes support for partial withdrawals and maximum balance calculations.
4. Play History & Analytics - SDK Reference
Display user transaction and betting history with filtering and analytics. The SDK provides methods to fetch transaction history, bet outcomes, and user activity data. Includes filtering by date range, outcome type, and token, plus formatted display helpers.
Custom Game Development Examples
Example 1: Simple Coin Flip Game
// games/CoinFlipGame.jsx
import React, { useState } from 'react';
import { useSession, useBets } from '@mcap/dev-sdk-react';
const CoinFlipGame = ({ sessionId, selectedToken }) => {
const [betAmount, setBetAmount] = useState('');
const [selectedSide, setSelectedSide] = useState('heads');
const [gameState, setGameState] = useState('ready'); // ready, betting, flipping, result
const [result, setResult] = useState(null);
const { placeBet, settleBet } = useBets();
const playGame = async () => {
if (!betAmount || !sessionId) return;
setGameState('betting');
try {
// Convert bet to wei
const amountWei = ethers.utils.parseUnits(betAmount, selectedToken.decimals);
// Place bet with MCAP
const bet = await placeBet({
sessionId: sessionId,
amount: amountWei.toString(),
betData: {
gameType: 'coin_flip',
selection: selectedSide,
timestamp: Date.now()
}
});
setGameState('flipping');
// Simulate coin flip (replace with your game logic)
setTimeout(async () => {
const flipResult = Math.random() < 0.5 ? 'heads' : 'tails';
const isWin = flipResult === selectedSide;
const payout = isWin ? amountWei.mul(2) : ethers.BigNumber.from(0);
// Settle bet with MCAP
await settleBet({
betId: bet.id,
outcome: isWin ? 'win' : 'loss',
payout: payout.toString(), // Payout in wei
gameData: {
flipResult: flipResult,
playerSelection: selectedSide,
multiplier: isWin ? 2 : 0
}
});
setResult({
flip: flipResult,
won: isWin,
payout: payout.toString()
});
setGameState('result');
// Reset after 3 seconds
setTimeout(() => {
setGameState('ready');
setResult(null);
setBetAmount('');
}, 3000);
}, 2000);
} catch (error) {
console.error('Game failed:', error);
setGameState('ready');
alert('Game failed: ' + error.message);
}
};
return (
<div className="coin-flip-game">
<h2>Coin Flip</h2>
{gameState === 'ready' && (
<div className="game-setup">
<div className="bet-amount">
<label>Bet Amount:</label>
<input
type="number"
value={betAmount}
onChange={(e) => setBetAmount(e.target.value)}
placeholder={`Amount in ${selectedToken.symbol}`}
/>
</div>
<div className="side-selection">
<label>Choose Side:</label>
<div className="sides">
<button
className={selectedSide === 'heads' ? 'selected' : ''}
onClick={() => setSelectedSide('heads')}
>
Heads
</button>
<button
className={selectedSide === 'tails' ? 'selected' : ''}
onClick={() => setSelectedSide('tails')}
>
Tails
</button>
</div>
</div>
<button onClick={playGame} disabled={!betAmount}>
Flip Coin
</button>
</div>
)}
{gameState === 'betting' && (
<div className="game-status">Placing bet...</div>
)}
{gameState === 'flipping' && (
<div className="game-status">
<div className="coin-animation">🪙</div>
Coin is flipping...
</div>
)}
{gameState === 'result' && result && (
<div className="game-result">
<div className="flip-result">
Result: {result.flip.toUpperCase()}
</div>
<div className={`outcome ${result.won ? 'win' : 'loss'}`}>
{result.won ? 'You Won! 🎉' : 'You Lost 😢'}
</div>
{result.won && (
<div className="payout">
Payout: {ethers.utils.formatUnits(result.payout, selectedToken.decimals)} {selectedToken.symbol}
</div>
)}
</div>
)}
</div>
);
};
export default CoinFlipGame;
Example 2: Multiplayer Rock Paper Scissors
// games/RockPaperScissors.jsx
import React, { useState, useEffect } from 'react';
import { useMultiplayer } from '@mcap/dev-sdk-react';
const RockPaperScissors = ({ sessionId, selectedToken }) => {
const [gameState, setGameState] = useState('lobby'); // lobby, waiting, playing, result
const [currentMatch, setCurrentMatch] = useState(null);
const [playerMove, setPlayerMove] = useState(null);
const [entryFee, setEntryFee] = useState('');
const {
createMatch,
joinMatch,
leaveMatch,
submitMove,
getMatchStatus
} = useMultiplayer();
const createNewMatch = async () => {
if (!entryFee) return;
try {
const amountWei = ethers.utils.parseUnits(entryFee, selectedToken.decimals);
const match = await createMatch({
gameType: 'rock_paper_scissors',
maxPlayers: 2,
entryFee: amountWei.toString(),
tokenId: selectedToken.id,
gameSettings: {
rounds: 1,
timeLimit: 30 // seconds per move
}
});
setCurrentMatch(match);
setGameState('waiting');
} catch (error) {
console.error('Failed to create match:', error);
alert('Failed to create match: ' + error.message);
}
};
const joinExistingMatch = async (matchId) => {
try {
const match = await joinMatch(matchId);
setCurrentMatch(match);
setGameState('playing');
} catch (error) {
console.error('Failed to join match:', error);
alert('Failed to join match: ' + error.message);
}
};
const makeMove = async (move) => {
setPlayerMove(move);
try {
await submitMove({
matchId: currentMatch.id,
move: move,
round: 1
});
// Wait for opponent move
pollForResult();
} catch (error) {
console.error('Failed to submit move:', error);
alert('Move submission failed: ' + error.message);
}
};
const pollForResult = async () => {
const checkResult = async () => {
const status = await getMatchStatus(currentMatch.id);
if (status.status === 'completed') {
setCurrentMatch(status);
setGameState('result');
} else if (status.status === 'active') {
// Still waiting for moves
setTimeout(checkResult, 2000);
}
};
setTimeout(checkResult, 1000);
};
const resetGame = () => {
setGameState('lobby');
setCurrentMatch(null);
setPlayerMove(null);
setEntryFee('');
};
return (
<div className="rps-game">
<h2>Rock Paper Scissors</h2>
{gameState === 'lobby' && (
<div className="lobby">
<div className="create-match">
<h3>Create New Match</h3>
<input
type="number"
value={entryFee}
onChange={(e) => setEntryFee(e.target.value)}
placeholder={`Entry fee in ${selectedToken.symbol}`}
/>
<button onClick={createNewMatch} disabled={!entryFee}>
Create Match
</button>
</div>
<div className="join-match">
<h3>Or Join Existing Match</h3>
{/* List of available matches would go here */}
<p>Available matches will appear here</p>
</div>
</div>
)}
{gameState === 'waiting' && (
<div className="waiting">
<h3>Waiting for Opponent...</h3>
<p>Match ID: {currentMatch?.id}</p>
<p>Entry Fee: {entryFee} {selectedToken.symbol}</p>
<button onClick={resetGame}>Cancel</button>
</div>
)}
{gameState === 'playing' && (
<div className="playing">
<h3>Make Your Move!</h3>
<div className="moves">
{['rock', 'paper', 'scissors'].map(move => (
<button
key={move}
onClick={() => makeMove(move)}
disabled={playerMove !== null}
className={playerMove === move ? 'selected' : ''}
>
{move === 'rock' ? '🪨' : move === 'paper' ? '📄' : '✂️'}
{move.charAt(0).toUpperCase() + move.slice(1)}
</button>
))}
</div>
{playerMove && (
<div className="waiting-opponent">
Your move: {playerMove}. Waiting for opponent...
</div>
)}
</div>
)}
{gameState === 'result' && currentMatch && (
<div className="result">
<h3>Game Result</h3>
<div className="moves-display">
<div>You: {playerMove}</div>
<div>Opponent: {currentMatch.opponentMove}</div>
</div>
<div className={`outcome ${currentMatch.winner === 'player' ? 'win' : 'loss'}`}>
{currentMatch.winner === 'player' ? 'You Won! 🎉' :
currentMatch.winner === 'opponent' ? 'You Lost 😢' : 'Draw 🤝'}
</div>
{currentMatch.winner === 'player' && (
<div className="prize">
Prize: {ethers.utils.formatUnits(currentMatch.prize, selectedToken.decimals)} {selectedToken.symbol}
</div>
)}
<button onClick={resetGame}>Play Again</button>
</div>
)}
</div>
);
};
export default RockPaperScissors;
Advanced Features
Custom Analytics Integration
// analytics/GameAnalytics.js
import { MCAPAnalytics } from '@mcap/dev-sdk';
export class GameAnalytics {
constructor(gameId, providerId) {
this.analytics = new MCAPAnalytics({
gameId,
providerId,
enableUserTracking: true
});
}
// Track game-specific events
trackGameStart(userId, gameType, betAmount, tokenSymbol) {
this.analytics.track('game_start', {
user_id: userId,
game_type: gameType,
bet_amount: betAmount,
token_symbol: tokenSymbol,
timestamp: Date.now()
});
}
trackGameEnd(userId, gameType, outcome, payout, duration) {
this.analytics.track('game_end', {
user_id: userId,
game_type: gameType,
outcome: outcome,
payout: payout,
game_duration: duration,
timestamp: Date.now()
});
}
trackUserAction(userId, action, metadata = {}) {
this.analytics.track('user_action', {
user_id: userId,
action: action,
...metadata,
timestamp: Date.now()
});
}
// Get game performance metrics
async getGameMetrics(timeRange = '7d') {
return await this.analytics.getMetrics({
timeRange,
metrics: [
'total_games',
'unique_players',
'total_volume',
'average_bet_size',
'player_retention',
'game_duration_avg'
]
});
}
// Get player insights
async getPlayerInsights(userId) {
return await this.analytics.getPlayerInsights(userId, {
include: [
'favorite_games',
'betting_patterns',
'win_rate',
'total_wagered',
'session_frequency'
]
});
}
}
Ready to build your custom game or application? Contact our development team at partnership@mcap.meme for personalized guidance and implementation support.