Auto-Entry Bot

A small reference auto-entry bot polls a deployed Quantum Raffle contract on a schedule and decides — via a user-editable applyLogic() — whether to buy a ticket. Use it as a starting point for automating your own entries.

It's designed to drop into an Anvil app. The plan is to publish a clone link so anyone can fork their own copy, plug in their wallet, and run it.

There are two ways to fire checkRaffle() on a schedule:

  • Paid Anvil tier — wire a Scheduled Task at checkRaffle directly.
  • Free Anvil tier — the module also registers checkRaffle() as an HTTP endpoint at /_/api/check_raffle. Publish the app, then point any free external cron (cron-job.org, EasyCron, GitHub Actions schedule, …) at the URL.

How it works

The entry point is checkRaffle(). On each invocation it:

  1. Connects to the configured RPC and loads the QuantumRaffleV2 contract.
  2. Reads on-chain state for the current game: gameId, isGameOver, entrantCount, lastTimestamp, prizePool, prizePerWinner, getNumWinners, getCurrentCohort, and the locked-in gameConfigRecord. If no game is active, it falls back to nextGameConfig.
  3. Reconstructs the bot's own ticket holdings in this game by querying GameEntered events filtered by the bot's address. The event carries the post-entry entrantCount and num_entries, so the IDs assigned to each tx are the contiguous range (entrantCount - num_entries + 1) … entrantCount.
  4. Computes how many of those tickets are currently in a winning slot, using the same rule as QuantumRaffleV2.isWinner (last entrant always wins; otherwise position-from-end must be a power of grandPrizeLogBase).
  5. Bundles everything into a state dict and hands it to applyLogic(state).
  6. If applyLogic returns True, submits a single-ticket transaction. If ALERT_EMAIL is configured, emails a confirmation via anvil.email.

Buying is just eth_sendTransaction to the contract address with value = entryAmount. If PRIZE_RECIPIENT is set, the tx data is the 20-byte recipient address — that triggers fallback() instead of receive() and routes prizes to the cold wallet (see Cold-Wallet Prize Routing).

Setup in Anvil

Same first three steps for both deployment paths — only step 4 differs.

  1. Clone the published Anvil app via the clone link.
  2. In the App Secrets panel, add:
    • RAFFLE_PRIVATE_KEY — the hot wallet's private key.
    • BOT_HTTP_SECRET (only needed for the free-tier path) — any random string. The HTTP endpoint refuses requests without it.
  3. Open the bot module and edit the CONFIG block:

    RPC_URL = "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"
    CONTRACT_ADDRESS = "0xYourQuantumRaffleV2Address"
    PRIZE_RECIPIENT = "0xYourColdWallet"  # strongly recommended
    FROM_BLOCK = 0                        # set to deploy block on slow chains
    GAS_RESERVE_ETH = 0.005
    MAX_PRIORITY_GWEI = 2.0
    ALERT_EMAIL = None                    # or "you@example.com"
    
  4. Pick one of:

    Paid tier — Anvil Scheduled Task. Add a Scheduled Task in Anvil's left sidebar, pick a cadence (e.g. every 10 minutes), and point it at checkRaffle.

    Free tier — published HTTP endpoint + external cron. Publish the Anvil app and point any free cron service at:

    https://<your-app-id>.anvil.app/_/api/check_raffle?key=<BOT_HTTP_SECRET>
    

Configuration reference

All in the bot module's CONFIG block. Edit and re-publish; nothing here is read at runtime from any other source except the private key.

Constant Purpose
RPC_URL HTTP RPC endpoint (Alchemy, Infura, Anvil node, etc.).
CONTRACT_ADDRESS Deployed QuantumRaffleV2 address.
PRIZE_RECIPIENT Optional cold wallet to receive payouts. None = prizes go to the hot wallet.
FROM_BLOCK Earliest block to scan for the bot's GameEntered events. Set to the deploy block on long chains.
GAS_RESERVE_ETH ETH the bot keeps in reserve for future gas. It won't buy if this would be eaten.
MAX_PRIORITY_GWEI EIP-1559 priority tip in gwei.
ALERT_EMAIL Optional address to email after every successful buy. Uses anvil.email.send.
MAX_WINNING_TICKETS_HELD Skip buying once the bot already holds this many currently-winning tickets. Default 1.
MAX_TICKETS_PER_GAME Hard cap on tickets bought per game. Default 5.
SNIPE_WINDOW_SECONDS If the deadline is closer than this, buy regardless of EV. Default 60.
MIN_EV_RATIO Outside the snipe window, only buy when prizePerWinner ≥ entry × this. Default 5.

The two env vars the module reads are RAFFLE_PRIVATE_KEY (always) and BOT_HTTP_SECRET (only when using the HTTP endpoint).

The default strategy

applyLogic(state) ships with a conservative template. In order:

  1. No active game → don't buy.
  2. Can't afford entry + gas reserve → don't buy.
  3. Already holding MAX_WINNING_TICKETS_HELD winning tickets → don't buy (your position is already strong).
  4. Inside the snipe window (seconds_until_end ≤ SNIPE_WINDOW_SECONDS) → buy. The last entrant always wins, so jumping in just before the deadline has high EV.
  5. At or above MAX_TICKETS_PER_GAME → don't buy.
  6. prizePerWinner ≥ entry × MIN_EV_RATIO → buy. Otherwise pass.

Writing your own strategy

applyLogic gets one argument — state, a plain dict — and must return True (buy one ticket) or False (skip this tick). Available keys:

Key Type Meaning
active bool True iff a game is in progress and not yet over.
is_over bool True iff isGameOver() returned true.
game_id int Current game ID. 0 means no game has started yet.
user_address str The hot wallet's checksummed address.
user_balance_wei int Hot wallet balance in wei.
user_tickets int Total tickets the bot has bought in this game.
user_entrant_ids list[int] Every entrant ID the bot owns in this game.
user_winning_tickets int How many of those entrant IDs are currently in a winning slot.
entry_amount_wei int Cost of one entry in wei (locked when the game started).
entrant_count int Total entrants in the active game.
last_timestamp int Unix timestamp of the most recent entry.
deadline_seconds int Inactivity timer (locked at game start).
seconds_until_end int last_timestamp + deadline - now, floored at 0.
prize_pool_wei int Current grand-prize pool.
prize_per_winner_wei int prizePool / numWinners.
num_winners int floor(log_base(entrantCount)) + 1.
current_cohort int floor(log_viralityBase(entrantCount)) + 1.
config dict The locked gameConfigRecord (all 8 fields, by name).

When the game is over or hasn't started, the dict is smaller — see the module's comments for the exact subset.

Examples:

# Always buy
def applyLogic(state):
    return state.get("active", False)
# Only enter once per game, and only deep into the deadline
def applyLogic(state):
    if not state.get("active"): return False
    if state["user_tickets"] > 0: return False
    return state["seconds_until_end"] <= 30

Operational notes

  • Single ticket per tick. The bot always buys exactly one entry per invocation. To buy more, schedule the task more frequently.
  • No retries on failed transactions. A reverted tx raises and the scheduled run errors out.
  • Prize claiming is out of scope. This module only enters games. Use claimPrize / claimAdoptionBonusPrize directly to collect winnings.

Security

  • Use a dedicated hot wallet. Generate a fresh keypair just for the bot. The private key has to live in Anvil Secrets in plaintext; treat it as compromisable.
  • Only fund what you're willing to spend. Top up with enough ETH for a handful of entries plus gas, refill as needed.
  • Route prizes to a cold wallet by setting PRIZE_RECIPIENT. Buys go through fallback() and any payout lands in the cold wallet, even if the hot key leaks later.
  • Set BOT_HTTP_SECRET if you use the HTTP endpoint. Without it, anyone who guesses the URL can fire your bot.

results matching ""

    No results matching ""