A market connector is any plugin that implements MarketAdapter from
@pairlens/market-engine. It owns its exchange connections, candle buffers,
and order execution.
The interface
import type { MarketAdapter } from '@pairlens/market-engine/adapter'
export interface MarketAdapter {
getInfo: () => MarketAdapterInfo
// Streaming. Each returns its own unsubscribe function.
subscribeCandles: (
pair: string,
timeframe: string,
country: string,
cb: CandleCallback,
) => () => void
subscribeTicker: (
pair: string,
country: string,
cb: TickerCallback,
) => () => void
subscribeOrderbook: (
pair: string,
country: string,
cb: OrderbookCallback,
) => () => void
// One-shot data
fetchHistoricalCandles: (
pair: string,
timeframe: string,
limit: number,
country: string,
) => Promise<Array<Candle>>
fetchTicker: (pair: string, country: string) => Promise<TickerSnapshot>
getInstruments: (filter?: InstrumentFilter) => Promise<Array<Instrument>>
// Trading, only when capabilities include 'trade'
placeOrder?: (params: OrderParams) => Promise<OrderResult>
cancelOrder?: (orderId: string) => Promise<OrderResult>
setCredentials?: (credentials: Record<string, string>) => void
destroy: () => void
}
Note the country parameter threaded through every data call. It is how
regional endpoint routing works, and ignoring it is the most common reason a
connector fails for users outside your own region.
Describing yourself
getInfo() returns the metadata the terminal uses to decide what to show and
what to allow:
type MarketAdapterInfo = {
marketId: string
displayName: string
assetClasses: Array<
'crypto-spot' | 'crypto-perp' | 'stocks' | 'prediction' | 'dex'
>
capabilities: Array<'read' | 'trade'>
credentialSchema: Array<CredentialField>
supportedTimeframes: Array<string>
iconUrl?: string
walletChain?: 'solana' | 'ethereum' | 'bitcoin'
/** DEX venues only: the connector supports resting limit orders. */
dexLimitOrders?: boolean
/** Exchange-native trigger (TP/SL) orders via OrderParams.trigger. */
triggerOrders?: boolean
/**
* How a market order reaches the book: 'native' (the venue accepts a
* priceless order) or 'none' (every order carries a price). Absent means
* the CEX default, native.
*/
marketOrders?: 'none' | 'native'
/**
* The venue rests limit orders only, so the ticket hides the market and
* limit toggle instead of letting the submit be rejected. Derived from
* marketOrders: 'none' when it is not declared.
*/
limitOnly?: boolean
}
credentialSchema drives the connect wizard’s form, so declare exactly the
fields your venue needs (an API key and secret, plus a passphrase where the
venue uses one) and mark secrets with type: 'secret'.
Note
triggerOrders is load-bearing. Declare it only if your venue really rests
native stop and take-profit orders. Workflow steps check this flag, and a
stop-loss is refused rather than faked on a venue without it, which is the
behaviour that keeps a trader from believing they are protected when they are
not.
Conformance
A shared harness in packages/plugins/src/test-utils/conformance.ts checks that
your adapter emits well-formed candles, tickers, and books, that reconnection
and backfill behave, and that order placement and cancellation return the
shapes the terminal expects. Run it against your connector before publishing:
bun test packages/plugins
There are separate suites for golden-file parsing, order conformance, and private WebSocket behaviour. A connector that passes all three behaves correctly in the terminal without you having to click through it.
Tip
Fourteen CEX connectors are built from one shared factory,
createCexConnectorPlugin. If your venue looks like a standard CEX, start
from that factory instead of the raw interface. It handles the parts that are
the same everywhere.
The two prediction-market connectors run on a separate factory of the same shape. An event contract has no base and quote asset and its price is a probability, so bending the spot factory around it would have cost both. If your venue prices outcomes rather than assets, that is the one to copy.
Credentials at runtime
Adapters receive credentials through setCredentials at the moment they are
needed for order routing. They never read them from disk or from a server. The
terminal hands them over straight from the OS keychain on desktop, or from the
encrypted credential vault in a browser.
Never log a credential, never include one in an error message, and never send one to a host outside your declared allowlist. All three are things a reviewer will look for.
Transport notes
WebSockets. Use port 443. Non-standard ports get blocked by corporate firewalls, and the failure looks like “the app is broken” rather than “your network blocked it”.
REST on desktop. Absolute-URL REST calls route through the native HTTP transport, which sidesteps CORS entirely. In the browser dev build they go through the Vite dev proxy. Write the calls once; the host picks the path.
Browsers and CORS. The hosted web terminal calls your REST API from the
page itself, so it only works if the venue serves CORS headers. If it does
not, declare requiresDesktop: true and the connector refuses cleanly in a
browser with a typed error instead of presenting a dead chart.
Declared hosts. Every host your connector touches must be in your manifest’s declared hosts, or the desktop CSP will block it. That includes any fallback or regional endpoint.
