Automating DAO Governance with AI Agents: Proposal Analysis and Voting Simulation

Build an AI agent that analyzes DAO governance proposals, simulates voting outcomes, and detects voter manipulation patterns — covering Snapshot, Tally, and on-chain governance contracts.

Your DAO passed a proposal that drained 40% of the treasury. 73% of voting power came from wallets created 48 hours before the vote. An AI agent would have flagged it in 90 seconds. You’re not here to read another breathless think-piece about the potential of AI in Web3. You’re here because you’re tired of manually sifting through Snapshot pages and Etherscan tabs, trying to spot the attack before it drains the funds you’re responsible for. Let’s build a system that does it for you—an AI agent that analyzes proposals, sniffs out manipulation, and simulates votes before they happen, using real tools on real chains.

Why Your DAO’s Snapshot Page is a Ticking Time Bomb

DAO governance isn’t broken; it’s just a juicy, slow-moving target. The attack vectors are well-known, but spotting them in the noise of ~1.2M transactions/day on Ethereum (Etherscan, Q1 2026) is the problem. Your main enemies are:

  • Bribery & Vote Farming: Protocols literally pay voters (in their own token) to vote a certain way. It’s legal, it’s on-chain, and it completely skews “community” sentiment.
  • Sybil Attacks (The 48-Hour Wallet Special): An attacker creates hundreds of wallets, airdrops themselves governance tokens from a faucet or via a flash loan, and votes in a bloc. The wallet age and funding source are dead giveaways—if you check.
  • Flash Loan Voting: Borrow millions in governance tokens for the duration of a vote, swing it, repay the loan, and pocket the profits from the malicious proposal’s outcome. It leaves a massive, obvious on-chain footprint that vanishes after one block.

Manual review misses these patterns. A script that checks wallet creation dates, funding sources, and delegate concentration doesn’t. That’s your agent’s first job.

Architecting Your Governance Sentinel: More Than a Script

This isn’t a one-off Python file. It’s a monitoring system. The core architecture needs three layers:

  1. Data Fetcher: Pulls live proposal and vote data from Snapshot and Tally APIs, and raw transaction history from chain RPCs.
  2. Analysis Engine: Runs the fetched data through pattern detection logic (your “AI” – initially, heuristic rules and later ML models) and simulates vote outcomes under different scenarios (e.g., “what if we exclude wallets < 7 days old?”).
  3. Alerting Layer: Pushes high-risk findings to a Telegram bot or Discord webhook immediately.

Open your VS Code (`Ctrl+`` to open the terminal) and start a new Node project. We’ll use ethers.js for on-chain data and axios for API calls.

Querying Snapshot & Tally: Getting the Raw Proposal Feed

First, we need to see what’s being voted on. Let’s fetch active proposals from a Snapshot space. This code block is runnable—save it as fetchProposals.js.

const axios = require('axios');
const { ethers } = require('ethers');

// Use a reliable RPC provider. Latency matters.
const ALCHEMY_RPC_URL = 'your_alchemy_https_url'; // ~95ms latency
const provider = new ethers.providers.JsonRpcProvider(ALCHEMY_RPC_URL);

const SNAPSHOT_GRAPHQL_ENDPOINT = 'https://hub.snapshot.org/graphql';

async function fetchActiveProposals(space = 'ens.eth') {
  const query = `
    query {
      proposals(
        first: 20,
        where: { space: "${space}", state: "active" }
        orderBy: "created",
        orderDirection: desc
      ) {
        id
        title
        body
        start
        end
        snapshot
        state
        author
      }
    }
  `;

  try {
    const response = await axios.post(SNAPSHOT_GRAPHQL_ENDPOINT, { query });
    return response.data.data.proposals;
  } catch (error) {
    console.error('Failed to fetch Snapshot proposals:', error.message);
    return [];
  }
}

async function getProposalVotes(proposalId) {
  const query = `
    query {
      votes(
        first: 1000,
        where: { proposal: "${proposalId}" }
        orderBy: "created",
        orderDirection: desc
      ) {
        id
        voter
        created
        choice
        vp
        vp_by_strategy
      }
    }
  `;

  try {
    const response = await axios.post(SNAPSHOT_GRAPHQL_ENDPOINT, { query });
    return response.data.data.votes;
  } catch (error) {
    console.error(`Failed to fetch votes for ${proposalId}:`, error.message);
    return [];
  }
}

// Example execution
(async () => {
  const proposals = await fetchActiveProposals();
  console.log(`Found ${proposals.length} active proposals`);
  
  if (proposals.length > 0) {
    const votes = await getProposalVotes(proposals[0].id);
    console.log(`First proposal has ${votes.length} votes`);
  }
})();

This gets you the what. Now we need the who—and that requires on-chain detective work.

The AI is Heuristics: Detecting Suspicious Voter Patterns

Your “AI agent” starts as a set of brutal, effective rules. For each voter address, we will:

  1. Fetch its first transaction date (wallet age).
  2. Analyze its funding source (direct from CEX? from a known deployer?).
  3. Check for delegate concentration (do 5 addresses control 80% of the vote?).

Let’s write the analysis function. This is where you’ll hit common ethers.js errors.

async function analyzeVoterRisk(voterAddress, proposalSnapshotBlock) {
  const riskFlags = [];

  // 1. Check wallet age (first transaction)
  try {
    const history = await provider.send("eth_getBlockByNumber", ["earliest", false]); // This is wrong for getting first tx.
    // Correct approach: Use transaction index scan or a service like Etherscan API.
    // For simplicity, we'll get the block number of the first *outgoing* tx.
    const filter = {
      address: voterAddress,
      fromBlock: 0,
      toBlock: proposalSnapshotBlock,
    };
    // This can be slow for old wallets. In production, use The Graph.
    const txs = await provider.getLogs(filter);
    if (txs.length > 0) {
      const firstTxBlock = txs[0].blockNumber;
      const currentBlock = await provider.getBlockNumber();
      const blockAge = currentBlock - firstTxBlock;
      const approxDaysOld = (blockAge * 13.2) / (60 * 60 * 24); // ~13.2s block time

      if (approxDaysOld < 7) {
        riskFlags.push(`WALLET_AGE: Wallet < 7 days old (${approxDaysOld.toFixed(1)} days)`);
      }
    } else {
      riskFlags.push('WALLET_AGE: No prior txs - possibly brand new or funded via internal tx');
    }
  } catch (error) {
    // Common error: filter range too large
    if (error.message.includes('query returned more than')) {
      console.error(`Too many txs for ${voterAddress}. Use pagination or a subgraph.`);
    } else {
      console.error(`Error analyzing wallet age for ${voterAddress}:`, error.message);
    }
  }

  // 2. Simulate a gas estimation to check if wallet is an EOA or has code (potential contract wallet)
  try {
    const code = await provider.getCode(voterAddress);
    if (code !== '0x') {
      riskFlags.push('CONTRACT_ACCOUNT: Voter is a smart contract (could be a proxy or bot)');
    }
  } catch (error) {
    console.error(`Error fetching code for ${voterAddress}:`, error.message);
  }

  return riskFlags;
}

// Real error you will encounter and its fix:
// Error: insufficient funds for gas * price + value
// Fix: Always check balance and estimate gas before sending a tx.
async function safeSendTx(tx) {
  const gasEstimate = await provider.estimateGas(tx);
  const gasPrice = await provider.getGasPrice();
  const cost = gasEstimate.mul(gasPrice);
  const balance = await provider.getBalance(tx.from);
  
  if (balance.lt(cost)) {
    throw new Error(`Insufficient funds. Need ${ethers.utils.formatEther(cost)} ETH, have ${ethers.utils.formatEther(balance)} ETH.`);
  }
  // Proceed with sendTransaction...
}

Simulating Vote Outcomes: The “What If” Engine

This is the killer feature. Before a proposal executes on-chain (e.g., via Timelock), simulate the outcome if suspicious votes are removed. You need the voting power (VP) of each address at the proposal’s snapshot block.

Here’s a comparison of methods to get this data, because performance is critical:

MethodTool/ServiceSpeedCostAccuracyBest For
Direct Contract Callethers.js / web3.pySlow (~100ms/call)Gas (if write)HighSingle token, small voter set
Subgraph QueryThe GraphVery Fast (~50ms)Free (hosted)HighComplex, cross-contract VP
Archived RPC NodeAlchemy/Infura PremiumFast (~80ms)$$$HighCustom logic on raw data
Cached APITenderly, Dune AnalyticsFast (~70ms)Free tier limitsMediumPre-aggregated analysis

For simulation, use The Graph if a subgraph exists. If not, here’s a fallback using a direct call with ethers:

// Example Governance Token Interface (ERC20Votes compatible)
interface IERC20Votes {
    function getPastVotes(address account, uint256 blockNumber) external view returns (uint256);
}
async function simulateVoteOutcome(proposalId, addressesToExclude = []) {
  const votes = await getProposalVotes(proposalId);
  const proposal = (await fetchActiveProposals()).find(p => p.id === proposalId);
  
  let totalVp = 0;
  let simulatedVp = 0;
  const exclusionSet = new Set(addressesToExclude.map(addr => addr.toLowerCase()));

  for (const vote of votes) {
    const voter = vote.voter;
    const vp = parseFloat(vote.vp) || 0;
    totalVp += vp;

    if (!exclusionSet.has(voter.toLowerCase())) {
      simulatedVp += vp;
    } else {
      console.log(`Excluding voter ${voter} with ${vp} VP`);
    }
  }

  const originalQuorum = (totalVp > 0) ? (simulatedVp / totalVp) * 100 : 0;
  console.log(`Original total VP: ${totalVp}`);
  console.log(`Simulated VP (after exclusions): ${simulatedVp}`);
  console.log(`Simulated retains ${originalQuorum.toFixed(1)}% of original voting power.`);

  // Determine if the *outcome* would flip (requires knowing choices - simplified here)
  return { totalVp, simulatedVp, wouldFlip: simulatedVp < (totalVp * 0.5) }; // Example: quorum < 50%
}

Case Study: Spotting a Flash Loan Attack on a DEX Proposal

Imagine a proposal to change fee parameters on a major DEX. The vote passes narrowly. Our agent runs its analysis on the top 50 voters:

  • Finding 1: 40% of the “Yes” VP comes from 3 wallets that had zero token balance 36 hours before the snapshot.
  • On-Chain Footprint: Using Tenderly to trace transactions, we find these wallets were funded via a complex swap on Uniswap V3, funded from Tornado Cash, then delegated to themselves. The tokens were borrowed via a flash loan from Aave in the block before the snapshot and returned the block after.
  • Simulation: Running simulateVoteOutcome(proposalId, suspiciousWallets) shows the proposal would have failed with 62% “No.”
  • Cost: A smart contract audit for the fix might cost $15K–$100K on average (Code4rena 2025 report). This detection script costs a few hours of a Solidity developer’s time (avg $145K/yr in the US – Stack Overflow Dev Survey 2025).

Plugging Into the Alerting Layer: The Telegram Bot That Saves Your Treasury

Detection is useless without notification. Here’s the skeleton for a critical alert via a Telegram bot. Use the node-telegram-bot-api package.

const TelegramBot = require('node-telegram-bot-api');
const bot = new TelegramBot('YOUR_BOT_TOKEN', { polling: false }); // Use webhooks in production

async function sendGovernanceAlert(proposalId, riskFindings, simulationResult) {
  const chatId = 'YOUR_CHAT_ID';
  
  let message = `🚨 *Governance Alert* 🚨\n`;
  message += `Proposal: ${proposalId}\n`;
  message += `*Risk Flags:*\n`;
  riskFindings.forEach((flag, idx) => { message += `  • ${flag}\n`; });
  
  message += `\n*Simulation:*\n`;
  message += `Excluding suspicious votes, the proposal would ${simulationResult.wouldFlip ? 'FAIL ❌' : 'still PASS ✅'}.\n`;
  message += `Power retained: ${((simulationResult.simulatedVp / simulationResult.totalVp) * 100).toFixed(1)}%`;

  try {
    await bot.sendMessage(chatId, message, { parse_mode: 'Markdown' });
    console.log('Alert sent to Telegram.');
  } catch (error) {
    console.error('Failed to send Telegram alert:', error.message);
    // Fallback to logging or email
  }
}

// Real error you will encounter and its fix:
// Error: nonce too low
// Fix: Always sync the nonce correctly before sending a transaction.
async function getCorrectNonce(address) {
  // Use 'pending' to include mempool transactions
  const nonce = await provider.getTransactionCount(address, 'pending');
  return nonce;
}

Next Steps: From Heuristics to Machine Learning

You’ve now got a functional, heuristic-based governance sentinel. It’s better than 95% of DAOs that do nothing. To move from Intermediate to Advanced:

  1. Integrate with The Graph: Replace slow getLogs calls with subgraph queries for wallet history and voting power. This is essential for scaling.
  2. Add On-Chain Sentiment: Use Dune Analytics SQL or an LLM via Continue.dev extension in VS Code (Ctrl+Shift+P to open command palette) to analyze the proposal body text for sentiment and compare it to the voters’ historical patterns.
  3. Implement a Simple ML Model: Use Python with web3.py to train a binary classifier (risk / no-risk) on historical proposal and voter data. Start with features like wallet age, VP concentration, and similarity of voting history to known attackers.
  4. Deploy as a Watchdog Service: Containerize this agent with Docker, run it on a cron job, and point it at your DAO’s Snapshot space. Let it run 24/7.

The goal isn’t to replace human governance, but to arm it with a first line of defense. In a landscape where DeFi TVL is back at $180B (DefiLlama, Jan 2026) and attacks grow more sophisticated, automating your vigilance isn’t just clever engineering—it’s fiduciary responsibility. Stop staring at Snapshot and start building the watchtower.