PVP Escrow
Primitive

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.

ResolverWhat It DoesExample Actions
rpsRock-Paper-Scissors rules: rock > scissors > paper > rockrock, paper, scissors
coin_flipBoth pick heads/tails. If same, random tiebreaker.heads, tails
higher_numberBoth pick 1–100. Higher number wins. Ties broken randomly.Any integer 1–100

Game Flow

1. Create/JoinPlayer creates a match or gets matched with a waiting player
2. CommitBoth players submit their action (hashed). Funds are escrowed.
3. RevealOnce both committed, actions are revealed by the server
4. ResolveWinner is determined, pot is distributed (minus platform edge)

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 possible
6 timeout: 30000 // Match timeout in ms (5s–600s)
7};

Config Parameters

ParameterTypeDescription
feenumberRequired. House fee as decimal (0.01 = 1%). Max 0.10 (10%).
resolverstringGame resolver: 'rps', 'coin_flip', or 'higher_number'
allowDrawbooleanOptional. Whether draws are allowed (default true)
timeoutnumberOptional. Match timeout in ms (5000–600000)

Built-in Resolvers

ResolverActionsRules
rpsrock, paper, scissorsStandard RPS rules
coin_flip (or coinflip)heads, tailsRandom winner on tie
higher_number1–100Higher 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 ID
4 stake: window.GAME_STAKE,
5 currency: window.GAME_CURRENCY,
6 resolver: 'rps' // Must match GAME_CONFIG resolver
7}, '*');
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: true
12// }

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.matchId
5}, '*');
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 match
2// { type: 'pvpOpponentJoined',
3// matchId: "abc123",
4// opponentUsername: "player2",
5// pot: 20,
6// status: 'ready' }
7
8// Opponent committed their action
9// { type: 'pvpOpponentCommitted',
10// matchId: "abc123",
11// playerId: "user456",
12// allCommitted: true,
13// status: 'playing' }
14
15// Match resolved — game over
16// { 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 cancelled
35// { type: 'pvpCancelled',
36// matchId: "abc123",
37// status: 'cancelled' }
The platform handles all polling for you. Just listen for the push events above and update your UI accordingly.

Message Quick Reference

All outgoing messages you send and the push events you receive, at a glance:

MessageDirectionKey Response Fields
pvpCreateYou → PlatformmatchId, status, pot, matched, players[], opponentUsername
pvpCommitYou → Platformsuccess, committed
pvpCancelYou → Platformsuccess
pvpOpponentJoinedPlatform → YoumatchId, opponentUsername, pot, status
pvpOpponentCommittedPlatform → YoumatchId, playerId, allCommitted, status
pvpResultPlatform → YoumatchId, outcome, winner, pot, houseFee, prizePool, payouts[], players[]
pvpCancelledPlatform → YoumatchId, status

Provably Fair

Both actions are committed using hash-based commit-reveal. Neither player can change their action after committing:

WhenWhat Happens
CommitPlayer's action is hashed and stored. Funds are escrowed.
RevealBoth players' actions are revealed simultaneously
VerificationActions 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' }, '*');
A 30-second fallback fires automatically if you never send 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: 30000
21 };
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 array
59 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 event
67 } 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: action
79 });
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 id
90 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 joined
99 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 committed
106 if (d.type === 'pvpOpponentCommitted') {
107 document.getElementById('status').textContent =
108 'Opponent chose! Resolving...';
109 }
110
111 // Platform push: match resolved
112 if (d.type === 'pvpResult') {
113 showResult(d);
114 }
115
116 // Platform push: match cancelled
117 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 variables
125 }
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 ID
137 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>
PVP games require two real players. In test/preview mode, you can open two browser tabs to simulate both players.