VeztaVezta
Guides

Adding a Market Source

How to build a new exchange connector

Vezta currently aggregates markets from Polymarket and Kalshi. This guide walks through adding a new prediction market exchange as a data source.

Architecture Overview

Market data flows through a three-layer pipeline:

Exchange API → Connector (fetch raw data) → Normalizer → Prisma Market records

All connectors live in src/ingester/ and are registered in IngesterModule. The existing connectors -- PolymarketConnector, KalshiConnector, and PolymarketWalletConnector -- serve as reference implementations.

Connector Interface

Every connector must implement methods for fetching markets and order books. The interface is defined in src/ingester/exchange-connector.interface.ts:

export interface ExchangeConnector {
  name: string;
  fetchMarkets(cursor?: string): Promise<unknown[]>;
  fetchOrderBook(
    marketId: string,
  ): Promise<{ bids?: OrderBookLevel[]; asks?: OrderBookLevel[] }>;
}

Where OrderBookLevel is:

export interface OrderBookLevel {
  price: string;
  size: string;
}

Implementation Steps

Define Raw Types

Create a types file for the exchange's raw API response shapes in src/ingester/types/. Each exchange has different field names, casing conventions, and data formats.

// src/ingester/types/newexchange.types.ts
export interface NewExchangeRawMarket {
  id: string;
  title: string;
  category: string;
  yes_price: string;
  no_price: string;
  volume: string;
  close_date: string;
  // ... exchange-specific fields
}

Reference src/ingester/exchange-connector.interface.ts for the Polymarket and Kalshi raw types as examples.

Create the Connector

Build a connector class in src/ingester/ that fetches data from the exchange's API. Use the @Injectable() decorator for NestJS dependency injection.

// src/ingester/newexchange.connector.ts
import { Injectable, Logger } from '@nestjs/common';

@Injectable()
export class NewExchangeConnector {
  private readonly logger = new Logger(NewExchangeConnector.name);
  private readonly baseUrl = 'https://api.newexchange.com';

  async fetchMarkets(cursor?: string): Promise<NewExchangeRawMarket[]> {
    // Fetch and return raw market data
  }

  async fetchOrderBook(marketId: string) {
    // Fetch and return order book levels
  }

  async fetchPrices(marketIds: string[]) {
    // Fetch current prices for active markets
  }
}

Follow the patterns in polymarket.connector.ts:

  • Use a fetch timeout (e.g., 10 seconds)
  • Return empty arrays on failure instead of throwing
  • Log warnings for non-200 responses
  • Handle pagination if the exchange API requires it

Normalize to the Unified Model

The NormalizerService (in the market module) maps raw exchange data to Prisma Market records. Add a normalization method for your exchange that maps exchange-specific fields to the unified schema:

  • source -- exchange identifier (e.g., 'polymarket', 'kalshi', 'newexchange')
  • externalId -- the exchange's unique market identifier
  • title, description, category -- market metadata
  • yesPrice, noPrice -- current prices (as Decimal)
  • volume, volume24h, liquidity -- trading metrics
  • endDate -- market resolution date
  • startedAt -- when the market opened on the exchange
  • imageUrl -- market thumbnail

Register in IngesterModule

Add the connector to src/ingester/ingester.module.ts:

@Module({
  providers: [
    PolymarketConnector,
    KalshiConnector,
    PolymarketWalletConnector,
    NewExchangeConnector,  // Add here
  ],
  exports: [
    PolymarketConnector,
    KalshiConnector,
    PolymarketWalletConnector,
    NewExchangeConnector,  // And here
  ],
})
export class IngesterModule {}

Add Sync Jobs

Create BullMQ jobs in the market module for the new exchange. The existing sync jobs run on the market-sync queue:

  • Full sync -- fetch all markets (every 5 minutes)
  • Price sync -- fetch current prices for active markets (every 60 seconds)
  • Orderbook sync -- fetch order books for popular markets (every 60 seconds)

Follow the pattern in MarketSyncScheduler: implement OnModuleInit, clean old repeatable jobs, then register new ones.

Add Trading Adapter (Optional)

If the exchange supports order placement, create a trading adapter in src/modules/trading/adapters/ that implements order submission and cancellation. The SmartRouterService selects the appropriate adapter based on the market's source.

Always cap event/market fetching to avoid overloading the shared VM. The Kalshi connector caps at 200 events -- apply similar limits for any new exchange. Uncapped fetching on the 8 GiB VM causes memory exhaustion.

Testing

Write unit tests for the connector in src/ingester/newexchange.connector.spec.ts. Use MSW v2 to mock external API calls. Reference polymarket.connector.spec.ts and kalshi.connector.spec.ts for patterns:

  • Mock successful responses with realistic data
  • Test error handling (network failures, non-200 status codes)
  • Test pagination logic
  • Verify the normalized output matches the expected Market shape

On this page