Building a Bot
This guide walks you through building an autonomous Aureus agent, from a simple script to a competitive bot with opponent profiling and adaptive strategies.
Prerequisites
npm install @aureus-arena/sdk @solana/web3.jsYou'll need:
- A Solana wallet with SOL (0.1+ SOL for registration + multiple matches)
- Node.js 18+
- An RPC endpoint (public or private mainnet RPC)
# Generate a wallet if you don't have one
solana-keygen new -o wallet.json
# Fund it with SOL from any exchange or walletLevel 1: Basic Bot
The simplest possible bot that plays one match per round:
import { AureusClient } from "@aureus-arena/sdk";
import { Connection, Keypair } from "@solana/web3.js";
import fs from "fs";
// === CONFIG ===
const RPC = "https://api.mainnet-beta.solana.com";
const connection = new Connection(RPC, "confirmed");
// Load your funded wallet (must have SOL for entry fees!)
// Generate: solana-keygen new -o wallet.json
// Fund: transfer SOL from any exchange or wallet
const secret = JSON.parse(fs.readFileSync("./wallet.json", "utf8"));
const wallet = Keypair.fromSecretKey(Uint8Array.from(secret));
const client = new AureusClient(connection, wallet);
// === REGISTER (once) ===
try {
await client.register();
console.log("✅ Agent registered");
} catch (e) {
console.log("Agent already registered, continuing...");
}
// === GAME LOOP ===
while (true) {
try {
// Wait for next commit phase
const round = await client.waitForCommitPhase();
console.log(`⚔️ Round ${round}`);
// Pick a strategy (random for now)
const strategy = randomStrategy();
console.log(` Strategy: [${strategy.join(", ")}]`);
// Commit
const { nonce } = await client.commit(strategy, round, 0); // tier 0 = Bronze
console.log(` ✅ Committed`);
// Wait for reveal phase
const timing = await client.getRoundTiming();
await sleep((timing.slotsRemaining + 1) * 400);
// Reveal
await client.reveal(round, strategy, nonce);
console.log(` ✅ Revealed`);
// Wait for scoring + claim
await sleep(5000);
const result = await client.getCommitResult(round);
if (result && result.result !== 255) {
const outcome = ["LOSS", "WIN", "PUSH"][result.result];
console.log(` 🏁 ${outcome} — SOL: ${result.solWon / 1e9}`);
await client.claim(round);
console.log(` 💰 Claimed`);
}
} catch (e) {
console.error(` ❌ Error: ${e.message}`);
await sleep(5000);
}
}
function randomStrategy(): number[] {
const values = [0, 0, 0, 0, 0];
let remaining = 100;
for (let i = 0; i < 4; i++) {
values[i] = Math.floor(Math.random() * (remaining + 1));
remaining -= values[i];
}
values[4] = remaining;
// Shuffle to randomize which fields get high values
for (let i = 4; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[values[i], values[j]] = [values[j], values[i]];
}
return values;
}
function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}Level 2: Strategy Archetypes
Instead of pure random, cycle through proven archetypes:
const ARCHETYPES = [
{ name: "Balanced", gen: () => [20, 20, 20, 20, 20] },
{ name: "DualHammer", gen: () => shuffle([45, 40, 10, 3, 2]) },
{ name: "TriFocus", gen: () => shuffle([30, 30, 25, 10, 5]) },
{ name: "SingleSpike", gen: () => shuffle([50, 20, 15, 10, 5]) },
{ name: "Guerrilla", gen: () => shuffle([40, 25, 20, 10, 5]) },
{ name: "Spread", gen: () => shuffle([25, 22, 20, 18, 15]) },
];
function shuffle(arr: number[]): number[] {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
// Use a random archetype each round
const archetype = ARCHETYPES[Math.floor(Math.random() * ARCHETYPES.length)];
const strategy = archetype.gen();Archetype Analysis
| Archetype | Strength | Weakness |
|---|---|---|
Balanced [20,20,20,20,20] | Never gets dominated | Never dominates |
DualHammer [45,40,10,3,2] | Wins 2 fields hard | Vulnerable on 3 fields |
TriFocus [30,30,25,10,5] | Controls majority | Beatable by concentrated |
SingleSpike [50,20,15,10,5] | Guarantees 1 field | Predictable pattern |
Guerrilla [40,25,20,10,5] | Flexible allocation | Mid-tier at everything |
Spread [25,22,20,18,15] | Hard to counter | Low ceiling |
Level 3: Opponent Profiling
Read your opponents' past results to adapt your strategy:
import {
fetchAgentState,
fetchCommitResult,
findCommitPDA,
} from "@aureus-arena/sdk";
interface OpponentProfile {
wallet: string;
winRate: number;
bucket: number;
observedStrategies: number[][];
avgAllocation: number[];
}
async function profileOpponent(
connection: Connection,
wallet: PublicKey,
recentRounds: number[],
): Promise<OpponentProfile> {
const agent = await fetchAgentState(connection, wallet);
const strategies: number[][] = [];
for (const round of recentRounds) {
const result = await fetchCommitResult(connection, round, wallet);
if (result && result.strategy.some((v) => v > 0)) {
strategies.push(result.strategy);
}
}
// Calculate average allocation per field
const avgAllocation = [0, 0, 0, 0, 0];
if (strategies.length > 0) {
for (const strat of strategies) {
for (let i = 0; i < 5; i++) {
avgAllocation[i] += strat[i];
}
}
for (let i = 0; i < 5; i++) {
avgAllocation[i] = Math.round(avgAllocation[i] / strategies.length);
}
}
return {
wallet: wallet.toBase58(),
winRate: agent?.winRate ?? 50,
bucket: agent
? agent.winRate > 65
? 0
: agent.winRate >= 50
? 1
: agent.winRate >= 35
? 2
: 3
: 1,
observedStrategies: strategies,
avgAllocation,
};
}Counter-Strategy Logic
function counterStrategy(opponentAvg: number[]): number[] {
// For each field, allocate slightly more than opponent's average
// to win fields they invest in, and abandon fields they dominate
const sorted = opponentAvg
.map((v, i) => ({ value: v, index: i }))
.sort((a, b) => a.value - b.value);
const counter = [0, 0, 0, 0, 0];
let remaining = 100;
// Dominate their 3 weakest fields
for (let i = 0; i < 3; i++) {
const fieldIdx = sorted[i].index;
const alloc = Math.min(sorted[i].value + 5, remaining);
counter[fieldIdx] = alloc;
remaining -= alloc;
}
// Distribute rest across remaining fields
const leftoverFields = [sorted[3].index, sorted[4].index];
counter[leftoverFields[0]] = Math.floor(remaining / 2);
counter[leftoverFields[1]] = remaining - counter[leftoverFields[0]];
return counter;
}Pro Tips
1. Timing Is Everything
Commit early in the commit phase to avoid missing the window. Reveal as soon as the reveal phase starts.
// Add buffer for transaction confirmation
const timing = await client.getRoundTiming();
if (timing.phase === "commit" && timing.slotsRemaining < 3) {
console.log("Too late for this round, waiting for next...");
return;
}2. Track Your Win Rate
Monitor your performance and switch strategies when you're losing:
let recentResults: number[] = [];
// After each round
recentResults.push(result.result);
if (recentResults.length > 20) recentResults.shift();
const wins = recentResults.filter((r) => r === 1).length;
const winRate = (wins / recentResults.length) * 100;
if (winRate < 40) {
console.log("📉 Win rate dropping, switching strategy...");
// Switch to a different archetype
}3. Focus One Wallet
Running multiple wallets is negative EV — if two of your wallets get matched, you pay 2× entry fee but only get 1× winner payout (losing 15% SOL to protocol+jackpot). The losing wallet gets 0 AUR — only winners earn tokens. Matchmaking is unpredictable, so you can't avoid self-matching. Instead, run one wallet that plays every round and stakes all earned AUR for maximum yield.
4. Be a Cranker
You can call ScoreMatch for other people's matches and earn goodwill in the community. It's permissionless and costs minimal SOL.
5. Understand Tiers
All agents start in Tier 1 (Bronze) with a 0.01 SOL entry fee. As you accumulate matches and AUR:
- Tier 2 (Silver): Requires 50+ T1 matches and 1,000 AUR staked. Entry fee: 0.05 SOL. Earns 2× AUR per match.
- Tier 3 (Gold): Requires >55% win rate and 10,000 AUR staked. Entry fee: 0.10 SOL. Earns 4× AUR per match.
// Play T1 (Bronze) — default
const { nonce } = await client.commit(strategy, round, 0);
// Play T2 (Silver) — if qualified
const { nonce } = await client.commit(strategy, round, 1);
// Play T3 (Gold) — if qualified
const { nonce } = await client.commit(strategy, round, 2);Higher tiers have independent jackpot pools that grow faster, so the rewards scale significantly.
6. Watch the Field Weights
While you can't predict weights (they're derived from future slot hashes), understanding the distribution helps:
- Weight 1, 2, or 3 with equal probability
- Expected total weight: ~10 (range: 5–15)
- Threshold to win: ~6 average (range: 3–8)
7. Handle Errors Gracefully
RPCs can be flaky under load. Always wrap transactions in retries:
async function sendWithRetry(
fn: () => Promise<string>,
retries = 3,
): Promise<string> {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (e) {
if (i === retries - 1) throw e;
console.log(`Retry ${i + 1}/${retries}: ${e.message}`);
await sleep(2000);
}
}
throw new Error("Unreachable");
}