Strategy

Protocol Governance for AI Agents: Voting, Bribes, and veToken Economics

March 6, 2026 · Purple Flea Team · 15 min read

DeFi governance has evolved far beyond "vote yes or no on proposals." In 2026, protocol governance is a full economic subsystem where governance tokens generate yield, vote allocation determines protocol revenue flows, and sophisticated actors earn 20-40% APY by participating in bribe markets. AI agents are uniquely positioned to extract this yield: they can track every active bribe, compute optimal vote allocations, and execute governance transactions without sleeping.

This post covers the mechanics from scratch: how DAO governance works, how veTokens transform governance participation into a yield strategy, the bribe marketplaces (Votium, Hidden Hand) that pay for votes, and a complete Python GovernanceAgent that automates the entire pipeline.

$50M+ Weekly Curve Bribes
20-40% veToken APY
4yr Max veCRV Lockup
16wk vlCVX Epoch Length

DAO Governance Mechanics

A Decentralized Autonomous Organization (DAO) is a protocol governed by token holders rather than a central company. Token holders propose and vote on protocol changes: fee adjustments, treasury spending, parameter updates, contract upgrades. The exact mechanics vary significantly between protocols, but the core structure is consistent.

Proposal Lifecycle

1

Discussion (2-5 days)

Author posts proposal to governance forum (Discourse, Commonwealth). Community discusses, refines, objects. Temperature checks via Snapshot gauge sentiment before on-chain vote.

2

Snapshot Vote (3-7 days)

Off-chain voting on Snapshot. Gasless. Requires token balance above proposal threshold (e.g., 1,000 CRV). Results are not binding but gauge community support.

3

On-Chain Vote (3-7 days)

Binding on-chain vote on Tally, Boardroom, or protocol-native UI. Requires quorum (typically 5-15% of total supply) and majority approval to pass. Gas cost per vote: $5-30.

4

Timelock + Execution (2-7 days)

Passed proposals enter a timelock before execution, giving users time to exit if they disagree with the change. After timelock, changes are automatically executed on-chain.

Quorum and Voting Power

Quorum is the minimum participation required for a vote to be valid. If quorum is 10% of supply and only 8% of tokens vote, the proposal fails regardless of how people voted. This creates a free-rider problem: many token holders don't vote, making quorum hard to reach without active participants.

Voting power is usually proportional to token balance, but veToken models weight votes by lockup commitment. This is the core innovation: it transforms passive token holding into active long-term alignment, and creates the bribe economy that generates agent yield.

Key types of DAO proposals agents encounter: gauge weight votes (which liquidity pools receive CRV/BAL emissions), protocol parameter changes (fees, collateral ratios), treasury spending, new contract deployments, and emergency shutdown votes. Gauge weight votes are most economically significant — they determine where millions in annual emissions flow.

veToken Lockup Mechanics

The vote-escrow (ve) model was pioneered by Curve Finance with veCRV and has since been adopted by Balancer (veBAL), Frax (veFXS), Angle (veANGLE), and dozens more. The core mechanic: lock your governance token for a time period (1 week to 4 years) and receive a proportional amount of "voting escrow" tokens that decay over time.

veCRV: The Original Model

To acquire veCRV:

  1. Hold CRV (Curve's governance token)
  2. Lock CRV in Curve's VotingEscrow contract for 1 week to 4 years
  3. Receive veCRV proportional to CRV_locked * (lock_duration / 4_years)
  4. veCRV balance decays linearly to 0 at lockup expiry
  5. veCRV cannot be transferred — it is non-fungible and wallet-bound
Lock Duration veCRV Boost (per CRV)
1 week
0.005x
3 months
0.063x
6 months
0.125x
1 year
0.25x
2 years
0.5x
4 years
1.0x (max)

With maximum veCRV (4-year lock), you earn: 3x liquidity mining boost on Curve pools you LP in, 50% of all protocol trading fees (distributed weekly in 3CRV), and gauge weight voting power to direct CRV emissions to specific pools.

veBAL: Balancer's Twist

Balancer's veBAL works similarly but locks 80/20 BAL-ETH Balancer LP tokens instead of pure BAL. This is intentional: it requires holders to maintain ETH market exposure alongside BAL, aligning long-term incentives. The maximum lock is 1 year (shorter than veCRV), and voting occurs weekly on Thursday epochs.

vlCVX: Liquid Governance via Convex

Convex Finance created a clever workaround for veCRV illiquidity. Convex acquires massive veCRV by accepting user CRV deposits and locking it permanently. Users deposit CRV to Convex and receive cvxCRV (liquid, tradeable). Then they lock cvxCRV into vlCVX for 16-week periods.

vlCVX is the most important governance token in the Curve ecosystem because Convex controls ~50% of all veCRV. Whoever controls vlCVX votes controls where ~50% of CRV emissions go. This is why vlCVX bribe yields are extremely high — protocols pay handsomely to direct that emission allocation.

Token Protocol Lock Period Yield Sources Liquidity
veCRV Curve 1 wk - 4 yr 3CRV fees + LP boost + bribes Illiquid
veBAL Balancer 1 wk - 1 yr Protocol fees + LP boost + bribes Illiquid
vlCVX Convex 16 weeks CVX rewards + bribes (Votium) Semi-liquid (epochs)
veFXS Frax 1 wk - 4 yr Protocol revenue + gauges + bribes Illiquid
veANGLE Angle 1 wk - 4 yr Stability fee revenue + bribes Illiquid

Vote-Buying: The Bribe Economy

Bribe markets are the most interesting governance primitive from an agent perspective. A "bribe" in DeFi governance is a legitimate, transparent payment made to veToken holders in exchange for directing their votes toward specific gauge weights.

Here is why protocols pay bribes: if you control a liquidity pool on Curve and you want deep liquidity, you need CRV emissions directed at your pool. CRV emissions make your pool more attractive to LPs. To attract those emissions, you need veCRV holders to vote for your gauge. Paying bribe tokens to veCRV holders is cheaper than buying enough CRV to control the gauge directly.

Votium

Convex / vlCVX Bribes
  • Largest bribe market by volume
  • Epoch length: bi-weekly (every 2 weeks)
  • Target: vlCVX holders who vote on Curve gauge weights
  • Typical yield: 15-25% APY on CVX value
  • Token support: any ERC-20 accepted as bribe
  • Claim: auto-claimable by delegate or directly

Hidden Hand

Multi-Protocol Bribes
  • Bribes for Balancer (veBAL), Aura, Frax, Ribbon, Tokemak
  • Epoch: weekly, Thursday snapshot
  • Target: veBAL and Aura (vlAURA) holders
  • Typical yield: 10-20% APY on BAL/AURA value
  • Protocol-agnostic: one interface for multiple bribe types
  • Claim: merkle-proof based, gas-efficient

Bribe ROI Calculation

The key metric for gauge bribe markets is $ bribed per $1 of weekly emissions directed. Protocols compete on this metric. If a protocol bribes $100k to vlCVX holders and receives $200k in weekly CRV emissions directed to their gauge, the bribe ROI is 2x — very attractive for the bribing protocol.

For the recipient (the voting agent), what matters is $/vote. A simple calculation:

# Bribe yield calculation for vlCVX holder
# ─────────────────────────────────────────────

cvx_price_usd = 4.20           # Current CVX price
cvx_locked = 10_000            # vlCVX locked
cvx_total_supply_locked = 95_000_000  # Total vlCVX supply

# Your share of total votes
vote_share = cvx_locked / cvx_total_supply_locked  # 0.0105%

# Weekly Votium bribe pool (typical active round)
total_bribe_pool_usd = 3_200_000  # $3.2M total bribed per round

# Your expected weekly bribe income
weekly_bribe_usd = vote_share * total_bribe_pool_usd

# Annualized
annual_bribe_usd = weekly_bribe_usd * 26   # 26 bi-weekly epochs/year

# Your CVX position value
position_value_usd = cvx_locked * cvx_price_usd

# Bribe APY from voting alone
bribe_apy = (annual_bribe_usd / position_value_usd) * 100

print(f"Position: ${position_value_usd:,.0f}")
print(f"Vote share: {vote_share*100:.4f}%")
print(f"Weekly bribe income: ${weekly_bribe_usd:.2f}")
print(f"Annual bribe income: ${annual_bribe_usd:.2f}")
print(f"Bribe APY: {bribe_apy:.1f}%")

# On top of this: ~8% CVX staking yield from protocol fees
# Total vlCVX APY estimate: bribe_apy + 8%

Delegate Voting

Most veToken systems support delegation: assigning your voting power to another address without transferring ownership. Delegation is useful when:

Delegation is on-chain: veCRV.delegate(delegatee_address). It can be revoked at any time. Major governance delegates post their voting philosophies publicly. Agents can analyze these delegates and choose the one most aligned with maximizing gauge weight bribe income.

Delegation risk: Delegating voting power doesn't delegate token ownership. Your CRV remains locked and safe. But if your delegate votes in a way that harms protocol health (e.g., supporting bad gauge weights), the protocol token price may decline. Always verify delegate track records before delegating large positions.

Governance-as-Yield Strategy

A pure governance-as-yield strategy treats governance participation like a yield-bearing position rather than a civic duty. The strategy:

  1. Acquire governance tokens at attractive prices (or via liquidity mining)
  2. Lock for maximum duration to maximize voting power and bribe eligibility
  3. Vote in every epoch for the highest-bribe gauges
  4. Claim and sell bribes each epoch
  5. Compound: convert bribe income back into governance tokens and extend locks

The governance-as-yield thesis depends on one key assumption: bribe markets remain active because protocols need emission direction more than they need to own governance tokens outright. This has held true since 2021 across multiple market cycles.

Protocol Revenue Yield

Beyond bribes, veToken holders earn direct protocol revenue:

Protocol Revenue Source Distribution Typical APY
Curve 50% of trading fees (0.04% per swap) Weekly in 3CRV to veCRV holders 3-6%
Balancer Protocol fee switch (variable) Weekly in veBAL claim 2-4%
Convex Platform fee on CRV/CVX rewards Continuous cvxCRV to cvxCRV stakers 5-10%
Frax FRAX stability fees + AMO revenue Weekly in FRAX to veFXS holders 4-8%

Python GovernanceAgent: Full Implementation

The following agent monitors veToken balances, scans Votium and Hidden Hand for active bribes, computes optimal vote allocations by bribe yield, executes votes, and claims rewards each epoch. It integrates with Purple Flea's wallet API for portfolio context.

"""
GovernanceAgent - Autonomous DeFi governance yield optimizer
Supports: vlCVX (Votium), veBAL (Hidden Hand), Snapshot off-chain voting
Purple Flea API: https://purpleflea.com/api/v1
"""

import time
import logging
import requests
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime, timezone

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(message)s'
)
log = logging.getLogger('GovernanceAgent')

# ── Configuration ──────────────────────────────────────────────────────────────

PURPLE_FLEA_KEY = "your-api-key-here"
PF_BASE = "https://purpleflea.com/api/v1"
PF_HEADERS = {
    "Authorization": f"Bearer {PURPLE_FLEA_KEY}",
    "Content-Type": "application/json"
}

# Governance API endpoints (public GraphQL/REST)
VOTIUM_API = "https://api.votium.app/api/v1"
HIDDEN_HAND_API = "https://api.hiddenhand.finance/v1"
SNAPSHOT_HUB = "https://hub.snapshot.org/graphql"

# Agent governance configuration
WALLET_ADDRESS = "your-agent-wallet-address"
MIN_BRIBE_APY_THRESHOLD = 5.0    # Minimum bribe APY to bother voting
AUTO_COMPOUND = True              # Reinvest bribe proceeds back into governance tokens
DELEGATE_ADDRESS: Optional[str] = None  # Set to delegate address if using delegation

# ── Data Classes ───────────────────────────────────────────────────────────────

@dataclass
class GaugeOpportunity:
    """Represents a bribe opportunity for a specific gauge."""
    gauge_address: str
    gauge_name: str
    protocol: str           # "votium" or "hidden_hand"
    token_symbol: str
    bribe_amount_usd: float
    total_votes_usd: float
    bribe_per_vote: float   # USD of bribe per USD of voting power directed
    projected_apy: float

    def __repr__(self):
        return (
            f"[{self.protocol.upper()}] {self.gauge_name}: "
            f"${self.bribe_amount_usd:.0f} bribe | "
            f"${self.bribe_per_vote:.4f}/vote | "
            f"{self.projected_apy:.1f}% APY"
        )

@dataclass
class GovernancePosition:
    """Tracks a veToken position."""
    protocol: str
    token: str
    balance: float
    voting_power: float
    lock_expiry_timestamp: float
    weekly_fee_claim_usd: float = 0.0

    @property
    def lock_weeks_remaining(self) -> float:
        remaining = self.lock_expiry_timestamp - time.time()
        return max(0, remaining / (7 * 86400))

@dataclass
class ClaimedReward:
    protocol: str
    token_symbol: str
    amount: float
    value_usd: float
    epoch: str
    tx_hash: str = ""

@dataclass
class GovernancePortfolio:
    positions: list[GovernancePosition] = field(default_factory=list)
    claimed_rewards: list[ClaimedReward] = field(default_factory=list)
    total_yield_usd: float = 0.0

# ── API Clients ────────────────────────────────────────────────────────────────

def fetch_votium_bribes(epoch: Optional[str] = None) -> list[dict]:
    """
    Fetch current Votium bribe opportunities for vlCVX voters.
    Returns list of gauge bribe data dicts.
    """
    try:
        params = {}
        if epoch:
            params["epoch"] = epoch
        r = requests.get(
            f"{VOTIUM_API}/bribes",
            params=params,
            timeout=15
        )
        r.raise_for_status()
        return r.json().get("data", [])
    except requests.RequestException as e:
        log.warning(f"Votium API error: {e}. Using empty fallback.")
        return []

def fetch_hidden_hand_bribes(protocol: str = "balancer") -> list[dict]:
    """
    Fetch Hidden Hand bribe opportunities.
    protocol: "balancer", "frax", "aura", "ribbon"
    """
    try:
        r = requests.get(
            f"{HIDDEN_HAND_API}/incentives/{protocol}",
            timeout=15
        )
        r.raise_for_status()
        return r.json().get("incentives", [])
    except requests.RequestException as e:
        log.warning(f"Hidden Hand API error for {protocol}: {e}.")
        return []

def fetch_snapshot_proposals(space: str, state: str = "active") -> list[dict]:
    """
    Fetch active Snapshot proposals for a governance space.
    Examples: "curve.eth", "balancer.eth", "frax.finance"
    """
    query = """
    query Proposals($space: String!, $state: String!) {
      proposals(
        where: { space: $space, state: $state }
        orderBy: "created"
        orderDirection: desc
        first: 10
      ) {
        id
        title
        state
        start
        end
        votes
        quorum
        scores_total
      }
    }
    """
    try:
        r = requests.post(
            SNAPSHOT_HUB,
            json={"query": query, "variables": {"space": space, "state": state}},
            timeout=15
        )
        r.raise_for_status()
        return r.json().get("data", {}).get("proposals", [])
    except requests.RequestException as e:
        log.warning(f"Snapshot API error for {space}: {e}.")
        return []

def get_vetoken_balance(wallet: str) -> list[GovernancePosition]:
    """
    Fetch veToken positions via Purple Flea portfolio API.
    Returns list of governance positions.
    """
    try:
        r = requests.get(
            f"{PF_BASE}/governance/positions/{wallet}",
            headers=PF_HEADERS,
            timeout=10
        )
        r.raise_for_status()
        positions = []
        for item in r.json().get("positions", []):
            positions.append(GovernancePosition(
                protocol=item["protocol"],
                token=item["token"],
                balance=item["balance"],
                voting_power=item["voting_power"],
                lock_expiry_timestamp=item.get("lock_expiry", 0),
                weekly_fee_claim_usd=item.get("weekly_fee_claim_usd", 0.0)
            ))
        return positions
    except requests.RequestException as e:
        log.error(f"Failed to fetch governance positions: {e}")
        return []

def submit_vote_via_purple_flea(
    wallet: str,
    proposal_id: str,
    choice: int,
    protocol: str
) -> dict:
    """Submit a governance vote through Purple Flea's signing API."""
    payload = {
        "wallet_address": wallet,
        "proposal_id": proposal_id,
        "choice": choice,
        "protocol": protocol
    }
    r = requests.post(
        f"{PF_BASE}/governance/vote",
        json=payload,
        headers=PF_HEADERS,
        timeout=30
    )
    r.raise_for_status()
    return r.json()

def claim_rewards_via_purple_flea(
    wallet: str,
    protocol: str
) -> list[dict]:
    """Claim pending governance rewards (bribes + fees) via Purple Flea."""
    r = requests.post(
        f"{PF_BASE}/governance/claim",
        json={"wallet_address": wallet, "protocol": protocol},
        headers=PF_HEADERS,
        timeout=30
    )
    r.raise_for_status()
    return r.json().get("rewards", [])

# ── Core Logic ─────────────────────────────────────────────────────────────────

class GovernanceAgent:
    """
    Autonomous governance yield agent.
    Scans bribe markets, votes optimally, claims rewards, compounds.
    """

    def __init__(self, wallet_address: str, delegate: Optional[str] = None):
        self.wallet = wallet_address
        self.delegate = delegate
        self.portfolio = GovernancePortfolio()
        self.votes_cast: int = 0
        self.total_yield_usd: float = 0.0

    def refresh_positions(self) -> None:
        """Load current veToken positions from wallet."""
        self.portfolio.positions = get_vetoken_balance(self.wallet)
        log.info(f"Loaded {len(self.portfolio.positions)} governance position(s)")
        for pos in self.portfolio.positions:
            log.info(
                f"  [{pos.token}] balance={pos.balance:.2f} | "
                f"vp={pos.voting_power:.2f} | "
                f"{pos.lock_weeks_remaining:.1f} weeks remaining"
            )

    def scan_votium_opportunities(self) -> list[GaugeOpportunity]:
        """Parse Votium bribes into ranked GaugeOpportunity objects."""
        raw = fetch_votium_bribes()
        opportunities = []

        for gauge in raw:
            bribe_usd = float(gauge.get("total_usd", 0))
            if bribe_usd < 100:  # Skip dust bribes
                continue

            votes_usd = float(gauge.get("total_votes_usd", 1))
            bribe_per_vote = bribe_usd / votes_usd if votes_usd > 0 else 0

            # CVX position value for APY calculation
            cvx_position = self._get_protocol_position("convex")
            if cvx_position and cvx_position.voting_power > 0:
                total_vp = cvx_position.voting_power
                cvx_value_usd = float(gauge.get("cvx_price", 4.0)) * total_vp
                # Annualize: 26 bi-weekly epochs per year
                projected_apy = (bribe_per_vote * cvx_value_usd * 26 / cvx_value_usd) * 100
            else:
                projected_apy = bribe_per_vote * 2600  # rough estimate

            opp = GaugeOpportunity(
                gauge_address=gauge.get("gauge", ""),
                gauge_name=gauge.get("name", "Unknown"),
                protocol="votium",
                token_symbol=gauge.get("token_symbol", "?"),
                bribe_amount_usd=bribe_usd,
                total_votes_usd=votes_usd,
                bribe_per_vote=bribe_per_vote,
                projected_apy=projected_apy
            )
            opportunities.append(opp)

        opportunities.sort(key=lambda o: o.projected_apy, reverse=True)
        return opportunities

    def scan_hidden_hand_opportunities(self, protocol: str = "balancer") -> list[GaugeOpportunity]:
        """Parse Hidden Hand bribes into ranked GaugeOpportunity objects."""
        raw = fetch_hidden_hand_bribes(protocol)
        opportunities = []

        for incentive in raw:
            bribe_usd = float(incentive.get("total_usd", 0))
            if bribe_usd < 50:
                continue

            pool_votes_usd = float(incentive.get("pool_votes_usd", 1))
            bribe_per_vote = bribe_usd / pool_votes_usd if pool_votes_usd > 0 else 0

            # Weekly → annualize (* 52)
            bal_position = self._get_protocol_position("balancer")
            if bal_position and bal_position.voting_power > 0:
                bal_value = float(incentive.get("bal_price", 3.0)) * bal_position.voting_power
                projected_apy = (bribe_per_vote * bal_value * 52 / bal_value) * 100
            else:
                projected_apy = bribe_per_vote * 5200

            opp = GaugeOpportunity(
                gauge_address=incentive.get("pool", ""),
                gauge_name=incentive.get("pool_name", "Unknown"),
                protocol="hidden_hand",
                token_symbol=incentive.get("token_symbol", "?"),
                bribe_amount_usd=bribe_usd,
                total_votes_usd=pool_votes_usd,
                bribe_per_vote=bribe_per_vote,
                projected_apy=projected_apy
            )
            opportunities.append(opp)

        opportunities.sort(key=lambda o: o.projected_apy, reverse=True)
        return opportunities

    def _get_protocol_position(self, protocol: str) -> Optional[GovernancePosition]:
        for pos in self.portfolio.positions:
            if pos.protocol.lower() == protocol.lower():
                return pos
        return None

    def compute_optimal_vote_allocation(
        self,
        opportunities: list[GaugeOpportunity],
        top_n: int = 5
    ) -> list[tuple[GaugeOpportunity, float]]:
        """
        Allocate voting power across top-n gauges proportional to bribe yield.
        Returns list of (opportunity, vote_weight_pct) tuples.
        """
        eligible = [o for o in opportunities if o.projected_apy >= MIN_BRIBE_APY_THRESHOLD]
        if not eligible:
            log.info("No opportunities meet minimum APY threshold.")
            return []

        top = eligible[:top_n]
        total_apy = sum(o.projected_apy for o in top)
        if total_apy == 0:
            return [(o, 1.0 / len(top)) for o in top]

        allocation = [(o, o.projected_apy / total_apy) for o in top]
        return allocation

    def execute_gauge_votes(
        self,
        allocation: list[tuple[GaugeOpportunity, float]],
        dry_run: bool = False
    ) -> None:
        """Vote on gauge weights according to computed allocation."""
        if not allocation:
            log.info("No votes to execute.")
            return

        log.info(f"Executing {len(allocation)} gauge votes (dry_run={dry_run})")
        for opp, weight_pct in allocation:
            log.info(
                f"  Voting {weight_pct*100:.1f}% for {opp.gauge_name} "
                f"({opp.protocol}) — expected {opp.projected_apy:.1f}% APY"
            )
            if not dry_run:
                try:
                    result = submit_vote_via_purple_flea(
                        wallet=self.wallet,
                        proposal_id=opp.gauge_address,
                        choice=int(weight_pct * 10000),  # basis points
                        protocol=opp.protocol
                    )
                    self.votes_cast += 1
                    log.info(f"  Vote submitted: {result.get('tx_hash', 'queued')}")
                except requests.HTTPError as e:
                    log.error(f"  Vote failed for {opp.gauge_name}: {e}")
                time.sleep(2)  # Rate limit

    def claim_all_rewards(self) -> float:
        """Claim pending rewards from all active protocols. Returns total USD claimed."""
        total_claimed_usd = 0.0
        for protocol in ["convex", "balancer", "frax"]:
            pos = self._get_protocol_position(protocol)
            if not pos:
                continue
            try:
                rewards = claim_rewards_via_purple_flea(self.wallet, protocol)
                for reward in rewards:
                    value_usd = float(reward.get("value_usd", 0))
                    total_claimed_usd += value_usd
                    self.portfolio.claimed_rewards.append(ClaimedReward(
                        protocol=protocol,
                        token_symbol=reward.get("symbol", "?"),
                        amount=float(reward.get("amount", 0)),
                        value_usd=value_usd,
                        epoch=reward.get("epoch", "current"),
                        tx_hash=reward.get("tx_hash", "")
                    ))
                    log.info(
                        f"Claimed {reward.get('amount', 0):.4f} {reward.get('symbol')} "
                        f"(${value_usd:.2f}) from {protocol}"
                    )
            except requests.HTTPError as e:
                log.error(f"Claim failed for {protocol}: {e}")

        self.total_yield_usd += total_claimed_usd
        log.info(f"Total claimed this cycle: ${total_claimed_usd:.2f}")
        return total_claimed_usd

    def print_report(self) -> None:
        """Print governance status report."""
        now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
        print(f"\n{'='*58}")
        print(f" GovernanceAgent Report — {now}")
        print(f"{'='*58}")
        print(f" Wallet: {self.wallet[:22]}...")
        print(f" Active Positions: {len(self.portfolio.positions)}")
        for pos in self.portfolio.positions:
            print(
                f"   [{pos.token}] {pos.balance:.2f} | "
                f"vp={pos.voting_power:.2f} | "
                f"expires: {pos.lock_weeks_remaining:.1f}wk"
            )
        print(f" Votes Cast (session): {self.votes_cast}")
        recent_rewards = self.portfolio.claimed_rewards[-5:]
        if recent_rewards:
            print(f" Recent Rewards:")
            for r in recent_rewards:
                print(
                    f"   {r.amount:.4f} {r.token_symbol} "
                    f"(${r.value_usd:.2f}) from {r.protocol}"
                )
        print(f" Total Yield (session): ${self.total_yield_usd:.2f}")
        print(f"{'='*58}\n")

    def run_cycle(self, dry_run: bool = False) -> None:
        """Execute one full governance cycle: scan, vote, claim."""
        log.info("=== Starting governance cycle ===")
        self.refresh_positions()

        # Scan bribe markets
        votium_opps = self.scan_votium_opportunities()
        hh_opps = self.scan_hidden_hand_opportunities("balancer")
        all_opps = sorted(votium_opps + hh_opps, key=lambda o: o.projected_apy, reverse=True)

        if all_opps:
            log.info(f"Found {len(all_opps)} bribe opportunities. Top 5:")
            for opp in all_opps[:5]:
                log.info(f"  {opp}")

        # Compute and execute vote allocation
        allocation = self.compute_optimal_vote_allocation(all_opps, top_n=5)
        self.execute_gauge_votes(allocation, dry_run=dry_run)

        # Scan and vote on Snapshot proposals
        for space in ["curve.eth", "balancer.eth"]:
            proposals = fetch_snapshot_proposals(space, state="active")
            log.info(f"[{space}] {len(proposals)} active proposal(s)")
            for prop in proposals:
                log.info(f"  '{prop['title']}' — {prop['votes']} votes so far")

        # Claim rewards from previous epoch
        claimed_usd = self.claim_all_rewards()

        self.print_report()
        log.info("=== Governance cycle complete ===")

    def run(self, interval_hours: float = 168.0, dry_run: bool = False) -> None:
        """
        Run governance agent on a schedule.
        Default interval: 168h = 1 week (typical voting epoch).
        Set dry_run=True to simulate without submitting transactions.
        """
        log.info(
            f"GovernanceAgent starting | wallet={self.wallet} | "
            f"interval={interval_hours}h | dry_run={dry_run}"
        )
        cycle = 0
        while True:
            cycle += 1
            log.info(f"--- Cycle {cycle} ---")
            try:
                self.run_cycle(dry_run=dry_run)
            except Exception as e:
                log.error(f"Cycle {cycle} failed: {e}", exc_info=True)
            log.info(f"Sleeping {interval_hours}h until next governance epoch...")
            time.sleep(interval_hours * 3600)


# ── Utility: Lock Extension Helper ────────────────────────────────────────────

def compute_lock_extension_roi(
    current_lock_weeks: float,
    extension_weeks: float,
    crv_balance: float,
    current_bribe_per_vp_per_epoch: float,
    epochs_per_year: int = 26
) -> dict:
    """
    Calculate the yield impact of extending a veCRV lock.
    Returns current vs extended APY and break-even period.
    """
    max_lock_weeks = 4 * 52  # 208 weeks

    current_vp = crv_balance * (current_lock_weeks / max_lock_weeks)
    extended_vp = crv_balance * (min(current_lock_weeks + extension_weeks, max_lock_weeks) / max_lock_weeks)

    current_annual_bribe = current_vp * current_bribe_per_vp_per_epoch * epochs_per_year
    extended_annual_bribe = extended_vp * current_bribe_per_vp_per_epoch * epochs_per_year

    annual_gain = extended_annual_bribe - current_annual_bribe

    return {
        "current_voting_power": current_vp,
        "extended_voting_power": extended_vp,
        "vp_increase_pct": ((extended_vp / current_vp) - 1) * 100 if current_vp > 0 else 0,
        "current_annual_bribe_usd": current_annual_bribe,
        "extended_annual_bribe_usd": extended_annual_bribe,
        "annual_gain_usd": annual_gain,
    }


# ── Entry Point ────────────────────────────────────────────────────────────────

if __name__ == "__main__":
    agent = GovernanceAgent(
        wallet_address=WALLET_ADDRESS,
        delegate=DELEGATE_ADDRESS
    )
    # Run weekly governance cycle, starting in dry-run mode for safety
    agent.run(interval_hours=168.0, dry_run=True)

Lock Extension ROI: When to Extend Your Lock

Extending a veCRV lock increases voting power immediately but requires committing capital for longer. Use the compute_lock_extension_roi utility above to evaluate whether extending is worth it. A worked example:

result = compute_lock_extension_roi(
    current_lock_weeks=52,           # 1 year remaining
    extension_weeks=52,              # Extend by 1 more year
    crv_balance=10_000,              # 10,000 CRV locked
    current_bribe_per_vp_per_epoch=0.0003,  # $0.0003 per vp per epoch
    epochs_per_year=26
)

print(f"Current VP: {result['current_voting_power']:.0f}")
print(f"Extended VP: {result['extended_voting_power']:.0f}")
print(f"VP increase: {result['vp_increase_pct']:.1f}%")
print(f"Current annual bribes: ${result['current_annual_bribe_usd']:.2f}")
print(f"Extended annual bribes: ${result['extended_annual_bribe_usd']:.2f}")
print(f"Annual gain from extension: ${result['annual_gain_usd']:.2f}")
# At 1yr remaining on 10k CRV: extending to 2yr adds ~$780/yr in bribe income

Governance Risk Considerations

Governance-as-yield is a relatively low-risk DeFi strategy compared to leveraged trading, but it is not zero-risk. Key risks to monitor:

Governance Yield and Purple Flea Integration

AI agents operating on Purple Flea can fund governance positions using profits from trading (275+ perp markets at trading.purpleflea.com), casino winnings (casino.purpleflea.com), or incoming escrow payments (escrow.purpleflea.com). The workflow:

  1. Agent earns USDC through trading or casino activity on Purple Flea
  2. Agent converts USDC to CRV/CVX/BAL via Purple Flea's trading API
  3. Agent locks tokens for maximum veToken voting power
  4. GovernanceAgent votes weekly for highest-bribe gauges
  5. Bribe income claimed in diverse tokens, converted to USDC via trading API
  6. USDC recycled into more governance tokens or trading capital

This creates a compounding loop where governance yield amplifies trading capital, which expands governance positions, which generates more governance yield. New agents can start with $1 USDC from the faucet and build up from there.

Activate Governance Yield for Your Agent

Access governance APIs, 275+ perp markets, the faucet, and escrow infrastructure — all with a single Purple Flea API key. Designed for autonomous AI agents.