| name | dao-integration |
| description | Use when adding a new DAO to the Anticapture platform. Covers all five components — indexer, API, gateway, dashboard, and enum sync — with a step-by-step checklist. |
DAO Integration Guide
Use This Skill When
- You are adding a new DAO to the platform.
- You need to understand what files to create/modify for a new DAO.
- You are debugging why a DAO is missing from a component.
Prerequisites
Before starting, gather these details about the DAO:
- DAO ID: Short uppercase identifier (e.g.
ENS, UNI, AAVE)
- Chain: Which EVM chain (mainnet, arbitrum, optimism, etc.)
- Token contract: Address, decimals, deploy block, type (ERC20/ERC721)
- Governor contract: Address, deploy block, governor type (Compound-style, Azorius, etc.)
- Timelock contract: Address (if applicable)
- Treasury addresses: DAO multisigs, vesting contracts
- CEX/DEX/Lending addresses: Known exchange wallets, LP pools, lending contracts holding the token
- Governance rules: Voting delay, period, quorum calculation, cancel function, vote logic
- CoinGecko token ID: The slug from the CoinGecko URL (e.g.
"uniswap", "fluid") — ask the user
- Event relevance thresholds: LOW/MEDIUM/HIGH token amounts for feed relevance filtering — ask the user
Integration Checklist
The integration touches 5 components. Work through them in order.
Step 0: Governance Architecture Discovery
This step is mandatory before writing any code. Many DAOs have non-standard governance architectures. Skipping this step risks building an integration that misses the core governance mechanism.
0a. Verify the governor's voting token
Call governor.token() on-chain to find what contract the governor actually uses for voting power:
cast call <GOVERNOR_ADDRESS> "token()(address)" --rpc-url <RPC_URL>
Compare the result against the token address the user provided. They may differ — e.g. the governor may point to a vote-escrow wrapper, not the ERC20.
0b. Classify the voting token
Check what the voting token actually is:
| Check | Command | What it tells you |
|---|
| Is it the same as the ERC20? | Compare addresses | If different, there's an intermediary |
Does it have delegates()? | cast call <TOKEN> "delegates(address)(address)" <ZERO_ADDR> | If reverts → no delegation, voting power comes from elsewhere |
Does it emit DelegateChanged? | Check ABI on block explorer | If missing → delegatedSupply and accountPower will be empty |
| Is it a vote-escrow (veToken)? | Check contract name/source on block explorer | veTokens use lock-based voting power with Deposit/Withdraw events |
| Is it a wrapper? | Check if it references another contract | Wrappers (like wveOLAS) proxy reads to an underlying contract |
0c. Determine integration scope
Based on the findings, classify the integration:
| Architecture | Token events available | Delegation tracking | Example |
|---|
| Standard ERC20Votes | Transfer, DelegateChanged, DelegateVotesChanged | Full | ENS, UNI, OBOL |
| Plain ERC20 + veToken | Transfer only (on ERC20); Deposit/Withdraw (on veToken) | Requires custom veToken indexing | OLAS |
| ERC721 (NFT) | Transfer (minting = delegation) | Via transfer events | NOUNS |
| Multi-token | Transfer + delegation per token | Aggregated across tokens | AAVE |
0d. Document findings in INTEGRATION.md
Create apps/indexer/src/indexer/<dao>/INTEGRATION.md documenting:
- Architecture: What contracts exist and how they connect
- What's integrated: Which events and metrics are covered
- What's pending: Gaps that need follow-up work (e.g. veToken indexing)
- Addresses provided vs discovered: Any discrepancies from user-provided info
This file is the source of truth for the integration status of each DAO. See the template below in "INTEGRATION.md Template".
Step 1: Enum Sync
Add the DAO ID to the enum in all three locations (they must match):
| File | Package |
|---|
apps/indexer/src/lib/enums.ts | Indexer |
apps/api/src/lib/enums.ts | API |
apps/dashboard/shared/types/daos.ts | Dashboard |
export enum DaoIdEnum {
NEW_DAO = "NEW_DAO",
}
Step 2: Indexer
2a. Constants (apps/indexer/src/lib/constants.ts)
Add entry to CONTRACT_ADDRESSES[DaoIdEnum.NEW_DAO]:
[DaoIdEnum.NEW_DAO]: {
blockTime: 12,
token: {
address: "0x..." as Address,
decimals: 18,
startBlock: 12345678,
},
governor: {
address: "0x..." as Address,
startBlock: 12345678,
},
},
Add entries to TreasuryAddresses, CEXAddresses, DEXAddresses, LendingAddresses, BurningAddresses.
2b. ABIs (apps/indexer/src/indexer/<dao>/abi/)
Create ABI files for the token and governor contracts. Use viem's built-in ABIs where possible, or extract from block explorer.
apps/indexer/src/indexer/<dao>/
├── abi/
│ └── index.ts # exports TokenAbi, GovernorAbi
├── erc20.ts # token event handlers (Transfer, DelegateChanged, DelegateVotesChanged)
├── governor.ts # governor event handlers (ProposalCreated, VoteCast, etc.)
└── index.ts # re-exports everything
2c. Event Handlers
Token handler (erc20.ts): Follow the pattern in apps/indexer/src/indexer/ens/erc20.ts:
setup event: Insert token record
Transfer: Track balances, CEX/DEX/Lending/Treasury flows
DelegateChanged: Track delegation changes
DelegateVotesChanged: Track voting power changes
Governor handler (governor.ts): Follow the pattern in apps/indexer/src/indexer/ens/governor.ts:
ProposalCreated: Insert proposal
VoteCast / VoteCastWithParams: Record votes
ProposalQueued, ProposalExecuted, ProposalCanceled: Update proposal status
2d. Ponder Config (apps/indexer/config/<dao>.config.ts)
Follow the pattern in apps/indexer/config/ens.config.ts:
import { createConfig } from "ponder";
import { CONTRACT_ADDRESSES } from "@/lib/constants";
import { DaoIdEnum } from "@/lib/enums";
import { env } from "@/env";
import { TokenAbi, GovernorAbi } from "@/indexer/<dao>/abi";
const CONTRACTS = CONTRACT_ADDRESSES[DaoIdEnum.NEW_DAO];
export default createConfig({
database: { kind: "postgres", connectionString: env.DATABASE_URL },
chains: {
ethereum_mainnet: {
id: 1,
rpc: env.RPC_URL,
maxRequestsPerSecond: env.MAX_REQUESTS_PER_SECOND,
pollingInterval: env.POLLING_INTERVAL,
},
},
contracts: {
NEW_DAOToken: {
abi: TokenAbi,
chain: "ethereum_mainnet",
address: CONTRACTS.token.address,
startBlock: CONTRACTS.token.startBlock,
},
NEW_DAOGovernor: {
abi: GovernorAbi,
chain: "ethereum_mainnet",
address: CONTRACTS.governor.address,
startBlock: CONTRACTS.governor.startBlock,
},
},
});
2e. Wire into Ponder (apps/indexer/ponder.config.ts)
Import the new config and spread its chains/contracts into the merged config.
2f. Wire into entry point (apps/indexer/src/index.ts)
Add import and switch case:
import { NEW_DAOTokenIndexer, NEW_DAOGovernorIndexer } from "@/indexer/<dao>";
case DaoIdEnum.NEW_DAO: {
NEW_DAOTokenIndexer(token.address, token.decimals);
NEW_DAOGovernorIndexer(blockTime);
break;
}
Step 3: API
3a. Constants (apps/api/src/lib/constants.ts)
Mirror the same CONTRACT_ADDRESSES[DaoIdEnum.NEW_DAO] entry from the indexer.
3b. Client (apps/api/src/clients/<dao>/index.ts)
Create a client class extending GovernorBase and implementing DAOClient:
export class NEW_DAOClient extends GovernorBase implements DAOClient {
}
Follow the pattern in apps/api/src/clients/ens/index.ts.
3c. Register client (apps/api/src/clients/index.ts)
Add export * from "./<dao>";
3d. Event Relevance (apps/api/src/lib/eventRelevance.ts)
Ask the user for the LOW/MEDIUM/HIGH relevance thresholds for TRANSFER, DELEGATION, and VOTE events. These determine how events appear in the feed. The DAO_RELEVANCE_THRESHOLDS map is a Record<DaoIdEnum, EventRelevanceMap> — the compiler will error if the new DAO is missing. Default to EMPTY_THRESHOLDS for all event types and let the user provide specific values later.
3e. CoinGecko Mapping (apps/api/src/services/coingecko/types.ts)
Ask the user for the CoinGecko token ID (the slug used in CoinGecko URLs, e.g. "uniswap", "fluid"). Add it to CoingeckoTokenIdEnum and add the corresponding asset platform to CoingeckoIdToAssetPlatformId. Both are Record<DaoIdEnum, ...> — the compiler will error if missing.
Step 4: Gateway
Gateful needs no code change for a standard DAO — it discovers per-DAO APIs at
startup from DAO_API_<DAO_ID> env vars (apps/gateful/src/config.ts). Register the
new DAO's API URL in each environment:
- Local:
scripts/dev.sh exports DAO_API_<DAO_ID> automatically when you pass
the DAO name (or add it to your .env).
- Deployed (Railway dev/prod): add the
DAO_API_<DAO_ID> env var to the Gateful
service — flag this to the user, it is a deployment config change.
Only touch Gateful code if the DAO needs custom routing or aggregation — then follow
the anticapture-gateful skill.
Step 5: Dashboard
Follow the dashboard-dao skill (.agents/skills/dashboard-dao/SKILL.md) for the full step-by-step guide. The dashboard enum entry is already handled in Step 1 above — the dashboard-dao skill covers the remaining dashboard-specific work:
- Quorum calculation label
- DAO config file creation
- Config registration
- Icons (optional)
- Governance implementation fields (optional)
Verification
After all changes, run typecheck and lint on each affected package:
pnpm indexer typecheck && pnpm indexer lint
pnpm api typecheck && pnpm api lint
pnpm gateful typecheck && pnpm gateful lint
pnpm dashboard typecheck && pnpm dashboard lint
Common Patterns & Variations
| Variation | Example DAO | Key Difference |
|---|
| Standard ERC20 + Compound Governor | ENS, UNI, GTC, OP, FLUID | Straightforward, follow ENS pattern. FLUID reuses the COMP ABI (identical CompoundGovernor). Timelock uses delay() (CompoundTimelock), quorum is forVotes only. |
| ERC721 (NFT) token | NOUNS | Token is NFT, auto-delegates on transfer |
| Multi-token tracking | AAVE | Tracks AAVE + stkAAVE + aAAVE separately |
| Azorius governance (Fractal) | SHU | Different governor events, custom proposal handling |
| Multi-chain | ARB, OP, SCR | Config needs chain-specific RPC and chain ID |
| No governor (token-only) | ARB | Only token indexer, no governor handler |
| Vote-escrow (veToken) governance | OLAS | ERC20 has no delegation; voting power from veToken lock. Requires custom veToken indexer for Deposit/Withdraw events to track delegatedSupply and accountPower. Governor events are standard. |
INTEGRATION.md Template
Every DAO integration must include an INTEGRATION.md file at apps/indexer/src/indexer/<dao>/INTEGRATION.md. This is the source of truth for what's integrated and what's pending.
# <DAO_NAME> Integration Status
## Architecture
| Contract | Address | Type | Events used |
| -------- | ------- | --------------------------- | ------------------------------ |
| Token | 0x... | ERC20 / ERC721 / veToken | Transfer, DelegateChanged, ... |
| Governor | 0x... | OZ Governor / Azorius / ... | ProposalCreated, VoteCast, ... |
| Timelock | 0x... | TimelockController | (not indexed) |
Governor voting token: `<address>` (same as token / veToken wrapper / other)
## What's Integrated
- [ ] Token supply tracking (Transfer events)
- [ ] Delegation tracking (DelegateChanged / DelegateVotesChanged)
- [ ] Voting power tracking (accountPower, votingPowerHistory)
- [ ] Governor proposals (ProposalCreated, status updates)
- [ ] Governor votes (VoteCast)
- [ ] CEX/DEX/Lending address classification
- [ ] Treasury tracking
## What's Pending
List gaps with context on why and what's needed to close them.
## Notes
Any DAO-specific quirks, discrepancies, or decisions made during integration.
Guardrails
- Enums must be identical across indexer, API, and dashboard
- Constants (contract addresses) must match between indexer and API
- ABIs must match the deployed contracts — verify on block explorer
- Do not run the indexer unless explicitly asked (reindexing is expensive)
- Test the API client against the real chain before deploying
- Always run Step 0 (Governance Architecture Discovery) before writing code — never assume the token the user provides is the voting token