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:
Data Size Filter : Only accounts matching the SPL token account size (165 bytes)
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 ();
})
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
Add your wallet address:
final var tokenOwner = PublicKey . fromBase58Encoded ( "YOUR_WALLET_ADDRESS_HERE" );
Run the example:
mvn exec:java -Dexec.mainClass= "software.sava.examples.SubscribeToTokenAccounts"
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