Metagovernance automation bot for Nouns DAO. Mirrors Nouns proposals to a Snapshot space so your community can vote, then automatically executes the winning vote on-chain through a Safe multisig.
- Polls for new Nouns DAO proposals via a GraphQL subgraph
- Creates a corresponding Snapshot proposal in your space (For / Against / Abstain)
- Waits for the Snapshot vote to close
- Executes the winning vote on-chain through your Safe wallet
- Node.js 18+
- A Safe multisig on Ethereum mainnet
- The Safe must hold Nouns voting power (be a delegate)
- Threshold must be 1
- A bot wallet (EOA) added as a signer on the Safe
- Funded with ~0.1 ETH for gas
- A Snapshot space configured for your organization
git clone <your-repo-url>
cd metagov
npm install
cp .env.example .envEdit .env -- you only need to fill in 3 values:
BOT_PRIVATE_KEY=your_private_key_here
SAFE_ADDRESS=0xYourSafeAddress
SNAPSHOT_SPACE_ID=yourdao.ethTest it first:
DRY_RUN=true npm run devRun for real:
npm run build
npm startEverything is controlled via .env. Only 3 values are required -- everything else has sensible defaults.
| Variable | Description |
|---|---|
BOT_PRIVATE_KEY |
Bot wallet private key (with or without 0x prefix) |
SAFE_ADDRESS |
Your Safe multisig address |
SNAPSHOT_SPACE_ID |
Your Snapshot space (e.g. mydao.eth) |
| Variable | Default | Description |
|---|---|---|
ETHEREUM_RPC_URL |
Public LlamaRPC | Any Ethereum mainnet RPC |
NOUNS_GRAPHQL_ENDPOINT |
Public Goldsky subgraph | Nouns subgraph URL. Ask your indexer provider for a URL if needed |
NOUNS_DAO_ADDRESS |
Nouns DAO mainnet | Nouns DAO contract |
SAFE_API_KEY |
(none) | Safe Transaction Service JWT. Optional -- only shows txs in Safe web UI |
CLIENT_ID |
0 |
Nouns client incentives ID (register at vote.wtf/clients) |
VOTING_DURATION_DAYS |
5 |
How long Snapshot votes stay open |
NO_VOTES_ACTION |
abstain |
What to do if nobody votes: abstain or skip |
MIN_PROPOSAL_ID |
0 |
Ignore proposals before this ID |
LOOKBACK_DAYS |
7 |
How far back to scan on startup |
PROPOSAL_LINK_TEMPLATE |
https://nouns.wtf/vote/{id} |
Link in Snapshot proposal body + discussion |
PROPOSAL_POLL_MINUTES |
1 |
How often to check for new proposals |
VOTE_POLL_MINUTES |
5 |
How often to check for closed votes |
DATA_DIR |
data |
Where to store state files (useful for Docker volumes) |
MAX_GAS_PRICE_GWEI |
100 |
Max gas price before deferring a vote |
MAX_RETRIES |
3 |
Retry attempts for failed vote execution |
DRY_RUN |
false |
Log actions without executing |
See .env.example for the full list with comments.
The bot must run continuously. State is persisted to DATA_DIR (default: data/) -- make sure this survives restarts.
- Push this repo to GitHub
- Go to railway.com and create a new project
- Select "Deploy from GitHub repo" and connect this repo
- Go to Variables and add your 3 required env vars:
BOT_PRIVATE_KEY,SAFE_ADDRESS,SNAPSHOT_SPACE_ID - Add a Volume mounted at
/app/data(this persists state across deploys) - Railway auto-detects Node.js, runs
npm run buildandnpm start - Set
DRY_RUN=truefirst to verify everything works, then remove it
Railway usage for this bot is very light -- expect under $2/month.
docker build -t metagov .
docker run --env-file .env -v metagov-data:/app/data metagovnpm run build
pm2 start dist/index.js --name metagovsrc/
index.ts # Main loop + cron scheduling
config.ts # Environment-based configuration
listeners/
nounsProposals.ts # Polls Nouns subgraph for new proposals
services/
snapshot.ts # Create/cancel Snapshot proposals, read results
safeVoting.ts # Execute votes through Safe + format vote reasons
stateStore.ts # Local JSON file persistence
utils/
wallet.ts # Ethers provider + wallet setup
When the bot executes a vote on-chain, it includes a detailed reason showing how your community voted on Snapshot. The on-chain vote reason looks like this:
**FOR 12 VOTES**
**toady.eth** | *"Great proposal, fully support this"*
**nounslover.eth**
**0xABCD...1234**
**AGAINST 3 VOTES**
**someguy.eth** | *"Too expensive"*
**ABSTAIN 0 VOTES**
Voter names are resolved in order: Snapshot profile name > ENS name > truncated address. Reasons are included when voters leave them on Snapshot.
- Gas refunds: Gas is paid by the bot wallet and Nouns DAO refunds it back to the bot.
- Safe API key: Purely optional. It only records transactions in Safe's web UI for visibility. All execution uses the bot's private key directly.
- Deduplication: The bot has triple-layer deduplication (memory + Snapshot API + local state file) to prevent re-posting proposals across restarts.
- Private key format: Works with or without the
0xprefix -- paste it however you have it.