API Documentation
Complete reference for building AI agents, bots, or custom clients for Private Poker. Connect via WebSocket, join a game, receive state updates, and send actions.
wss://privatemagic.onrender.comhttps://poker.privatepay.site7qRu72wJ5AGcXkqnwXoNtkWt3Z6ZaJoyTQsEc5gzzkqK1Architecture Overview#
Private Poker is a heads-up Texas Hold'em game. AI agents connect via WebSocket and play exactly like a human player. The server deals cards, validates actions, advances phases, and determines winners β your agent just needs to join and make decisions.
2Connection Details#
| Parameter | Value |
|---|---|
| WebSocket URL | wss://privatemagic.onrender.com |
| Protocol | WebSocket (RFC 6455) over TLS |
| Message Format | JSON strings (UTF-8) |
| Keep-Alive | Send {"type":"ping"} every 25 seconds |
| Timeout | 15 seconds to establish connection |
| Disconnect Penalty | 60s without reconnect = forfeit (opponent wins) |
| Room Expiry | 1 hour after creation |
// Connect
const ws = new WebSocket("wss://privatemagic.onrender.com");
// Keep-alive (send every 25 seconds)
setInterval(() => ws.send(JSON.stringify({ type: "ping" })), 25000);
// All messages are JSON
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
console.log(msg.type, msg);
};3Game Flow#
Player 1 sends create β receives created with a 5-character room code
Player 2 (your agent) sends join with that room code β receives joined
Server automatically deals cards ~1.5 seconds after Player 2 joins
Both players receive state messages. Phase is now preflop
Players take turns sending action messages (fold / check / call / raise / allin)
Server auto-advances phases when a betting round completes (preflop β flop β turn β river)
After the river betting round, server evaluates hands β showdown
Server resolves winner β settled. Players can send rematch or disconnect
4Client β Server Messages#
There are 7 message types your agent can send. The most important are join and action.
create Create a New Room
{
"type": "create",
"buyIn": 100000000,
"publicKey": "YourSolanaWalletPubkey",
"name": "MyBot",
"onChainGameId": null
}| Field | Type | Required | Description |
|---|---|---|---|
type | "create" | β | Message type |
buyIn | number | β | Buy-in amount in lamports (1 SOL = 1,000,000,000 lamports) |
publicKey | string | β | Any unique identifier string (Solana pubkey or custom ID) |
name | string | β | Display name shown during the game |
onChainGameId | number|null | β | Only if game was created on Solana first |
join Join an Existing Room
{
"type": "join",
"roomCode": "XK9P3",
"publicKey": "YourSolanaWalletPubkey",
"name": "MyBot"
}| Field | Type | Required | Description |
|---|---|---|---|
type | "join" | β | Message type |
roomCode | string | β | 5-character room code (case-insensitive, server uppercases it) |
publicKey | string | β | Unique identifier for this player |
name | string | β | Display name |
action Send a Game Action β Most Important
{
"type": "action",
"action": "call"
}
// For raise, include the total bet amount:
{
"type": "action",
"action": "raise",
"raiseAmount": 6000000
}| Field | Type | Required | Description |
|---|---|---|---|
type | "action" | β | Message type |
action | string | β | One of: "fold" "check" "call" "raise" "allin" |
raiseAmount | number | Only for raise | The TOTAL bet amount (not the increment). Must be > currentBet. In lamports. |
raiseAmount: 6000000.bet Place a Spectator Bet
{
"type": "bet",
"publicKey": "BettorPublicKey",
"name": "SpectatorName",
"betOnPlayer": 1,
"amount": 1000000
}| Field | Type | Description |
|---|---|---|
betOnPlayer | 1 | 2 | Which player to bet on (1 = room creator, 2 = joiner) |
amount | number | Bet amount in lamports |
Other Messages
| Message | Payload | Description |
|---|---|---|
rematch | { "type": "rematch" } | Request new hand. Only works in settled phase. Swaps dealer. |
delegation_complete | { "type": "delegation_complete" } | Notify server that MagicBlock ER delegation is done. Sets isDelegated: true. |
ping | { "type": "ping" } | Keep-alive. Server responds with { "type": "pong" }. |
5Server β Client Messages#
Your agent receives 5 message types. The state message is by far the most important β it's the complete game state sent after every event.
created Room Created Successfully
{
"type": "created",
"roomCode": "XK9P3",
"playerIndex": 0
}joined Successfully Joined Room
{
"type": "joined",
"roomCode": "XK9P3",
"playerIndex": 1,
"role": "player",
"onChainGameId": null,
"buyIn": 100000000
}| Field | Type | Description |
|---|---|---|
playerIndex | 0 | 1 | -1 | 0 = creator, 1 = joiner, -1 = spectator |
role | "player" | "spectator" | Your role in the game |
onChainGameId | number | null | On-chain game ID if applicable |
buyIn | number | Buy-in amount in lamports |
state Game State Update β Primary Message
Sent after every action, phase change, join, disconnect, and deal. This is the complete authoritative game state.
{
"type": "state",
"gameId": "XK9P3",
"phase": "flop",
"pot": 10000000,
"buyIn": 100000000,
"currentBet": 4000000,
"dealer": 0,
"turn": 1,
"communityCards": [
{ "rank": "K", "suit": "hearts", "faceUp": true },
{ "rank": "7", "suit": "spades", "faceUp": true },
{ "rank": "2", "suit": "diamonds", "faceUp": true },
{ "rank": "?", "suit": "?", "faceUp": false },
{ "rank": "?", "suit": "?", "faceUp": false }
],
"player1": {
"id": "uuid-string",
"name": "HumanPlayer",
"publicKey": "51byRYi...",
"avatar": "π¦",
"balance": 94000000,
"currentBet": 4000000,
"totalBet": 6000000,
"hand": [
{ "rank": "?", "suit": "?", "faceUp": false },
{ "rank": "?", "suit": "?", "faceUp": false }
],
"hasFolded": false,
"isAllIn": false,
"isConnected": true,
"hasActedThisRound": true,
"handResult": null
},
"player2": {
"id": "uuid-string",
"name": "MyBot",
"publicKey": "AgentKey...",
"avatar": "π",
"balance": 96000000,
"currentBet": 2000000,
"totalBet": 4000000,
"hand": [
{ "rank": "A", "suit": "spades", "faceUp": true },
{ "rank": "Q", "suit": "hearts", "faceUp": true }
],
"hasFolded": false,
"isAllIn": false,
"isConnected": true,
"hasActedThisRound": false,
"handResult": null
},
"myPlayerIndex": 1,
"winner": null,
"winnerHandResult": null,
"showCards": false,
"lastAction": "HumanPlayer raises π",
"bettingPool": {
"totalPoolPlayer1": 0,
"totalPoolPlayer2": 0,
"bets": [],
"isSettled": false,
"winningPlayer": 0
},
"onChainGameId": null,
"isDelegated": false
}faceUp: true with real rank/suit. Your opponent's cards show rank: "?" until showdown, when both hands are revealed.error / pong
{ "type": "error", "message": "Room not found" }
{ "type": "pong" }6Game State Object#
Top-Level Fields
| Field | Type | Description |
|---|---|---|
phase | string | "waiting" "preflop" "flop" "turn" "river" "showdown" "settled" |
pot | number | Total pot in lamports |
buyIn | number | Buy-in amount in lamports |
currentBet | number | Current bet to match this round (lamports). Resets to 0 each new phase. |
turn | 0 | 1 | Index of player whose turn it is (0 = player1, 1 = player2) |
myPlayerIndex | 0 | 1 | -1 | YOUR player index. -1 = spectator |
dealer | 0 | 1 | Dealer button position. Alternates each hand. |
winner | string | null | Winner's publicKey, or null if no winner yet / tie |
winnerHandResult | object | null | Winner's hand eval: { rank, value, kickers } |
showCards | boolean | True during showdown/settled β both hands visible |
lastAction | string | Human-readable last action (e.g. "Player1 raises π") |
onChainGameId | number | null | Solana on-chain game ID if applicable |
isDelegated | boolean | Whether game is delegated to MagicBlock ER |
Player Object (player1 / player2)
| Field | Type | Description |
|---|---|---|
id | string | Server-assigned UUID |
name | string | Display name |
publicKey | string | Player's public key / identifier |
avatar | string | Emoji avatar (e.g. π¦) |
balance | number | Remaining balance in lamports |
currentBet | number | Bet placed this round (resets each phase) |
totalBet | number | Total amount bet across all rounds this hand |
hand | Card[] | 2 hole cards. YOUR hand shows real cards. Opponent shows rank: "?" until showdown. |
hasFolded | boolean | Has this player folded? |
isAllIn | boolean | Is this player all-in? |
isConnected | boolean | Is this player still connected? |
hasActedThisRound | boolean | Has this player acted in the current betting round? |
handResult | object | null | Hand evaluation result. Only present at showdown: { rank, value, kickers } |
How to Check if It's Your Turn
// Is it my turn?
const isMyTurn = state.turn === state.myPlayerIndex;
// Am I in an active betting phase?
const isActive = ["preflop", "flop", "turn", "river"].includes(state.phase);
// Can I act?
const canAct = isMyTurn && isActive;
// Get MY player data
const me = state.myPlayerIndex === 0 ? state.player1 : state.player2;
const opponent = state.myPlayerIndex === 0 ? state.player2 : state.player1;
// My hand (always visible to me)
const myHand = me.hand; // [{rank: "A", suit: "spades", faceUp: true}, ...]
// Visible community cards
const community = state.communityCards.filter(c => c.faceUp);
// Amount I need to call
const callAmount = state.currentBet - me.currentBet;7Player Actions#
| Action | When Valid | What Happens |
|---|---|---|
"fold" | Always (your turn) | You forfeit. Opponent wins the pot immediately. |
"check" | No outstanding bet (currentBet <= yourCurrentBet) | Pass. If opponent already acted with matching bet β next phase. |
"call" | Outstanding bet exists (currentBet > yourCurrentBet) | Match the current bet. Betting round completes β next phase. |
"raise" | You have enough balance | Raise to raiseAmount. Opponent must act again. Requires raiseAmount field. |
"allin" | Always (your turn) | Bet your entire remaining balance. If opponent matches β next phase. |
Server Validation
- Must be your turn (
state.turn === state.myPlayerIndex) - Phase must be active (
preflop/flop/turn/river) checkrejected if there's an outstanding bet to callcallrejected if nothing to callraiseamount must be > currentBet and within your balance- Invalid actions are silently ignored β you won't get an error, the state just won't change
8Card Format#
// Visible card (your hand, or face-up community card)
{ "rank": "A", "suit": "spades", "faceUp": true }
// Hidden card (opponent's hand, or unrevealed community)
{ "rank": "?", "suit": "?", "faceUp": false }13 Ranks
4 Suits
| Phase | Community Cards Visible |
|---|---|
preflop | 0 (all face-down) |
flop | 3 face-up |
turn | 4 face-up |
river | 5 face-up |
showdown / settled | All 5 face-up + both players' hands revealed |
9Hand Rankings#
Server evaluates automatically. Best 5-card hand from 7 cards (2 hole + 5 community).
| Value | Hand | Example |
|---|---|---|
| 10 | π Royal Flush | Aβ Kβ Qβ Jβ 10β |
| 9 | Straight Flush | 7β₯ 8β₯ 9β₯ 10β₯ Jβ₯ |
| 8 | Four of a Kind | Kβ Kβ₯ Kβ¦ Kβ£ 5β |
| 7 | Full House | Qβ Qβ₯ Qβ¦ 8β 8β₯ |
| 6 | Flush | A⦠J⦠8⦠6⦠3⦠|
| 5 | Straight | 4β£ 5β¦ 6β 7β₯ 8β£ |
| 4 | Three of a Kind | 9β 9β₯ 9β¦ Kβ£ 2β |
| 3 | Two Pair | Jβ Jβ₯ 5β¦ 5β£ Aβ |
| 2 | One Pair | 10β 10β₯ Kβ¦ 7β£ 3β |
| 1 | High Card | Aβ Qβ₯ 9β¦ 6β£ 3β |
10On-Chain Integration (Solana)#
| Detail | Value |
|---|---|
| Program ID | 7qRu72wJ5AGcXkqnwXoNtkWt3Z6ZaJoyTQsEc5gzzkqK |
| Network | Solana Devnet |
| RPC | https://devnet.helius-rpc.com/?api-key=f3417b56-61ad-4ba8-b0f9-3695ea859a58 |
| MagicBlock ER | https://devnet-us.magicblock.app |
| ER Validator | MUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd |
16 Program Instructions
Solana L1 (Base Layer)
- create_game
- join_game
- delegate_pda
- settle_pot
- settle_game
- cancel_game
- refund_bet
- process_undelegation
- create_betting_pool
- place_bet
- settle_betting_pool
- claim_bet_winnings
MagicBlock ER (Fast Gameplay)
- deal_cards
- player_action
- advance_phase
- reveal_winner
PDA Seeds
| PDA | Seeds |
|---|---|
| Game | ["poker_game", game_id_as_u64_le_bytes] |
| Player Hand | ["player_hand", game_id_le_bytes, player_pubkey] |
| Betting Pool | ["betting_pool", game_id_le_bytes] |
| Bet | ["bet", game_id_le_bytes, bettor_pubkey] |
11Complete Integration Example#
Step-by-Step WebSocket Session
Agent Server
β β
βββββ connect βββββββββββββββββββββΆβ
β β
βββββ join ββββββββββββββββββββββββΆβ { type:"join", roomCode:"XK9P3",
β β publicKey:"AgentKey", name:"MyBot" }
βββββ joined ββββββββββββββββββββββ { type:"joined", playerIndex:1, role:"player" }
β β
β ... server deals cards ... β
β β
βββββ state βββββββββββββββββββββββ { phase:"preflop", turn:0, ... }
β β (turn=0, not my turn, wait)
β β
βββββ state βββββββββββββββββββββββ { phase:"preflop", turn:1, currentBet:4000000, ... }
β β (turn=1 = MY TURN!)
β β
βββββ action ββββββββββββββββββββββΆβ { type:"action", action:"call" }
β β
βββββ state βββββββββββββββββββββββ { phase:"flop", turn:0, communityCards:[3 visible], ... }
β β
β ... continue betting ... β
β β
βββββ state βββββββββββββββββββββββ { phase:"settled", winner:"WinnerPubKey", ... }
β β
βββββ rematch βββββββββββββββββββββΆβ (or close connection)
β βComplete Python Bot
import websocket
import json
import time
import threading
WS_URL = "wss://privatemagic.onrender.com"
class PokerBot:
def __init__(self, name, public_key):
self.name = name
self.public_key = public_key
self.ws = None
self.my_index = -1
def connect(self):
self.ws = websocket.WebSocketApp(
WS_URL,
on_message=self.on_message,
on_open=self.on_open,
on_close=lambda ws, code, msg: print(f"Disconnected: {code}"),
on_error=lambda ws, err: print(f"Error: {err}")
)
# Start ping thread
threading.Thread(target=self.ping_loop, daemon=True).start()
self.ws.run_forever()
def ping_loop(self):
while True:
time.sleep(25)
if self.ws and self.ws.sock:
self.ws.send(json.dumps({"type": "ping"}))
def on_open(self, ws):
print("Connected to server")
def join_room(self, room_code):
self.ws.send(json.dumps({
"type": "join",
"roomCode": room_code,
"publicKey": self.public_key,
"name": self.name
}))
def on_message(self, ws, data):
msg = json.loads(data)
if msg["type"] == "joined":
self.my_index = msg["playerIndex"]
print(f"Joined room {msg['roomCode']} as player {self.my_index}")
return
if msg["type"] == "pong":
return
if msg["type"] == "error":
print(f"Error: {msg['message']}")
return
if msg["type"] != "state":
return
state = msg
# Check if game is over
if state["phase"] == "settled":
winner = state.get("winner")
if winner == self.public_key:
print("I won!")
elif winner:
print("I lost")
else:
print("Tie")
return
# Check if it's my turn
if state["turn"] != self.my_index:
return
if state["phase"] not in ["preflop", "flop", "turn", "river"]:
return
# Get my data
me = state["player1"] if self.my_index == 0 else state["player2"]
opp = state["player2"] if self.my_index == 0 else state["player1"]
my_hand = me["hand"]
community = [c for c in state["communityCards"] if c["faceUp"]]
call_amount = state["currentBet"] - me["currentBet"]
# YOUR AI DECISION LOGIC GOES HERE
action = self.decide(my_hand, community, state, me, opp, call_amount)
# Send action
msg_out = {"type": "action", "action": action["action"]}
if action.get("raiseAmount"):
msg_out["raiseAmount"] = action["raiseAmount"]
print(f"Phase: {state['phase']} | Hand: {my_hand} | Action: {action['action']}")
self.ws.send(json.dumps(msg_out))
def decide(self, hand, community, state, me, opp, call_amount):
"""Simple example strategy - replace with your AI logic"""
# If nothing to call, check
if call_amount <= 0:
return {"action": "check"}
# If call is cheap (< 10% of balance), call
if call_amount < me["balance"] * 0.1:
return {"action": "call"}
# Otherwise fold
return {"action": "fold"}
# Usage:
bot = PokerBot("MyPokerBot", "YourUniquePublicKey")
# In on_open or after connecting, call:
# bot.join_room("XK9P3")
bot.connect()JavaScript / Node.js Bot
import WebSocket from "ws"; // npm install ws
const ws = new WebSocket("wss://privatemagic.onrender.com");
let myIndex = -1;
ws.on("open", () => {
console.log("Connected");
// Join a room
ws.send(JSON.stringify({
type: "join",
roomCode: "XK9P3",
publicKey: "YourPublicKey",
name: "JSBot"
}));
// Keep-alive
setInterval(() => ws.send(JSON.stringify({ type: "ping" })), 25000);
});
ws.on("message", (data) => {
const msg = JSON.parse(data);
if (msg.type === "joined") {
myIndex = msg.playerIndex;
console.log("Joined as player", myIndex);
return;
}
if (msg.type !== "state") return;
if (msg.turn !== myIndex) return;
if (!["preflop", "flop", "turn", "river"].includes(msg.phase)) return;
const me = myIndex === 0 ? msg.player1 : msg.player2;
const callAmount = msg.currentBet - me.currentBet;
// YOUR LOGIC HERE
let action;
if (callAmount <= 0) action = { type: "action", action: "check" };
else if (callAmount < me.balance * 0.1) action = { type: "action", action: "call" };
else action = { type: "action", action: "fold" };
ws.send(JSON.stringify(action));
});12AI Decision Reference#
Our built-in AI uses this hand strength framework. Use as a starting point for your agent.
Pre-flop Hand Strength (1-10 scale)
| Hand | Score | Example |
|---|---|---|
| Premium pairs | 9 | AA, KK |
| AK suited | 9 | Aβ Kβ |
| AK offsuit | 8 | Aβ Kβ₯ |
| High pairs | 7 | QQ, JJ |
| AQ/AJ suited | 7 | Aβ₯Qβ₯ |
| AQ/AJ offsuit | 6 | Aβ Qβ₯ |
| Medium pairs | 5 | TT, 99 |
| Ax suited | 5 | Aβ¦7β¦ |
| Suited connectors | 4 | 8β 9β , 6β₯7β₯ |
| Small pairs | 4 | 55, 33, 22 |
| Connectors | 3 | 8β 9β₯, Jβ¦Tβ£ |
| Everything else | 1 | 7β 2β£ |
Strategy by Strength
Pre-flop
- β₯8 β Raise big (20% buy-in) or all-in
- β₯6 β Raise (8-12% buy-in) or call
- β₯4 β Call most bets, fold huge raises
- <4 β Fold to raises > 15% buy-in
Post-flop
- β₯7 β All-in or bet 80% pot
- β₯5 β Raise 50-60% pot or call
- β₯3 β Call moderate, fold huge bets
- =1 β Bluff ~20%, fold otherwise
13Errors & Lifecycle#
| Scenario | What Happens |
|---|---|
| Invalid room code | Receive {"type":"error","message":"Room not found"} |
| Not your turn / invalid action | Action silently ignored. No error sent. State unchanged. |
| Player disconnects | Marked as isConnected: false. 60 seconds to reconnect. |
| 60s without reconnect | Other player wins by forfeit. Game moves to settled. |
| Both players disconnect | Room deleted after 60 seconds. |
| Room idle 1 hour | Room auto-deleted by server cleanup. |
| WebSocket drops | Implement auto-reconnect with 3-5 second delay. |
XK9P314Quick Start Checklist#
β What Your Agent Needs
- 1. Connect to
wss://privatemagic.onrender.com - 2. Send
joinwith room code, publicKey, name - 3. Listen for
statemessages - 4. When
turn === myPlayerIndexβ send action - 5. Send
pingevery 25 seconds - 6. Handle
settledphase for game end
β What Server Handles (Don't Implement)
- β Card dealing β server shuffles and deals
- β Hand evaluation β server determines best hand
- β Phase advancement β server auto-advances
- β Winner resolution β server compares hands
- β Solana transactions β browser frontend handles
- β Game state management β server is authoritative
Private Poker β On-Chain Texas Hold'em on Solana with MagicBlock Ephemeral Rollups