Automating web3 workflows at scale — a sanitized case study
How I built custom tooling to manage hundreds of wallets, automate on-chain transactions, and run social bots across multiple protocols.
In 2023 I went deep into crypto/web3 automation. Not trading bots in the traditional sense — more like infrastructure for operating at scale across protocols. This is a sanitized overview of what I built, the architecture decisions that worked, and the ones that didn't.
The problem space
Web3 workflows involve repetitive, time-sensitive actions across multiple chains and platforms:
┌─ DAILY OPERATIONS (per wallet) ──────────────────┐
│ │
│ 06:00 Check gas prices across 5 chains │
│ 06:05 Execute pending bridge transactions │
│ 08:00 Claim daily rewards on 3 protocols │
│ 10:00 Post on Farcaster (varied content) │
│ 12:00 Interact with target contracts │
│ 14:00 Reply to threads (context-aware) │
│ 18:00 Swap accumulated tokens │
│ 22:00 Reconcile balances, flag anomalies │
│ │
│ × 200+ wallets │
│ × 30 days │
│ = ~48,000 operations/month │
│ │
└──────────────────────────────────────────────────┘Doing this manually for 5 wallets is tedious. For 200+, it's impossible without automation. And the automation itself needs to be undetectable — protocols actively filter out bot-like behavior.
Architecture: four layers
┌─────────────────────────────────────────────────────┐
│ ORCHESTRATOR │
│ (workflow definitions, scheduling, state machine) │
├─────────────────────────────────────────────────────┤
│ │ │ │ │
│ ┌──────▼──────┐ ┌────▼─────┐ ┌─────▼──────┐ │
│ │ WALLET │ │ TX │ │ SOCIAL │ │
│ │ MANAGER │ │ ENGINE │ │ LAYER │ │
│ │ │ │ │ │ │ │
│ │ • keygen │ │ • queue │ │ • farcaster│ │
│ │ • balances │ │ • nonce │ │ • content │ │
│ │ • gas mgmt │ │ • retry │ │ • timing │ │
│ │ • identity │ │ • multi- │ │ • persona │ │
│ │ profiles │ │ chain │ │ mgmt │ │
│ └─────────────┘ └──────────┘ └────────────┘ │
│ │ │ │ │
├─────────▼──────────────▼──────────────▼─────────────┤
│ INFRASTRUCTURE LAYER │
│ (RPC rotation, proxy management, monitoring) │
└─────────────────────────────────────────────────────┘Layer 1: Wallet manager
Each wallet needs its own identity — not just a private key, but a behavioral profile that makes it look like a real person.
@dataclass
class WalletProfile:
address: str
private_key: str # encrypted at rest
chain_configs: dict[str, ChainConfig]
# Behavioral fingerprint
timezone: str # affects activity hours
gas_preference: str # "low" | "medium" | "aggressive"
tx_delay_range: tuple # (min_seconds, max_seconds)
daily_tx_limit: int # varies per wallet
active_hours: tuple # (start_hour, end_hour)
# Anti-detection
rpc_provider: str # different per wallet
proxy_group: str # residential proxy rotation
user_agent: str # for web3 dapp interactions💡 Key insight
The biggest anti-detection measure isn't technical — it's behavioral. Wallets that transact at the same time, with the same gas amount, in the same order get flagged instantly. Randomization at every level is mandatory.
Layer 2: Transaction engine
The hardest engineering problem: nonce management at scale. When you're sending transactions from 200 wallets concurrently, the naive approach breaks immediately.
# The naive approach (breaks at scale)
nonce = web3.eth.get_transaction_count(address, 'pending')
tx = build_tx(nonce=nonce)
web3.eth.send_raw_transaction(tx)
# Problem: if two txs fire simultaneously, they get the same nonce
# Result: one succeeds, one fails, nonce tracker is now wrong
# My approach: local nonce manager with pre-allocation
class NonceManager:
def __init__(self, address: str, chain: str):
self.address = address
self.chain = chain
self.local_nonce = self._sync_from_chain()
self._lock = asyncio.Lock()
async def get_next(self) -> int:
async with self._lock:
nonce = self.local_nonce
self.local_nonce += 1
return nonce
async def handle_failure(self, nonce: int, error: str):
if "nonce too low" in error:
# Another tx confirmed first, resync
self.local_nonce = await self._sync_from_chain()
elif "replacement underpriced" in error:
# Tx stuck, bump gas and retry with same nonce
await self._retry_with_gas_bump(nonce)The transaction queue itself uses a priority system:
| Priority | Type | Retry policy |
|---|---|---|
| P0 — Critical | Time-sensitive claims, expiring approvals | 3 retries, gas bump each time |
| P1 — High | Daily interactions, swaps | 5 retries, exponential backoff |
| P2 — Normal | Social actions, non-urgent transfers | 3 retries, skip if gas too high |
| P3 — Low | Cleanup, consolidation | 1 attempt, defer to next cycle |
Layer 3: Social automation
Farcaster bots that post and reply need to look human. The key challenges:
class SocialBot:
def generate_post(self, wallet: WalletProfile) -> str:
"""
Rules:
- No two wallets post the same content
- Timing varies by wallet's timezone profile
- Content references recent on-chain activity (makes it look organic)
- Mix of original posts and replies (70/30 ratio)
- Never post within 5 min of another wallet in same proxy group
"""
context = self.get_recent_activity(wallet)
template = self.pick_template(wallet.persona_type)
return self.fill_template(template, context, variation=True)
def schedule_reply(self, wallet: WalletProfile, target_cast: str):
"""
Reply timing:
- Not instant (suspicious)
- Not too late (irrelevant)
- Sweet spot: 3-45 minutes after original post
- Jitter based on wallet's "personality" speed
"""
base_delay = random.uniform(180, 2700) # 3-45 min
personality_factor = wallet.reply_speed # 0.5 (fast) to 2.0 (slow)
delay = base_delay * personality_factor
self.queue_action(wallet, 'reply', target_cast, delay=delay)Layer 4: Infrastructure
The invisible layer that makes everything else work:
class RPCManager:
providers = {
"ethereum": [
{"url": "https://rpc1.example.com", "weight": 3, "failures": 0},
{"url": "https://rpc2.example.com", "weight": 2, "failures": 0},
{"url": "https://rpc3.example.com", "weight": 1, "failures": 0},
]
}
def get_provider(self, chain: str, wallet: str) -> str:
"""
Selection logic:
1. Filter out providers with >5 recent failures
2. Weight by reliability score
3. Sticky per wallet (same wallet → same provider, reduces fingerprint)
4. Fallback to backup if primary is down
"""
available = [p for p in self.providers[chain] if p["failures"] < 5]
if not available:
self.alert("All RPCs degraded", chain=chain)
return self.emergency_fallback(chain)
return self.weighted_select(available, seed=wallet)⚠️ Lesson learned the hard way
Public RPCs rate limit at ~25 req/s. With 200 wallets checking balances simultaneously, that's 200 requests in a burst. I burned through 3 providers in the first week before implementing proper request spreading and caching.
Monitoring and observability
┌─ DAILY REPORT: 2024-01-15 ──────────────────────┐
│ │
│ Wallets active: 187/200 (13 paused) │
│ Transactions sent: 1,247 │
│ Success rate: 94.3% │
│ Failed (retried): 52 │
│ Failed (permanent): 19 │
│ Gas spent: 2.4 ETH ($5,280) │
│ Social posts: 340 │
│ Social replies: 127 │
│ │
│ ⚠️ Alerts: │
│ - Wallet 0x7f3a: nonce stuck (resolved) │
│ - Arbitrum RPC: 12% error rate (switched) │
│ - Farcaster: 3 posts flagged (paused wallet) │
│ │
└──────────────────────────────────────────────────┘What worked vs. what didn't
| ✓ Worked | ✗ Didn't work |
|---|---|
| Local nonce management | Relying on pending nonce from RPC |
| Per-wallet behavioral profiles | Uniform timing across all wallets |
| Priority-based tx queue | FIFO queue (critical txs got stuck) |
| Sticky RPC per wallet | Random RPC rotation (inconsistent state) |
| Daily reconciliation reports | Only alerting on failures (missed drift) |
The transferable lessons
💡 Patterns that apply to any automation at scale
- • Automation at scale is an infrastructure problem, not a scripting problem
- • Idempotency, retries, and circuit breakers aren't optional
- • Behavioral randomization beats technical obfuscation for anti-detection
- • Monitor for drift, not just failures
- • The reverse-engineering skill is universal — APIs, RPCs, social platforms all work the same way under the hood
I've moved on from active web3 operations, but every automation project I take on now uses the same architecture patterns. The domain changes. The engineering doesn't.
Related posts
- Automation PatternsMay 19, 20268 min read
CloakBrowser: I tested it against 5 bot detectors — here's what happened
CloakBrowser claims to be a stealth Chromium that passes every bot detection test. I installed it, ran it against reCAPTCHA v3, Cloudflare Turnstile, and FingerprintJS to see if the hype is real.
- API Reverse EngineeringApr 22, 202612 min read
How I reverse-engineered an HR attendance API in 3 days
A practical walkthrough of the methodology I use when there's no documentation: capturing traffic, mapping endpoints, and validating assumptions before writing a single line of automation.