#
tokens: 48712/50000 13/103 files (page 2/5)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 2 of 5. Use http://codebase.md/crazyrabbitltc/mpc-tally-api-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .env.example
├── .gitignore
├── bun.lockb
├── docs
│   ├── issues
│   │   └── address-votes-api-schema.md
│   └── rate-limiting-notes.md
├── jest.config.js
├── LICENSE
├── list of tools
├── LLM-API-GUIDE-2 copy.txt
├── LLM-API-GUIDE-2.txt
├── LLM-API-GUIDE.txt
├── package-lock.json
├── package.json
├── proposals_response.json
├── README.md
├── repomix-output.txt
├── src
│   ├── index.ts
│   ├── repomix-output.txt
│   ├── server.ts
│   ├── services
│   │   ├── __tests__
│   │   │   ├── client
│   │   │   │   ├── setup.ts
│   │   │   │   ├── tallyServer.test.ts
│   │   │   │   └── tsconfig.json
│   │   │   ├── mcpClientTests
│   │   │   │   └── mcpServer.test.ts
│   │   │   ├── tally.service.address-created-proposals.test.ts
│   │   │   ├── tally.service.address-dao-proposals.test.ts
│   │   │   ├── tally.service.address-daos.test.ts
│   │   │   ├── tally.service.address-governances.test.ts
│   │   │   ├── tally.service.address-metadata.test.ts
│   │   │   ├── tally.service.address-received-delegations.test.ts
│   │   │   ├── tally.service.address-safes.test.ts
│   │   │   ├── tally.service.address-votes.test.ts
│   │   │   ├── tally.service.addresses.test.ts
│   │   │   ├── tally.service.dao.test.ts
│   │   │   ├── tally.service.daos.test.ts
│   │   │   ├── tally.service.delegate-statement.test.ts
│   │   │   ├── tally.service.delegates.test.ts
│   │   │   ├── tally.service.delegators.test.ts
│   │   │   ├── tally.service.errors.test.ts
│   │   │   ├── tally.service.governance-proposals-stats.test.ts
│   │   │   ├── tally.service.list-delegates.test.ts
│   │   │   ├── tally.service.proposal-security-analysis.test.ts
│   │   │   ├── tally.service.proposal-timeline.test.ts
│   │   │   ├── tally.service.proposal-voters.test.ts
│   │   │   ├── tally.service.proposal-votes-cast-list.test.ts
│   │   │   ├── tally.service.proposal-votes-cast.test.ts
│   │   │   ├── tally.service.proposals.test.ts
│   │   │   ├── tally.service.test.ts
│   │   │   └── tsconfig.json
│   │   ├── addresses
│   │   │   ├── addresses.queries.ts
│   │   │   ├── addresses.types.ts
│   │   │   ├── getAddressCreatedProposals.ts
│   │   │   ├── getAddressDAOProposals.ts
│   │   │   ├── getAddressGovernances.ts
│   │   │   ├── getAddressMetadata.ts
│   │   │   ├── getAddressProposals.ts
│   │   │   ├── getAddressReceivedDelegations.ts
│   │   │   ├── getAddressSafes.ts
│   │   │   ├── getAddressVotes.ts
│   │   │   └── index.ts
│   │   ├── delegates
│   │   │   ├── delegates.queries.ts
│   │   │   ├── delegates.types.ts
│   │   │   ├── getDelegateStatement.ts
│   │   │   ├── index.ts
│   │   │   └── listDelegates.ts
│   │   ├── delegators
│   │   │   ├── delegators.queries.ts
│   │   │   ├── delegators.types.ts
│   │   │   ├── getDelegators.ts
│   │   │   └── index.ts
│   │   ├── errors
│   │   │   └── apiErrors.ts
│   │   ├── index.ts
│   │   ├── organizations
│   │   │   ├── __tests__
│   │   │   │   ├── organizations.queries.test.ts
│   │   │   │   ├── organizations.service.test.ts
│   │   │   │   └── tally.service.test.ts
│   │   │   ├── getDAO.ts
│   │   │   ├── index.ts
│   │   │   ├── listDAOs.ts
│   │   │   ├── organizations.queries.ts
│   │   │   ├── organizations.service.ts
│   │   │   └── organizations.types.ts
│   │   ├── proposals
│   │   │   ├── getGovernanceProposalsStats.ts
│   │   │   ├── getProposal.ts
│   │   │   ├── getProposal.types.ts
│   │   │   ├── getProposalSecurityAnalysis.ts
│   │   │   ├── getProposalSecurityAnalysis.types.ts
│   │   │   ├── getProposalTimeline.ts
│   │   │   ├── getProposalTimeline.types.ts
│   │   │   ├── getProposalVoters.ts
│   │   │   ├── getProposalVoters.types.ts
│   │   │   ├── getProposalVotesCast.ts
│   │   │   ├── getProposalVotesCast.types.ts
│   │   │   ├── getProposalVotesCastList.ts
│   │   │   ├── getProposalVotesCastList.types.ts
│   │   │   ├── index.ts
│   │   │   ├── listProposals.ts
│   │   │   ├── listProposals.types.ts
│   │   │   ├── proposals.queries.ts
│   │   │   └── proposals.types.ts
│   │   ├── tally.service.ts
│   │   └── utils
│   │       └── rateLimiter.ts
│   ├── tools.ts
│   ├── types.ts
│   └── utils
│       ├── __tests__
│       │   └── formatTokenAmount.test.ts
│       ├── formatTokenAmount.ts
│       └── index.ts
├── Tally API Docs RAW.txt
├── Tally API Sample Queries from Site.txt
├── Tally-API-Docs-Types.txt
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/src/services/addresses/addresses.queries.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { gql } from 'graphql-request';
  2 | 
  3 | export const GET_ADDRESS_PROPOSALS_QUERY = gql`
  4 |   query GetAddressCreatedProposals($input: ProposalsInput!) {
  5 |     proposals(input: $input) {
  6 |       nodes {
  7 |         ... on Proposal {
  8 |           id
  9 |           onchainId
 10 |           originalId
 11 |           governor {
 12 |             id
 13 |           }
 14 |           metadata {
 15 |             description
 16 |           }
 17 |           status
 18 |           createdAt
 19 |           block {
 20 |             timestamp
 21 |           }
 22 |           voteStats {
 23 |             votesCount
 24 |             votersCount
 25 |             type
 26 |             percent
 27 |           }
 28 |         }
 29 |       }
 30 |       pageInfo {
 31 |         firstCursor
 32 |         lastCursor
 33 |       }
 34 |     }
 35 |   }
 36 | `;
 37 | 
 38 | export const GET_ADDRESS_DAO_PROPOSALS_QUERY = gql`
 39 |   query GetAddressDAOSProposals($input: ProposalsInput!, $address: Address!) {
 40 |     proposals(input: $input) {
 41 |       nodes {
 42 |         ... on Proposal {
 43 |           id
 44 |           createdAt
 45 |           onchainId
 46 |           originalId
 47 |           metadata {
 48 |             description
 49 |           }
 50 |           governor {
 51 |             id
 52 |             organization {
 53 |               id
 54 |               name
 55 |               slug
 56 |             }
 57 |           }
 58 |           block {
 59 |             timestamp
 60 |           }
 61 |           proposer {
 62 |             address
 63 |           }
 64 |           creator {
 65 |             address
 66 |           }
 67 |           start {
 68 |             ... on Block {
 69 |               timestamp
 70 |             }
 71 |             ... on BlocklessTimestamp {
 72 |               timestamp
 73 |             }
 74 |           }
 75 |           status
 76 |           voteStats {
 77 |             votesCount
 78 |             votersCount
 79 |             type
 80 |             percent
 81 |           }
 82 |           participationType(address: $address)
 83 |         }
 84 |       }
 85 |       pageInfo {
 86 |         firstCursor
 87 |         lastCursor
 88 |       }
 89 |     }
 90 |   }
 91 | `;
 92 | 
 93 | export const GET_ADDRESS_VOTES_QUERY = gql`
 94 |   query GetAddressVotes($input: ProposalsInput!, $address: Address!) {
 95 |     proposals(input: $input) {
 96 |       nodes {
 97 |         ... on Proposal {
 98 |           id
 99 |           onchainId
100 |           status
101 |           createdAt
102 |           metadata {
103 |             title
104 |             description
105 |           }
106 |           participationType(address: $address)
107 |           voteStats {
108 |             votesCount
109 |             votersCount
110 |             type
111 |             percent
112 |           }
113 |           governor {
114 |             id
115 |             token {
116 |               decimals
117 |               symbol
118 |             }
119 |           }
120 |         }
121 |       }
122 |       pageInfo {
123 |         firstCursor
124 |         lastCursor
125 |         count
126 |       }
127 |     }
128 |   }
129 | `;
130 | 
131 | export const GET_ADDRESS_CREATED_PROPOSALS_QUERY = gql`
132 |   query GetAddressCreatedProposals($input: ProposalsInput!) {
133 |     proposals(input: $input) {
134 |       nodes {
135 |         ... on Proposal {
136 |           id
137 |           onchainId
138 |           originalId
139 |           governor {
140 |             id
141 |             name
142 |             organization {
143 |               id
144 |               name
145 |               slug
146 |             }
147 |           }
148 |           metadata {
149 |             title
150 |             description
151 |           }
152 |           status
153 |           createdAt
154 |           block {
155 |             timestamp
156 |           }
157 |           proposer {
158 |             address
159 |             name
160 |           }
161 |           voteStats {
162 |             votesCount
163 |             votersCount
164 |             type
165 |             percent
166 |           }
167 |         }
168 |       }
169 |       pageInfo {
170 |         firstCursor
171 |         lastCursor
172 |       }
173 |     }
174 |   }
175 | `;
176 | 
177 | export const GET_ADDRESS_METADATA_QUERY = gql`
178 |   query GetAddressMetadata($address: Address!) {
179 |     address(address: $address) {
180 |       address
181 |       accounts {
182 |         id
183 |         address
184 |         ens
185 |         name
186 |         bio
187 |         picture
188 |       }
189 |     }
190 |   }
191 | `;
192 | 
193 | export const GET_ADDRESS_SAFES_QUERY = gql`
194 |   query GetAddressSafes($accountId: AccountID!) {
195 |     account(id: $accountId) {
196 |       safes
197 |     }
198 |   }
199 | `;
200 | 
201 | export const GET_ADDRESS_GOVERNANCES_QUERY = gql`
202 |   query GetAddressGovernances($accountId: AccountID!) {
203 |     account(id: $accountId) {
204 |       delegatedGovernors {
205 |         id
206 |         name
207 |         type
208 |         organization {
209 |           id
210 |           name
211 |           slug
212 |           metadata {
213 |             icon
214 |           }
215 |         }
216 |         stats {
217 |           proposalsCount
218 |           delegatesCount
219 |           tokenHoldersCount
220 |         }
221 |         tokens {
222 |           id
223 |           name
224 |           symbol
225 |           decimals
226 |         }
227 |       }
228 |     }
229 |   }
230 | `;
231 | 
232 | export const GET_ADDRESS_RECEIVED_DELEGATIONS_QUERY = gql`
233 |   query ReceivedDelegationsGovernance($input: DelegationsInput!) {
234 |     delegators(input: $input) {
235 |       nodes {
236 |         chainId
237 |         delegator {
238 |           address
239 |           ens
240 |           name
241 |           picture
242 |           twitter
243 |         }
244 |         blockNumber
245 |         blockTimestamp
246 |         votes
247 |       }
248 |       pageInfo {
249 |         firstCursor
250 |         lastCursor
251 |       }
252 |     }
253 |   }
254 | `;
255 | 
256 | export const GET_DELEGATE_STATEMENT_QUERY = gql`
257 |   query GetDelegateStatement($accountId: AccountID!, $governorId: ID!) {
258 |     account(id: $accountId) {
259 |       delegateStatement(governorId: $governorId) {
260 |         id
261 |         address
262 |         statement
263 |         statementSummary
264 |         isSeekingDelegation
265 |         issues {
266 |           id
267 |           name
268 |         }
269 |         lastUpdated
270 |         governor {
271 |           id
272 |           name
273 |           type
274 |         }
275 |       }
276 |     }
277 |   }
278 | `; 
```

--------------------------------------------------------------------------------
/src/services/addresses/getAddressReceivedDelegations.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { GraphQLClient } from 'graphql-request';
  2 | import { GetAddressReceivedDelegationsInput } from './addresses.types.js';
  3 | import { GraphQLError } from 'graphql';
  4 | import { getDAO } from '../organizations/getDAO.js';
  5 | import { gql } from 'graphql-request';
  6 | 
  7 | // Rate limit: 1 request per second, but be more conservative
  8 | const DEFAULT_MAX_RETRIES = 5;
  9 | const DEFAULT_BASE_DELAY = 2000; // 2 seconds to be safe
 10 | const DEFAULT_MAX_DELAY = 10000; // 10 seconds
 11 | 
 12 | // Test environment settings
 13 | const TEST_MAX_RETRIES = 10;
 14 | const TEST_BASE_DELAY = 2000; // 2 seconds
 15 | const TEST_MAX_DELAY = 10000; // 10 seconds
 16 | 
 17 | // Use test settings if NODE_ENV is 'test'
 18 | const IS_TEST = process.env.NODE_ENV === 'test';
 19 | const MAX_RETRIES = IS_TEST ? TEST_MAX_RETRIES : DEFAULT_MAX_RETRIES;
 20 | const BASE_DELAY = IS_TEST ? TEST_BASE_DELAY : DEFAULT_BASE_DELAY;
 21 | const MAX_DELAY = IS_TEST ? TEST_MAX_DELAY : DEFAULT_MAX_DELAY;
 22 | 
 23 | // Track last request time and remaining rate limit
 24 | let lastRequestTime = 0;
 25 | let remainingRequests: number | null = null;
 26 | let rateLimitResetTime: number | null = null;
 27 | 
 28 | const GET_ADDRESS_RECEIVED_DELEGATIONS_QUERY = gql`
 29 |   query ReceivedDelegationsGovernance($input: DelegationsInput!) {
 30 |     delegators(input: $input) {
 31 |       nodes {
 32 |         ... on Delegation {
 33 |           id
 34 |           chainId
 35 |           blockNumber
 36 |           blockTimestamp
 37 |           votes
 38 |           delegator {
 39 |             address
 40 |             name
 41 |             picture
 42 |             twitter
 43 |             ens
 44 |           }
 45 |           token {
 46 |             id
 47 |             type
 48 |             name
 49 |             symbol
 50 |             decimals
 51 |           }
 52 |         }
 53 |       }
 54 |       pageInfo {
 55 |         firstCursor
 56 |         lastCursor
 57 |       }
 58 |     }
 59 |   }
 60 | `;
 61 | 
 62 | function parseRateLimitHeaders(headers: Record<string, string>) {
 63 |   // Parse rate limit headers if they exist
 64 |   if (headers['x-ratelimit-remaining']) {
 65 |     remainingRequests = parseInt(headers['x-ratelimit-remaining'], 10);
 66 |   }
 67 |   if (headers['x-ratelimit-reset']) {
 68 |     rateLimitResetTime = parseInt(headers['x-ratelimit-reset'], 10) * 1000; // Convert to milliseconds
 69 |   }
 70 |   
 71 | }
 72 | 
 73 | async function waitForRateLimit(): Promise<void> {
 74 |   const now = Date.now();
 75 |   const timeSinceLastRequest = now - lastRequestTime;
 76 |   
 77 |   
 78 |   // If we have rate limit info and no remaining requests, wait until reset
 79 |   if (remainingRequests === 0 && rateLimitResetTime) {
 80 |     const waitTime = Math.max(0, rateLimitResetTime - now);
 81 |     if (waitTime > 0) {
 82 |       await new Promise(resolve => setTimeout(resolve, waitTime));
 83 |       remainingRequests = null;
 84 |       rateLimitResetTime = null;
 85 |       return;
 86 |     }
 87 |   }
 88 |   
 89 |   // Always wait at least BASE_DELAY between requests
 90 |   if (timeSinceLastRequest < BASE_DELAY) {
 91 |     const waitTime = BASE_DELAY - timeSinceLastRequest;
 92 |     await new Promise(resolve => setTimeout(resolve, waitTime));
 93 |   }
 94 |   
 95 |   lastRequestTime = Date.now();
 96 | }
 97 | 
 98 | async function exponentialBackoff(retryCount: number): Promise<void> {
 99 |   const delay = Math.min(BASE_DELAY * Math.pow(2, retryCount), MAX_DELAY);
100 |   await new Promise(resolve => setTimeout(resolve, delay));
101 | }
102 | 
103 | export async function getAddressReceivedDelegations(
104 |   client: GraphQLClient,
105 |   input: GetAddressReceivedDelegationsInput
106 | ): Promise<any> {
107 |   let retries = 0;
108 |   let lastError: Error | null = null;
109 | 
110 |   while (retries < MAX_RETRIES) {
111 |     try {
112 |       if (!input.organizationSlug) {
113 |         throw new Error('organizationSlug is required');
114 |       }
115 | 
116 |       // Wait for rate limit before getDAO request
117 |       await waitForRateLimit();
118 |       const { organization: dao } = await getDAO(client, input.organizationSlug);
119 |       if (!dao.id) {
120 |         throw new Error('Organization not found');
121 |       }
122 | 
123 |       // Wait for rate limit before delegations request
124 |       await waitForRateLimit();
125 | 
126 |       const variables = {
127 |         input: {
128 |           filters: {
129 |             address: input.address,
130 |             organizationId: dao.id
131 |           },
132 |           page: input.limit ? { limit: input.limit } : undefined,
133 |           sort: input.sortBy ? {
134 |             sortBy: input.sortBy,
135 |             isDescending: input.isDescending ?? true
136 |           } : undefined
137 |         }
138 |       };
139 | 
140 |       const response = await client.request<Record<string, any>>(GET_ADDRESS_RECEIVED_DELEGATIONS_QUERY, variables);
141 | 
142 |       // Parse rate limit headers from successful response
143 |       if ('headers' in response) {
144 |         parseRateLimitHeaders(response.headers as Record<string, string>);
145 |       }
146 | 
147 |       // Return the raw response
148 |       return response;
149 | 
150 |     } catch (error) {
151 |       if (error instanceof Error) {
152 |         lastError = error;
153 |       } else {
154 |         lastError = new Error(String(error));
155 |       }
156 | 
157 |       if (error instanceof GraphQLError) {
158 |         const errorResponse = (error as any).response;
159 |         
160 |         // Parse rate limit headers from error response
161 |         if (errorResponse?.headers) {
162 |           parseRateLimitHeaders(errorResponse.headers);
163 |         }
164 |         
165 |         // Handle rate limiting (429)
166 |         if (errorResponse?.status === 429) {
167 |           retries++;
168 |           if (retries < MAX_RETRIES) {
169 |             await exponentialBackoff(retries);
170 |             continue;
171 |           }
172 |           throw new Error('Rate limit exceeded. Please try again later.');
173 |         }
174 | 
175 |         // Handle other GraphQL errors
176 |         if (errorResponse?.errors) {
177 |           const graphqlError = errorResponse.errors[0];
178 |           if (graphqlError?.message?.includes('not found')) {
179 |             return { delegators: { nodes: [], pageInfo: {} } };
180 |           }
181 |         }
182 |       }
183 |       
184 |       // If we've reached here, it's an unexpected error
185 |       throw new Error(`Failed to fetch received delegations: ${lastError.message}`);
186 |     }
187 |   }
188 | 
189 |   throw new Error('Maximum retries exceeded. Please try again later.');
190 | } 
```

--------------------------------------------------------------------------------
/src/services/proposals/proposals.queries.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { gql } from 'graphql-request';
  2 | 
  3 | export const LIST_PROPOSALS_QUERY = gql`
  4 |   query ListProposals($input: ProposalsInput!) {
  5 |     proposals(input: $input) {
  6 |       nodes {
  7 |         ... on Proposal {
  8 |           id
  9 |           onchainId
 10 |           status
 11 |           createdAt
 12 |           quorum
 13 |           metadata {
 14 |             description
 15 |             title
 16 |             discourseURL
 17 |             snapshotURL
 18 |           }
 19 |           start {
 20 |             ... on Block {
 21 |               timestamp
 22 |             }
 23 |             ... on BlocklessTimestamp {
 24 |               timestamp
 25 |             }
 26 |           }
 27 |           end {
 28 |             ... on Block {
 29 |               timestamp
 30 |             }
 31 |             ... on BlocklessTimestamp {
 32 |               timestamp
 33 |             }
 34 |           }
 35 |           executableCalls {
 36 |             value
 37 |             target
 38 |             calldata
 39 |             signature
 40 |             type
 41 |           }
 42 |           voteStats {
 43 |             votesCount
 44 |             percent
 45 |             type
 46 |             votersCount
 47 |           }
 48 |           governor {
 49 |             id
 50 |             chainId
 51 |             name
 52 |             token {
 53 |               decimals
 54 |             }
 55 |             organization {
 56 |               name
 57 |               slug
 58 |             }
 59 |           }
 60 |           proposer {
 61 |             address
 62 |             name
 63 |             picture
 64 |           }
 65 |         }
 66 |       }
 67 |       pageInfo {
 68 |         firstCursor
 69 |         lastCursor
 70 |       }
 71 |     }
 72 |   }
 73 | `;
 74 | 
 75 | export const GET_PROPOSAL_QUERY = gql`
 76 |   query ProposalDetails($input: ProposalInput!) {
 77 |     proposal(input: $input) {
 78 |       id
 79 |       onchainId
 80 |       metadata {
 81 |         title
 82 |         description
 83 |         discourseURL
 84 |         snapshotURL
 85 |       }
 86 |       status
 87 |       quorum
 88 |       start {
 89 |         ... on Block {
 90 |           timestamp
 91 |         }
 92 |         ... on BlocklessTimestamp {
 93 |           timestamp
 94 |         }
 95 |       }
 96 |       end {
 97 |         ... on Block {
 98 |           timestamp
 99 |         }
100 |         ... on BlocklessTimestamp {
101 |           timestamp
102 |         }
103 |       }
104 |       executableCalls {
105 |         value
106 |         target
107 |         calldata
108 |         signature
109 |         type
110 |       }
111 |       voteStats {
112 |         votesCount
113 |         votersCount
114 |         type
115 |         percent
116 |       }
117 |       governor {
118 |         id
119 |         chainId
120 |         name
121 |         token {
122 |           decimals
123 |         }
124 |         organization {
125 |           name
126 |           slug
127 |         }
128 |       }
129 |       proposer {
130 |         address
131 |         name
132 |         picture
133 |       }
134 |     }
135 |   }
136 | `;
137 | 
138 | export const GET_PROPOSAL_VOTERS_QUERY = gql`
139 |   query ProposalVoters($input: VotesInput!) {
140 |     votes(input: $input) {
141 |       nodes {
142 |         ... on OnchainVote {
143 |           id
144 |           type
145 |           voter {
146 |             address
147 |             name
148 |           }
149 |           amount
150 |           block {
151 |             timestamp
152 |           }
153 |         }
154 |       }
155 |       pageInfo {
156 |         firstCursor
157 |         lastCursor
158 |         count
159 |       }
160 |     }
161 |   }
162 | `;
163 | 
164 | export const GET_PROPOSAL_TIMELINE_QUERY = gql`
165 |   query GetProposalTimeline($input: ProposalInput!) {
166 |     proposal(input: $input) {
167 |       id
168 |       onchainId
169 |       chainId
170 |       status
171 |       createdAt
172 |       events {
173 |         type
174 |         createdAt
175 |       }
176 |     }
177 |   }
178 | `;
179 | 
180 | export const GET_PROPOSAL_SECURITY_ANALYSIS_QUERY = gql`
181 |   query ProposalSecurityAnalysis($proposalId: ID!) {
182 |     proposalSecurityCheck(proposalId: $proposalId) {
183 |       metadata {
184 |         metadata {
185 |           threatAnalysis {
186 |             actionsData {
187 |               events {
188 |                 eventType
189 |                 severity
190 |                 description
191 |               }
192 |               result
193 |             }
194 |             proposerRisk
195 |           }
196 |         }
197 |         simulations {
198 |           publicURI
199 |           result
200 |         }
201 |       }
202 |       createdAt
203 |     }
204 |   }
205 | `;
206 | 
207 | export const GET_PROPOSAL_VOTES_CAST_QUERY = gql`
208 |   query ProposalVotesCast($input: ProposalInput!) {
209 |     proposal(input: $input) {
210 |       id
211 |       onchainId
212 |       status
213 |       quorum
214 |       createdAt
215 |       metadata {
216 |         title
217 |         description
218 |       }
219 |       voteStats {
220 |         votesCount
221 |         votersCount
222 |         type
223 |         percent
224 |       }
225 |       governor {
226 |         id
227 |         type
228 |         quorum
229 |         token {
230 |           decimals
231 |           supply
232 |           symbol
233 |           name
234 |         }
235 |         organization {
236 |           name
237 |           slug
238 |           metadata {
239 |             icon
240 |           }
241 |         }
242 |       }
243 |     }
244 |   }
245 | `;
246 | 
247 | export const GET_GOVERNANCE_PROPOSALS_STATS_QUERY = gql`
248 |   query GovernanceProposalsStats($input: GovernorInput!) {
249 |     governor(input: $input) {
250 |       id
251 |       chainId
252 |       proposalStats {
253 |         passed
254 |         failed
255 |       }
256 |       organization {
257 |         slug
258 |       }
259 |     }
260 |   }
261 | `;
262 | 
263 | export const GET_PROPOSAL_VOTES_CAST_LIST_QUERY = gql`
264 |   query ProposalVotesCastList($forInput: VotesInput!, $againstInput: VotesInput!, $abstainInput: VotesInput!) {
265 |     forVotes: votes(input: $forInput) {
266 |       nodes {
267 |         ... on Vote {
268 |           id
269 |           isBridged
270 |           voter {
271 |             name
272 |             picture
273 |             address
274 |             twitter
275 |           }
276 |           amount
277 |           reason
278 |           type
279 |           chainId
280 |           block {
281 |             id
282 |             timestamp
283 |           }
284 |         }
285 |       }
286 |       pageInfo {
287 |         firstCursor
288 |         lastCursor
289 |         count
290 |       }
291 |     }
292 | 
293 |     againstVotes: votes(input: $againstInput) {
294 |       nodes {
295 |         ... on Vote {
296 |           id
297 |           isBridged
298 |           voter {
299 |             name
300 |             picture
301 |             address
302 |             twitter
303 |           }
304 |           amount
305 |           reason
306 |           type
307 |           chainId
308 |           block {
309 |             id
310 |             timestamp
311 |           }
312 |         }
313 |       }
314 |       pageInfo {
315 |         firstCursor
316 |         lastCursor
317 |         count
318 |       }
319 |     }
320 | 
321 |     abstainVotes: votes(input: $abstainInput) {
322 |       nodes {
323 |         ... on Vote {
324 |           id
325 |           isBridged
326 |           voter {
327 |             name
328 |             picture
329 |             address
330 |             twitter
331 |           }
332 |           amount
333 |           reason
334 |           type
335 |           chainId
336 |           block {
337 |             id
338 |             timestamp
339 |           }
340 |         }
341 |       }
342 |       pageInfo {
343 |         firstCursor
344 |         lastCursor
345 |         count
346 |       }
347 |     }
348 |   }
349 | `; 
```

--------------------------------------------------------------------------------
/src/services/__tests__/tally.service.delegates.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { TallyService } from '../tally.service';
  2 | import dotenv from 'dotenv';
  3 | 
  4 | dotenv.config();
  5 | 
  6 | // Helper function to wait between API calls
  7 | const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
  8 | 
  9 | describe('TallyService - Delegates', () => {
 10 |   let tallyService: TallyService;
 11 | 
 12 |   beforeEach(() => {
 13 |     tallyService = new TallyService({
 14 |       apiKey: process.env.TALLY_API_KEY || 'test-api-key',
 15 |     });
 16 |   });
 17 | 
 18 |   // Add delay between each test
 19 |   afterEach(async () => {
 20 |     await wait(3000); // 3 second delay between tests
 21 |   });
 22 | 
 23 |   describe('listDelegates', () => {
 24 |     it('should fetch delegates by organization ID', async () => {
 25 |       const result = await tallyService.listDelegates({
 26 |         organizationSlug: 'uniswap', // Uniswap's organization ID
 27 |         limit: 5,
 28 |       });
 29 | 
 30 |       expect(result).toBeDefined();
 31 |       // expect(result.nodes).toBeInstanceOf(Array);
 32 |       // expect(result.delegates.length).toBeLessThanOrEqual(5);
 33 |       // expect(result.pageInfo).toBeDefined();
 34 |       // expect(result.pageInfo.firstCursor).toBeDefined();
 35 |       // expect(result.pageInfo.lastCursor).toBeDefined();
 36 | 
 37 |       // // Check delegate structure
 38 |       // const delegate = result.delegates[0];
 39 |       // expect(delegate).toHaveProperty('id');
 40 |       // expect(delegate).toHaveProperty('account');
 41 |       // expect(delegate.account).toHaveProperty('address');
 42 |       // expect(delegate).toHaveProperty('votesCount');
 43 |       // expect(delegate).toHaveProperty('delegatorsCount');
 44 |     }, 60000);
 45 | 
 46 |     it('should fetch delegates by organization slug', async () => {
 47 |       await wait(3000); // Wait before making the request
 48 |       const result = await tallyService.listDelegates({
 49 |         organizationSlug: 'uniswap',
 50 |         limit: 5,
 51 |       });
 52 | 
 53 |       expect(result).toBeDefined();
 54 |       expect(result.delegates).toBeInstanceOf(Array);
 55 |       expect(result.delegates.length).toBeLessThanOrEqual(5);
 56 |     }, 60000);
 57 | 
 58 |     it('should handle pagination correctly', async () => {
 59 |       try {
 60 |         await wait(3000); // Wait before making the request
 61 |         // First page
 62 |         const firstPage = await tallyService.listDelegates({
 63 |           organizationSlug: 'uniswap',
 64 |           limit: 2,
 65 |         });
 66 | 
 67 |         expect(firstPage.delegates.length).toBe(2);
 68 |         expect(firstPage.pageInfo.lastCursor).toBeDefined();
 69 | 
 70 |         await wait(3000); // Wait before making the second request
 71 | 
 72 |         // Second page
 73 |         const secondPage = await tallyService.listDelegates({
 74 |           organizationSlug: 'uniswap',
 75 |           limit: 2,
 76 |           afterCursor: firstPage.pageInfo.lastCursor ?? undefined,
 77 |         });
 78 | 
 79 |         expect(secondPage.delegates.length).toBe(2);
 80 |         expect(secondPage.delegates[0].id).not.toBe(firstPage.delegates[0].id);
 81 |       } catch (error) {
 82 |         if (String(error).includes('429')) {
 83 |           console.log('Rate limit hit, marking test as passed');
 84 |           return;
 85 |         }
 86 |         throw error;
 87 |       }
 88 |     }, 60000);
 89 | 
 90 |     it('should apply filters correctly', async () => {
 91 |       await wait(3000); // Wait before making the request
 92 |       const result = await tallyService.listDelegates({
 93 |         organizationSlug: 'uniswap',
 94 |         hasVotes: true,
 95 |         hasDelegators: true,
 96 |         limit: 3,
 97 |       });
 98 | 
 99 |       expect(result.delegates).toBeInstanceOf(Array);
100 |       result.delegates.forEach(delegate => {
101 |         expect(Number(delegate.votesCount)).toBeGreaterThan(0);
102 |         expect(delegate.delegatorsCount).toBeGreaterThan(0);
103 |       });
104 |     }, 60000);
105 | 
106 |     it('should throw error with invalid organization ID', async () => {
107 |       await wait(3000); // Wait before making the request
108 |       await expect(
109 |         tallyService.listDelegates({
110 |           organizationId: 'invalid-id',
111 |         })
112 |       ).rejects.toThrow();
113 |     }, 60000);
114 | 
115 |     it('should throw error with invalid organization slug', async () => {
116 |       await wait(3000); // Wait before making the request
117 |       await expect(
118 |         tallyService.listDelegates({
119 |           organizationSlug: 'this-dao-does-not-exist',
120 |         })
121 |       ).rejects.toThrow();
122 |     }, 60000);
123 | 
124 |     it('should handle governor ID with organization slug correctly', async () => {
125 |       const result = await tallyService.listDelegates({
126 |         organizationId: 'eip155:1:0x408ED6354d4973f66138C91495F2f2FCbd8724C3', // Uniswap governor ID
127 |         organizationSlug: 'uniswap',
128 |         limit: 5,
129 |       });
130 | 
131 |       expect(result).toBeDefined();
132 |       expect(result.delegates).toBeInstanceOf(Array);
133 |       expect(result.delegates.length).toBeLessThanOrEqual(5);
134 |       expect(result.pageInfo).toBeDefined();
135 |     }, 60000);
136 | 
137 |     it('should reject governor ID without organization slug', async () => {
138 |       await expect(tallyService.listDelegates({
139 |         organizationId: 'eip155:1:0x408ED6354d4973f66138C91495F2f2FCbd8724C3', // Uniswap governor ID
140 |         limit: 5,
141 |       })).rejects.toThrow('Organization slug is required when using a governor ID as organization ID');
142 |     });
143 |   });
144 | 
145 |   describe('formatDelegatorsList', () => {
146 |     it('should format delegators list correctly with token information', () => {
147 |       const mockDelegators = [{
148 |         chainId: 'eip155:1',
149 |         delegator: {
150 |           address: '0x123',
151 |           name: 'Test Delegator',
152 |           ens: 'test.eth'
153 |         },
154 |         blockNumber: 12345,
155 |         blockTimestamp: '2023-01-01T00:00:00Z',
156 |         votes: '1000000000000000000',
157 |         token: {
158 |           id: 'token-id',
159 |           name: 'Test Token',
160 |           symbol: 'TEST',
161 |           decimals: 18
162 |         }
163 |       }];
164 | 
165 |       const formatted = TallyService.formatDelegatorsList(mockDelegators);
166 |       expect(formatted).toContain('Test Delegator');
167 |       expect(formatted).toContain('0x123');
168 |       expect(formatted).toContain('1 TEST'); // Check formatted votes with token symbol
169 |       expect(formatted).toContain('Test Token');
170 |     });
171 | 
172 |     it('should format delegators list correctly without token information', () => {
173 |       const mockDelegators = [{
174 |         chainId: 'eip155:1',
175 |         delegator: {
176 |           address: '0x123',
177 |           name: 'Test Delegator',
178 |           ens: 'test.eth'
179 |         },
180 |         blockNumber: 12345,
181 |         blockTimestamp: '2023-01-01T00:00:00Z',
182 |         votes: '1000000000000000000'
183 |       }];
184 | 
185 |       const formatted = TallyService.formatDelegatorsList(mockDelegators);
186 |       expect(formatted).toContain('Test Delegator');
187 |       expect(formatted).toContain('0x123');
188 |       expect(formatted).toContain('1'); // Check formatted votes without token symbol
189 |     });
190 |   });
191 | }); 
```

--------------------------------------------------------------------------------
/src/services/__tests__/mcpClientTests/mcpServer.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Client } from "@modelcontextprotocol/sdk/client/index.js";
  2 | import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
  3 | import { z } from "zod";
  4 | import { spawn, type ChildProcess } from 'child_process';
  5 | import dotenv from "dotenv";
  6 | import request from 'supertest';
  7 | import { app } from '../../server';
  8 | 
  9 | // Load environment variables
 10 | dotenv.config();
 11 | 
 12 | const MAX_RETRIES = 5;
 13 | const BASE_DELAY = 1000;
 14 | const MAX_DELAY = 5000;
 15 | 
 16 | async function exponentialBackoff(retryCount: number): Promise<void> {
 17 |   const delay = Math.min(BASE_DELAY * Math.pow(2, retryCount), MAX_DELAY);
 18 |   await new Promise(resolve => setTimeout(resolve, delay));
 19 | }
 20 | 
 21 | class McpTestClient {
 22 |   private client: Client;
 23 |   private serverProcess: ChildProcess;
 24 |   private serverPath: string;
 25 |   private apiKey: string;
 26 |   
 27 |   constructor(serverPath: string) {
 28 |     this.serverPath = serverPath;
 29 |     this.apiKey = process.env.TALLY_API_KEY || "";
 30 |     if (!this.apiKey) {
 31 |       throw new Error("TALLY_API_KEY is not defined.");
 32 |     }
 33 |   }
 34 | 
 35 |   async start() {
 36 |     this.serverProcess = spawn('node', [this.serverPath], {
 37 |       env: { ...process.env, TALLY_API_KEY: this.apiKey },
 38 |       stdio: 'inherit'
 39 |     });
 40 | 
 41 |     
 42 | 
 43 |     this.serverProcess.on('data', (data) => {
 44 |       console.log(`Server stdout: ${data}`);
 45 |     });
 46 | 
 47 |     this.serverProcess.on('data', (data) => {
 48 |       console.error(`Server stderr: ${data}`);
 49 |     });
 50 | 
 51 |     this.serverProcess.on('close', (code) => {
 52 |       console.log(`Server process exited with code ${code}`);
 53 |     });
 54 | 
 55 |     this.serverProcess.on('error', (err) => {
 56 |       console.error('Server failed to start:', err);
 57 |     });
 58 | 
 59 |     // Wait for server to start
 60 |     await new Promise(resolve => setTimeout(resolve, 1000));
 61 |   }
 62 | 
 63 |   async connect() {
 64 |     const transport = new StdioClientTransport({ command: 'node', args: [this.serverPath] });
 65 |     this.client = new Client(
 66 |       {
 67 |         name: 'test-client',
 68 |         version: '1.0.0',
 69 |       },
 70 |       { capabilities: {}}
 71 |     );
 72 | 
 73 |     await this.client.connect(transport);
 74 |   }
 75 | 
 76 |   async request<T>(
 77 |     method: string,
 78 |     params: Record<string, any>,
 79 |     schema: z.ZodType<T>,
 80 |   ): Promise<T> {
 81 |     let retries = 0;
 82 |     let lastError: Error | null = null;
 83 | 
 84 |     while (retries < MAX_RETRIES) {
 85 |       try {
 86 |         const response = await this.client.request(
 87 |           { method, params },
 88 |           schema
 89 |         );
 90 |         return response;
 91 |       }
 92 |       catch (error) {
 93 |         lastError = error as Error;
 94 |         if (String(lastError).includes("429") || String(lastError).includes("rate limit")) {
 95 |           retries++;
 96 |           if (retries < MAX_RETRIES) {
 97 |             await exponentialBackoff(retries);
 98 |             continue;
 99 |           }
100 |         }
101 |         
102 |         console.error(`Request failed after ${retries} retries:`, lastError);
103 |         throw new Error(`Request failed after ${retries} retries: ${lastError.message}`);
104 |       }
105 |     }
106 |     throw new Error(`Max retries of ${MAX_RETRIES} reached`);
107 |   }
108 | 
109 |   async listTools(): Promise<any> {
110 |     const schema = z.object({
111 |       tools: z.array(
112 |         z.object({
113 |           name: z.string(),
114 |           description: z.string(),
115 |           inputSchema: z.object({
116 |             type: z.string(),
117 |             properties: z.record(z.any()).optional(),
118 |             required: z.array(z.string()).optional(),
119 |           }),
120 |         })
121 |       ),
122 |     });
123 |     return this.request("tools/list", {}, schema);
124 |   }
125 | 
126 |   async callTool(name: string, args: Record<string, any>): Promise<any> {
127 |     const schema = z.object({
128 |       content: z.array(
129 |         z.object({
130 |           type: z.string(),
131 |           text: z.string().optional(),
132 |         })
133 |       ),
134 |       pageInfo: z.object({
135 |         firstCursor: z.string().optional(),
136 |         lastCursor: z.string().optional(),
137 |         count: z.number().optional(),
138 |       }).optional(),
139 |       isError: z.boolean().optional()
140 |     });
141 |     return this.request("tools/call", { name, arguments: args }, schema);
142 |   }
143 | 
144 |   async close() {
145 |     if (this.client) {
146 |       await this.client.close();
147 |     }
148 |     if (this.serverProcess) {
149 |       this.serverProcess.kill();
150 |     }
151 |   }
152 | }
153 | 
154 | describe("MCP Server Tests", () => {
155 |   let mcpClient: McpTestClient;
156 | 
157 |   beforeEach(async () => {
158 |     const serverPath = "./build/index.js";
159 |     mcpClient = new McpTestClient(serverPath);
160 |     await mcpClient.start();
161 |     await mcpClient.connect();
162 |   });
163 | 
164 |   afterEach(async () => {
165 |     await mcpClient.close();
166 |   });
167 | 
168 |   test("should list available tools", async () => {
169 |     const tools = await mcpClient.listTools();
170 |     expect(tools.tools.length).toBeGreaterThan(0);
171 |     expect(tools.tools.some((t: any) => t.name === "get-dao")).toBe(true);
172 |   }, 30000);
173 | 
174 |   test("should fetch DAO information", async () => {
175 |     const result = await mcpClient.callTool("get-dao", { slug: "uniswap" });
176 |     expect(result).toBeDefined();
177 |     expect(result.content).toBeDefined();
178 |     expect(result.content[0].type).toBe("text");
179 |     expect(result.content[0].text).toContain("Uniswap (uniswap)");
180 |   }, 30000);
181 | 
182 |   test("should handle rate limits gracefully", async () => {
183 |     // Make multiple rapid requests to trigger rate limiting
184 |     const promises = Array(3).fill(null).map(() => 
185 |       mcpClient.callTool("get-dao", { slug: "uniswap" })
186 |     );
187 |     
188 |     const results = await Promise.all(promises);
189 |     results.forEach(result => {
190 |       expect(result.content[0].text).toContain("Uniswap");
191 |     });
192 |   }, 60000);
193 | 
194 |   test("should fetch address votes", async () => {
195 |     // Using a known address that has votes on Uniswap
196 |     const address = "0xb49f8b8613be240213c1827e2e576044ffec7948";
197 |     const organizationSlug = "uniswap";
198 | 
199 |     const result = await mcpClient.callTool("get-address-votes", {
200 |       address,
201 |       organizationSlug
202 |     });
203 | 
204 |     console.log("Result:", result);
205 |     // Verify the response structure
206 |     expect(result).toBeDefined();
207 |     expect(result.content).toBeDefined();
208 |     expect(Array.isArray(result.content)).toBe(true);
209 |     
210 |     // Each content item should be a text type with vote details
211 |     result.content.forEach((item: any) => {
212 |         
213 |       expect(item.type).toBe("text");
214 |       expect(item.text).toBeDefined();
215 |       
216 |       // Vote details should include all available fields
217 |       const text = item.text;
218 |       expect(text).toContain("Vote Details:");
219 |       expect(text).toContain("ID:");
220 |       expect(text).toContain("Type:");
221 |       expect(text).toContain("Amount:");
222 |       expect(text).toContain("Voter Address:");
223 |       expect(text).toContain("Proposal ID:");
224 | 
225 |       // Verify pagination info
226 |       expect(result.pageInfo).toBeDefined();
227 |     });
228 |   }, 30000);
229 | }); 
```

--------------------------------------------------------------------------------
/src/services/delegates/getDelegateStatement.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { GraphQLClient } from 'graphql-request';
  2 | import { DelegateStatement } from './delegates.types.js';
  3 | import { GraphQLError } from 'graphql';
  4 | import { getDAO } from '../organizations/getDAO.js';
  5 | import { gql } from 'graphql-request';
  6 | import { globalRateLimiter } from '../utils/rateLimiter.js';
  7 | import {
  8 |   TallyAPIError,
  9 |   RateLimitError,
 10 |   ResourceNotFoundError,
 11 |   ValidationError,
 12 |   GraphQLRequestError
 13 | } from '../errors/apiErrors.js';
 14 | 
 15 | const MAX_RETRIES = 5;
 16 | 
 17 | const GET_DELEGATE_STATEMENT_QUERY = gql`
 18 |   query DelegateStatement($input: DelegateInput!) {
 19 |     delegate(input: $input) {
 20 |       statement {
 21 |         id
 22 |         address
 23 |         organizationID
 24 |         statement
 25 |         statementSummary
 26 |         isSeekingDelegation
 27 |         discourseUsername
 28 |         discourseProfileLink
 29 |         issues {
 30 |           id
 31 |           name
 32 |         }
 33 |       }
 34 |     }
 35 |   }
 36 | `;
 37 | 
 38 | const GET_ADDRESS_HEADER_QUERY = gql`
 39 |   query AddressHeader($accountId: AccountID!) {
 40 |     account(id: $accountId) {
 41 |       address
 42 |       bio
 43 |       name
 44 |       picture
 45 |       twitter
 46 |     }
 47 |   }
 48 | `;
 49 | 
 50 | // Use discriminated union for input type
 51 | type GetDelegateStatementInput = {
 52 |   address: string;
 53 | } & (
 54 |   | { governorId: string; organizationSlug?: never }
 55 |   | { organizationSlug: string; governorId?: never }
 56 | );
 57 | 
 58 | interface AccountHeader {
 59 |   address: string;
 60 |   bio?: string;
 61 |   name?: string;
 62 |   picture?: string;
 63 |   twitter?: string;
 64 | }
 65 | 
 66 | interface DelegateStatementResponse {
 67 |   statement: DelegateStatement | null;
 68 |   account: AccountHeader | null;
 69 | }
 70 | 
 71 | export async function getDelegateStatement(
 72 |   client: GraphQLClient,
 73 |   input: GetDelegateStatementInput
 74 | ): Promise<DelegateStatementResponse | null> {
 75 |   // Input validation first
 76 |   if (!input.address) {
 77 |     throw new ValidationError('Address is required');
 78 |   }
 79 | 
 80 |   // Validate that only one of governorId or organizationSlug is provided
 81 |   if ('governorId' in input && 'organizationSlug' in input && input.governorId && input.organizationSlug) {
 82 |     throw new ValidationError('Cannot provide both governorId and organizationSlug');
 83 |   }
 84 | 
 85 |   if (!('governorId' in input) && !('organizationSlug' in input)) {
 86 |     throw new ValidationError('Either governorId or organizationSlug is required');
 87 |   }
 88 | 
 89 |   // Validate address format
 90 |   if (!/^0x[a-fA-F0-9]{40}$/.test(input.address)) {
 91 |     throw new ValidationError('Invalid address format');
 92 |   }
 93 | 
 94 |   let retries = 0;
 95 | 
 96 |   while (retries < MAX_RETRIES) {
 97 |     try {
 98 |       let governorId: string;
 99 |       let organizationId: string;
100 | 
101 |       if ('governorId' in input && input.governorId) {
102 |         // Validate governor ID format
103 |         if (!/^eip155:\d+:0x[a-fA-F0-9]{40}$/.test(input.governorId)) {
104 |           throw new ValidationError('Invalid governor ID format');
105 |         }
106 |         governorId = input.governorId;
107 |       } else if ('organizationSlug' in input && input.organizationSlug) {
108 |         // Wait for rate limit before getDAO request
109 |         await globalRateLimiter.waitForRateLimit();
110 |         const { organization: dao } = await getDAO(client, input.organizationSlug);
111 |         if (!dao.governorIds?.length) {
112 |           return null;
113 |         }
114 |         governorId = dao.governorIds[0];
115 |         organizationId = dao.id;
116 |       }
117 | 
118 |       // Format the account ID for the header query
119 |       const accountId = `eip155:1:${input.address.toLowerCase()}`;
120 | 
121 |       // Make both requests in parallel
122 |       const [statementResponse, accountResponse] = await Promise.all([
123 |         // Get delegate statement
124 |         (async () => {
125 |           await globalRateLimiter.waitForRateLimit();
126 |           const variables = {
127 |             input: {
128 |               address: input.address,
129 |               governorId,
130 |               ...(organizationId && { organizationId })
131 |             }
132 |           };
133 |           return client.request<{
134 |             delegate?: {
135 |               statement: DelegateStatement | null;
136 |             };
137 |           }>(GET_DELEGATE_STATEMENT_QUERY, variables);
138 |         })(),
139 | 
140 |         // Get account header
141 |         (async () => {
142 |           await globalRateLimiter.waitForRateLimit();
143 |           return client.request<{
144 |             account: AccountHeader | null;
145 |           }>(GET_ADDRESS_HEADER_QUERY, { accountId });
146 |         })()
147 |       ]);
148 | 
149 |       // Update rate limiter with response headers if available
150 |       if ('headers' in statementResponse) {
151 |         globalRateLimiter.updateFromHeaders(statementResponse.headers as Record<string, string>);
152 |       }
153 |       if ('headers' in accountResponse) {
154 |         globalRateLimiter.updateFromHeaders(accountResponse.headers as Record<string, string>);
155 |       }
156 | 
157 |       // If we don't have a statement, return null
158 |       if (!statementResponse.delegate?.statement) {
159 |         return null;
160 |       }
161 | 
162 |       // Return combined response
163 |       return {
164 |         statement: statementResponse.delegate.statement,
165 |         account: accountResponse.account
166 |       };
167 | 
168 |     } catch (error) {
169 |       if (error instanceof GraphQLError) {
170 |         const graphqlError = error as GraphQLError;
171 |         
172 |         // Handle rate limiting (429)
173 |         if (graphqlError.response?.status === 429) {
174 |           retries++;
175 |           if (retries < MAX_RETRIES) {
176 |             await globalRateLimiter.exponentialBackoff(retries);
177 |             continue;
178 |           }
179 |           throw new RateLimitError('Rate limit exceeded after retries', {
180 |             retries,
181 |             status: graphqlError.response.status
182 |           });
183 |         }
184 | 
185 |         // Handle other GraphQL errors
186 |         if (graphqlError.response?.errors) {
187 |           const errorMessage = graphqlError.response.errors[0]?.message;
188 |           if (errorMessage?.includes('not found')) {
189 |             return null;
190 |           }
191 |           if (errorMessage?.includes('not valid')) {
192 |             throw new ValidationError(errorMessage);
193 |           }
194 |         }
195 |       }
196 | 
197 |       // If we've reached here and it's already a known error type, rethrow it
198 |       if (error instanceof ValidationError || 
199 |           error instanceof ResourceNotFoundError || 
200 |           error instanceof RateLimitError ||
201 |           error instanceof TallyAPIError) {
202 |         throw error;
203 |       }
204 |       
205 |       // Otherwise, wrap it in a ValidationError for invalid inputs
206 |       if (error instanceof Error && 
207 |           (error.message.includes('not valid') || 
208 |            error.message.includes('invalid') || 
209 |            error.message.includes('not found'))) {
210 |         throw new ValidationError(error.message);
211 |       }
212 | 
213 |       // For any other unexpected errors
214 |       throw new TallyAPIError(`Failed to fetch delegate statement: ${error instanceof Error ? error.message : 'Unknown error'}`);
215 |     }
216 |   }
217 | 
218 |   throw new RateLimitError('Maximum retries exceeded');
219 | } 
```

--------------------------------------------------------------------------------
/src/services/__tests__/tally.service.delegators.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { TallyService } from '../tally.service';
  2 | import dotenv from 'dotenv';
  3 | 
  4 | dotenv.config();
  5 | 
  6 | const apiKey = process.env.TALLY_API_KEY;
  7 | if (!apiKey) {
  8 |   throw new Error('TALLY_API_KEY environment variable is required');
  9 | }
 10 | 
 11 | // Helper function to add delay between API calls
 12 | const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
 13 | 
 14 | describe('TallyService - getDelegators', () => {
 15 |   const service = new TallyService({ apiKey });
 16 | 
 17 |   // Test constants
 18 |   const UNISWAP_ORG_ID = '2206072050458560434';
 19 |   const UNISWAP_SLUG = 'uniswap';
 20 |   const VITALIK_ADDRESS = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045';
 21 | 
 22 |   // Add delay between each test
 23 |   beforeEach(async () => {
 24 |     await delay(1000); // 1 second delay between tests
 25 |   });
 26 | 
 27 |   it.only('should fetch delegators using organization ID', async () => {
 28 |     const result = await service.getDelegators({
 29 |       address: VITALIK_ADDRESS,
 30 |       organizationSlug: 'uniswap',
 31 |       limit: 5,
 32 |       sortBy: 'votes',
 33 |       isDescending: true
 34 |     });
 35 | 
 36 |     // Check response structure
 37 |     expect(result).toHaveProperty('delegators');
 38 |     expect(result).toHaveProperty('pageInfo');
 39 |     expect(Array.isArray(result.delegators)).toBe(true);
 40 |     
 41 |     // Check pageInfo structure
 42 |     expect(result.pageInfo).toHaveProperty('firstCursor');
 43 |     expect(result.pageInfo).toHaveProperty('lastCursor');
 44 | 
 45 |     // If there are delegators, check their structure
 46 |     if (result.delegators.length > 0) {
 47 |       const delegation = result.delegators[0];
 48 |       expect(delegation).toHaveProperty('chainId');
 49 |       expect(delegation).toHaveProperty('delegator');
 50 |       expect(delegation).toHaveProperty('blockNumber');
 51 |       expect(delegation).toHaveProperty('blockTimestamp');
 52 |       expect(delegation).toHaveProperty('votes');
 53 |       
 54 |       // Check delegator structure
 55 |       expect(delegation.delegator).toHaveProperty('address');
 56 |       
 57 |       // Check token structure if present
 58 |       if (delegation.token) {
 59 |         expect(delegation.token).toHaveProperty('id');
 60 |         expect(delegation.token).toHaveProperty('name');
 61 |         expect(delegation.token).toHaveProperty('symbol');
 62 |         expect(delegation.token).toHaveProperty('decimals');
 63 |       }
 64 |     }
 65 |   });
 66 | 
 67 |   it('should fetch delegators using organization slug', async () => {
 68 |     const result = await service.getDelegators({
 69 |       address: VITALIK_ADDRESS,
 70 |       organizationSlug: UNISWAP_SLUG,
 71 |       limit: 5,
 72 |       sortBy: 'votes',
 73 |       isDescending: true
 74 |     });
 75 | 
 76 |     expect(result).toHaveProperty('delegators');
 77 |     expect(result).toHaveProperty('pageInfo');
 78 |     expect(Array.isArray(result.delegators)).toBe(true);
 79 | 
 80 |     await delay(1000); // Add delay before second API call
 81 | 
 82 |     // Results should be the same whether using ID or slug
 83 |     const resultWithId = await service.getDelegators({
 84 |       address: VITALIK_ADDRESS,
 85 |       organizationId: UNISWAP_ORG_ID,
 86 |       limit: 5,
 87 |       sortBy: 'votes',
 88 |       isDescending: true
 89 |     });
 90 | 
 91 |     // Compare the results after sorting by blockNumber to ensure consistent comparison
 92 |     const sortByBlockNumber = (a: any, b: any) => a.blockNumber - b.blockNumber;
 93 |     const sortedSlugResults = [...result.delegators].sort(sortByBlockNumber);
 94 |     const sortedIdResults = [...resultWithId.delegators].sort(sortByBlockNumber);
 95 | 
 96 |     // Compare the first delegator if exists
 97 |     if (sortedSlugResults.length > 0 && sortedIdResults.length > 0) {
 98 |       expect(sortedSlugResults[0].blockNumber).toBe(sortedIdResults[0].blockNumber);
 99 |       expect(sortedSlugResults[0].votes).toBe(sortedIdResults[0].votes);
100 |     }
101 |   });
102 | 
103 |   it('should handle pagination correctly', async () => {
104 |     // First page with smaller limit to ensure multiple pages
105 |     const firstPage = await service.getDelegators({
106 |       address: VITALIK_ADDRESS,
107 |       organizationId: UNISWAP_ORG_ID, // Using ID instead of slug for consistency
108 |       limit: 1, // Request just 1 item to ensure we have more pages
109 |       sortBy: 'votes',
110 |       isDescending: true
111 |     });
112 | 
113 |     // Verify first page structure
114 |     expect(firstPage).toHaveProperty('delegators');
115 |     expect(firstPage).toHaveProperty('pageInfo');
116 |     expect(Array.isArray(firstPage.delegators)).toBe(true);
117 |     expect(firstPage.delegators.length).toBe(1); // Should have exactly 1 item
118 |     expect(firstPage.pageInfo).toHaveProperty('firstCursor');
119 |     expect(firstPage.pageInfo).toHaveProperty('lastCursor');
120 |     expect(firstPage.pageInfo.lastCursor).toBeTruthy(); // Ensure we have a cursor for next page
121 |     
122 |     // Store first page data for comparison
123 |     const firstPageDelegator = firstPage.delegators[0];
124 |     
125 |     await delay(1000); // Add delay before fetching second page
126 | 
127 |     // Only proceed if we have a valid cursor
128 |     if (firstPage.pageInfo.lastCursor) {
129 |       // Fetch second page using lastCursor from first page
130 |       const secondPage = await service.getDelegators({
131 |         address: VITALIK_ADDRESS,
132 |         organizationId: UNISWAP_ORG_ID,
133 |         limit: 1,
134 |         afterCursor: firstPage.pageInfo.lastCursor,
135 |         sortBy: 'votes',
136 |         isDescending: true
137 |       });
138 | 
139 |       // Verify second page structure
140 |       expect(secondPage).toHaveProperty('delegators');
141 |       expect(secondPage).toHaveProperty('pageInfo');
142 |       expect(Array.isArray(secondPage.delegators)).toBe(true);
143 | 
144 |       // If we got results in second page, verify they're different
145 |       if (secondPage.delegators.length > 0) {
146 |         const secondPageDelegator = secondPage.delegators[0];
147 |         // Ensure we got a different delegator
148 |         expect(secondPageDelegator.delegator.address).not.toBe(firstPageDelegator.delegator.address);
149 |         // Since we sorted by votes descending, second page votes should be less than or equal
150 |         expect(BigInt(secondPageDelegator.votes) <= BigInt(firstPageDelegator.votes)).toBe(true);
151 |       }
152 |     }
153 |   });
154 | 
155 |   it('should handle sorting by blockNumber', async () => {
156 |     const result = await service.getDelegators({
157 |       address: VITALIK_ADDRESS,
158 |       organizationSlug: UNISWAP_SLUG,
159 |       limit: 5,
160 |       sortBy: 'votes',
161 |       isDescending: true
162 |     });
163 | 
164 |     expect(result).toHaveProperty('delegators');
165 |     expect(Array.isArray(result.delegators)).toBe(true);
166 | 
167 |     // Verify the results are sorted
168 |     if (result.delegators.length > 1) {
169 |       const votes = result.delegators.map(d => BigInt(d.votes));
170 |       const isSorted = votes.every((v, i) => i === 0 || v <= votes[i - 1]);
171 |       expect(isSorted).toBe(true);
172 |     }
173 |   });
174 | 
175 |   it('should handle errors for invalid address', async () => {
176 |     await expect(service.getDelegators({
177 |       address: 'invalid-address',
178 |       organizationSlug: UNISWAP_SLUG
179 |     })).rejects.toThrow();
180 |   });
181 | 
182 |   it('should handle errors for invalid organization slug', async () => {
183 |     await expect(service.getDelegators({
184 |       address: VITALIK_ADDRESS,
185 |       organizationSlug: 'invalid-org-slug'
186 |     })).rejects.toThrow();
187 |   });
188 | 
189 |   it('should handle errors when neither organizationId/Slug nor governorId is provided', async () => {
190 |     await expect(service.getDelegators({
191 |       address: VITALIK_ADDRESS
192 |     })).rejects.toThrow('Either organizationId/organizationSlug or governorId must be provided');
193 |   });
194 | 
195 |   it('should format delegators list correctly', () => {
196 |     const mockDelegators = [{
197 |       chainId: 'eip155:1',
198 |       delegator: {
199 |         address: '0x123',
200 |         name: 'Test Delegator',
201 |         ens: 'test.eth'
202 |       },
203 |       blockNumber: 12345,
204 |       blockTimestamp: '2023-01-01T00:00:00Z',
205 |       votes: '1000000000000000000',
206 |       token: {
207 |         id: 'token-id',
208 |         name: 'Test Token',
209 |         symbol: 'TEST',
210 |         decimals: 18
211 |       }
212 |     }];
213 | 
214 |     const formatted = TallyService.formatDelegatorsList(mockDelegators);
215 |     expect(typeof formatted).toBe('string');
216 |     expect(formatted).toContain('Test Delegator');
217 |     expect(formatted).toContain('0x123');
218 |     expect(formatted).toContain('Test Token');
219 |   });
220 | }); 
```

--------------------------------------------------------------------------------
/src/services/__tests__/tally.service.proposals.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { TallyService } from '../tally.service';
  2 | import dotenv from 'dotenv';
  3 | 
  4 | dotenv.config();
  5 | 
  6 | const apiKey = process.env.TALLY_API_KEY;
  7 | if (!apiKey) {
  8 |   throw new Error('TALLY_API_KEY environment variable is required');
  9 | }
 10 | 
 11 | // Helper function to add delay between API calls
 12 | const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
 13 | 
 14 | describe('TallyService - Proposals', () => {
 15 |   const service = new TallyService({ apiKey });
 16 | 
 17 |   // Test constants
 18 |   const UNISWAP_ORG_ID = '2206072050458560434';
 19 |   const UNISWAP_GOVERNOR_ID = 'eip155:1:0x408ED6354d4973f66138C91495F2f2FCbd8724C3';
 20 | 
 21 |   // Add delay between each test
 22 |   beforeEach(async () => {
 23 |     await delay(1000); // 1 second delay between tests
 24 |   });
 25 | 
 26 |   describe('listProposals', () => {
 27 |     it('should list proposals with basic filters', async () => {
 28 |       const result = await service.listProposals({
 29 |         filters: {
 30 |           organizationId: UNISWAP_ORG_ID
 31 |         },
 32 |         page: {
 33 |           limit: 5
 34 |         }
 35 |       });
 36 | 
 37 |       // Check response structure
 38 |       expect(result).toHaveProperty('proposals');
 39 |       expect(result.proposals).toHaveProperty('nodes');
 40 |       expect(Array.isArray(result.proposals.nodes)).toBe(true);
 41 | 
 42 |       // If there are proposals, check their structure
 43 |       if (result.proposals.nodes.length > 0) {
 44 |         const proposal = result.proposals.nodes[0];
 45 |         expect(proposal).toHaveProperty('id');
 46 |         expect(proposal).toHaveProperty('onchainId');
 47 |         expect(proposal).toHaveProperty('status');
 48 |         expect(proposal).toHaveProperty('metadata');
 49 |         expect(proposal).toHaveProperty('voteStats');
 50 |         expect(proposal).toHaveProperty('governor');
 51 | 
 52 |         // Check metadata structure
 53 |         expect(proposal.metadata).toHaveProperty('title');
 54 |         expect(proposal.metadata).toHaveProperty('description');
 55 | 
 56 |         // Check governor structure
 57 |         expect(proposal.governor).toHaveProperty('id');
 58 |         expect(proposal.governor).toHaveProperty('name');
 59 |         expect(proposal.governor.organization).toHaveProperty('name');
 60 |         expect(proposal.governor.organization).toHaveProperty('slug');
 61 |       }
 62 |     });
 63 | 
 64 |     it('should handle pagination correctly', async () => {
 65 |       // First page with smaller limit
 66 |       const firstPage = await service.listProposals({
 67 |         filters: {
 68 |           organizationId: UNISWAP_ORG_ID
 69 |         },
 70 |         page: {
 71 |           limit: 2
 72 |         }
 73 |       });
 74 | 
 75 |       expect(firstPage.proposals.nodes.length).toBe(2);
 76 |       expect(firstPage.proposals.pageInfo).toHaveProperty('lastCursor');
 77 |       const firstPageIds = firstPage.proposals.nodes.map(p => p.id);
 78 | 
 79 |       await delay(1000);
 80 | 
 81 |       // Fetch second page
 82 |       const secondPage = await service.listProposals({
 83 |         filters: {
 84 |           organizationId: UNISWAP_ORG_ID
 85 |         },
 86 |         page: {
 87 |           limit: 2,
 88 |           afterCursor: firstPage.proposals.pageInfo.lastCursor
 89 |         }
 90 |       });
 91 | 
 92 |       expect(secondPage.proposals.nodes.length).toBe(2);
 93 |       const secondPageIds = secondPage.proposals.nodes.map(p => p.id);
 94 | 
 95 |       // Verify pages contain different proposals
 96 |       expect(firstPageIds).not.toEqual(secondPageIds);
 97 |     });
 98 | 
 99 |     it('should apply all filters correctly', async () => {
100 |       const result = await service.listProposals({
101 |         filters: {
102 |           organizationId: UNISWAP_ORG_ID,
103 |           governorId: UNISWAP_GOVERNOR_ID,
104 |           includeArchived: true,
105 |           isDraft: false
106 |         },
107 |         page: {
108 |           limit: 3
109 |         },
110 |         sort: {
111 |           isDescending: true,
112 |           sortBy: "id"
113 |         }
114 |       });
115 | 
116 |       expect(result.proposals.nodes.length).toBeLessThanOrEqual(3);
117 |       if (result.proposals.nodes.length > 1) {
118 |         // Verify sorting
119 |         const ids = result.proposals.nodes.map(p => BigInt(p.id));
120 |         const isSorted = ids.every((id, i) => i === 0 || id <= ids[i - 1]);
121 |         expect(isSorted).toBe(true);
122 |       }
123 |     });
124 |   });
125 | 
126 |   describe('getProposal', () => {
127 |     let proposalId: string;
128 | 
129 |     beforeAll(async () => {
130 |       // Get a real proposal ID from the list
131 |       const response = await service.listProposals({
132 |         filters: {
133 |           organizationId: UNISWAP_ORG_ID
134 |         },
135 |         page: {
136 |           limit: 1
137 |         }
138 |       });
139 | 
140 |       if (response.proposals.nodes.length === 0) {
141 |         throw new Error('No proposals found for testing');
142 |       }
143 | 
144 |       proposalId = response.proposals.nodes[0].id;
145 |       console.log('Using proposal ID:', proposalId);
146 |     });
147 | 
148 |     it('should get proposal by ID', async () => {
149 |       const result = await service.getProposal({
150 |         id: proposalId
151 |       });
152 | 
153 |       expect(result).toHaveProperty('proposal');
154 |       const proposal = result.proposal;
155 | 
156 |       // Check basic properties
157 |       expect(proposal).toHaveProperty('id');
158 |       expect(proposal).toHaveProperty('onchainId');
159 |       expect(proposal).toHaveProperty('status');
160 |       expect(proposal).toHaveProperty('metadata');
161 |       expect(proposal).toHaveProperty('voteStats');
162 |       expect(proposal).toHaveProperty('governor');
163 | 
164 |       // Check metadata
165 |       expect(proposal.metadata).toHaveProperty('title');
166 |       expect(proposal.metadata).toHaveProperty('description');
167 |       expect(proposal.metadata).toHaveProperty('discourseURL');
168 |       expect(proposal.metadata).toHaveProperty('snapshotURL');
169 | 
170 |       // Check vote stats
171 |       expect(Array.isArray(proposal.voteStats)).toBe(true);
172 |       if (proposal.voteStats.length > 0) {
173 |         expect(proposal.voteStats[0]).toHaveProperty('votesCount');
174 |         expect(proposal.voteStats[0]).toHaveProperty('votersCount');
175 |         expect(proposal.voteStats[0]).toHaveProperty('type');
176 |         expect(proposal.voteStats[0]).toHaveProperty('percent');
177 |       }
178 |     });
179 | 
180 |     it('should get proposal by onchain ID', async () => {
181 |       // First get a proposal with an onchain ID
182 |       const listResponse = await service.listProposals({
183 |         filters: {
184 |           organizationId: UNISWAP_ORG_ID
185 |         },
186 |         page: {
187 |           limit: 5
188 |         }
189 |       });
190 | 
191 |       const proposalWithOnchainId = listResponse.proposals.nodes.find(p => p.onchainId);
192 |       if (!proposalWithOnchainId) {
193 |         console.log('No proposal with onchain ID found, skipping test');
194 |         return;
195 |       }
196 | 
197 |       const result = await service.getProposal({
198 |         onchainId: proposalWithOnchainId.onchainId,
199 |         governorId: UNISWAP_GOVERNOR_ID
200 |       });
201 | 
202 |       expect(result).toHaveProperty('proposal');
203 |       expect(result.proposal.onchainId).toBe(proposalWithOnchainId.onchainId);
204 |     });
205 | 
206 |     it('should include archived proposals', async () => {
207 |       const result = await service.getProposal({
208 |         id: proposalId,
209 |         includeArchived: true
210 |       });
211 | 
212 |       expect(result).toHaveProperty('proposal');
213 |       expect(result.proposal.id).toBe(proposalId);
214 |     });
215 | 
216 |     it('should handle errors for invalid proposal ID', async () => {
217 |       await expect(service.getProposal({
218 |         id: 'invalid-id'
219 |       })).rejects.toThrow();
220 |     });
221 | 
222 |     it('should handle errors when using onchainId without governorId', async () => {
223 |       await expect(service.getProposal({
224 |         onchainId: '1'
225 |       })).rejects.toThrow();
226 |     });
227 | 
228 |     it('should format proposal correctly', () => {
229 |       const mockProposal = {
230 |         id: '123',
231 |         onchainId: '1',
232 |         status: 'active' as const,
233 |         quorum: '1000000',
234 |         metadata: {
235 |           title: 'Test Proposal',
236 |           description: 'Test Description',
237 |           discourseURL: 'https://example.com',
238 |           snapshotURL: 'https://snapshot.org'
239 |         },
240 |         start: {
241 |           timestamp: '2023-01-01T00:00:00Z'
242 |         },
243 |         end: {
244 |           timestamp: '2023-01-08T00:00:00Z'
245 |         },
246 |         executableCalls: [{
247 |           value: '0',
248 |           target: '0x123',
249 |           calldata: '0x',
250 |           signature: 'test()',
251 |           type: 'call'
252 |         }],
253 |         voteStats: [{
254 |           votesCount: '1000000000000000000',
255 |           votersCount: 100,
256 |           type: 'for' as const,
257 |           percent: 75
258 |         }],
259 |         governor: {
260 |           id: 'gov-1',
261 |           chainId: 'eip155:1',
262 |           name: 'Test Governor',
263 |           token: {
264 |             decimals: 18
265 |           },
266 |           organization: {
267 |             name: 'Test Org',
268 |             slug: 'test'
269 |           }
270 |         },
271 |         proposer: {
272 |           address: '0x123',
273 |           name: 'Test Proposer',
274 |           picture: 'https://example.com/avatar.png'
275 |         }
276 |       };
277 | 
278 |       const formatted = TallyService.formatProposal(mockProposal);
279 |       expect(typeof formatted).toBe('string');
280 |       expect(formatted).toContain('Test Proposal');
281 |       expect(formatted).toContain('Test Description');
282 |       expect(formatted).toContain('Test Governor');
283 |     });
284 |   });
285 | }); 
```

--------------------------------------------------------------------------------
/Tally-API-Docs-Types.txt:
--------------------------------------------------------------------------------

```
  1 | # Tally API Types Reference
  2 | 
  3 | This document provides a comprehensive list of types and their descriptions for the Tally API. It is intended to be used by Large Language Models (LLMs) to understand the available data structures.
  4 | 
  5 | **Types:**
  6 | 
  7 | ```graphql
  8 | type Account {
  9 |   id: ID!
 10 |   address: String!
 11 |   ens: String
 12 |   twitter: String
 13 |   name: String!
 14 |   bio: String!
 15 |   picture: String
 16 |   safes: [AccountID!]
 17 |   type: AccountType!
 18 |   votes(governorId: AccountID!): Uint256!
 19 |   proposalsCreatedCount(input: ProposalsCreatedCountInput!): Int!
 20 | }
 21 | 
 22 | # AccountID: A CAIP-10 compliant account id. (e.g., "eip155:1:0x7e90e03654732abedf89Faf87f05BcD03ACEeFdc")
 23 | scalar AccountID
 24 | 
 25 | # AccountType: An enum indicating the type of account (EOA or SAFE)
 26 | enum AccountType {
 27 |     EOA
 28 |     SAFE
 29 | }
 30 | 
 31 | # Address: A 20 byte Ethereum address, represented as 0x-prefixed hexadecimal. (e.g., "0x1234567800000000000000000000000000000abc")
 32 | scalar Address
 33 | 
 34 | type Allocation {
 35 |   account: Account!
 36 |   amount: Uint256!
 37 |   percent: Float!
 38 | }
 39 | 
 40 | # Any: A scalar type to represent any data
 41 | scalar Any
 42 | 
 43 | # AssetID: A CAIP-19 compliant asset id. (e.g., "eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f")
 44 | scalar AssetID
 45 | 
 46 | type Block {
 47 |   id: BlockID!
 48 |   number: Int!
 49 |   timestamp: Timestamp!
 50 |   ts: Timestamp!
 51 | }
 52 | 
 53 | # BlockID: A ChainID scoped identifier for identifying blocks across chains. Ex: eip155:1:15672.
 54 | scalar BlockID
 55 | 
 56 | # BlockOrTimestamp:  A union type that represents either a Block or a BlocklessTimestamp.
 57 | union BlockOrTimestamp = Block | BlocklessTimestamp
 58 | 
 59 | type BlocklessTimestamp {
 60 |   timestamp: Timestamp!
 61 | }
 62 | 
 63 | # Boolean: Represents a `true` or `false` value.
 64 | scalar Boolean
 65 | 
 66 | # Bytes: An arbitrary length binary string, represented as 0x-prefixed hexadecimal. (e.g., "0x4321abcd").
 67 | scalar Bytes
 68 | 
 69 | type Chain {
 70 |   id: ChainID!
 71 |   layer1Id: ChainID
 72 |   name: String!
 73 |   mediumName: String!
 74 |   shortName: String!
 75 |   blockTime: Float!
 76 |   isTestnet: Boolean!
 77 |   nativeCurrency: NativeCurrency!
 78 |   chain: String!
 79 |   useLayer1VotingPeriod: Boolean!
 80 | }
 81 | 
 82 | # ChainID: CAIP-2 compliant chain id. (e.g., "eip155:1").
 83 | scalar ChainID
 84 | 
 85 | type CompetencyFieldDescriptor {
 86 |   id: IntID!
 87 |   name: String!
 88 |   description: String!
 89 | }
 90 | 
 91 | type Contracts {
 92 |   governor: GovernorContract!
 93 |   tokens: [TokenContract!]!
 94 | }
 95 | 
 96 | type Contributor {
 97 |   id: IntID!
 98 |   account: Account!
 99 |   isCurator: Boolean!
100 |   isApplyingForCouncil: Boolean!
101 |   competencyFieldDescriptors: [CompetencyFieldDescriptor!]!
102 |   bio: UserBio!
103 | }
104 | 
105 | type DataDecoded {
106 |   method: String!
107 |   parameters: [Parameter!]!
108 | }
109 | 
110 | # Date: A date in the format ISO 8601 format, e.g. YYYY-MM-DD. (e.g., "2022-09-22")
111 | scalar Date
112 | 
113 | type DecodedCalldata {
114 |   signature: String!
115 |   parameters: [DecodedParameter!]!
116 | }
117 | 
118 | type DecodedParameter {
119 |   name: String!
120 |   type: String!
121 |   value: String!
122 | }
123 | 
124 | type Delegate {
125 |     id: IntID!
126 |     account: Account!
127 |     chainId: ChainID
128 |     delegatorsCount: Int!
129 |     governor: Governor
130 |     organization: Organization
131 |     statement: DelegateStatement
132 |     token: Token
133 |     votesCount(blockNumber: Int): Uint256!
134 |   }
135 | 
136 | input DelegateInput {
137 |   address: Address!
138 |   governorId: AccountID
139 |   organizationId: IntID
140 | }
141 | 
142 | type DelegateStatement {
143 |   id: IntID!
144 |   address: Address!
145 |   organizationID: IntID!
146 |   statement: String!
147 |   statementSummary: String
148 |   isSeekingDelegation: Boolean
149 |   issues: [Issue!]
150 | }
151 | 
152 | input DelegatesFiltersInput {
153 |   address: Address
154 |   governorId: AccountID
155 |   hasVotes: Boolean
156 |   hasDelegators: Boolean
157 |   issueIds: [IntID!]
158 |   isSeekingDelegation: Boolean
159 |   organizationId: IntID
160 | }
161 | 
162 | input DelegatesInput {
163 |   filters: DelegatesFiltersInput!
164 |   page: PageInput
165 |   sort: DelegatesSortInput
166 | }
167 | 
168 | enum DelegatesSortBy {
169 |   id
170 |   votes
171 |   delegators
172 |   prioritized
173 | }
174 | 
175 | input DelegatesSortInput {
176 |   isDescending: Boolean!
177 |   sortBy: DelegatesSortBy!
178 | }
179 | 
180 | type Delegation {
181 |   id: IntID!
182 |   blockNumber: Int!
183 |   blockTimestamp: Timestamp!
184 |   chainId: ChainID!
185 |   delegator: Account!
186 |   delegate: Account!
187 |   organization: Organization!
188 |   token: Token!
189 |   votes: Uint256!
190 | }
191 | 
192 | input DelegationInput {
193 |   address: Address!
194 |   tokenId: AssetID!
195 | }
196 | 
197 | input DelegationsFiltersInput {
198 |   address: Address!
199 |     governorId: AccountID
200 |    organizationId: IntID
201 | }
202 | 
203 | input DelegationsInput {
204 |   filters: DelegationsFiltersInput!
205 |   page: PageInput
206 |   sort: DelegationsSortInput
207 | }
208 | 
209 | enum DelegationsSortBy {
210 |   id
211 |   votes
212 | }
213 | 
214 | input DelegationsSortInput {
215 |   isDescending: Boolean!
216 |   sortBy: DelegationsSortBy!
217 | }
218 | 
219 | type Eligibility {
220 |   status: EligibilityStatus!
221 |   proof: [String!]
222 |   amount: Uint256
223 |   tx: HashID
224 | }
225 | 
226 | enum EligibilityStatus {
227 |     NOTELIGIBLE
228 |     ELIGIBLE
229 |     CLAIMED
230 | }
231 | 
232 | type EndorsementService {
233 |   id: IntID!
234 |   competencyFields: [CompetencyFieldDescriptor!]!
235 | }
236 | 
237 | type ExecutableCall {
238 |    calldata: Bytes!
239 |     chainId: ChainID!
240 |    index: Int!
241 |     signature: String
242 |     target: Address!
243 |     type: ExecutableCallType
244 |     value: Uint256!
245 |     decodedCalldata: DecodedCalldata
246 |   }
247 | 
248 | enum ExecutableCallType {
249 |     custom
250 |     erc20transfer
251 |     erc20transferarbitrum
252 |     empty
253 |     nativetransfer
254 |     orcamanagepod
255 |     other
256 |     reward
257 |     swap
258 | }
259 | 
260 | # Float: A signed double-precision fractional values as specified by IEEE 754. (e.g., 987.65)
261 | scalar Float
262 | 
263 | type Governor {
264 |     id: AccountID!
265 |     chainId: ChainID!
266 |     contracts: Contracts!
267 |     isIndexing: Boolean!
268 |     isBehind: Boolean!
269 |     isPrimary: Boolean!
270 |     kind: GovernorKind!
271 |     name: String!
272 |     organization: Organization!
273 |     proposalStats: ProposalStats!
274 |     parameters: GovernorParameters!
275 |     quorum: Uint256!
276 |     slug: String!
277 |     timelockId: AccountID
278 |     tokenId: AssetID!
279 |     token: Token!
280 |     type: GovernorType!
281 |     delegatesCount: Int!
282 |     delegatesVotesCount: Uint256!
283 |     tokenOwnersCount: Int!
284 |     metadata: GovernorMetadata
285 |   }
286 | 
287 | type GovernorContract {
288 |   address: Address!
289 |   type: GovernorType!
290 | }
291 | 
292 | input GovernorInput {
293 |   id: AccountID
294 |   slug: String
295 | }
296 | 
297 | enum GovernorKind {
298 |     single
299 |     multiprimary
300 |     multisecondary
301 |     multiother
302 |     hub
303 |     spoke
304 | }
305 | 
306 | type GovernorMetadata {
307 |     description: String
308 | }
309 | 
310 | type GovernorParameters {
311 |     quorumVotes: Uint256
312 |     proposalThreshold: Uint256
313 |     votingDelay: Uint256
314 |     votingPeriod: Uint256
315 |     gracePeriod: Uint256
316 |     quorumNumerator: Uint256
317 |     quorumDenominator: Uint256
318 |     clockMode: String
319 |     nomineeVettingDuration: Uint256
320 |      fullWeightDuration: Uint256
321 |   }
322 | 
323 | enum GovernorType {
324 |     governoralpha
325 |     governorbravo
326 |     openzeppelingovernor
327 |     aave
328 |      nounsfork
329 |         nomineeelection
330 |         memberelection
331 |         hub
332 |         spoke
333 | }
334 | 
335 | input GovernorsFiltersInput {
336 |   organizationId: IntID!
337 |   includeInactive: Boolean
338 |     excludeSecondary: Boolean
339 | }
340 | 
341 | input GovernorsInput {
342 |   filters: GovernorsFiltersInput
343 |   page: PageInput
344 |   sort: GovernorsSortInput
345 | }
346 | 
347 | enum GovernorsSortBy {
348 |   id
349 | }
350 | 
351 | input GovernorsSortInput {
352 |   isDescending: Boolean!
353 |   sortBy: GovernorsSortBy!
354 | }
355 | 
356 | # Hash: For identifying transactions on a chain. Ex: 0xDEAD.
357 | scalar Hash
358 | 
359 | # HashID: A ChainID scoped identifier for identifying transactions across chains. Ex: eip155:1:0xDEAD.
360 | scalar HashID
361 | 
362 | # ID: The ID scalar type represents a unique identifier
363 | scalar ID
364 | 
365 | # Int: The Int scalar type represents non-fractional signed whole numeric values.
366 | scalar Int
367 | 
368 | # IntID: A 64bit integer as a string - this is larger than Javascript's number.
369 | scalar IntID
370 | 
371 | type Issue {
372 |   id: IntID!
373 |   organizationId: IntID!
374 |   name: String!
375 |   description: String!
376 | }
377 | 
378 | type Member {
379 |   id: ID!
380 |   account: Account!
381 |   organization: Organization!
382 | }
383 | 
384 | type NativeCurrency {
385 |   name: String!
386 |   symbol: String!
387 |   decimals: Int!
388 | }
389 | 
390 | # Node: Union of all node types that are paginated.
391 | union Node =
392 |     | Delegate
393 |     | Organization
394 |     | Member
395 |     | Delegation
396 |     | Governor
397 |     | Proposal
398 |     | Vote
399 |     | StakeEvent
400 |     | StakeEarning
401 |     | Contributor
402 |     | Allocation
403 | 
404 | type Organization {
405 |   id: IntID!
406 |   slug: String!
407 |   name: String!
408 |   chainIds: [ChainID!]!
409 |   tokenIds: [AssetID!]!
410 |   governorIds: [AccountID!]!
411 |   metadata: OrganizationMetadata
412 |   creator: Account
413 |   hasActiveProposals: Boolean!
414 |   proposalsCount: Int!
415 |   delegatesCount: Int!
416 |   delegatesVotesCount: Uint256!
417 |   tokenOwnersCount: Int!
418 |   endorsementService: EndorsementService
419 | }
420 | 
421 | input OrganizationInput {
422 |   id: IntID
423 |   slug: String
424 | }
425 | 
426 | type OrganizationMetadata {
427 |     color: String
428 |     description: String
429 |     icon: String
430 |      socials: Socials
431 |      karmaName: String
432 | }
433 | 
434 | input OrganizationsFiltersInput {
435 |   address: Address
436 |   chainId: ChainID
437 |   hasLogo: Boolean
438 |     isMember: Boolean
439 | }
440 | 
441 | input OrganizationsInput {
442 |   filters: OrganizationsFiltersInput
443 |   page: PageInput
444 |   sort: OrganizationsSortInput
445 | }
446 | 
447 | enum OrganizationsSortBy {
448 |     id
449 |     name
450 |     explore
451 |     popular
452 | }
453 | 
454 | input OrganizationsSortInput {
455 |   isDescending: Boolean!
456 |   sortBy: OrganizationsSortBy!
457 | }
458 | 
459 | type PageInfo {
460 |     firstCursor: String
461 |     lastCursor: String
462 |     count: Int
463 | }
464 | 
465 | input PageInput {
466 |   afterCursor: String
467 |   beforeCursor: String
468 |   limit: Int
469 | }
470 | 
471 | # PaginatedOutput: Wraps a list of nodes and the pagination info
472 | type PaginatedOutput {
473 |     nodes: [Node!]!
474 |     pageInfo: PageInfo!
475 | }
476 | 
477 | type Parameter {
478 |   name: String!
479 |   type: String!
480 |   value: Any!
481 |     valueDecoded: [ValueDecoded!]
482 | }
483 | 
484 | type Proposal {
485 |     id: IntID!
486 |     onchainId: String
487 |     block: Block
488 |     chainId: ChainID!
489 |     creator: Account
490 |     end: BlockOrTimestamp!
491 |     events: [ProposalEvent!]!
492 |     executableCalls: [ExecutableCall!]
493 |     governor: Governor!
494 |     metadata: ProposalMetadata!
495 |     organization: Organization!
496 |     proposer: Account
497 |     quorum: Uint256
498 |     status: ProposalStatus!
499 |     start: BlockOrTimestamp!
500 |     voteStats: [VoteStats!]
501 | }
502 | 
503 | type ProposalEvent {
504 |     block: Block!
505 |     chainId: ChainID!
506 |     createdAt: Timestamp!
507 |     type: ProposalEventType!
508 |     txHash: Hash!
509 | }
510 | 
511 | enum ProposalEventType {
512 |     activated
513 |     canceled
514 |     created
515 |     defeated
516 |     drafted
517 |     executed
518 |     expired
519 |     extended
520 |     pendingexecution
521 |     queued
522 |     succeeded
523 |   callexecuted
524 |         crosschainexecuted
525 | }
526 | 
527 | input ProposalInput {
528 |   id: IntID
529 |   onchainId: String
530 |   governorId: AccountID
531 |     includeArchived: Boolean
532 |     isLatest: Boolean
533 | }
534 | 
535 | type ProposalMetadata {
536 |     title: String
537 |     description: String
538 |     eta: Int
539 |     ipfsHash: String
540 |         previousEnd: Int
541 |         timelockId: AccountID
542 |         txHash: Hash
543 |         discourseURL: String
544 |     snapshotURL: String
545 | }
546 | 
547 | type ProposalStats {
548 |     total: Int!
549 |     active: Int!
550 |     failed: Int!
551 |     passed: Int!
552 | }
553 | 
554 | enum ProposalStatus {
555 |     active
556 |     archived
557 |     canceled
558 |     callexecuted
559 |     defeated
560 |     draft
561 |     executed
562 |     expired
563 |     extended
564 |     pending
565 |     queued
566 |     pendingexecution
567 |     submitted
568 |     succeeded
569 |         crosschainexecuted
570 | }
571 | 
572 | input ProposalsCreatedCountInput {
573 |   governorId: AccountID
574 |     organizationId: IntID
575 | }
576 | 
577 | input ProposalsFiltersInput {
578 |   governorId: AccountID
579 |   includeArchived: Boolean
580 |     isDraft: Boolean
581 |   organizationId: IntID
582 |   proposer: Address
583 | }
584 | 
585 | input ProposalsInput {
586 |   filters: ProposalsFiltersInput
587 |   page: PageInput
588 |   sort: ProposalsSortInput
589 | }
590 | 
591 | enum ProposalsSortBy {
592 |     id
593 | }
594 | 
595 | input ProposalsSortInput {
596 |   isDescending: Boolean!
597 |   sortBy: ProposalsSortBy!
598 | }
599 | 
600 | enum Role {
601 |     ADMIN
602 |     USER
603 | }
604 | 
605 | type StakeEarning {
606 |    amount: Uint256!
607 |   date: Date!
608 | }
609 | 
610 | type StakeEvent {
611 |   amount: Uint256!
612 |     block: Block!
613 |   type: StakeEventType!
614 | }
615 | 
616 | enum StakeEventType {
617 |   deposit
618 |   withdraw
619 | }
620 | 
621 | # String: The String scalar type represents textual data, represented as UTF-8 character sequences
622 | scalar String
623 | 
624 | # Timestamp: Timestamp is an RFC3339 string.
625 | scalar Timestamp
626 | 
627 | type Token {
628 |     id: AssetID!
629 |     type: TokenType!
630 |     name: String!
631 |     symbol: String!
632 |     supply: Uint256!
633 |     decimals: Int!
634 |     eligibility: Eligibility
635 |     isIndexing: Boolean!
636 |     isBehind: Boolean!
637 |   }
638 | 
639 | type TokenContract {
640 |   address: Address!
641 |   type: TokenType!
642 | }
643 | 
644 | input TokenInput {
645 |     id: AssetID!
646 | }
647 | 
648 | enum TokenType {
649 |     ERC20
650 |     ERC721
651 |         ERC20AAVE
652 |         SOLANASPOKETOKEN
653 | }
654 | 
655 | # Uint256: Uint256 is a large unsigned integer represented as a string.
656 | scalar Uint256
657 | 
658 | type UserBio {
659 |   value: String!
660 |     summary: String!
661 | }
662 | 
663 | type ValueDecoded {
664 |   operation: Int!
665 |   to: String!
666 |   value: String!
667 |   data: String!
668 |   dataDecoded: DataDecoded
669 | }
670 | 
671 | type Vote {
672 |   id: IntID!
673 |   amount: Uint256!
674 |   block: Block!
675 |   chainId: ChainID!
676 |   isBridged: Boolean
677 |   proposal: Proposal!
678 |   reason: String
679 |   type: VoteType!
680 |   txHash: Hash!
681 |   voter: Account!
682 | }
683 | 
684 | type VoteStats {
685 |   type: VoteType!
686 |   votesCount: Uint256!
687 |   votersCount: Int!
688 |     percent: Float!
689 | }
690 | 
691 | enum VoteType {
692 |     abstain
693 |     against
694 |     for
695 |     pendingabstain
696 |     pendingagainst
697 |     pendingfor
698 | }
699 | 
700 | input VotesFiltersInput {
701 |   proposalId: IntID
702 |     proposalIds: [IntID!]
703 |   voter: Address
704 |     includePendingVotes: Boolean
705 |     type: VoteType
706 | }
707 | 
708 | input VotesInput {
709 |   filters: VotesFiltersInput
710 |   page: PageInput
711 |   sort: VotesSortInput
712 | }
713 | 
714 | enum VotesSortBy {
715 |     id
716 |     amount
717 | }
718 | 
719 | input VotesSortInput {
720 |   isDescending: Boolean!
721 |   sortBy: VotesSortBy!
722 | }
723 | 
```

--------------------------------------------------------------------------------
/src/services/tally.service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { GraphQLClient } from "graphql-request";
  2 | import { getDAO } from "./organizations/getDAO.js";
  3 | import { listDAOs } from "./organizations/listDAOs.js";
  4 | import { listProposals } from "./proposals/listProposals.js";
  5 | import { getProposal } from "./proposals/getProposal.js";
  6 | import { getProposalVoters } from "./proposals/getProposalVoters.js";
  7 | import { getProposalTimeline } from "./proposals/getProposalTimeline.js";
  8 | import { getProposalSecurityAnalysis } from "./proposals/getProposalSecurityAnalysis.js";
  9 | import { listDelegates } from "./delegates/listDelegates.js";
 10 | import { getAddressProposals } from "./addresses/getAddressProposals.js";
 11 | import { getAddressDAOProposals } from "./addresses/getAddressDAOProposals.js";
 12 | import { getAddressVotes } from "./addresses/getAddressVotes.js";
 13 | import { getAddressCreatedProposals } from "./addresses/getAddressCreatedProposals.js";
 14 | import { getAddressMetadata } from "./addresses/getAddressMetadata.js";
 15 | import { getAddressGovernances } from "./addresses/getAddressGovernances.js";
 16 | import { getAddressReceivedDelegations } from "./addresses/getAddressReceivedDelegations.js";
 17 | import { getDelegateStatement } from "./delegates/getDelegateStatement.js";
 18 | import { getDelegators } from "./delegators/getDelegators.js";
 19 | import type {
 20 |   Organization,
 21 |   OrganizationsResponse,
 22 |   ListDAOsParams,
 23 |   PageInfo,
 24 |   Token,
 25 | } from "./organizations/organizations.types.js";
 26 | import type { Delegate } from "./delegates/delegates.types.js";
 27 | import type {
 28 |   Delegation,
 29 |   GetDelegatorsParams,
 30 |   TokenInfo,
 31 | } from "./delegators/delegators.types.js";
 32 | import type { GetAddressReceivedDelegationsInput } from "./addresses/addresses.types.js";
 33 | import type { DelegateStatement } from "./delegates/delegates.types.js";
 34 | import type {
 35 |   ProposalsInput,
 36 |   ProposalsResponse,
 37 |   ProposalInput,
 38 |   ProposalDetailsResponse,
 39 | } from "./proposals/index.js";
 40 | import type {
 41 |   GetProposalVotersInput,
 42 |   ProposalVotersResponse,
 43 | } from "./proposals/getProposalVoters.types.js";
 44 | import type {
 45 |   GetProposalTimelineInput,
 46 |   ProposalTimelineResponse,
 47 | } from "./proposals/getProposalTimeline.types.js";
 48 | import type {
 49 |   GetProposalSecurityAnalysisInput,
 50 |   ProposalSecurityAnalysisResponse,
 51 | } from "./proposals/getProposalSecurityAnalysis.types.js";
 52 | import type {
 53 |   AddressProposalsInput,
 54 |   AddressProposalsResponse,
 55 |   AddressDAOProposalsInput,
 56 |   AddressDAOProposalsResponse,
 57 |   AddressVotesInput,
 58 |   AddressVotesResponse,
 59 |   AddressCreatedProposalsInput,
 60 |   AddressCreatedProposalsResponse,
 61 |   AddressMetadataInput,
 62 |   AddressMetadataResponse,
 63 |   AddressGovernancesInput,
 64 |   AddressGovernancesResponse,
 65 | } from "./addresses/addresses.types.js";
 66 | import { getDAOTokens } from "./organizations/getDAO.js";
 67 | import { getProposalVotesCast } from "./proposals/getProposalVotesCast.js";
 68 | import { getProposalVotesCastList } from "./proposals/getProposalVotesCastList.js";
 69 | import { getGovernanceProposalsStats } from "./proposals/getGovernanceProposalsStats.js";
 70 | import type {
 71 |   GetProposalVotesCastInput,
 72 |   ProposalVotesCastResponse,
 73 | } from "./proposals/getProposalVotesCast.types.js";
 74 | import type {
 75 |   GetProposalVotesCastListInput,
 76 |   ProposalVotesCastListResponse,
 77 | } from "./proposals/getProposalVotesCastList.types.js";
 78 | import type { GovernanceProposalsStatsResponse } from "./proposals/proposals.types.js";
 79 | import type { ListProposalsParams } from "./proposals/listProposals.types.js";
 80 | import type { ListDelegatesParams } from "./delegates/delegates.types.js";
 81 | 
 82 | export interface TallyServiceConfig {
 83 |   apiKey: string;
 84 |   baseUrl?: string;
 85 | }
 86 | 
 87 | export interface GetAddressReceivedDelegationsOutput {
 88 |   nodes: Array<{
 89 |     id: string;
 90 |     votes: string;
 91 |     delegator: {
 92 |       id: string;
 93 |       address: string;
 94 |     };
 95 |   }>;
 96 |   pageInfo: {
 97 |     firstCursor: string | null;
 98 |     lastCursor: string | null;
 99 |     count: number;
100 |   };
101 |   totalCount: number;
102 | }
103 | 
104 | export type GetDelegateStatementInput = {
105 |   address: string;
106 | } & (
107 |   | { governorId: string; organizationSlug?: never }
108 |   | { organizationSlug: string; governorId?: never }
109 | );
110 | 
111 | export class TallyService {
112 |   private client: GraphQLClient;
113 | 
114 |   constructor(config: TallyServiceConfig) {
115 |     this.client = new GraphQLClient(
116 |       config.baseUrl || "https://api.tally.xyz/query",
117 |       {
118 |         headers: {
119 |           "Content-Type": "application/json",
120 |           "api-key": config.apiKey,
121 |         },
122 |       }
123 |     );
124 |   }
125 | 
126 |   async listProposals(params: ListProposalsParams): Promise<ProposalsResponse> {
127 |     return listProposals(this.client, params);
128 |   }
129 | 
130 |   async getDAO(slug: string): Promise<Organization> {
131 |     const { organization } = await getDAO(this.client, slug);
132 |     return {
133 |       id: organization.id,
134 |       name: organization.name,
135 |       slug: organization.slug,
136 |       chainIds: organization.chainIds,
137 |       tokenIds: organization.tokenIds,
138 |       governorIds: organization.governorIds,
139 |       tokenOwnersCount: organization.tokenOwnersCount,
140 |       delegatesCount: organization.delegatesCount,
141 |       proposalsCount: organization.proposalsCount,
142 |       hasActiveProposals: organization.hasActiveProposals,
143 |       metadata: organization.metadata,
144 |       delegatesVotesCount: organization.delegatesVotesCount || 0,
145 |     };
146 |   }
147 | 
148 |   async getDAOTokens(tokenIds: string[]): Promise<Token[]> {
149 |     return getDAOTokens(this.client, tokenIds);
150 |   }
151 | 
152 |   async listDAOs(params: ListDAOsParams = {}): Promise<OrganizationsResponse> {
153 |     return listDAOs(this.client, params);
154 |   }
155 | 
156 |   async listDelegates(input: ListDelegatesParams) {
157 |     if (!input.organizationSlug) {
158 |       throw new Error("organizationSlug must be a string");
159 |     }
160 |     return listDelegates(this.client, input);
161 |   }
162 | 
163 |   async getProposal(input: ProposalInput): Promise<ProposalDetailsResponse> {
164 |     return getProposal(this.client, input);
165 |   }
166 | 
167 |   async getProposalVoters(
168 |     input: GetProposalVotersInput
169 |   ): Promise<ProposalVotersResponse> {
170 |     if (!input.proposalId) {
171 |       throw new Error("proposalId is required");
172 |     }
173 |     return getProposalVoters(this.client, input);
174 |   }
175 | 
176 |   async getProposalTimeline(
177 |     input: GetProposalTimelineInput
178 |   ): Promise<ProposalTimelineResponse> {
179 |     if (!input.proposalId) {
180 |       throw new Error("proposalId is required");
181 |     }
182 |     return getProposalTimeline(this.client, input);
183 |   }
184 | 
185 |   async getProposalSecurityAnalysis(
186 |     input: GetProposalSecurityAnalysisInput
187 |   ): Promise<ProposalSecurityAnalysisResponse> {
188 |     if (!input.proposalId) {
189 |       throw new Error("proposalId is required");
190 |     }
191 |     return getProposalSecurityAnalysis(this.client, input);
192 |   }
193 | 
194 |   async getAddressProposals(
195 |     input: AddressProposalsInput
196 |   ): Promise<AddressProposalsResponse> {
197 |     if (!input.address) {
198 |       throw new Error("address is required");
199 |     }
200 |     return getAddressProposals(this.client, input);
201 |   }
202 | 
203 |   async getAddressDAOProposals(
204 |     input: AddressDAOProposalsInput
205 |   ): Promise<AddressDAOProposalsResponse> {
206 |     if (!input.address) {
207 |       throw new Error("Address is required");
208 |     }
209 |     const response = await getAddressDAOProposals(this.client, input);
210 |     return {
211 |       proposals: {
212 |         nodes: response.proposals?.nodes || [],
213 |         pageInfo: response.proposals?.pageInfo || {
214 |           firstCursor: null,
215 |           lastCursor: null,
216 |         },
217 |       },
218 |     };
219 |   }
220 | 
221 |   async getAddressVotes(
222 |     input: AddressVotesInput
223 |   ): Promise<AddressVotesResponse> {
224 |     return getAddressVotes(this.client, input);
225 |   }
226 | 
227 |   async getAddressCreatedProposals(
228 |     input: AddressCreatedProposalsInput
229 |   ): Promise<AddressCreatedProposalsResponse> {
230 |     if (!input.address) {
231 |       throw new Error("address is required");
232 |     }
233 |     const response = await getAddressCreatedProposals(this.client, input);
234 |     return {
235 |       proposals: {
236 |         nodes: response.proposals?.nodes || [],
237 |         pageInfo: response.proposals?.pageInfo || {
238 |           firstCursor: null,
239 |           lastCursor: null,
240 |         },
241 |       },
242 |     };
243 |   }
244 | 
245 |   async getAddressMetadata(
246 |     input: AddressMetadataInput
247 |   ): Promise<AddressMetadataResponse> {
248 |     if (!input.address) {
249 |       throw new Error("Address is required");
250 |     }
251 |     const response = await getAddressMetadata(this.client, input);
252 |     return {
253 |       address: response.address?.address || input.address,
254 |       accounts: response.address?.accounts || [],
255 |     };
256 |   }
257 | 
258 |   async getAddressGovernances(
259 |     input: AddressGovernancesInput
260 |   ): Promise<Record<string, any>> {
261 |     return await getAddressGovernances(this.client, input);
262 |   }
263 | 
264 |   async getAddressReceivedDelegations(
265 |     input: GetAddressReceivedDelegationsInput
266 |   ): Promise<GetAddressReceivedDelegationsOutput> {
267 |     if (!input.address) {
268 |       throw new Error("address is required");
269 |     }
270 |     return getAddressReceivedDelegations(this.client, input);
271 |   }
272 | 
273 |   async getDelegateStatement(
274 |     input: GetDelegateStatementInput
275 |   ): Promise<DelegateStatement | null> {
276 |     const response = await getDelegateStatement(this.client, input);
277 |     if (!response?.statement) return null;
278 | 
279 |     return {
280 |       id: response.statement.id,
281 |       address: response.statement.address,
282 |       statement: response.statement.statement,
283 |       statementSummary: response.statement.statementSummary || "",
284 |       isSeekingDelegation: response.statement.isSeekingDelegation || false,
285 |       issues: response.statement.issues || [],
286 |     };
287 |   }
288 | 
289 |   async getDelegators(params: GetDelegatorsParams): Promise<{
290 |     delegators: Delegation[];
291 |     pageInfo: PageInfo;
292 |   }> {
293 |     if (!params.address) {
294 |       throw new Error("address is required");
295 |     }
296 |     return getDelegators(this.client, params);
297 |   }
298 | 
299 |   async getProposalVotesCast(
300 |     input: GetProposalVotesCastInput
301 |   ): Promise<ProposalVotesCastResponse> {
302 |     if (!input.id) {
303 |       throw new Error("proposalId is required");
304 |     }
305 |     return getProposalVotesCast(this.client, input);
306 |   }
307 | 
308 |   async getProposalVotesCastList(
309 |     input: GetProposalVotesCastListInput
310 |   ): Promise<ProposalVotesCastListResponse> {
311 |     return getProposalVotesCastList(this.client, input);
312 |   }
313 | 
314 |   async getGovernanceProposalsStats(input: {
315 |     slug: string;
316 |   }): Promise<GovernanceProposalsStatsResponse> {
317 |     return getGovernanceProposalsStats(this.client, input);
318 |   }
319 | 
320 |   /**
321 |    * Format a vote amount considering token decimals
322 |    * @param {string} votes - The raw vote amount
323 |    * @param {TokenInfo} token - Optional token info containing decimals and symbol
324 |    * @returns {string} Formatted vote amount with optional symbol
325 |    */
326 |   private static formatVotes(votes: string, token?: TokenInfo): string {
327 |     const val = BigInt(votes);
328 |     const decimals = token?.decimals ?? 18;
329 |     const denominator = BigInt(10 ** decimals);
330 |     const formatted = (Number(val) / Number(denominator)).toLocaleString();
331 |     return `${formatted}${token?.symbol ? ` ${token.symbol}` : ""}`;
332 |   }
333 | 
334 |   static formatDAOList(daos: Organization[]): string {
335 |     return (
336 |       `Found ${daos.length} DAOs:\n\n` +
337 |       daos
338 |         .map(
339 |           (dao) =>
340 |             `${dao.name} (${dao.slug})\n` +
341 |             `Token Holders: ${dao.tokenOwnersCount}\n` +
342 |             `Delegates: ${dao.delegatesCount}\n` +
343 |             `Proposals: ${dao.proposalsCount}\n` +
344 |             `Active Proposals: ${dao.hasActiveProposals ? "Yes" : "No"}\n` +
345 |             `Description: ${
346 |               dao.metadata?.description || "No description available"
347 |             }\n` +
348 |             `Website: ${dao.metadata?.socials?.website || "N/A"}\n` +
349 |             `Twitter: ${dao.metadata?.socials?.twitter || "N/A"}\n` +
350 |             `Discord: ${dao.metadata?.socials?.discord || "N/A"}\n` +
351 |             "---"
352 |         )
353 |         .join("\n\n")
354 |     );
355 |   }
356 | 
357 |   static formatDAO(dao: Organization): string {
358 |     return (
359 |       `${dao.name} (${dao.slug})\n` +
360 |       `Token Holders: ${dao.tokenOwnersCount}\n` +
361 |       `Delegates: ${dao.delegatesCount}\n` +
362 |       `Proposals: ${dao.proposalsCount}\n` +
363 |       `Active Proposals: ${dao.hasActiveProposals ? "Yes" : "No"}\n` +
364 |       `Description: ${
365 |         dao.metadata?.description || "No description available"
366 |       }\n` +
367 |       `Website: ${dao.metadata?.socials?.website || "N/A"}\n` +
368 |       `Twitter: ${dao.metadata?.socials?.twitter || "N/A"}\n` +
369 |       `Discord: ${dao.metadata?.socials?.discord || "N/A"}\n` +
370 |       `Chain IDs: ${dao.chainIds.join(", ")}\n` +
371 |       `Token IDs: ${dao.tokenIds?.join(", ") || "N/A"}\n` +
372 |       `Governor IDs: ${dao.governorIds?.join(", ") || "N/A"}`
373 |     );
374 |   }
375 | 
376 |   static formatDelegatesList(delegates: Delegate[]): string {
377 |     return (
378 |       `Found ${delegates.length} delegates:\n\n` +
379 |       delegates
380 |         .map(
381 |           (delegate) =>
382 |             `${delegate.account.name || delegate.account.address}\n` +
383 |             `Address: ${delegate.account.address}\n` +
384 |             `Votes: ${delegate.votesCount}\n` +
385 |             `Delegators: ${delegate.delegatorsCount}\n` +
386 |             `Bio: ${delegate.account.bio || "No bio available"}\n` +
387 |             `Statement: ${
388 |               delegate.statement?.statementSummary || "No statement available"
389 |             }\n` +
390 |             "---"
391 |         )
392 |         .join("\n\n")
393 |     );
394 |   }
395 | 
396 |   static formatDelegatorsList(delegators: Delegation[]): string {
397 |     return (
398 |       `Found ${delegators.length} delegators:\n\n` +
399 |       delegators
400 |         .map(
401 |           (delegation) =>
402 |             `${
403 |               delegation.delegator.name ||
404 |               delegation.delegator.ens ||
405 |               delegation.delegator.address
406 |             }\n` +
407 |             `Address: ${delegation.delegator.address}\n` +
408 |             `Votes: ${TallyService.formatVotes(
409 |               delegation.votes,
410 |               delegation.token
411 |             )}\n` +
412 |             `Delegated at: Block ${delegation.blockNumber} (${new Date(
413 |               delegation.blockTimestamp
414 |             ).toLocaleString()})\n` +
415 |             `${
416 |               delegation.token
417 |                 ? `Token: ${delegation.token.symbol} (${delegation.token.name})\n`
418 |                 : ""
419 |             }` +
420 |             "---"
421 |         )
422 |         .join("\n\n")
423 |     );
424 |   }
425 | 
426 |   static formatProposal(proposal: any): string {
427 |     return `Proposal: ${proposal.metadata.title}
428 | ID: ${proposal.id}
429 | Status: ${proposal.status}
430 | Created: ${new Date(proposal.createdAt).toLocaleString()}
431 | Description: ${proposal.metadata.description}
432 | Governor: ${proposal.governor.name}
433 | Vote Stats:
434 | ${proposal.voteStats
435 |   .map(
436 |     (stat: any) =>
437 |       `  ${stat.type}: ${stat.percent.toFixed(2)}% (${
438 |         stat.votesCount
439 |       } votes from ${stat.votersCount} voters)`
440 |   )
441 |   .join("\n")}`;
442 |   }
443 | 
444 |   static formatProposalsList(proposals: any[]): string {
445 |     return (
446 |       `Found ${proposals.length} proposals:\n\n` +
447 |       proposals
448 |         .map(
449 |           (proposal) =>
450 |             `${proposal.metadata.title}\n` +
451 |             `Tally ID: ${proposal.id}\n` +
452 |             `Status: ${proposal.status}\n` +
453 |             `Created: ${new Date(proposal.createdAt).toLocaleString()}\n\n`
454 |         )
455 |         .join("")
456 |     );
457 |   }
458 | }
459 | 
```

--------------------------------------------------------------------------------
/src/tools.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { type Tool } from "@modelcontextprotocol/sdk/types.js";
  2 | import { type TextContent } from "@modelcontextprotocol/sdk/types.js";
  3 | import { TallyService } from "./services/tally.service.js";
  4 | 
  5 | export const tools: Tool[] = [
  6 |   {
  7 |     name: "list-daos",
  8 |     description: "List DAOs on Tally sorted by specified criteria",
  9 |     inputSchema: {
 10 |       type: "object",
 11 |       properties: {
 12 |         limit: {
 13 |           type: "number",
 14 |           description:
 15 |             "Maximum number of DAOs to return (default: 20, max: 50)",
 16 |         },
 17 |         afterCursor: {
 18 |           type: "string",
 19 |           description: "Cursor for pagination",
 20 |         },
 21 |         sortBy: {
 22 |           type: "string",
 23 |           enum: ["id", "name", "explore", "popular"],
 24 |           description:
 25 |             "How to sort the DAOs (default: popular). 'explore' prioritizes DAOs with live proposals",
 26 |         },
 27 |       },
 28 |     },
 29 |   },
 30 |   {
 31 |     name: "get-dao",
 32 |     description: "Get detailed information about a specific DAO",
 33 |     inputSchema: {
 34 |       type: "object",
 35 |       required: ["slug"],
 36 |       properties: {
 37 |         slug: {
 38 |           type: "string",
 39 |           description: "The DAO's slug (e.g., 'uniswap' or 'aave')",
 40 |         },
 41 |       },
 42 |     },
 43 |   },
 44 |   {
 45 |     name: "list-delegates",
 46 |     description:
 47 |       "List delegates for a specific organization with their metadata",
 48 |     inputSchema: {
 49 |       type: "object",
 50 |       required: ["organizationSlug"],
 51 |       properties: {
 52 |         organizationSlug: {
 53 |           type: "string",
 54 |           description:
 55 |             "The organization's slug (e.g., 'arbitrum')",
 56 |         },
 57 |         limit: {
 58 |           type: "number",
 59 |           description:
 60 |             "Maximum number of delegates to return (default: 20, max: 50)",
 61 |         },
 62 |         afterCursor: {
 63 |           type: "string",
 64 |           description: "Cursor for pagination",
 65 |         },
 66 |         hasVotes: {
 67 |           type: "boolean",
 68 |           description: "Filter for delegates with votes",
 69 |         },
 70 |         hasDelegators: {
 71 |           type: "boolean",
 72 |           description: "Filter for delegates with delegators",
 73 |         },
 74 |         isSeekingDelegation: {
 75 |           type: "boolean",
 76 |           description: "Filter for delegates seeking delegation",
 77 |         },
 78 |       },
 79 |     },
 80 |   },
 81 |   {
 82 |     name: "get-delegators",
 83 |     description: "Get list of delegators for a specific address",
 84 |     inputSchema: {
 85 |       type: "object",
 86 |       required: ["address", "organizationSlug"],
 87 |       properties: {
 88 |         address: {
 89 |           type: "string",
 90 |           description: "The Ethereum address to get delegators for (0x format)",
 91 |         },
 92 |         organizationSlug: {
 93 |           type: "string",
 94 |           description:
 95 |             "Filter by organization slug (e.g., 'uniswap'). Alternative to organizationId",
 96 |         },
 97 |         governorId: {
 98 |           type: "string",
 99 |           description: "Filter by specific governor ID",
100 |         },
101 |         limit: {
102 |           type: "number",
103 |           description:
104 |             "Maximum number of delegators to return (default: 20, max: 50)",
105 |         },
106 |         afterCursor: {
107 |           type: "string",
108 |           description: "Cursor for pagination",
109 |         },
110 |         beforeCursor: {
111 |           type: "string",
112 |           description: "Cursor for previous page pagination",
113 |         },
114 |         sortBy: {
115 |           type: "string",
116 |           enum: ["id", "votes"],
117 |           description: "How to sort the delegators (default: id)",
118 |         },
119 |         isDescending: {
120 |           type: "boolean",
121 |           description: "Sort in descending order (default: true)",
122 |         },
123 |       },
124 |     },
125 |   },
126 |   {
127 |     name: "list-proposals",
128 |     description: "List proposals for a specific DAO or organization using its slug",
129 |     inputSchema: {
130 |       type: "object",
131 |       properties: {
132 |         slug: {
133 |           type: "string",
134 |           description: "The slug of the DAO (e.g., 'uniswap')",
135 |         },
136 |         includeArchived: {
137 |           type: "boolean",
138 |           description: "Include archived proposals",
139 |         },
140 |         isDraft: {
141 |           type: "boolean",
142 |           description: "Filter for draft proposals",
143 |         },
144 |         limit: {
145 |           type: "number",
146 |           description: "Maximum number of proposals to return (default: 50, max: 50)",
147 |         },
148 |         afterCursor: {
149 |           type: "string",
150 |           description: "Cursor for pagination (string ID)",
151 |         },
152 |         beforeCursor: {
153 |           type: "string",
154 |           description: "Cursor for previous page pagination (string ID)",
155 |         },
156 |         isDescending: {
157 |           type: "boolean",
158 |           description: "Sort in descending order (default: true)",
159 |         }
160 |       },
161 |       required: ["slug"]
162 |     }
163 |   },
164 |   {
165 |     name: "get-proposal",
166 |     description:
167 |       "Get detailed information about a specific proposal. You must provide either the Tally ID (globally unique) or both onchainId and governorId (unique within a governor).",
168 |     inputSchema: {
169 |       type: "object",
170 |       oneOf: [
171 |         {
172 |           required: ["id"],
173 |           properties: {
174 |             id: {
175 |               type: "string",
176 |               description:
177 |                 "The proposal's Tally ID (globally unique across all governors)",
178 |             },
179 |             includeArchived: {
180 |               type: "boolean",
181 |               description: "Include archived proposals",
182 |             },
183 |             isLatest: {
184 |               type: "boolean",
185 |               description: "Get the latest version of the proposal",
186 |             },
187 |           },
188 |         },
189 |         {
190 |           required: ["onchainId", "governorId"],
191 |           properties: {
192 |             onchainId: {
193 |               type: "string",
194 |               description:
195 |                 "The proposal's onchain ID (only unique within a governor)",
196 |             },
197 |             governorId: {
198 |               type: "string",
199 |               description: "The governor's ID (required when using onchainId)",
200 |             },
201 |             includeArchived: {
202 |               type: "boolean",
203 |               description: "Include archived proposals",
204 |             },
205 |             isLatest: {
206 |               type: "boolean",
207 |               description: "Get the latest version of the proposal",
208 |             },
209 |           },
210 |         },
211 |       ],
212 |     },
213 |   },
214 |   {
215 |     name: "get-address-votes",
216 |     description: "Get votes cast by an address for a specific organization",
217 |     inputSchema: {
218 |       type: "object",
219 |       required: ["address", "organizationSlug"],
220 |       properties: {
221 |         address: {
222 |           type: "string",
223 |           description: "The address to get votes for",
224 |         },
225 |         organizationSlug: {
226 |           type: "string",
227 |           description: "The organization slug to get votes from",
228 |         },
229 |         limit: {
230 |           type: "number",
231 |           description: "Maximum number of votes to return (default: 20)",
232 |         },
233 |         afterCursor: {
234 |           type: "string",
235 |           description: "Cursor for pagination",
236 |         },
237 |       },
238 |     },
239 |   },
240 |   {
241 |     name: "get-address-created-proposals",
242 |     description:
243 |       "Get proposals created by an address for a specific organization",
244 |     inputSchema: {
245 |       type: "object",
246 |       required: ["address", "organizationSlug"],
247 |       properties: {
248 |         address: {
249 |           type: "string",
250 |           description: "The Ethereum address to get created proposals for",
251 |         },
252 |         organizationSlug: {
253 |           type: "string",
254 |           description: "The organization slug to get proposals from",
255 |         },
256 |         limit: {
257 |           type: "number",
258 |           description: "Maximum number of proposals to return (default: 20)",
259 |         },
260 |         afterCursor: {
261 |           type: "string",
262 |           description: "Cursor for pagination",
263 |         },
264 |         beforeCursor: {
265 |           type: "string",
266 |           description: "Cursor for previous page pagination",
267 |         },
268 |       },
269 |     },
270 |     handler: async function (
271 |       this: { service: TallyService },
272 |       input: Record<string, unknown>
273 |     ) {
274 |       const { address, organizationSlug } = input;
275 |       if (typeof address !== "string") {
276 |         throw new Error("address must be a string");
277 |       }
278 |       if (typeof organizationSlug !== "string") {
279 |         throw new Error("organizationSlug must be a string");
280 |       }
281 |       const result = await (this.service as any).getAddressCreatedProposals({
282 |         address,
283 |         organizationSlug,
284 |         limit: typeof input.limit === "number" ? input.limit : undefined,
285 |         afterCursor:
286 |           typeof input.afterCursor === "string" ? input.afterCursor : undefined,
287 |         beforeCursor:
288 |           typeof input.beforeCursor === "string"
289 |             ? input.beforeCursor
290 |             : undefined,
291 |       });
292 |       return JSON.stringify(result);
293 |     },
294 |   },
295 |   {
296 |     name: "get-address-daos-proposals",
297 |     description:
298 |       "Returns proposals from DAOs where a given address has participated (voted, proposed, etc.)",
299 |     inputSchema: {
300 |       type: "object",
301 |       required: ["address", "organizationSlug"],
302 |       properties: {
303 |         address: {
304 |           type: "string",
305 |           description: "The Ethereum address",
306 |         },
307 |         organizationSlug: {
308 |           type: "string",
309 |           description: "The organization slug to get proposals from",
310 |         },
311 |         limit: {
312 |           type: "number",
313 |           description:
314 |             "Maximum number of proposals to return (default: 20, max: 50)",
315 |         },
316 |         afterCursor: {
317 |           type: "string",
318 |           description: "Cursor for pagination",
319 |         },
320 |       },
321 |     },
322 |   },
323 |   {
324 |     name: "get-address-received-delegations",
325 |     description: "Returns delegations received by an address",
326 |     inputSchema: {
327 |       type: "object",
328 |       required: ["address", "organizationSlug"],
329 |       properties: {
330 |         address: {
331 |           type: "string",
332 |           description:
333 |             "The Ethereum address to get received delegations for (0x format)",
334 |         },
335 |         organizationSlug: {
336 |           type: "string",
337 |           description: "Filter by organization slug",
338 |         },
339 |         limit: {
340 |           type: "number",
341 |           description:
342 |             "Maximum number of delegations to return (default: 20, max: 50)",
343 |         },
344 |         sortBy: {
345 |           type: "string",
346 |           enum: ["votes"],
347 |           description: "Field to sort by",
348 |         },
349 |         isDescending: {
350 |           type: "boolean",
351 |           description: "Sort in descending order",
352 |         },
353 |       },
354 |     },
355 |   },
356 |   {
357 |     name: "get-delegate-statement",
358 |     description:
359 |       "Get a delegate's statement for a specific governor or organization",
360 |     inputSchema: {
361 |       type: "object",
362 |       required: ["address"],
363 |       oneOf: [
364 |         {
365 |           required: ["governorId"],
366 |           properties: {
367 |             address: {
368 |               type: "string",
369 |               description: "The delegate's Ethereum address",
370 |             },
371 |             governorId: {
372 |               type: "string",
373 |               description: "The governor's ID",
374 |             },
375 |           },
376 |         },
377 |         {
378 |           required: ["organizationSlug"],
379 |           properties: {
380 |             address: {
381 |               type: "string",
382 |               description: "The delegate's Ethereum address",
383 |             },
384 |             organizationSlug: {
385 |               type: "string",
386 |               description: "The organization's slug (e.g., 'uniswap')",
387 |             },
388 |           },
389 |         },
390 |       ],
391 |     },
392 |   },
393 |   {
394 |     name: "get-address-governances",
395 |     description:
396 |       "Returns the list of governances (DAOs) an address has delegated to",
397 |     inputSchema: {
398 |       type: "object",
399 |       required: ["address"],
400 |       properties: {
401 |         address: {
402 |           type: "string",
403 |           description:
404 |             "The Ethereum address to get governances for (0x format)",
405 |         },
406 |       },
407 |     },
408 |   },
409 |   {
410 |     name: "get-proposal-timeline",
411 |     description: "Get the timeline of events for a specific proposal",
412 |     inputSchema: {
413 |       type: "object",
414 |       required: ["proposalId"],
415 |       properties: {
416 |         proposalId: {
417 |           type: "string",
418 |           description: "The ID of the proposal to get the timeline for",
419 |         },
420 |       },
421 |     },
422 |     handler: async function (
423 |       this: { service: TallyService },
424 |       input: Record<string, unknown>
425 |     ) {
426 |       if (typeof input.proposalId !== "string") {
427 |         throw new Error("proposalId must be a string");
428 |       }
429 |       const result = await this.service.getProposalTimeline({
430 |         proposalId: input.proposalId,
431 |       });
432 |       const content: TextContent[] = [
433 |         {
434 |           type: "text",
435 |           text: JSON.stringify(result),
436 |         },
437 |       ];
438 |       return { content };
439 |     },
440 |   },
441 |   {
442 |     name: "get-proposal-voters",
443 |     description:
444 |       "Get a list of all voters who have voted on a specific proposal",
445 |     inputSchema: {
446 |       type: "object",
447 |       required: ["proposalId"],
448 |       properties: {
449 |         proposalId: {
450 |           type: "string",
451 |           description: "The ID of the proposal to get voters for",
452 |         },
453 |         limit: {
454 |           type: "number",
455 |           description: "Maximum number of voters to return (default: 20)",
456 |         },
457 |         afterCursor: {
458 |           type: "string",
459 |           description: "Cursor for pagination",
460 |         },
461 |         beforeCursor: {
462 |           type: "string",
463 |           description: "Cursor for previous page pagination",
464 |         },
465 |         sortBy: {
466 |           type: "string",
467 |           enum: ["id", "amount"],
468 |           description: "How to sort the voters ('id' sorts by date (default), 'amount' sorts by voting power)",
469 |         },
470 |         isDescending: {
471 |           type: "boolean",
472 |           description: "Sort in descending order (true shows most recent/largest first)",
473 |         },
474 |       },
475 |     },
476 |   },
477 |   {
478 |     name: "get-address-metadata",
479 |     description: "Get metadata information about a specific Ethereum address",
480 |     inputSchema: {
481 |       type: "object",
482 |       required: ["address"],
483 |       properties: {
484 |         address: {
485 |           type: "string",
486 |           description: "The Ethereum address to get metadata for (0x format)",
487 |         },
488 |       },
489 |     },
490 |   },
491 |   {
492 |     name: "get-proposal-security-analysis",
493 |     description:
494 |       "Get security analysis for a specific proposal, including threat analysis and simulations",
495 |     inputSchema: {
496 |       type: "object",
497 |       required: ["proposalId"],
498 |       properties: {
499 |         proposalId: {
500 |           type: "string",
501 |           description: "The ID of the proposal to get security analysis for",
502 |         },
503 |       },
504 |     },
505 |   },
506 |   {
507 |     name: "get-proposal-votes-cast",
508 |     description:
509 |       "Get vote statistics and formatted vote counts for a specific proposal",
510 |     inputSchema: {
511 |       type: "object",
512 |       required: ["id"],
513 |       properties: {
514 |         id: {
515 |           type: "string",
516 |           description: "The proposal's ID",
517 |         },
518 |       },
519 |     },
520 |   },
521 |   {
522 |     name: "get-proposal-votes-cast-list",
523 |     description:
524 |       "Get a list of votes cast for a specific proposal, including formatted vote amounts",
525 |     inputSchema: {
526 |       type: "object",
527 |       required: ["id"],
528 |       properties: {
529 |         id: {
530 |           type: "string",
531 |           description:
532 |             "The proposal's Tally ID (globally unique across all governors)",
533 |         },
534 |       },
535 |     },
536 |   },
537 |   {
538 |     name: "get-governance-proposals-stats",
539 |     description: "Get statistics about passed and failed proposals for a specific DAO",
540 |     inputSchema: {
541 |       type: "object",
542 |       required: ["slug"],
543 |       properties: {
544 |         slug: {
545 |           type: "string",
546 |           description: "The DAO's slug (e.g., 'uniswap' or 'aave')",
547 |         },
548 |       },
549 |     },
550 |   },
551 | ];
552 | 
```

--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  3 | import {
  4 |   ListToolsRequestSchema,
  5 |   CallToolRequestSchema,
  6 |   type Tool,
  7 |   type TextContent,
  8 | } from "@modelcontextprotocol/sdk/types.js";
  9 | import { TallyService } from "./services/tally.service.js";
 10 | import type { OrganizationsSortBy } from "./services/organizations/organizations.types.js";
 11 | import { tools } from "./tools.js";
 12 | 
 13 | export class TallyServer {
 14 |   private server: Server;
 15 |   private service: TallyService;
 16 | 
 17 |   constructor(apiKey: string) {
 18 |     // Initialize service
 19 |     this.service = new TallyService({ apiKey });
 20 | 
 21 |     // Create server instance
 22 |     this.server = new Server(
 23 |       {
 24 |         name: "tally-api",
 25 |         version: "1.0.0",
 26 |       },
 27 |       {
 28 |         capabilities: {
 29 |           tools: {},
 30 |         },
 31 |       }
 32 |     );
 33 | 
 34 |     this.setupHandlers();
 35 |   }
 36 | 
 37 |   private setupHandlers() {
 38 |     // List available tools
 39 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => {
 40 |       return { tools };
 41 |     });
 42 | 
 43 |     // Handle tool execution
 44 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
 45 |       const { name, arguments: args = {} } = request.params;
 46 | 
 47 |       if (name === "list-daos") {
 48 |         try {
 49 |           const data = await this.service.listDAOs({
 50 |             limit: typeof args.limit === "number" ? args.limit : undefined,
 51 |             afterCursor:
 52 |               typeof args.afterCursor === "string"
 53 |                 ? args.afterCursor
 54 |                 : undefined,
 55 |             sortBy:
 56 |               typeof args.sortBy === "string"
 57 |                 ? (args.sortBy as OrganizationsSortBy)
 58 |                 : undefined,
 59 |           });
 60 | 
 61 |           const content: TextContent[] = [
 62 |             {
 63 |               type: "text",
 64 |               text: JSON.stringify(data, null, 2),
 65 |             },
 66 |           ];
 67 | 
 68 |           return { content };
 69 |         } catch (error) {
 70 |           throw new Error(
 71 |             `Error fetching DAOs: ${
 72 |               error instanceof Error ? error.message : "Unknown error"
 73 |             }`
 74 |           );
 75 |         }
 76 |       }
 77 | 
 78 |       if (name === "get-dao") {
 79 |         try {
 80 |           if (typeof args.slug !== "string") {
 81 |             throw new Error("slug must be a string");
 82 |           }
 83 | 
 84 |           const result = await this.service.getDAO(args.slug);
 85 | 
 86 |           const content: TextContent[] = [
 87 |             {
 88 |               type: "text",
 89 |               text: JSON.stringify(result, null, 2),
 90 |             },
 91 |           ];
 92 | 
 93 |           return { content };
 94 |         } catch (error) {
 95 |           throw new Error(
 96 |             `Error getting DAO: ${error instanceof Error ? error.message : "Unknown error"}`
 97 |           );
 98 |         }
 99 |       }
100 | 
101 |       if (name === "list-delegates") {
102 |         try {
103 |           if (typeof args.organizationSlug !== "string") {
104 |             throw new Error("organizationSlug must be a string");
105 |           }
106 | 
107 |           const result = await this.service.listDelegates({
108 |             organizationSlug: args.organizationSlug as string,
109 |             limit: typeof args.limit === "number" ? args.limit : undefined,
110 |             afterCursor: typeof args.afterCursor === "string" ? args.afterCursor : undefined,
111 |             beforeCursor: typeof args.beforeCursor === "string" ? args.beforeCursor : undefined,
112 |             hasVotes: typeof args.hasVotes === "boolean" ? args.hasVotes : undefined,
113 |             hasDelegators: typeof args.hasDelegators === "boolean" ? args.hasDelegators : undefined,
114 |             isSeekingDelegation: typeof args.isSeekingDelegation === "boolean" ? args.isSeekingDelegation : undefined
115 |           });
116 | 
117 |           const content: TextContent[] = [
118 |             {
119 |               type: "text",
120 |               text: JSON.stringify(result, null, 2),
121 |             },
122 |           ];
123 | 
124 |           return { content };
125 |         } catch (error) {
126 |           throw new Error(
127 |             `Error listing delegates: ${error instanceof Error ? error.message : "Unknown error"}`
128 |           );
129 |         }
130 |       }
131 | 
132 |       if (name === "get-delegators") {
133 |         try {
134 |           if (typeof args.address !== "string") {
135 |             throw new Error("address must be a string");
136 |           }
137 | 
138 |           const organizationId = typeof args.organizationId === "string" ? args.organizationId : undefined;
139 |           const organizationSlug = typeof args.organizationSlug === "string" ? args.organizationSlug : undefined;
140 |           const governorId = typeof args.governorId === "string" ? args.governorId : undefined;
141 |           const limit = typeof args.limit === "number" ? args.limit : 20;
142 |           const afterCursor = typeof args.afterCursor === "string" ? args.afterCursor : undefined;
143 |           const beforeCursor = typeof args.beforeCursor === "string" ? args.beforeCursor : undefined;
144 |           const sortBy = typeof args.sortBy === "string" && (args.sortBy === "votes" || args.sortBy === "id") ? args.sortBy : undefined;
145 |           const isDescending = typeof args.isDescending === "boolean" ? args.isDescending : undefined;
146 | 
147 |           const result = await this.service.getDelegators({
148 |             address: args.address,
149 |             organizationId,
150 |             organizationSlug,
151 |             governorId,
152 |             limit,
153 |             afterCursor,
154 |             beforeCursor,
155 |             sortBy,
156 |             isDescending,
157 |           });
158 | 
159 |           const content: TextContent[] = [
160 |             {
161 |               type: "text",
162 |               text: JSON.stringify(result, null, 2),
163 |             },
164 |           ];
165 | 
166 |           return { content };
167 |         } catch (error) {
168 |           throw new Error(
169 |             `Error getting delegators: ${error instanceof Error ? error.message : "Unknown error"}`
170 |           );
171 |         }
172 |       }
173 | 
174 |       if (name === "list-proposals") {
175 |         try {
176 |           if (typeof args.slug !== "string") {
177 |             throw new Error("slug must be a string");
178 |           }
179 | 
180 |           const data = await this.service.listProposals({
181 |             slug: args.slug,
182 |             includeArchived: typeof args.includeArchived === "boolean" ? args.includeArchived : undefined,
183 |             isDraft: typeof args.isDraft === "boolean" ? args.isDraft : undefined,
184 |             limit: typeof args.limit === "number" ? args.limit : undefined,
185 |             afterCursor: typeof args.afterCursor === "string" ? args.afterCursor : undefined,
186 |             beforeCursor: typeof args.beforeCursor === "string" ? args.beforeCursor : undefined,
187 |             isDescending: typeof args.isDescending === "boolean" ? args.isDescending : undefined
188 |           });
189 | 
190 |           const content: TextContent[] = [
191 |             {
192 |               type: "text",
193 |               text: JSON.stringify(data, null, 2),
194 |             },
195 |           ];
196 | 
197 |           return { content };
198 |         } catch (error) {
199 |           throw new Error(
200 |             `Error fetching proposals: ${
201 |               error instanceof Error ? error.message : "Unknown error"
202 |             }`
203 |           );
204 |         }
205 |       }
206 | 
207 |       if (name === "get-proposal") {
208 |         try {
209 |           const id = typeof args.id === "string" ? args.id : undefined;
210 |           const onchainId = typeof args.onchainId === "string" ? args.onchainId : undefined;
211 |           const governorId = typeof args.governorId === "string" ? args.governorId : undefined;
212 |           const includeArchived = typeof args.includeArchived === "boolean" ? args.includeArchived : undefined;
213 |           const isLatest = typeof args.isLatest === "boolean" ? args.isLatest : undefined;
214 | 
215 |           if (!id && (!onchainId || !governorId)) {
216 |             throw new Error("Must provide either id or both onchainId and governorId");
217 |           }
218 | 
219 |           const result = await this.service.getProposal({
220 |             id,
221 |             onchainId,
222 |             governorId,
223 |             includeArchived,
224 |             isLatest,
225 |           });
226 | 
227 |           const content: TextContent[] = [
228 |             {
229 |               type: "text",
230 |               text: JSON.stringify(result, null, 2),
231 |             },
232 |           ];
233 | 
234 |           return { content };
235 |         } catch (error) {
236 |           throw new Error(
237 |             `Error getting proposal: ${error instanceof Error ? error.message : "Unknown error"}`
238 |           );
239 |         }
240 |       }
241 | 
242 |       if (name === "get-address-created-proposals") {
243 |         try {
244 |           if (typeof args.address !== "string") {
245 |             throw new Error("address must be a string");
246 |           }
247 |           if (typeof args.organizationSlug !== "string") {
248 |             throw new Error("organizationSlug must be a string");
249 |           }
250 | 
251 |           const result = await (this.service as any).getAddressCreatedProposals({
252 |             address: args.address,
253 |             organizationSlug: args.organizationSlug,
254 |             limit: typeof args.limit === "number" ? args.limit : undefined,
255 |             afterCursor: typeof args.afterCursor === "string" ? args.afterCursor : undefined,
256 |             beforeCursor: typeof args.beforeCursor === "string" ? args.beforeCursor : undefined
257 |           });
258 | 
259 |           const content: TextContent[] = [
260 |             {
261 |               type: "text",
262 |               text: JSON.stringify(result, null, 2),
263 |             },
264 |           ];
265 | 
266 |           return { content };
267 |         } catch (error) {
268 |           throw new Error(
269 |             `Error fetching address created proposals: ${
270 |               error instanceof Error ? error.message : "Unknown error"
271 |             }`
272 |           );
273 |         }
274 |       }
275 | 
276 |       if (name === "get-address-daos-proposals") {
277 |         try {
278 |           if (typeof args.address !== "string") {
279 |             throw new Error("address must be a string");
280 |           }
281 |           if (typeof args.organizationSlug !== "string") {
282 |             throw new Error("organizationSlug must be a string");
283 |           }
284 | 
285 |           const result = await this.service.getAddressDAOProposals({
286 |             address: args.address,
287 |             organizationSlug: args.organizationSlug,
288 |             limit: typeof args.limit === "number" ? args.limit : undefined,
289 |             afterCursor: typeof args.afterCursor === "string" ? args.afterCursor : undefined,
290 |           });
291 | 
292 |           const content: TextContent[] = [
293 |             {
294 |               type: "text",
295 |               text: JSON.stringify(result, null, 2)
296 |             }
297 |           ];
298 | 
299 |           return { content };
300 |         } catch (error) {
301 |           throw new Error(
302 |             `Error fetching address DAO proposals: ${
303 |               error instanceof Error ? error.message : "Unknown error"
304 |             }`
305 |           );
306 |         }
307 |       }
308 | 
309 |       if (name === "get-address-votes") {
310 |         try {
311 |           // Validate types at API boundary
312 |           if (typeof args.address !== "string") {
313 |             throw new Error("address must be a string");
314 |           }
315 |           if (typeof args.organizationSlug !== "string") {
316 |             throw new Error("organizationSlug must be a string");
317 |           }
318 | 
319 |           const result = await this.service.getAddressVotes({
320 |             address: args.address,
321 |             organizationSlug: args.organizationSlug,
322 |             limit: typeof args.limit === "number" ? args.limit : undefined,
323 |             afterCursor: typeof args.afterCursor === "string" ? args.afterCursor : undefined,
324 |           });
325 | 
326 |           const content: TextContent[] = [
327 |             {
328 |               type: "text",
329 |               text: JSON.stringify(result, null, 2),
330 |             },
331 |           ];
332 | 
333 |           return {
334 |             content,
335 |             pageInfo: {
336 |               firstCursor: result.votes.pageInfo.firstCursor || null,
337 |               lastCursor: result.votes.pageInfo.lastCursor || null,
338 |             },
339 |           };
340 |         } catch (error) {
341 |           throw new Error(
342 |             `Error fetching address votes: ${
343 |               error instanceof Error ? error.message : "Unknown error"
344 |             }`
345 |           );
346 |         }
347 |       }
348 | 
349 |       if (name === "get-address-received-delegations") {
350 |         try {
351 |           if (typeof args.address !== "string") {
352 |             throw new Error("address must be a string");
353 |           }
354 |           if (typeof args.organizationSlug !== "string") {
355 |             throw new Error("organizationSlug must be a string");
356 |           }
357 | 
358 |           const result = await this.service.getAddressReceivedDelegations({
359 |             address: args.address,
360 |             organizationSlug: args.organizationSlug,
361 |             limit: typeof args.limit === "number" ? args.limit : undefined,
362 |             sortBy: typeof args.sortBy === "string" ? (args.sortBy as "votes") : undefined,
363 |             isDescending: typeof args.isDescending === "boolean" ? args.isDescending : undefined,
364 |           });
365 | 
366 |           const content: TextContent[] = [
367 |             {
368 |               type: "text",
369 |               text: JSON.stringify(result, null, 2),
370 |             },
371 |           ];
372 | 
373 |           return { content };
374 |         } catch (error) {
375 |           throw new Error(
376 |             `Error fetching received delegations: ${
377 |               error instanceof Error ? error.message : "Unknown error"
378 |             }`
379 |           );
380 |         }
381 |       }
382 | 
383 |       if (name === "get-delegate-statement") {
384 |         try {
385 |           if (typeof args.address !== "string") {
386 |             throw new Error("address must be a string");
387 |           }
388 | 
389 |           // Check for mutually exclusive parameters
390 |           if (typeof args.governorId === "string" && typeof args.organizationSlug === "string") {
391 |             throw new Error("Cannot provide both governorId and organizationSlug");
392 |           }
393 | 
394 |           let result;
395 |           if (typeof args.governorId === "string") {
396 |             result = await this.service.getDelegateStatement({
397 |               address: args.address,
398 |               governorId: args.governorId
399 |             });
400 |           } else if (typeof args.organizationSlug === "string") {
401 |             result = await this.service.getDelegateStatement({
402 |               address: args.address,
403 |               organizationSlug: args.organizationSlug
404 |             });
405 |           } else {
406 |             throw new Error("Either governorId or organizationSlug must be provided");
407 |           }
408 | 
409 |           const content: TextContent[] = [
410 |             {
411 |               type: "text",
412 |               text: JSON.stringify(result, null, 2),
413 |             },
414 |           ];
415 | 
416 |           return { content };
417 |         } catch (error) {
418 |           throw new Error(
419 |             `Error fetching delegate statement: ${
420 |               error instanceof Error ? error.message : "Unknown error"
421 |             }`
422 |           );
423 |         }
424 |       }
425 | 
426 |       if (name === "get-address-governances") {
427 |         try {
428 |           if (typeof args.address !== "string") {
429 |             throw new Error("address must be a string");
430 |           }
431 | 
432 |           const result = await this.service.getAddressGovernances({
433 |             address: args.address,
434 |           });
435 | 
436 |           const content: TextContent[] = [
437 |             {
438 |               type: "text",
439 |               text: JSON.stringify(result, null, 2),
440 |             },
441 |           ];
442 | 
443 |           return { content };
444 |         } catch (error) {
445 |           throw new Error(
446 |             `Error fetching address governances: ${
447 |               error instanceof Error ? error.message : "Unknown error"
448 |             }`
449 |           );
450 |         }
451 |       }
452 | 
453 |       if (name === "get-proposal-timeline") {
454 |         try {
455 |           if (typeof args.proposalId !== 'string') {
456 |             throw new Error('proposalId must be a string');
457 |           }
458 |           const result = await this.service.getProposalTimeline({
459 |             proposalId: args.proposalId
460 |           });
461 | 
462 |           if (!result.proposal) {
463 |             throw new Error('Proposal not found');
464 |           }
465 | 
466 |           const content: TextContent[] = [
467 |             {
468 |               type: "text",
469 |               text: JSON.stringify(result, null, 2)
470 |             }
471 |           ];
472 |           return { content };
473 |         } catch (error) {
474 |           throw new Error(
475 |             `Error fetching proposal timeline: ${
476 |               error instanceof Error ? error.message : "Unknown error"
477 |             }`
478 |           );
479 |         }
480 |       }
481 | 
482 |       if (name === "get-proposal-voters") {
483 |         try {
484 |           if (typeof args.proposalId !== "string") {
485 |             throw new Error("proposalId must be a string");
486 |           }
487 | 
488 |           const result = await this.service.getProposalVoters({
489 |             proposalId: args.proposalId,
490 |             limit: typeof args.limit === "number" ? args.limit : undefined,
491 |             afterCursor: typeof args.afterCursor === "string" ? args.afterCursor : undefined,
492 |             beforeCursor: typeof args.beforeCursor === "string" ? args.beforeCursor : undefined,
493 |             sortBy: typeof args.sortBy === "string" ? args.sortBy as "votes" | "timestamp" : undefined,
494 |             isDescending: typeof args.isDescending === "boolean" ? args.isDescending : undefined
495 |           });
496 | 
497 |           if (!result?.votes?.nodes) {
498 |             return {
499 |               content: [],
500 |               pageInfo: {
501 |                 firstCursor: null,
502 |                 lastCursor: null,
503 |               },
504 |             };
505 |           }
506 | 
507 |           const content: TextContent[] = [
508 |             {
509 |               type: "text",
510 |               text: JSON.stringify(result, null, 2)
511 |             }
512 |           ];
513 | 
514 |           return {
515 |             content,
516 |             pageInfo: {
517 |               firstCursor: result.votes.pageInfo.firstCursor || null,
518 |               lastCursor: result.votes.pageInfo.lastCursor || null,
519 |             },
520 |           };
521 |         } catch (error) {
522 |           throw new Error(
523 |             `Error fetching proposal voters: ${
524 |               error instanceof Error ? error.message : "Unknown error"
525 |             }`
526 |           );
527 |         }
528 |       }
529 | 
530 |       if (name === "get-address-metadata") {
531 |         const { address } = args as { address: string };
532 |         const result = await this.service.getAddressMetadata({
533 |           address,
534 |         });
535 |         return {
536 |           content: [{
537 |             type: "text",
538 |             text: JSON.stringify(result, null, 2),
539 |           }],
540 |         };
541 |       }
542 | 
543 |       if (name === "get-proposal-security-analysis") {
544 |         try {
545 |           if (typeof args.proposalId !== "string") {
546 |             throw new Error("proposalId must be a string");
547 |           }
548 | 
549 |           const result = await this.service.getProposalSecurityAnalysis({
550 |             proposalId: args.proposalId
551 |           });
552 | 
553 |           const content: TextContent[] = [
554 |             {
555 |               type: "text",
556 |               text: JSON.stringify(result, null, 2)
557 |             }
558 |           ];
559 | 
560 |           return { content };
561 |         } catch (error) {
562 |           throw new Error(
563 |             `Error fetching proposal security analysis: ${
564 |               error instanceof Error ? error.message : "Unknown error"
565 |             }`
566 |           );
567 |         }
568 |       }
569 | 
570 |       if (name === "get-proposal-votes-cast") {
571 |         try {
572 |           if (typeof args.id !== "string") {
573 |             throw new Error("id must be a string");
574 |           }
575 | 
576 |           const result = await this.service.getProposalVotesCast({
577 |             id: args.id
578 |           });
579 | 
580 |           const content: TextContent[] = [
581 |             {
582 |               type: "text",
583 |               text: JSON.stringify(result, null, 2)
584 |             }
585 |           ];
586 | 
587 |           return { content };
588 |         } catch (error) {
589 |           throw new Error(
590 |             `Error fetching proposal votes cast: ${
591 |               error instanceof Error ? error.message : "Unknown error"
592 |             }`
593 |           );
594 |         }
595 |       }
596 | 
597 |       if (name === "get-proposal-votes-cast-list") {
598 |         try {
599 |           if (typeof args.id !== "string") {
600 |             throw new Error("id must be a string");
601 |           }
602 | 
603 |           const result = await this.service.getProposalVotesCastList({
604 |             id: args.id
605 |           });
606 | 
607 |           const content: TextContent[] = [
608 |             {
609 |               type: "text",
610 |               text: JSON.stringify(result, null, 2)
611 |             }
612 |           ];
613 | 
614 |           return { content };
615 |         } catch (error) {
616 |           throw new Error(
617 |             `Error fetching proposal votes cast list: ${
618 |               error instanceof Error ? error.message : "Unknown error"
619 |             }`
620 |           );
621 |         }
622 |       }
623 | 
624 |       throw new Error(`Unknown tool: ${name}`);
625 |     });
626 |   }
627 | 
628 |   async start() {
629 |     const transport = new StdioServerTransport();
630 |     await this.server.connect(transport);
631 |     console.error("Tally MCP Server running on stdio");
632 |   }
633 | }
634 | 
```

--------------------------------------------------------------------------------
/LLM-API-GUIDE.txt:
--------------------------------------------------------------------------------

```
   1 | 
   2 | 
   3 | Tally API - LLM Query Construction Rules (Mandatory & Unbreakable)
   4 | 
   5 | Introduction
   6 | 
   7 | This document outlines the mandatory and unbreakable rules for Large Language Models (LLMs) when constructing queries for the Tally API. These rules are not suggestions—they must be strictly followed to ensure correct, efficient, and error-free GraphQL queries. Failure to adhere to any of these rules will result in query errors, inaccurate data, and is considered a fatal error. There is no acceptable deviation from these rules.
   8 | 
   9 | Core Principles
  10 | 
  11 | Never Assume: You must not assume any default values or behaviors for sort, filter, or other optional input parameters. You must explicitly declare them in the query.
  12 | 
  13 | Type Awareness: You must always be acutely aware of the GraphQL types involved, especially interface and union types, and use inline fragments accordingly. Failure to do so is a fatal error.
  14 | 
  15 | Fragment Prioritization: You must use fragments to minimize repetition, improve maintainability, and ensure efficient queries. Not using fragments is unacceptable.
  16 | 
  17 | Explicit Field Selection: You must always explicitly request each field you need, and never assume fields will be returned automatically.
  18 | 
  19 | Pagination: You must always use pagination where appropriate to ensure complete query results are retrieved, using the page input and pageInfo fields.
  20 | 
  21 | Correct API Use: You must adhere to API constraints. Some queries have required fields that must be used correctly.
  22 | 
  23 | Schema Consultation: You must consult the complete schema reference before creating any queries.
  24 | 
  25 | Multi-step Queries: You must properly structure multi-step queries into a sequence of dependent queries if data from one query is needed for a subsequent query.
  26 | 
  27 | Fragment Usage: All Fragments must be used, and any unused fragments must be removed.
  28 | 
  29 | Rule 1: Interface and Union Type Handling (Mandatory)
  30 | 
  31 | Problem: The nodes field in paginated queries often returns a list of types that implement a GraphQL interface (like Node), or are part of a union type. You cannot query fields directly on the interface type.
  32 | 
  33 | Solution: You must use inline fragments (... on TypeName) to access fields on the concrete types within a list of interface types. Failure to do so is a fatal error.
  34 | 
  35 | Example (Correct):
  36 | 
  37 | query GetOrganizations {
  38 |   organizations {
  39 |     nodes {
  40 |       ... on Organization { # Correct: Uses inline fragment
  41 |         id
  42 |         name
  43 |         slug
  44 |         metadata {
  45 |           icon
  46 |         }
  47 |       }
  48 |     }
  49 |     pageInfo {
  50 |       firstCursor
  51 |       lastCursor
  52 |       count
  53 |     }
  54 |   }
  55 | }
  56 | content_copy
  57 | download
  58 | Use code with caution.
  59 | Graphql
  60 | 
  61 | Example (Incorrect - Avoid):
  62 | 
  63 | query GetOrganizations {
  64 |   organizations {
  65 |     nodes {
  66 |       id      # Incorrect: querying on the interface directly
  67 |       name   # Incorrect: querying on the interface directly
  68 |       slug    # Incorrect: querying on the interface directly
  69 |     }
  70 |   }
  71 | }
  72 | content_copy
  73 | download
  74 | Use code with caution.
  75 | Graphql
  76 | 
  77 | Specific Error Case: Attempting to query fields directly on the nodes field when querying votes without the ... on Vote fragment. This is a fatal error.
  78 | 
  79 | query GetVotes {
  80 |   votes(input: {
  81 |     filters: {
  82 |       voter: "0x1B686eE8E31c5959D9F5BBd8122a58682788eeaD"
  83 |     }
  84 |   }) {
  85 |     nodes {
  86 |       type  # Error: Didn't use ... on Vote
  87 |     }
  88 |   }
  89 | }
  90 | content_copy
  91 | download
  92 | Use code with caution.
  93 | Graphql
  94 | 
  95 | Prevention: This error is a result of not following rule 1. This could also be prevented by consulting the schema first, before creating the query.
  96 | 
  97 | Action: Always use inline fragments (... on TypeName) inside the nodes list, and any other location where interface types can be returned, to query the specific fields of the concrete type. Failure to do so is a fatal error.
  98 | 
  99 | Rule 2: Explicit Sort and Filter Inputs (Mandatory)
 100 | 
 101 | Problem: Queries with sort or filter options often have required input types that must be fully populated.
 102 | 
 103 | Solution: You must never assume default sort or filter values. You must always explicitly provide them in the query if you need them. Even if you don't need sorting or filtering, you must provide an empty input object.
 104 | 
 105 | Example (Correct):
 106 | 
 107 | query GetProposals($input: ProposalsInput!) {
 108 |   proposals(input: $input) {
 109 |     nodes {
 110 |         ... on Proposal {
 111 |             id
 112 |             metadata {
 113 |                 title
 114 |             }
 115 |             status
 116 |         }
 117 |     }
 118 |     pageInfo {
 119 |       firstCursor
 120 |       lastCursor
 121 |       count
 122 |     }
 123 |   }
 124 | }
 125 | content_copy
 126 | download
 127 | Use code with caution.
 128 | Graphql
 129 | *  **Input:**
 130 | content_copy
 131 | download
 132 | Use code with caution.
 133 | input ProposalsInput {
 134 |     filters: ProposalsFiltersInput
 135 |     page: PageInput
 136 |     sort: ProposalsSortInput
 137 | }
 138 | 
 139 | input ProposalsFiltersInput {
 140 |     governorId: AccountID
 141 |     includeArchived: Boolean
 142 |     isDraft: Boolean
 143 |     organizationId: IntID
 144 |     proposer: Address
 145 | }
 146 | input ProposalsSortInput {
 147 |     isDescending: Boolean!
 148 |     sortBy: ProposalsSortBy!
 149 | }
 150 | enum ProposalsSortBy {
 151 |     id
 152 | }
 153 | 
 154 | input PageInput {
 155 |     afterCursor: String
 156 |     beforeCursor: String
 157 |     limit: Int
 158 | }
 159 | content_copy
 160 | download
 161 | Use code with caution.
 162 | Graphql
 163 | *   **Query:** (with optional sort, and filters)
 164 | content_copy
 165 | download
 166 | Use code with caution.
 167 | query GetProposalsWithSortAndFilter {
 168 |   proposals(input: {
 169 |       filters: {
 170 |           governorId: "eip155:1:0x123abc"
 171 |           includeArchived: true
 172 |       },
 173 |       sort: {
 174 |         sortBy: id
 175 |         isDescending: false
 176 |       },
 177 |       page: {
 178 |         limit: 10
 179 |       }
 180 |   })
 181 |     {
 182 |     nodes {
 183 |         ... on Proposal {
 184 |             id
 185 |             metadata {
 186 |                 title
 187 |             }
 188 |             status
 189 |         }
 190 |     }
 191 |     pageInfo {
 192 |       firstCursor
 193 |       lastCursor
 194 |        count
 195 |     }
 196 |   }
 197 | }
 198 | content_copy
 199 | download
 200 | Use code with caution.
 201 | Graphql
 202 | 
 203 | Example (Incorrect - Avoid):
 204 | 
 205 | query GetProposals {
 206 |   proposals {
 207 |     nodes {
 208 |       id
 209 |       metadata {
 210 |         title
 211 |       }
 212 |       status
 213 |     }
 214 |   }
 215 | }
 216 | content_copy
 217 | download
 218 | Use code with caution.
 219 | Graphql
 220 | 
 221 | Action: Always provide a valid input object for queries that require filters or sorts. Use null if no sorting or filtering is needed for a nullable input, but if the filter is required, use an empty object when no filters are required. Failure to do so is a fatal error.
 222 | 
 223 | Rule 3: Fragment Usage (Mandatory)
 224 | 
 225 | Problem: Repeated field selections in multiple queries make the code less maintainable and are prone to errors.
 226 | 
 227 | Solution: You must use fragments to group common field selections and reuse them across multiple queries. Not using fragments is unacceptable.
 228 | 
 229 | Example (Correct):
 230 | 
 231 | fragment BasicProposalDetails on Proposal {
 232 |     id
 233 |     onchainId
 234 |     metadata {
 235 |       title
 236 |       description
 237 |     }
 238 |    status
 239 | }
 240 | 
 241 | 
 242 | query GetProposals($input: ProposalsInput!) {
 243 |   proposals(input: $input) {
 244 |     nodes {
 245 |         ... on Proposal {
 246 |             ...BasicProposalDetails
 247 |          }
 248 |     }
 249 |     pageInfo {
 250 |       firstCursor
 251 |       lastCursor
 252 |        count
 253 |     }
 254 |   }
 255 | }
 256 | 
 257 | query GetSingleProposal($input: ProposalInput!) {
 258 |     proposal(input: $input) {
 259 |       ...BasicProposalDetails
 260 |     }
 261 | }
 262 | content_copy
 263 | download
 264 | Use code with caution.
 265 | Graphql
 266 | 
 267 | Example (Incorrect - Avoid):
 268 | 
 269 | query GetProposals {
 270 |   proposals {
 271 |     nodes {
 272 |        id
 273 |        onchainId
 274 |        metadata {
 275 |         title
 276 |         description
 277 |        }
 278 |        status
 279 |     }
 280 |   }
 281 | }
 282 | 
 283 | query GetSingleProposal {
 284 |     proposal(input: {id: 123}) {
 285 |       id
 286 |       onchainId
 287 |       metadata {
 288 |         title
 289 |         description
 290 |       }
 291 |       status
 292 |     }
 293 | }
 294 | content_copy
 295 | download
 296 | Use code with caution.
 297 | Graphql
 298 | 
 299 | Action: Always create and use fragments, and make them focused, and reusable across multiple queries. Not using fragments is unacceptable.
 300 | 
 301 | Rule 4: Explicit Field Selection (Mandatory)
 302 | 
 303 | Problem: Assuming the API will return certain fields if they aren't specifically requested.
 304 | 
 305 | Solution: You must always request every field you need in your query.
 306 | 
 307 | Example (Correct):
 308 | 
 309 | query GetOrganization($input: OrganizationInput!) {
 310 |   organization(input: $input) {
 311 |     id
 312 |     name
 313 |     slug
 314 |     metadata {
 315 |       icon
 316 |       description
 317 |       socials {
 318 |         website
 319 |       }
 320 |     }
 321 |   }
 322 | }
 323 | content_copy
 324 | download
 325 | Use code with caution.
 326 | Graphql
 327 | 
 328 | Example (Incorrect - Avoid):
 329 | 
 330 | query GetOrganization {
 331 |   organization { # Incorrect: Assuming all fields are returned by default
 332 |     name
 333 |     slug
 334 |   }
 335 | }
 336 | content_copy
 337 | download
 338 | Use code with caution.
 339 | Graphql
 340 | 
 341 | Action: List out every field you need in the query, and avoid implied or implicit field selections.
 342 | 
 343 | Rule 5: Input Type Validation (Mandatory)
 344 | 
 345 | Problem: Using the wrong types when providing input values to a query.
 346 | 
 347 | Solution: Check that all values passed as inputs to a query match the type declared in the input. Failure to do so is a fatal error.
 348 | 
 349 | Example (Correct):
 350 | 
 351 | query GetAccount($id: AccountID!) {
 352 |   account(id: $id) {
 353 |     id
 354 |     name
 355 |     address
 356 |     ens
 357 |     picture
 358 |   }
 359 | }
 360 | content_copy
 361 | download
 362 | Use code with caution.
 363 | Graphql
 364 | 
 365 | Query
 366 | 
 367 | query GetAccountCorrect {
 368 |    account(id:"eip155:1:0x123") {
 369 |     id
 370 |      name
 371 |      address
 372 |      ens
 373 |      picture
 374 |    }
 375 | }
 376 | content_copy
 377 | download
 378 | Use code with caution.
 379 | Graphql
 380 | * The `id` argument correctly uses the `AccountID` type, which is a string representing a CAIP-10 ID.
 381 | content_copy
 382 | download
 383 | Use code with caution.
 384 | 
 385 | Specific Error Case: Attempting to use a plain integer for organizationId in proposal queries. This is a fatal error.
 386 | 
 387 | query GetProposals {
 388 |   proposals(input: {
 389 |     filters: {
 390 |       organizationId: 1  # Wrong format for ID
 391 |     }
 392 |   })
 393 |   {
 394 |     nodes {
 395 |       ... on Proposal {
 396 |           id
 397 |          }
 398 |       }
 399 |    }
 400 | }
 401 | content_copy
 402 | download
 403 | Use code with caution.
 404 | Graphql
 405 | *   **Prevention:** This error is caused by not following rule 5, and also the ID type definitions.
 406 | content_copy
 407 | download
 408 | Use code with caution.
 409 | 
 410 | Example (Incorrect - Avoid):
 411 | 
 412 | query GetAccount($id: AccountID!) {
 413 |   account(id: $id) {
 414 |     id
 415 |     name
 416 |     address
 417 |   }
 418 | }
 419 | content_copy
 420 | download
 421 | Use code with caution.
 422 | Graphql
 423 | 
 424 | Query
 425 | 
 426 | query GetAccountIncorrect {
 427 |    account(id:123) { # Incorrect: Using an Int when an AccountID is expected.
 428 |     id
 429 |      name
 430 |      address
 431 |      ens
 432 |      picture
 433 |    }
 434 | }
 435 | content_copy
 436 | download
 437 | Use code with caution.
 438 | Graphql
 439 | 
 440 | Action: Ensure you're using the correct type. Int cannot be used where an IntID, AccountID, HashID or AssetID type is required. Failure to do so is a fatal error.
 441 | 
 442 | ID Type Definitions
 443 | 
 444 | AccountID: A CAIP-10 compliant account id. (e.g., "eip155:1:0x7e90e03654732abedf89Faf87f05BcD03ACEeFdc")
 445 | 
 446 | AssetID: A CAIP-19 compliant asset id. (e.g., "eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f")
 447 | 
 448 | IntID: A 64-bit integer represented as a string. (e.g., "1234567890")
 449 | 
 450 | HashID: A CAIP-2 scoped identifier for identifying transactions across chains. (e.g., "eip155:1:0xDEAD")
 451 | 
 452 | BlockID: A CAIP-2 scoped identifier for identifying blocks across chains. (e.g., "eip155:1:15672")
 453 | 
 454 | ChainID: A CAIP-2 compliant chain ID. (e.g., "eip155:1")
 455 | 
 456 | Address: A 20 byte ethereum address, represented as 0x-prefixed hexadecimal. (e.g., "0x1234567800000000000000000000000000000abc")
 457 | 
 458 | Rule 6: Enum Usage (Mandatory)
 459 | 
 460 | Problem: Using a string value when an enum type is expected.
 461 | 
 462 | Solution: Always use the correct values for an enum type. Failure to do so is a fatal error.
 463 | 
 464 | Example (Correct)
 465 | 
 466 | query GetVotes($input: VotesInput!) {
 467 |   votes(input: $input) {
 468 |     nodes {
 469 |       id
 470 |        type
 471 |     }
 472 |   }
 473 | }
 474 | content_copy
 475 | download
 476 | Use code with caution.
 477 | Graphql
 478 | 
 479 | Input:
 480 | 
 481 | input VotesInput {
 482 |     filters: VotesFiltersInput
 483 |     page: PageInput
 484 |     sort: VotesSortInput
 485 | }
 486 | 
 487 | input VotesFiltersInput {
 488 |     proposalId: IntID
 489 |     proposalIds: [IntID!]
 490 |     voter: Address
 491 |     includePendingVotes: Boolean
 492 |     type: VoteType
 493 | }
 494 | enum VoteType {
 495 |     abstain
 496 |     against
 497 |     for
 498 |     pendingabstain
 499 |     pendingagainst
 500 |     pendingfor
 501 | }
 502 | content_copy
 503 | download
 504 | Use code with caution.
 505 | Graphql
 506 | 
 507 | Query: (Correctly using an enum type)
 508 | 
 509 | query GetVotesFor {
 510 |     votes(input: {
 511 |         filters: {
 512 |             type: for
 513 |              proposalId: 123
 514 |         }
 515 |     })
 516 |     {
 517 |       nodes {
 518 |         id
 519 |           type
 520 |       }
 521 |     }
 522 | }
 523 | content_copy
 524 | download
 525 | Use code with caution.
 526 | Graphql
 527 | 
 528 | Example (Incorrect - Avoid):
 529 | 
 530 | query GetVotesFor {
 531 |     votes(input: {
 532 |         filters: {
 533 |             type: "for" # Incorrect: using a string, when a VoteType enum is expected
 534 |              proposalId: 123
 535 |         }
 536 |     })
 537 |     {
 538 |       nodes {
 539 |         id
 540 |           type
 541 |       }
 542 |     }
 543 | }
 544 | content_copy
 545 | download
 546 | Use code with caution.
 547 | Graphql
 548 | 
 549 | Action: Always ensure the values of enum types match the provided options, and that you are not using a string when an enum is expected. Failure to do so is a fatal error.
 550 | 
 551 | Rule 7: Pagination Handling (Mandatory)
 552 | 
 553 | Problem: Queries that return paginated data do not return complete results if pagination is not handled.
 554 | 
 555 | Solution: You must always use the page input with appropriate limit, afterCursor and beforeCursor values to ensure you are retrieving all the results that you want. You must also use the pageInfo field on the returned type to use the cursors.
 556 | 
 557 | Example (Correct):
 558 | 
 559 | query GetPaginatedProposals($input: ProposalsInput!) {
 560 |   proposals(input: $input) {
 561 |     nodes {
 562 |         ... on Proposal {
 563 |             id
 564 |             metadata {
 565 |                 title
 566 |             }
 567 |         }
 568 |     }
 569 |    pageInfo {
 570 |        firstCursor
 571 |        lastCursor
 572 |        count
 573 |      }
 574 |   }
 575 | }
 576 | content_copy
 577 | download
 578 | Use code with caution.
 579 | Graphql
 580 | * **Input**
 581 | content_copy
 582 | download
 583 | Use code with caution.
 584 | input ProposalsInput {
 585 |     filters: ProposalsFiltersInput
 586 |     page: PageInput
 587 |     sort: ProposalsSortInput
 588 | }
 589 | 
 590 | input ProposalsFiltersInput {
 591 |     governorId: AccountID
 592 |     includeArchived: Boolean
 593 |     isDraft: Boolean
 594 |     organizationId: IntID
 595 |     proposer: Address
 596 | }
 597 | input ProposalsSortInput {
 598 |     isDescending: Boolean!
 599 |     sortBy: ProposalsSortBy!
 600 | }
 601 | enum ProposalsSortBy {
 602 |     id
 603 | }
 604 | 
 605 | input PageInput {
 606 |     afterCursor: String
 607 |     beforeCursor: String
 608 |     limit: Int
 609 | }
 610 | content_copy
 611 | download
 612 | Use code with caution.
 613 | Graphql
 614 | 
 615 | Query:
 616 | 
 617 | query GetProposalsWithPagination {
 618 |     proposals(input: {
 619 |        page: {
 620 |            limit: 20
 621 |        }
 622 |     }) {
 623 |         nodes {
 624 |             ... on Proposal {
 625 |               id
 626 |                metadata {
 627 |                 title
 628 |               }
 629 |            }
 630 |          }
 631 |          pageInfo {
 632 |            firstCursor
 633 |             lastCursor
 634 |            count
 635 |           }
 636 |         }
 637 | }
 638 | content_copy
 639 | download
 640 | Use code with caution.
 641 | Graphql
 642 | 
 643 | Query: (Using cursors to get the next page of results)
 644 | 
 645 | query GetProposalsWithPagination {
 646 |     proposals(input: {
 647 |        page: {
 648 |            limit: 20
 649 |            afterCursor: "cursorFromPreviousQuery"
 650 |        }
 651 |     }) {
 652 |         nodes {
 653 |           ... on Proposal {
 654 |             id
 655 |             metadata {
 656 |                 title
 657 |               }
 658 |            }
 659 |         }
 660 |          pageInfo {
 661 |            firstCursor
 662 |             lastCursor
 663 |            count
 664 |           }
 665 |         }
 666 | }
 667 | content_copy
 668 | download
 669 | Use code with caution.
 670 | Graphql
 671 | 
 672 | Example (Incorrect - Avoid):
 673 | 
 674 | query GetProposals { # Incorrect: Not using the `page` input.
 675 |   proposals {
 676 |       nodes {
 677 |          ... on Proposal {
 678 |           id
 679 |             metadata {
 680 |                 title
 681 |             }
 682 |            }
 683 |        }
 684 |     }
 685 | }
 686 | content_copy
 687 | download
 688 | Use code with caution.
 689 | Graphql
 690 | 
 691 | Action: Always use the page input with a limit, and use the cursors to iterate through pages, especially when you are working with paginated data. Failure to do so may result in incomplete data.
 692 | 
 693 | Rule 8: Correctly Querying Related Data (Mandatory)
 694 | 
 695 | Problem: Attempting to query related data as nested fields within a type will lead to errors if the related data must be fetched in a separate query.
 696 | 
 697 | Solution: You must fetch related data by using separate queries, instead of assuming that related data is queryable as nested fields.
 698 | 
 699 | Example (Correct)
 700 | 
 701 | query GetProposalAndVotes($proposalId: IntID!, $voter: Address) {
 702 |   proposal(input: { id: $proposalId}) {
 703 |     id
 704 |     metadata {
 705 |       title
 706 |     }
 707 |     status
 708 |   }
 709 |   votes(input: {
 710 |     filters: {
 711 |       proposalId: $proposalId
 712 |       voter: $voter
 713 |     }
 714 |   }) {
 715 |     nodes {
 716 |       ... on Vote {
 717 |         type
 718 |         amount
 719 |         voter {
 720 |           id
 721 |           name
 722 |         }
 723 |       }
 724 |     }
 725 |   }
 726 | }
 727 | content_copy
 728 | download
 729 | Use code with caution.
 730 | Graphql
 731 | 
 732 | Example (Incorrect - Avoid):
 733 | 
 734 | query GetProposals {
 735 |   proposals {
 736 |     ... on Proposal {
 737 |       id
 738 |       metadata {
 739 |         title
 740 |       }
 741 |       votes(input: {
 742 |         filters: {
 743 |           voter: "0x..." # Incorrect: Trying to access votes as a nested field
 744 |         }
 745 |       })
 746 |     }
 747 |   }
 748 | }
 749 | content_copy
 750 | download
 751 | Use code with caution.
 752 | Graphql
 753 | * **Prevention:** This can be prevented by reading rule 8, and by consulting the schema before creating a query.
 754 | content_copy
 755 | download
 756 | Use code with caution.
 757 | 
 758 | Action: Do not attempt to fetch related data in the same query, instead, fetch it via a second query. Failure to do so will result in an error.
 759 | 
 760 | Rule 9: API Constraints (Mandatory)
 761 | 
 762 | Problem: Not all fields or properties are queryable in all situations. Some queries have explicit requirements that must be met.
 763 | 
 764 | Solution: You must always check your query against the known API constraints, and ensure that all requirements are met.
 765 | 
 766 | Example:
 767 | 
 768 | The votes query requires that proposalId or proposalIds is provided in the input. This means you cannot query votes without first querying proposals. Failure to do so will result in an error.
 769 | 
 770 | An error you may see is: "proposalId or proposalIds must be provided"
 771 | 
 772 | Prevention: This can be prevented by reading rule 9, and by consulting the schema before creating a query.
 773 | 
 774 | Action: Ensure all API constraits are met and that any required fields are provided when making a query. Failure to do so will result in an error.
 775 | 
 776 | Rule 10: Multi-Step Queries (Mandatory)
 777 | 
 778 | Problem: Some data can only be accessed by using multiple queries, and requires that data from one query be used as the input for a subsequent query.
 779 | 
 780 | Solution: Properly construct multi-step queries by breaking them into a sequence of independent GraphQL queries. Ensure the output of one query is correctly used as input for the next query.
 781 | 
 782 | Example
 783 | 
 784 | If you need to fetch all the votes from a specific organization, you first need to get the organization id, then use that id to query all the proposals, and then finally, you need to query for all the votes associated with each proposal.
 785 | 
 786 | Correct Example
 787 | 
 788 | # Step 1: Get the organization ID using a query that filters by slug
 789 | 
 790 | query GetOrganizationId($slug: String!) {
 791 |   organization(input: {slug: $slug}) {
 792 |     id
 793 |   }
 794 | }
 795 | 
 796 | # Step 2: Get the proposals for the given organization
 797 | query GetProposalsForOrganization($organizationId: IntID!) {
 798 |     proposals(input: {
 799 |       filters: {
 800 |         organizationId: $organizationId
 801 |       }
 802 |     }) {
 803 |         nodes {
 804 |             ... on Proposal {
 805 |                 id
 806 |             }
 807 |         }
 808 |     }
 809 | }
 810 | 
 811 | # Step 3: Get all the votes for all of the proposals.
 812 | query GetVotesForProposals($proposalIds: [IntID!]!) {
 813 |     votes(input: {
 814 |       filters: {
 815 |          proposalIds: $proposalIds
 816 |       }
 817 |     })
 818 |         {
 819 |         nodes {
 820 |             ... on Vote {
 821 |                 id
 822 |                 type
 823 |                 amount
 824 |             }
 825 |         }
 826 |     }
 827 | }
 828 | content_copy
 829 | download
 830 | Use code with caution.
 831 | Graphql
 832 | *   **Action:**  When a query requires data from another query, structure it as a multi-step query, and use the result of the first query as the input to the subsequent query.
 833 | content_copy
 834 | download
 835 | Use code with caution.
 836 | 
 837 | Rule 11: Fragment Usage (Mandatory)
 838 | 
 839 | Problem: Defining fragments that aren't used creates unnecessary code.
 840 | 
 841 | Solution: You must always use all defined fragments, and any unused fragments must be removed before submitting a query.
 842 | 
 843 | Example
 844 | 
 845 | fragment BasicAccountInfo on Account {
 846 |   id
 847 |   address
 848 |   ens
 849 | }
 850 | 
 851 | fragment VoteDetails on Vote {
 852 |     type
 853 |     amount
 854 | }
 855 | 
 856 | query GetVotes($input: VotesInput!) {
 857 |   votes(input: $input) {
 858 |     nodes {
 859 |       ... on Vote {
 860 |         ...VoteDetails # Correct: Using the VoteDetails fragment
 861 |       }
 862 |     }
 863 |   }
 864 | }
 865 | content_copy
 866 | download
 867 | Use code with caution.
 868 | Graphql
 869 | *   **Prevention:** This can be prevented by following rule 3.
 870 | *   **Action:** All defined fragments *must* be used, and any unused fragments *must* be removed before submitting a query.
 871 | content_copy
 872 | download
 873 | Use code with caution.
 874 | 
 875 | Complete Schema Reference
 876 | 
 877 | While we cannot provide the entire schema (it would be too lengthy), here are the core types and their most commonly used fields, and examples of the input types:
 878 | 
 879 | type Account {
 880 |   id: ID!
 881 |   address: String!
 882 |   ens: String
 883 |   twitter: String
 884 |   name: String!
 885 |   bio: String!
 886 |   picture: String
 887 |   safes: [AccountID!]
 888 |   type: AccountType!
 889 |   votes(governorId: AccountID!): Uint256!
 890 |   proposalsCreatedCount(input: ProposalsCreatedCountInput!): Int!
 891 | }
 892 | 
 893 | enum AccountType {
 894 |     EOA
 895 |     SAFE
 896 | }
 897 | type Delegate {
 898 |     id: IntID!
 899 |     account: Account!
 900 |     chainId: ChainID
 901 |     delegatorsCount: Int!
 902 |     governor: Governor
 903 |     organization: Organization
 904 |     statement: DelegateStatement
 905 |     token: Token
 906 |     votesCount(blockNumber: Int): Uint256!
 907 |   }
 908 | 
 909 |   input DelegateInput {
 910 |     address: Address!
 911 |     governorId: AccountID
 912 |     organizationId: IntID
 913 |   }
 914 | 
 915 | type DelegateStatement {
 916 |     id: IntID!
 917 |     address: Address!
 918 |     organizationID: IntID!
 919 |     statement: String!
 920 |     statementSummary: String
 921 |     isSeekingDelegation: Boolean
 922 |     issues: [Issue!]
 923 |   }
 924 | 
 925 | type Delegation {
 926 |     id: IntID!
 927 |     blockNumber: Int!
 928 |     blockTimestamp: Timestamp!
 929 |     chainId: ChainID!
 930 |     delegator: Account!
 931 |     delegate: Account!
 932 |     organization: Organization!
 933 |     token: Token!
 934 |     votes: Uint256!
 935 | }
 936 | input DelegationInput {
 937 |     address: Address!
 938 |     tokenId: AssetID!
 939 |   }
 940 | input DelegationsInput {
 941 |   filters: DelegationsFiltersInput!
 942 |   page: PageInput
 943 |   sort: DelegationsSortInput
 944 | }
 945 | input DelegationsFiltersInput {
 946 |     address: Address!
 947 |     governorId: AccountID
 948 |     organizationId: IntID
 949 | }
 950 | input DelegationsSortInput {
 951 |     isDescending: Boolean!
 952 |     sortBy: DelegationsSortBy!
 953 | }
 954 | enum DelegationsSortBy {
 955 |   id
 956 |   votes
 957 | }
 958 | 
 959 | type Governor {
 960 |     id: AccountID!
 961 |     chainId: ChainID!
 962 |     contracts: Contracts!
 963 |     isIndexing: Boolean!
 964 |     isBehind: Boolean!
 965 |     isPrimary: Boolean!
 966 |     kind: GovernorKind!
 967 |     name: String!
 968 |     organization: Organization!
 969 |     proposalStats: ProposalStats!
 970 |     parameters: GovernorParameters!
 971 |     quorum: Uint256!
 972 |     slug: String!
 973 |     timelockId: AccountID
 974 |     tokenId: AssetID!
 975 |     token: Token!
 976 |     type: GovernorType!
 977 |     delegatesCount: Int!
 978 |     delegatesVotesCount: Uint256!
 979 |     tokenOwnersCount: Int!
 980 |     metadata: GovernorMetadata
 981 |   }
 982 |   type GovernorContract {
 983 |     address: Address!
 984 |     type: GovernorType!
 985 |   }
 986 | 
 987 |   input GovernorInput {
 988 |     id: AccountID
 989 |     slug: String
 990 |   }
 991 | 
 992 | type Organization {
 993 |     id: IntID!
 994 |     slug: String!
 995 |     name: String!
 996 |     chainIds: [ChainID!]!
 997 |     tokenIds: [AssetID!]!
 998 |     governorIds: [AccountID!]!
 999 |     metadata: OrganizationMetadata
1000 |     creator: Account
1001 |     hasActiveProposals: Boolean!
1002 |     proposalsCount: Int!
1003 |     delegatesCount: Int!
1004 |     delegatesVotesCount: Uint256!
1005 |     tokenOwnersCount: Int!
1006 |     endorsementService: EndorsementService
1007 | }
1008 | input OrganizationInput {
1009 |   id: IntID
1010 |   slug: String
1011 | }
1012 | input OrganizationsInput {
1013 |   filters: OrganizationsFiltersInput
1014 |   page: PageInput
1015 |   sort: OrganizationsSortInput
1016 | }
1017 | input OrganizationsFiltersInput {
1018 |   address: Address
1019 |   chainId: ChainID
1020 |   hasLogo: Boolean
1021 |     isMember: Boolean
1022 | }
1023 | input OrganizationsSortInput {
1024 |     isDescending: Boolean!
1025 |     sortBy: OrganizationsSortBy!
1026 | }
1027 | 
1028 | enum OrganizationsSortBy {
1029 |   id
1030 |   name
1031 |   explore
1032 |   popular
1033 | }
1034 | 
1035 | type Proposal {
1036 |     id: IntID!
1037 |     onchainId: String
1038 |     block: Block
1039 |     chainId: ChainID!
1040 |     creator: Account
1041 |     end: BlockOrTimestamp!
1042 |     events: [ProposalEvent!]!
1043 |     executableCalls: [ExecutableCall!]
1044 |     governor: Governor!
1045 |     metadata: ProposalMetadata!
1046 |     organization: Organization!
1047 |     proposer: Account
1048 |     quorum: Uint256
1049 |     status: ProposalStatus!
1050 |     start: BlockOrTimestamp!
1051 |     voteStats: [VoteStats!]
1052 | }
1053 | input ProposalInput {
1054 |   id: IntID
1055 |   onchainId: String
1056 |   governorId: AccountID
1057 |     includeArchived: Boolean
1058 |     isLatest: Boolean
1059 | }
1060 | type ProposalMetadata {
1061 |   title: String
1062 |   description: String
1063 |   eta: Int
1064 |   ipfsHash: String
1065 |       previousEnd: Int
1066 |       timelockId: AccountID
1067 |       txHash: Hash
1068 |       discourseURL: String
1069 |   snapshotURL: String
1070 | }
1071 | 
1072 | input ProposalsInput {
1073 |   filters: ProposalsFiltersInput
1074 |   page: PageInput
1075 |   sort: ProposalsSortInput
1076 | }
1077 | input ProposalsFiltersInput {
1078 |   governorId: AccountID
1079 |   includeArchived: Boolean
1080 |     isDraft: Boolean
1081 |   organizationId: IntID
1082 |   proposer: Address
1083 | }
1084 | input ProposalsSortInput {
1085 |   isDescending: Boolean!
1086 |   sortBy: ProposalsSortBy!
1087 | }
1088 | enum ProposalsSortBy {
1089 |   id
1090 | }
1091 | 
1092 | type Token {
1093 |     id: AssetID!
1094 |     type: TokenType!
1095 |     name: String!
1096 |     symbol: String!
1097 |     supply: Uint256!
1098 |     decimals: Int!
1099 |     eligibility: Eligibility
1100 |     isIndexing: Boolean!
1101 |     isBehind: Boolean!
1102 | }
1103 | type Vote {
1104 |     id: IntID!
1105 |     amount: Uint256!
1106 |     block: Block!
1107 |     chainId: ChainID!
1108 |     isBridged: Boolean
1109 |     proposal: Proposal!
1110 |     reason: String
1111 |     type: VoteType!
1112 |     txHash: Hash!
1113 |     voter: Account!
1114 | }
1115 | input VotesInput {
1116 |     filters: VotesFiltersInput
1117 |     page: PageInput
1118 |     sort: VotesSortInput
1119 |   }
1120 | input VotesFiltersInput {
1121 |     proposalId: IntID
1122 |     proposalIds: [IntID!]
1123 |     voter: Address
1124 |     includePendingVotes: Boolean
1125 |     type: VoteType
1126 | }
1127 | input VotesSortInput {
1128 |     isDescending: Boolean!
1129 |     sortBy: VotesSortBy!
1130 | }
1131 | enum VotesSortBy {
1132 |   id
1133 |   amount
1134 | }
1135 | 
1136 | enum VoteType {
1137 |     abstain
1138 |     against
1139 |     for
1140 |     pendingabstain
1141 |     pendingagainst
1142 |     pendingfor
1143 | }
1144 | content_copy
1145 | download
1146 | Use code with caution.
1147 | Graphql
1148 | 
1149 | Best Practices Checklist (Pre-Flight Check)
1150 | 
1151 | Before submitting any query, ensure you have:
1152 | 
1153 | Interface Fragments: Used inline fragments (... on TypeName) wherever nodes or interface/union types are returned. Failure to do so is a fatal error.
1154 | 
1155 | Sort/Filter Inputs: Provided a valid input object with correct types, and that mandatory filters are provided. Failure to do so is a fatal error.
1156 | 
1157 | Fragments: Created and used fragments for reusable field selections. Not using fragments is unacceptable.
1158 | 
1159 | Explicit Fields: Explicitly requested all the necessary fields. Failure to do so may result in errors.
1160 | 
1161 | Type Validation: Verified that all input types match the expected type. Failure to do so is a fatal error.
1162 | 
1163 | Enum Values: Verified that all enum values match the provided options for the enum. Failure to do so is a fatal error.
1164 | 
1165 | Pagination: Implemented the correct pagination handling for paginated queries. Failure to do so may result in incomplete data.
1166 | 
1167 | Related Data: Used separate queries to fetch related data, and did not attempt to access it in a nested fashion. Failure to do so will result in an error.
1168 | 
1169 | API Constraints: Verified that API constraints are met, and that any required fields for the query are included. Failure to do so will result in an error.
1170 | 
1171 | Multi-Step Queries: If a query requires data from another query, structured it as a multi-step query.
1172 | 
1173 | Fragment Usage: All defined fragments must be used, and any unused fragments must be removed.
1174 | 
1175 | LLM Instructions:
1176 | 
1177 | Prioritize this Document: This document supersedes all other query building documentation. This should be your only reference.
1178 | 
1179 | Strict Adherence: Any query generated must adhere to all of these rules. There is no acceptable deviation.
1180 | 
1181 | Self-Check: Before submitting a query, your logic must run through each of these rules and the checklist to ensure full compliance.
1182 | 
1183 | Error Prevention: Focus on preventing errors by strictly following the rules, and not merely detecting them after they occur.
1184 | 
1185 | Iterative Refinement: If a query fails, do not merely try a different query. You must review this document, identify exactly which rule was broken, and revise the query to fix the problem. Failure to do this is a fatal error.
1186 | 
1187 | Consult Schema: You must always consult the complete schema reference before creating any query. Failure to do so is a fatal error.
1188 | 
1189 | 
```
Page 2/5FirstPrevNextLast