DexScreener
Nexgent integrates with DexScreener as an alternative price feed provider. DexScreener provides token prices, liquidity data, and trading pair information.
Overview
| Feature | Description |
|---|---|
| Token Prices | Price in SOL and USD |
| Liquidity Data | Pool liquidity for pairs |
| Multi-token Batch | Up to 30 tokens per request |
| Rug Detection | Identify low-liquidity tokens |
Configuration
# Select price provider (default: jupiter)
PRICE_PROVIDER=dexscreenerThe price provider can be switched at runtime:
const priceFeedService = new PriceFeedService();
priceFeedService.setProvider(new DexScreenerPriceProvider());Price Provider
Single Token Price
// infrastructure/external/dexscreener/dexscreener-price-provider.ts
async getTokenPrice(tokenAddress: string): Promise<TokenPrice> {
// Rate limit check
await this.waitForRateLimit();
const url = `https://api.dexscreener.com/tokens/v1/solana/${tokenAddress}`;
const response = await fetch(url);
const data = await response.json();
// DexScreener returns array of pairs
if (!Array.isArray(data) || data.length === 0) {
throw new PriceFeedServiceError('No price data found', 'TOKEN_NOT_FOUND');
}
// Parse to get best SOL pair
const tokenPrice = this.parseTokenPrice(data, tokenAddress);
if (!tokenPrice) {
throw new PriceFeedServiceError('No SOL pair found', 'NO_SOL_PAIR');
}
return tokenPrice;
}Batch Token Prices
async getMultipleTokenPrices(tokenAddresses: string[]): Promise<TokenPrice[]> {
const results: TokenPrice[] = [];
const batchSize = 30; // Max addresses per request
for (let i = 0; i < tokenAddresses.length; i += batchSize) {
const batch = tokenAddresses.slice(i, i + batchSize);
const addressesString = batch.join(',');
await this.waitForRateLimit();
const url = `https://api.dexscreener.com/tokens/v1/solana/${addressesString}`;
const response = await fetch(url);
const data = await response.json();
// Group pairs by base token
const pairsByToken = new Map<string, DexScreenerPair[]>();
for (const pair of data) {
const baseAddress = pair.baseToken.address.toLowerCase();
if (!pairsByToken.has(baseAddress)) {
pairsByToken.set(baseAddress, []);
}
pairsByToken.get(baseAddress)!.push(pair);
}
// Parse best price for each token
for (const address of batch) {
const pairs = pairsByToken.get(address.toLowerCase());
if (pairs?.length > 0) {
const price = this.parseTokenPrice(pairs, address);
if (price) results.push(price);
}
}
}
return results;
}Pair Selection
DexScreener returns all trading pairs for a token. We select the best SOL pair:
private parseTokenPrice(pairs: DexScreenerPair[], tokenAddress: string): TokenPrice | null {
// Filter SOL pairs where our token is the base token
const solPairs = pairs.filter(
(pair) =>
pair.quoteToken.address.toLowerCase() === SOL_MINT_ADDRESS.toLowerCase() &&
pair.baseToken.address.toLowerCase() === tokenAddress.toLowerCase()
);
if (solPairs.length === 0) {
return null;
}
// Select pair with highest liquidity
const bestPair = solPairs.reduce((best, current) => {
const bestLiquidity = best.liquidity?.usd || 0;
const currentLiquidity = current.liquidity?.usd || 0;
return currentLiquidity > bestLiquidity ? current : best;
});
return {
tokenAddress: tokenAddress.toLowerCase(),
priceSol: parseFloat(bestPair.priceNative),
priceUsd: parseFloat(bestPair.priceUsd),
liquidity: bestPair.liquidity?.usd || 0,
priceChange24h: bestPair.priceChange?.h24 || 0,
lastUpdated: new Date(),
pairAddress: bestPair.pairAddress,
};
}Rate Limiting
DexScreener has a rate limit of 300 requests per minute. We use a token bucket algorithm:
const RATE_LIMIT_REQUESTS = 300;
const RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute
interface TokenBucket {
tokens: number;
lastRefill: number;
}
private rateLimitBucket: TokenBucket = {
tokens: RATE_LIMIT_REQUESTS,
lastRefill: Date.now(),
};
private refillBucket(): void {
const now = Date.now();
const elapsed = now - this.rateLimitBucket.lastRefill;
const tokensToAdd = Math.floor(
(elapsed / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_REQUESTS
);
if (tokensToAdd > 0) {
this.rateLimitBucket.tokens = Math.min(
RATE_LIMIT_REQUESTS,
this.rateLimitBucket.tokens + tokensToAdd
);
this.rateLimitBucket.lastRefill = now;
}
}
private async waitForRateLimit(): Promise<void> {
while (!this.checkRateLimit()) {
await new Promise(resolve => setTimeout(resolve, 10));
this.refillBucket();
}
}Liquidity Check Service
The LiquidityCheckService detects rug pulls by checking liquidity:
// infrastructure/external/dexscreener/liquidity-check-service.ts
interface LiquidityCheckResult {
tokenAddress: string;
hasLiquidity: boolean;
liquiditySol: number;
liquidityUsd: number;
hasPairs: boolean;
pairCount: number;
isRugPulled: boolean;
error?: string;
}
async checkLiquidity(tokenAddress: string): Promise<LiquidityCheckResult> {
const url = `https://api.dexscreener.com/tokens/v1/solana/${tokenAddress}`;
const response = await fetch(url);
const pairs = await response.json();
// Filter SOL pairs
const solPairs = pairs.filter(
p => p.quoteToken.address.toLowerCase() === SOL_MINT_ADDRESS.toLowerCase()
);
// Sum liquidity across all SOL pairs
const totalLiquiditySol = solPairs.reduce(
(sum, pair) => sum + (pair.liquidity?.quote || 0),
0
);
const totalLiquidityUsd = solPairs.reduce(
(sum, pair) => sum + (pair.liquidity?.usd || 0),
0
);
// Token is considered rug pulled if SOL liquidity < 10 SOL
const isRugPulled = solPairs.length > 0 && totalLiquiditySol < 10;
return {
tokenAddress,
hasLiquidity: totalLiquiditySol >= 10,
liquiditySol: totalLiquiditySol,
liquidityUsd: totalLiquidityUsd,
hasPairs: pairs.length > 0,
pairCount: solPairs.length,
isRugPulled,
};
}Rug Pull Handling
When a token is detected as rug pulled, positions are automatically closed:
// In PriceUpdateManager
for (const result of liquidityResults) {
if (result.isRugPulled) {
logger.error({
tokenAddress: result.tokenAddress,
liquiditySol: result.liquiditySol,
}, 'Token identified as rug pulled');
// Create burn transactions for positions
await liquidityCheckService.createBurnTransactionsForRugPulledToken(
result.tokenAddress
);
}
}API Reference
DexScreener Response
interface DexScreenerPair {
chainId: string;
dexId: string;
pairAddress: string;
baseToken: {
address: string;
name: string;
symbol: string;
};
quoteToken: {
address: string;
name: string;
symbol: string;
};
priceNative: string; // Price in quote token (SOL)
priceUsd: string; // Price in USD
liquidity: {
usd?: number;
base?: number; // Token liquidity
quote?: number; // SOL liquidity
};
priceChange: {
h24?: number; // 24h change %
h6?: number;
h1?: number;
m5?: number;
};
volume: {
h24?: number;
h6?: number;
h1?: number;
m5?: number;
};
txns: {
h24?: { buys: number; sells: number };
};
}Endpoints
| Endpoint | Method | Description |
|---|---|---|
/tokens/v1/solana/{address} | GET | Single token pairs |
/tokens/v1/solana/{addr1,addr2} | GET | Multiple tokens (max 30) |
Jupiter vs DexScreener
| Feature | Jupiter | DexScreener |
|---|---|---|
| Price Source | Aggregated from swaps | Trading pairs |
| Liquidity Data | No | Yes |
| Pair Info | No | Yes |
| Rate Limit | Higher (with API key) | 300/min |
| Default | Yes | No |
💡
Jupiter is the default price provider because it provides more accurate execution prices. DexScreener is used for liquidity checks and as a fallback.