While traditional market analysis reacts to price movements after they occur, on-chain analysis provides a unique leading-indicator perspective. This guide explores how to track the capital flows of the market's most influential participants, including whales, using Covalent's GoldRush Streaming API on the Base blockchain. If you haven’t checked part 1 of this guide, you can check it out here.
A whale discovery system that:
Identifies top trading wallets for any token on the Base network.
Analyses trading volume, profit/loss metrics, and wallet behaviour.
Provides real-time monitoring capabilities for whale activity
Node.js v18+ and npm
Basic knowledge of TypeScript and WebSocket connections
Covalent GoldRush API key
Basic understanding of EVM chains (Base)
Create a new directory and set up a Node.js project:
mkdir whale-tracker
cd whale-tracker
npm init -yInstall the required dependencies:
npm install @covalenthq/client-sdk ws dotenv
npm install typescript ts-node @types/node --save-devInitialize TypeScript configuration:
npx tsc --initCreate your main file:
touch whale-tracker.tsLet's break down the whale tracking step by step. Open your whale-tracker.ts file and follow along:
Step 2.1: Import Dependencies and WebSocket Setup
import { GoldRushClient } from "@covalenthq/client-sdk";
import WebSocket from "ws";
import * as dotenv from "dotenv";
dotenv.config();
(global as any).WebSocket = WebSocket;What this does:
Imports the Covalent GoldRush client for API access.
Imports WebSocket for real-time connections.
Sets up WebSocket globally for our Node.js environment.
Step 2.2: Define TypeScript Interfaces for Type Safety
interface ContractMetadata {
contract_address: string;
contract_name: string;
contract_ticker_symbol: string;
contract_decimals: number;
}
interface WalletData {
token: string;
address: string;
volume: number;
transactions_count: number;
pnl_realized_usd: number;
balance: number;
balance_pretty: string;
pnl_unrealized_usd: number;
contract_metadata: ContractMetadata;
}
interface QueryResponse {
data?: {
upnlForToken: WalletData[];
};
error?: boolean;
error_message?: string;
error_code?: number;
}Ensures type safety when working with the API responses.
Provides autocomplete and error checking in your IDE.
Step 2.3: Initialize the GoldRush Client
const API_KEY = process.env.COVALENT_API_KEY;
const client = new GoldRushClient(
API_KEY,
{},
{
onConnecting: () => console.log(" Connecting to Covalent service..."),
onOpened: () => console.log(" Connected to Covalent service!"),
onClosed: () => console.log(" Disconnected from Covalent service"),
onError: (error) => console.error(" Streaming error:", error),
}
);Replace the API key with your own from Covalent.
Event handlers provide connection status feedback.
The client manages WebSocket connections automatically.
Step 3.1: Function Definition and Setup
const getTopTradingWallets = async (
chainName: string,
tokenAddress: string,
tokenSymbol?: string
): Promise<WalletData[] | null> => {
console.log("\n" + "=".repeat(80));
console.log("Querying Top Trading Wallets");
console.log("Chain: " + chainName);
console.log("Token: " + tokenAddress);
if (tokenSymbol) console.log("Symbol: " + tokenSymbol);
console.log("=".repeat(80) + "\n");This function takes three parameters:
chainName: The blockchain network (e.g., "BASE_MAINNET")
tokenAddress: The contract address of the token to analyze
tokenSymbol: Optional human-readable symbol for display
Step 3.2: GraphQL Query Execution
return new Promise((resolve) => {
let hasResponded = false;
const unsubscribe = client.StreamingService.rawQuery(
`query {
upnlForToken(
chain_name: ${chainName}
token_address: "${tokenAddress}"
) {
token
address
volume
transactions_count
pnl_realized_usd
balance
balance_pretty
pnl_unrealized_usd
contract_metadata {
contract_address
contract_name
contract_ticker_symbol
contract_decimals
}
}
}`,
{},
{Uses Covalent's upnlForToken endpoint
Retrieves 30 days of trading data for the specified token
Returns wallet addresses, volumes, P&L, and metadata
Step 3.3: Response Handling and Error Management
next: (response: QueryResponse) => {
if (hasResponded) return;
hasResponded = true;
// Check for API errors
if (response.error) {
console.error("API Error: " + response.error_message);
console.log("Error Code: " + response.error_code);
unsubscribe();
resolve(null);
return;
}
const wallets = response.data?.upnlForToken;
if (!wallets || wallets.length === 0) {
console.log("No trading data available for this token");
console.log("\nPossible reasons:");
console.log(" • Token has no recent trading activity (last 30 days)");
console.log(" • Token is not indexed on this chain yet");
console.log(" • Feature is in beta and not fully available");
console.log("\nNote: Currently works best with established tokens\n");
unsubscribe();
resolve(null);
return;
}Step 3.4: Data Modelling
console.log("Found " + wallets.length + " trading wallets\n");
// Calculate totals
const totalVolume = wallets.reduce((sum, w) => sum + w.volume, 0);
const totalTransactions = wallets.reduce((sum, w) => sum + w.transactions_count, 0);
const totalRealizedPnL = wallets.reduce((sum, w) => sum + w.pnl_realized_usd, 0);
const totalUnrealizedPnL = wallets.reduce((sum, w) => sum + w.pnl_unrealized_usd, 0);
console.log("Summary Statistics:");
console.log(" Total Volume: " + totalVolume.toLocaleString() + " tokens");
console.log(" Total Transactions: " + totalTransactions.toLocaleString());
console.log(" Total Realized P&L: $" + totalRealizedPnL.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }));
console.log(" Total Unrealized P&L: $" + totalUnrealizedPnL.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }));
console.log("\n" + "-".repeat(80) + "\n");What we're calculating:
Total Volume: Sum of all tokens traded by identified wallets
Total Transactions: Combined trade count across all wallets
Realized P&L: Actual profits/losses from completed trades
Unrealized P&L: Paper gains/losses from current holdings
Step 3.5: Display Top Wallets
const displayCount = Math.min(wallets.length, 10);
console.log("Top " + displayCount + " Wallets by Volume:\n");
wallets.slice(0, displayCount).forEach((wallet, index) => {
const totalPnL = wallet.pnl_realized_usd + wallet.pnl_unrealized_usd;
const shortAddress = wallet.address.slice(0, 8) + "..." + wallet.address.slice(-6);
const tokenSymbol = wallet.contract_metadata.contract_ticker_symbol || "Unknown";
console.log((index + 1) + ". " + shortAddress);
console.log(" Token: " + tokenSymbol);
console.log(" Volume: " + wallet.volume.toLocaleString() + " " + tokenSymbol);
console.log(" Transactions: " + wallet.transactions_count.toLocaleString());
console.log(" Balance: " + parseFloat(wallet.balance_pretty).toLocaleString() + " " + tokenSymbol);
console.log(" Realized P&L: $" + wallet.pnl_realized_usd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }));
console.log(" Unrealized P&L: $" + wallet.pnl_unrealized_usd.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }));
console.log(" Total P&L: $" + totalPnL.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }));
console.log();
});Key wallet metrics displayed:
Trading volume and transaction frequency
Current token holdings
Profit and loss breakdown
Step 3.6: Export Whale Addresses for Monitoring
if (wallets.length > 10) {
console.log("... and " + (wallets.length - 10) + " more wallets\n");
}
// Export whale addresses for monitoring
console.log("-".repeat(80));
console.log("Whale Addresses for Real-time Monitoring:\n");
wallets.slice(0, 10).forEach((wallet, index) => {
console.log((index + 1) + ". " + wallet.address);
});
console.log("\n" + "-".repeat(80));
console.log("Copy these addresses to use with Wallet Activity Stream");
console.log("-".repeat(80) + "\n");
unsubscribe();
resolve(wallets);Step 3.7: Error and Timeout Handling
error: (error) => {
if (hasResponded) return;
hasResponded = true;
console.error("Error fetching top trading wallets:", error);
unsubscribe();
resolve(null);
},
complete: () => {
if (!hasResponded) {
console.log("Query completed without response");
resolve(null);
}
}
// Auto-timeout after 30 seconds
setTimeout(() => {
if (!hasResponded) {
console.log("Query timeout after 30 seconds");
unsubscribe();
resolve(null);
}
}, 30000);const TOKEN_EXAMPLES = {
BASE_MAINNET: {
DEGEN: {
address: "0x4ed4E862860beD51a9570b96d89aF5E1B0Efefed",
symbol: "DEGEN"
},
AXR: {
address: "0x58Db197E91Bc8Cf1587F75850683e4bd0730e6BF",
symbol: "AXR"
},
USDC: {
address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
symbol: "USDC"
}
},
};const main = async (): Promise<void> => {
console.log("Starting Whale Discovery Analysis...\n");
try {
console.log("Analyzing Specific Token on Base");
const degenWhales = await getTopTradingWallets(
"BASE_MAINNET",
TOKEN_EXAMPLES.BASE_MAINNET.DEGEN.address,
TOKEN_EXAMPLES.BASE_MAINNET.DEGEN.symbol
);
if (degenWhales) {
console.log("Successfully retrieved Degen whale data!");
}
} catch (error) {
console.error("Fatal error:", error);
} finally {
setTimeout(async () => {
await client.StreamingService.disconnect();
console.log("\n" + "=".repeat(80));
console.log("Analysis complete! Disconnected from Covalent service.");
console.log("=".repeat(80) + "\n");
process.exit(0);
}, 1000);
}
};
// Run the main function
main().catch(console.error);
// Export for use in other modules
export {
getTopTradingWallets,
TOKEN_EXAMPLES,
type WalletData
};Step 6: Running the Whale Tracker
Open your terminal and run:
npx ts-node whale-tracker.tsYou should see output similar to this:

In Part 3, we'll expand this foundation to include:
Automated alert system for significant whale movements
Portfolio correlation analysis across multiple digital assets
Predictive modeling based on historical whale behavior patterns
Happy building, and see you in Part 3, where we'll bring these whale insights to life with real-time monitoring and insights!