Skip to main content

Overview

The API service is an Apollo GraphQL server that indexes governance data from multiple blockchain networks and protocols. It uses Checkpoint, a blockchain indexing framework, to process events and serve unified governance data.
The API supports Snapshot X, Compound Governor (Governor Bravo), and OpenZeppelin Governor protocols across multiple EVM chains and Starknet.

Quick Start

Prerequisites

  • Node.js >= 22.6
  • MySQL 8.0
  • RPC endpoints for networks you want to index

Installation

cd apps/api
yarn install

Environment Setup

Copy .env.example to .env and configure:
DATABASE_URL=mysql://user:password@localhost:3306/sx_api
IS_INDEXER=true  # Set to false for API-only mode

# Enable/disable protocols
ENABLE_SNAPSHOT_X=true      # Default: true
ENABLE_GOVERNOR_BRAVO=true  # Default: false
ENABLE_OPENZEPPELIN=true    # Default: false

# Network RPC URLs
ETH_NODE_URL=https://...
SEPOLIA_NODE_URL=https://...
BASE_NODE_URL=https://...
# ... other networks

Start Development Server

With Docker (includes MySQL):
docker compose up mysql
yarn dev
The GraphQL API will be available at http://localhost:3000/graphql.

Architecture

Checkpoint Indexer

Checkpoint is a blockchain indexing framework that:
  • Processes blockchain events in real-time
  • Stores structured data in MySQL
  • Automatically generates GraphQL schema and resolvers
  • Supports multiple networks and protocols
The API can run in indexer mode (processes blocks) or API-only mode (serves data without indexing).

Modes of Operation

apps/api/src/index.ts
const IS_INDEXER = process.env.IS_INDEXER === 'true';

if (IS_INDEXER) {
  // Start both API server and indexer
  await startApiServer(checkpoint);
  await startIndexer(checkpoint);
} else {
  // API-only mode
  await startApiServer(checkpoint);
}

Supported Protocols

Snapshot X

Hybrid onchain/offchain governance with cross-chain support

Compound Governor

Governor Bravo standard used by Compound and forks

OpenZeppelin

OpenZeppelin Governor contract standard

Enabling Protocols

From apps/api/src/evm/index.ts:
// SnapshotX runs by default unless explicitly disabled
export const ENABLE_SNAPSHOT_X = process.env.ENABLE_SNAPSHOT_X !== 'false';
export const ENABLE_GOVERNOR_BRAVO = process.env.ENABLE_GOVERNOR_BRAVO === 'true';
export const ENABLE_OPENZEPPELIN = process.env.ENABLE_OPENZEPPELIN === 'true';

const protocols: Protocols = {
  snapshotX: ENABLE_SNAPSHOT_X,
  governorBravo: ENABLE_GOVERNOR_BRAVO,
  openZeppelin: ENABLE_OPENZEPPELIN
};

Supported Networks

EVM Networks

  • Ethereum Mainnet (eth)
  • Ethereum Sepolia (sep)
  • Optimism (oeth)
  • Polygon (matic)
  • Arbitrum One (arb1)
  • Base (base)
  • Mantle (mnt)
  • ApeChain (ape)
  • Curtis (curtis)

Starknet Networks

  • Starknet Mainnet
  • Starknet Sepolia
Each network can be configured with different protocol support.

GraphQL Schema

Checkpoint auto-generates a GraphQL schema from the data models. The schema includes:

Core Types

type Space {
  id: String!
  name: String
  controller: String!
  voting_delay: Int!
  min_voting_period: Int!
  max_voting_period: Int!
  proposal_threshold: String!
  validation_strategy: String!
  strategies: [Strategy!]!
  proposals: [Proposal!]!
}

type Proposal {
  id: String!
  proposal_id: Int!
  space: Space!
  author: User!
  execution_hash: String!
  metadata_uri: String!
  start_block_number: Int!
  min_end_block_number: Int!
  max_end_block_number: Int!
  snapshot_block_number: Int!
  execution_strategy: String!
  execution_strategy_type: String!
  state: String!
  votes: [Vote!]!
  vote_count: Int!
  created: Int!
}

type Vote {
  id: String!
  voter: User!
  proposal: Proposal!
  choice: Int!
  voting_power: String!
  created: Int!
}

type User {
  id: String!
  proposal_count: Int!
  vote_count: Int!
  proposals: [Proposal!]!
  votes: [Vote!]!
}

Example Queries

# Get all spaces
query Spaces {
  spaces {
    id
    name
    controller
    proposal_count
  }
}

# Get proposals for a space
query SpaceProposals($spaceId: String!) {
  proposals(where: { space: $spaceId }) {
    id
    proposal_id
    author { id }
    metadata_uri
    state
    vote_count
    created
  }
}

# Get votes for a proposal
query ProposalVotes($proposalId: String!) {
  votes(where: { proposal: $proposalId }) {
    voter { id }
    choice
    voting_power
    created
  }
}

Package Scripts

From package.json:
{
  "scripts": {
    "codegen": "checkpoint generate",
    "build": "tsc",
    "dev": "nodemon src/index.ts",
    "dev:debug": "nodemon --exec \"node --inspect --require ts-node/register src/index.ts\"",
    "start": "node dist/src/index.js",
    "test": "vitest run",
    "lint": "eslint src/ --ext .ts",
    "lint:fix": "yarn lint --fix"
  }
}

Key Commands

yarn dev

Database Configuration

The API supports multiple database connection methods:
apps/api/src/index.ts
function getDatabaseConnection() {
  if (process.env.DATABASE_URL) {
    return process.env.DATABASE_URL;
  }

  if (process.env.DATABASE_URL_INDEX) {
    return process.env[`DATABASE_URL_${process.env.DATABASE_URL_INDEX}`];
  }

  throw new Error('No valid database connection URL found.');
}

Using Docker

# Start MySQL on port 3306
docker compose up mysql

# Default connection from .env.example
DATABASE_URL=mysql://root:password@localhost:3306/sx_api

Checkpoint Configuration

Checkpoint is initialized with:
const checkpoint = new Checkpoint(schema, {
  logLevel: LogLevel.Info,
  resetOnConfigChange: true,
  pinoOptions,
  overridesConfig: overrides,
  dbConnection: getDatabaseConnection()
});

Schema Generation

Run yarn codegen to generate the GraphQL schema:
yarn codegen
This creates .checkpoint/schema.gql used by the UI and other clients.

Protocol Writers

Each protocol has dedicated “writers” that process blockchain events:
apps/api/src/evm/index.ts
function createWriters(config: EVMConfig) {
  let writers: Record<string, evm.Writer> = {};

  if (config.snapshotXConfig) {
    writers = applyProtocolPrefixToWriters(
      'snapshotX',
      createSnapshotXWriters(config, config.snapshotXConfig)
    );
  }

  if (config.governorBravoConfig) {
    writers = {
      ...writers,
      ...applyProtocolPrefixToWriters(
        'governorBravo',
        createGovernorBravoWriters(config, config.governorBravoConfig)
      )
    };
  }

  if (config.openZeppelinConfig) {
    writers = {
      ...writers,
      ...applyProtocolPrefixToWriters(
        'openZeppelin',
        createOpenZeppelinWriters(config, config.openZeppelinConfig)
      )
    };
  }

  return writers;
}

Dependencies

Key dependencies from package.json:
{
  "dependencies": {
    "@apollo/server": "^5.1.0",
    "@snapshot-labs/checkpoint": "^0.1.0-beta.67",
    "@snapshot-labs/sx": "^0.1.0",
    "express": "^4.21.2",
    "graphql": "^16.11.0",
    "starknet": "7.6.4",
    "viem": "^2.38.0",
    "pino": "^9.9.0",
    "dotenv": "^16.0.1"
  }
}

Logging

The API uses Pino for structured logging with optional Logtail integration:
import logger from './logger';

logger.info('Indexer started');
logger.error({ err }, 'Failed to process block');
logger.fatal({ err }, 'Uncaught exception');

Testing

yarn test
Tests use Vitest and require MySQL to be running.

Production Deployment

  1. Build the application:
    yarn build
    
  2. Set environment variables
  3. Start the service:
    yarn start
    
For high availability, run multiple API-only instances (IS_INDEXER=false) behind a load balancer, with a single indexer instance.

GraphQL Playground

Access the interactive GraphQL playground at:
http://localhost:3000/graphql

Using with Local Checkpoint

For development with a local Checkpoint instance:
cd checkpoint/node_modules/graphql
yarn link

cd sx-api
yarn link graphql
This ensures a single copy of the graphql package to avoid conflicts.

Build docs developers (and LLMs) love