DEV Community

Cover image for Building a Solana Account Explorer: Query On-Chain Data Like a Database
Lymah
Lymah Subscriber

Posted on

Building a Solana Account Explorer: Query On-Chain Data Like a Database

From Concept to Code

In Part 1 of this series, I explained that everything on Solana is an account with five fields: lamports, data, owner, executable, and rent_epoch.

That's the theory. But theory only sticks when you run it against real data.

So I built a command-line account explorer that queries any Solana address and prints all five fields in a readable format. In this post, I'll walk you through how I built it, what I learned from the output, and the three things that genuinely surprised me when I started querying real accounts.

By the end, you'll have a tool you can run against any address on the network β€” and a much sharper intuition for what's actually living on-chain.


What We're Building

A CLI tool that takes any Solana address and returns:

  • Balance in both SOL and lamports
  • Owner program (with friendly names for known programs)
  • Whether it's executable or a data account
  • Data size and a hex preview of the first bytes
  • Rent epoch

Here's what the output looks like against three different account types:

A wallet account:

Wallet account

The Token Program:

Toke program
The System Program itself:

System Program

Same five fields. Three completely different accounts. Let's build it.


Setup

mkdir solana-account-explorer
cd solana-account-explorer
npm init -y
npm install @solana/kit
Enter fullscreen mode Exit fullscreen mode

Create explorer.mjs β€” we use .mjs for ES module syntax with top-level await.


The Full Code

import { createSolanaRpc } from "@solana/kit";

// Known program addresses mapped to friendly names
const KNOWN_PROGRAMS = {
  "11111111111111111111111111111111": "System Program",
  "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA": "Token Program",
  "TokenzQdsByhw9vBi3wq3kQtj78oYCa9PzPgcGW8K3": "Token Program (SPL-2022)",
  "ATokenGPvbdGVqstVQmcLsNZAqeEoctVrGMMXuLYDJ": "Associated Token Program",
  "9xQeWvG816bUx9EPjHmaT23yfBYP5ihLeLSKcsFqrXa": "Raydium Program",
  "JUP4Fb2cqiRUcaTHdrPC8h2gNsszFildan3xnjnZALn": "Jupiter Aggregator",
  "DezXAZ8z7PnrnRJjz3wXBoRgixVpdZuvXAfqJ7DKPs1": "Magic Eden",
  "BPFLoaderUpgradeab1e11111111111111111111111": "BPF Loader (Program Upgrades)",
  "BPFLoader2111111111111111111111111111111111": "BPF Loader",
};

// Get address from command line argument
const address = process.argv[2];

if (!address) {
  console.error("❌ Error: No address provided!");
  console.error("Usage: node explorer.mjs <solana-address>");
  process.exit(1);
}

// Connect to devnet
const rpc = createSolanaRpc("https://api.devnet.solana.com");

async function exploreAccount() {
  try {
    console.log("\nπŸ” Solana Account Explorer\n");
    console.log("━".repeat(60));

    // Fetch balance and account info in parallel
    const [balance, accountInfo] = await Promise.all([
      rpc.getBalance(address).send(),
      rpc.getAccountInfo(address).send(),
    ]);

    // Convert lamports to SOL
    const solBalance = (Number(balance.value) / 1e9).toFixed(9);
    console.log(`\nπŸ“ Address:\n   ${address}\n`);
    console.log(`πŸ’° Balance:\n   ${solBalance} SOL (${balance.value} lamports)\n`);

    if (!accountInfo.value) {
      console.log("ℹ️  This address does not exist on the blockchain yet.\n");
      console.log("━".repeat(60) + "\n");
      return;
    }

    // Resolve owner to friendly name if known
    const owner = accountInfo.value.owner;
    const ownerName = KNOWN_PROGRAMS[owner] || owner;
    console.log(`πŸ‘€ Owner:\n   ${ownerName}\n`);

    // Executable flag
    const isExecutable = accountInfo.value.executable;
    console.log(
      `βš™οΈ  Executable:\n   ${isExecutable ? "Yes (Program Account)" : "No (Data/Wallet Account)"}\n`
    );

    // Rent epoch
    console.log(`πŸ“… Rent Epoch:\n   ${accountInfo.value.rentEpoch}\n`);

    // Data size and hex preview
    const dataLength = accountInfo.value.data.length;
    console.log(`πŸ“¦ Data:\n   Size: ${dataLength} bytes\n`);

    if (dataLength > 0) {
      const displaySize = Math.min(64, dataLength);
      const dataSample = Buffer.from(accountInfo.value.data)
        .slice(0, displaySize)
        .toString("hex");
      console.log(`   First ${displaySize} bytes (hex):`);
      console.log(`   ${dataSample}`);
      if (dataLength > displaySize) {
        console.log(`   ... (${dataLength - displaySize} more bytes omitted)`);
      }
    } else {
      console.log(`   (empty)\n`);
    }

    console.log("\n" + "━".repeat(60) + "\n");
  } catch (error) {
    console.error("❌ Error exploring account:");
    console.error(error.message);
    process.exit(1);
  }
}

exploreAccount();
Enter fullscreen mode Exit fullscreen mode

Run it against any address:

# Query the System Program
node explorer.mjs 11111111111111111111111111111111

# Query the Token Program
node explorer.mjs TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA

# Query your own wallet
node explorer.mjs <your-wallet-address>
Enter fullscreen mode Exit fullscreen mode

What the Output Taught Me

The System Program owns itself β€” sort of

When I queried 11111111111111111111111111111111, I expected to see the System Program as its own owner. Instead I got NativeLoader.

That threw me until I understood: NativeLoader is the Solana runtime's built-in loader. It owns the System Program the same way a kernel owns its own system calls. The System Program is baked into the runtime at a level below the normal account ownership model. It's not a program that was deployed β€” it was compiled into the validator itself.

Key difference from Token Program: The Token Program is owned by BPF Loader (Program Upgrades), which means it was deployed like any other program and can be upgraded. The System Program cannot β€” it's part of the protocol.

The Token Program is only 48 bytes β€” but controls everything

Look at the Token Program output: 48 bytes of data. That's it. 48 bytes of what turns out to be a pointer β€” not the actual bytecode.

This is because the Token Program uses the upgradeable BPF loader pattern. The 48 bytes in the main account point to a separate program data account that holds the actual bytecode. The split exists so the program can be upgraded without changing its address.

This was my first real encounter with indirection on Solana β€” the account you call isn't always where the code lives.

A wallet with 0 bytes of data is a complete account

The wallet output shows Size: 0 bytes and (empty) for data. In Web2 terms that looks like a broken record β€” a row with no content. But on Solana, a wallet doesn't need data. Its entire purpose is to hold lamports. The owner field (System Program) and the lamports field are all it needs to function.

This is the account model in action: the structure is always the same, but some fields are intentionally empty depending on the account's purpose.


Three Things Worth Noticing in Your Own Queries

1. Compare executable accounts vs data accounts

Run the explorer on a program address and a wallet address back to back. Notice how executable: Yes vs executable: No changes everything about what that account does β€” even though both have identical field structures.

2. Watch the owner field

Every account you'll ever encounter is owned by exactly one program. Query five random addresses and look at what owns them. You'll start recognizing patterns: wallets owned by System Program, tokens owned by Token Program, NFT metadata owned by Metaplex's program.

3. The data field is where all the interesting stuff hides

The hex bytes in the data field aren't random β€” they're structured binary data that each program knows how to decode. That wall of hex is actually a token supply, or a price oracle, or a governance vote. Decoding that raw data is exactly what Post 3 covers.


The KNOWN_PROGRAMS Map β€” Why It Matters

One design choice worth explaining: the KNOWN_PROGRAMS lookup table at the top of the file.

Without it, the owner field just shows a raw base58 address like BPFLoaderUpgradeab1e11111111111111111111111. With it, you see BPF Loader (Program Upgrades) β€” immediately meaningful.

This is the same pattern Solana Explorer uses under the hood. As you build on Solana, you'll develop your own mental map of which program addresses do what. This table is a starter version of that map. Add to it as you encounter new programs.


Try It Yourself

# Copy the code above into explorer.mjs, then:
npm install @solana/kit

# Try these three to see the contrast clearly:
node explorer.mjs 11111111111111111111111111111111
node explorer.mjs TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA
node explorer.mjs SysvarC1ock11111111111111111111111111111111
Enter fullscreen mode Exit fullscreen mode

Each one will show you the same five fields, completely different values. That contrast is worth seeing with your own eyes.


What's Next

This explorer shows you the raw bytes in the data field β€” but doesn't tell you what they mean. A token mint's data field contains supply, decimals, and mint authority. A governance account's data field contains vote counts and proposal state. All of it is encoded in binary.

Post 3 covers exactly that: three methods for decoding raw Solana account data, from manual buffer parsing to using Anchor's IDL to using pre-built deserializers. That's where the hex bytes stop being noise and start being information.


Series: The Account Model Foundation

  • Part 1: The Account Model Explained
  • Part 2: Building an Account Explorer (this post)
  • Part 3: Decoding Account Data (coming May 20)
  • Part 4: System Program Deep Dive (coming May 22)

What address did you query first? Drop it in the comments β€” I'd love to see what people are exploring.

Part of the 100 Days of Solana Epoch 1 Writing Challenge.

Top comments (0)