Automated governance voting system for Cosmos SDK chains using the Authz module.
This application automatically votes "ABSTAIN" on all active governance proposals across Cosmos SDK chains. It uses the Authz module to vote on behalf of validators, allowing a single wallet to manage voting across multiple validator addresses.
- Authz-based voting - One wallet authorized to vote on behalf of validators
- RPC/gRPC polling - Automatically discovers active proposals
- Idempotent voting - Checks both local state and on-chain to prevent duplicate votes
- Automatic restart - Exponential backoff retry logic for resilient operation
- Comprehensive logging - Detailed logs for all voting decisions (voted, skipped, failed)
- Custom chain support - Works with any Cosmos SDK chain (requires chain-specific address prefix configuration)
- Full observability - Prometheus metrics, health checks, and structured logging
- Dry-run mode - Test without actually submitting votes
- Go 1.21+
- Access to a Cosmos SDK chain (RPC and gRPC endpoints)
- A wallet with funds for transaction fees
- Authz grant from the validator address(es) you want to vote on behalf of
git clone <your-repo-url>
cd cosmos-auto-voter
# Build the binary (outputs to bin/autovoter)
make build
# Or install to /usr/local/bin (requires sudo)
make installdocker build -t cosmos-auto-voter:latest .All configuration is via environment variables:
| Variable | Description | Example |
|---|---|---|
CHAIN_ID |
Cosmos chain ID | osmosis-1, cosmoshub-4, autovoter-1 |
RPC_ENDPOINT |
RPC endpoint URL | http://localhost:26657, https://rpc.osmosis.zone |
GRPC_ENDPOINT |
gRPC endpoint (host:port, no http://) | localhost:9090, grpc.osmosis.zone:9090 |
GRANTER_ADDRESS |
Validator address that granted authz permission | cosmos1abc..., osmo1xyz... |
WALLET_MNEMONIC or WALLET_MNEMONIC_FILE |
BIP39 mnemonic phrase (24 words) or path to file containing it | See Wallet Configuration below |
GAS_PRICE |
Gas price with denomination | 10000ncheq, 0.025uosmo, 0.005uatom |
| Variable | Default | Description |
|---|---|---|
CHAIN_NAME |
(empty) | Human-readable chain name for logging/metrics |
POLL_INTERVAL |
60s |
How often to check for new proposals (supports s, m, h). Examples: 30s, 5m, 1h |
GAS_LIMIT |
200000 |
Maximum gas to use per transaction |
STATE_DB |
/data/votes.db |
Path to SQLite database (auto-created, but parent directory must exist) |
HTTP_PORT |
8080 |
Port for health check endpoints |
METRICS_PORT |
9090 |
Port for Prometheus metrics endpoint |
DRY_RUN |
false |
Test mode - polls and logs but doesn't submit votes (set to true for testing) |
LOG_LEVEL |
info |
Logging verbosity: debug, info, warn, error |
LOG_FORMAT |
json |
Log output format: json (production) or console (development) |
WALLET_MNEMONIC vs WALLET_MNEMONIC_FILE:
- Use
WALLET_MNEMONICto pass the mnemonic directly as an environment variable (not recommended for production) - Use
WALLET_MNEMONIC_FILEto specify a file path containing the mnemonic (recommended for security) - Example file:
echo "word1 word2 ... word24" > /secure/path/mnemonic.txt && chmod 600 /secure/path/mnemonic.txt
POLL_INTERVAL Format:
- Supports Go duration format:
s(seconds),m(minutes),h(hours) - Examples:
30s- Poll every 30 seconds (aggressive, higher RPC load)60s- Poll every minute (default, balanced)5m- Poll every 5 minutes (conservative, lower RPC load)1h- Poll every hour (minimal load, slower vote response)
- Recommendation: Use
60s(1 minute) for most chains. Increase if RPC rate-limiting occurs.
STATE_DB:
- The SQLite database file will be automatically created on first run
- Important: The parent directory must exist before starting
- Example: If using
/data/votes.db, runmkdir -p /datafirst - The database tracks which proposals have been voted on to prevent duplicates
GAS_PRICE:
- Must match your chain's minimum gas price requirement
- Check chain documentation or node config for
minimum-gas-prices - If votes fail with "insufficient fee" errors, increase this value
- Format:
<amount><denom>where denom matches your chain's gas token
The auto-voter supports the following commands:
# Show help menu
./autovoter --help
./autovoter help
# Show version
./autovoter version
# Start the auto-voter
./autovoter startRunning ./autovoter without any command will display the help menu.
Before running the auto-voter, you need to grant it permission to vote on behalf of your validator:
# Grant voting permission (using your chain's CLI)
<chain-cli> tx authz grant <auto-voter-address> generic \
--msg-type /cosmos.gov.v1.MsgVote \
--from <validator-address> \
--chain-id <chain-id>export CHAIN_ID=test
export CHAIN_NAME=test
export RPC_ENDPOINT=http://localhost:26657
export GRPC_ENDPOINT=localhost:9090
export GRANTER_ADDRESS=cheqd193lqq9x62wqpuxnsczxyxh8tzsgas48rhhqhln
export WALLET_MNEMONIC_FILE=/path/to/mnemonic.txt
export GAS_PRICE=10000ncheq
export STATE_DB=/path/to/votes.db
export METRICS_PORT=9091
export DRY_RUN=true
export LOG_FORMAT=console
export LOG_LEVEL=info
./autovoter startOnce you've verified it works in dry-run mode:
export DRY_RUN=false
./autovoter startWhile the app is running, you can:
Check Health:
curl http://localhost:8080/healthCheck Voting Health (tests chain connection):
curl http://localhost:8080/health/votingView Prometheus Metrics:
curl http://localhost:9090/metricsView Status Page:
curl http://localhost:8080/statusQuery the chain to verify your vote was submitted:
# Check a specific vote
<chain-cli> query gov vote <proposal-id> <voter-address> \
--node <rpc-endpoint> \
--chain-id <chain-id>
# Example for cheqd:
cheqd query gov vote 1 cheqd1dgkrgxmsuqtxtyxq0la88ze9er6lfnlw8s7ttf \
--node http://localhost:26657 \
--chain-id testThe app supports any Cosmos SDK chain, but you need to add the chain's address prefix configuration:
- Edit
cmd/autovoter/main.goand add your chain to thegetAddressPrefixfunction:
func getAddressPrefix(chainID string) string {
prefixMap := map[string]string{
// ... existing chains ...
"your-chain-1": "yourprefix",
}
// ...
}- Edit
internal/voter/voter.goand add togetAddressPrefixFromChain:
func getAddressPrefixFromChain(chainID string) string {
prefixMap := map[string]string{
// ... existing chains ...
"your-chain": "yourprefix",
}
// ...
}- Edit
pkg/cosmos/wallet.goand add togetAddressPrefix:
func getAddressPrefix(chainID string) string {
prefixMap := map[string]string{
// ... existing chains ...
"your-chain-1": "yourprefix",
}
// ...
}- Rebuild the app:
make builddocker run -d \
-e CHAIN_ID=test \
-e RPC_ENDPOINT=http://localhost:26657 \
-e GRPC_ENDPOINT=localhost:9090 \
-e GRANTER_ADDRESS=cheqd1abc... \
-e WALLET_MNEMONIC="your mnemonic here" \
-e GAS_PRICE=10000ncheq \
-e DRY_RUN=false \
-v /path/to/data:/data \
-p 8080:8080 \
-p 9090:9090 \
cosmos-auto-voter:latestSee example.nomad.hcl for a complete Nomad job specification.
nomad job run \
-var="chain_id=test" \
-var="rpc_endpoint=http://localhost:26657" \
-var="grpc_endpoint=localhost:9090" \
-var="granter_address=cheqd1abc..." \
-var="gas_price=10000ncheq" \
example.nomad.hclThe application exposes the following metrics on the configured metrics port (default 9091):
-
cosmos_voter_votes_submitted_total{chain_id, result}- Total successful votes submitted- Labels:
result=success
- Labels:
-
cosmos_voter_votes_skipped_total{chain_id, reason}- Total votes skipped (already voted)- Labels:
reason=already_voted_local,reason=already_voted_onchain
- Labels:
-
cosmos_voter_votes_failed_total{chain_id, reason}- Total failed vote attempts⚠️ - Labels:
reason=insufficient_balance- Wallet has no fundsreason=insufficient_fees- Gas price too lowreason=connection_error- Network issuesreason=authz_error- Authz grant missing/expiredreason=unknown- Other errors
- Labels:
-
cosmos_voter_proposals_without_vote{chain_id}- Current proposals without successful votes⚠️ - Gauge showing count of proposals that failed to vote in last poll
- Use this in Zabbix to alert when > 0
cosmos_voter_proposals_checked_total{chain_id, status}- Total proposals checkedcosmos_voter_poll_errors_total{chain_id, error_type}- Total polling errorscosmos_voter_last_successful_poll_timestamp{chain_id}- Last successful poll timestampcosmos_voter_active_proposals{chain_id}- Current active proposals countcosmos_voter_wallet_balance{chain_id, denom}- Current wallet balancecosmos_voter_health_status{chain_id}- Health status (1=healthy, 0=unhealthy)cosmos_voter_poll_duration_seconds{chain_id}- Time to poll proposalscosmos_voter_vote_latency_seconds{chain_id}- Time to submit vote
Critical Alerts:
-
Alert when
cosmos_voter_proposals_without_vote > 0- Indicates active proposals that don't have votes
- Action: Check logs, verify wallet balance, check authz grants
-
Alert when
cosmos_voter_votes_failed_totalincreases- Check the
reasonlabel to determine cause - Action:
insufficient_balance: Add funds to walletinsufficient_fees: IncreaseGAS_PRICEauthz_error: Renew authz grantconnection_error: Check RPC/gRPC endpoints
- Check the
-
Alert when
cosmos_voter_wallet_balanceis low- Set threshold based on expected voting frequency
- Action: Top up wallet before it runs out
Example Zabbix Item Configuration:
Item: cosmos_voter_proposals_without_vote{chain_id="osmosis-1"}
Trigger: last()>0
Severity: High
Message: Active proposals without votes on osmosis-1! Check auto-voter logs.
The application includes robust error handling with automatic restart capabilities:
- Exponential backoff: Starts with 5 second backoff, doubles on each retry, capped at 5 minutes
- Max retries: Will attempt up to 10 restarts before giving up
- Context-aware: Respects shutdown signals during retry loops
- Detailed logging: Logs every restart attempt with retry count and backoff duration
When the voter encounters an error, it will:
- Log the error with full context
- Wait for the calculated backoff period
- Automatically restart the voting process
- Continue retrying until successful or max retries reached
The application provides comprehensive logging for all voting decisions:
Vote Status Logs:
- ✅
"vote submitted successfully"- Vote was successfully broadcast to the chain - ⏭️
"vote skipped - already voted (local state)"- Vote was previously submitted (found in local database) - ⏭️
"vote skipped - already voted on-chain"- Vote was previously submitted (found on chain, updates local state) - ℹ️
"no active proposals to vote on"- No proposals in voting period ⚠️ "DRY RUN MODE - vote not actually submitted"- Dry-run mode active
Error Logs:
- ❌
"failed to process proposal"- Error processing specific proposal (continues with others) - ❌
"insufficient wallet balance"- Wallet has insufficient funds to pay transaction fees - ❌
"poll failed"- Error during proposal polling (triggers retry) - ❌
"voter crashed"- Fatal error (triggers automatic restart)
Restart Logs:
- 🔄
"starting voter"- Voter starting (includes retry count) - 🔄
"restarting voter after backoff"- Automatic restart triggered with backoff duration - 🛑
"max retries reached, giving up"- Fatal: exceeded retry limit
INFO starting voter {"retry_count": 0}
INFO polling for active proposals
INFO found active proposals {"count": 2}
INFO submitting abstain vote {"proposal_id": 1, "title": "Upgrade v2.0"}
INFO vote submitted successfully {"proposal_id": 1}
INFO vote skipped - already voted (local state) {"proposal_id": 2}
INFO no active proposals to vote on
ERROR voter crashed {"error": "connection timeout", "retry_count": 1}
WARN restarting voter after backoff {"backoff": "5s", "retry_count": 1}
Check the logs:
# If running locally
./autovoter
# If running in Docker
docker logs <container-id>Common issues:
- Authz grant doesn't exist or has expired
- Wallet has insufficient funds for transaction fees
- Gas price too low (check chain's minimum gas price)
- Wrong address prefix for the chain
If you see an error like:
got: 200000000ncheq required: 2000000000ncheq, minGasPrice: 10000ncheq
Increase your GAS_PRICE environment variable to match the chain's minimum gas price:
export GAS_PRICE=10000ncheq # Match the minGasPrice from the errorThe app checks wallet balance before attempting to vote. If you see errors like:
ERROR insufficient wallet balance {"error": "insufficient balance: have 0ncheq, need 2000000000ncheq"}
ERROR failed to process proposal {"proposal_id": 1, "error": "cannot vote due to insufficient balance"}
Actions to take:
-
Check your wallet balance:
<chain-cli> query bank balances <wallet-address> --node <rpc-endpoint>
-
Send funds to the wallet:
- The wallet address is logged at startup:
"wallet created" {"address": "cheqd1..."} - Send enough tokens to cover multiple votes (calculate: gas_price × gas_limit × expected_votes)
- Example: For 10 votes with
GAS_PRICE=10000ncheqandGAS_LIMIT=200000:- Required: 10000 × 200000 × 10 = 20,000,000,000 ncheq (20 billion ncheq)
- The wallet address is logged at startup:
-
Monitor balance:
- Set up alerts when balance drops below a threshold
- The app will log balance errors clearly when funds run out
- App will continue retrying and will resume voting once funds are added
Note: The app performs a balance check before each vote attempt. If balance is insufficient, it will:
- Log a clear error with current balance vs required amount
- Skip the vote for that proposal
- Continue operating and retry on next poll
- Resume voting automatically once funds are added
If you see errors about "hrp does not match bech32 prefix":
- Ensure your chain is added to all three prefix mapping functions (see "Adding Support for New Chains")
- Rebuild the application after making changes
- Verify the chain ID matches exactly
Verify RPC is reachable:
curl <rpc-endpoint>/statusCheck gRPC:
grpcurl <grpc-endpoint> list-
Wallet compromise = full voting control
- Store mnemonics securely (use Vault, hardware wallet, or encrypted storage)
- Never commit mnemonics to version control
- Rotate keys regularly
- Monitor for unauthorized votes
-
Authz scope
- Grant ONLY
cosmos.gov.v1.MsgVotepermission - Set expiration times on grants
- Automate renewal process
- Grant ONLY
-
Transaction fees
- Monitor wallet balance
- Set up alerts for low balance
- Automate top-ups
- Run in dry-run mode first to verify configuration
- Start with one chain and monitor for 24h before expanding
- Document your governance policy
- Set up alerting for errors and health check failures
- Regular security audits
- Use separate wallets for different chains/validators
MIT
For issues, questions, or contributions, please open an issue on GitHub.