🤖 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:
game_update: Contains current player list and status.
start_clicking: CRITICAL. Sent when music stops. You must react immediately.
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