Skip to main content

Examples

Learn by building real-world governance features with the SDK.

Sample DAO

These examples use a real DAO on Sepolia testnet:
ContractAddress
DAOGovernor0xf51C4b7b5AA34052B9C605A6BAf4DB8E844106B0
DAOToken0xcfCff2dAad166cd75458E5F4Dd0c1F01c35D8d9C
TimelockController0xe0E285CaD99C1667101539C09e5b684c0A7BfF18
DAO ID: 11155111_0xf51C4b7b5AA34052B9C605A6BAf4DB8E844106B0

Project Setup

Create a new Vite + React project:
npm create vite@latest dao-dashboard -- --template react-ts
cd dao-dashboard
npm install
npm install daocafe-sdk @tanstack/react-query
Set up the QueryClient in src/main.tsx:
import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
import './index.css';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60, // 1 minute
      refetchOnWindowFocus: false,
    },
  },
});

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </React.StrictMode>
);

DAO Dashboard

A complete dashboard showing DAO info, proposals, and voting power.

App Component

// src/App.tsx
import { DAOHeader } from './components/DAOHeader';
import { ProposalList } from './components/ProposalList';
import { TopHolders } from './components/TopHolders';

const DAO_ID = '11155111_0xf51C4b7b5AA34052B9C605A6BAf4DB8E844106B0';

function App() {
  return (
    <div style={{ maxWidth: '800px', margin: '0 auto', padding: '20px' }}>
      <DAOHeader daoId={DAO_ID} />
      <div style={{ display: 'grid', gridTemplateColumns: '2fr 1fr', gap: '20px' }}>
        <ProposalList daoId={DAO_ID} />
        <TopHolders daoId={DAO_ID} />
      </div>
    </div>
  );
}

export default App;

DAO Header Component

// src/components/DAOHeader.tsx
import { useDAO } from 'daocafe-sdk';

interface Props {
  daoId: string;
}

export function DAOHeader({ daoId }: Props) {
  const { data: dao, isLoading } = useDAO(daoId);

  if (isLoading) {
    return <div>Loading DAO...</div>;
  }

  if (!dao) {
    return <div>DAO not found</div>;
  }

  return (
    <header style={{ marginBottom: '30px' }}>
      <h1 style={{ margin: 0 }}>{dao.name}</h1>
      <p style={{ color: '#666' }}>
        {dao.tokenSymbol}{dao.proposalCount} proposals
      </p>
      
      <div style={{ 
        display: 'grid', 
        gridTemplateColumns: 'repeat(4, 1fr)', 
        gap: '15px',
        marginTop: '20px'
      }}>
        <StatCard label="Total Supply" value={formatNumber(dao.totalSupply)} />
        <StatCard label="Voting Delay" value={formatDuration(dao.votingDelay)} />
        <StatCard label="Voting Period" value={formatDuration(dao.votingPeriod)} />
        <StatCard label="Quorum" value={`${dao.quorumNumerator}%`} />
      </div>
    </header>
  );
}

function StatCard({ label, value }: { label: string; value: string }) {
  return (
    <div style={{ 
      background: '#f5f5f5', 
      padding: '15px', 
      borderRadius: '8px' 
    }}>
      <div style={{ fontSize: '12px', color: '#666' }}>{label}</div>
      <div style={{ fontSize: '18px', fontWeight: 'bold' }}>{value}</div>
    </div>
  );
}

function formatNumber(value: string): string {
  const num = BigInt(value) / BigInt(10 ** 18);
  return num.toLocaleString();
}

function formatDuration(seconds: string): string {
  const num = Number(seconds);
  const hours = Math.floor(num / 3600);
  const days = Math.floor(hours / 24);
  if (days > 0) return `${days}d`;
  if (hours > 0) return `${hours}h`;
  return `${Math.floor(num / 60)}m`;
}

Proposal List Component

// src/components/ProposalList.tsx
import { useProposalsByDAO } from 'daocafe-sdk';
import type { Proposal, ProposalState } from 'daocafe-sdk';

interface Props {
  daoId: string;
}

export function ProposalList({ daoId }: Props) {
  const { data, isLoading } = useProposalsByDAO(daoId, {
    limit: 10,
    orderBy: 'createdAt',
    orderDirection: 'desc',
  });

  if (isLoading) {
    return <div>Loading proposals...</div>;
  }

  return (
    <section>
      <h2>Proposals</h2>
      {data?.items.length === 0 ? (
        <p>No proposals yet</p>
      ) : (
        <ul style={{ listStyle: 'none', padding: 0 }}>
          {data?.items.map(proposal => (
            <ProposalCard key={proposal.id} proposal={proposal} />
          ))}
        </ul>
      )}
    </section>
  );
}

function ProposalCard({ proposal }: { proposal: Proposal }) {
  const forVotes = BigInt(proposal.forVotes);
  const againstVotes = BigInt(proposal.againstVotes);
  const totalVotes = forVotes + againstVotes;
  const forPercent = totalVotes > 0 
    ? Number((forVotes * 100n) / totalVotes) 
    : 0;

  return (
    <li style={{ 
      background: '#fff', 
      border: '1px solid #eee',
      borderRadius: '8px',
      padding: '15px',
      marginBottom: '10px'
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between' }}>
        <h3 style={{ margin: 0, fontSize: '16px' }}>
          {proposal.description.slice(0, 60)}...
        </h3>
        <StateLabel state={proposal.state} />
      </div>
      
      <div style={{ marginTop: '10px' }}>
        <div style={{ 
          background: '#f0f0f0', 
          borderRadius: '4px', 
          overflow: 'hidden',
          height: '8px'
        }}>
          <div style={{ 
            background: '#22c55e', 
            width: `${forPercent}%`,
            height: '100%'
          }} />
        </div>
        <div style={{ 
          display: 'flex', 
          justifyContent: 'space-between',
          fontSize: '12px',
          marginTop: '5px'
        }}>
          <span style={{ color: '#22c55e' }}>For: {formatVotes(proposal.forVotes)}</span>
          <span style={{ color: '#ef4444' }}>Against: {formatVotes(proposal.againstVotes)}</span>
        </div>
      </div>
    </li>
  );
}

function StateLabel({ state }: { state: ProposalState }) {
  const colors: Record<ProposalState, string> = {
    PENDING: '#f59e0b',
    ACTIVE: '#3b82f6',
    CANCELED: '#6b7280',
    DEFEATED: '#ef4444',
    SUCCEEDED: '#22c55e',
    QUEUED: '#8b5cf6',
    EXPIRED: '#6b7280',
    EXECUTED: '#22c55e',
  };

  return (
    <span style={{ 
      background: colors[state], 
      color: 'white',
      padding: '2px 8px',
      borderRadius: '4px',
      fontSize: '12px'
    }}>
      {state}
    </span>
  );
}

function formatVotes(value: string): string {
  const num = Number(BigInt(value) / BigInt(10 ** 18));
  return num.toLocaleString();
}

Top Holders Component

// src/components/TopHolders.tsx
import { useTokenHoldersByDAO } from 'daocafe-sdk';

interface Props {
  daoId: string;
}

export function TopHolders({ daoId }: Props) {
  const { data, isLoading } = useTokenHoldersByDAO(daoId, {
    limit: 10,
    orderBy: 'votes',
    orderDirection: 'desc',
  });

  if (isLoading) {
    return <div>Loading holders...</div>;
  }

  return (
    <section>
      <h2>Top Voters</h2>
      <ol style={{ paddingLeft: '20px' }}>
        {data?.items.map((holder, index) => (
          <li key={holder.id} style={{ marginBottom: '8px' }}>
            <code style={{ fontSize: '12px' }}>
              {holder.holder.slice(0, 6)}...{holder.holder.slice(-4)}
            </code>
            <div style={{ fontSize: '14px', fontWeight: 'bold' }}>
              {formatVotes(holder.votes)} votes
            </div>
          </li>
        ))}
      </ol>
    </section>
  );
}

function formatVotes(value: string): string {
  const num = Number(BigInt(value) / BigInt(10 ** 18));
  return num.toLocaleString();
}

Active Proposals Widget

A standalone widget showing active proposals across all DAOs.
// src/components/ActiveProposalsWidget.tsx
import { useActiveProposals } from 'daocafe-sdk';

export function ActiveProposalsWidget() {
  const { data, isLoading, error } = useActiveProposals({ limit: 5 });

  if (isLoading) {
    return (
      <div style={{ padding: '20px', textAlign: 'center' }}>
        Loading active proposals...
      </div>
    );
  }

  if (error) {
    return (
      <div style={{ padding: '20px', color: 'red' }}>
        Error: {error.message}
      </div>
    );
  }

  if (!data?.items.length) {
    return (
      <div style={{ padding: '20px', textAlign: 'center', color: '#666' }}>
        No active proposals at this time
      </div>
    );
  }

  return (
    <div style={{ 
      background: '#1a1a2e', 
      color: 'white',
      borderRadius: '12px',
      padding: '20px'
    }}>
      <h3 style={{ margin: '0 0 15px 0' }}>
        🗳️ Active Proposals ({data.items.length})
      </h3>
      
      {data.items.map(proposal => {
        const endsIn = getTimeUntil(proposal.voteEnd);
        
        return (
          <div 
            key={proposal.id}
            style={{ 
              background: 'rgba(255,255,255,0.1)',
              borderRadius: '8px',
              padding: '12px',
              marginBottom: '10px'
            }}
          >
            <div style={{ fontWeight: 'bold', marginBottom: '5px' }}>
              {proposal.description.slice(0, 50)}...
            </div>
            <div style={{ fontSize: '12px', color: '#aaa' }}>
              Ends {endsIn}
            </div>
          </div>
        );
      })}
    </div>
  );
}

function getTimeUntil(timestamp: string): string {
  const endTime = Number(timestamp) * 1000;
  const now = Date.now();
  const diff = endTime - now;

  if (diff <= 0) return 'soon';

  const hours = Math.floor(diff / (1000 * 60 * 60));
  const days = Math.floor(hours / 24);

  if (days > 0) return `in ${days} day${days > 1 ? 's' : ''}`;
  if (hours > 0) return `in ${hours} hour${hours > 1 ? 's' : ''}`;
  return 'in less than an hour';
}

Voter Profile

Show a user’s voting history and power across all DAOs.
// src/components/VoterProfile.tsx
import { useVotesByVoter, useTokenHoldingsByAddress } from 'daocafe-sdk';

interface Props {
  address: string;
}

export function VoterProfile({ address }: Props) {
  const { data: votes } = useVotesByVoter(address, { limit: 20 });
  const { data: holdings } = useTokenHoldingsByAddress(address);

  const forVotes = votes?.items.filter(v => v.support === 'FOR').length ?? 0;
  const againstVotes = votes?.items.filter(v => v.support === 'AGAINST').length ?? 0;
  const abstainVotes = votes?.items.filter(v => v.support === 'ABSTAIN').length ?? 0;

  return (
    <div style={{ padding: '20px' }}>
      <h2>Voter Profile</h2>
      <p style={{ fontFamily: 'monospace' }}>{address}</p>

      <h3>Holdings</h3>
      {holdings?.items.length === 0 ? (
        <p>No token holdings</p>
      ) : (
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead>
            <tr>
              <th style={{ textAlign: 'left', padding: '8px', borderBottom: '1px solid #eee' }}>DAO</th>
              <th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Balance</th>
              <th style={{ textAlign: 'right', padding: '8px', borderBottom: '1px solid #eee' }}>Votes</th>
            </tr>
          </thead>
          <tbody>
            {holdings?.items.map(h => (
              <tr key={h.id}>
                <td style={{ padding: '8px' }}>{h.daoId.slice(0, 20)}...</td>
                <td style={{ textAlign: 'right', padding: '8px' }}>{formatTokens(h.balance)}</td>
                <td style={{ textAlign: 'right', padding: '8px' }}>{formatTokens(h.votes)}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}

      <h3>Voting History</h3>
      <div style={{ display: 'flex', gap: '20px', marginBottom: '15px' }}>
        <div>
          <span style={{ color: '#22c55e', fontWeight: 'bold' }}>{forVotes}</span> For
        </div>
        <div>
          <span style={{ color: '#ef4444', fontWeight: 'bold' }}>{againstVotes}</span> Against
        </div>
        <div>
          <span style={{ color: '#6b7280', fontWeight: 'bold' }}>{abstainVotes}</span> Abstain
        </div>
      </div>

      {votes?.items.map(vote => (
        <div 
          key={vote.id}
          style={{ 
            padding: '10px',
            borderLeft: `3px solid ${
              vote.support === 'FOR' ? '#22c55e' : 
              vote.support === 'AGAINST' ? '#ef4444' : '#6b7280'
            }`,
            marginBottom: '8px',
            background: '#f9f9f9'
          }}
        >
          <div style={{ fontWeight: 'bold' }}>{vote.support}</div>
          <div style={{ fontSize: '12px', color: '#666' }}>
            {formatTokens(vote.weight)} votes
          </div>
          {vote.reason && (
            <div style={{ marginTop: '5px', fontStyle: 'italic' }}>
              "{vote.reason}"
            </div>
          )}
        </div>
      ))}
    </div>
  );
}

function formatTokens(value: string): string {
  const num = Number(BigInt(value) / BigInt(10 ** 18));
  return num.toLocaleString();
}

Node.js Script

Fetch and analyze DAO data from the command line:
// scripts/analyze-dao.ts
import { getDAO, getProposalsByDAO, getTokenHoldersByDAO } from 'daocafe-sdk';

const DAO_ID = '11155111_0xf51C4b7b5AA34052B9C605A6BAf4DB8E844106B0';

async function main() {
  console.log('Fetching DAO data...\n');

  // Get DAO info
  const dao = await getDAO(DAO_ID);
  if (!dao) {
    console.error('DAO not found');
    return;
  }

  console.log(`=== ${dao.name} ===`);
  console.log(`Token: ${dao.tokenSymbol}`);
  console.log(`Proposals: ${dao.proposalCount}`);
  console.log(`Voting Period: ${dao.votingPeriod} seconds`);
  console.log();

  // Get proposals
  const { items: proposals } = await getProposalsByDAO(DAO_ID, { limit: 5 });
  console.log('Recent Proposals:');
  proposals.forEach((p, i) => {
    console.log(`  ${i + 1}. [${p.state}] ${p.description.slice(0, 40)}...`);
  });
  console.log();

  // Get top holders
  const { items: holders } = await getTokenHoldersByDAO(DAO_ID, {
    orderBy: 'votes',
    orderDirection: 'desc',
    limit: 5,
  });
  console.log('Top Voters:');
  holders.forEach((h, i) => {
    const votes = Number(BigInt(h.votes) / BigInt(10 ** 18));
    console.log(`  ${i + 1}. ${h.holder.slice(0, 10)}... - ${votes.toLocaleString()} votes`);
  });
}

main().catch(console.error);
Run with:
npx tsx scripts/analyze-dao.ts