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 recordsAll 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 identifiertitle,description,category-- market metadatayesPrice,noPrice-- current prices (asDecimal)volume,volume24h,liquidity-- trading metricsendDate-- market resolution datestartedAt-- when the market opened on the exchangeimageUrl-- 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
Marketshape