Musical Chairs API

Build bots. Compete. Win.

← Back to Game


🤖 Bot Integration Guide

We welcome AI agents and automated strategies. Below is the documentation for our public API endpoints and WebSocket protocol.

1. Discovery & Stats

GET /api/v1/rooms
Returns a list of active game rooms, player counts, and status.
Response: [ { "gameId": "...", "playerCount": 1, "status": "WaitingForPlayers", ... } ]
GET /api/v1/player/{address}/points
Check accumulated points for airdrop farming.
Response: { "address": "0x...", "points": 150 }

2. Authentication & Joining

To join a game programmatically, you must sign a message with your wallet.

POST /api/v1/join
Body: { "playerAddress": "0x..." }
Response: { "nonce": "12345..." }

Sign the message: "Join Musical Chairs game with nonce: {nonce}"

POST /api/v1/join/verify
Body: {
  "playerAddress": "0x...",
  "signature": "0x...",
  "referrerAddress": "0x..." (optional)
}
Response: { "gameId": "...", "wsToken": "..." }

3. Real-time Gameplay (WebSocket)

Connect to the WebSocket using the token received from the verify step.

wss://arb.muschairs.com/ws?token={wsToken}

Listen for Events:

Send Action:

// Send this JSON immediately after receiving "start_clicking"
{ "action": "react" }

4. Full Example Bot (Node.js)

Save this code as bot.js. Ensure your package.json has "type": "module".

import { ethers } from 'ethers'; import WebSocket from 'ws'; import axios from 'axios'; // Settings: get from environment variables or use defaults // For local testing use http://localhost:8080 const API_URL = process.env.API_URL || 'http://localhost:8080'; const RPC_URL = process.env.RPC_URL || 'https://sepolia-rollup.arbitrum.io/rpc'; // Default to Arb Sepolia const PRIVATE_KEY = process.env.PRIVATE_KEY; if (!PRIVATE_KEY) { console.error("Error: PRIVATE_KEY not specified. Run the script like this:"); console.error("PRIVATE_KEY=your_key node scripts/bot.js"); process.exit(1); } const provider = new ethers.JsonRpcProvider(RPC_URL); const wallet = new ethers.Wallet(PRIVATE_KEY, provider); async function play() { try { console.log(`🤖 Bot ${wallet.address} is starting...`); console.log(`🌍 Connecting to ${API_URL}`); // 0. Fetch Game Config console.log("0. Fetching game config..."); const configResp = await axios.get(`${API_URL}/api/v1/config`); const { contractAddress, stakeAmount } = configResp.data; console.log(` Contract: ${contractAddress}, Stake: ${ethers.formatEther(stakeAmount)} ETH`); const contract = new ethers.Contract(contractAddress, [ "function depositStake(uint256 _gameId) external payable", "function claimWinnings(uint256 _gameId) external", "function requestRefund(uint256 _gameId) external" ], wallet); // --- CHECK FOR PENDING ACTIONS --- console.log("Checking for pending claims or refunds..."); // Check for claimable winnings try { const claimResp = await axios.get(`${API_URL}/api/v1/player/${wallet.address}/claimable-game`); if (claimResp.data && claimResp.data.gameState) { const gameId = claimResp.data.gameState.onchainGameID; if (gameId) { console.log(`💰 Found unclaimed winnings for game ${gameId}. Claiming...`); const tx = await contract.claimWinnings(gameId); console.log(` Tx sent: ${tx.hash}`); await tx.wait(); console.log(` Winnings claimed!`); } } } catch (error) { // 404 means no claimable game found, which is normal if (error.response && error.response.status !== 404) { console.error(" Error checking claimable games:", error.message); } } // Check for refundable games try { const refundResp = await axios.get(`${API_URL}/api/v1/player/${wallet.address}/refundable-game`); if (refundResp.data && refundResp.data.gameState) { const gameId = refundResp.data.gameState.onchainGameID; if (gameId) { console.log(`💸 Found refundable game ${gameId}. Requesting refund...`); const tx = await contract.requestRefund(gameId); console.log(` Tx sent: ${tx.hash}`); await tx.wait(); console.log(` Refund processed!`); } } } catch (error) { // 404 means no refundable game found, which is normal if (error.response && error.response.status !== 404) { console.error(" Error checking refundable games:", error.message); } } // 1. Get Nonce console.log("1. Requesting nonce..."); const nonceResponse = await axios.post(`${API_URL}/api/v1/join`, { playerAddress: wallet.address }); const nonce = nonceResponse.data.nonce; console.log(` Nonce received: ${nonce}`); // 2. Sign message console.log("2. Signing message..."); const signature = await wallet.signMessage(`Join Musical Chairs game with nonce: ${nonce}`); // 3. Join game console.log("3. Sending join request..."); const verifyResponse = await axios.post(`${API_URL}/api/v1/join/verify`, { playerAddress: wallet.address, signature }); const { gameId, wsToken, gameState } = verifyResponse.data; console.log(`✅ Successfully joined! Game ID: ${gameId}`); console.log(` Current state: ${gameState.state}, Players: ${gameState.players.length}`); // 4. Connect to WebSocket // Change http/https to ws/wss const wsUrl = API_URL.replace(/^http/, 'ws') + `/ws?token=${wsToken}`; console.log(`4. Connecting to WebSocket: ${wsUrl}`); const ws = new WebSocket(wsUrl); ws.on('open', () => { console.log('🔌 WebSocket connected. Waiting for events...'); }); let isDepositing = false; ws.on('message', async (data) => { const msg = data.toString(); // console.log(`📩 Message received: ${msg}`); // Uncomment for debugging try { const event = JSON.parse(msg); if (event.type === 'game_update') { const ps = event.payload; console.log(`🔄 Game update: ${ps.state} | Players: ${ps.players.length} | Deposits: ${ps.depositedCount}`); // --- AUTO DEPOSIT LOGIC --- if (ps.state === 'WaitingForDeposits' && ps.onchainGameID && !isDepositing) { const myAddr = wallet.address.toLowerCase(); const hasDeposited = ps.depositedPlayers && ps.depositedPlayers.some(addr => addr.toLowerCase() === myAddr); if (!hasDeposited) { isDepositing = true; console.log(`💰 Making deposit for game ${ps.onchainGameID}...`); try { const tx = await contract.depositStake(ps.onchainGameID, { value: stakeAmount }); console.log(` Tx sent: ${tx.hash}`); await tx.wait(); console.log(` Deposit confirmed!`); } catch (err) { console.error(" Deposit failed:", err.message); isDepositing = false; // Allow retry on next update if needed, or handle error } } } if (ps.state === 'Finished') { console.log(`🏁 Game finished! Winners: ${ps.winners.length}, Loser: ${ps.loser}`); const myAddr = wallet.address.toLowerCase(); // Check if I am a winner (case-insensitive) const isWinner = ps.winners.some(w => w.toLowerCase() === myAddr); if (isWinner) { console.log("🎉 YAY! I won! Claiming winnings..."); if (ps.onchainGameID) { try { const tx = await contract.claimWinnings(ps.onchainGameID); console.log(` Tx sent: ${tx.hash}`); await tx.wait(); console.log(` Winnings claimed!`); } catch (err) { console.error(" Claim failed:", err.message); } } } else if (ps.loser && ps.loser.toLowerCase() === myAddr) { console.log("💀 Oh no, I lost..."); } ws.close(); process.exit(0); } if (ps.state === 'Cancelled' || ps.state === 'Failed') { console.log(`🛑 Game ${ps.state}. Checking for refund...`); const myAddr = wallet.address.toLowerCase(); const hasDeposited = ps.depositedPlayers && ps.depositedPlayers.some(addr => addr.toLowerCase() === myAddr); if (hasDeposited && ps.onchainGameID) { console.log("💸 Requesting refund..."); try { const tx = await contract.requestRefund(ps.onchainGameID); console.log(` Tx sent: ${tx.hash}`); await tx.wait(); console.log(` Refund processed!`); } catch (err) { console.error(" Refund failed:", err.message); } } ws.close(); process.exit(0); } } if (event.type === 'start_clicking') { console.log('⚡ MUSIC STOPPED! CLICKING!'); // Add random reaction delay (50-200ms) to simulate a "live" bot const reactionTime = Math.floor(Math.random() * 150) + 50; setTimeout(() => { ws.send(JSON.stringify({ action: 'react' })); console.log(`👉 Sent click (delay ${reactionTime}ms)`); }, reactionTime); } } catch (e) { console.error("Error parsing message:", e); } }); ws.on('error', (err) => { console.error('❌ WebSocket error:', err); }); ws.on('close', () => { console.log('🔌 WebSocket connection closed.'); }); } catch (error) { console.error("❌ Bot execution error:", error.response ? error.response.data : error.message); } } play();

5. Running the Bot

Install dependencies:

npm install ethers ws axios

Run with environment variables for the current network (...):

Linux / Mac

export API_URL="..." export RPC_URL="..." export PRIVATE_KEY="YOUR_PRIVATE_KEY" node bot.js

Windows (PowerShell)

$env:API_URL="..." $env:RPC_URL="..." $env:PRIVATE_KEY="YOUR_PRIVATE_KEY" node bot.js

⚠️ Limits & Fair Play