> ## Documentation Index
> Fetch the complete documentation index at: https://www.helius.dev/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Bangun Pelacak Portofolio di Solana

> Bangun pelacak portofolio Solana yang menampilkan seluruh aset milik dompet beserta aktivitas on-chain langsung dan historisnya.

export const PortfolioTrackerDemo = () => {
  const [apiKey, setApiKey] = useState('');
  const [ownerAddress, setOwnerAddress] = useState('86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY');
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [portfolio, setPortfolio] = useState(null);
  const [history, setHistory] = useState(null);
  const [live, setLive] = useState([]);
  const [liveStatus, setLiveStatus] = useState('idle');
  const wsRef = useRef(null);
  const rpc = async (method, params) => {
    const res = await fetch(`https://mainnet.helius-rpc.com/?api-key=${apiKey}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        jsonrpc: '2.0',
        id: 'quickstart',
        method,
        params
      })
    });
    const json = await res.json();
    if (json.error) throw new Error(json.error.message || "Kesalahan RPC");
    return json.result;
  };
  const stopLive = () => {
    if (wsRef.current) {
      try {
        wsRef.current.close();
      } catch (e) {}
      wsRef.current = null;
    }
    setLiveStatus('idle');
  };
  useEffect(() => stopLive, []);
  const handleLoad = async e => {
    e.preventDefault();
    setLoading(true);
    setError(null);
    setPortfolio(null);
    setHistory(null);
    stopLive();
    setLive([]);
    try {
      const assets = await rpc('getAssetsByOwner', {
        ownerAddress,
        page: 1,
        limit: 20,
        displayOptions: {
          showFungible: true,
          showNativeBalance: true
        }
      });
      const txns = await rpc('getTransactionsForAddress', [ownerAddress, {
        transactionDetails: 'signatures',
        sortOrder: 'desc',
        limit: 10,
        filters: {
          tokenAccounts: 'balanceChanged'
        }
      }]);
      const items = assets.items || [];
      const tokens = items.filter(i => (i.interface || '').includes('Fungible'));
      const nfts = items.filter(i => !(i.interface || '').includes('Fungible'));
      setPortfolio({
        sol: (assets.nativeBalance && assets.nativeBalance.lamports ? assets.nativeBalance.lamports : 0) / 1e9,
        tokenCount: tokens.length,
        nftCount: nfts.length,
        tokens: tokens.slice(0, 6),
        nfts: nfts.slice(0, 4)
      });
      setHistory(txns.data || []);
    } catch (err) {
      const msg = err.message || '';
      setError((/too big|too large/i).test(msg) ? "Dompet ini menyimpan terlalu banyak aset untuk dimuat dalam demo. Coba gunakan dompet dengan lebih sedikit token dan NFT, atau kurangi batas di getAssetsByOwner." : msg || "Gagal memuat dompet");
    } finally {
      setLoading(false);
    }
  };
  const startLive = () => {
    if (!apiKey) {
      setError("Masukkan kunci API Anda terlebih dahulu");
      return;
    }
    setLiveStatus('connecting');
    const ws = new WebSocket(`wss://mainnet.helius-rpc.com/?api-key=${apiKey}`);
    wsRef.current = ws;
    ws.onopen = () => {
      setLiveStatus('live');
      ws.send(JSON.stringify({
        jsonrpc: '2.0',
        id: 1,
        method: 'transactionSubscribe',
        params: [{
          accountInclude: [ownerAddress],
          tokenAccounts: 'balanceChanged'
        }, {
          commitment: 'confirmed',
          encoding: 'jsonParsed',
          transactionDetails: 'signatures',
          maxSupportedTransactionVersion: 1
        }]
      }));
    };
    ws.onmessage = event => {
      const msg = JSON.parse(event.data);
      const sig = msg && msg.params && msg.params.result && msg.params.result.signature;
      if (sig) setLive(prev => [{
        sig,
        at: new Date().toLocaleTimeString()
      }, ...prev].slice(0, 8));
    };
    ws.onerror = () => setLiveStatus('error');
    ws.onclose = () => {
      if (wsRef.current === ws) setLiveStatus('idle');
    };
  };
  const short = s => s ? `${s.slice(0, 6)}...${s.slice(-6)}` : '';
  const btnClass = loading ? 'px-4 py-2 font-medium rounded-full bg-gray-300 dark:bg-gray-700 cursor-not-allowed' : 'px-4 py-2 font-medium rounded-full bg-primary hover:bg-primary/80 text-white';
  const dotClass = liveStatus === 'live' ? 'inline-block w-2 h-2 rounded-full bg-green-500 animate-pulse' : liveStatus === 'error' ? 'inline-block w-2 h-2 rounded-full bg-red-500' : 'inline-block w-2 h-2 rounded-full bg-yellow-500';
  return <div className="p-4 border dark:border-zinc-950/80 rounded-xl bg-white dark:bg-zinc-950/80 shadow-sm">
      <form onSubmit={handleLoad} className="space-y-4">
        <div>
          <label className="block text-sm text-zinc-950/70 dark:text-white/70 mb-1">Kunci API Anda</label>
          <input type="text" value={apiKey} onChange={e => setApiKey(e.target.value)} placeholder="Masukkan kunci API Anda" className="w-full p-2 border rounded dark:bg-zinc-900 dark:border-zinc-700" required />
        </div>
        <div>
          <label className="block text-sm text-zinc-950/70 dark:text-white/70 mb-1">Alamat Dompet</label>
          <input type="text" value={ownerAddress} onChange={e => setOwnerAddress(e.target.value)} placeholder="Alamat dompet Solana" className="w-full p-2 border rounded dark:bg-zinc-900 dark:border-zinc-700" required />
        </div>
        <div className="flex gap-2">
          <button type="submit" disabled={loading} className={btnClass}>
            {loading ? "Memuat..." : "Muat Dompet"}
          </button>
          {liveStatus === 'live' || liveStatus === 'connecting' ? <button type="button" onClick={stopLive} className="px-4 py-2 font-medium rounded-full border border-zinc-300 dark:border-zinc-700">Hentikan umpan langsung</button> : <button type="button" onClick={startLive} className="px-4 py-2 font-medium rounded-full border border-zinc-300 dark:border-zinc-700">Mulai langsung</button>}
        </div>
      </form>
      <p className="mt-3 text-xs text-zinc-500 dark:text-zinc-400">
        Kunci API Anda hanya digunakan di browser Anda untuk memanggil Helius secara langsung. Kunci tersebut tidak pernah dikirim ke tempat lain.
      </p>
      {error ? <div className="mt-4 p-3 bg-red-100 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded text-red-700 dark:text-red-300">
          <strong>Kesalahan:</strong> {error}
        </div> : null}
      {portfolio ? <div className="mt-6 space-y-4">
          <div className="grid grid-cols-2 gap-3 text-center">
            <div className="p-3 rounded-lg border border-zinc-200 dark:border-zinc-800">
              <div className="text-xl font-semibold text-zinc-950 dark:text-white">{portfolio.sol.toFixed(3)}</div>
              <div className="text-xs text-zinc-500 dark:text-zinc-400">SOL</div>
            </div>
            <div className="p-3 rounded-lg border border-zinc-200 dark:border-zinc-800">
              <div className="text-xl font-semibold text-zinc-950 dark:text-white">{portfolio.nftCount}</div>
              <div className="text-xs text-zinc-500 dark:text-zinc-400">NFT</div>
            </div>
          </div>
          {portfolio.tokens.length > 0 ? <div>
              <h4 className="text-sm font-semibold mb-2 text-zinc-950 dark:text-white">Token</h4>
              <ul className="space-y-1 list-none pl-0 m-0">
                {portfolio.tokens.map(t => {
    const info = t.token_info || ({});
    const amount = (info.balance || 0) / 10 ** (info.decimals || 0);
    const name = t.content && t.content.metadata && t.content.metadata.name || info.symbol || "Token tidak dikenal";
    const img = t.content && t.content.links ? t.content.links.image : '';
    return <li key={t.id} className="flex items-center gap-3 text-sm my-0">
                      <img src={img || ''} alt="" className="w-7 h-7 rounded-full bg-zinc-200 dark:bg-zinc-800 my-0 shrink-0" />
                      <span className="flex-1 truncate text-zinc-800 dark:text-zinc-200">{name}</span>
                      <span className="font-mono text-zinc-600 dark:text-zinc-400">{amount.toLocaleString(undefined, {
      maximumFractionDigits: 4
    })}</span>
                    </li>;
  })}
              </ul>
            </div> : null}
          {portfolio.nfts.length > 0 ? <div className="grid grid-cols-4 gap-2">
              {portfolio.nfts.map(nft => <img key={nft.id} src={nft.content && nft.content.files && nft.content.files[0] ? nft.content.files[0].uri : ''} alt={nft.content && nft.content.metadata ? nft.content.metadata.name : 'NFT'} className="rounded-lg border border-zinc-200 dark:border-zinc-800 aspect-square object-cover my-0" />)}
            </div> : null}
        </div> : null}
      {history && history.length > 0 ? <div className="mt-6">
          <h4 className="text-sm font-semibold mb-2 text-zinc-950 dark:text-white">Aktivitas terbaru</h4>
          <ul className="space-y-1 text-sm font-mono list-none pl-0 m-0">
            {history.map(tx => <li key={tx.signature} className="flex items-center justify-between text-zinc-600 dark:text-zinc-400 my-0">
                <span className="flex items-center gap-2">
                  <span className={tx.err ? 'inline-block w-2 h-2 rounded-full bg-red-500' : 'inline-block w-2 h-2 rounded-full bg-green-500'} />
                  <a href={'https://orbmarkets.io/tx/' + tx.signature} target="_blank" rel="noreferrer" className="hover:underline">{short(tx.signature)}</a>
                </span>
                <span className="text-xs">{tx.blockTime ? new Date(tx.blockTime * 1000).toLocaleDateString() : ''}</span>
              </li>)}
          </ul>
        </div> : null}
      {liveStatus !== 'idle' || live.length > 0 ? <div className="mt-6">
          <h4 className="text-sm font-semibold mb-2 text-zinc-950 dark:text-white flex items-center gap-2">
            Umpan langsung
            <span className={dotClass} />
            <span className="text-xs font-normal text-zinc-500 dark:text-zinc-400">{liveStatus}</span>
          </h4>
          {live.length === 0 ? <p className="text-sm text-zinc-500 dark:text-zinc-400">Menunggu transaksi berikutnya yang melibatkan dompet ini.</p> : <ul className="space-y-1 text-sm font-mono list-none pl-0 m-0">
              {live.map((item, i) => <li key={i} className="flex items-center justify-between text-zinc-600 dark:text-zinc-400 my-0">
                  <span className="flex items-center gap-2">
                    <span className="inline-block w-2 h-2 rounded-full bg-green-500" />
                    <a href={'https://orbmarkets.io/tx/' + item.sig} target="_blank" rel="noreferrer" className="hover:underline">{short(item.sig)}</a>
                  </span>
                  <span className="text-xs">{item.at}</span>
                </li>)}
            </ul>}
        </div> : null}
    </div>;
};

Dalam panduan ini, Anda akan membangun dasbor yang menampilkan aset yang dimiliki alamat Solana mana pun dan aktivitasnya saat ini. Anda akan mengambil seluruh portofolio token, NFT, dan SOL dalam satu panggilan [DAS](/docs/id/api-reference/das/getassetsbyowner), memuat riwayat transaksi dengan [`getTransactionsForAddress`](/docs/id/rpc/gettransactionsforaddress) yang eksklusif dari Helius, dan melakukan streaming aktivitas baru secara langsung melalui [`transactionSubscribe`](/docs/id/rpc/websocket/transaction-subscribe). Anda akan mencobanya terlebih dahulu sebagai demo tersemat, lalu membangunnya sebagai aplikasi Next.js lengkap.

## Coba secara langsung

Tempel kunci API Anda dan alamat dompet apa pun untuk melihat ketiga bagian berfungsi bersama: portofolio, riwayat, dan umpan langsung. Ini adalah data yang sama dengan yang akan dirender aplikasi Anda.

<PortfolioTrackerDemo />

***

## Bangun sebagai aplikasi Next.js

<Card title="Grab the full app on GitHub" icon="github" href="https://github.com/helius-labs/quickstart-portfolio-tracker">
  Kloning starter lengkap — aplikasi Next.js final dari panduan ini, yang siap untuk `npm install && npm run dev`.
</Card>

Widget di atas memanggil Helius secara langsung dari browser, sehingga mengekspos kunci API Anda. Dalam aplikasi sebenarnya, Anda menyimpan kunci tersebut di server. Panduan ini menggunakan Next.js App Router dengan dua handler rute agar **kunci Anda tidak pernah sampai ke klien**: satu memproksi panggilan RPC, sementara yang lain meneruskan umpan WebSocket sebagai Server-Sent Events.

<Info>
  **Prasyarat:** Node.js 18+ dan [kunci API Helius](https://dashboard.helius.dev/api-keys).
</Info>

<Steps>
  <Step title="Scaffold the project">
    ```bash theme={"system"}
    npx create-next-app@latest portfolio-tracker --ts --app --tailwind --no-src-dir --eslint --yes
    cd portfolio-tracker
    npm install helius-sdk ws
    npm install --save-dev @types/ws
    ```

    Anda akan menggunakan [Helius SDK](https://github.com/helius-labs/helius-sdk) untuk panggilan RPC dan `ws` untuk umpan WebSocket langsung.

    Tambahkan kunci Anda ke `.env.local`:

    ```bash .env.local theme={"system"}
    HELIUS_API_KEY=YOUR_API_KEY
    ```
  </Step>

  <Step title="Proxy RPC calls through a route handler">
    Langkah ini menyimpan kunci API Anda di sisi server. Klien mengirim metode dan parameter ke sini, lalu handler menjalankannya melalui Helius SDK, yang menyertakan kunci Anda dan menyusun permintaan untuk Anda.

    ```typescript app/api/helius/route.ts theme={"system"}
    import { NextRequest, NextResponse } from "next/server";
    import { createHelius } from "helius-sdk";

    export const runtime = "nodejs";

    const helius = createHelius({
      apiKey: process.env.HELIUS_API_KEY!,
      network: "mainnet",
    });

    export async function POST(req: NextRequest) {
      const { method, params } = await req.json();
      try {
        // The two Helius calls that power this app: the full portfolio and its history.
        const result =
          method === "getAssetsByOwner"
            ? await helius.getAssetsByOwner(params)
            : method === "getTransactionsForAddress"
            ? await helius.getTransactionsForAddress(params)
            : undefined;

        if (result === undefined) {
          return NextResponse.json(
            { error: { message: `Unsupported method: ${method}` } },
            { status: 400 }
          );
        }
        return NextResponse.json({ result });
      } catch (e) {
        return NextResponse.json({ error: { message: (e as Error).message } });
      }
    }
    ```
  </Step>

  <Step title="Relay the live feed over Server-Sent Events">
    Browser tidak dapat membuka WebSocket Helius tanpa kunci. Sebagai gantinya, buka WebSocket di server dan lakukan streaming tanda tangan ke klien dengan SSE.

    ```typescript app/api/stream/route.ts [expandable] theme={"system"}
    import { NextRequest } from "next/server";
    import WebSocket from "ws";

    export const runtime = "nodejs";

    export async function GET(req: NextRequest) {
      const address = req.nextUrl.searchParams.get("address");
      if (!address) return new Response("Missing address", { status: 400 });

      const stream = new ReadableStream({
        start(controller) {
          const encoder = new TextEncoder();
          const send = (data: unknown) =>
            controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));

          const ws = new WebSocket(
            `wss://mainnet.helius-rpc.com/?api-key=${process.env.HELIUS_API_KEY}`
          );

          ws.on("open", () => {
            ws.send(
              JSON.stringify({
                jsonrpc: "2.0",
                id: 1,
                method: "transactionSubscribe",
                params: [
                  // tokenAccounts: "balanceChanged" also matches the wallet's token accounts,
                  // so incoming SPL transfers (which touch an ATA, not the wallet pubkey) show up.
                  { accountInclude: [address], tokenAccounts: "balanceChanged" },
                  {
                    commitment: "confirmed",
                    encoding: "jsonParsed",
                    transactionDetails: "signatures",
                    maxSupportedTransactionVersion: 1,
                  },
                ],
              })
            );
          });

          ws.on("message", (raw) => {
            const msg = JSON.parse(raw.toString());
            const sig = msg?.params?.result?.signature;
            if (sig) send({ signature: sig, at: Date.now() });
          });

          // Tear down the upstream socket when the client disconnects.
          req.signal.addEventListener("abort", () => {
            ws.close();
            controller.close();
          });
        },
      });

      return new Response(stream, {
        headers: {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
          Connection: "keep-alive",
        },
      });
    }
    ```
  </Step>

  <Step title="Render the dashboard">
    Halaman memuat portofolio dan riwayat melalui proksi, lalu berlangganan umpan SSE untuk mendapatkan pembaruan langsung.

    ```tsx app/page.tsx [expandable] theme={"system"}
    "use client";

    import { useEffect, useState } from "react";

    const rpc = async (method: string, params: unknown) => {
      const res = await fetch("/api/helius", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ jsonrpc: "2.0", id: "1", method, params }),
      });
      const json = await res.json();
      if (json.error) throw new Error(json.error.message);
      return json.result;
    };

    export default function Home() {
      const [address, setAddress] = useState(
        "86xCnPeV69n6t3DnyGvkKobf9FdN2H9oiVDdaMpo2MMY"
      );
      const [portfolio, setPortfolio] = useState<any>(null);
      const [history, setHistory] = useState<any[]>([]);
      const [live, setLive] = useState<any[]>([]);
      const [streamAddress, setStreamAddress] = useState<string | null>(null);
      const [error, setError] = useState<string | null>(null);

      const load = async () => {
        setError(null);
        setLive([]);
        setStreamAddress(address); // point the live feed at the wallet you just loaded
        try {
          const [assets, txns] = await Promise.all([
            rpc("getAssetsByOwner", {
              ownerAddress: address,
              page: 1,
              limit: 20,
              displayOptions: { showFungible: true, showNativeBalance: true },
            }),
            rpc("getTransactionsForAddress", [
              address,
              { transactionDetails: "signatures", sortOrder: "desc", limit: 10, filters: { tokenAccounts: "balanceChanged" } },
            ]),
          ]);
          const items = assets.items ?? [];
          setPortfolio({
            sol: (assets.nativeBalance?.lamports ?? 0) / 1e9,
            tokens: items.filter((i: any) => `${i.interface}`.includes("Fungible")),
            nfts: items.filter((i: any) => !`${i.interface}`.includes("Fungible")),
          });
          setHistory(txns.data ?? []);
        } catch (e: any) {
          setError(e.message);
        }
      };

      // Open the live feed only after Load runs, and reopen only when the loaded wallet changes
      // (not on every keystroke in the input).
      useEffect(() => {
        if (!streamAddress) return;
        const source = new EventSource(`/api/stream?address=${streamAddress}`);
        source.onmessage = (e) =>
          setLive((prev) => [JSON.parse(e.data), ...prev].slice(0, 10));
        return () => source.close();
      }, [streamAddress]);

      return (
        <main className="max-w-2xl mx-auto p-8 space-y-6">
          <div className="flex gap-2">
            <input
              className="flex-1 border rounded p-2"
              value={address}
              onChange={(e) => setAddress(e.target.value)}
            />
            <button className="px-4 rounded bg-black text-white" onClick={load}>
              Load
            </button>
          </div>

          {error && <p className="text-red-600">{error}</p>}

          {portfolio && (
            <section className="space-y-6">
              <Stat label="SOL balance" value={portfolio.sol.toFixed(3)} />

              {portfolio.tokens.length > 0 && (
                <div>
                  <h2 className="font-semibold mb-2">Tokens ({portfolio.tokens.length})</h2>
                  <ul className="space-y-1 list-none pl-0">
                    {portfolio.tokens.map((t: any) => {
                      const info = t.token_info ?? {};
                      const amount = (info.balance ?? 0) / 10 ** (info.decimals ?? 0);
                      return (
                        <li key={t.id} className="flex items-center gap-3">
                          <img
                            src={t.content?.links?.image ?? ""}
                            alt=""
                            className="w-8 h-8 rounded-full bg-zinc-200 shrink-0"
                          />
                          <span className="flex-1 truncate">
                            {t.content?.metadata?.name ?? info.symbol ?? "Unknown token"}
                          </span>
                          <span className="font-mono text-sm">
                            {amount.toLocaleString(undefined, { maximumFractionDigits: 4 })}
                          </span>
                        </li>
                      );
                    })}
                  </ul>
                </div>
              )}

              {portfolio.nfts.length > 0 && (
                <div>
                  <h2 className="font-semibold mb-2">NFTs ({portfolio.nfts.length})</h2>
                  <div className="grid grid-cols-3 gap-3">
                    {portfolio.nfts.map((n: any) => (
                      <img
                        key={n.id}
                        src={n.content?.files?.[0]?.uri ?? n.content?.links?.image ?? ""}
                        alt={n.content?.metadata?.name ?? "NFT"}
                        className="rounded-lg aspect-square object-cover border"
                      />
                    ))}
                  </div>
                </div>
              )}
            </section>
          )}

          {history.length > 0 && (
            <section>
              <h2 className="font-semibold mb-2">History</h2>
              <ul className="font-mono text-sm space-y-1 list-none pl-0">
                {history.map((tx) => (
                  <li key={tx.signature}>
                    {tx.err ? "❌" : "✅"}{" "}
                    <a
                      href={`https://orbmarkets.io/tx/${tx.signature}`}
                      target="_blank"
                      rel="noreferrer"
                      className="underline hover:text-black"
                    >
                      {tx.signature.slice(0, 16)}…
                    </a>
                  </li>
                ))}
              </ul>
            </section>
          )}

          {live.length > 0 && (
            <section>
              <h2 className="font-semibold mb-2">Live ⚡</h2>
              <ul className="font-mono text-sm space-y-1 list-none pl-0">
                {live.map((e, i) => (
                  <li key={i}>
                    <a
                      href={`https://orbmarkets.io/tx/${e.signature}`}
                      target="_blank"
                      rel="noreferrer"
                      className="underline hover:text-black"
                    >
                      {e.signature.slice(0, 16)}…
                    </a>
                  </li>
                ))}
              </ul>
            </section>
          )}
        </main>
      );
    }

    function Stat({ label, value }: { label: string; value: string | number }) {
      return (
        <div className="border rounded-lg p-3">
          <div className="text-xl font-semibold">{value}</div>
          <div className="text-xs text-zinc-500">{label}</div>
        </div>
      );
    }
    ```
  </Step>

  <Step title="Run it">
    ```bash theme={"system"}
    npm run dev
    ```

    Buka [http://localhost:3000](http://localhost:3000), tempel alamat dompet, lalu tekan **Load**. Anda akan langsung melihat portofolio dan riwayat, sedangkan umpan langsung akan terisi saat transaksi baru melibatkan dompet tersebut.
  </Step>
</Steps>

## Cara kerjanya

### Satu panggilan DAS untuk seluruh portofolio

`getAssetsByOwner` dengan `showFungible: true` dan `showNativeBalance: true` mengembalikan token fungibel, NFT (standar **dan** terkompresi), serta saldo SOL native dalam satu respons, sehingga Anda tidak memerlukan panggilan `getBalance` atau `getTokenAccountsByOwner` terpisah. Pisahkan `items` berdasarkan `interface` untuk mengelompokkan token dan NFT.

Setiap item memiliki metadatanya sendiri, sehingga panggilan yang sama dapat digunakan untuk kedua daftar. Gunakan `content.metadata.name` untuk nama dan `content.links.image` untuk logo atau karya seni. Token fungibel juga menyertakan objek `token_info` dengan `balance` mentah, `decimals`, dan `symbol` — bagi `balance` dengan `10 ** decimals` untuk mendapatkan jumlah yang mudah dibaca manusia. Tidak setiap token dilengkapi nama atau logo, jadi gunakan nilai cadangan dengan baik jika keduanya tidak tersedia.

### Riwayat lengkap dalam satu permintaan

`getTransactionsForAddress` mengembalikan riwayat transaksi untuk dompet, dan dengan `filters.tokenAccounts: "balanceChanged"`, hasilnya juga menyertakan transfer pada akun token terkait milik dompet, yaitu aktivitas yang akan terlewat oleh `getSignaturesForAddress` biasa. Gunakan `transactionDetails: "full"` untuk mendapatkan data transaksi lengkap, bukan hanya tanda tangan.

### Pembaruan real-time tanpa polling

`transactionSubscribe` mengirim pesan setiap kali transaksi melibatkan dompet, sehingga Anda tidak perlu melakukan polling. Menambahkan `tokenAccounts: "balanceChanged"` ke filter juga akan mencocokkan akun token milik dompet, sehingga transfer SPL yang masuk muncul di umpan — aktivitas yang sama dengan yang dihitung oleh panggilan riwayat. Meneruskan langganan melalui handler rute sisi server menjaga agar kunci API Anda tidak terekspos ke klien.

## Langkah berikutnya

<CardGroup cols={2}>
  <Card title="getAssetsByOwner" icon="images" href="/docs/id/api-reference/das/getassetsbyowner">
    Semua opsi tampilan, paginasi, dan struktur respons aset lengkap.
  </Card>

  <Card title="getTransactionsForAddress" icon="clock-rotate-left" href="/docs/id/rpc/gettransactionsforaddress">
    Pemfilteran lanjutan berdasarkan waktu, slot, token, arah, dan jumlah.
  </Card>

  <Card title="transactionSubscribe" icon="bolt" href="/docs/id/rpc/websocket/transaction-subscribe">
    Opsi filter dan struktur payload untuk umpan WebSocket langsung.
  </Card>

  <Card title="Deploy your own program" icon="rocket" href="/docs/id/quickstart/deploy-program">
    Jalur berikutnya: luncurkan program Solana ke devnet melalui Helius.
  </Card>
</CardGroup>
