Setup guide: Automating daily transactions across 50 wallets safely
Setup guide: Automating daily transactions across 50 wallets safely
Managing one wallet manually is fine. Managing five is annoying. Managing fifty is impossible without automation, and trying anyway is how you miss interaction windows, mess up gas timing, or forget wallets entirely for days at a stretch. I’ve been running multi-wallet setups out of Singapore since 2023, and at the point where I crossed 30 active wallets, the manual approach broke down completely.
This guide is for operators who already understand what airdrop farming is, have funded wallets ready to go, and want a repeatable, scriptable system for executing daily transactions across all of them. the outcome is a Python-based automation stack running on a VPS with cron scheduling, proxy rotation per wallet, and basic alerting, so you wake up each morning to a log file rather than 50 open browser tabs.
This is not a beginner’s introduction to crypto. this guide assumes you know what a private key is, can SSH into a server, and understand that you are responsible for your own operational security. Nothing here is legal or tax advice.
what you need
Infrastructure
- A VPS with at least 2 vCPU and 4 GB RAM. I use Hetzner CX22 (currently ~4.15 EUR/month) in their Helsinki region. DigitalOcean or Vultr work fine too.
- Ubuntu 22.04 LTS on that VPS
- Python 3.11+
- web3.py 6.x (official docs)
RPC access - Alchemy free tier gives you 300M compute units/month, which covers ~50 wallets doing 2-3 transactions each daily. their Growth plan at $49/month removes rate limits if you scale past that. - Alternatively, run your own node with Erigon if you’re on a chain where that’s practical
Proxy setup - Residential or mobile proxies for wallet isolation. I use around 5-10 rotating proxies from a provider with a real IP pool (not datacenter IPs). see proxyscraping.org/blog/ for provider comparisons and what to look for in rotation configs. - Each wallet should route through a different IP per session to avoid behavioral clustering
Security
- A secrets manager or at minimum an encrypted .env file approach. I use python-dotenv with the env file on a separate encrypted volume.
- Do not store private keys in plain text in your script files. this is the most common mistake I see.
Costs (rough estimate for 50 wallets) - VPS: ~5 EUR/month - RPC: $0 to $49/month depending on volume - Proxies: $20-80/month depending on provider and GB consumed - Gas: depends entirely on the chain, not budgeted here
step by step
step 1: provision your VPS and set up Python
SSH into your server and run:
sudo apt update && sudo apt upgrade -y
sudo apt install python3.11 python3-pip python3-venv git -y
python3 -m venv ~/wallet-bot/venv
source ~/wallet-bot/venv/bin/activate
pip install web3 python-dotenv requests
create your project directory structure:
wallet-bot/
wallets.csv # wallet addresses only, no keys here
keys/ # encrypted volume or secrets manager
scripts/
transact.py
monitor.py
logs/
.env
expected output: Python 3.11 environment with web3.py installed, confirming pip show web3 returns version 6.x or higher.
if it breaks: if web3 fails to install, check that you have libssl-dev and build-essential installed on Ubuntu. run sudo apt install build-essential libssl-dev.
step 2: set up RPC endpoints
Sign up for Alchemy and create a new app for each chain you’re targeting. you’ll get a unique HTTPS endpoint per app. do not share one endpoint across all wallets without rate limit handling.
In your .env file:
ALCHEMY_ETH_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY_HERE
ALCHEMY_ARB_URL=https://arb-mainnet.g.alchemy.com/v2/YOUR_KEY_HERE
expected output: web3.is_connected() returns True when you test in a Python shell.
if it breaks: Alchemy occasionally changes their endpoint format. always check the Alchemy docs for the current URL structure per chain.
step 3: store private keys securely
I keep keys in a separate file that lives on a LUKS-encrypted partition. on Hetzner, you can attach a block volume and encrypt it with:
sudo cryptsetup luksFormat /dev/sdb
sudo cryptsetup open /dev/sdb keys_volume
sudo mkfs.ext4 /dev/mapper/keys_volume
sudo mount /dev/mapper/keys_volume /mnt/keys
your keys.csv lives at /mnt/keys/keys.csv. it has two columns: address,private_key. the volume is unmounted when the bot is not running in production.
For simpler setups, use a .env file with strict permissions:
chmod 600 ~/wallet-bot/.env
but do not commit this file to any repository. add it to .gitignore before you ever run git init.
expected output: ls -la .env shows -rw------- permissions.
if it breaks: if you accidentally commit keys to git, rotate them immediately. there are bots scanning GitHub for exposed keys within seconds of a push.
step 4: configure proxy rotation
Each transaction session should use a different IP. here’s a minimal proxy wrapper:
import requests
import random
PROXIES = [
"http://user:[email protected]:8080",
"http://user:[email protected]:8080",
# add all your proxies here
]
def get_proxy():
return {"http": random.choice(PROXIES), "https": random.choice(PROXIES)}
for web3.py, you’ll pass the proxy through a custom HTTPProvider session:
from web3 import Web3
from web3.middleware import geth_poa_middleware
import requests
session = requests.Session()
session.proxies = get_proxy()
w3 = Web3(Web3.HTTPProvider(RPC_URL, session=session))
expected output: w3.eth.block_number returns a valid block number, meaning the connection works through the proxy.
if it breaks: residential proxies sometimes block certain RPC providers. test each proxy against your RPC URL manually with curl --proxy YOUR_PROXY_URL YOUR_RPC_URL to identify broken ones.
step 5: write the core transaction script
Here’s a stripped-down version of what my daily runner looks like for a simple ETH transfer or contract interaction:
import os
import csv
import time
import logging
from web3 import Web3
from dotenv import load_dotenv
load_dotenv()
logging.basicConfig(filename="logs/run.log", level=logging.INFO,
format="%(asctime)s %(message)s")
RPC_URL = os.getenv("ALCHEMY_ETH_URL")
w3 = Web3(Web3.HTTPProvider(RPC_URL))
def load_wallets(path):
wallets = []
with open(path) as f:
reader = csv.DictReader(f)
for row in reader:
wallets.append(row)
return wallets
def transact(wallet):
address = wallet["address"]
key = wallet["private_key"]
nonce = w3.eth.get_transaction_count(address)
gas_price = w3.eth.gas_price
tx = {
"nonce": nonce,
"to": "0xTARGET_CONTRACT_OR_ADDRESS",
"value": w3.to_wei(0.001, "ether"),
"gas": 21000,
"gasPrice": gas_price,
"chainId": 1,
}
signed = w3.eth.account.sign_transaction(tx, key)
tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction)
logging.info(f"{address} tx sent: {tx_hash.hex()}")
return tx_hash.hex()
wallets = load_wallets("/mnt/keys/keys.csv")
for wallet in wallets:
try:
transact(wallet)
time.sleep(random.uniform(30, 90)) # stagger sends
except Exception as e:
logging.error(f"{wallet['address']} failed: {e}")
the time.sleep with randomized delay is important. sending 50 transactions in 10 seconds is a red flag pattern. spacing them 30-90 seconds apart looks more organic and avoids RPC rate limits.
expected output: logs/run.log fills with tx sent: lines, one per wallet.
if it breaks: nonce too low errors usually mean a previous transaction is still pending. query w3.eth.get_transaction_count(address, "pending") to get the correct nonce.
step 6: test on one wallet first
Before running against all 50, isolate one wallet and run the script with a --dry-run flag that skips send_raw_transaction. confirm the constructed transaction looks correct, gas estimate is sane, and the proxy is working. only then expand to your full wallet list.
expected output: dry run prints transaction details without broadcasting anything.
if it breaks: if gas estimates look wildly off, you may be on the wrong chain ID. double-check the chainId field matches the network your RPC connects to. Ethereum mainnet is 1, Arbitrum One is 42161. the Ethereum documentation on transactions covers the tx fields in detail.
step 7: schedule with cron
On your VPS, set up a cron job to run the script daily at a randomized time. avoid scheduling exactly at midnight UTC, since that’s when many other bots run and gas spikes.
crontab -e
add:
0 14 * * * /home/ubuntu/wallet-bot/venv/bin/python /home/ubuntu/wallet-bot/scripts/transact.py >> /home/ubuntu/wallet-bot/logs/cron.log 2>&1
this runs at 14:00 UTC daily. if you want randomization, use a wrapper shell script with a sleep $((RANDOM % 3600)) before calling Python.
expected output: cron runs the script without manual intervention, logs appear in cron.log.
if it breaks: cron doesn’t inherit your shell environment. use absolute paths for both Python and the script. if the venv’s Python isn’t found, add source /home/ubuntu/wallet-bot/venv/bin/activate && before the Python call inside a shell script wrapper.
step 8: set up basic monitoring
At minimum, you want an alert when the script fails entirely or when wallets start running low on gas. I use a simple Telegram bot for this. the python-telegram-bot library has a straightforward send_message API.
import requests
def alert(msg):
token = os.getenv("TG_TOKEN")
chat_id = os.getenv("TG_CHAT_ID")
requests.post(f"https://api.telegram.org/bot{token}/sendMessage",
json={"chat_id": chat_id, "text": msg})
call alert() in your exception handlers. you’ll know within minutes if something breaks rather than finding out the next morning that 50 wallets did nothing for 24 hours.
common pitfalls
storing keys in plaintext on the VPS root volume. if your VPS is ever compromised, every wallet is gone. use an encrypted volume or a secrets manager at minimum.
no delay between transactions. sending all 50 transactions in rapid succession is an obvious bot signature. it can also exhaust your RPC compute units in minutes. always randomize delays.
using the same RPC endpoint and IP for all wallets. an RPC provider can see which addresses you’re querying from which IP. if all 50 wallets come from the same IP, the on-chain behavioral clustering they see will be obvious.
not handling nonce collisions. if your script runs twice (for example, a cron overlap or a manual re-run), you can end up with duplicate nonces and stuck transactions. add a lock file check at the start of your script.
no log rotation. log files grow forever if you don’t cap them. add logrotate config for your logs directory on day one, not after six months when the disk is full.
scaling this
at 10x (500 wallets): you need a proper secrets manager, not a CSV file. HashiCorp Vault (open source) or AWS Secrets Manager are both real options. you also need multiple Alchemy API keys to avoid hitting rate limits, and a load balancer in front of your RPC calls. at this scale, the multiaccountops.com/blog/ has practical write-ups on infrastructure patterns worth reading.
at 100x (5000 wallets): a single VPS is not enough. you need a task queue (Celery with Redis, or BullMQ if you prefer Node) distributing work across multiple workers. your proxy pool needs to be in the hundreds, and you’ll want per-wallet execution history in a database, not just log files. gas management becomes a dedicated module: you need automatic top-ups when wallet balances drop below threshold.
at 1000x (50,000 wallets): this is a different engineering problem entirely. you’re running a distributed system with database replication, monitoring dashboards, and likely a dedicated DevOps hire. the bottleneck at this scale is usually RPC throughput and private key management, not the scripting layer. running your own archive nodes per chain starts to make financial sense.
where to go next
- How to set up wallet generation at scale for airdrop farming covers the process of creating and organizing large wallet batches from a single seed structure, with labeling conventions that keep tracking manageable.
- Proxy setup for multi-wallet operations: residential vs mobile vs datacenter goes deeper on IP isolation strategies, what proxy types actually work for RPC calls, and how to test your setup before going live.
- The /blog/ index has more walkthroughs on specific chain setups and tooling reviews.
Written by Xavier Fok
disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-05-19.