Building Custom Connectors
Step-by-step guide to creating a custom exchange connector for Barfinex — from project scaffolding to registration and deployment.
Overview
Barfinex uses a plugin architecture for exchange connectivity. Every exchange, broker, or data source is a separate npm package (libs/exchange-*) that implements a standard set of interfaces. The Provider service loads connectors dynamically at runtime using NestJS LazyModuleLoader — no changes to core code required.
This guide walks you through creating a connector from scratch, publishing it as an npm package, and registering it in your Barfinex instance.
What You Get
A custom connector gives your Barfinex instance:
- Real-time market data — trades, orderbook snapshots, candlesticks via WebSocket
- Account management — balances, positions, portfolio snapshots
- Order execution — open, close, modify orders through a unified API
- Instrument discovery — automatic listing of tradable pairs/symbols
- Full integration — your connector works with Detector strategies, Advisor LLM decisions, Inspector risk management, and Studio dashboard out of the box
Architecture
┌─────────────────────────────────────────────────┐ │ Provider (port 8081) │ │ │ │ ExchangeConnectorFactory │ │ └── LazyModuleLoader │ │ ├── @barfinex/exchange-binance │ │ ├── @barfinex/exchange-kraken │ │ ├── @barfinex/exchange-alpaca │ │ └── @your-org/exchange-myexchange ◄── │ Your connector │ │ │ ExchangeConnectorRegistry │ │ └── Map<"type:market", ExchangeConnectorBundle>│ │ │ │ ExchangeManagerService │ │ └── activate / deactivate / status / catalog │ └─────────────────────────────────────────────────┘
Each connector provides an ExchangeConnectorBundle consisting of:
| Component | Interface | Responsibility |
|---|---|---|
| Client Service | IExchangeClientService | Connection lifecycle, readiness check |
| WebSocket Manager | IExchangeWsManager | All streaming subscriptions |
| Account API | IExchangeAccountApi | Balances, positions, account info |
| Market API | IExchangeMarketApi | Instruments, prices |
| Order API | IExchangeOrderApi | Order execution |
| Adapters | ExchangeAdapterSet | Raw → normalized data transforms |
Prerequisites
- Node.js 18+
- TypeScript 5+
- Basic NestJS module knowledge
- Access to the exchange's API documentation
- Barfinex instance running (for testing)
Step 1. Scaffold the Project
Create a new library in the monorepo or as a standalone npm package:
# Option A: Inside the monorepo mkdir libs/exchange-myexchange cd libs/exchange-myexchange # Option B: Standalone npm package mkdir barfinex-exchange-myexchange cd barfinex-exchange-myexchange npm init -y
Recommended directory structure:
exchange-myexchange/ ├── src/ │ ├── index.ts # Public exports │ ├── exchange-myexchange.module.ts # NestJS module │ ├── client/ │ │ └── myexchange-client.service.ts # Connection management │ ├── ws/ │ │ └── myexchange-ws.manager.ts # WebSocket subscriptions │ ├── api/ │ │ ├── myexchange-account.api.ts # Account REST endpoints │ │ ├── myexchange-market.api.ts # Market REST endpoints │ │ └── myexchange-order.api.ts # Order execution │ ├── adapters/ │ │ ├── trade.adapter.ts # Raw → Trade transform │ │ ├── orderbook.adapter.ts # Raw → OrderBook transform │ │ ├── candle.adapter.ts # Raw → Candle transform │ │ ├── account.adapter.ts # Raw → Account/Balance transform │ │ ├── instruments.adapter.ts # Raw → Instrument transform │ │ └── instrument-prices.adapter.ts # Raw → Price transform │ └── utils/ │ └── timeframe.converter.ts # TimeFrame → exchange interval ├── package.json └── tsconfig.json
Install Dependencies
npm install @barfinex/types @nestjs/common rxjs npm install -D typescript @types/node
Step 2. Implement the Interfaces
2.1 Client Service
The client service manages the connection lifecycle:
// src/client/myexchange-client.service.ts
import { Injectable, Logger } from '@nestjs/common';
import type { IExchangeClientService } from '@barfinex/types';
import type { MarketType } from '@barfinex/types';
@Injectable()
export class MyExchangeClientService implements IExchangeClientService {
private readonly logger = new Logger(MyExchangeClientService.name);
private ready = false;
private credentials: Record<string, string> = {};
setCredentials(creds: Record<string, string>) {
this.credentials = creds;
}
async ensureReady(timeoutMs = 10000): Promise<void> {
if (this.ready) return;
// Initialize connection to the exchange
// e.g. authenticate, load instrument metadata
this.logger.log('Connecting to MyExchange...');
// Your initialization logic here:
// await this.authenticate(this.credentials);
// await this.loadInstruments();
this.ready = true;
this.logger.log('MyExchange connection ready');
}
validateExchangeInstruments(
marketType: MarketType,
symbols: string[],
): { validInstruments: string[]; removedInstruments: string[] } {
// Validate that requested symbols exist on the exchange
const valid = symbols.filter((s) => this.isValidSymbol(s, marketType));
const removed = symbols.filter((s) => !this.isValidSymbol(s, marketType));
return { validInstruments: valid, removedInstruments: removed };
}
private isValidSymbol(symbol: string, marketType: MarketType): boolean {
// Your validation logic
return true;
}
}
2.2 WebSocket Manager
The WebSocket manager handles all streaming subscriptions:
// src/ws/myexchange-ws.manager.ts
import { Injectable, Logger } from '@nestjs/common';
import type {
IExchangeWsManager,
MarketType,
Instrument,
TimeFrame,
} from '@barfinex/types';
@Injectable()
export class MyExchangeWsManager implements IExchangeWsManager {
private readonly logger = new Logger(MyExchangeWsManager.name);
async subscribeToTrade(
options: { marketType: MarketType; instruments: Instrument[] },
handler: (trade: any) => void,
): Promise<() => void> {
// Connect to exchange WebSocket trade stream
// Parse messages and call handler(normalizedTrade)
const symbols = options.instruments.map((i) => i.symbol);
this.logger.log(`Subscribing to trades: ${symbols.join(', ')}`);
// Your WebSocket subscription logic here
// const ws = new WebSocket(TRADE_STREAM_URL);
// ws.on('message', (msg) => handler(parseTradeMessage(msg)));
// Return unsubscribe function
return () => {
this.logger.log(`Unsubscribing from trades: ${symbols.join(', ')}`);
// ws.close();
};
}
async subscribeToOrderBook(
options: { marketType: MarketType; instruments: Instrument[] },
handler: any,
): Promise<() => void> {
// Similar to subscribeToTrade but for orderbook depth
return () => {};
}
async subscribeToCandles(
options: { marketType: MarketType; instruments: Instrument[]; interval: TimeFrame },
handler: any,
): Promise<() => void> {
// Subscribe to kline/candlestick stream
return () => {};
}
async subscribeToAccount(
options: { marketType: MarketType },
handler: any,
): Promise<() => void> {
// Subscribe to account updates (balance changes, position updates)
return () => {};
}
async subscribeToInstruments(
options: { marketType: MarketType },
handler: any,
): Promise<() => void> {
// Subscribe to instrument list changes (new pairs, delistings)
return () => {};
}
async subscribeToInstrumentPrices(
options: { marketType: MarketType; symbols?: string[] },
handler: any,
): Promise<() => void> {
// Subscribe to ticker/price updates
return () => {};
}
async destroy(): Promise<void> {
// Close all WebSocket connections
this.logger.log('Destroying all WebSocket connections');
}
}
2.3 Account API
// src/api/myexchange-account.api.ts
import { Injectable } from '@nestjs/common';
import type { IExchangeAccountApi, MarketType } from '@barfinex/types';
@Injectable()
export class MyExchangeAccountApi implements IExchangeAccountApi {
async getAssetsInfo(connectorType: string, marketType: MarketType) {
// Fetch balances and positions from exchange REST API
return {
assets: [
// { currency: 'USDT', free: 1000, locked: 0, total: 1000 }
],
positions: [],
};
}
async getAccountInfo(connectorType: string, marketType: MarketType) {
// Fetch account metadata (permissions, fee tier, etc.)
return {
id: 'my-account-id',
canTrade: true,
canWithdraw: true,
};
}
}
2.4 Market API
// src/api/myexchange-market.api.ts
import { Injectable } from '@nestjs/common';
import type { IExchangeMarketApi, MarketType } from '@barfinex/types';
@Injectable()
export class MyExchangeMarketApi implements IExchangeMarketApi {
async getInstrumentsInfo(connectorType: string, marketType: MarketType) {
// Return all tradable instruments for this market type
return [
// { symbol: 'BTCUSDT', baseAsset: 'BTC', quoteAsset: 'USDT', ... }
];
}
async getPrices(connectorType: string, marketType: MarketType) {
// Return current prices for all instruments
return {
// BTCUSDT: { value: 65000, moment: Date.now() }
};
}
}
2.5 Order API
// src/api/myexchange-order.api.ts
import { Injectable } from '@nestjs/common';
import type { IExchangeOrderApi } from '@barfinex/types';
@Injectable()
export class MyExchangeOrderApi implements IExchangeOrderApi {
async openOrder(params: any) {
// Submit order to exchange
// Return normalized Order object
}
async closeOrder(params: any) {
// Close specific order
}
async closeAllOrders(params: any) {
// Close all open orders
}
async getOpenOrders(params: any) {
// Return list of open orders
return [];
}
}
2.6 Data Adapters
Adapters transform raw exchange data into Barfinex's normalized format:
// src/adapters/trade.adapter.ts
export function createTradeAdapter(handler: any) {
return (marketType: string) => (rawMessage: any) => {
// Transform exchange-specific trade message into standard format:
handler({
symbol: rawMessage.s, // e.g. 'BTCUSDT'
price: parseFloat(rawMessage.p),
quantity: parseFloat(rawMessage.q),
timestamp: rawMessage.T,
side: rawMessage.m ? 'sell' : 'buy',
tradeId: rawMessage.t?.toString(),
});
};
}
// src/adapters/orderbook.adapter.ts
export function createOrderBookAdapter(handler: any) {
return (marketType: string) => (rawMessage: any) => {
handler({
symbol: rawMessage.s,
bids: rawMessage.bids?.map(([p, q]: string[]) => ({
price: parseFloat(p),
quantity: parseFloat(q),
})),
asks: rawMessage.asks?.map(([p, q]: string[]) => ({
price: parseFloat(p),
quantity: parseFloat(q),
})),
timestamp: rawMessage.T || Date.now(),
});
};
}
// src/adapters/candle.adapter.ts
export function createCandleAdapter(options: any) {
return (rawMessage: any) => {
options.handler({
symbol: rawMessage.s,
open: parseFloat(rawMessage.o),
high: parseFloat(rawMessage.h),
low: parseFloat(rawMessage.l),
close: parseFloat(rawMessage.c),
volume: parseFloat(rawMessage.v),
timestamp: rawMessage.t,
interval: options.interval,
});
};
}
// src/adapters/account.adapter.ts
export function createAccountAdapter(options: any, handler: any) {
return (marketType: string) => (rawMessage: any) => {
handler({
assets: rawMessage.balances?.map((b: any) => ({
currency: b.asset,
free: parseFloat(b.free),
locked: parseFloat(b.locked),
total: parseFloat(b.free) + parseFloat(b.locked),
})),
});
};
}
// src/adapters/instruments.adapter.ts
export function createInstrumentsAdapter(options: any) {
return (rawMessage: any) => {
// Transform instrument list updates
};
}
// src/adapters/instrument-prices.adapter.ts
export function createInstrumentPricesAdapter(logger: any, handler: any) {
return (marketType: string) => (rawMessage: any) => {
handler({
symbol: rawMessage.s,
price: parseFloat(rawMessage.c),
timestamp: rawMessage.E,
});
};
}
2.7 Timeframe Converter
Maps Barfinex's standard TimeFrame enum to exchange-specific interval strings:
// src/utils/timeframe.converter.ts
import { TimeFrame } from '@barfinex/types';
const TIMEFRAME_MAP: Record<string, string> = {
[TimeFrame['1m']]: '1m',
[TimeFrame['5m']]: '5m',
[TimeFrame['15m']]: '15m',
[TimeFrame['1h']]: '1h',
[TimeFrame['4h']]: '4h',
[TimeFrame['1d']]: '1d',
[TimeFrame['1w']]: '1w',
};
export function convertTimeFrame(tf: TimeFrame): string {
return TIMEFRAME_MAP[tf] ?? '1m';
}
Step 3. Create the NestJS Module
// src/exchange-myexchange.module.ts
import { Module } from '@nestjs/common';
import { MyExchangeClientService } from './client/myexchange-client.service';
import { MyExchangeWsManager } from './ws/myexchange-ws.manager';
import { MyExchangeAccountApi } from './api/myexchange-account.api';
import { MyExchangeMarketApi } from './api/myexchange-market.api';
import { MyExchangeOrderApi } from './api/myexchange-order.api';
@Module({
providers: [
MyExchangeClientService,
MyExchangeWsManager,
MyExchangeAccountApi,
MyExchangeMarketApi,
MyExchangeOrderApi,
],
exports: [
MyExchangeClientService,
MyExchangeWsManager,
MyExchangeAccountApi,
MyExchangeMarketApi,
MyExchangeOrderApi,
],
})
export class ExchangeMyExchangeModule {}
Step 4. Create the Public Entry Point
Export everything the factory needs to resolve your connector:
// src/index.ts
export { ExchangeMyExchangeModule } from './exchange-myexchange.module';
export { MyExchangeClientService } from './client/myexchange-client.service';
export { MyExchangeWsManager } from './ws/myexchange-ws.manager';
export { MyExchangeAccountApi } from './api/myexchange-account.api';
export { MyExchangeMarketApi } from './api/myexchange-market.api';
export { MyExchangeOrderApi } from './api/myexchange-order.api';
export { createTradeAdapter } from './adapters/trade.adapter';
export { createOrderBookAdapter } from './adapters/orderbook.adapter';
export { createCandleAdapter } from './adapters/candle.adapter';
export { createAccountAdapter } from './adapters/account.adapter';
export { createInstrumentsAdapter } from './adapters/instruments.adapter';
export { createInstrumentPricesAdapter } from './adapters/instrument-prices.adapter';
export { convertTimeFrame } from './utils/timeframe.converter';
Step 5. Add ConnectorType
Add your exchange to the ConnectorType enum in libs/types/src/connector.interface.ts:
export enum ConnectorType {
// ...existing types...
myexchange = 'myexchange',
}
Step 6. Register in the Factory
Add a loader entry in apps/provider/src/connector/exchange-manager/exchange-connector.factory.ts:
[ConnectorType.myexchange]: async () => {
const mod = await import('@your-org/exchange-myexchange');
return {
module: mod.ExchangeMyExchangeModule,
tokens: {
client: mod.MyExchangeClientService,
ws: mod.MyExchangeWsManager,
accountApi: mod.MyExchangeAccountApi,
marketApi: mod.MyExchangeMarketApi,
orderApi: mod.MyExchangeOrderApi,
},
adapters: {
createTradeAdapter: mod.createTradeAdapter,
createOrderBookAdapter: mod.createOrderBookAdapter,
createCandleAdapter: mod.createCandleAdapter,
createAccountAdapter: mod.createAccountAdapter,
createInstrumentsAdapter: mod.createInstrumentsAdapter,
createInstrumentPricesAdapter: mod.createInstrumentPricesAdapter,
convertTimeFrame: mod.convertTimeFrame,
},
};
},
Step 7. Add to the Exchange Catalog
Register your exchange in libs/types/src/exchange-catalog.interface.ts:
{
connectorType: ConnectorType.myexchange,
displayName: 'My Exchange',
segment: ExchangeSegment.CRYPTO_MAJOR, // or appropriate segment
supportedMarkets: [MarketType.spot, MarketType.futures],
credentials: [
{ key: 'apiKey', label: 'API Key', type: 'text', required: true },
{ key: 'apiSecret', label: 'API Secret', type: 'password', required: true },
],
website: 'https://www.myexchange.com',
description: 'Description of your exchange',
maturity: ExchangeMaturity.BETA,
features: {
spotTrading: true,
futuresTrading: true,
marginTrading: false,
orderbookStreaming: true,
tradeStreaming: true,
candleStreaming: true,
historicalData: true,
orderExecution: true,
testnetAvailable: false,
},
},
Step 8. Publish & Install
As an npm package (recommended for external contributions):
# Build npm run build # Publish npm publish --access public
Then in your Barfinex instance:
npm install @your-org/exchange-myexchange
As a monorepo library:
Add the library to nest-cli.json:
{
"projects": {
"exchange-myexchange": {
"type": "library",
"root": "libs/exchange-myexchange",
"entryFile": "index",
"sourceRoot": "libs/exchange-myexchange/src"
}
}
}
Step 9. Test Your Connector
Unit tests
Test each adapter independently:
import { createTradeAdapter } from '../src/adapters/trade.adapter';
describe('Trade Adapter', () => {
it('should normalize a raw trade message', () => {
const handler = jest.fn();
const adapter = createTradeAdapter(handler)('spot');
adapter({
s: 'BTCUSDT',
p: '65000.50',
q: '0.001',
T: 1711800000000,
m: false,
t: 12345,
});
expect(handler).toHaveBeenCalledWith({
symbol: 'BTCUSDT',
price: 65000.50,
quantity: 0.001,
timestamp: 1711800000000,
side: 'buy',
tradeId: '12345',
});
});
});
Integration test with Provider
# Start Provider
npm run start:dev -- provider
# Activate your connector via API
curl -X POST http://localhost:8081/api/exchanges/activate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"connectorType": "myexchange",
"marketTypes": ["spot"],
"credentials": {
"apiKey": "YOUR_KEY",
"apiSecret": "YOUR_SECRET"
}
}'
# Check status
curl http://localhost:8081/api/exchanges/active \
-H "Authorization: Bearer YOUR_TOKEN"
Reference: Full Interface Contracts
IExchangeClientService
| Method | Returns | Description |
|---|---|---|
ensureReady(timeoutMs?) | Promise<void> | Initialize connection, throw on timeout |
validateExchangeInstruments(marketType, symbols) | { validInstruments, removedInstruments } | Validate symbol list |
IExchangeWsManager
| Method | Returns | Description |
|---|---|---|
subscribeToTrade(options, handler) | Promise<() => void> | Stream trades, return unsubscribe fn |
subscribeToOrderBook(options, handler) | Promise<() => void> | Stream orderbook updates |
subscribeToCandles(options, handler) | Promise<() => void> | Stream candlesticks |
subscribeToAccount(options, handler) | Promise<() => void> | Stream balance/position updates |
subscribeToInstruments(options, handler) | Promise<() => void> | Stream instrument changes |
subscribeToInstrumentPrices(options, handler) | Promise<() => void> | Stream ticker prices |
destroy() | Promise<void> | Clean up all connections |
ExchangeAdapterSet
| Function | Signature |
|---|---|
createTradeAdapter | (handler) => (marketType) => (msg) => void |
createOrderBookAdapter | (handler) => (marketType) => (msg) => void |
createCandleAdapter | (options) => (msg) => void |
createAccountAdapter | (options, handler) => (marketType) => (msg) => void |
createInstrumentsAdapter | (options) => (msg) => void |
createInstrumentPricesAdapter | (logger, handler) => (marketType) => (msg) => void |
convertTimeFrame | (tf: TimeFrame) => string | number |
Available Exchange Segments
| Segment | Use for |
|---|---|
CRYPTO_MAJOR | Top-tier crypto exchanges (Binance, Kraken, Coinbase) |
CRYPTO_DERIVATIVES | Crypto derivatives platforms (Deribit, Bitget) |
CRYPTO_OTHER | Smaller crypto exchanges |
US_BROKER | US stock brokers (Alpaca, Interactive Brokers) |
EU_BROKER | European brokers (eToro, DEGIRO) |
APAC_BROKER | Asia-Pacific brokers (Tiger Brokers, Futu) |
CIS_BROKER | CIS region brokers (Tinkoff) |
KZ_BROKER | Kazakhstan brokers (KASE, Freedom Finance) |
FOREX | Forex brokers (OANDA, Pepperstone) |
FUTURES_DERIVATIVES | Traditional futures (CME, ICE) |
OTC_INSTITUTIONAL | OTC/Institutional desks (Wintermute, Cumberland) |
MARKET_DATA | Data-only providers (Polygon.io, Tiingo) |
DEX | Decentralized exchanges (Uniswap, SushiSwap) |
Tips & Best Practices
- Start with data-only — implement trades and orderbook streaming first, add order execution later
- Use testnet — if the exchange offers a testnet/sandbox environment, use it during development
- Handle reconnection — WebSocket connections will drop; implement automatic reconnection with exponential backoff
- Normalize symbols — map exchange-specific symbol formats (e.g.,
BTC_USDT) to Barfinex standard (BTCUSDT) - Rate limiting — respect exchange API rate limits; implement request queuing if needed
- Error mapping — map exchange-specific error codes to meaningful error messages
- Logging — use NestJS
Loggerfor structured logging that integrates with Provider's log pipeline
Next Steps
- Provider API Reference — full REST API documentation
- Architecture Overview — how services interact
- Detector Rule Engine — build strategies that use your connector's data
- Studio Features — see your connector in the dashboard