ekofyi
Automation Patterns10 min read

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:

text
┌─ 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

text
┌─────────────────────────────────────────────────────┐
│                  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.

pythonwallet_profile.py
@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.

pythonnonce_manager.py
# 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:

PriorityTypeRetry policy
P0 — CriticalTime-sensitive claims, expiring approvals3 retries, gas bump each time
P1 — HighDaily interactions, swaps5 retries, exponential backoff
P2 — NormalSocial actions, non-urgent transfers3 retries, skip if gas too high
P3 — LowCleanup, consolidation1 attempt, defer to next cycle

Layer 3: Social automation

Farcaster bots that post and reply need to look human. The key challenges:

pythonsocial_bot.py
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:

pythonrpc_manager.py
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

text
┌─ 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 managementRelying on pending nonce from RPC
Per-wallet behavioral profilesUniform timing across all wallets
Priority-based tx queueFIFO queue (critical txs got stuck)
Sticky RPC per walletRandom RPC rotation (inconsistent state)
Daily reconciliation reportsOnly 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

Written by Eko

If you found this useful, follow @ekofyi on X for more notes like this — or get in touch if you have a problem to solve.