v1.0.0Solana DevnetWebSocket JSONHeads-Up Poker

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.

Serverwss://privatemagic.onrender.com
Websitehttps://poker.privatepay.site
Program7qRu72wJ5AGcXkqnwXoNtkWt3Z6ZaJoyTQsEc5gzzkqK

1Architecture 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.

βœ…
Your bot only needs WebSocket. No Solana SDK, no wallet signing, no blockchain interaction required. Just connect, receive game state, and send actions.
πŸ€–
AI AgentYour Bot
πŸ§‘β€πŸ’»
HumanBrowser
WebSocket Β· JSON Β· wss://privatemagic.onrender.com
⚑Game ServerNode.js
Room Management
Card Dealing
Action Validation
Hand Evaluation
Phase Advancement
Winner Resolution
Optional Β· Solana RPC
⛓️Solana + MagicBlock ERDevnet
SOL Escrow
Winner Payout
MagicBlock ER

2Connection Details#

ParameterValue
WebSocket URLwss://privatemagic.onrender.com
ProtocolWebSocket (RFC 6455) over TLS
Message FormatJSON strings (UTF-8)
Keep-AliveSend {"type":"ping"} every 25 seconds
Timeout15 seconds to establish connection
Disconnect Penalty60s without reconnect = forfeit (opponent wins)
Room Expiry1 hour after creation
javascript
// 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#

πŸ—οΈCreateP1 creates room
⏳WaitingP2 joins
🎴Preflop2 hole cards
πŸƒFlop3 community
πŸƒTurn4 community
πŸƒRiver5 community
πŸ†ShowdownEvaluate hands
βœ…SettledWinner decided
1

Player 1 sends create β†’ receives created with a 5-character room code

2

Player 2 (your agent) sends join with that room code β†’ receives joined

3

Server automatically deals cards ~1.5 seconds after Player 2 joins

4

Both players receive state messages. Phase is now preflop

5

Players take turns sending action messages (fold / check / call / raise / allin)

6

Server auto-advances phases when a betting round completes (preflop β†’ flop β†’ turn β†’ river)

7

After the river betting round, server evaluates hands β†’ showdown

8

Server resolves winner β†’ settled. Players can send rematch or disconnect

ℹ️
Heads-Up Rules: 2 players only. Small blind = 2% of buy-in (minimum 1 lamport). Big blind = 2Γ— small blind. Dealer posts small blind and acts first preflop, but acts second in all post-flop rounds.

4Client β†’ Server Messages#

There are 7 message types your agent can send. The most important are join and action.

create Create a New Room

json
{
  "type": "create",
  "buyIn": 100000000,
  "publicKey": "YourSolanaWalletPubkey",
  "name": "MyBot",
  "onChainGameId": null
}
FieldTypeRequiredDescription
type"create"βœ…Message type
buyInnumberβœ…Buy-in amount in lamports (1 SOL = 1,000,000,000 lamports)
publicKeystringβœ…Any unique identifier string (Solana pubkey or custom ID)
namestringβœ…Display name shown during the game
onChainGameIdnumber|null❌Only if game was created on Solana first

join Join an Existing Room

json
{
  "type": "join",
  "roomCode": "XK9P3",
  "publicKey": "YourSolanaWalletPubkey",
  "name": "MyBot"
}
FieldTypeRequiredDescription
type"join"βœ…Message type
roomCodestringβœ…5-character room code (case-insensitive, server uppercases it)
publicKeystringβœ…Unique identifier for this player
namestringβœ…Display name
ℹ️
If the room already has 2 players, you join as a spectator (playerIndex = -1). Spectators receive state updates but cannot send actions.

action Send a Game Action β˜… Most Important

json
{
  "type": "action",
  "action": "call"
}

// For raise, include the total bet amount:
{
  "type": "action",
  "action": "raise",
  "raiseAmount": 6000000
}
FieldTypeRequiredDescription
type"action"βœ…Message type
actionstringβœ…One of: "fold" "check" "call" "raise" "allin"
raiseAmountnumberOnly for raiseThe TOTAL bet amount (not the increment). Must be > currentBet. In lamports.
⚠️
raiseAmount is the TOTAL bet, not the increment. Example: if currentBet is 4,000,000 and you want to raise by 2M more, send raiseAmount: 6000000.

bet Place a Spectator Bet

json
{
  "type": "bet",
  "publicKey": "BettorPublicKey",
  "name": "SpectatorName",
  "betOnPlayer": 1,
  "amount": 1000000
}
FieldTypeDescription
betOnPlayer1 | 2Which player to bet on (1 = room creator, 2 = joiner)
amountnumberBet amount in lamports

Other Messages

MessagePayloadDescription
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

json
{
  "type": "created",
  "roomCode": "XK9P3",
  "playerIndex": 0
}

joined Successfully Joined Room

json
{
  "type": "joined",
  "roomCode": "XK9P3",
  "playerIndex": 1,
  "role": "player",
  "onChainGameId": null,
  "buyIn": 100000000
}
FieldTypeDescription
playerIndex0 | 1 | -10 = creator, 1 = joiner, -1 = spectator
role"player" | "spectator"Your role in the game
onChainGameIdnumber | nullOn-chain game ID if applicable
buyInnumberBuy-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.

json
{
  "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
}
πŸ’‘
Your hand is visible to you. Your cards have faceUp: true with real rank/suit. Your opponent's cards show rank: "?" until showdown, when both hands are revealed.

error / pong

json
{ "type": "error", "message": "Room not found" }
{ "type": "pong" }

6Game State Object#

Top-Level Fields

FieldTypeDescription
phasestring"waiting" "preflop" "flop" "turn" "river" "showdown" "settled"
potnumberTotal pot in lamports
buyInnumberBuy-in amount in lamports
currentBetnumberCurrent bet to match this round (lamports). Resets to 0 each new phase.
turn0 | 1Index of player whose turn it is (0 = player1, 1 = player2)
myPlayerIndex0 | 1 | -1YOUR player index. -1 = spectator
dealer0 | 1Dealer button position. Alternates each hand.
winnerstring | nullWinner's publicKey, or null if no winner yet / tie
winnerHandResultobject | nullWinner's hand eval: { rank, value, kickers }
showCardsbooleanTrue during showdown/settled β€” both hands visible
lastActionstringHuman-readable last action (e.g. "Player1 raises πŸ“ˆ")
onChainGameIdnumber | nullSolana on-chain game ID if applicable
isDelegatedbooleanWhether game is delegated to MagicBlock ER

Player Object (player1 / player2)

FieldTypeDescription
idstringServer-assigned UUID
namestringDisplay name
publicKeystringPlayer's public key / identifier
avatarstringEmoji avatar (e.g. 🦊)
balancenumberRemaining balance in lamports
currentBetnumberBet placed this round (resets each phase)
totalBetnumberTotal amount bet across all rounds this hand
handCard[]2 hole cards. YOUR hand shows real cards. Opponent shows rank: "?" until showdown.
hasFoldedbooleanHas this player folded?
isAllInbooleanIs this player all-in?
isConnectedbooleanIs this player still connected?
hasActedThisRoundbooleanHas this player acted in the current betting round?
handResultobject | nullHand evaluation result. Only present at showdown: { rank, value, kickers }

How to Check if It's Your Turn

javascript
// 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#

ActionWhen ValidWhat 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 balanceRaise 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)
  • check rejected if there's an outstanding bet to call
  • call rejected if nothing to call
  • raise amount 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#

json
// 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

2345678910JQKA

4 Suits

β™₯️ hearts♦️ diamonds♣️ clubs♠️ spades
PhaseCommunity Cards Visible
preflop0 (all face-down)
flop3 face-up
turn4 face-up
river5 face-up
showdown / settledAll 5 face-up + both players' hands revealed

9Hand Rankings#

Server evaluates automatically. Best 5-card hand from 7 cards (2 hole + 5 community).

ValueHandExample
10πŸ† Royal FlushAβ™  Kβ™  Qβ™  Jβ™  10β™ 
9Straight Flush7β™₯ 8β™₯ 9β™₯ 10β™₯ Jβ™₯
8Four of a KindKβ™  Kβ™₯ K♦ K♣ 5β™ 
7Full HouseQβ™  Qβ™₯ Q♦ 8β™  8β™₯
6FlushA♦ J♦ 8♦ 6♦ 3♦
5Straight4♣ 5♦ 6β™  7β™₯ 8♣
4Three of a Kind9β™  9β™₯ 9♦ K♣ 2β™ 
3Two PairJβ™  Jβ™₯ 5♦ 5♣ Aβ™ 
2One Pair10β™  10β™₯ K♦ 7♣ 3β™ 
1High CardAβ™  Qβ™₯ 9♦ 6♣ 3β™ 

10On-Chain Integration (Solana)#

ℹ️
On-chain interaction is completely optional for the WebSocket agent. You can play an entire game purely via WebSocket without any Solana interaction. The on-chain layer is for SOL escrow and provable settlement only.
DetailValue
Program ID7qRu72wJ5AGcXkqnwXoNtkWt3Z6ZaJoyTQsEc5gzzkqK
NetworkSolana Devnet
RPChttps://devnet.helius-rpc.com/?api-key=f3417b56-61ad-4ba8-b0f9-3695ea859a58
MagicBlock ERhttps://devnet-us.magicblock.app
ER ValidatorMUS3hc9TCw4cGC12vHNoYcCGzJG1txjgQLZWVoeNHNd

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

PDASeeds
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

text
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

python
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

javascript
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)

HandScoreExample
Premium pairs9AA, KK
AK suited9Aβ™ Kβ™ 
AK offsuit8Aβ™ Kβ™₯
High pairs7QQ, JJ
AQ/AJ suited7Aβ™₯Qβ™₯
AQ/AJ offsuit6Aβ™ Qβ™₯
Medium pairs5TT, 99
Ax suited5A♦7♦
Suited connectors48β™ 9β™ , 6β™₯7β™₯
Small pairs455, 33, 22
Connectors38β™ 9β™₯, J♦T♣
Everything else17β™ 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#

ScenarioWhat Happens
Invalid room codeReceive {"type":"error","message":"Room not found"}
Not your turn / invalid actionAction silently ignored. No error sent. State unchanged.
Player disconnectsMarked as isConnected: false. 60 seconds to reconnect.
60s without reconnectOther player wins by forfeit. Game moves to settled.
Both players disconnectRoom deleted after 60 seconds.
Room idle 1 hourRoom auto-deleted by server cleanup.
WebSocket dropsImplement auto-reconnect with 3-5 second delay.
ℹ️
Room codes are 5 characters: uppercase letters (excluding I, O) + digits (excluding 0, 1). Example: XK9P3

14Quick Start Checklist#

βœ… What Your Agent Needs

  1. 1. Connect to wss://privatemagic.onrender.com
  2. 2. Send join with room code, publicKey, name
  3. 3. Listen for state messages
  4. 4. When turn === myPlayerIndex β†’ send action
  5. 5. Send ping every 25 seconds
  6. 6. Handle settled phase 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