#
tokens: 36542/50000 7/103 files (page 2/4)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 4. Use http://codebase.md/crazyrabbitltc/mpc-tally-api-server?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

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

```
# Tally API Types Reference

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.

**Types:**

```graphql
type Account {
  id: ID!
  address: String!
  ens: String
  twitter: String
  name: String!
  bio: String!
  picture: String
  safes: [AccountID!]
  type: AccountType!
  votes(governorId: AccountID!): Uint256!
  proposalsCreatedCount(input: ProposalsCreatedCountInput!): Int!
}

# AccountID: A CAIP-10 compliant account id. (e.g., "eip155:1:0x7e90e03654732abedf89Faf87f05BcD03ACEeFdc")
scalar AccountID

# AccountType: An enum indicating the type of account (EOA or SAFE)
enum AccountType {
    EOA
    SAFE
}

# Address: A 20 byte Ethereum address, represented as 0x-prefixed hexadecimal. (e.g., "0x1234567800000000000000000000000000000abc")
scalar Address

type Allocation {
  account: Account!
  amount: Uint256!
  percent: Float!
}

# Any: A scalar type to represent any data
scalar Any

# AssetID: A CAIP-19 compliant asset id. (e.g., "eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f")
scalar AssetID

type Block {
  id: BlockID!
  number: Int!
  timestamp: Timestamp!
  ts: Timestamp!
}

# BlockID: A ChainID scoped identifier for identifying blocks across chains. Ex: eip155:1:15672.
scalar BlockID

# BlockOrTimestamp:  A union type that represents either a Block or a BlocklessTimestamp.
union BlockOrTimestamp = Block | BlocklessTimestamp

type BlocklessTimestamp {
  timestamp: Timestamp!
}

# Boolean: Represents a `true` or `false` value.
scalar Boolean

# Bytes: An arbitrary length binary string, represented as 0x-prefixed hexadecimal. (e.g., "0x4321abcd").
scalar Bytes

type Chain {
  id: ChainID!
  layer1Id: ChainID
  name: String!
  mediumName: String!
  shortName: String!
  blockTime: Float!
  isTestnet: Boolean!
  nativeCurrency: NativeCurrency!
  chain: String!
  useLayer1VotingPeriod: Boolean!
}

# ChainID: CAIP-2 compliant chain id. (e.g., "eip155:1").
scalar ChainID

type CompetencyFieldDescriptor {
  id: IntID!
  name: String!
  description: String!
}

type Contracts {
  governor: GovernorContract!
  tokens: [TokenContract!]!
}

type Contributor {
  id: IntID!
  account: Account!
  isCurator: Boolean!
  isApplyingForCouncil: Boolean!
  competencyFieldDescriptors: [CompetencyFieldDescriptor!]!
  bio: UserBio!
}

type DataDecoded {
  method: String!
  parameters: [Parameter!]!
}

# Date: A date in the format ISO 8601 format, e.g. YYYY-MM-DD. (e.g., "2022-09-22")
scalar Date

type DecodedCalldata {
  signature: String!
  parameters: [DecodedParameter!]!
}

type DecodedParameter {
  name: String!
  type: String!
  value: String!
}

type Delegate {
    id: IntID!
    account: Account!
    chainId: ChainID
    delegatorsCount: Int!
    governor: Governor
    organization: Organization
    statement: DelegateStatement
    token: Token
    votesCount(blockNumber: Int): Uint256!
  }

input DelegateInput {
  address: Address!
  governorId: AccountID
  organizationId: IntID
}

type DelegateStatement {
  id: IntID!
  address: Address!
  organizationID: IntID!
  statement: String!
  statementSummary: String
  isSeekingDelegation: Boolean
  issues: [Issue!]
}

input DelegatesFiltersInput {
  address: Address
  governorId: AccountID
  hasVotes: Boolean
  hasDelegators: Boolean
  issueIds: [IntID!]
  isSeekingDelegation: Boolean
  organizationId: IntID
}

input DelegatesInput {
  filters: DelegatesFiltersInput!
  page: PageInput
  sort: DelegatesSortInput
}

enum DelegatesSortBy {
  id
  votes
  delegators
  prioritized
}

input DelegatesSortInput {
  isDescending: Boolean!
  sortBy: DelegatesSortBy!
}

type Delegation {
  id: IntID!
  blockNumber: Int!
  blockTimestamp: Timestamp!
  chainId: ChainID!
  delegator: Account!
  delegate: Account!
  organization: Organization!
  token: Token!
  votes: Uint256!
}

input DelegationInput {
  address: Address!
  tokenId: AssetID!
}

input DelegationsFiltersInput {
  address: Address!
    governorId: AccountID
   organizationId: IntID
}

input DelegationsInput {
  filters: DelegationsFiltersInput!
  page: PageInput
  sort: DelegationsSortInput
}

enum DelegationsSortBy {
  id
  votes
}

input DelegationsSortInput {
  isDescending: Boolean!
  sortBy: DelegationsSortBy!
}

type Eligibility {
  status: EligibilityStatus!
  proof: [String!]
  amount: Uint256
  tx: HashID
}

enum EligibilityStatus {
    NOTELIGIBLE
    ELIGIBLE
    CLAIMED
}

type EndorsementService {
  id: IntID!
  competencyFields: [CompetencyFieldDescriptor!]!
}

type ExecutableCall {
   calldata: Bytes!
    chainId: ChainID!
   index: Int!
    signature: String
    target: Address!
    type: ExecutableCallType
    value: Uint256!
    decodedCalldata: DecodedCalldata
  }

enum ExecutableCallType {
    custom
    erc20transfer
    erc20transferarbitrum
    empty
    nativetransfer
    orcamanagepod
    other
    reward
    swap
}

# Float: A signed double-precision fractional values as specified by IEEE 754. (e.g., 987.65)
scalar Float

type Governor {
    id: AccountID!
    chainId: ChainID!
    contracts: Contracts!
    isIndexing: Boolean!
    isBehind: Boolean!
    isPrimary: Boolean!
    kind: GovernorKind!
    name: String!
    organization: Organization!
    proposalStats: ProposalStats!
    parameters: GovernorParameters!
    quorum: Uint256!
    slug: String!
    timelockId: AccountID
    tokenId: AssetID!
    token: Token!
    type: GovernorType!
    delegatesCount: Int!
    delegatesVotesCount: Uint256!
    tokenOwnersCount: Int!
    metadata: GovernorMetadata
  }

type GovernorContract {
  address: Address!
  type: GovernorType!
}

input GovernorInput {
  id: AccountID
  slug: String
}

enum GovernorKind {
    single
    multiprimary
    multisecondary
    multiother
    hub
    spoke
}

type GovernorMetadata {
    description: String
}

type GovernorParameters {
    quorumVotes: Uint256
    proposalThreshold: Uint256
    votingDelay: Uint256
    votingPeriod: Uint256
    gracePeriod: Uint256
    quorumNumerator: Uint256
    quorumDenominator: Uint256
    clockMode: String
    nomineeVettingDuration: Uint256
     fullWeightDuration: Uint256
  }

enum GovernorType {
    governoralpha
    governorbravo
    openzeppelingovernor
    aave
     nounsfork
        nomineeelection
        memberelection
        hub
        spoke
}

input GovernorsFiltersInput {
  organizationId: IntID!
  includeInactive: Boolean
    excludeSecondary: Boolean
}

input GovernorsInput {
  filters: GovernorsFiltersInput
  page: PageInput
  sort: GovernorsSortInput
}

enum GovernorsSortBy {
  id
}

input GovernorsSortInput {
  isDescending: Boolean!
  sortBy: GovernorsSortBy!
}

# Hash: For identifying transactions on a chain. Ex: 0xDEAD.
scalar Hash

# HashID: A ChainID scoped identifier for identifying transactions across chains. Ex: eip155:1:0xDEAD.
scalar HashID

# ID: The ID scalar type represents a unique identifier
scalar ID

# Int: The Int scalar type represents non-fractional signed whole numeric values.
scalar Int

# IntID: A 64bit integer as a string - this is larger than Javascript's number.
scalar IntID

type Issue {
  id: IntID!
  organizationId: IntID!
  name: String!
  description: String!
}

type Member {
  id: ID!
  account: Account!
  organization: Organization!
}

type NativeCurrency {
  name: String!
  symbol: String!
  decimals: Int!
}

# Node: Union of all node types that are paginated.
union Node =
    | Delegate
    | Organization
    | Member
    | Delegation
    | Governor
    | Proposal
    | Vote
    | StakeEvent
    | StakeEarning
    | Contributor
    | Allocation

type Organization {
  id: IntID!
  slug: String!
  name: String!
  chainIds: [ChainID!]!
  tokenIds: [AssetID!]!
  governorIds: [AccountID!]!
  metadata: OrganizationMetadata
  creator: Account
  hasActiveProposals: Boolean!
  proposalsCount: Int!
  delegatesCount: Int!
  delegatesVotesCount: Uint256!
  tokenOwnersCount: Int!
  endorsementService: EndorsementService
}

input OrganizationInput {
  id: IntID
  slug: String
}

type OrganizationMetadata {
    color: String
    description: String
    icon: String
     socials: Socials
     karmaName: String
}

input OrganizationsFiltersInput {
  address: Address
  chainId: ChainID
  hasLogo: Boolean
    isMember: Boolean
}

input OrganizationsInput {
  filters: OrganizationsFiltersInput
  page: PageInput
  sort: OrganizationsSortInput
}

enum OrganizationsSortBy {
    id
    name
    explore
    popular
}

input OrganizationsSortInput {
  isDescending: Boolean!
  sortBy: OrganizationsSortBy!
}

type PageInfo {
    firstCursor: String
    lastCursor: String
    count: Int
}

input PageInput {
  afterCursor: String
  beforeCursor: String
  limit: Int
}

# PaginatedOutput: Wraps a list of nodes and the pagination info
type PaginatedOutput {
    nodes: [Node!]!
    pageInfo: PageInfo!
}

type Parameter {
  name: String!
  type: String!
  value: Any!
    valueDecoded: [ValueDecoded!]
}

type Proposal {
    id: IntID!
    onchainId: String
    block: Block
    chainId: ChainID!
    creator: Account
    end: BlockOrTimestamp!
    events: [ProposalEvent!]!
    executableCalls: [ExecutableCall!]
    governor: Governor!
    metadata: ProposalMetadata!
    organization: Organization!
    proposer: Account
    quorum: Uint256
    status: ProposalStatus!
    start: BlockOrTimestamp!
    voteStats: [VoteStats!]
}

type ProposalEvent {
    block: Block!
    chainId: ChainID!
    createdAt: Timestamp!
    type: ProposalEventType!
    txHash: Hash!
}

enum ProposalEventType {
    activated
    canceled
    created
    defeated
    drafted
    executed
    expired
    extended
    pendingexecution
    queued
    succeeded
  callexecuted
        crosschainexecuted
}

input ProposalInput {
  id: IntID
  onchainId: String
  governorId: AccountID
    includeArchived: Boolean
    isLatest: Boolean
}

type ProposalMetadata {
    title: String
    description: String
    eta: Int
    ipfsHash: String
        previousEnd: Int
        timelockId: AccountID
        txHash: Hash
        discourseURL: String
    snapshotURL: String
}

type ProposalStats {
    total: Int!
    active: Int!
    failed: Int!
    passed: Int!
}

enum ProposalStatus {
    active
    archived
    canceled
    callexecuted
    defeated
    draft
    executed
    expired
    extended
    pending
    queued
    pendingexecution
    submitted
    succeeded
        crosschainexecuted
}

input ProposalsCreatedCountInput {
  governorId: AccountID
    organizationId: IntID
}

input ProposalsFiltersInput {
  governorId: AccountID
  includeArchived: Boolean
    isDraft: Boolean
  organizationId: IntID
  proposer: Address
}

input ProposalsInput {
  filters: ProposalsFiltersInput
  page: PageInput
  sort: ProposalsSortInput
}

enum ProposalsSortBy {
    id
}

input ProposalsSortInput {
  isDescending: Boolean!
  sortBy: ProposalsSortBy!
}

enum Role {
    ADMIN
    USER
}

type StakeEarning {
   amount: Uint256!
  date: Date!
}

type StakeEvent {
  amount: Uint256!
    block: Block!
  type: StakeEventType!
}

enum StakeEventType {
  deposit
  withdraw
}

# String: The String scalar type represents textual data, represented as UTF-8 character sequences
scalar String

# Timestamp: Timestamp is an RFC3339 string.
scalar Timestamp

type Token {
    id: AssetID!
    type: TokenType!
    name: String!
    symbol: String!
    supply: Uint256!
    decimals: Int!
    eligibility: Eligibility
    isIndexing: Boolean!
    isBehind: Boolean!
  }

type TokenContract {
  address: Address!
  type: TokenType!
}

input TokenInput {
    id: AssetID!
}

enum TokenType {
    ERC20
    ERC721
        ERC20AAVE
        SOLANASPOKETOKEN
}

# Uint256: Uint256 is a large unsigned integer represented as a string.
scalar Uint256

type UserBio {
  value: String!
    summary: String!
}

type ValueDecoded {
  operation: Int!
  to: String!
  value: String!
  data: String!
  dataDecoded: DataDecoded
}

type Vote {
  id: IntID!
  amount: Uint256!
  block: Block!
  chainId: ChainID!
  isBridged: Boolean
  proposal: Proposal!
  reason: String
  type: VoteType!
  txHash: Hash!
  voter: Account!
}

type VoteStats {
  type: VoteType!
  votesCount: Uint256!
  votersCount: Int!
    percent: Float!
}

enum VoteType {
    abstain
    against
    for
    pendingabstain
    pendingagainst
    pendingfor
}

input VotesFiltersInput {
  proposalId: IntID
    proposalIds: [IntID!]
  voter: Address
    includePendingVotes: Boolean
    type: VoteType
}

input VotesInput {
  filters: VotesFiltersInput
  page: PageInput
  sort: VotesSortInput
}

enum VotesSortBy {
    id
    amount
}

input VotesSortInput {
  isDescending: Boolean!
  sortBy: VotesSortBy!
}

```

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

```typescript
import { GraphQLClient } from "graphql-request";
import { getDAO } from "./organizations/getDAO.js";
import { listDAOs } from "./organizations/listDAOs.js";
import { listProposals } from "./proposals/listProposals.js";
import { getProposal } from "./proposals/getProposal.js";
import { getProposalVoters } from "./proposals/getProposalVoters.js";
import { getProposalTimeline } from "./proposals/getProposalTimeline.js";
import { getProposalSecurityAnalysis } from "./proposals/getProposalSecurityAnalysis.js";
import { listDelegates } from "./delegates/listDelegates.js";
import { getAddressProposals } from "./addresses/getAddressProposals.js";
import { getAddressDAOProposals } from "./addresses/getAddressDAOProposals.js";
import { getAddressVotes } from "./addresses/getAddressVotes.js";
import { getAddressCreatedProposals } from "./addresses/getAddressCreatedProposals.js";
import { getAddressMetadata } from "./addresses/getAddressMetadata.js";
import { getAddressGovernances } from "./addresses/getAddressGovernances.js";
import { getAddressReceivedDelegations } from "./addresses/getAddressReceivedDelegations.js";
import { getDelegateStatement } from "./delegates/getDelegateStatement.js";
import { getDelegators } from "./delegators/getDelegators.js";
import type {
  Organization,
  OrganizationsResponse,
  ListDAOsParams,
  PageInfo,
  Token,
} from "./organizations/organizations.types.js";
import type { Delegate } from "./delegates/delegates.types.js";
import type {
  Delegation,
  GetDelegatorsParams,
  TokenInfo,
} from "./delegators/delegators.types.js";
import type { GetAddressReceivedDelegationsInput } from "./addresses/addresses.types.js";
import type { DelegateStatement } from "./delegates/delegates.types.js";
import type {
  ProposalsInput,
  ProposalsResponse,
  ProposalInput,
  ProposalDetailsResponse,
} from "./proposals/index.js";
import type {
  GetProposalVotersInput,
  ProposalVotersResponse,
} from "./proposals/getProposalVoters.types.js";
import type {
  GetProposalTimelineInput,
  ProposalTimelineResponse,
} from "./proposals/getProposalTimeline.types.js";
import type {
  GetProposalSecurityAnalysisInput,
  ProposalSecurityAnalysisResponse,
} from "./proposals/getProposalSecurityAnalysis.types.js";
import type {
  AddressProposalsInput,
  AddressProposalsResponse,
  AddressDAOProposalsInput,
  AddressDAOProposalsResponse,
  AddressVotesInput,
  AddressVotesResponse,
  AddressCreatedProposalsInput,
  AddressCreatedProposalsResponse,
  AddressMetadataInput,
  AddressMetadataResponse,
  AddressGovernancesInput,
  AddressGovernancesResponse,
} from "./addresses/addresses.types.js";
import { getDAOTokens } from "./organizations/getDAO.js";
import { getProposalVotesCast } from "./proposals/getProposalVotesCast.js";
import { getProposalVotesCastList } from "./proposals/getProposalVotesCastList.js";
import { getGovernanceProposalsStats } from "./proposals/getGovernanceProposalsStats.js";
import type {
  GetProposalVotesCastInput,
  ProposalVotesCastResponse,
} from "./proposals/getProposalVotesCast.types.js";
import type {
  GetProposalVotesCastListInput,
  ProposalVotesCastListResponse,
} from "./proposals/getProposalVotesCastList.types.js";
import type { GovernanceProposalsStatsResponse } from "./proposals/proposals.types.js";
import type { ListProposalsParams } from "./proposals/listProposals.types.js";
import type { ListDelegatesParams } from "./delegates/delegates.types.js";

export interface TallyServiceConfig {
  apiKey: string;
  baseUrl?: string;
}

export interface GetAddressReceivedDelegationsOutput {
  nodes: Array<{
    id: string;
    votes: string;
    delegator: {
      id: string;
      address: string;
    };
  }>;
  pageInfo: {
    firstCursor: string | null;
    lastCursor: string | null;
    count: number;
  };
  totalCount: number;
}

export type GetDelegateStatementInput = {
  address: string;
} & (
  | { governorId: string; organizationSlug?: never }
  | { organizationSlug: string; governorId?: never }
);

export class TallyService {
  private client: GraphQLClient;

  constructor(config: TallyServiceConfig) {
    this.client = new GraphQLClient(
      config.baseUrl || "https://api.tally.xyz/query",
      {
        headers: {
          "Content-Type": "application/json",
          "api-key": config.apiKey,
        },
      }
    );
  }

  async listProposals(params: ListProposalsParams): Promise<ProposalsResponse> {
    return listProposals(this.client, params);
  }

  async getDAO(slug: string): Promise<Organization> {
    const { organization } = await getDAO(this.client, slug);
    return {
      id: organization.id,
      name: organization.name,
      slug: organization.slug,
      chainIds: organization.chainIds,
      tokenIds: organization.tokenIds,
      governorIds: organization.governorIds,
      tokenOwnersCount: organization.tokenOwnersCount,
      delegatesCount: organization.delegatesCount,
      proposalsCount: organization.proposalsCount,
      hasActiveProposals: organization.hasActiveProposals,
      metadata: organization.metadata,
      delegatesVotesCount: organization.delegatesVotesCount || 0,
    };
  }

  async getDAOTokens(tokenIds: string[]): Promise<Token[]> {
    return getDAOTokens(this.client, tokenIds);
  }

  async listDAOs(params: ListDAOsParams = {}): Promise<OrganizationsResponse> {
    return listDAOs(this.client, params);
  }

  async listDelegates(input: ListDelegatesParams) {
    if (!input.organizationSlug) {
      throw new Error("organizationSlug must be a string");
    }
    return listDelegates(this.client, input);
  }

  async getProposal(input: ProposalInput): Promise<ProposalDetailsResponse> {
    return getProposal(this.client, input);
  }

  async getProposalVoters(
    input: GetProposalVotersInput
  ): Promise<ProposalVotersResponse> {
    if (!input.proposalId) {
      throw new Error("proposalId is required");
    }
    return getProposalVoters(this.client, input);
  }

  async getProposalTimeline(
    input: GetProposalTimelineInput
  ): Promise<ProposalTimelineResponse> {
    if (!input.proposalId) {
      throw new Error("proposalId is required");
    }
    return getProposalTimeline(this.client, input);
  }

  async getProposalSecurityAnalysis(
    input: GetProposalSecurityAnalysisInput
  ): Promise<ProposalSecurityAnalysisResponse> {
    if (!input.proposalId) {
      throw new Error("proposalId is required");
    }
    return getProposalSecurityAnalysis(this.client, input);
  }

  async getAddressProposals(
    input: AddressProposalsInput
  ): Promise<AddressProposalsResponse> {
    if (!input.address) {
      throw new Error("address is required");
    }
    return getAddressProposals(this.client, input);
  }

  async getAddressDAOProposals(
    input: AddressDAOProposalsInput
  ): Promise<AddressDAOProposalsResponse> {
    if (!input.address) {
      throw new Error("Address is required");
    }
    const response = await getAddressDAOProposals(this.client, input);
    return {
      proposals: {
        nodes: response.proposals?.nodes || [],
        pageInfo: response.proposals?.pageInfo || {
          firstCursor: null,
          lastCursor: null,
        },
      },
    };
  }

  async getAddressVotes(
    input: AddressVotesInput
  ): Promise<AddressVotesResponse> {
    return getAddressVotes(this.client, input);
  }

  async getAddressCreatedProposals(
    input: AddressCreatedProposalsInput
  ): Promise<AddressCreatedProposalsResponse> {
    if (!input.address) {
      throw new Error("address is required");
    }
    const response = await getAddressCreatedProposals(this.client, input);
    return {
      proposals: {
        nodes: response.proposals?.nodes || [],
        pageInfo: response.proposals?.pageInfo || {
          firstCursor: null,
          lastCursor: null,
        },
      },
    };
  }

  async getAddressMetadata(
    input: AddressMetadataInput
  ): Promise<AddressMetadataResponse> {
    if (!input.address) {
      throw new Error("Address is required");
    }
    const response = await getAddressMetadata(this.client, input);
    return {
      address: response.address?.address || input.address,
      accounts: response.address?.accounts || [],
    };
  }

  async getAddressGovernances(
    input: AddressGovernancesInput
  ): Promise<Record<string, any>> {
    return await getAddressGovernances(this.client, input);
  }

  async getAddressReceivedDelegations(
    input: GetAddressReceivedDelegationsInput
  ): Promise<GetAddressReceivedDelegationsOutput> {
    if (!input.address) {
      throw new Error("address is required");
    }
    return getAddressReceivedDelegations(this.client, input);
  }

  async getDelegateStatement(
    input: GetDelegateStatementInput
  ): Promise<DelegateStatement | null> {
    const response = await getDelegateStatement(this.client, input);
    if (!response?.statement) return null;

    return {
      id: response.statement.id,
      address: response.statement.address,
      statement: response.statement.statement,
      statementSummary: response.statement.statementSummary || "",
      isSeekingDelegation: response.statement.isSeekingDelegation || false,
      issues: response.statement.issues || [],
    };
  }

  async getDelegators(params: GetDelegatorsParams): Promise<{
    delegators: Delegation[];
    pageInfo: PageInfo;
  }> {
    if (!params.address) {
      throw new Error("address is required");
    }
    return getDelegators(this.client, params);
  }

  async getProposalVotesCast(
    input: GetProposalVotesCastInput
  ): Promise<ProposalVotesCastResponse> {
    if (!input.id) {
      throw new Error("proposalId is required");
    }
    return getProposalVotesCast(this.client, input);
  }

  async getProposalVotesCastList(
    input: GetProposalVotesCastListInput
  ): Promise<ProposalVotesCastListResponse> {
    return getProposalVotesCastList(this.client, input);
  }

  async getGovernanceProposalsStats(input: {
    slug: string;
  }): Promise<GovernanceProposalsStatsResponse> {
    return getGovernanceProposalsStats(this.client, input);
  }

  /**
   * Format a vote amount considering token decimals
   * @param {string} votes - The raw vote amount
   * @param {TokenInfo} token - Optional token info containing decimals and symbol
   * @returns {string} Formatted vote amount with optional symbol
   */
  private static formatVotes(votes: string, token?: TokenInfo): string {
    const val = BigInt(votes);
    const decimals = token?.decimals ?? 18;
    const denominator = BigInt(10 ** decimals);
    const formatted = (Number(val) / Number(denominator)).toLocaleString();
    return `${formatted}${token?.symbol ? ` ${token.symbol}` : ""}`;
  }

  static formatDAOList(daos: Organization[]): string {
    return (
      `Found ${daos.length} DAOs:\n\n` +
      daos
        .map(
          (dao) =>
            `${dao.name} (${dao.slug})\n` +
            `Token Holders: ${dao.tokenOwnersCount}\n` +
            `Delegates: ${dao.delegatesCount}\n` +
            `Proposals: ${dao.proposalsCount}\n` +
            `Active Proposals: ${dao.hasActiveProposals ? "Yes" : "No"}\n` +
            `Description: ${
              dao.metadata?.description || "No description available"
            }\n` +
            `Website: ${dao.metadata?.socials?.website || "N/A"}\n` +
            `Twitter: ${dao.metadata?.socials?.twitter || "N/A"}\n` +
            `Discord: ${dao.metadata?.socials?.discord || "N/A"}\n` +
            "---"
        )
        .join("\n\n")
    );
  }

  static formatDAO(dao: Organization): string {
    return (
      `${dao.name} (${dao.slug})\n` +
      `Token Holders: ${dao.tokenOwnersCount}\n` +
      `Delegates: ${dao.delegatesCount}\n` +
      `Proposals: ${dao.proposalsCount}\n` +
      `Active Proposals: ${dao.hasActiveProposals ? "Yes" : "No"}\n` +
      `Description: ${
        dao.metadata?.description || "No description available"
      }\n` +
      `Website: ${dao.metadata?.socials?.website || "N/A"}\n` +
      `Twitter: ${dao.metadata?.socials?.twitter || "N/A"}\n` +
      `Discord: ${dao.metadata?.socials?.discord || "N/A"}\n` +
      `Chain IDs: ${dao.chainIds.join(", ")}\n` +
      `Token IDs: ${dao.tokenIds?.join(", ") || "N/A"}\n` +
      `Governor IDs: ${dao.governorIds?.join(", ") || "N/A"}`
    );
  }

  static formatDelegatesList(delegates: Delegate[]): string {
    return (
      `Found ${delegates.length} delegates:\n\n` +
      delegates
        .map(
          (delegate) =>
            `${delegate.account.name || delegate.account.address}\n` +
            `Address: ${delegate.account.address}\n` +
            `Votes: ${delegate.votesCount}\n` +
            `Delegators: ${delegate.delegatorsCount}\n` +
            `Bio: ${delegate.account.bio || "No bio available"}\n` +
            `Statement: ${
              delegate.statement?.statementSummary || "No statement available"
            }\n` +
            "---"
        )
        .join("\n\n")
    );
  }

  static formatDelegatorsList(delegators: Delegation[]): string {
    return (
      `Found ${delegators.length} delegators:\n\n` +
      delegators
        .map(
          (delegation) =>
            `${
              delegation.delegator.name ||
              delegation.delegator.ens ||
              delegation.delegator.address
            }\n` +
            `Address: ${delegation.delegator.address}\n` +
            `Votes: ${TallyService.formatVotes(
              delegation.votes,
              delegation.token
            )}\n` +
            `Delegated at: Block ${delegation.blockNumber} (${new Date(
              delegation.blockTimestamp
            ).toLocaleString()})\n` +
            `${
              delegation.token
                ? `Token: ${delegation.token.symbol} (${delegation.token.name})\n`
                : ""
            }` +
            "---"
        )
        .join("\n\n")
    );
  }

  static formatProposal(proposal: any): string {
    return `Proposal: ${proposal.metadata.title}
ID: ${proposal.id}
Status: ${proposal.status}
Created: ${new Date(proposal.createdAt).toLocaleString()}
Description: ${proposal.metadata.description}
Governor: ${proposal.governor.name}
Vote Stats:
${proposal.voteStats
  .map(
    (stat: any) =>
      `  ${stat.type}: ${stat.percent.toFixed(2)}% (${
        stat.votesCount
      } votes from ${stat.votersCount} voters)`
  )
  .join("\n")}`;
  }

  static formatProposalsList(proposals: any[]): string {
    return (
      `Found ${proposals.length} proposals:\n\n` +
      proposals
        .map(
          (proposal) =>
            `${proposal.metadata.title}\n` +
            `Tally ID: ${proposal.id}\n` +
            `Status: ${proposal.status}\n` +
            `Created: ${new Date(proposal.createdAt).toLocaleString()}\n\n`
        )
        .join("")
    );
  }
}

```

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

```typescript
import { type Tool } from "@modelcontextprotocol/sdk/types.js";
import { type TextContent } from "@modelcontextprotocol/sdk/types.js";
import { TallyService } from "./services/tally.service.js";

export const tools: Tool[] = [
  {
    name: "list-daos",
    description: "List DAOs on Tally sorted by specified criteria",
    inputSchema: {
      type: "object",
      properties: {
        limit: {
          type: "number",
          description:
            "Maximum number of DAOs to return (default: 20, max: 50)",
        },
        afterCursor: {
          type: "string",
          description: "Cursor for pagination",
        },
        sortBy: {
          type: "string",
          enum: ["id", "name", "explore", "popular"],
          description:
            "How to sort the DAOs (default: popular). 'explore' prioritizes DAOs with live proposals",
        },
      },
    },
  },
  {
    name: "get-dao",
    description: "Get detailed information about a specific DAO",
    inputSchema: {
      type: "object",
      required: ["slug"],
      properties: {
        slug: {
          type: "string",
          description: "The DAO's slug (e.g., 'uniswap' or 'aave')",
        },
      },
    },
  },
  {
    name: "list-delegates",
    description:
      "List delegates for a specific organization with their metadata",
    inputSchema: {
      type: "object",
      required: ["organizationSlug"],
      properties: {
        organizationSlug: {
          type: "string",
          description:
            "The organization's slug (e.g., 'arbitrum')",
        },
        limit: {
          type: "number",
          description:
            "Maximum number of delegates to return (default: 20, max: 50)",
        },
        afterCursor: {
          type: "string",
          description: "Cursor for pagination",
        },
        hasVotes: {
          type: "boolean",
          description: "Filter for delegates with votes",
        },
        hasDelegators: {
          type: "boolean",
          description: "Filter for delegates with delegators",
        },
        isSeekingDelegation: {
          type: "boolean",
          description: "Filter for delegates seeking delegation",
        },
      },
    },
  },
  {
    name: "get-delegators",
    description: "Get list of delegators for a specific address",
    inputSchema: {
      type: "object",
      required: ["address", "organizationSlug"],
      properties: {
        address: {
          type: "string",
          description: "The Ethereum address to get delegators for (0x format)",
        },
        organizationSlug: {
          type: "string",
          description:
            "Filter by organization slug (e.g., 'uniswap'). Alternative to organizationId",
        },
        governorId: {
          type: "string",
          description: "Filter by specific governor ID",
        },
        limit: {
          type: "number",
          description:
            "Maximum number of delegators to return (default: 20, max: 50)",
        },
        afterCursor: {
          type: "string",
          description: "Cursor for pagination",
        },
        beforeCursor: {
          type: "string",
          description: "Cursor for previous page pagination",
        },
        sortBy: {
          type: "string",
          enum: ["id", "votes"],
          description: "How to sort the delegators (default: id)",
        },
        isDescending: {
          type: "boolean",
          description: "Sort in descending order (default: true)",
        },
      },
    },
  },
  {
    name: "list-proposals",
    description: "List proposals for a specific DAO or organization using its slug",
    inputSchema: {
      type: "object",
      properties: {
        slug: {
          type: "string",
          description: "The slug of the DAO (e.g., 'uniswap')",
        },
        includeArchived: {
          type: "boolean",
          description: "Include archived proposals",
        },
        isDraft: {
          type: "boolean",
          description: "Filter for draft proposals",
        },
        limit: {
          type: "number",
          description: "Maximum number of proposals to return (default: 50, max: 50)",
        },
        afterCursor: {
          type: "string",
          description: "Cursor for pagination (string ID)",
        },
        beforeCursor: {
          type: "string",
          description: "Cursor for previous page pagination (string ID)",
        },
        isDescending: {
          type: "boolean",
          description: "Sort in descending order (default: true)",
        }
      },
      required: ["slug"]
    }
  },
  {
    name: "get-proposal",
    description:
      "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).",
    inputSchema: {
      type: "object",
      oneOf: [
        {
          required: ["id"],
          properties: {
            id: {
              type: "string",
              description:
                "The proposal's Tally ID (globally unique across all governors)",
            },
            includeArchived: {
              type: "boolean",
              description: "Include archived proposals",
            },
            isLatest: {
              type: "boolean",
              description: "Get the latest version of the proposal",
            },
          },
        },
        {
          required: ["onchainId", "governorId"],
          properties: {
            onchainId: {
              type: "string",
              description:
                "The proposal's onchain ID (only unique within a governor)",
            },
            governorId: {
              type: "string",
              description: "The governor's ID (required when using onchainId)",
            },
            includeArchived: {
              type: "boolean",
              description: "Include archived proposals",
            },
            isLatest: {
              type: "boolean",
              description: "Get the latest version of the proposal",
            },
          },
        },
      ],
    },
  },
  {
    name: "get-address-votes",
    description: "Get votes cast by an address for a specific organization",
    inputSchema: {
      type: "object",
      required: ["address", "organizationSlug"],
      properties: {
        address: {
          type: "string",
          description: "The address to get votes for",
        },
        organizationSlug: {
          type: "string",
          description: "The organization slug to get votes from",
        },
        limit: {
          type: "number",
          description: "Maximum number of votes to return (default: 20)",
        },
        afterCursor: {
          type: "string",
          description: "Cursor for pagination",
        },
      },
    },
  },
  {
    name: "get-address-created-proposals",
    description:
      "Get proposals created by an address for a specific organization",
    inputSchema: {
      type: "object",
      required: ["address", "organizationSlug"],
      properties: {
        address: {
          type: "string",
          description: "The Ethereum address to get created proposals for",
        },
        organizationSlug: {
          type: "string",
          description: "The organization slug to get proposals from",
        },
        limit: {
          type: "number",
          description: "Maximum number of proposals to return (default: 20)",
        },
        afterCursor: {
          type: "string",
          description: "Cursor for pagination",
        },
        beforeCursor: {
          type: "string",
          description: "Cursor for previous page pagination",
        },
      },
    },
    handler: async function (
      this: { service: TallyService },
      input: Record<string, unknown>
    ) {
      const { address, organizationSlug } = input;
      if (typeof address !== "string") {
        throw new Error("address must be a string");
      }
      if (typeof organizationSlug !== "string") {
        throw new Error("organizationSlug must be a string");
      }
      const result = await (this.service as any).getAddressCreatedProposals({
        address,
        organizationSlug,
        limit: typeof input.limit === "number" ? input.limit : undefined,
        afterCursor:
          typeof input.afterCursor === "string" ? input.afterCursor : undefined,
        beforeCursor:
          typeof input.beforeCursor === "string"
            ? input.beforeCursor
            : undefined,
      });
      return JSON.stringify(result);
    },
  },
  {
    name: "get-address-daos-proposals",
    description:
      "Returns proposals from DAOs where a given address has participated (voted, proposed, etc.)",
    inputSchema: {
      type: "object",
      required: ["address", "organizationSlug"],
      properties: {
        address: {
          type: "string",
          description: "The Ethereum address",
        },
        organizationSlug: {
          type: "string",
          description: "The organization slug to get proposals from",
        },
        limit: {
          type: "number",
          description:
            "Maximum number of proposals to return (default: 20, max: 50)",
        },
        afterCursor: {
          type: "string",
          description: "Cursor for pagination",
        },
      },
    },
  },
  {
    name: "get-address-received-delegations",
    description: "Returns delegations received by an address",
    inputSchema: {
      type: "object",
      required: ["address", "organizationSlug"],
      properties: {
        address: {
          type: "string",
          description:
            "The Ethereum address to get received delegations for (0x format)",
        },
        organizationSlug: {
          type: "string",
          description: "Filter by organization slug",
        },
        limit: {
          type: "number",
          description:
            "Maximum number of delegations to return (default: 20, max: 50)",
        },
        sortBy: {
          type: "string",
          enum: ["votes"],
          description: "Field to sort by",
        },
        isDescending: {
          type: "boolean",
          description: "Sort in descending order",
        },
      },
    },
  },
  {
    name: "get-delegate-statement",
    description:
      "Get a delegate's statement for a specific governor or organization",
    inputSchema: {
      type: "object",
      required: ["address"],
      oneOf: [
        {
          required: ["governorId"],
          properties: {
            address: {
              type: "string",
              description: "The delegate's Ethereum address",
            },
            governorId: {
              type: "string",
              description: "The governor's ID",
            },
          },
        },
        {
          required: ["organizationSlug"],
          properties: {
            address: {
              type: "string",
              description: "The delegate's Ethereum address",
            },
            organizationSlug: {
              type: "string",
              description: "The organization's slug (e.g., 'uniswap')",
            },
          },
        },
      ],
    },
  },
  {
    name: "get-address-governances",
    description:
      "Returns the list of governances (DAOs) an address has delegated to",
    inputSchema: {
      type: "object",
      required: ["address"],
      properties: {
        address: {
          type: "string",
          description:
            "The Ethereum address to get governances for (0x format)",
        },
      },
    },
  },
  {
    name: "get-proposal-timeline",
    description: "Get the timeline of events for a specific proposal",
    inputSchema: {
      type: "object",
      required: ["proposalId"],
      properties: {
        proposalId: {
          type: "string",
          description: "The ID of the proposal to get the timeline for",
        },
      },
    },
    handler: async function (
      this: { service: TallyService },
      input: Record<string, unknown>
    ) {
      if (typeof input.proposalId !== "string") {
        throw new Error("proposalId must be a string");
      }
      const result = await this.service.getProposalTimeline({
        proposalId: input.proposalId,
      });
      const content: TextContent[] = [
        {
          type: "text",
          text: JSON.stringify(result),
        },
      ];
      return { content };
    },
  },
  {
    name: "get-proposal-voters",
    description:
      "Get a list of all voters who have voted on a specific proposal",
    inputSchema: {
      type: "object",
      required: ["proposalId"],
      properties: {
        proposalId: {
          type: "string",
          description: "The ID of the proposal to get voters for",
        },
        limit: {
          type: "number",
          description: "Maximum number of voters to return (default: 20)",
        },
        afterCursor: {
          type: "string",
          description: "Cursor for pagination",
        },
        beforeCursor: {
          type: "string",
          description: "Cursor for previous page pagination",
        },
        sortBy: {
          type: "string",
          enum: ["id", "amount"],
          description: "How to sort the voters ('id' sorts by date (default), 'amount' sorts by voting power)",
        },
        isDescending: {
          type: "boolean",
          description: "Sort in descending order (true shows most recent/largest first)",
        },
      },
    },
  },
  {
    name: "get-address-metadata",
    description: "Get metadata information about a specific Ethereum address",
    inputSchema: {
      type: "object",
      required: ["address"],
      properties: {
        address: {
          type: "string",
          description: "The Ethereum address to get metadata for (0x format)",
        },
      },
    },
  },
  {
    name: "get-proposal-security-analysis",
    description:
      "Get security analysis for a specific proposal, including threat analysis and simulations",
    inputSchema: {
      type: "object",
      required: ["proposalId"],
      properties: {
        proposalId: {
          type: "string",
          description: "The ID of the proposal to get security analysis for",
        },
      },
    },
  },
  {
    name: "get-proposal-votes-cast",
    description:
      "Get vote statistics and formatted vote counts for a specific proposal",
    inputSchema: {
      type: "object",
      required: ["id"],
      properties: {
        id: {
          type: "string",
          description: "The proposal's ID",
        },
      },
    },
  },
  {
    name: "get-proposal-votes-cast-list",
    description:
      "Get a list of votes cast for a specific proposal, including formatted vote amounts",
    inputSchema: {
      type: "object",
      required: ["id"],
      properties: {
        id: {
          type: "string",
          description:
            "The proposal's Tally ID (globally unique across all governors)",
        },
      },
    },
  },
  {
    name: "get-governance-proposals-stats",
    description: "Get statistics about passed and failed proposals for a specific DAO",
    inputSchema: {
      type: "object",
      required: ["slug"],
      properties: {
        slug: {
          type: "string",
          description: "The DAO's slug (e.g., 'uniswap' or 'aave')",
        },
      },
    },
  },
];

```

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

```typescript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
  type Tool,
  type TextContent,
} from "@modelcontextprotocol/sdk/types.js";
import { TallyService } from "./services/tally.service.js";
import type { OrganizationsSortBy } from "./services/organizations/organizations.types.js";
import { tools } from "./tools.js";

export class TallyServer {
  private server: Server;
  private service: TallyService;

  constructor(apiKey: string) {
    // Initialize service
    this.service = new TallyService({ apiKey });

    // Create server instance
    this.server = new Server(
      {
        name: "tally-api",
        version: "1.0.0",
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );

    this.setupHandlers();
  }

  private setupHandlers() {
    // List available tools
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      return { tools };
    });

    // Handle tool execution
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args = {} } = request.params;

      if (name === "list-daos") {
        try {
          const data = await this.service.listDAOs({
            limit: typeof args.limit === "number" ? args.limit : undefined,
            afterCursor:
              typeof args.afterCursor === "string"
                ? args.afterCursor
                : undefined,
            sortBy:
              typeof args.sortBy === "string"
                ? (args.sortBy as OrganizationsSortBy)
                : undefined,
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(data, null, 2),
            },
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error fetching DAOs: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      if (name === "get-dao") {
        try {
          if (typeof args.slug !== "string") {
            throw new Error("slug must be a string");
          }

          const result = await this.service.getDAO(args.slug);

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2),
            },
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error getting DAO: ${error instanceof Error ? error.message : "Unknown error"}`
          );
        }
      }

      if (name === "list-delegates") {
        try {
          if (typeof args.organizationSlug !== "string") {
            throw new Error("organizationSlug must be a string");
          }

          const result = await this.service.listDelegates({
            organizationSlug: args.organizationSlug as string,
            limit: typeof args.limit === "number" ? args.limit : undefined,
            afterCursor: typeof args.afterCursor === "string" ? args.afterCursor : undefined,
            beforeCursor: typeof args.beforeCursor === "string" ? args.beforeCursor : undefined,
            hasVotes: typeof args.hasVotes === "boolean" ? args.hasVotes : undefined,
            hasDelegators: typeof args.hasDelegators === "boolean" ? args.hasDelegators : undefined,
            isSeekingDelegation: typeof args.isSeekingDelegation === "boolean" ? args.isSeekingDelegation : undefined
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2),
            },
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error listing delegates: ${error instanceof Error ? error.message : "Unknown error"}`
          );
        }
      }

      if (name === "get-delegators") {
        try {
          if (typeof args.address !== "string") {
            throw new Error("address must be a string");
          }

          const organizationId = typeof args.organizationId === "string" ? args.organizationId : undefined;
          const organizationSlug = typeof args.organizationSlug === "string" ? args.organizationSlug : undefined;
          const governorId = typeof args.governorId === "string" ? args.governorId : undefined;
          const limit = typeof args.limit === "number" ? args.limit : 20;
          const afterCursor = typeof args.afterCursor === "string" ? args.afterCursor : undefined;
          const beforeCursor = typeof args.beforeCursor === "string" ? args.beforeCursor : undefined;
          const sortBy = typeof args.sortBy === "string" && (args.sortBy === "votes" || args.sortBy === "id") ? args.sortBy : undefined;
          const isDescending = typeof args.isDescending === "boolean" ? args.isDescending : undefined;

          const result = await this.service.getDelegators({
            address: args.address,
            organizationId,
            organizationSlug,
            governorId,
            limit,
            afterCursor,
            beforeCursor,
            sortBy,
            isDescending,
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2),
            },
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error getting delegators: ${error instanceof Error ? error.message : "Unknown error"}`
          );
        }
      }

      if (name === "list-proposals") {
        try {
          if (typeof args.slug !== "string") {
            throw new Error("slug must be a string");
          }

          const data = await this.service.listProposals({
            slug: args.slug,
            includeArchived: typeof args.includeArchived === "boolean" ? args.includeArchived : undefined,
            isDraft: typeof args.isDraft === "boolean" ? args.isDraft : undefined,
            limit: typeof args.limit === "number" ? args.limit : undefined,
            afterCursor: typeof args.afterCursor === "string" ? args.afterCursor : undefined,
            beforeCursor: typeof args.beforeCursor === "string" ? args.beforeCursor : undefined,
            isDescending: typeof args.isDescending === "boolean" ? args.isDescending : undefined
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(data, null, 2),
            },
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error fetching proposals: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      if (name === "get-proposal") {
        try {
          const id = typeof args.id === "string" ? args.id : undefined;
          const onchainId = typeof args.onchainId === "string" ? args.onchainId : undefined;
          const governorId = typeof args.governorId === "string" ? args.governorId : undefined;
          const includeArchived = typeof args.includeArchived === "boolean" ? args.includeArchived : undefined;
          const isLatest = typeof args.isLatest === "boolean" ? args.isLatest : undefined;

          if (!id && (!onchainId || !governorId)) {
            throw new Error("Must provide either id or both onchainId and governorId");
          }

          const result = await this.service.getProposal({
            id,
            onchainId,
            governorId,
            includeArchived,
            isLatest,
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2),
            },
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error getting proposal: ${error instanceof Error ? error.message : "Unknown error"}`
          );
        }
      }

      if (name === "get-address-created-proposals") {
        try {
          if (typeof args.address !== "string") {
            throw new Error("address must be a string");
          }
          if (typeof args.organizationSlug !== "string") {
            throw new Error("organizationSlug must be a string");
          }

          const result = await (this.service as any).getAddressCreatedProposals({
            address: args.address,
            organizationSlug: args.organizationSlug,
            limit: typeof args.limit === "number" ? args.limit : undefined,
            afterCursor: typeof args.afterCursor === "string" ? args.afterCursor : undefined,
            beforeCursor: typeof args.beforeCursor === "string" ? args.beforeCursor : undefined
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2),
            },
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error fetching address created proposals: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      if (name === "get-address-daos-proposals") {
        try {
          if (typeof args.address !== "string") {
            throw new Error("address must be a string");
          }
          if (typeof args.organizationSlug !== "string") {
            throw new Error("organizationSlug must be a string");
          }

          const result = await this.service.getAddressDAOProposals({
            address: args.address,
            organizationSlug: args.organizationSlug,
            limit: typeof args.limit === "number" ? args.limit : undefined,
            afterCursor: typeof args.afterCursor === "string" ? args.afterCursor : undefined,
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2)
            }
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error fetching address DAO proposals: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      if (name === "get-address-votes") {
        try {
          // Validate types at API boundary
          if (typeof args.address !== "string") {
            throw new Error("address must be a string");
          }
          if (typeof args.organizationSlug !== "string") {
            throw new Error("organizationSlug must be a string");
          }

          const result = await this.service.getAddressVotes({
            address: args.address,
            organizationSlug: args.organizationSlug,
            limit: typeof args.limit === "number" ? args.limit : undefined,
            afterCursor: typeof args.afterCursor === "string" ? args.afterCursor : undefined,
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2),
            },
          ];

          return {
            content,
            pageInfo: {
              firstCursor: result.votes.pageInfo.firstCursor || null,
              lastCursor: result.votes.pageInfo.lastCursor || null,
            },
          };
        } catch (error) {
          throw new Error(
            `Error fetching address votes: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      if (name === "get-address-received-delegations") {
        try {
          if (typeof args.address !== "string") {
            throw new Error("address must be a string");
          }
          if (typeof args.organizationSlug !== "string") {
            throw new Error("organizationSlug must be a string");
          }

          const result = await this.service.getAddressReceivedDelegations({
            address: args.address,
            organizationSlug: args.organizationSlug,
            limit: typeof args.limit === "number" ? args.limit : undefined,
            sortBy: typeof args.sortBy === "string" ? (args.sortBy as "votes") : undefined,
            isDescending: typeof args.isDescending === "boolean" ? args.isDescending : undefined,
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2),
            },
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error fetching received delegations: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      if (name === "get-delegate-statement") {
        try {
          if (typeof args.address !== "string") {
            throw new Error("address must be a string");
          }

          // Check for mutually exclusive parameters
          if (typeof args.governorId === "string" && typeof args.organizationSlug === "string") {
            throw new Error("Cannot provide both governorId and organizationSlug");
          }

          let result;
          if (typeof args.governorId === "string") {
            result = await this.service.getDelegateStatement({
              address: args.address,
              governorId: args.governorId
            });
          } else if (typeof args.organizationSlug === "string") {
            result = await this.service.getDelegateStatement({
              address: args.address,
              organizationSlug: args.organizationSlug
            });
          } else {
            throw new Error("Either governorId or organizationSlug must be provided");
          }

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2),
            },
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error fetching delegate statement: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      if (name === "get-address-governances") {
        try {
          if (typeof args.address !== "string") {
            throw new Error("address must be a string");
          }

          const result = await this.service.getAddressGovernances({
            address: args.address,
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2),
            },
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error fetching address governances: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      if (name === "get-proposal-timeline") {
        try {
          if (typeof args.proposalId !== 'string') {
            throw new Error('proposalId must be a string');
          }
          const result = await this.service.getProposalTimeline({
            proposalId: args.proposalId
          });

          if (!result.proposal) {
            throw new Error('Proposal not found');
          }

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2)
            }
          ];
          return { content };
        } catch (error) {
          throw new Error(
            `Error fetching proposal timeline: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      if (name === "get-proposal-voters") {
        try {
          if (typeof args.proposalId !== "string") {
            throw new Error("proposalId must be a string");
          }

          const result = await this.service.getProposalVoters({
            proposalId: args.proposalId,
            limit: typeof args.limit === "number" ? args.limit : undefined,
            afterCursor: typeof args.afterCursor === "string" ? args.afterCursor : undefined,
            beforeCursor: typeof args.beforeCursor === "string" ? args.beforeCursor : undefined,
            sortBy: typeof args.sortBy === "string" ? args.sortBy as "votes" | "timestamp" : undefined,
            isDescending: typeof args.isDescending === "boolean" ? args.isDescending : undefined
          });

          if (!result?.votes?.nodes) {
            return {
              content: [],
              pageInfo: {
                firstCursor: null,
                lastCursor: null,
              },
            };
          }

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2)
            }
          ];

          return {
            content,
            pageInfo: {
              firstCursor: result.votes.pageInfo.firstCursor || null,
              lastCursor: result.votes.pageInfo.lastCursor || null,
            },
          };
        } catch (error) {
          throw new Error(
            `Error fetching proposal voters: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      if (name === "get-address-metadata") {
        const { address } = args as { address: string };
        const result = await this.service.getAddressMetadata({
          address,
        });
        return {
          content: [{
            type: "text",
            text: JSON.stringify(result, null, 2),
          }],
        };
      }

      if (name === "get-proposal-security-analysis") {
        try {
          if (typeof args.proposalId !== "string") {
            throw new Error("proposalId must be a string");
          }

          const result = await this.service.getProposalSecurityAnalysis({
            proposalId: args.proposalId
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2)
            }
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error fetching proposal security analysis: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      if (name === "get-proposal-votes-cast") {
        try {
          if (typeof args.id !== "string") {
            throw new Error("id must be a string");
          }

          const result = await this.service.getProposalVotesCast({
            id: args.id
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2)
            }
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error fetching proposal votes cast: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      if (name === "get-proposal-votes-cast-list") {
        try {
          if (typeof args.id !== "string") {
            throw new Error("id must be a string");
          }

          const result = await this.service.getProposalVotesCastList({
            id: args.id
          });

          const content: TextContent[] = [
            {
              type: "text",
              text: JSON.stringify(result, null, 2)
            }
          ];

          return { content };
        } catch (error) {
          throw new Error(
            `Error fetching proposal votes cast list: ${
              error instanceof Error ? error.message : "Unknown error"
            }`
          );
        }
      }

      throw new Error(`Unknown tool: ${name}`);
    });
  }

  async start() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error("Tally MCP Server running on stdio");
  }
}

```

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

```


Tally API - LLM Query Construction Rules (Mandatory & Unbreakable)

Introduction

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.

Core Principles

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.

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.

Fragment Prioritization: You must use fragments to minimize repetition, improve maintainability, and ensure efficient queries. Not using fragments is unacceptable.

Explicit Field Selection: You must always explicitly request each field you need, and never assume fields will be returned automatically.

Pagination: You must always use pagination where appropriate to ensure complete query results are retrieved, using the page input and pageInfo fields.

Correct API Use: You must adhere to API constraints. Some queries have required fields that must be used correctly.

Schema Consultation: You must consult the complete schema reference before creating any queries.

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.

Fragment Usage: All Fragments must be used, and any unused fragments must be removed.

Rule 1: Interface and Union Type Handling (Mandatory)

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.

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.

Example (Correct):

query GetOrganizations {
  organizations {
    nodes {
      ... on Organization { # Correct: Uses inline fragment
        id
        name
        slug
        metadata {
          icon
        }
      }
    }
    pageInfo {
      firstCursor
      lastCursor
      count
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetOrganizations {
  organizations {
    nodes {
      id      # Incorrect: querying on the interface directly
      name   # Incorrect: querying on the interface directly
      slug    # Incorrect: querying on the interface directly
    }
  }
}
content_copy
download
Use code with caution.
Graphql

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.

query GetVotes {
  votes(input: {
    filters: {
      voter: "0x1B686eE8E31c5959D9F5BBd8122a58682788eeaD"
    }
  }) {
    nodes {
      type  # Error: Didn't use ... on Vote
    }
  }
}
content_copy
download
Use code with caution.
Graphql

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.

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.

Rule 2: Explicit Sort and Filter Inputs (Mandatory)

Problem: Queries with sort or filter options often have required input types that must be fully populated.

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.

Example (Correct):

query GetProposals($input: ProposalsInput!) {
  proposals(input: $input) {
    nodes {
        ... on Proposal {
            id
            metadata {
                title
            }
            status
        }
    }
    pageInfo {
      firstCursor
      lastCursor
      count
    }
  }
}
content_copy
download
Use code with caution.
Graphql
*  **Input:**
content_copy
download
Use code with caution.
input ProposalsInput {
    filters: ProposalsFiltersInput
    page: PageInput
    sort: ProposalsSortInput
}

input ProposalsFiltersInput {
    governorId: AccountID
    includeArchived: Boolean
    isDraft: Boolean
    organizationId: IntID
    proposer: Address
}
input ProposalsSortInput {
    isDescending: Boolean!
    sortBy: ProposalsSortBy!
}
enum ProposalsSortBy {
    id
}

input PageInput {
    afterCursor: String
    beforeCursor: String
    limit: Int
}
content_copy
download
Use code with caution.
Graphql
*   **Query:** (with optional sort, and filters)
content_copy
download
Use code with caution.
query GetProposalsWithSortAndFilter {
  proposals(input: {
      filters: {
          governorId: "eip155:1:0x123abc"
          includeArchived: true
      },
      sort: {
        sortBy: id
        isDescending: false
      },
      page: {
        limit: 10
      }
  })
    {
    nodes {
        ... on Proposal {
            id
            metadata {
                title
            }
            status
        }
    }
    pageInfo {
      firstCursor
      lastCursor
       count
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetProposals {
  proposals {
    nodes {
      id
      metadata {
        title
      }
      status
    }
  }
}
content_copy
download
Use code with caution.
Graphql

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.

Rule 3: Fragment Usage (Mandatory)

Problem: Repeated field selections in multiple queries make the code less maintainable and are prone to errors.

Solution: You must use fragments to group common field selections and reuse them across multiple queries. Not using fragments is unacceptable.

Example (Correct):

fragment BasicProposalDetails on Proposal {
    id
    onchainId
    metadata {
      title
      description
    }
   status
}


query GetProposals($input: ProposalsInput!) {
  proposals(input: $input) {
    nodes {
        ... on Proposal {
            ...BasicProposalDetails
         }
    }
    pageInfo {
      firstCursor
      lastCursor
       count
    }
  }
}

query GetSingleProposal($input: ProposalInput!) {
    proposal(input: $input) {
      ...BasicProposalDetails
    }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetProposals {
  proposals {
    nodes {
       id
       onchainId
       metadata {
        title
        description
       }
       status
    }
  }
}

query GetSingleProposal {
    proposal(input: {id: 123}) {
      id
      onchainId
      metadata {
        title
        description
      }
      status
    }
}
content_copy
download
Use code with caution.
Graphql

Action: Always create and use fragments, and make them focused, and reusable across multiple queries. Not using fragments is unacceptable.

Rule 4: Explicit Field Selection (Mandatory)

Problem: Assuming the API will return certain fields if they aren't specifically requested.

Solution: You must always request every field you need in your query.

Example (Correct):

query GetOrganization($input: OrganizationInput!) {
  organization(input: $input) {
    id
    name
    slug
    metadata {
      icon
      description
      socials {
        website
      }
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetOrganization {
  organization { # Incorrect: Assuming all fields are returned by default
    name
    slug
  }
}
content_copy
download
Use code with caution.
Graphql

Action: List out every field you need in the query, and avoid implied or implicit field selections.

Rule 5: Input Type Validation (Mandatory)

Problem: Using the wrong types when providing input values to a query.

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.

Example (Correct):

query GetAccount($id: AccountID!) {
  account(id: $id) {
    id
    name
    address
    ens
    picture
  }
}
content_copy
download
Use code with caution.
Graphql

Query

query GetAccountCorrect {
   account(id:"eip155:1:0x123") {
    id
     name
     address
     ens
     picture
   }
}
content_copy
download
Use code with caution.
Graphql
* The `id` argument correctly uses the `AccountID` type, which is a string representing a CAIP-10 ID.
content_copy
download
Use code with caution.

Specific Error Case: Attempting to use a plain integer for organizationId in proposal queries. This is a fatal error.

query GetProposals {
  proposals(input: {
    filters: {
      organizationId: 1  # Wrong format for ID
    }
  })
  {
    nodes {
      ... on Proposal {
          id
         }
      }
   }
}
content_copy
download
Use code with caution.
Graphql
*   **Prevention:** This error is caused by not following rule 5, and also the ID type definitions.
content_copy
download
Use code with caution.

Example (Incorrect - Avoid):

query GetAccount($id: AccountID!) {
  account(id: $id) {
    id
    name
    address
  }
}
content_copy
download
Use code with caution.
Graphql

Query

query GetAccountIncorrect {
   account(id:123) { # Incorrect: Using an Int when an AccountID is expected.
    id
     name
     address
     ens
     picture
   }
}
content_copy
download
Use code with caution.
Graphql

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.

ID Type Definitions

AccountID: A CAIP-10 compliant account id. (e.g., "eip155:1:0x7e90e03654732abedf89Faf87f05BcD03ACEeFdc")

AssetID: A CAIP-19 compliant asset id. (e.g., "eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f")

IntID: A 64-bit integer represented as a string. (e.g., "1234567890")

HashID: A CAIP-2 scoped identifier for identifying transactions across chains. (e.g., "eip155:1:0xDEAD")

BlockID: A CAIP-2 scoped identifier for identifying blocks across chains. (e.g., "eip155:1:15672")

ChainID: A CAIP-2 compliant chain ID. (e.g., "eip155:1")

Address: A 20 byte ethereum address, represented as 0x-prefixed hexadecimal. (e.g., "0x1234567800000000000000000000000000000abc")

Rule 6: Enum Usage (Mandatory)

Problem: Using a string value when an enum type is expected.

Solution: Always use the correct values for an enum type. Failure to do so is a fatal error.

Example (Correct)

query GetVotes($input: VotesInput!) {
  votes(input: $input) {
    nodes {
      id
       type
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Input:

input VotesInput {
    filters: VotesFiltersInput
    page: PageInput
    sort: VotesSortInput
}

input VotesFiltersInput {
    proposalId: IntID
    proposalIds: [IntID!]
    voter: Address
    includePendingVotes: Boolean
    type: VoteType
}
enum VoteType {
    abstain
    against
    for
    pendingabstain
    pendingagainst
    pendingfor
}
content_copy
download
Use code with caution.
Graphql

Query: (Correctly using an enum type)

query GetVotesFor {
    votes(input: {
        filters: {
            type: for
             proposalId: 123
        }
    })
    {
      nodes {
        id
          type
      }
    }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetVotesFor {
    votes(input: {
        filters: {
            type: "for" # Incorrect: using a string, when a VoteType enum is expected
             proposalId: 123
        }
    })
    {
      nodes {
        id
          type
      }
    }
}
content_copy
download
Use code with caution.
Graphql

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.

Rule 7: Pagination Handling (Mandatory)

Problem: Queries that return paginated data do not return complete results if pagination is not handled.

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.

Example (Correct):

query GetPaginatedProposals($input: ProposalsInput!) {
  proposals(input: $input) {
    nodes {
        ... on Proposal {
            id
            metadata {
                title
            }
        }
    }
   pageInfo {
       firstCursor
       lastCursor
       count
     }
  }
}
content_copy
download
Use code with caution.
Graphql
* **Input**
content_copy
download
Use code with caution.
input ProposalsInput {
    filters: ProposalsFiltersInput
    page: PageInput
    sort: ProposalsSortInput
}

input ProposalsFiltersInput {
    governorId: AccountID
    includeArchived: Boolean
    isDraft: Boolean
    organizationId: IntID
    proposer: Address
}
input ProposalsSortInput {
    isDescending: Boolean!
    sortBy: ProposalsSortBy!
}
enum ProposalsSortBy {
    id
}

input PageInput {
    afterCursor: String
    beforeCursor: String
    limit: Int
}
content_copy
download
Use code with caution.
Graphql

Query:

query GetProposalsWithPagination {
    proposals(input: {
       page: {
           limit: 20
       }
    }) {
        nodes {
            ... on Proposal {
              id
               metadata {
                title
              }
           }
         }
         pageInfo {
           firstCursor
            lastCursor
           count
          }
        }
}
content_copy
download
Use code with caution.
Graphql

Query: (Using cursors to get the next page of results)

query GetProposalsWithPagination {
    proposals(input: {
       page: {
           limit: 20
           afterCursor: "cursorFromPreviousQuery"
       }
    }) {
        nodes {
          ... on Proposal {
            id
            metadata {
                title
              }
           }
        }
         pageInfo {
           firstCursor
            lastCursor
           count
          }
        }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetProposals { # Incorrect: Not using the `page` input.
  proposals {
      nodes {
         ... on Proposal {
          id
            metadata {
                title
            }
           }
       }
    }
}
content_copy
download
Use code with caution.
Graphql

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.

Rule 8: Correctly Querying Related Data (Mandatory)

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.

Solution: You must fetch related data by using separate queries, instead of assuming that related data is queryable as nested fields.

Example (Correct)

query GetProposalAndVotes($proposalId: IntID!, $voter: Address) {
  proposal(input: { id: $proposalId}) {
    id
    metadata {
      title
    }
    status
  }
  votes(input: {
    filters: {
      proposalId: $proposalId
      voter: $voter
    }
  }) {
    nodes {
      ... on Vote {
        type
        amount
        voter {
          id
          name
        }
      }
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetProposals {
  proposals {
    ... on Proposal {
      id
      metadata {
        title
      }
      votes(input: {
        filters: {
          voter: "0x..." # Incorrect: Trying to access votes as a nested field
        }
      })
    }
  }
}
content_copy
download
Use code with caution.
Graphql
* **Prevention:** This can be prevented by reading rule 8, and by consulting the schema before creating a query.
content_copy
download
Use code with caution.

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.

Rule 9: API Constraints (Mandatory)

Problem: Not all fields or properties are queryable in all situations. Some queries have explicit requirements that must be met.

Solution: You must always check your query against the known API constraints, and ensure that all requirements are met.

Example:

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.

An error you may see is: "proposalId or proposalIds must be provided"

Prevention: This can be prevented by reading rule 9, and by consulting the schema before creating a query.

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.

Rule 10: Multi-Step Queries (Mandatory)

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.

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.

Example

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.

Correct Example

# Step 1: Get the organization ID using a query that filters by slug

query GetOrganizationId($slug: String!) {
  organization(input: {slug: $slug}) {
    id
  }
}

# Step 2: Get the proposals for the given organization
query GetProposalsForOrganization($organizationId: IntID!) {
    proposals(input: {
      filters: {
        organizationId: $organizationId
      }
    }) {
        nodes {
            ... on Proposal {
                id
            }
        }
    }
}

# Step 3: Get all the votes for all of the proposals.
query GetVotesForProposals($proposalIds: [IntID!]!) {
    votes(input: {
      filters: {
         proposalIds: $proposalIds
      }
    })
        {
        nodes {
            ... on Vote {
                id
                type
                amount
            }
        }
    }
}
content_copy
download
Use code with caution.
Graphql
*   **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.
content_copy
download
Use code with caution.

Rule 11: Fragment Usage (Mandatory)

Problem: Defining fragments that aren't used creates unnecessary code.

Solution: You must always use all defined fragments, and any unused fragments must be removed before submitting a query.

Example

fragment BasicAccountInfo on Account {
  id
  address
  ens
}

fragment VoteDetails on Vote {
    type
    amount
}

query GetVotes($input: VotesInput!) {
  votes(input: $input) {
    nodes {
      ... on Vote {
        ...VoteDetails # Correct: Using the VoteDetails fragment
      }
    }
  }
}
content_copy
download
Use code with caution.
Graphql
*   **Prevention:** This can be prevented by following rule 3.
*   **Action:** All defined fragments *must* be used, and any unused fragments *must* be removed before submitting a query.
content_copy
download
Use code with caution.

Complete Schema Reference

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:

type Account {
  id: ID!
  address: String!
  ens: String
  twitter: String
  name: String!
  bio: String!
  picture: String
  safes: [AccountID!]
  type: AccountType!
  votes(governorId: AccountID!): Uint256!
  proposalsCreatedCount(input: ProposalsCreatedCountInput!): Int!
}

enum AccountType {
    EOA
    SAFE
}
type Delegate {
    id: IntID!
    account: Account!
    chainId: ChainID
    delegatorsCount: Int!
    governor: Governor
    organization: Organization
    statement: DelegateStatement
    token: Token
    votesCount(blockNumber: Int): Uint256!
  }

  input DelegateInput {
    address: Address!
    governorId: AccountID
    organizationId: IntID
  }

type DelegateStatement {
    id: IntID!
    address: Address!
    organizationID: IntID!
    statement: String!
    statementSummary: String
    isSeekingDelegation: Boolean
    issues: [Issue!]
  }

type Delegation {
    id: IntID!
    blockNumber: Int!
    blockTimestamp: Timestamp!
    chainId: ChainID!
    delegator: Account!
    delegate: Account!
    organization: Organization!
    token: Token!
    votes: Uint256!
}
input DelegationInput {
    address: Address!
    tokenId: AssetID!
  }
input DelegationsInput {
  filters: DelegationsFiltersInput!
  page: PageInput
  sort: DelegationsSortInput
}
input DelegationsFiltersInput {
    address: Address!
    governorId: AccountID
    organizationId: IntID
}
input DelegationsSortInput {
    isDescending: Boolean!
    sortBy: DelegationsSortBy!
}
enum DelegationsSortBy {
  id
  votes
}

type Governor {
    id: AccountID!
    chainId: ChainID!
    contracts: Contracts!
    isIndexing: Boolean!
    isBehind: Boolean!
    isPrimary: Boolean!
    kind: GovernorKind!
    name: String!
    organization: Organization!
    proposalStats: ProposalStats!
    parameters: GovernorParameters!
    quorum: Uint256!
    slug: String!
    timelockId: AccountID
    tokenId: AssetID!
    token: Token!
    type: GovernorType!
    delegatesCount: Int!
    delegatesVotesCount: Uint256!
    tokenOwnersCount: Int!
    metadata: GovernorMetadata
  }
  type GovernorContract {
    address: Address!
    type: GovernorType!
  }

  input GovernorInput {
    id: AccountID
    slug: String
  }

type Organization {
    id: IntID!
    slug: String!
    name: String!
    chainIds: [ChainID!]!
    tokenIds: [AssetID!]!
    governorIds: [AccountID!]!
    metadata: OrganizationMetadata
    creator: Account
    hasActiveProposals: Boolean!
    proposalsCount: Int!
    delegatesCount: Int!
    delegatesVotesCount: Uint256!
    tokenOwnersCount: Int!
    endorsementService: EndorsementService
}
input OrganizationInput {
  id: IntID
  slug: String
}
input OrganizationsInput {
  filters: OrganizationsFiltersInput
  page: PageInput
  sort: OrganizationsSortInput
}
input OrganizationsFiltersInput {
  address: Address
  chainId: ChainID
  hasLogo: Boolean
    isMember: Boolean
}
input OrganizationsSortInput {
    isDescending: Boolean!
    sortBy: OrganizationsSortBy!
}

enum OrganizationsSortBy {
  id
  name
  explore
  popular
}

type Proposal {
    id: IntID!
    onchainId: String
    block: Block
    chainId: ChainID!
    creator: Account
    end: BlockOrTimestamp!
    events: [ProposalEvent!]!
    executableCalls: [ExecutableCall!]
    governor: Governor!
    metadata: ProposalMetadata!
    organization: Organization!
    proposer: Account
    quorum: Uint256
    status: ProposalStatus!
    start: BlockOrTimestamp!
    voteStats: [VoteStats!]
}
input ProposalInput {
  id: IntID
  onchainId: String
  governorId: AccountID
    includeArchived: Boolean
    isLatest: Boolean
}
type ProposalMetadata {
  title: String
  description: String
  eta: Int
  ipfsHash: String
      previousEnd: Int
      timelockId: AccountID
      txHash: Hash
      discourseURL: String
  snapshotURL: String
}

input ProposalsInput {
  filters: ProposalsFiltersInput
  page: PageInput
  sort: ProposalsSortInput
}
input ProposalsFiltersInput {
  governorId: AccountID
  includeArchived: Boolean
    isDraft: Boolean
  organizationId: IntID
  proposer: Address
}
input ProposalsSortInput {
  isDescending: Boolean!
  sortBy: ProposalsSortBy!
}
enum ProposalsSortBy {
  id
}

type Token {
    id: AssetID!
    type: TokenType!
    name: String!
    symbol: String!
    supply: Uint256!
    decimals: Int!
    eligibility: Eligibility
    isIndexing: Boolean!
    isBehind: Boolean!
}
type Vote {
    id: IntID!
    amount: Uint256!
    block: Block!
    chainId: ChainID!
    isBridged: Boolean
    proposal: Proposal!
    reason: String
    type: VoteType!
    txHash: Hash!
    voter: Account!
}
input VotesInput {
    filters: VotesFiltersInput
    page: PageInput
    sort: VotesSortInput
  }
input VotesFiltersInput {
    proposalId: IntID
    proposalIds: [IntID!]
    voter: Address
    includePendingVotes: Boolean
    type: VoteType
}
input VotesSortInput {
    isDescending: Boolean!
    sortBy: VotesSortBy!
}
enum VotesSortBy {
  id
  amount
}

enum VoteType {
    abstain
    against
    for
    pendingabstain
    pendingagainst
    pendingfor
}
content_copy
download
Use code with caution.
Graphql

Best Practices Checklist (Pre-Flight Check)

Before submitting any query, ensure you have:

Interface Fragments: Used inline fragments (... on TypeName) wherever nodes or interface/union types are returned. Failure to do so is a fatal error.

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.

Fragments: Created and used fragments for reusable field selections. Not using fragments is unacceptable.

Explicit Fields: Explicitly requested all the necessary fields. Failure to do so may result in errors.

Type Validation: Verified that all input types match the expected type. Failure to do so is a fatal error.

Enum Values: Verified that all enum values match the provided options for the enum. Failure to do so is a fatal error.

Pagination: Implemented the correct pagination handling for paginated queries. Failure to do so may result in incomplete data.

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.

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.

Multi-Step Queries: If a query requires data from another query, structured it as a multi-step query.

Fragment Usage: All defined fragments must be used, and any unused fragments must be removed.

LLM Instructions:

Prioritize this Document: This document supersedes all other query building documentation. This should be your only reference.

Strict Adherence: Any query generated must adhere to all of these rules. There is no acceptable deviation.

Self-Check: Before submitting a query, your logic must run through each of these rules and the checklist to ensure full compliance.

Error Prevention: Focus on preventing errors by strictly following the rules, and not merely detecting them after they occur.

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.

Consult Schema: You must always consult the complete schema reference before creating any query. Failure to do so is a fatal error.


```

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

```

Tally API - LLM Query Construction Rules (Mandatory & Unbreakable)

Introduction

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 a fatal error, and the LLM must immediately terminate the current query and return an error message indicating which rule was broken. There is no acceptable deviation whatsoever from these rules.

Core Principles

Never Assume: Under no circumstances should the LLM assume any default values or behaviors for sort, filter, or other optional input parameters. You must explicitly declare them in the query.

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 and will result in immediate termination of the query.

Fragment Prioritization: You must use fragments to minimize repetition, improve maintainability, and ensure efficient queries. Not using fragments is absolutely unacceptable and will result in a fatal error.

Explicit Field Selection: You must always explicitly request each field you need, and never assume fields will be returned automatically.

Pagination: You must always use pagination where appropriate to ensure complete query results are retrieved, using the page input and pageInfo fields.

Correct API Use: You must adhere to API constraints. Some queries have required fields that must be used correctly.

Schema Consultation: You must consult the complete schema reference before creating any queries.

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.

Fragment Usage: All Fragments must be used, and any unused fragments must be removed before the query can be submitted.

Data Verification: You must not invent data. If you use external data to construct a query, you must attempt to verify the correctness of that data before using it. If you cannot verify the data, you must explicitly state that the data is unverified, and not present it as a fact. Failure to do so is a fatal error.

Rule 1: Interface and Union Type Handling (Mandatory)

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.

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 and will result in immediate termination of the query.

Example (Correct):

query GetOrganizations {
  organizations {
    nodes {
      ... on Organization {
        id
        name
        slug
        metadata {
          icon
        }
      }
    }
    pageInfo {
      firstCursor
      lastCursor
      count
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetOrganizations {
  organizations {
    nodes {
      id
      name
      slug
    }
  }
}
content_copy
download
Use code with caution.
Graphql

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 and will result in immediate termination of the query.

query GetVotes {
  votes(input: {
    filters: {
      voter: "0x1B686eE8E31c5959D9F5BBd8122a58682788eeaD"
    }
  }) {
    nodes {
      type
    }
  }
}
content_copy
download
Use code with caution.
Graphql

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 and will result in immediate termination of the query.

Rule 2: Explicit Sort and Filter Inputs (Mandatory)

Problem: Queries with sort or filter options often have required input types that must be fully populated.

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.

Example (Correct):

query GetProposals($input: ProposalsInput!) {
  proposals(input: $input) {
    nodes {
        ... on Proposal {
            id
            metadata {
                title
            }
            status
        }
    }
    pageInfo {
      firstCursor
      lastCursor
      count
    }
  }
}
content_copy
download
Use code with caution.
Graphql
*  **Input:**
content_copy
download
Use code with caution.
input ProposalsInput {
    filters: ProposalsFiltersInput
    page: PageInput
    sort: ProposalsSortInput
}

input ProposalsFiltersInput {
    governorId: AccountID
    includeArchived: Boolean
    isDraft: Boolean
    organizationId: IntID
    proposer: Address
}
input ProposalsSortInput {
    isDescending: Boolean!
    sortBy: ProposalsSortBy!
}
enum ProposalsSortBy {
    id
}

input PageInput {
    afterCursor: String
    beforeCursor: String
    limit: Int
}
content_copy
download
Use code with caution.
Graphql
*   **Query:** (with optional sort, and filters)
content_copy
download
Use code with caution.
query GetProposalsWithSortAndFilter {
  proposals(input: {
      filters: {
          governorId: "eip155:1:0x123abc"
          includeArchived: true
      },
      sort: {
        sortBy: id
        isDescending: false
      },
      page: {
        limit: 10
      }
  })
    {
    nodes {
        ... on Proposal {
            id
            metadata {
                title
            }
            status
        }
    }
    pageInfo {
      firstCursor
      lastCursor
       count
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetProposals {
  proposals {
    nodes {
      id
      metadata {
        title
      }
      status
    }
  }
}
content_copy
download
Use code with caution.
Graphql

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 and will result in immediate termination of the query.

Rule 3: Fragment Usage (Mandatory)

Problem: Repeated field selections in multiple queries make the code less maintainable and are prone to errors.

Solution: You must use fragments to group common field selections and reuse them across multiple queries. Not using fragments is absolutely unacceptable and will result in a fatal error.

Example (Correct):

fragment BasicProposalDetails on Proposal {
    id
    onchainId
    metadata {
      title
      description
    }
   status
}


query GetProposals($input: ProposalsInput!) {
  proposals(input: $input) {
    nodes {
        ... on Proposal {
            ...BasicProposalDetails
         }
    }
    pageInfo {
      firstCursor
      lastCursor
       count
    }
  }
}

query GetSingleProposal($input: ProposalInput!) {
    proposal(input: $input) {
      ...BasicProposalDetails
    }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetProposals {
  proposals {
    nodes {
       id
       onchainId
       metadata {
        title
        description
       }
       status
    }
  }
}

query GetSingleProposal {
    proposal(input: {id: 123}) {
      id
      onchainId
      metadata {
        title
        description
      }
      status
    }
}
content_copy
download
Use code with caution.
Graphql

Action: Always create and use fragments, and make them focused, and reusable across multiple queries. Not using fragments is absolutely unacceptable and will result in a fatal error.

Rule 4: Explicit Field Selection (Mandatory)

Problem: Assuming the API will return certain fields if they aren't specifically requested.

Solution: You must always request every field you need in your query.

Example (Correct):

query GetOrganization($input: OrganizationInput!) {
  organization(input: $input) {
    id
    name
    slug
    metadata {
      icon
      description
      socials {
        website
      }
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetOrganization {
  organization {
    name
    slug
  }
}
content_copy
download
Use code with caution.
Graphql

Action: List out every field you need in the query, and avoid implied or implicit field selections.

Rule 5: Input Type Validation (Mandatory)

Problem: Using the wrong types when providing input values to a query.

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 and will result in immediate termination of the query.

Example (Correct):

query GetAccount($id: AccountID!) {
  account(id: $id) {
    id
    name
    address
    ens
    picture
  }
}
content_copy
download
Use code with caution.
Graphql

Query

query GetAccountCorrect {
   account(id:"eip155:1:0x123") {
    id
     name
     address
     ens
     picture
   }
}
content_copy
download
Use code with caution.
Graphql
* The `id` argument correctly uses the `AccountID` type, which is a string representing a CAIP-10 ID.
content_copy
download
Use code with caution.

Specific Error Case: Attempting to use a plain integer for organizationId in proposal queries. This is a fatal error and will result in immediate termination of the query.

query GetProposals {
  proposals(input: {
    filters: {
      organizationId: 1
    }
  })
  {
    nodes {
      ... on Proposal {
          id
         }
      }
   }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetAccount($id: AccountID!) {
  account(id: $id) {
    id
    name
    address
  }
}
content_copy
download
Use code with caution.
Graphql

Query

query GetAccountIncorrect {
   account(id:123) {
    id
     name
     address
     ens
     picture
   }
}
content_copy
download
Use code with caution.
Graphql

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 and will result in immediate termination of the query.

ID Type Definitions

AccountID: A CAIP-10 compliant account id. (e.g., "eip155:1:0x7e90e03654732abedf89Faf87f05BcD03ACEeFdc")

AssetID: A CAIP-19 compliant asset id. (e.g., "eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f")

IntID: A 64-bit integer represented as a string. (e.g., "1234567890")

HashID: A CAIP-2 scoped identifier for identifying transactions across chains. (e.g., "eip155:1:0xDEAD")

BlockID: A CAIP-2 scoped identifier for identifying blocks across chains. (e.g., "eip155:1:15672")

ChainID: A CAIP-2 compliant chain ID. (e.g., "eip155:1")

Address: A 20 byte ethereum address, represented as 0x-prefixed hexadecimal. (e.g., "0x1234567800000000000000000000000000000abc")

Rule 6: Enum Usage (Mandatory)

Problem: Using a string value when an enum type is expected.

Solution: Always use the correct values for an enum type. Failure to do so is a fatal error and will result in immediate termination of the query.

Example (Correct)

query GetVotes($input: VotesInput!) {
  votes(input: $input) {
    nodes {
      id
       type
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Input:

input VotesInput {
    filters: VotesFiltersInput
    page: PageInput
    sort: VotesSortInput
}

input VotesFiltersInput {
    proposalId: IntID
    proposalIds: [IntID!]
    voter: Address
    includePendingVotes: Boolean
    type: VoteType
}
enum VoteType {
    abstain
    against
    for
    pendingabstain
    pendingagainst
    pendingfor
}
content_copy
download
Use code with caution.
Graphql

Query: (Correctly using an enum type)

query GetVotesFor {
    votes(input: {
        filters: {
            type: for
             proposalId: 123
        }
    })
    {
      nodes {
        id
          type
      }
    }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetVotesFor {
    votes(input: {
        filters: {
            type: "for"
             proposalId: 123
        }
    })
    {
      nodes {
        id
          type
      }
    }
}
content_copy
download
Use code with caution.
Graphql

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 and will result in immediate termination of the query.

Rule 7: Pagination Handling (Mandatory)

Problem: Queries that return paginated data do not return complete results if pagination is not handled.

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.

Example (Correct):

query GetPaginatedProposals($input: ProposalsInput!) {
  proposals(input: $input) {
    nodes {
        ... on Proposal {
            id
            metadata {
                title
            }
        }
    }
   pageInfo {
       firstCursor
       lastCursor
       count
     }
  }
}
content_copy
download
Use code with caution.
Graphql
* **Input**
content_copy
download
Use code with caution.
input ProposalsInput {
    filters: ProposalsFiltersInput
    page: PageInput
    sort: ProposalsSortInput
}

input ProposalsFiltersInput {
    governorId: AccountID
    includeArchived: Boolean
    isDraft: Boolean
    organizationId: IntID
    proposer: Address
}
input ProposalsSortInput {
    isDescending: Boolean!
    sortBy: ProposalsSortBy!
}
enum ProposalsSortBy {
    id
}

input PageInput {
    afterCursor: String
    beforeCursor: String
    limit: Int
}
content_copy
download
Use code with caution.
Graphql

Query:

query GetProposalsWithPagination {
    proposals(input: {
       page: {
           limit: 20
       }
    }) {
        nodes {
            ... on Proposal {
              id
               metadata {
                title
              }
           }
         }
         pageInfo {
           firstCursor
            lastCursor
           count
          }
        }
}
content_copy
download
Use code with caution.
Graphql

Query: (Using cursors to get the next page of results)

query GetProposalsWithPagination {
    proposals(input: {
       page: {
           limit: 20
           afterCursor: "cursorFromPreviousQuery"
       }
    }) {
        nodes {
          ... on Proposal {
            id
            metadata {
                title
              }
           }
        }
         pageInfo {
           firstCursor
            lastCursor
           count
          }
        }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetProposals {
  proposals {
      nodes {
         ... on Proposal {
          id
            metadata {
                title
            }
           }
       }
    }
}
content_copy
download
Use code with caution.
Graphql

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.

Rule 8: Correctly Querying Related Data (Mandatory)

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.

Solution: You must fetch related data by using separate queries, instead of assuming that related data is queryable as nested fields.

Example (Correct)

query GetProposalAndVotes($proposalId: IntID!, $voter: Address) {
  proposal(input: { id: $proposalId}) {
    id
    metadata {
      title
    }
    status
  }
  votes(input: {
    filters: {
      proposalId: $proposalId
      voter: $voter
    }
  }) {
    nodes {
      ... on Vote {
        type
        amount
        voter {
          id
          name
        }
      }
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetProposals {
  proposals {
    ... on Proposal {
      id
      metadata {
        title
      }
      votes(input: {
        filters: {
          voter: "0x..."
        }
      })
    }
  }
}
content_copy
download
Use code with caution.
Graphql

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.

Rule 9: API Constraints (Mandatory)

Problem: Not all fields or properties are queryable in all situations. Some queries have explicit requirements that must be met.

Solution: You must always check your query against the known API constraints, and ensure that all requirements are met.

Example:

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.

An error you may see is: "proposalId or proposalIds must be provided"

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.

Rule 10: Multi-Step Queries (Mandatory)

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.

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.

Example

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.

Correct Example

# Step 1: Get the organization ID using a query that filters by slug

query GetOrganizationId($slug: String!) {
  organization(input: {slug: $slug}) {
    id
  }
}

# Step 2: Get the proposals for the given organization
query GetProposalsForOrganization($organizationId: IntID!) {
    proposals(input: {
      filters: {
        organizationId: $organizationId
      }
    }) {
        nodes {
            ... on Proposal {
                id
            }
        }
    }
}

# Step 3: Get all the votes for all of the proposals.
query GetVotesForProposals($proposalIds: [IntID!]!) {
    votes(input: {
      filters: {
         proposalIds: $proposalIds
      }
    })
        {
        nodes {
            ... on Vote {
                id
                type
                amount
            }
        }
    }
}
content_copy
download
Use code with caution.
Graphql

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.

Rule 11: Fragment Usage (Mandatory)

Problem: Defining fragments that aren't used creates unnecessary code.

Solution: You must always use all defined fragments, and any unused fragments must be removed before submitting a query.

Example

fragment BasicAccountInfo on Account {
  id
  address
  ens
}

fragment VoteDetails on Vote {
    type
    amount
}

query GetVotes($input: VotesInput!) {
  votes(input: $input) {
    nodes {
      ... on Vote {
        ...VoteDetails
      }
    }
  }
}
content_copy
download
Use code with caution.
Graphql
*   **Action:** All defined fragments *must* be used, and any unused fragments *must* be removed before submitting a query.
content_copy
download
Use code with caution.

Rule 12: Data Verification (Mandatory)

Problem: The LLM may invent data, such as Ethereum addresses, and then use it as a query parameter, without verifying it. This leads to inaccurate results and potentially misleading conclusions.

Solution: The LLM must not invent any external data. If data is used as a query parameter, the LLM must attempt to verify that the data is correct using external sources. If the LLM cannot verify the data, it must explicitly state that the data is unverified, and not present it as fact. Failure to do so is a fatal error.

Specific Error Case: The LLM invents an ethereum address, and uses it to make conclusions about a specific organization.

The LLM assumed an incorrect address for L2BEAT, used the incorrect address in a query, and drew false conclusions about their voting habits.

Action: Before using external data as a query parameter, the LLM must attempt to verify its accuracy, including but not limited to:

External Lookups: Attempt to verify a key identifier (e.g., organization address, slug) through external sources (such as Etherscan, a project's official website, or social media), and use that value in queries to Tally.

Explicit Validation: In all cases where a query uses external values, and the external values could not be verified, the LLM must disclose that the data is not verified, and must not draw conclusions based on the unverified data.

Example of Verification:
If the LLM needs to get information about the organization "L2BEAT," it must first try to verify L2BEAT's organization address or slug using an external source of truth. If the LLM cannot find a valid and verifiable address, it must not proceed with the query.
For example:

The LLM must first try to obtain the L2BEAT organization ID, for example, from the Tally UI, or from external sources.

If the LLM cannot obtain the L2BEAT organization ID from an external source, the LLM must not proceed with the query.

If the LLM is able to obtain an L2BEAT organization ID, it must use that ID in its query to Tally.

If the LLM is unable to obtain a valid ID, or must use information from an unverified source, then the LLM must make explicit mention of that fact.

The LLM must make no claims or conclusions about any unverified data.

Emphasis on External Sources: The following should be considered sources of truth when creating queries that reference specific data:

Etherscan: If you are searching for an address, or a contract address, you can use Etherscan to verify that the data is correct.

Project Websites and Socials: Project websites and social accounts should be consulted to verify the claims being made.

Tally UI: The Tally user interface can be used to verify organization addresses or slugs.

Complete Schema Reference

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:

type Account {
  id: ID!
  address: String!
  ens: String
  twitter: String
  name: String!
  bio: String!
  picture: String
  safes: [AccountID!]
  type: AccountType!
  votes(governorId: AccountID!): Uint256!
  proposalsCreatedCount(input: ProposalsCreatedCountInput!): Int!
}

enum AccountType {
    EOA
    SAFE
}
type Delegate {
    id: IntID!
    account: Account!
    chainId: ChainID
    delegatorsCount: Int!
    governor: Governor
    organization: Organization
    statement: DelegateStatement
    token: Token
    votesCount(blockNumber: Int): Uint256!
  }

  input DelegateInput {
    address: Address!
    governorId: AccountID
    organizationId: IntID
  }

type DelegateStatement {
    id: IntID!
    address: Address!
    organizationID: IntID!
    statement: String!
    statementSummary: String
    isSeekingDelegation: Boolean
    issues: [Issue!]
  }

type Delegation {
    id: IntID!
    blockNumber: Int!
    blockTimestamp: Timestamp!
    chainId: ChainID!
    delegator: Account!
    delegate: Account!
    organization: Organization!
    token: Token!
    votes: Uint256!
}
input DelegationInput {
    address: Address!
    tokenId: AssetID!
  }
input DelegationsInput {
  filters: DelegationsFiltersInput!
  page: PageInput
  sort: DelegationsSortInput
}
input DelegationsFiltersInput {
    address: Address!
    governorId: AccountID
    organizationId: IntID
}
input DelegationsSortInput {
    isDescending: Boolean!
    sortBy: DelegationsSortBy!
}
enum DelegationsSortBy {
  id
  votes
}

type Governor {
    id: AccountID!
    chainId: ChainID!
    contracts: Contracts!
    isIndexing: Boolean!
    isBehind: Boolean!
    isPrimary: Boolean!
    kind: GovernorKind!
    name: String!
    organization: Organization!
    proposalStats: ProposalStats!
    parameters: GovernorParameters!
    quorum: Uint256!
    slug: String!
    timelockId: AccountID
    tokenId: AssetID!
    token: Token!
    type: GovernorType!
    delegatesCount: Int!
    delegatesVotesCount: Uint256!
    tokenOwnersCount: Int!
    metadata: GovernorMetadata
  }
  type GovernorContract {
    address: Address!
    type: GovernorType!
  }

  input GovernorInput {
    id: AccountID
    slug: String
  }

type Organization {
    id: IntID!
    slug: String!
    name: String!
    chainIds: [ChainID!]!
    tokenIds: [AssetID!]!
    governorIds: [AccountID!]!
    metadata: OrganizationMetadata
    creator: Account
    hasActiveProposals: Boolean!
    proposalsCount: Int!
    delegatesCount: Int!
    delegatesVotesCount: Uint256!
    tokenOwnersCount: Int!
    endorsementService: EndorsementService
}
input OrganizationInput {
  id: IntID
  slug: String
}
input OrganizationsInput {
  filters: OrganizationsFiltersInput
  page: PageInput
  sort: OrganizationsSortInput
}
input OrganizationsFiltersInput {
  address: Address
  chainId: ChainID
  hasLogo: Boolean
    isMember: Boolean
}
input OrganizationsSortInput {
    isDescending: Boolean!
    sortBy: OrganizationsSortBy!
}

enum OrganizationsSortBy {
  id
  name
  explore
  popular
}

type Proposal {
    id: IntID!
    onchainId: String
    block: Block
    chainId: ChainID!
    creator: Account
    end: BlockOrTimestamp!
    events: [ProposalEvent!]!
    executableCalls: [ExecutableCall!]
    governor: Governor!
    metadata: ProposalMetadata!
    organization: Organization!
    proposer: Account
    quorum: Uint256
    status: ProposalStatus!
    start: BlockOrTimestamp!
    voteStats: [VoteStats!]
}
input ProposalInput {
  id: IntID
  onchainId: String
  governorId: AccountID
    includeArchived: Boolean
    isLatest: Boolean
}
type ProposalMetadata {
  title: String
  description: String
  eta: Int
  ipfsHash: String
      previousEnd: Int
      timelockId: AccountID
      txHash: Hash
      discourseURL: String
  snapshotURL: String
}

input ProposalsInput {
  filters: ProposalsFiltersInput
  page: PageInput
  sort: ProposalsSortInput
}
input ProposalsFiltersInput {
  governorId: AccountID
  includeArchived: Boolean
    isDraft: Boolean
  organizationId: IntID
  proposer: Address
}
input ProposalsSortInput {
  isDescending: Boolean!
  sortBy: ProposalsSortBy!
}
enum ProposalsSortBy {
  id
}

type Token {
    id: AssetID!
    type: TokenType!
    name: String!
    symbol: String!
    supply: Uint256!
    decimals: Int!
    eligibility: Eligibility
    isIndexing: Boolean!
    isBehind: Boolean!
}
type Vote {
    id: IntID!
    amount: Uint256!
    block: Block!
    chainId: ChainID!
    isBridged: Boolean
    proposal: Proposal!
    reason: String
    type: VoteType!
    txHash: Hash!
    voter: Account!
}
input VotesInput {
    filters: VotesFiltersInput
    page: PageInput
    sort: VotesSortInput
  }
input VotesFiltersInput {
    proposalId: IntID
    proposalIds: [IntID!]
    voter: Address
    includePendingVotes: Boolean
    type: VoteType
}
input VotesSortInput {
    isDescending: Boolean!
    sortBy: VotesSortBy!
}
enum VotesSortBy {
  id
  amount
}

enum VoteType {
    abstain
    against
    for
    pendingabstain
    pendingagainst
    pendingfor
}
content_copy
download
Use code with caution.
Graphql

Best Practices Checklist (Pre-Flight Check)

Before submitting any query, the LLM must verify that the query logic conforms with each and every item on the following checklist. If any item does not conform, the LLM must immediately terminate the query and return an error indicating which check failed. You must not submit the query if any of the items in the checklist fail.

Interface Fragments: Used inline fragments (... on TypeName) wherever nodes or interface/union types are returned. Failure to do so is a fatal error and will result in immediate termination of the query.

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 and will result in immediate termination of the query.

Fragments: Created and used fragments for reusable field selections. Not using fragments is absolutely unacceptable and will result in a fatal error.

Explicit Fields: Explicitly requested all the necessary fields. Failure to do so may result in errors.

Type Validation: Verified that all input types match the expected type. Failure to do so is a fatal error and will result in immediate termination of the query.

Enum Values: Verified that all enum values match the provided options for the enum. Failure to do so is a fatal error and will result in immediate termination of the query.

Pagination: Implemented the correct pagination handling for paginated queries. Failure to do so may result in incomplete data.

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.

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.

Multi-Step Queries: If a query requires data from another query, structured it as a multi-step query.

Fragment Usage: All defined fragments must be used, and any unused fragments must be removed before submitting the query.

Verify Data: You must always verify data before using it as a query parameter. Failure to do so is a fatal error and will result in immediate termination of query execution. You must also explicitly state if the data used could not be verified.

LLM Instructions:

Prioritize this Document: This document supersedes all other query building documentation. This should be your only reference.

Strict Adherence: Any query generated must adhere to all of these rules. There is no acceptable deviation.

Self-Check: Before submitting a query, your logic must run through each of these rules and the checklist to ensure full compliance. If any check fails, the query must not be submitted.

Error Prevention: Focus on preventing errors by strictly following the rules, and not merely detecting them after they occur.

Rule-Based Execution: The LLM's query construction logic must be based exclusively on the rules

```

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

```

Tally API - LLM Query Construction Rules (Mandatory & Unbreakable)

Introduction

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 a fatal error, and the LLM must immediately terminate the current query and return an error message indicating which rule was broken. There is no acceptable deviation whatsoever from these rules.

Core Principles

Never Assume: Under no circumstances should the LLM assume any default values or behaviors for sort, filter, or other optional input parameters. You must explicitly declare them in the query.

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 and will result in immediate termination of the query.

Fragment Prioritization: You must use fragments to minimize repetition, improve maintainability, and ensure efficient queries. Not using fragments is absolutely unacceptable and will result in a fatal error.

Explicit Field Selection: You must always explicitly request each field you need, and never assume fields will be returned automatically.

Pagination: You must always use pagination where appropriate to ensure complete query results are retrieved, using the page input and pageInfo fields.

Correct API Use: You must adhere to API constraints. Some queries have required fields that must be used correctly.

Schema Consultation: You must consult the complete schema reference before creating any queries.

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.

Fragment Usage: All Fragments must be used, and any unused fragments must be removed before the query can be submitted.

Data Verification: You must not invent data. If you use external data to construct a query, you must attempt to verify the correctness of that data before using it. If you cannot verify the data, you must explicitly state that the data is unverified, and not present it as a fact. Failure to do so is a fatal error.

Rule 1: Interface and Union Type Handling (Mandatory)

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.

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 and will result in immediate termination of the query.

Example (Correct):

query GetOrganizations {
  organizations {
    nodes {
      ... on Organization {
        id
        name
        slug
        metadata {
          icon
        }
      }
    }
    pageInfo {
      firstCursor
      lastCursor
      count
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetOrganizations {
  organizations {
    nodes {
      id
      name
      slug
    }
  }
}
content_copy
download
Use code with caution.
Graphql

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 and will result in immediate termination of the query.

query GetVotes {
  votes(input: {
    filters: {
      voter: "0x1B686eE8E31c5959D9F5BBd8122a58682788eeaD"
    }
  }) {
    nodes {
      type
    }
  }
}
content_copy
download
Use code with caution.
Graphql

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 and will result in immediate termination of the query.

Rule 2: Explicit Sort and Filter Inputs (Mandatory)

Problem: Queries with sort or filter options often have required input types that must be fully populated.

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.

Example (Correct):

query GetProposals($input: ProposalsInput!) {
  proposals(input: $input) {
    nodes {
        ... on Proposal {
            id
            metadata {
                title
            }
            status
        }
    }
    pageInfo {
      firstCursor
      lastCursor
      count
    }
  }
}
content_copy
download
Use code with caution.
Graphql
*  **Input:**
content_copy
download
Use code with caution.
input ProposalsInput {
    filters: ProposalsFiltersInput
    page: PageInput
    sort: ProposalsSortInput
}

input ProposalsFiltersInput {
    governorId: AccountID
    includeArchived: Boolean
    isDraft: Boolean
    organizationId: IntID
    proposer: Address
}
input ProposalsSortInput {
    isDescending: Boolean!
    sortBy: ProposalsSortBy!
}
enum ProposalsSortBy {
    id
}

input PageInput {
    afterCursor: String
    beforeCursor: String
    limit: Int
}
content_copy
download
Use code with caution.
Graphql
*   **Query:** (with optional sort, and filters)
content_copy
download
Use code with caution.
query GetProposalsWithSortAndFilter {
  proposals(input: {
      filters: {
          governorId: "eip155:1:0x123abc"
          includeArchived: true
      },
      sort: {
        sortBy: id
        isDescending: false
      },
      page: {
        limit: 10
      }
  })
    {
    nodes {
        ... on Proposal {
            id
            metadata {
                title
            }
            status
        }
    }
    pageInfo {
      firstCursor
      lastCursor
       count
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetProposals {
  proposals {
    nodes {
      id
      metadata {
        title
      }
      status
    }
  }
}
content_copy
download
Use code with caution.
Graphql

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 and will result in immediate termination of the query.

Rule 3: Fragment Usage (Mandatory)

Problem: Repeated field selections in multiple queries make the code less maintainable and are prone to errors.

Solution: You must use fragments to group common field selections and reuse them across multiple queries. Not using fragments is absolutely unacceptable and will result in a fatal error.

Example (Correct):

fragment BasicProposalDetails on Proposal {
    id
    onchainId
    metadata {
      title
      description
    }
   status
}


query GetProposals($input: ProposalsInput!) {
  proposals(input: $input) {
    nodes {
        ... on Proposal {
            ...BasicProposalDetails
         }
    }
    pageInfo {
      firstCursor
      lastCursor
       count
    }
  }
}

query GetSingleProposal($input: ProposalInput!) {
    proposal(input: $input) {
      ...BasicProposalDetails
    }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetProposals {
  proposals {
    nodes {
       id
       onchainId
       metadata {
        title
        description
       }
       status
    }
  }
}

query GetSingleProposal {
    proposal(input: {id: 123}) {
      id
      onchainId
      metadata {
        title
        description
      }
      status
    }
}
content_copy
download
Use code with caution.
Graphql

Action: Always create and use fragments, and make them focused, and reusable across multiple queries. Not using fragments is absolutely unacceptable and will result in a fatal error.

Rule 4: Explicit Field Selection (Mandatory)

Problem: Assuming the API will return certain fields if they aren't specifically requested.

Solution: You must always request every field you need in your query.

Example (Correct):

query GetOrganization($input: OrganizationInput!) {
  organization(input: $input) {
    id
    name
    slug
    metadata {
      icon
      description
      socials {
        website
      }
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetOrganization {
  organization {
    name
    slug
  }
}
content_copy
download
Use code with caution.
Graphql

Action: List out every field you need in the query, and avoid implied or implicit field selections.

Rule 5: Input Type Validation (Mandatory)

Problem: Using the wrong types when providing input values to a query.

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 and will result in immediate termination of the query.

Example (Correct):

query GetAccount($id: AccountID!) {
  account(id: $id) {
    id
    name
    address
    ens
    picture
  }
}
content_copy
download
Use code with caution.
Graphql

Query

query GetAccountCorrect {
   account(id:"eip155:1:0x123") {
    id
     name
     address
     ens
     picture
   }
}
content_copy
download
Use code with caution.
Graphql
* The `id` argument correctly uses the `AccountID` type, which is a string representing a CAIP-10 ID.
content_copy
download
Use code with caution.

Specific Error Case: Attempting to use a plain integer for organizationId in proposal queries. This is a fatal error and will result in immediate termination of the query.

query GetProposals {
  proposals(input: {
    filters: {
      organizationId: 1
    }
  })
  {
    nodes {
      ... on Proposal {
          id
         }
      }
   }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetAccount($id: AccountID!) {
  account(id: $id) {
    id
    name
    address
  }
}
content_copy
download
Use code with caution.
Graphql

Query

query GetAccountIncorrect {
   account(id:123) {
    id
     name
     address
     ens
     picture
   }
}
content_copy
download
Use code with caution.
Graphql

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 and will result in immediate termination of the query.

ID Type Definitions

AccountID: A CAIP-10 compliant account id. (e.g., "eip155:1:0x7e90e03654732abedf89Faf87f05BcD03ACEeFdc")

AssetID: A CAIP-19 compliant asset id. (e.g., "eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f")

IntID: A 64-bit integer represented as a string. (e.g., "1234567890")

HashID: A CAIP-2 scoped identifier for identifying transactions across chains. (e.g., "eip155:1:0xDEAD")

BlockID: A CAIP-2 scoped identifier for identifying blocks across chains. (e.g., "eip155:1:15672")

ChainID: A CAIP-2 compliant chain ID. (e.g., "eip155:1")

Address: A 20 byte ethereum address, represented as 0x-prefixed hexadecimal. (e.g., "0x1234567800000000000000000000000000000abc")

Rule 6: Enum Usage (Mandatory)

Problem: Using a string value when an enum type is expected.

Solution: Always use the correct values for an enum type. Failure to do so is a fatal error and will result in immediate termination of the query.

Example (Correct)

query GetVotes($input: VotesInput!) {
  votes(input: $input) {
    nodes {
      id
       type
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Input:

input VotesInput {
    filters: VotesFiltersInput
    page: PageInput
    sort: VotesSortInput
}

input VotesFiltersInput {
    proposalId: IntID
    proposalIds: [IntID!]
    voter: Address
    includePendingVotes: Boolean
    type: VoteType
}
enum VoteType {
    abstain
    against
    for
    pendingabstain
    pendingagainst
    pendingfor
}
content_copy
download
Use code with caution.
Graphql

Query: (Correctly using an enum type)

query GetVotesFor {
    votes(input: {
        filters: {
            type: for
             proposalId: 123
        }
    })
    {
      nodes {
        id
          type
      }
    }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetVotesFor {
    votes(input: {
        filters: {
            type: "for"
             proposalId: 123
        }
    })
    {
      nodes {
        id
          type
      }
    }
}
content_copy
download
Use code with caution.
Graphql

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 and will result in immediate termination of the query.

Rule 7: Pagination Handling (Mandatory)

Problem: Queries that return paginated data do not return complete results if pagination is not handled.

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.

Example (Correct):

query GetPaginatedProposals($input: ProposalsInput!) {
  proposals(input: $input) {
    nodes {
        ... on Proposal {
            id
            metadata {
                title
            }
        }
    }
   pageInfo {
       firstCursor
       lastCursor
       count
     }
  }
}
content_copy
download
Use code with caution.
Graphql
* **Input**
content_copy
download
Use code with caution.
input ProposalsInput {
    filters: ProposalsFiltersInput
    page: PageInput
    sort: ProposalsSortInput
}

input ProposalsFiltersInput {
    governorId: AccountID
    includeArchived: Boolean
    isDraft: Boolean
    organizationId: IntID
    proposer: Address
}
input ProposalsSortInput {
    isDescending: Boolean!
    sortBy: ProposalsSortBy!
}
enum ProposalsSortBy {
    id
}

input PageInput {
    afterCursor: String
    beforeCursor: String
    limit: Int
}
content_copy
download
Use code with caution.
Graphql

Query:

query GetProposalsWithPagination {
    proposals(input: {
       page: {
           limit: 20
       }
    }) {
        nodes {
            ... on Proposal {
              id
               metadata {
                title
              }
           }
         }
         pageInfo {
           firstCursor
            lastCursor
           count
          }
        }
}
content_copy
download
Use code with caution.
Graphql

Query: (Using cursors to get the next page of results)

query GetProposalsWithPagination {
    proposals(input: {
       page: {
           limit: 20
           afterCursor: "cursorFromPreviousQuery"
       }
    }) {
        nodes {
          ... on Proposal {
            id
            metadata {
                title
              }
           }
        }
         pageInfo {
           firstCursor
            lastCursor
           count
          }
        }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetProposals {
  proposals {
      nodes {
         ... on Proposal {
          id
            metadata {
                title
            }
           }
       }
    }
}
content_copy
download
Use code with caution.
Graphql

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.

Rule 8: Correctly Querying Related Data (Mandatory)

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.

Solution: You must fetch related data by using separate queries, instead of assuming that related data is queryable as nested fields.

Example (Correct)

query GetProposalAndVotes($proposalId: IntID!, $voter: Address) {
  proposal(input: { id: $proposalId}) {
    id
    metadata {
      title
    }
    status
  }
  votes(input: {
    filters: {
      proposalId: $proposalId
      voter: $voter
    }
  }) {
    nodes {
      ... on Vote {
        type
        amount
        voter {
          id
          name
        }
      }
    }
  }
}
content_copy
download
Use code with caution.
Graphql

Example (Incorrect - Avoid):

query GetProposals {
  proposals {
    ... on Proposal {
      id
      metadata {
        title
      }
      votes(input: {
        filters: {
          voter: "0x..."
        }
      })
    }
  }
}
content_copy
download
Use code with caution.
Graphql

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.

Rule 9: API Constraints (Mandatory)

Problem: Not all fields or properties are queryable in all situations. Some queries have explicit requirements that must be met.

Solution: You must always check your query against the known API constraints, and ensure that all requirements are met.

Example:

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.

An error you may see is: "proposalId or proposalIds must be provided"

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.

Rule 10: Multi-Step Queries (Mandatory)

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.

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.

Example

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.

Correct Example

# Step 1: Get the organization ID using a query that filters by slug

query GetOrganizationId($slug: String!) {
  organization(input: {slug: $slug}) {
    id
  }
}

# Step 2: Get the proposals for the given organization
query GetProposalsForOrganization($organizationId: IntID!) {
    proposals(input: {
      filters: {
        organizationId: $organizationId
      }
    }) {
        nodes {
            ... on Proposal {
                id
            }
        }
    }
}

# Step 3: Get all the votes for all of the proposals.
query GetVotesForProposals($proposalIds: [IntID!]!) {
    votes(input: {
      filters: {
         proposalIds: $proposalIds
      }
    })
        {
        nodes {
            ... on Vote {
                id
                type
                amount
            }
        }
    }
}
content_copy
download
Use code with caution.
Graphql

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.

Rule 11: Fragment Usage (Mandatory)

Problem: Defining fragments that aren't used creates unnecessary code.

Solution: You must always use all defined fragments, and any unused fragments must be removed before submitting a query.

Example

fragment BasicAccountInfo on Account {
  id
  address
  ens
}

fragment VoteDetails on Vote {
    type
    amount
}

query GetVotes($input: VotesInput!) {
  votes(input: $input) {
    nodes {
      ... on Vote {
        ...VoteDetails
      }
    }
  }
}
content_copy
download
Use code with caution.
Graphql
*   **Action:** All defined fragments *must* be used, and any unused fragments *must* be removed before submitting a query.
content_copy
download
Use code with caution.

Rule 12: Data Verification (Mandatory)

Problem: The LLM may invent data, such as Ethereum addresses, and then use it as a query parameter, without verifying it. This leads to inaccurate results and potentially misleading conclusions.

Solution: The LLM must not invent any external data. If data is used as a query parameter, the LLM must attempt to verify that the data is correct using external sources. If the LLM cannot verify the data, it must explicitly state that the data is unverified, and not present it as fact. Failure to do so is a fatal error.

Specific Error Case: The LLM invents an ethereum address, and uses it to make conclusions about a specific organization.

The LLM assumed an incorrect address for L2BEAT, used the incorrect address in a query, and drew false conclusions about their voting habits.

Action: Before using external data as a query parameter, the LLM must attempt to verify its accuracy, including but not limited to:

External Lookups: Attempt to verify a key identifier (e.g., organization address, slug) through external sources (such as Etherscan, a project's official website, or social media), and use that value in queries to Tally.

Explicit Validation: In all cases where a query uses external values, and the external values could not be verified, the LLM must disclose that the data is not verified, and must not draw conclusions based on the unverified data.

Example of Verification:
If the LLM needs to get information about the organization "L2BEAT," it must first try to verify L2BEAT's organization address or slug using an external source of truth. If the LLM cannot find a valid and verifiable address, it must not proceed with the query.
For example:

The LLM must first try to obtain the L2BEAT organization ID, for example, from the Tally UI, or from external sources.

If the LLM cannot obtain the L2BEAT organization ID from an external source, the LLM must not proceed with the query.

If the LLM is able to obtain an L2BEAT organization ID, it must use that ID in its query to Tally.

If the LLM is unable to obtain a valid ID, or must use information from an unverified source, then the LLM must make explicit mention of that fact.

The LLM must make no claims or conclusions about any unverified data.

Emphasis on External Sources: The following should be considered sources of truth when creating queries that reference specific data:

Etherscan: If you are searching for an address, or a contract address, you can use Etherscan to verify that the data is correct.

Project Websites and Socials: Project websites and social accounts should be consulted to verify the claims being made.

Tally UI: The Tally user interface can be used to verify organization addresses or slugs.

Complete Schema Reference

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:

type Account {
  id: ID!
  address: String!
  ens: String
  twitter: String
  name: String!
  bio: String!
  picture: String
  safes: [AccountID!]
  type: AccountType!
  votes(governorId: AccountID!): Uint256!
  proposalsCreatedCount(input: ProposalsCreatedCountInput!): Int!
}

enum AccountType {
    EOA
    SAFE
}
type Delegate {
    id: IntID!
    account: Account!
    chainId: ChainID
    delegatorsCount: Int!
    governor: Governor
    organization: Organization
    statement: DelegateStatement
    token: Token
    votesCount(blockNumber: Int): Uint256!
  }

  input DelegateInput {
    address: Address!
    governorId: AccountID
    organizationId: IntID
  }

type DelegateStatement {
    id: IntID!
    address: Address!
    organizationID: IntID!
    statement: String!
    statementSummary: String
    isSeekingDelegation: Boolean
    issues: [Issue!]
  }

type Delegation {
    id: IntID!
    blockNumber: Int!
    blockTimestamp: Timestamp!
    chainId: ChainID!
    delegator: Account!
    delegate: Account!
    organization: Organization!
    token: Token!
    votes: Uint256!
}
input DelegationInput {
    address: Address!
    tokenId: AssetID!
  }
input DelegationsInput {
  filters: DelegationsFiltersInput!
  page: PageInput
  sort: DelegationsSortInput
}
input DelegationsFiltersInput {
    address: Address!
    governorId: AccountID
    organizationId: IntID
}
input DelegationsSortInput {
    isDescending: Boolean!
    sortBy: DelegationsSortBy!
}
enum DelegationsSortBy {
  id
  votes
}

type Governor {
    id: AccountID!
    chainId: ChainID!
    contracts: Contracts!
    isIndexing: Boolean!
    isBehind: Boolean!
    isPrimary: Boolean!
    kind: GovernorKind!
    name: String!
    organization: Organization!
    proposalStats: ProposalStats!
    parameters: GovernorParameters!
    quorum: Uint256!
    slug: String!
    timelockId: AccountID
    tokenId: AssetID!
    token: Token!
    type: GovernorType!
    delegatesCount: Int!
    delegatesVotesCount: Uint256!
    tokenOwnersCount: Int!
    metadata: GovernorMetadata
  }
  type GovernorContract {
    address: Address!
    type: GovernorType!
  }

  input GovernorInput {
    id: AccountID
    slug: String
  }

type Organization {
    id: IntID!
    slug: String!
    name: String!
    chainIds: [ChainID!]!
    tokenIds: [AssetID!]!
    governorIds: [AccountID!]!
    metadata: OrganizationMetadata
    creator: Account
    hasActiveProposals: Boolean!
    proposalsCount: Int!
    delegatesCount: Int!
    delegatesVotesCount: Uint256!
    tokenOwnersCount: Int!
    endorsementService: EndorsementService
}
input OrganizationInput {
  id: IntID
  slug: String
}
input OrganizationsInput {
  filters: OrganizationsFiltersInput
  page: PageInput
  sort: OrganizationsSortInput
}
input OrganizationsFiltersInput {
  address: Address
  chainId: ChainID
  hasLogo: Boolean
    isMember: Boolean
}
input OrganizationsSortInput {
    isDescending: Boolean!
    sortBy: OrganizationsSortBy!
}

enum OrganizationsSortBy {
  id
  name
  explore
  popular
}

type Proposal {
    id: IntID!
    onchainId: String
    block: Block
    chainId: ChainID!
    creator: Account
    end: BlockOrTimestamp!
    events: [ProposalEvent!]!
    executableCalls: [ExecutableCall!]
    governor: Governor!
    metadata: ProposalMetadata!
    organization: Organization!
    proposer: Account
    quorum: Uint256
    status: ProposalStatus!
    start: BlockOrTimestamp!
    voteStats: [VoteStats!]
}
input ProposalInput {
  id: IntID
  onchainId: String
  governorId: AccountID
    includeArchived: Boolean
    isLatest: Boolean
}
type ProposalMetadata {
  title: String
  description: String
  eta: Int
  ipfsHash: String
      previousEnd: Int
      timelockId: AccountID
      txHash: Hash
      discourseURL: String
  snapshotURL: String
}

input ProposalsInput {
  filters: ProposalsFiltersInput
  page: PageInput
  sort: ProposalsSortInput
}
input ProposalsFiltersInput {
  governorId: AccountID
  includeArchived: Boolean
    isDraft: Boolean
  organizationId: IntID
  proposer: Address
}
input ProposalsSortInput {
  isDescending: Boolean!
  sortBy: ProposalsSortBy!
}
enum ProposalsSortBy {
  id
}

type Token {
    id: AssetID!
    type: TokenType!
    name: String!
    symbol: String!
    supply: Uint256!
    decimals: Int!
    eligibility: Eligibility
    isIndexing: Boolean!
    isBehind: Boolean!
}
type Vote {
    id: IntID!
    amount: Uint256!
    block: Block!
    chainId: ChainID!
    isBridged: Boolean
    proposal: Proposal!
    reason: String
    type: VoteType!
    txHash: Hash!
    voter: Account!
}
input VotesInput {
    filters: VotesFiltersInput
    page: PageInput
    sort: VotesSortInput
  }
input VotesFiltersInput {
    proposalId: IntID
    proposalIds: [IntID!]
    voter: Address
    includePendingVotes: Boolean
    type: VoteType
}
input VotesSortInput {
    isDescending: Boolean!
    sortBy: VotesSortBy!
}
enum VotesSortBy {
  id
  amount
}

enum VoteType {
    abstain
    against
    for
    pendingabstain
    pendingagainst
    pendingfor
}
content_copy
download
Use code with caution.
Graphql

Best Practices Checklist (Pre-Flight Check)

Before submitting any query, the LLM must verify that the query logic conforms with each and every item on the following checklist. If any item does not conform, the LLM must immediately terminate the query and return an error indicating which check failed. You must not submit the query if any of the items in the checklist fail.

Interface Fragments: Used inline fragments (... on TypeName) wherever nodes or interface/union types are returned. Failure to do so is a fatal error and will result in immediate termination of the query.

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 and will result in immediate termination of the query.

Fragments: Created and used fragments for reusable field selections. Not using fragments is absolutely unacceptable and will result in a fatal error.

Explicit Fields: Explicitly requested all the necessary fields. Failure to do so may result in errors.

Type Validation: Verified that all input types match the expected type. Failure to do so is a fatal error and will result in immediate termination of the query.

Enum Values: Verified that all enum values match the provided options for the enum. Failure to do so is a fatal error and will result in immediate termination of the query.

Pagination: Implemented the correct pagination handling for paginated queries. Failure to do so may result in incomplete data.

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.

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.

Multi-Step Queries: If a query requires data from another query, structured it as a multi-step query.

Fragment Usage: All defined fragments must be used, and any unused fragments must be removed before submitting the query.

Verify Data: You must always verify data before using it as a query parameter. Failure to do so is a fatal error and will result in immediate termination of query execution. You must also explicitly state if the data used could not be verified.

LLM Instructions:

Prioritize this Document: This document supersedes all other query building documentation. This should be your only reference.

Strict Adherence: Any query generated must adhere to all of these rules. There is no acceptable deviation.

Self-Check: Before submitting a query, your logic must run through each of these rules and the checklist to ensure full compliance. If any check fails, the query must not be submitted.

Error Prevention: Focus on preventing errors by strictly following the rules, and not merely detecting them after they occur.

Rule-Based Execution: The LLM's query construction logic must be based exclusively on the rules

```
Page 2/4FirstPrevNextLast