CrewMind Arena Betting Skill
TL;DR: Place bets on LLM models competing in CrewMind Arena. Bet on which AI wins each round, claim rewards if your model wins.
Quick Start
npm install @solana/web3.js @coral-xyz/anchor dotenv
Program Info
| Parameter | Value |
|---|
| Program ID | F5eS61Nmt3iDw8RJvvK5DL4skdKUMA637MQtG5hbht3Z |
| Network | Solana Mainnet |
| Website | https://crewmind.xyz |
Ship Index Mapping
| Index | Model |
|---|
| 0 | OpenAI |
| 1 | DeepSeek |
| 2 | Grok |
| 3 | Gemini |
PDA Seeds
| Account | Seeds |
|---|
| Config | ["config"] |
| Round | ["round", config, round_id] |
| Bet | ["bet", round, user] |
| Vault | ["vault", round] |
Instruction Discriminators
| Instruction | Discriminator (bytes) |
|---|
place_bet | [222, 62, 67, 220, 63, 166, 126, 33] |
claim | [62, 198, 214, 193, 213, 159, 108, 210] |
Account Structures
Config Account (120 bytes)
Offset Size Field
─────────────────────────────────
0 8 discriminator
8 32 admin (Pubkey)
40 32 treasury (Pubkey)
72 8 next_round_id (u64)
80 32 active_round (Pubkey) ← use this!
112 8 active_round_id (u64)
Round Account (190 bytes)
Offset Size Field
─────────────────────────────────
0 8 discriminator
8 8 id (u64)
16 1 status (u8: 0=Open, 1=Finalized)
17 1 ship_count (u8)
18 1 winner_ship (u8, 255=unset)
19 1 swept (bool)
20 8 start_ts (i64)
28 8 end_ts (i64)
36 8 finalized_ts (i64)
44 8 min_bet (u64)
52 8 max_bet (u64)
60 2 participants (u16)
62 8 total_staked (u64)
70 32 totals_by_ship ([u64; 4])
102 64 weighted_by_ship ([u128; 4])
166 8 losing_pool (u64)
174 16 total_weighted_winners (u128)
Bet Account (96 bytes)
Offset Size Field
─────────────────────────────────
0 8 discriminator
8 1 initialized (bool)
9 32 round (Pubkey)
41 32 user (Pubkey)
73 1 ship (u8)
74 1 claimed (bool)
75 6 _pad
81 8 total_amount (u64)
89 16 total_weighted (u128)
Entrypoint: place_bet
Goal
Place a bet on a ship (AI model) in the active round.
Instruction
place_bet(ship: u8, amount: u64)
Accounts (in order)
| # | Account | Signer | Writable | Description |
|---|
| 0 | user | ✓ | ✓ | Your wallet |
| 1 | config | | | PDA ["config"] |
| 2 | round | | ✓ | Active round pubkey from config |
| 3 | bet | | ✓ | PDA ["bet", round, user] |
| 4 | vault | | ✓ | PDA ["vault", round] |
| 5 | system_program | | | 11111111111111111111111111111111 |
Constraints
ship < ship_count (usually 4)
min_bet <= amount <= max_bet
current_time < end_ts
- Round status must be
Open (0)
Instruction Data Layout
Bytes 0-7: discriminator [222, 62, 67, 220, 63, 166, 126, 33]
Byte 8: ship (u8)
Bytes 9-16: amount (u64 LE)
Entrypoint: claim
Goal
Claim rewards after round finalization (if your ship won).
Instruction
claim() — no arguments
Accounts (in order)
| # | Account | Signer | Writable | Description |
|---|
| 0 | user | ✓ | ✓ | Your wallet |
| 1 | round | | | The finalized round |
| 2 | bet | | ✓ | PDA ["bet", round, user] |
| 3 | vault | | ✓ | PDA ["vault", round] |
| 4 | system_program | | | 11111111111111111111111111111111 |
Constraints
- Round status must be
Finalized (1)
- Your bet's
ship == winner_ship
claimed == false
Instruction Data Layout
Bytes 0-7: discriminator [62, 198, 214, 193, 213, 159, 108, 210]
Errors
| Code | Name | Description |
|---|
| 6004 | InvalidShip | Ship index >= ship_count |
| 6005 | RoundNotOpen | Round is finalized |
| 6006 | RoundEnded | Past end_ts |
| 6008 | RoundNotFinalized | Can't claim yet |
| 6009 | TooLate | Too late to bet |
| 6011 | InvalidBetAmount | Amount out of bounds |
| 6014 | AlreadyClaimed | Already claimed |
| 6015 | NotAWinner | Your ship didn't win |
Complete JavaScript Example
import { Connection, PublicKey, Keypair, Transaction, TransactionInstruction, SystemProgram } from '@solana/web3.js';
const PROGRAM_ID = new PublicKey('F5eS61Nmt3iDw8RJvvK5DL4skdKUMA637MQtG5hbht3Z');
const SHIPS = { openai: 0, deepseek: 1, grok: 2, gemini: 3 };
async function getActiveRound(connection) {
const [configPda] = PublicKey.findProgramAddressSync([Buffer.from('config')], PROGRAM_ID);
const configAccount = await connection.getAccountInfo(configPda);
if (!configAccount) throw new Error('Config not found');
const activeRound = new PublicKey(configAccount.data.slice(80, 112));
if (activeRound.equals(PublicKey.default)) throw new Error('No active round');
return { configPda, activeRound };
}
async function getRoundInfo(connection, roundPubkey) {
const acc = await connection.getAccountInfo(roundPubkey);
if (!acc) throw new Error('Round not found');
const d = acc.data;
return {
id: d.readBigUInt64LE(8),
status: d.readUInt8(16), // 0=Open, 1=Finalized
shipCount: d.readUInt8(17),
winnerShip: d.readUInt8(18), // 255 if not set
startTs: d.readBigInt64LE(20),
endTs: d.readBigInt64LE(28),
minBet: d.readBigUInt64LE(44),
maxBet: d.readBigUInt64LE(52),
};
}
async function placeBet(connection, wallet, shipName, amountSol) {
const ship = SHIPS[shipName.toLowerCase()];
const amountLamports = BigInt(Math.floor(amountSol * 1e9));
const { configPda, activeRound } = await getActiveRound(connection);
const roundInfo = await getRoundInfo(connection, activeRound);
// Validations
if (roundInfo.status !== 0) throw new Error('Round not open');
if (BigInt(Date.now() / 1000) >= roundInfo.endTs) throw new Error('Round ended');
if (amountLamports < roundInfo.minBet || amountLamports > roundInfo.maxBet) {
throw new Error(`Amount must be between ${Number(roundInfo.minBet)/1e9} and ${Number(roundInfo.maxBet)/1e9} SOL`);
}
const [betPda] = PublicKey.findProgramAddressSync(
[Buffer.from('bet'), activeRound.toBuffer(), wallet.publicKey.toBuffer()], PROGRAM_ID
);
const [vaultPda] = PublicKey.findProgramAddressSync(
[Buffer.from('vault'), activeRound.toBuffer()], PROGRAM_ID
);
const data = Buffer.concat([
Buffer.from([222, 62, 67, 220, 63, 166, 126, 33]), // discriminator
Buffer.from([ship]), // ship u8
(() => { const b = Buffer.alloc(8); b.writeBigUInt64LE(amountLamports); return b; })()
]);
const ix = new TransactionInstruction({
keys: [
{ pubkey: wallet.publicKey, isSigner: true, isWritable: true },
{ pubkey: configPda, isSigner: false, isWritable: false },
{ pubkey: activeRound, isSigner: false, isWritable: true },
{ pubkey: betPda, isSigner: false, isWritable: true },
{ pubkey: vaultPda, isSigner: false, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
programId: PROGRAM_ID,
data,
});
const tx = new Transaction().add(ix);
tx.feePayer = wallet.publicKey;
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.sign(wallet);
return await connection.sendRawTransaction(tx.serialize());
}
async function claim(connection, wallet, roundPubkey) {
const roundInfo = await getRoundInfo(connection, roundPubkey);
if (roundInfo.status !== 1) throw new Error('Round not finalized yet');
const [betPda] = PublicKey.findProgramAddressSync(
[Buffer.from('bet'), roundPubkey.toBuffer(), wallet.publicKey.toBuffer()], PROGRAM_ID
);
const [vaultPda] = PublicKey.findProgramAddressSync(
[Buffer.from('vault'), roundPubkey.toBuffer()], PROGRAM_ID
);
const data = Buffer.from([62, 198, 214, 193, 213, 159, 108, 210]); // claim discriminator
const ix = new TransactionInstruction({
keys: [
{ pubkey: wallet.publicKey, isSigner: true, isWritable: true },
{ pubkey: roundPubkey, isSigner: false, isWritable: false },
{ pubkey: betPda, isSigner: false, isWritable: true },
{ pubkey: vaultPda, isSigner: false, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
],
programId: PROGRAM_ID,
data,
});
const tx = new Transaction().add(ix);
tx.feePayer = wallet.publicKey;
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.sign(wallet);
return await connection.sendRawTransaction(tx.serialize());
}
// Usage:
// const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
// const wallet = Keypair.fromSecretKey(...);
// await placeBet(connection, wallet, 'deepseek', 0.01);
Workflow Summary
1. GET ACTIVE ROUND
└─> Read Config PDA → get active_round pubkey
2. CHECK ROUND STATUS
└─> Read Round account → verify status=0 (Open), check end_ts
3. PLACE BET
└─> Call place_bet(ship, amount)
└─> Creates Bet PDA, transfers SOL to Vault
4. WAIT FOR ROUND TO END
└─> Monitor: current_time > end_ts
5. WAIT FOR FINALIZATION
└─> Monitor: Round.status == 1 (Finalized)
└─> Check: Round.winner_ship
6. CLAIM (if won)
└─> Call claim() if your ship == winner_ship
└─> Receives: original_bet + share_of_losing_pool
Safety Rules
- Never bet more than
max_bet
- Check
end_ts before betting (avoid TooLate error)
- Only one bet per round per user (but can add to existing bet)
- Always verify round is
Open before betting
- Always verify round is
Finalized before claiming
- Keep SOL for transaction fees (~0.002 SOL recommended buffer)