Skip to content

Adding a provider

Adding a new provider is the single most impactful kind of contribution. Every supported exchange, brokerage, chain, and price source comes from this same pattern — one directory per provider under packages/clients/providers/src/providers/.

The provider directory layout (each is a self-contained adapter):

  • Directorypackages/clients/providers/src/providers/
    • Directorybinance/
    • Directorybitcoin/
    • Directorybitget/
    • Directorybitstamp/
    • Directorybybit/
    • Directorycoinbase/
    • Directorycoingecko/
    • Directorydefillama/
    • Directoryetherscan/
    • Directoryfinnhub/
    • Directoryfrankfurter/
    • Directorygate/
    • Directorygemini/
    • Directoryhuobi/
    • Directoryibkr/
    • Directorykraken/
    • Directorykucoin/
    • Directorymexc/
    • Directoryokx/
    • Directorysolana/
    • Directoryton/
    • Directorytron/
    • Directorywise/
    • Directoryyahoo-finance/
    • Directoryai-deepseek/
    • Directoryai-openai/
    • Directoryai-perplexity/

Each provider implements one or more capability interfaces:

CapabilityInterfaceWhat it does
PricingPriceProviderQuote current and historical prices for a token in a base currency.
BalancesBalanceProviderRead the current balance of an account / wallet for a list of tokens.
TransactionsTransactionProviderPage through historical transactions of an account / wallet.
Token identityTokenIdentityProviderEnrich a token row with provider-specific metadata.
AI inferenceAIProviderParse a screenshot / answer a structured question.
OAuthOAuthProviderDrive the OAuth handshake (Binance is the only one currently).

A provider can implement any subset. CoinGecko implements PriceProvider + TokenIdentityProvider. Etherscan implements BalanceProvider + TransactionProvider + TokenIdentityProvider. Kraken implements all four (price, balance, tx, identity).

  1. Open an issue first for non-trivial providers (anything with auth flows, anything that introduces new capability shapes, anything where the upstream is complicated). For a small pricing source with a key, just file the PR.

  2. Create the directory:

    Terminal window
    mkdir packages/clients/providers/src/providers/myprovider
    cd packages/clients/providers/src/providers/myprovider
  3. Implement the capability adapter. Copy from the closest existing provider that implements the same capability:

    • Pricing-only? Copy coingecko/ or frankfurter/.
    • Exchange (price + balance + tx)? Copy kraken/.
    • Blockchain RPC? Copy etherscan/ (EVM) or helius/ (Solana — actually under solana/).
    • AI? Copy ai-openai/.

    The directory typically contains:

    myprovider/
    index.ts # exports the @Service() class(es)
    MyProviderClient.ts # raw HTTP / SDK wrapper
    MyProviderPriceProvider.ts # if implementing PriceProvider
    types.ts # upstream payload shapes
  4. Register the provider. Each capability has a registry under packages/clients/providers/src/core/. The provider class @Service()-registers itself; routers that need a price / balance / tx provider for a given context look it up.

  5. Wire up env vars. If the provider needs an API key, declare it in the data-provider’s apps/backend/data-provider/.env.example and read it via packages/infra/config/’s helpers. The PricingService / BalanceService route automatically falls through providers that report “not configured”.

  6. Add an institution row. For exchanges / brokerages / chains, add a seed entry to the institutions catalogue migration so the UI shows the integration:

    • Migration in packages/infra/db/src/migrations/ adding an INSERT INTO institutions ....
    • For blockchains, also a row in institution_blockchain_mappings.
  7. Write tests. Two layers:

    • Unit tests stubbing the upstream HTTP client.
    • Integration tests with withTestDb if the provider writes to the schema (token identity providers do; pure pricing providers typically don’t).
  8. Document the env var in .env.example (root) and in Optional integration keys.

  9. Run the full checks:

    Terminal window
    bun run type-check
    bun lint:fix
    bun test --preload ./packages/business/domain/test-preload.ts \
    packages/ --timeout 30000
  10. Open the PR with a conventional-commit message:

    feat(providers): add MyProvider pricing adapter
@Service()
export class MyProviderPriceProvider implements PriceProvider {
private readonly client = Container.get(MyProviderClient);
async getCurrentPrice(
token: Token,
base: Token
): Promise<PriceResult | null> {
const data = await this.client.fetch(`/price/${token.symbol}/${base.symbol}`);
if (!data) return null;
return {
price: new Decimal(data.price),
timestamp: new Date(data.ts),
source: 'myprovider',
granularity: 'intraday',
};
}
async getHistoricalPrice(
token: Token,
base: Token,
at: Date
): Promise<PriceResult | null> {
// ...
}
}
@Service()
export class MyProviderTokenIdentityProvider implements TokenIdentityProvider {
private readonly client = Container.get(MyProviderClient);
async enrich(token: Token): Promise<Partial<TokenMetadata>> {
const id = await this.client.lookupBySymbol(token.symbol);
if (!id) return {};
return { myprovider: { id, symbol: token.symbol } };
}
}

The merged TokenMetadata is written back to tokens.providerMetadata. See Tokens & market segments.

  • A new exchange that just calls one undocumented endpoint, no retry handling, no rate-limit awareness. Get to “production shape” before submitting.
  • A pricing adapter that doesn’t implement historical prices. Historical prices are what the rollup needs; current-only providers are partial.
  • An AI provider that doesn’t have a clear use case (we have three; we don’t need a fourth without a reason).