Skip to content

Writing a Custom Provider

Community providers plug into DeFiRuntime through ProtocolProvider interfaces (SwapProvider, YieldProvider, or BridgeProvider).

Example: swap provider implementing SwapProvider:

import type {
ProviderCapability,
Quote,
QuoteRequest,
SwapProvider,
UnsignedTransaction,
} from '@stendar/core';
export class MySwapProvider implements SwapProvider {
readonly id = 'my-swap';
readonly chains = ['ethereum-mainnet'];
readonly capabilities: ProviderCapability[] = ['swap'];
async getQuote(request: QuoteRequest): Promise<Quote> {
// Call upstream API and map to runtime quote format.
return {
provider: this.id,
inputToken: request.inputToken,
outputToken: request.outputToken,
inAmount: request.amount,
outAmount: request.amount,
priceImpactPct: 0,
};
}
async buildSwapTransaction(quote: Quote, userAddress: string): Promise<UnsignedTransaction> {
// Return chain-specific unsigned tx payload.
return {
chainType: 'evm',
chain: 'ethereum-mainnet',
to: '0x0000000000000000000000000000000000000000',
data: '0x',
value: 0n,
meta: { userAddress, quoteProvider: quote.provider },
};
}
}

Bridge integrations follow the same pattern with BridgeProvider:

import type {
BridgeProvider,
BridgeQuote,
BridgeQuoteRequest,
BridgeResultV2,
BridgeStatus,
ProviderCapability,
UnsignedTransaction,
WalletAdapter,
} from '@stendar/core';
export class MyBridgeProvider implements BridgeProvider {
readonly id = 'my-bridge';
readonly chains = ['ethereum-mainnet', 'solana-mainnet'];
readonly capabilities: ProviderCapability[] = ['bridge'];
async getBridgeQuote(request: BridgeQuoteRequest): Promise<BridgeQuote> {
// Map upstream bridge quote into Stendar's BridgeQuote format.
return {} as BridgeQuote;
}
async executeBridge(_quote: BridgeQuote, _wallet: WalletAdapter): Promise<BridgeResultV2> {
return { bridgeId: 'bridge-123', status: 'pending' as BridgeStatus };
}
async getBridgeStatus(_bridgeId: string): Promise<BridgeStatus> {
return 'pending';
}
async buildBridgeTransaction(_quote: BridgeQuote, _userAddress: string): Promise<UnsignedTransaction> {
// Return chain-specific unsigned transaction payload.
return {
chainType: 'evm',
chain: 'ethereum-mainnet',
to: '0x0000000000000000000000000000000000000000',
data: '0x',
value: 0n,
};
}
}
import { validatePlugin, type PluginManifest } from '@stendar/core';
const manifest: PluginManifest = {
id: 'my-swap',
name: 'My Swap Provider',
version: '1.0.0',
type: 'swap',
chains: ['ethereum-mainnet'],
author: 'community',
description: 'Example community swap provider',
};
const provider = new MySwapProvider();
const validation = validatePlugin(manifest, provider);
if (!validation.valid) {
throw new Error(validation.errors.join('; '));
}
import { ConsoleLogger, DeFiRuntime, PluginRegistry } from '@stendar/core';
const logger = new ConsoleLogger({ component: 'plugin' });
const registry = new PluginRegistry(logger);
registry.registerPlugin(manifest, provider);
const runtime = new DeFiRuntime({
chains,
providers: [provider],
wallet,
});

If you only need local wiring, you can skip registerPlugin() and pass provider instances directly in runtime config.