Skip to main content
This example demonstrates how to subscribe to token account changes using WebSocket connections. This is useful for monitoring wallet balances, tracking token transfers, and building real-time applications.

Overview

The SubscribeToTokenAccounts example shows how to:
  • Connect to Solana via WebSocket
  • Subscribe to token accounts for a specific owner
  • Filter accounts by size and owner
  • Parse and display token account data in real-time

Complete Example

Here’s the full working example from the Sava examples repository:
package software.sava.examples;

import software.sava.core.accounts.PublicKey;
import software.sava.core.accounts.SolanaAccounts;
import software.sava.core.accounts.token.TokenAccount;
import software.sava.core.rpc.Filter;
import software.sava.rpc.json.http.SolanaNetwork;
import software.sava.rpc.json.http.request.Commitment;
import software.sava.rpc.json.http.ws.SolanaRpcWebsocket;

import java.net.http.HttpClient;
import java.util.List;

public final class SubscribeToTokenAccounts {

  public static void main(final String[] args) throws InterruptedException {
    final var solanaAccounts = SolanaAccounts.MAIN_NET;
    final var tokenProgram = solanaAccounts.tokenProgram();
    final var tokenOwner = PublicKey.fromBase58Encoded(""); // Add your wallet address
    
    try (final var httpClient = HttpClient.newHttpClient()) {
      final var webSocket = SolanaRpcWebsocket.build()
          .uri(SolanaNetwork.MAIN_NET.getWebSocketEndpoint())
          .webSocketBuilder(httpClient)
          .commitment(Commitment.CONFIRMED)
          .solanaAccounts(solanaAccounts)
          .onOpen(ws -> System.out.println("Websocket connected to " + ws.endpoint().getHost()))
          .onClose((ws, statusCode, reason) -> {
            ws.close();
            System.out.format("%d: %s%n", statusCode, reason);
          })
          .onError((ws, throwable) -> {
            ws.close();
            throwable.printStackTrace(System.err);
          })
          .create();

      webSocket.programSubscribe(
          tokenProgram,
          List.of(
              Filter.createDataSizeFilter(TokenAccount.BYTES),
              Filter.createMemCompFilter(TokenAccount.OWNER_OFFSET, tokenOwner)
          ),
          accountInfo -> {
            final var tokenAccount = TokenAccount.read(accountInfo.pubKey(), accountInfo.data());
            System.out.println(tokenAccount);
          });

      webSocket.connect().join();

      Thread.sleep(Integer.MAX_VALUE);
    }
  }
}

Code Breakdown

1. Setup Accounts and Configuration

final var solanaAccounts = SolanaAccounts.MAIN_NET;
final var tokenProgram = solanaAccounts.tokenProgram();
final var tokenOwner = PublicKey.fromBase58Encoded("YOUR_WALLET_ADDRESS");
  • SolanaAccounts.MAIN_NET provides system program addresses for mainnet
  • tokenProgram() returns the SPL Token program ID
  • Replace the empty string with your wallet’s public key to monitor

2. Create WebSocket Connection

final var webSocket = SolanaRpcWebsocket.build()
    .uri(SolanaNetwork.MAIN_NET.getWebSocketEndpoint())
    .webSocketBuilder(httpClient)
    .commitment(Commitment.CONFIRMED)
    .solanaAccounts(solanaAccounts)
    .onOpen(ws -> System.out.println("Websocket connected to " + ws.endpoint().getHost()))
    .onClose((ws, statusCode, reason) -> {
      ws.close();
      System.out.format("%d: %s%n", statusCode, reason);
    })
    .onError((ws, throwable) -> {
      ws.close();
      throwable.printStackTrace(System.err);
    })
    .create();
The WebSocket builder uses a fluent API to configure:
  • URI: Mainnet WebSocket endpoint
  • Commitment: CONFIRMED level ensures data reliability
  • Callbacks: Handlers for connection lifecycle events

3. Subscribe with Filters

webSocket.programSubscribe(
    tokenProgram,
    List.of(
        Filter.createDataSizeFilter(TokenAccount.BYTES),
        Filter.createMemCompFilter(TokenAccount.OWNER_OFFSET, tokenOwner)
    ),
    accountInfo -> {
      final var tokenAccount = TokenAccount.read(accountInfo.pubKey(), accountInfo.data());
      System.out.println(tokenAccount);
    });
The subscription uses two filters:
  1. Data Size Filter: Only accounts matching the SPL token account size (165 bytes)
  2. Memory Comparison Filter: Only accounts owned by the specified wallet
When a matching account is updated, the callback:
  • Parses the account data into a TokenAccount object
  • Prints the token account information

4. Connect and Listen

webSocket.connect().join();
Thread.sleep(Integer.MAX_VALUE);
The connection is established and the program waits indefinitely for updates.

Understanding Filters

Data Size Filter

Filter.createDataSizeFilter(TokenAccount.BYTES)
Filters accounts by exact byte size. SPL token accounts are always 165 bytes, so this ensures we only receive token accounts, not other program data.

Memory Comparison Filter

Filter.createMemCompFilter(TokenAccount.OWNER_OFFSET, tokenOwner)
Compares bytes at a specific offset in the account data. TokenAccount.OWNER_OFFSET (32 bytes) is where the owner’s public key is stored in a token account.

Working with Token Account Data

The TokenAccount class provides structured access to token account data:
TokenAccount tokenAccount = TokenAccount.read(accountInfo.pubKey(), accountInfo.data());

// Access token account fields
PublicKey accountKey = tokenAccount.publicKey();
PublicKey mint = tokenAccount.mint();
PublicKey owner = tokenAccount.owner();
long amount = tokenAccount.amount();
byte decimals = tokenAccount.decimals();

System.out.format("""
  Token Account: %s
  Mint: %s
  Owner: %s
  Amount: %d
  Decimals: %d
  """,
  accountKey.toBase58(),
  mint.toBase58(),
  owner.toBase58(),
  amount,
  decimals
);

Customizing the Example

Monitor a Specific Token Mint

To only track accounts for a specific token (e.g., USDC):
final var usdcMint = PublicKey.fromBase58Encoded(
  "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
);

webSocket.programSubscribe(
    tokenProgram,
    List.of(
        Filter.createDataSizeFilter(TokenAccount.BYTES),
        Filter.createMemCompFilter(TokenAccount.OWNER_OFFSET, tokenOwner),
        Filter.createMemCompFilter(0, usdcMint) // Mint is at offset 0
    ),
    accountInfo -> {
      final var tokenAccount = TokenAccount.read(accountInfo.pubKey(), accountInfo.data());
      System.out.println("USDC Account Update: " + tokenAccount);
    });

Use a Custom RPC Endpoint

For better performance, use a dedicated RPC provider:
import java.net.URI;

final var customEndpoint = URI.create("wss://mainnet.helius-rpc.com/?api-key=YOUR_KEY");

final var webSocket = SolanaRpcWebsocket.build()
    .uri(customEndpoint)
    // ... rest of configuration
    .create();

Error Handling

The example includes basic error handling:
.onError((ws, throwable) -> {
  ws.close();
  throwable.printStackTrace(System.err);
})
For production applications, implement more robust error handling:
.onError((ws, throwable) -> {
  System.err.println("WebSocket error: " + throwable.getMessage());
  
  // Log error details
  logger.error("WebSocket connection failed", throwable);
  
  // Attempt reconnection
  scheduleReconnect();
  
  ws.close();
})

Performance Considerations

WebSocket subscriptions are efficient but can generate high volumes of data on busy wallets. Consider implementing rate limiting or buffering for production applications.
  • Filters: Use precise filters to minimize unnecessary data
  • Commitment Level: CONFIRMED balances reliability and speed
  • Connection Pooling: Reuse HTTP clients across subscriptions
  • Resource Cleanup: Always close WebSocket connections in a try-with-resources block

Running the Example

  1. Add your wallet address:
final var tokenOwner = PublicKey.fromBase58Encoded("YOUR_WALLET_ADDRESS_HERE");
  1. Run the example:
mvn exec:java -Dexec.mainClass="software.sava.examples.SubscribeToTokenAccounts"
  1. Trigger updates by sending tokens to/from the monitored wallet

Next Steps

Address Lookup Tables

Learn about address lookup tables

RPC Methods

Explore all available RPC methods

Build docs developers (and LLMs) love