Skip to main content
Bridge Wrapped aggregates bridging activity from three major bridge protocols to provide a complete view of your cross-chain transactions.

Supported bridges

Bridge Wrapped integrates with three leading bridge aggregators:

Across Protocol

Fast, secure bridging with optimistic verification

Relay

High-performance cross-chain transactions

LiFi

Multi-protocol aggregation for best rates

Architecture

The aggregation system uses a modular adapter pattern with a unified interface:
interface BridgeProviderAdapter {
  readonly name: BridgeProvider;
  fetchTransactions(
    address: string,
    startTimestamp: number,
    endTimestamp: number
  ): Promise<NormalizedBridgeTransaction[]>;
}
Each bridge adapter implements this interface, ensuring consistent data normalization across all providers.

Bridge adapters

Across Protocol

The Across adapter fetches deposit data from the Across API:
src/services/bridges/across.ts
export class AcrossAdapter implements BridgeProviderAdapter {
  readonly name = 'across' as const;
  private baseUrl = API_URLS.ACROSS;

  async fetchTransactions(
    address: string,
    startTimestamp: number,
    endTimestamp: number
  ): Promise<NormalizedBridgeTransaction[]> {
    const allTransactions: NormalizedBridgeTransaction[] = [];
    let offset = 0;
    let hasMore = true;

    while (hasMore) {
      const response = await retryWithBackoff(() =>
        this.fetchPage(address, offset)
      );

      if (!response.deposits || response.deposits.length === 0) {
        hasMore = false;
        break;
      }

      for (const deposit of response.deposits) {
        const normalized = await this.normalizeDeposit(deposit);
        if (!normalized) continue;

        if (isWithinYear(normalized.timestamp, startTimestamp, endTimestamp)) {
          allTransactions.push(normalized);
        }

        if (normalized.timestamp < startTimestamp) {
          hasMore = false;
          break;
        }
      }

      offset += PAGINATION.ACROSS_LIMIT;
    }

    return allTransactions;
  }
}
Across deposits include price data directly from the API, reducing the need for external price lookups.

Relay

The Relay adapter uses cursor-based pagination:
src/services/bridges/relay.ts
export class RelayAdapter implements BridgeProviderAdapter {
  readonly name = 'relay' as const;
  private baseUrl = API_URLS.RELAY;

  async fetchTransactions(
    address: string,
    startTimestamp: number,
    endTimestamp: number
  ): Promise<NormalizedBridgeTransaction[]> {
    const allTransactions: NormalizedBridgeTransaction[] = [];
    let continuation: string | undefined;
    let hasMore = true;

    while (hasMore) {
      const response = await retryWithBackoff(() =>
        this.fetchPage(address, continuation)
      );

      if (!response.requests || response.requests.length === 0) {
        hasMore = false;
        break;
      }

      for (const request of response.requests) {
        const normalized = await this.normalizeRequest(request);
        if (!normalized) continue;

        if (isWithinYear(normalized.timestamp, startTimestamp, endTimestamp)) {
          allTransactions.push(normalized);
        }
      }

      continuation = response.continuation;
      if (!continuation) hasMore = false;
    }

    return allTransactions;
  }
}

LiFi

The LiFi adapter leverages their analytics API:
src/services/bridges/lifi.ts
export class LiFiAdapter implements BridgeProviderAdapter {
  readonly name = 'lifi' as const;
  private baseUrl = API_URLS.LIFI;

  async fetchTransactions(
    address: string,
    startTimestamp: number,
    endTimestamp: number
  ): Promise<NormalizedBridgeTransaction[]> {
    const allTransactions: NormalizedBridgeTransaction[] = [];
    let nextCursor: string | undefined;
    let hasMore = true;

    while (hasMore) {
      const response = await retryWithBackoff(() =>
        this.fetchPage(address, startTimestamp, endTimestamp, nextCursor)
      );

      if (!response.transfers || response.transfers.length === 0) {
        hasMore = false;
        break;
      }

      for (const transfer of response.transfers) {
        const normalized = await this.normalizeTransfer(transfer);
        if (normalized) {
          allTransactions.push(normalized);
        }
      }

      nextCursor = response.hasNext ? response.next : undefined;
      if (!nextCursor) hasMore = false;
    }

    return allTransactions;
  }
}
LiFi’s API supports timestamp filtering directly, allowing for more efficient queries compared to client-side filtering.

Data normalization

All bridge transactions are normalized to a common format:
interface NormalizedBridgeTransaction {
  id: string;
  provider: 'across' | 'relay' | 'lifi';
  txHash: string;
  timestamp: number;
  sourceChainId: number;
  sourceChainName: string;
  destinationChainId: number;
  destinationChainName: string;
  tokenSymbol: string;
  tokenAddress: string;
  amount: string;
  amountFormatted: number;
  amountUSD: number;
  status: 'pending' | 'completed' | 'failed';
}

Token amount parsing

Token amounts are converted from raw values to human-readable decimals:
function parseTokenAmount(amount: string, decimals: number): number {
  const value = BigInt(amount);
  const divisor = BigInt(10 ** decimals);
  return Number(value) / Number(divisor);
}

Price enrichment

Bridge Wrapped enhances transaction data with USD values using CoinMarketCap’s API:
const tokenInfo = await coinMarketCapService.getTokenInfo(tokenAddress);
const tokenSymbol = tokenInfo?.symbol || deposit.token?.symbol;
const tokenDecimals = tokenInfo?.decimals || deposit.token?.decimals || 18;

const amountFormatted = parseTokenAmount(deposit.inputAmount, tokenDecimals);
const amountUSD = amountFormatted * tokenPriceUSD;

Aggregation logic

The BridgeAggregator class orchestrates data fetching from all bridges:
src/services/bridges/aggregator.ts
export class BridgeAggregator {
  async getWrappedStats(
    address: string,
    year: number = 2025
  ): Promise<BridgeWrappedStats> {
    const startTimestamp = Math.floor(
      new Date(`${year}-01-01T00:00:00Z`).getTime() / 1000
    );
    const endTimestamp = Math.floor(
      new Date(`${year}-12-31T23:59:59Z`).getTime() / 1000
    );

    // Fetch transactions from all providers in parallel
    const [acrossTransactions, relayTransactions, lifiTransactions] =
      await Promise.all([
        acrossAdapter.fetchTransactions(address, startTimestamp, endTimestamp)
          .catch((err) => {
            console.error('Across fetch failed:', err);
            return [];
          }),
        relayAdapter.fetchTransactions(address, startTimestamp, endTimestamp)
          .catch((err) => {
            console.error('Relay fetch failed:', err);
            return [];
          }),
        lifiAdapter.fetchTransactions(address, startTimestamp, endTimestamp)
          .catch((err) => {
            console.error('LiFi fetch failed:', err);
            return [];
          }),
      ]);

    // Combine all transactions
    const allTransactions = [
      ...acrossTransactions,
      ...relayTransactions,
      ...lifiTransactions,
    ];

    // Deduplicate transactions
    const deduplicatedTransactions = this.deduplicateTransactions(allTransactions);

    return this.calculateStats(address, year, deduplicatedTransactions);
  }
}
1

Parallel fetching

All three bridge APIs are queried simultaneously using Promise.all() for optimal performance
2

Error handling

Individual bridge failures don’t crash the entire process - failed requests return empty arrays
3

Deduplication

Transactions are deduplicated based on transaction hash and chain pair
4

Statistics calculation

Normalized transactions are processed to generate comprehensive analytics

Deduplication

To prevent double-counting transactions that appear in multiple bridge APIs:
private deduplicateTransactions(
  transactions: NormalizedBridgeTransaction[]
): NormalizedBridgeTransaction[] {
  const seen = new Map<string, NormalizedBridgeTransaction>();

  for (const tx of transactions) {
    const key = `${tx.txHash.toLowerCase()}-${tx.sourceChainId}-${tx.destinationChainId}`;

    if (!seen.has(key)) {
      seen.set(key, tx);
    } else {
      // Prefer transaction with USD value
      const existing = seen.get(key)!;
      if (tx.amountUSD > 0 && existing.amountUSD === 0) {
        seen.set(key, tx);
      }
    }
  }

  return Array.from(seen.values());
}
The deduplication key combines:
  • Transaction hash (lowercase)
  • Source chain ID
  • Destination chain ID
When duplicates are found, Bridge Wrapped prefers the transaction with USD value data to ensure accurate volume calculations.

Statistics calculation

After aggregation and deduplication, the system calculates:
  • Total bridging actions: Count of all transactions
  • Total volume USD: Sum of all transaction values
  • Chain statistics: Most used source/destination chains and highest volume chains
  • Token statistics: Most bridged tokens and top tokens by count
  • Temporal statistics: Busiest day and monthly activity patterns
  • Provider breakdown: Transaction counts and volumes per bridge
return {
  walletAddress: address,
  year,
  generatedAt: new Date().toISOString(),
  totalBridgingActions,
  totalVolumeUSD,
  mostUsedSourceChain,
  mostUsedDestinationChain,
  highestVolumeDestination,
  mostBridgedToken,
  busiestDay,
  providerBreakdown: providerStats,
  monthlyActivity: formattedMonthlyActivity,
  topSourceChains,
  topDestinationChains,
  topTokens,
  transactions: sortedTransactions,
};

Error handling and retry logic

All API requests use exponential backoff for resilience:
async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries = 3,
  baseDelay = 1000
): Promise<T> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      await new Promise(resolve => 
        setTimeout(resolve, baseDelay * Math.pow(2, i))
      );
    }
  }
  throw new Error('Max retries exceeded');
}
The retry logic uses exponential backoff: 1s, 2s, 4s delays between attempts. This prevents overwhelming bridge APIs during temporary outages.

Performance considerations

Fetching from all three bridges simultaneously reduces total loading time from ~15s to ~5s for typical wallets.
Each adapter implements safety limits (100 pages max) to prevent infinite loops from malformed API responses.
When transactions fall outside the requested time range, pagination stops early to avoid unnecessary API calls.
CoinMarketCap token lookups are batched using getMultipleTokenInfo() to reduce API calls.

Build docs developers (and LLMs) love