Blog

Developer Guide

Tracking Stellar's $2B RWA Milestone On-Chain: Build a Tokenized-Asset Analytics Dashboard with LumenQuery APIs

Stellar crossed the $2 billion mark in tokenized real-world assets in 2026, with payment volume up 72% year-over-year and developer participation increasing 86%. These are not speculative DeFi tokens. They are government bonds, fund shares, and institutional stablecoins issued by regulated entities like Franklin Templeton, WisdomTree, and Circle.

This guide walks you through building a live RWA analytics dashboard that tracks tokenized asset issuance, holder distribution, and transfer activity on Stellar using the Horizon API and LumenQuery endpoints.

Understanding the RWA Landscape on Stellar

Major Tokenized Assets

AssetIssuerCategoryApprox. Value
USDCCircleStablecoin$1B+
BENJI (FOBXX)Franklin TempletonMoney market fund$400M+
WisdomTree Prime tokensWisdomTreeFund shares$100M+
SHXStrongholdTokenized USD$50M+

Why Stellar for RWAs

RequirementHow Stellar Delivers
Regulatory complianceNative auth flags (required, revocable, clawback)
Low transaction costs~0.00001 XLM per transaction
Fast settlement5-7 second finality
Programmable logicSoroban smart contracts
Institutional adoptionSDF partnerships with Franklin Templeton, DTCC, Visa

Project Architecture

We will build a dashboard with four sections:

  • RWA Overview: Total value, asset count, holder count
  • Asset Breakdown: Per-asset metrics (supply, holders, compliance flags)
  • Holder Distribution: How assets are distributed across accounts
  • Transfer Activity: Recent transfers charted over time
  • Step 1: Define Your RWA Asset Registry

    // lib/rwa-registry.ts
    interface RWAAsset {
      code: string;
      issuer: string;
      name: string;
      category: 'stablecoin' | 'fund' | 'bond' | 'commodity' | 'other';
    }
    
    const RWA_ASSETS: RWAAsset[] = [
      {
        code: 'USDC',
        issuer: 'GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN',
        name: 'USD Coin',
        category: 'stablecoin',
      },
      {
        code: 'BENJI',
        issuer: 'GBHNGLLIE3KWGKCHIKMHJ5HVZHYFF32DLXTUENE3AFMUQT2SGHSPSA2A',
        name: 'Franklin OnChain US Govt Money Fund',
        category: 'fund',
      },
      // Add additional RWA assets as they launch
    ];

    Step 2: Fetch Asset Metrics from Horizon

    // lib/rwa-fetcher.ts
    const HORIZON = 'https://horizon.stellar.org';
    
    async function fetchAssetMetrics(asset: RWAAsset) {
      const res = await fetch(
        `${HORIZON}/assets?asset_code=${asset.code}&asset_issuer=${asset.issuer}`
      );
      const data = await res.json();
    
      if (!data._embedded?.records?.length) {
        return { ...asset, totalSupply: 0, authorizedAccounts: 0 };
      }
    
      const record = data._embedded.records[0];
    
      return {
        code: asset.code,
        issuer: asset.issuer,
        name: asset.name,
        category: asset.category,
        totalSupply: parseFloat(record.amount),
        authorizedAccounts: record.accounts.authorized,
        pendingAccounts: record.accounts.authorized_to_maintain_liabilities,
        authRequired: record.flags.auth_required,
        authRevocable: record.flags.auth_revocable,
        clawbackEnabled: record.flags.auth_clawback_enabled,
      };
    }
    
    async function fetchAllAssetMetrics() {
      const results = await Promise.all(
        RWA_ASSETS.map(asset => fetchAssetMetrics(asset))
      );
      return results.filter(r => r.totalSupply > 0);
    }

    Step 3: Track Holder Distribution

    async function getTopHolders(assetCode: string, issuer: string, limit = 10) {
      const res = await fetch(
        `${HORIZON}/accounts?asset=${assetCode}:${issuer}&limit=200&order=desc`
      );
      const data = await res.json();
    
      // Get total supply for percentage calculation
      const assetRes = await fetch(
        `${HORIZON}/assets?asset_code=${assetCode}&asset_issuer=${issuer}`
      );
      const assetData = await assetRes.json();
      const totalSupply = parseFloat(assetData._embedded.records[0]?.amount || '0');
    
      return data._embedded.records
        .map((account: any) => {
          const trustline = account.balances.find(
            (b: any) => b.asset_code === assetCode && b.asset_issuer === issuer
          );
          if (!trustline) return null;
    
          const balance = parseFloat(trustline.balance);
          return {
            accountId: account.id,
            balance,
            percentOfSupply: totalSupply > 0 ? (balance / totalSupply) * 100 : 0,
            authorized: trustline.is_authorized,
          };
        })
        .filter(Boolean)
        .sort((a: any, b: any) => b.balance - a.balance)
        .slice(0, limit);
    }

    Step 4: Track Transfer Activity

    async function getTransferActivity(assetCode: string, issuer: string, hours = 24) {
      let url = `${HORIZON}/payments?limit=200&order=desc`;
      const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000);
      const transfers: { timestamp: Date; amount: number }[] = [];
    
      let pages = 0;
      while (url && pages < 10) {
        const res = await fetch(url);
        const data = await res.json();
        let done = false;
    
        for (const record of data._embedded.records) {
          if (new Date(record.created_at) < cutoff) { done = true; break; }
    
          if (record.type === 'payment' &&
              record.asset_code === assetCode &&
              record.asset_issuer === issuer) {
            transfers.push({
              timestamp: new Date(record.created_at),
              amount: parseFloat(record.amount),
            });
          }
        }
    
        if (done || data._embedded.records.length < 200) break;
        url = data._links?.next?.href;
        pages++;
      }
    
      // Bucket into hourly intervals
      const buckets = new Map();
      for (const transfer of transfers) {
        const hour = new Date(transfer.timestamp);
        hour.setMinutes(0, 0, 0);
        const key = hour.toISOString();
    
        if (!buckets.has(key)) {
          buckets.set(key, { timestamp: key, volume: 0, count: 0 });
        }
        const bucket = buckets.get(key);
        bucket.volume += transfer.amount;
        bucket.count++;
      }
    
      return Array.from(buckets.values()).sort(
        (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
      );
    }

    Step 5: Build the API Route

    // app/api/rwa/route.ts
    import { NextResponse } from 'next/server';
    
    export async function GET(request: Request) {
      const { searchParams } = new URL(request.url);
      const section = searchParams.get('section') || 'overview';
    
      try {
        switch (section) {
          case 'overview': {
            const metrics = await fetchAllAssetMetrics();
            const totalValue = metrics.reduce((sum, m) => sum + m.totalSupply, 0);
            const totalHolders = metrics.reduce(
              (sum, m) => sum + m.authorizedAccounts, 0
            );
    
            return NextResponse.json({
              totalValue,
              totalHolders,
              assetCount: metrics.length,
              assets: metrics,
            });
          }
    
          case 'holders': {
            const assetCode = searchParams.get('asset') || 'USDC';
            const issuer = RWA_ASSETS.find(a => a.code === assetCode)?.issuer;
            if (!issuer) return NextResponse.json({ error: 'Unknown asset' }, { status: 400 });
    
            const holders = await getTopHolders(assetCode, issuer);
            return NextResponse.json({ holders });
          }
    
          default:
            return NextResponse.json({ error: 'Invalid section' }, { status: 400 });
        }
      } catch (error) {
        return NextResponse.json({ error: 'Failed to fetch RWA data' }, { status: 500 });
      }
    }

    Step 6: Add Caching for Performance

    RWA data does not change every second. Cache aggressively:

    import Redis from 'ioredis';
    
    const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
    
    async function getCachedOrFetch<T>(
      key: string,
      ttlSeconds: number,
      fetcher: () => Promise<T>
    ): Promise<T> {
      const cached = await redis.get(key);
      if (cached) return JSON.parse(cached);
    
      const data = await fetcher();
      await redis.setex(key, ttlSeconds, JSON.stringify(data));
      return data;
    }
    
    // Usage:
    const overview = await getCachedOrFetch(
      'rwa:overview',
      300, // 5-minute cache
      () => fetchAllAssetMetrics()
    );

    Recommended Cache TTLs

    Data TypeTTLReason
    Asset supply5 minutesChanges infrequently
    Holder counts10 minutesRelatively stable
    Transfer activity1 minuteMore dynamic
    Top holders15 minutesChanges slowly

    Step 7: Add Real-Time Updates with SSE

    For live transfer monitoring, use LumenQuery's SSE endpoint:

    'use client';
    import { useEffect, useState } from 'react';
    
    export function useRWAStream(assetCode: string) {
      const [transfers, setTransfers] = useState([]);
    
      useEffect(() => {
        const es = new EventSource('/api/transactions/stream');
    
        es.onmessage = (event) => {
          try {
            const tx = JSON.parse(event.data);
    
            const rwaOps = tx.operations?.filter(
              (op: any) => op.type === 'payment' && op.asset_code === assetCode
            );
    
            if (rwaOps?.length) {
              for (const op of rwaOps) {
                setTransfers(prev => [{
                  hash: tx.hash,
                  asset: assetCode,
                  amount: op.amount,
                  from: op.from,
                  to: op.to,
                  timestamp: tx.created_at,
                }, ...prev].slice(0, 50));
              }
            }
          } catch (err) { /* skip malformed events */ }
        };
    
        return () => es.close();
      }, [assetCode]);
    
      return transfers;
    }

    Key Metrics to Track

    CategoryMetricWhat It Tells You
    SupplyTotal supplyMarket size of the tokenized asset
    SupplySupply change (24h)Issuance/redemption activity
    HoldersUnique holdersAdoption breadth
    HoldersTop 10 concentrationRisk concentration
    TransfersVolume (24h)Capital movement
    TransfersAverage transfer sizeRetail vs institutional split
    CompliancePending authorizationsKYC pipeline
    ComplianceFrozen accountsRegulatory activity

    Using LumenQuery's Existing Endpoints

    You do not need to build everything from scratch. LumenQuery already provides endpoints that support RWA analytics:

    // Network-level metrics
    const network = await fetch('https://lumenquery.io/api/analytics/network');
    
    // Token-level metrics
    const tokens = await fetch('https://lumenquery.io/api/analytics/tokens');
    
    // Live transaction stream (filter for RWA assets client-side)
    const stream = new EventSource('https://lumenquery.io/api/transactions/stream');
    
    // Natural language queries
    const query = await fetch('https://lumenquery.io/api/query', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ query: 'recent USDC payments larger than 100000' }),
    });

    How LumenQuery Helps

    LumenQuery provides the complete infrastructure stack for RWA analytics:

  • Horizon API: Query asset metadata, holder lists, and transfer history via api.lumenquery.io
  • Analytics Dashboard: View network-wide metrics including RWA volumes on /analytics
  • Live Transactions: Monitor RWA transfers in real time on /dashboard/transactions
  • Smart Contract Explorer: Inspect compliance contracts governing RWA assets on /contracts
  • Portfolio Intelligence: Track RWA positions across multiple accounts on /portfolio
  • Natural Language Query: Ask "USDC transfers over 1 million in the last 24 hours" on /query
  • Redis Caching: Built-in caching layer reduces Horizon API load for high-frequency dashboards

  • *Build real-world asset analytics on reliable infrastructure. LumenQuery provides managed Horizon API and Soroban RPC with caching, analytics, and real-time streaming. Start free.*