Blog

Developer Guide

How to Build a Stellar Payment Dashboard Using Horizon API Data

A payment dashboard is one of the most practical things you can build on Stellar. It shows account balances, tracks incoming and outgoing payments, displays transaction status, and gives users visibility into their on-chain activity. This tutorial walks through building one using the Horizon API.

What We Are Building

A dashboard that displays:

  • Account balances — XLM and all trusted assets
  • Payment history — recent incoming and outgoing payments with amounts, assets, and timestamps
  • Transaction status — success/failure indicators with fee and ledger info
  • Ledger info — current network state
  • Fetching Account Balances

    The /accounts/{id} endpoint returns everything about an account in a single call:

    const HORIZON = 'https://horizon.stellar.org';
    
    async function getAccountData(accountId) {
      const res = await fetch(`${HORIZON}/accounts/${accountId}`);
      if (res.status === 404) return { error: 'Account not found or not funded' };
      const account = await res.json();
    
      return {
        id: account.account_id,
        sequence: account.sequence,
        balances: account.balances.map(b => ({
          asset: b.asset_type === 'native' ? 'XLM' : `${b.asset_code}`,
          balance: parseFloat(b.balance),
          issuer: b.asset_issuer || null,
        })),
        signers: account.signers.length,
        subentries: account.subentry_count,
        homeDomain: account.home_domain || null,
      };
    }

    Displaying Balances

    function renderBalances(balances) {
      return balances
        .sort((a, b) => b.balance - a.balance)
        .map(b => `${b.asset}: ${b.balance.toLocaleString()}`)
        .join('\n');
    }

    Fetching Payment History

    The /accounts/{id}/payments endpoint returns all payment-type operations:

    async function getPayments(accountId, limit = 20) {
      const res = await fetch(
        `${HORIZON}/accounts/${accountId}/payments?limit=${limit}&order=desc`
      );
      const data = await res.json();
    
      return data._embedded.records.map(p => ({
        id: p.id,
        type: p.type,
        createdAt: p.created_at,
        amount: p.amount,
        asset: p.asset_type === 'native' ? 'XLM' : p.asset_code,
        from: p.from,
        to: p.to,
        direction: p.to === accountId ? 'incoming' : 'outgoing',
        transactionHash: p.transaction_hash,
      }));
    }

    This gives you the data to render a payment feed with direction indicators, amounts, and timestamps.

    Fetching Transaction Details

    For each payment, you can fetch the full transaction to show status and fees:

    async function getTransaction(hash) {
      const res = await fetch(`${HORIZON}/transactions/${hash}`);
      const tx = await res.json();
    
      return {
        hash: tx.hash,
        ledger: tx.ledger,
        successful: tx.successful,
        feeCharged: parseInt(tx.fee_charged),
        operationCount: tx.operation_count,
        memo: tx.memo || null,
        memoType: tx.memo_type,
        createdAt: tx.created_at,
      };
    }

    Fetching Current Ledger Info

    Show the user what is happening on the network right now:

    async function getNetworkStatus() {
      const [ledgerRes, feeRes] = await Promise.all([
        fetch(`${HORIZON}/ledgers?limit=1&order=desc`),
        fetch(`${HORIZON}/fee_stats`),
      ]);
    
      const ledger = (await ledgerRes.json())._embedded.records[0];
      const fees = await feeRes.json();
    
      return {
        currentLedger: ledger.sequence,
        closedAt: ledger.closed_at,
        transactionCount: ledger.successful_transaction_count,
        baseFee: parseInt(fees.fee_charged.min),
        protocolVersion: ledger.protocol_version,
      };
    }

    Putting It Together

    async function buildDashboard(accountId) {
      const [account, payments, network] = await Promise.all([
        getAccountData(accountId),
        getPayments(accountId, 10),
        getNetworkStatus(),
      ]);
    
      console.log('=== Account ===');
      console.log(`ID: ${account.id}`);
      account.balances.forEach(b =>
        console.log(`  ${b.asset}: ${b.balance.toLocaleString()}`)
      );
    
      console.log('\n=== Recent Payments ===');
      payments.forEach(p =>
        console.log(`  ${p.direction === 'incoming' ? '←' : '→'} ${p.amount} ${p.asset} (${p.createdAt})`)
      );
    
      console.log('\n=== Network ===');
      console.log(`  Ledger: #${network.currentLedger}`);
      console.log(`  TXs in last ledger: ${network.transactionCount}`);
      console.log(`  Base fee: ${network.baseFee} stroops`);
    }

    Auto-Refreshing

    Poll for new payments using cursors:

    async function pollPayments(accountId, onNewPayment) {
      let cursor = 'now';
      setInterval(async () => {
        const res = await fetch(
          `${HORIZON}/accounts/${accountId}/payments?cursor=${cursor}&order=asc&limit=50`
        );
        const data = await res.json();
        for (const payment of data._embedded.records) {
          onNewPayment(payment);
          cursor = payment.paging_token;
        }
      }, 5000);
    }

    Next Steps

  • Add WebSocket-style streaming using Horizon's SSE support
  • Integrate with the LumenQuery Live Transaction Viewer for decoded operations
  • Use Network Analytics for TPS and fee trends

  • *Build your payment dashboard on reliable infrastructure. LumenQuery provides managed Horizon API with sub-100ms response times. Start free.*