DAO Governance Voting Agent with WaaP CLI
What are we cooking?
An AI-powered Node.js agent that participates in DAO governance on Snapshot using the WaaP CLI.
This recipe demonstrates how an agent can monitor proposals, analyze them with an LLM, and cast votes — all without a private key ever touching disk or memory. And because Snapshot votes are gasless (signed off-chain, stored on IPFS), your agent doesn’t need any ETH or gas tokens to participate.
The agent will:
- Fetch active governance proposals from a Snapshot space (ENS DAO).
- Send proposal text to Claude for analysis and a vote recommendation.
- Sign an EIP-712 vote using
waap-cli sign-typed-data(2PC-MPC, no raw key). - Submit the signed vote to the Snapshot sequencer.
- Verify the vote was recorded.
Key Components
- WaaP CLI — For securely signing EIP-712 vote payloads behind 2PC-MPC.
- Snapshot GraphQL API — For discovering proposals and verifying votes.
- Snapshot Sequencer — For submitting signed votes (gasless, off-chain).
- Claude API — For analyzing proposal text and recommending a vote.
- ENS DAO (
ens.eth) — The target governance space (swap for your own DAO).
Project Setup
Create a new project and install dependencies:
mkdir waap-snapshot-agent && cd waap-snapshot-agent
npm init -y
npm install @anthropic-ai/sdk
npm install -g @human.tech/waap-cli@latestSet your Anthropic API key:
export ANTHROPIC_API_KEY="sk-ant-..."Setup a WaaP wallet for your agent
You can use your email for multiple agent wallets by appending a suffix with + (e.g., youremail+gov-agent@example.com).
# Create an account for your agent
waap-cli signup --email youremail+gov-agent@example.com --password '12345678!'
# Or login to an existing one
waap-cli login --email youremail+gov-agent@example.com --password '12345678!'
# Get the wallet address
waap-cli whoamiNo funding required — Snapshot votes are gasless. Your agent wallet just needs to sign, not pay gas.
Note: To have actual voting power on ENS DAO, this wallet needs to hold (or be delegated) ENS tokens. The signing flow works regardless — you can adapt the
SPACEvariable to any Snapshot space where your agent holds governance tokens.
The Recipe Workflow
1. Fetching Active Proposals (Snapshot GraphQL API)
Snapshot exposes a public GraphQL API. We query it directly for active proposals in our target space:
const SNAPSHOT_GRAPHQL = "https://hub.snapshot.org/graphql";
const SPACE = "ens.eth";
async function getActiveProposals() {
console.log(`Fetching active proposals for ${SPACE}...`);
const query = `
query {
proposals(
first: 5,
where: { space: "${SPACE}", state: "active" },
orderBy: "created",
orderDirection: desc
) {
id
title
body
choices
start
end
state
type
space { id name }
}
}
`;
const response = await fetch(SNAPSHOT_GRAPHQL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
const { data } = await response.json();
const proposals = data.proposals;
proposals.forEach((p, i) => {
console.log(`[${i}] ${p.title}`);
console.log(` Choices: ${p.choices.join(", ")}`);
console.log(` Ends: ${new Date(p.end * 1000).toISOString()}`);
});
return proposals;
}2. Analyzing Proposals with Claude
This is where it becomes a genuinely agentic workflow. Instead of blindly voting, the agent sends the proposal text to Claude for analysis:
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
async function analyzeProposal(proposal) {
console.log(`Analyzing proposal: "${proposal.title}"...`);
const message = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [
{
role: "user",
content: `You are a DAO governance analyst. Analyze this proposal and recommend how to vote.
**Proposal:** ${proposal.title}
**Description:**
${proposal.body.slice(0, 3000)}
**Choices:** ${proposal.choices.map((c, i) => `${i + 1}. ${c}`).join(", ")}
Respond with:
1. A brief summary (2-3 sentences)
2. Your recommended vote (the choice NUMBER, 1-indexed)
3. Your reasoning (2-3 sentences)
Format your response as:
SUMMARY: ...
VOTE: <number>
REASONING: ...`,
},
],
});
const responseText = message.content[0].text;
console.log(responseText);
// Parse the recommended choice
const voteMatch = responseText.match(/VOTE:\s*(\d+)/);
const choice = voteMatch ? parseInt(voteMatch[1]) : 1;
const reasonMatch = responseText.match(/REASONING:\s*([\s\S]*)/);
const reason = reasonMatch ? reasonMatch[1].trim().slice(0, 140) : "";
return { choice, reason };
}3. Signing the Vote with WaaP CLI (EIP-712)
Snapshot votes use EIP-712 structured signing — the same standard used by Polymarket orders, Permit2 approvals, and other off-chain signature schemes. Instead of importing ethers and exposing a private key, we shell out to waap-cli sign-typed-data:
import { execSync } from "child_process";
function buildVoteTypedData(voterAddress, proposal, choice, reason) {
return {
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" },
],
Vote: [
{ name: "from", type: "address" },
{ name: "space", type: "string" },
{ name: "timestamp", type: "uint64" },
{ name: "proposal", type: "bytes32" },
{ name: "choice", type: "uint32" },
{ name: "reason", type: "string" },
{ name: "app", type: "string" },
{ name: "metadata", type: "string" },
],
},
domain: {
name: "snapshot",
version: "0.1.4",
},
primaryType: "Vote",
message: {
from: voterAddress,
space: "ens.eth",
timestamp: Math.floor(Date.now() / 1000),
proposal: proposal.id,
choice: choice,
reason: reason,
app: "waap-snapshot-agent",
metadata: "{}",
},
};
}
function signWithWaaP(typedData) {
const dataString = JSON.stringify(typedData).replace(/'/g, "'\\''");
// WaaP CLI signs behind the 2PC-MPC enclave — no private key exposure
const signature = execSync(
`waap-cli sign-typed-data --data '${dataString}'`,
{ encoding: "utf-8" }
);
return signature.trim(); // Returns 0x...
}4. Submitting the Vote to Snapshot
With the signature in hand, we submit an envelope to the Snapshot sequencer. No API key or HMAC headers needed — just the address, signature, and typed data:
const SNAPSHOT_SEQUENCER = "https://seq.snapshot.org";
async function submitVote(address, signature, typedData) {
console.log("Submitting vote to Snapshot sequencer...");
const envelope = {
address,
sig: signature,
data: {
domain: typedData.domain,
types: { Vote: typedData.types.Vote },
message: typedData.message,
},
};
const response = await fetch(SNAPSHOT_SEQUENCER, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(envelope),
});
const result = await response.json();
if (result.id) {
console.log(`Vote submitted! Receipt: ${result.id}`);
} else {
console.error("Vote submission failed:", result);
}
return result;
}5. Verifying the Vote
Query the Snapshot GraphQL API to confirm the vote was recorded:
async function verifyVote(proposalId, voter) {
const query = `
query {
votes(
where: { proposal: "${proposalId}", voter: "${voter}" }
) {
id
voter
choice
reason
created
}
}
`;
const response = await fetch(SNAPSHOT_GRAPHQL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query }),
});
const { data } = await response.json();
if (data.votes.length > 0) {
console.log("Vote confirmed:", data.votes[0]);
} else {
console.log("Vote not yet indexed. Check Snapshot UI in a few minutes.");
}
}6. Running the Agent
Putting it all together:
// index.js
async function runAgent() {
// Get wallet address from WaaP
const whoamiOutput = execSync("waap-cli whoami", { encoding: "utf-8" }).trim();
const walletAddress = (
whoamiOutput.match(/0x[a-fA-F0-9]{40}/) || [whoamiOutput]
)[0];
console.log(`Agent wallet: ${walletAddress}`);
// Step 1: Fetch active proposals
const proposals = await getActiveProposals();
if (proposals.length === 0) return;
// Step 2: Analyze with Claude
const proposal = proposals[0];
const { choice, reason } = await analyzeProposal(proposal);
console.log(`Decision: Vote choice ${choice} ("${proposal.choices[choice - 1]}")`);
// Step 3: Build and sign the vote
const typedData = buildVoteTypedData(walletAddress, proposal, choice, reason);
const signature = signWithWaaP(typedData);
console.log(`Signature: ${signature.slice(0, 20)}...`);
// Step 4: Submit
await submitVote(walletAddress, signature, typedData);
// Step 5: Verify
await verifyVote(proposal.id, walletAddress);
}
runAgent().catch(console.error);Run it:
node index.jsNext Steps
Now that your agent can analyze proposals and vote autonomously:
- Cron scheduling — Run
node index.json a daily cron to catch new proposals. - Multi-DAO monitoring — Extend the
SPACEvariable to an array and monitor multiple DAOs (Uniswap, Aave, Arbitrum, etc.). - Policy guardrails — Use
waap-cli policy set --daily-spend-limit 100to cap agent spending per day. - Custom voting strategies — Tune the Claude prompt with your DAO’s values, constitution, or delegate platform to align votes with your governance philosophy.
- Vote delegation tracking — Query the DAO’s token contract to check delegated voting power before casting.
Related
- WaaP CLI and Skills — CLI command reference and agent workflows
- Permission Tokens — Pre-approved spending scopes for automated flows
- Polymarket Agent — Another recipe showing EIP-712 signing for DeFi trading