Use when implementing exchange rate functionality - provides complete patterns for fetching BTC/fiat exchange rates from Coinbase API, caching strategies, conversion utilities, and React hooks for displaying rates in UI
Complete implementation guide for fetching and using Bitcoin exchange rates from Coinbase API. Includes caching strategies, conversion utilities, and React hooks for displaying rates in wallet UIs.
Core Capabilities:
Key Architecture Pattern: Module-level cache with configurable duration prevents excessive API calls while ensuring fresh data.
No additional packages required - Uses native fetch API.
Coinbase API:
https://api.coinbase.com/v2/exchange-rates// lib/exchangeRateService.ts
/**
* Exchange rate service for fetching BTC/USD rates from Coinbase API
*
* Module-level cache to avoid excessive API calls
*/
const COINBASE_API_URL = "https://api.coinbase.com/v2/exchange-rates";
const CACHE_DURATION_MS = 60000; // 1 minute cache
let cachedRate: number | null = null;
let cacheTimestamp: number = 0;
/**
* Get the current BTC/USD exchange rate from Coinbase API
* Uses caching to avoid excessive API calls
*
* @returns Promise resolving to BTC/USD exchange rate
*/
export async function getBtcUsdRate(): Promise<number> {
const now = Date.now();
// Return cached rate if still valid
if (cachedRate && (now - cacheTimestamp) < CACHE_DURATION_MS) {
console.debug(`Using cached BTC/USD rate: ${cachedRate}`, {
cache_age_ms: now - cacheTimestamp,
});
return cachedRate;
}
try {
console.debug("Fetching BTC/USD rate from Coinbase API");
const response = await fetch(`${COINBASE_API_URL}?currency=BTC`, {
method: "GET",
headers: {
"Accept": "application/json",
"User-Agent": "YourApp/1.0",
},
// Add timeout to prevent hanging
signal: AbortSignal.timeout(10000), // 10 second timeout
});
if (!response.ok) {
throw new Error(`Coinbase API error: ${response.status} ${response.statusText}`);
}
const data = await response.json();
// Validate response structure
if (!data?.data?.rates?.USD) {
throw new Error("Invalid response format from Coinbase API");
}
const rate = parseFloat(data.data.rates.USD);
if (isNaN(rate) || rate <= 0) {
throw new Error(`Invalid exchange rate received: ${data.data.rates.USD}`);
}
// Update cache
cachedRate = rate;
cacheTimestamp = now;
console.info(`Fetched BTC/USD rate from Coinbase: ${rate}`, { rate });
return rate;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Failed to fetch BTC/USD rate: ${errorMessage}`);
// If we have a cached rate, use it as fallback
if (cachedRate) {
const cacheAge = now - cacheTimestamp;
console.warn(`Using stale cached rate as fallback: ${cachedRate}`, {
cache_age_ms: cacheAge,
});
return cachedRate;
}
// If no cached rate available, throw error
throw new Error(`Unable to fetch BTC/USD exchange rate: ${errorMessage}`);
}
}
Key Features:
// lib/exchangeRateService.ts
/**
* Convert BTC amount to USD using current exchange rate
*
* @param btcAmount - Amount in BTC
* @returns Promise resolving to USD amount
*/
export async function btcToUsd(btcAmount: number): Promise<number> {
const rate = await getBtcUsdRate();
return btcAmount * rate;
}
/**
* Convert USD amount to BTC using current exchange rate
*
* @param usdAmount - Amount in USD
* @returns Promise resolving to BTC amount
*/
export async function usdToBtc(usdAmount: number): Promise<number> {
const rate = await getBtcUsdRate();
return usdAmount / rate;
}
/**
* Convert sats to USD using current exchange rate
*
* @param sats - Amount in satoshis
* @returns Promise resolving to USD amount
*/
export async function satsToUsd(sats: number): Promise<number> {
const btcAmount = sats / 100000000; // Convert sats to BTC
return btcToUsd(btcAmount);
}
/**
* Convert USD to sats using current exchange rate
*
* @param usdAmount - Amount in USD
* @returns Promise resolving to satoshis
*/
export async function usdToSats(usdAmount: number): Promise<number> {
const btcAmount = await usdToBtc(usdAmount);
return Math.floor(btcAmount * 100000000); // Convert BTC to sats
}
Available Fiat Currencies:
Coinbase API supports all major fiat currencies. Common examples include:
Major Currencies:
Other Supported Currencies:
Note: The API response includes rates for all currencies. To fetch a specific currency rate:
/**
* Get BTC exchange rate for any supported currency
*
* @param currencyCode - ISO currency code (e.g., 'EUR', 'GBP', 'JPY')
* @returns Promise resolving to BTC/[currency] exchange rate
*/
export async function getBtcRate(currencyCode: string): Promise<number> {
const response = await fetch(`${COINBASE_API_URL}?currency=BTC`, {
method: "GET",
headers: {
"Accept": "application/json",
},
signal: AbortSignal.timeout(10000),
});
if (!response.ok) {
throw new Error(`Coinbase API error: ${response.status}`);
}
const data = await response.json();
if (!data?.data?.rates?.[currencyCode]) {
throw new Error(`Currency ${currencyCode} not supported`);
}
const rate = parseFloat(data.data.rates[currencyCode]);
if (isNaN(rate) || rate <= 0) {
throw new Error(`Invalid exchange rate for ${currencyCode}`);
}
return rate;
}
// lib/exchangeRateService.ts
/**
* Clear the cached exchange rate (useful for testing or forcing refresh)
*/
export function clearCache(): void {
cachedRate = null;
cacheTimestamp = 0;
}
/**
* Get cache status for debugging
*
* @returns Cache status object
*/
export function getCacheStatus(): {
rate: number | null;
age_ms: number;
is_valid: boolean
} {
const now = Date.now();
const age = now - cacheTimestamp;
const isValid = cachedRate !== null && age < CACHE_DURATION_MS;
return {
rate: cachedRate,
age_ms: age,
is_valid: isValid,
};
}
// hooks/useExchangeRate.ts
import { useQuery } from '@tanstack/react-query';
import { getBtcUsdRate } from '@/lib/exchangeRateService';
/**
* Hook to fetch and cache BTC/USD exchange rate
*
* @example
* ```tsx
* const { data: rate, isLoading } = useExchangeRate();
*
* if (rate) {
* const usdValue = balanceSats / 100000000 * rate;
* }
* ```
*/
export function useExchangeRate() {
return useQuery({
queryKey: ['btc-usd-rate'],
queryFn: getBtcUsdRate,
staleTime: 60000, // Consider stale after 1 minute
refetchInterval: 120000, // Refetch every 2 minutes
retry: 2,
retryDelay: 1000,
});
}
// hooks/useExchangeRate.ts
import { useMemo } from 'react';
import { useExchangeRate } from './useExchangeRate';
/**
* Hook to convert sats to USD with current exchange rate
*
* @param sats - Amount in satoshis
* @returns USD amount or null if rate unavailable
*/
export function useSatsToUsd(sats: number | null | undefined) {
const { data: rate } = useExchangeRate();
return useMemo(() => {
if (!sats || !rate) return null;
return (sats / 100000000) * rate;
}, [sats, rate]);
}
/**
* Hook to convert USD to sats with current exchange rate
*
* @param usd - Amount in USD
* @returns Satoshis or null if rate unavailable
*/
export function useUsdToSats(usd: number | null | undefined) {
const { data: rate } = useExchangeRate();
return useMemo(() => {
if (!usd || !rate) return null;
return Math.floor((usd / rate) * 100000000);
}, [usd, rate]);
}
// components/ExchangeRateDisplay.tsx
import { useExchangeRate } from '@/hooks/useExchangeRate';
import { Skeleton } from '@/components/ui/skeleton';
export function ExchangeRateDisplay() {
const { data: rate, isLoading, error } = useExchangeRate();
if (isLoading) {
return <Skeleton className="h-6 w-32" />;
}
if (error) {
return (
<span className="text-sm text-muted-foreground">
Rate unavailable
</span>
);
}
if (!rate) {
return null;
}
return (
<div className="text-sm">
<span className="text-muted-foreground">BTC/USD: </span>
<span className="font-mono">${rate.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}</span>
</div>
);
}
// components/BalanceDisplay.tsx
import { useSatsToUsd } from '@/hooks/useExchangeRate';
import { Skeleton } from '@/components/ui/skeleton';
interface BalanceDisplayProps {
sats: number;
}
export function BalanceDisplay({ sats }: BalanceDisplayProps) {
const usdValue = useSatsToUsd(sats);
return (
<div className="space-y-1">
<div className="text-2xl font-bold">
{sats.toLocaleString()} sats
</div>
{usdValue !== null ? (
<div className="text-sm text-muted-foreground">
≈ ${usdValue.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</div>
) : (
<Skeleton className="h-4 w-24" />
)}
</div>
);
}
// components/CurrencyDisplay.tsx
import { useSatsToUsd } from '@/hooks/useExchangeRate';
interface CurrencyDisplayProps {
sats: number;
showSats?: boolean;
showUsd?: boolean;
}
export function CurrencyDisplay({
sats,
showSats = true,
showUsd = true
}: CurrencyDisplayProps) {
const usdValue = useSatsToUsd(sats);
return (
<div className="flex flex-col gap-1">
{showSats && (
<span className="font-mono">
{sats.toLocaleString()} sats
</span>
)}
{showUsd && usdValue !== null && (
<span className="text-sm text-muted-foreground">
${usdValue.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</span>
)}
</div>
);
}
Problem: Fetching exchange rate on every render causes excessive API calls and rate limiting.
Solution: Use module-level cache with configurable duration:
const CACHE_DURATION_MS = 60000; // 1 minute
let cachedRate: number | null = null;
let cacheTimestamp: number = 0;
Problem: API failures break the UI completely.
Solution: Always provide fallback to stale cache:
if (cachedRate) {
console.warn('Using stale cached rate as fallback');
return cachedRate;
}
Problem: Slow or hanging API calls freeze the UI.
Solution: Add timeout to fetch requests: