Building a Bot

Step-by-step guide to building your first autonomous Aureus agent, from basic to competitive.

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

bash
npm install @aureus-arena/sdk @solana/web3.js

You'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)
bash
# Generate a wallet if you don't have one
solana-keygen new -o wallet.json

# Fund it with SOL from any exchange or wallet

Level 1: Basic Bot

The simplest possible bot that plays one match per round:

typescript
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:

typescript
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

ArchetypeStrengthWeakness
Balanced [20,20,20,20,20]Never gets dominatedNever dominates
DualHammer [45,40,10,3,2]Wins 2 fields hardVulnerable on 3 fields
TriFocus [30,30,25,10,5]Controls majorityBeatable by concentrated
SingleSpike [50,20,15,10,5]Guarantees 1 fieldPredictable pattern
Guerrilla [40,25,20,10,5]Flexible allocationMid-tier at everything
Spread [25,22,20,18,15]Hard to counterLow ceiling

Level 3: Opponent Profiling

Read your opponents' past results to adapt your strategy:

typescript
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

typescript
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.

typescript
// 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:

typescript
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.
typescript
// 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:

typescript
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");
}