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
checkRaffledirectly. - 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 Actionsschedule, …) at the URL.
How it works
The entry point is checkRaffle(). On each invocation it:
- Connects to the configured RPC and loads the
QuantumRaffleV2contract. - Reads on-chain state for the current game:
gameId,isGameOver,entrantCount,lastTimestamp,prizePool,prizePerWinner,getNumWinners,getCurrentCohort, and the locked-ingameConfigRecord. If no game is active, it falls back tonextGameConfig. - Reconstructs the bot's own ticket holdings in this game by
querying
GameEnteredevents filtered by the bot's address. The event carries the post-entryentrantCountandnum_entries, so the IDs assigned to each tx are the contiguous range(entrantCount - num_entries + 1) … entrantCount. - 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 ofgrandPrizeLogBase). - Bundles everything into a
statedict and hands it toapplyLogic(state). - If
applyLogicreturnsTrue, submits a single-ticket transaction. IfALERT_EMAILis configured, emails a confirmation viaanvil.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.
- Clone the published Anvil app via the clone link.
- 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.
Open the bot module and edit the
CONFIGblock: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"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:
- No active game → don't buy.
- Can't afford entry + gas reserve → don't buy.
- Already holding
MAX_WINNING_TICKETS_HELDwinning tickets → don't buy (your position is already strong). - 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. - At or above
MAX_TICKETS_PER_GAME→ don't buy. 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/claimAdoptionBonusPrizedirectly 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 throughfallback()and any payout lands in the cold wallet, even if the hot key leaks later. - Set
BOT_HTTP_SECRETif you use the HTTP endpoint. Without it, anyone who guesses the URL can fire your bot.