Player-vs-player with escrowed stakes — commit, reveal, resolve.
Both players stake equal amounts into escrow. Each commits an action secretly, then actions are revealed and the winner takes the pot. Good for rock-paper-scissors, coin flip, higher-number games.
How It Works
You define:
Resolver type (rps, coin_flip, higher_number), valid actions
Platform handles:
Matchmaking, escrow, commit-reveal, payout
What is a Resolver?
A resolver is a server-side function that determines the winner of a PVP match. You pick a resolver in your GAME_CONFIG — the platform uses it to automatically evaluate both players' actions and decide the outcome. You never write win/loss logic yourself; the resolver handles it.
| Resolver | What It Does | Example Actions |
|---|---|---|
| rps | Rock-Paper-Scissors rules: rock > scissors > paper > rock | rock, paper, scissors |
| coin_flip | Both pick heads/tails. If same, random tiebreaker. | heads, tails |
| higher_number | Both pick 1–100. Higher number wins. Ties broken randomly. | Any integer 1–100 |
Game Flow
Game Config
1window.GAME_CONFIG = {2 primitive: 'pvp',3 fee: 0.01, // House fee (0.01 = 1%, max 0.10)4 resolver: 'rps', // 'rps' | 'coin_flip' | 'higher_number'5 allowDraw: true, // Whether draws are possible6 timeout: 30000 // Match timeout in ms (5s–600s)7};Config Parameters
| Parameter | Type | Description |
|---|---|---|
| fee | number | Required. House fee as decimal (0.01 = 1%). Max 0.10 (10%). |
| resolver | string | Game resolver: 'rps', 'coin_flip', or 'higher_number' |
| allowDraw | boolean | Optional. Whether draws are allowed (default true) |
| timeout | number | Optional. Match timeout in ms (5000–600000) |
Built-in Resolvers
| Resolver | Actions | Rules |
|---|---|---|
| rps | rock, paper, scissors | Standard RPS rules |
| coin_flip (or coinflip) | heads, tails | Random winner on tie |
| higher_number | 1–100 | Higher number wins, ties resolved randomly |
PostMessage API
PVP games use three outgoing messages: create, commit, and cancel. When a resolver is configured, the platform auto-resolves the match server-side once both players commit — your game does not need to determine or send the outcome. The platform pushes status updates to your iframe automatically — no polling required.
1. Create / Join a Match
1parent.postMessage({2 type: 'pvpCreate',3 id: Date.now(), // Unique request ID4 stake: window.GAME_STAKE,5 currency: window.GAME_CURRENCY,6 resolver: 'rps' // Must match GAME_CONFIG resolver7}, '*');8
9// Response (matched by id):10// If WAITING for opponent:11// {12// matchId: "abc123",13// status: 'waiting',14// pot: 10,15// fee: 0.01,16// stakeUsd: 10,17// resolver: "rps",18// matched: false,19// players: [{ id: "user123", stakeUsd: 10 }]20// }21
22// If MATCHED immediately:23// {24// matchId: "abc123",25// status: 'ready',26// pot: 20,27// fee: 0.01,28// stakeUsd: 10,29// resolver: "rps",30// matched: true,31// opponentUsername: "player2",32// players: [33// { id: "user456", stakeUsd: 10 },34// { id: "user123", stakeUsd: 10 }35// ]36// }2. Commit an Action
1parent.postMessage({2 type: 'pvpCommit',3 id: Date.now(),4 matchId: state.matchId,5 action: 'rock'6}, '*');7
8// Response (matched by id):9// {10// success: true,11// committed: true12// }3. Cancel a Match
Cancel a match while waiting for an opponent (before the match is ready):
1parent.postMessage({2 type: 'pvpCancel',3 id: Date.now(),4 matchId: state.matchId5}, '*');6
7// Response (matched by id):8// { success: true }4. Platform Push Events
The platform automatically polls the match status and pushes these events to your iframe. You do not need to poll manually:
1// Opponent joined your match2// { type: 'pvpOpponentJoined',3// matchId: "abc123",4// opponentUsername: "player2",5// pot: 20,6// status: 'ready' }7
8// Opponent committed their action9// { type: 'pvpOpponentCommitted',10// matchId: "abc123",11// playerId: "user456",12// allCommitted: true,13// status: 'playing' }14
15// Match resolved — game over16// { type: 'pvpResult',17// matchId: "abc123",18// status: 'resolved',19// outcome: 'player1_wins',20// winner: "user123",21// pot: 20,22// houseFee: 0.20,23// prizePool: 19.80,24// serverSeed: "abc...",25// payouts: [26// { playerId: "user123", amount: 19.80 },27// { playerId: "user456", amount: 0 }28// ],29// players: [30// { id: "user123", action: "rock" },31// { id: "user456", action: "scissors" }32// ] }33
34// Match cancelled35// { type: 'pvpCancelled',36// matchId: "abc123",37// status: 'cancelled' }Message Quick Reference
All outgoing messages you send and the push events you receive, at a glance:
| Message | Direction | Key Response Fields |
|---|---|---|
| pvpCreate | You → Platform | matchId, status, pot, matched, players[], opponentUsername |
| pvpCommit | You → Platform | success, committed |
| pvpCancel | You → Platform | success |
| pvpOpponentJoined | Platform → You | matchId, opponentUsername, pot, status |
| pvpOpponentCommitted | Platform → You | matchId, playerId, allCommitted, status |
| pvpResult | Platform → You | matchId, outcome, winner, pot, houseFee, prizePool, payouts[], players[] |
| pvpCancelled | Platform → You | matchId, status |
Provably Fair
Both actions are committed using hash-based commit-reveal. Neither player can change their action after committing:
| When | What Happens |
|---|---|
| Commit | Player's action is hashed and stored. Funds are escrowed. |
| Reveal | Both players' actions are revealed simultaneously |
| Verification | Actions are verified against commitments before resolving |
Balance Reveal Timing
The player's visible balance updates as soon as the match resolves, is cancelled, or auto-resolves after a commit. To show a winner reveal animation before the balance updates, send balanceReveal when your animation finishes.
1// In your pvpResult / pvpCancelled handler, after the reveal animation:2parent.postMessage({ type: 'balanceReveal' }, '*');balanceReveal. Stake deductions (pvpCreate / pvpJoin) always update immediately — only the final payout or refund is deferred.Complete Template
1<!DOCTYPE html>2<html>3<head><title>My RPS Game</title></head>4<body>5 <button onclick="findMatch()">PLAY</button>6 <div id="status"></div>7 <div id="actions" style="display:none">8 <button onclick="commit('rock')">🪨 Rock</button>9 <button onclick="commit('paper')">📄 Paper</button>10 <button onclick="commit('scissors')">✂️ Scissors</button>11 </div>12 <div id="result"></div>13
14 <script>15 window.GAME_CONFIG = {16 primitive: 'pvp',17 fee: 0.01,18 resolver: 'rps',19 allowDraw: true,20 timeout: 3000021 };22
23 let matchId = null;24 let myPlayerId = null;25 const pendingRequests = new Map();26 let requestId = 0;27
28 // Request/response helper (id-based matching)29 function sendToParent(data) {30 return new Promise((resolve, reject) => {31 const id = ++requestId;32 pendingRequests.set(id, { resolve, reject });33 var payload = Object.assign({}, data, { id: id });34 if (window.TEST_MODE) {35 payload.gameConfig = window.GAME_CONFIG_FOR_API;36 }37 parent.postMessage(payload, '*');38 setTimeout(() => {39 if (pendingRequests.has(id)) {40 pendingRequests.delete(id);41 reject(new Error('Timeout'));42 }43 }, 30000);44 });45 }46
47 async function findMatch() {48 document.getElementById('status').textContent = 'Searching...';49 try {50 const d = await sendToParent({51 type: 'pvpCreate',52 stake: window.GAME_STAKE || 1,53 currency: window.GAME_CURRENCY || 'USDT',54 resolver: 'rps'55 });56 if (d.error) return alert(d.error);57 matchId = d.matchId;58 // Your player ID is always the last entry in the players array59 myPlayerId = d.players?.[d.players.length - 1]?.id || null;60
61 if (d.matched) {62 document.getElementById('status').textContent =63 'Opponent found! Choose your action:';64 document.getElementById('actions').style.display = 'block';65 }66 // If not matched, wait for pvpOpponentJoined push event67 } catch (err) { alert(err.message); }68 }69
70 async function commit(action) {71 document.getElementById('actions').style.display = 'none';72 document.getElementById('status').textContent =73 'Committing...';74 try {75 const d = await sendToParent({76 type: 'pvpCommit',77 matchId: matchId,78 action: action79 });80 if (d.error) return alert(d.error);81 document.getElementById('status').textContent =82 'Waiting for opponent...';83 } catch (err) { alert(err.message); }84 }85
86 window.addEventListener('message', (e) => {87 const d = e.data;88
89 // Resolve pending requests by id90 if (d.id && pendingRequests.has(d.id)) {91 const h = pendingRequests.get(d.id);92 pendingRequests.delete(d.id);93 if (d.error) h.reject(new Error(d.error));94 else h.resolve(d);95 return;96 }97
98 // Platform push: opponent joined99 if (d.type === 'pvpOpponentJoined') {100 document.getElementById('status').textContent =101 d.opponentUsername + ' joined! Choose your action:';102 document.getElementById('actions').style.display = 'block';103 }104
105 // Platform push: opponent committed106 if (d.type === 'pvpOpponentCommitted') {107 document.getElementById('status').textContent =108 'Opponent chose! Resolving...';109 }110
111 // Platform push: match resolved112 if (d.type === 'pvpResult') {113 showResult(d);114 }115
116 // Platform push: match cancelled117 if (d.type === 'pvpCancelled') {118 document.getElementById('status').textContent =119 'Match cancelled';120 matchId = null;121 }122
123 if (d.type === 'stakeUpdate') {124 // Update local stake variables125 }126 });127
128 function showResult(d) {129 // d.players = [{ id, action }, { id, action }]130 // d.outcome = 'player1_wins' | 'player2_wins' | 'draw'131 // d.winner = winning player's ID (or null for draw)132 // d.payouts = [{ playerId, amount }, ...]133 const emoji = { rock:'🪨', paper:'📄', scissors:'✂️' };134 const p1 = d.players?.[0];135 const p2 = d.players?.[1];136 // Compare winner to our stored player ID137 var status = d.outcome === 'draw' ? 'Draw!'138 : d.winner === myPlayerId ? 'You won!' : 'You lost!';139 document.getElementById('result').innerHTML =140 '<p>' + (emoji[p1?.action] || '?') + ' vs ' +141 (emoji[p2?.action] || '?') + '</p>' +142 '<p><b>' + status + '</b></p>';143 matchId = null;144 }145
146 parent.postMessage({ type: 'gameReady' }, '*');147 </script>148</body>149</html>