Skip to content

Instantly share code, notes, and snippets.

@sarvagnakadiya
Created September 17, 2025 16:58
Show Gist options
  • Select an option

  • Save sarvagnakadiya/e28978b2fb55818b2f3c36e1271c7a21 to your computer and use it in GitHub Desktop.

Select an option

Save sarvagnakadiya/e28978b2fb55818b2f3c36e1271c7a21 to your computer and use it in GitHub Desktop.
import axios from "axios";
import { Coin, GeckoTokenFetchResponse } from "../interfaces/coingecko";
import { getCoingeckoApiKey, COINGECKO_BASE_URL, ETH_ADDRESS } from "../config";
import { GeckoTerminalResponse } from "../interfaces/coingecko";
import { getCurrentNetworkConfig } from "../config/networks";
/**
* Validate Coingecko API key
*/
function validateCoingeckoApiKey() {
const apiKey = getCoingeckoApiKey();
if (!apiKey) {
throw new Error(
"Coingecko API key is not set. Please set it during SDK initialization.",
);
}
return apiKey;
}
/**
* Calculate Levenshtein distance between two strings
*/
export function levenshteinDistance(a: string, b: string): number {
const dp = Array.from({ length: a.length + 1 }, () =>
Array(b.length + 1).fill(0),
);
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
dp[i][j] = Math.min(
dp[i - 1][j] + 1,
dp[i][j - 1] + 1,
dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1),
);
}
}
return dp[a.length][b.length];
}
/**
* Calculate match score for a coin against search query
*/
function calculateScore(coin: Coin, query: string): number {
const queryLower = query.toLowerCase();
// Get individual scores
const symbolScore = baseScore(coin.symbol, queryLower);
const nameScore = baseScore(coin.name, queryLower);
const idScore = baseScore(coin.id, queryLower);
// If id has a perfect match (1000), return that score immediately
if (idScore === 1000) {
return 1000;
}
// If name or symbol has a perfect match (1000), return that score
if (symbolScore === 1000 || nameScore === 1000) {
return 1000;
}
// If name or symbol has a high prefix match (900+), return that score
if (symbolScore >= 900 || nameScore >= 900) {
return Math.max(symbolScore, nameScore);
}
// For other cases, prioritize name and symbol matches
const nameSymbolScore = Math.max(symbolScore, nameScore);
// Consider id score more strongly
if (idScore > 500) {
return Math.max(nameSymbolScore, idScore);
}
// Only consider id score if name/symbol have decent matches
if (nameSymbolScore > 500) {
// Add a small bonus from id if it matches well
return nameSymbolScore + (idScore > 300 ? 50 : 0);
}
return 0;
}
function baseScore(field: string, query: string): number {
const fieldLower = field.toLowerCase();
const queryLower = query.toLowerCase();
// If exact match, return highest score
if (fieldLower === queryLower) return 1000;
// Skip tokens that start with special characters
if (fieldLower.startsWith("_") || fieldLower.startsWith("-")) {
return 0;
}
if (field.length === 1 && query.length > 1) {
return 0;
}
// Check if the field contains the exact query as a whole word
// This will match "usdt" in "bridged-usdt-base" but not in "usd-token"
const exactWordMatch = new RegExp(`\\b${queryLower}\\b`);
if (exactWordMatch.test(fieldLower)) {
return 950;
}
// Check if the field contains the query as a substring
if (fieldLower.includes(queryLower)) {
// Give higher score if it's at the end of the field (for symbols like "ufet")
if (fieldLower.endsWith(queryLower)) {
return 900;
}
return 800;
}
// For symbols, check if the query is contained within the symbol
if (field.length <= 10) {
// Only apply this to short fields like symbols
const regex = new RegExp(queryLower.split("").join(".*"), "i");
if (regex.test(fieldLower)) {
return 100;
}
}
// Split by common separators to handle protocol names better
const fieldParts = fieldLower.split(/[-_]/);
const queryParts = queryLower.split(/[-_]/);
// Check prefix match (first part before separator)
const fieldPrefix = fieldParts[0];
const queryPrefix = queryParts[0];
// If the field starts with the query, give it a very high score
if (fieldPrefix.startsWith(queryPrefix)) {
// Give extra bonus if it's an exact prefix match
if (fieldPrefix === queryPrefix) {
return 900;
}
return 850;
}
// If the query starts with the field prefix, give it a high score
if (queryPrefix.startsWith(fieldPrefix)) {
return 800;
}
// Calculate character-by-character match score for partial matches
let score = 0;
const minLength = Math.min(fieldPrefix.length, queryPrefix.length);
// Check each character position with decreasing weight
for (let i = 0; i < minLength; i++) {
const weight = 1 - i / minLength; // Higher weight for earlier positions
if (fieldPrefix[i] === queryPrefix[i]) {
score += 100 * weight; // Perfect match at position
} else if (i === 0) {
// If first character doesn't match, return 0
return 0;
}
}
// If we have a good initial match, add bonus for length similarity
if (score > 0) {
const lengthSimilarity =
1 -
Math.abs(fieldPrefix.length - queryPrefix.length) /
Math.max(fieldPrefix.length, queryPrefix.length);
score += lengthSimilarity * 50;
}
// Only return high-quality matches
return score > 300 ? score : 0;
}
/**
* Normalize token names to handle ETH variations
*/
const TOKEN_NORMALIZATION_MAP: Record<string, string> = {
// Ethereum
eth: "ethereum",
ethereum: "ethereum",
ether: "ethereum",
native: "ethereum",
// USDC
usdc: "usd-coin",
"usd coin": "usd-coin",
"usd-coin": "usd-coin",
usdcoin: "usd-coin",
// USDT
usdt: "l2-standard-bridged-usdt-base",
tether: "l2-standard-bridged-usdt-base",
"usd tether": "l2-standard-bridged-usdt-base",
"usd-tether": "l2-standard-bridged-usdt-base",
// Add more mappings here...
};
export function normalizeTokenName(token: string): string {
const normalized = token.toLowerCase().trim();
return TOKEN_NORMALIZATION_MAP[normalized] || normalized;
}
/**
* Fetch market data for multiple tokens at once
*/
export async function fetchTokensMarketData(
tokenIds: string[],
): Promise<any[]> {
try {
const apiKey = validateCoingeckoApiKey();
const response = await axios.get(
`${COINGECKO_BASE_URL}/api/v3/coins/markets`,
{
params: {
vs_currency: "usd",
ids: tokenIds.join(","),
},
headers: {
"x-cg-pro-api-key": apiKey,
},
},
);
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
throw new Error(
"Invalid Coingecko API key. Please check your API key configuration.",
);
}
throw new Error(`Failed to fetch tokens market data: ${error.message}`);
}
throw error;
}
}
/**
* Search for a coin ID using fuzzy matching
*/
export async function searchCoin(query: string): Promise<string[]> {
const normalizedQuery = normalizeTokenName(query);
// Direct return for known mappings
if (
["ethereum", "usd-coin", "l2-standard-bridged-usdt-base"].includes(
normalizedQuery,
)
) {
return [normalizedQuery];
}
try {
const apiKey = validateCoingeckoApiKey();
const response = await axios.get<Coin[]>(
`${COINGECKO_BASE_URL}/api/v3/coins/list?asset_platform_id=base`,
{
headers: {
"x-cg-pro-api-key": apiKey,
},
},
);
const coins = response.data;
const scored = coins
.map((coin) => ({
id: coin.id,
score: calculateScore(coin, normalizedQuery),
}))
.filter((c) => c.score > 500)
.sort((a, b) => b.score - a.score)
.slice(0, 3);
console.log("Scored tokens:", scored);
const marketData = await fetchTokensMarketData(scored.map((c) => c.id));
const marketDataMap = new Map(marketData.map((d) => [d.id, d.market_cap]));
const ranked = scored
.map((c) => ({
id: c.id,
score: c.score,
marketCap: marketDataMap.get(c.id) || 0,
}))
.sort((a, b) => b.marketCap - a.marketCap);
console.log("Ranked tokens:", ranked);
return ranked.map((c) => c.id);
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
throw new Error(
"Invalid Coingecko API key. Please check your API key configuration.",
);
}
throw new Error(`Failed to search coin: ${error.message}`);
}
throw error;
}
}
/**
* Fetch detailed token information from CoinGecko
*/
export async function fetchTokenDetails(
tokenId: string,
): Promise<GeckoTerminalResponse> {
try {
const apiKey = validateCoingeckoApiKey();
const response = await axios.get<GeckoTerminalResponse>(
`${COINGECKO_BASE_URL}/api/v3/coins/${tokenId}`,
{
headers: {
"x-cg-pro-api-key": apiKey,
},
},
);
// Extract only the needed fields from the response
const data = response.data;
// Create a filtered response with only the necessary fields
const filteredData: GeckoTerminalResponse = {
symbol: data.symbol,
name: data.name,
detail_platforms: data.detail_platforms,
categories: data.categories,
description: {
en: data.description?.en || "",
},
image: {
small: data.image?.small || "",
},
market_data: {
current_price: {
eth: data.market_data?.current_price?.eth || 0,
usd: data.market_data?.current_price?.usd || 0,
},
ath: {
eth: data.market_data?.ath?.eth || 0,
usd: data.market_data?.ath?.usd || 0,
},
atl: {
eth: data.market_data?.atl?.eth || 0,
usd: data.market_data?.atl?.usd || 0,
},
market_cap: {
eth: data.market_data?.market_cap?.eth || 0,
usd: data.market_data?.market_cap?.usd || 0,
},
fully_diluted_valuation: {
eth: data.market_data?.fully_diluted_valuation?.eth || 0,
usd: data.market_data?.fully_diluted_valuation?.usd || 0,
},
total_volume: {
eth: data.market_data?.total_volume?.eth || 0,
usd: data.market_data?.total_volume?.usd || 0,
},
high_24h: {
eth: data.market_data?.high_24h?.eth || 0,
usd: data.market_data?.high_24h?.usd || 0,
},
low_24h: {
eth: data.market_data?.low_24h?.eth || 0,
usd: data.market_data?.low_24h?.usd || 0,
},
price_change_percentage_24h:
data.market_data?.price_change_percentage_24h || 0,
},
};
return filteredData;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
throw new Error(
"Invalid Coingecko API key. Please check your API key configuration.",
);
}
throw new Error(`Failed to fetch token details: ${error.message}`);
}
throw error;
}
}
/**
* Fetch detailed token information from CoinGecko by contract address
*/
export async function fetchTokenDetailsByContract(
contractAddress: string,
): Promise<GeckoTerminalResponse | null> {
try {
const apiKey = validateCoingeckoApiKey();
const response = await axios.get<GeckoTokenFetchResponse>(
`${COINGECKO_BASE_URL}/api/v3/onchain/networks/base/tokens/${contractAddress}`,
{
headers: {
"x-cg-pro-api-key": apiKey,
},
},
);
// Extract only the needed fields from the response
const data = response.data.data;
// Create a filtered response with only the necessary fields
const filteredData: GeckoTerminalResponse = {
symbol: data.attributes.symbol,
name: data.attributes.name,
detail_platforms: {
base: {
contract_address: data.attributes.address,
decimal_place: data.attributes.decimals,
},
},
categories: [],
description: {
en: data.attributes.name || "",
},
image: {
small: data.attributes.image_url || "",
},
market_data: {
current_price: {
eth: 0,
usd: Number(data.attributes.price_usd) || 0,
},
ath: {
eth: 0,
usd: 0,
},
atl: {
eth: 0,
usd: 0,
},
market_cap: {
eth: 0,
usd: Number(data.attributes.market_cap_usd) || 0,
},
fully_diluted_valuation: {
eth: 0,
usd: Number(data.attributes.fdv_usd) || 0,
},
total_volume: {
eth: 0,
usd: 0,
},
high_24h: {
eth: 0,
usd: Number(data.attributes.volume_usd.h24) || 0,
},
low_24h: {
eth: 0,
usd: 0,
},
price_change_percentage_24h: 0,
},
};
return filteredData;
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 401) {
console.error(
"Invalid Coingecko API key. Please check your API key configuration.",
);
} else if (error.response?.status === 404) {
console.log(
`Token with contract address ${contractAddress} not found in CoinGecko database`,
);
} else {
console.error(
`Failed to fetch token details by contract: ${error.message}`,
);
}
return null;
}
console.error(
`Error fetching token details for ${contractAddress}:`,
error,
);
return null;
}
}
/**
* Get token address and decimals, handling ETH specially
*/
export function getTokenAddressAndDecimals(
tokenId: string,
tokenData: GeckoTerminalResponse,
): { address: string; decimals: number } {
const networkConfig = getCurrentNetworkConfig();
const platformId = networkConfig.coingeckoName;
const address =
tokenId === "ethereum"
? ETH_ADDRESS
: tokenData.detail_platforms?.[platformId]?.contract_address ||
ETH_ADDRESS;
const decimals =
tokenId === "ethereum"
? 18
: (tokenData.detail_platforms?.[platformId]?.decimal_place ?? 18);
return { address, decimals };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment