In Part 1, we built the most critical and most frequently overlooked piece of any profitability tracker: Memory. Our ingestor.ts script successfully scanned our wallet's Solana transaction history using the Covalent GoldRush API and created a "Ledger of Truth" in an SQLite database.
We now have our "Anchor"—a cryptographically verified record of every LP deposit, containing:
token_a_initial_amount
token_a_initial_price
token_b_initial_amount
token_b_initial_price
timestamp
This database of "what we did" is our anchor in the past. Now, we need to build the engine that compares this anchor to the chaotic reality of the present.
In Part 2, we will build the "Brain." We will write the code to:
Fetch live, real-time pool data (reserves and prices) from Solana.
Implement the core Impermanent Loss (IL) formula in TypeScript, comparing our "Anchor" state to the "Current" state.
Go Beyond IL to calculate true profitability by fetching and including accrued trading fees.
Tackle the advanced case of Concentrated Liquidity (Orca Whirlpools), which uses different math.
Combine everything into a single, runnable script that produces a complete PnL report for every position in our database.
To calculate IL, we need to know the current state of two things for every position in our database:
Current Token Prices: What are SOL and RAY worth right now?
Current Pool State: What is the total reserve of SOL and RAY in the pool, and what is the total supply of LP tokens?
We will use the Covalent GoldRush API for both, ensuring a single, consistent data source.
Let's create a new file, src/monitor.ts. This will be our main application file for this part.
//create src/monitor.ts
import { GoldRushClient } from "@covalenthq/client-sdk";
import * as dotenv from "dotenv";
import { initDB } from "./db"; // Import from Part 1
import { Database } from "sqlite";
dotenv.config();
const API_KEY = process.env.GOLDRUSH_API_KEY!;
const WALLET = process.env.WALLET_ADDRESS!;
const client = new GoldRushClient(API_KEY);
// --- 1. LIVE PRICE FETCHER ---
/**
* Fetches the current USD price for a list of token addresses on Solana.
*/
async function fetchCurrentPrices(tokenAddresses: string[]): Promise<Map<string, number>> {
console.log(`Fetching live prices for ${tokenAddresses.length} tokens...`);
const priceMap = new Map<string, number>();
const resp = await client.PricingService.getTokenPrices(
"solana-mainnet",
"USD",
tokenAddresses
);
if (resp.error) {
console.error("Failed to fetch prices:", resp.error_message);
return priceMap;
}
resp.data.items.forEach(item => {
priceMap.set(item.contract_address, item.price);
});
return priceMap;
}
// --- 2. LIVE POOL STATE FETCHER ---
/**
* Fetches the current reserves and LP supply for a specific pool.
*/
async function fetchCurrentPoolState(poolAddress: string): Promise<any> {
// We use the GoldRush DEXService, which is optimized for AMMs
const resp = await client.DEXService.getPoolByAddress(
"solana-mainnet",
poolAddress
);
if (resp.error) {
console.error(`Failed to fetch pool state for ${poolAddress}:`, resp.error_message);
return null;
}
// We only care about the first pool found
const pool = resp.data.items[0];
return {
current_total_lp_supply: parseFloat(pool.total_liquidity) / Math.pow(10, pool.lp_token_decimals),
current_reserve_a: parseFloat(pool.token_0.reserve) / Math.pow(10, pool.token_0.contract_decimals),
current_reserve_b: parseFloat(pool.token_1.reserve) / Math.pow(10, pool.token_1.contract_decimals),
};
}This code gives us our two crucial data streams. The fetchCurrentPrices function gives us the data for the "HODL" scenario, and fetchCurrentPoolState gives us the data for the "Current LP" scenario.
This is the core of the guide. We will now translate the abstract, academic IL formula into clean, reusable TypeScript functions.
The standard formula for IL is:
𝐼𝐿 = (2 * 𝑠𝑞𝑟𝑡(𝑘)) / (1 + 𝑘) - 1
...where k is the price ratio change.
This formula is elegant but not practical for coding. A developer-friendly approach is to compare two distinct portfolio values:
Impermanent Loss = (Value if HODL) - (Value in LP)
Let's build functions for each part.
This is the "Alternate Reality" calculation. We take the initial quantities of tokens we deposited (from our database) and multiply them by the current prices (from the API).
// Add this to src/monitor.ts
/**
* Calculates the current USD value of our *initial* bundle of tokens.
* This is the "What if I just held?" scenario.
*/
function calculateHodlValue(position: any, currentPrices: Map<string, number>): number {
const priceA = currentPrices.get(position.token_a_address) || 0;
const priceB = currentPrices.get(position.token_b_address) || 0;
const hodlValueA = position.token_a_initial_amount * priceA;
const hodlValueB = position.token_b_initial_amount * priceB;
return hodlValueA + hodlValueB;
}This is the "Current Reality" calculation. When you provide liquidity, the Automated Market Maker (AMM) constantly changes the ratio of your tokens. If SOL pumps, you will end up with less SOL and more USDC.
To find out what you own right now, you must determine your percentage share of the pool and apply it to the pool's current reserves.
// Add this to src/monitor.ts
/**
* Calculates the current USD value of our holdings *inside* the LP.
* This is the "What's my LP token worth?" scenario.
*/
function calculateCurrentLpValue(
position: any,
poolState: any,
currentPrices: Map<string, number>
): { value: number, currentAmountA: number, currentAmountB: number } {
// 1. Calculate our share of the pool
const userShare = position.lp_initial_amount / poolState.current_total_lp_supply;
// 2. Determine our current token amounts
// Note: We check symbol to match our 'A' and 'B' from the DB
let currentAmountA, currentAmountB;
if (position.token_a_symbol === poolState.token_0_symbol) {
currentAmountA = poolState.current_reserve_a * userShare;
currentAmountB = poolState.current_reserve_b * userShare;
} else {
currentAmountA = poolState.current_reserve_b * userShare;
currentAmountB = poolState.current_reserve_a * userShare;
}
// 3. Calculate the USD value of those amounts
const priceA = currentPrices.get(position.token_a_address) || 0;
const priceB = currentPrices.get(position.token_b_address) || 0;
const value = (currentAmountA * priceA) + (currentAmountB * priceB);
return { value, currentAmountA, currentAmountB };
}
Now we just combine them.
// Add this to src/monitor.ts
interface ILReport {
hodlValue: number;
currentLpValue: number;
impermanentLossUSD: number;
impermanentLossPct: number;
}
function calculateImpermanentLoss(
position: any,
poolState: any,
currentPrices: Map<string, number>
): ILReport {
const hodlValue = calculateHodlValue(position, currentPrices);
const { value: currentLpValue } = calculateCurrentLpValue(position, poolState, currentPrices);
if (hodlValue === 0) {
// Avoid divide-by-zero, e.g., if API prices are missing
return { hodlValue: 0, currentLpValue: 0, impermanentLossUSD: 0, impermanentLossPct: 0 };
}
const impermanentLossUSD = hodlValue - currentLpValue;
const impermanentLossPct = (impermanentLossUSD / hodlValue) * 100;
return {
hodlValue,
currentLpValue,
impermanentLossUSD,
impermanentLossPct
};
}
You now have a production-ready function that can calculate the exact IL for any standard AMM position, as long as you have the "Anchor" data.
Here is the secret: Impermanent Loss doesn't matter. Only Total Profitability does.
IL is an opportunity cost (HODL vs. LP), not a real PnL. It famously ignores the entire reason we provide liquidity: to earn trading fees.
If your IL is -5% but you earned +8% in fees, you are +3% profitable. Your strategy is working. A simple IL calculator would tell you you're losing.
True Profitability = (Current LP Value + Accrued Fees) - (Initial Deposit Value)
How do we find Accrued Fees?
This is notoriously difficult on standard AMMs, as fees are auto-compounded back into the pool, growing the pool's $k$ constant.
The Solution: We don't need to find the fees. The calculateCurrentLpValue function we already wrote implicitly includes the fees. Because the pool's reserves (current_reserve_a, current_reserve_b) have grown from fees, our userShare of that pool is automatically worth more.
Therefore, our True PnL (vs. HODL) is:
Net PnL = (Current LP Value) - (HODL Value)
A negative value means IL is larger than your fee rewards. A positive value means your fees have overcome your IL. This is the only number that matters.
This is the advanced topic that separates a basic tracker from a pro tool. On Raydium, you provide liquidity across the entire price range ($0 to $\infty$). On Orca Whirlpools (a fork of Uniswap V3), you provide liquidity in a specific price range (e.g., SOL between $150 and $160).
Why this breaks our math:
IL is NOT gradual: If the price moves outside your range, your position becomes 100% one asset (e.g., all USDC or all SOL). Your IL is no longer "impermanent"—it's fully realized until the price moves back.
Fees are NOT implicit: Fees are collected separately and must be claimed. They do not auto-compound into the pool.
We cannot use the (x*y=k) math from Step 2. We must query the Covalent DEXService for specialized Uniswap V3-style pool data.
// Add this to src/monitor.ts
/**
* Fetches the state of a Concentrated Liquidity position.
* This is different from a standard pool!
*/
async function fetchConcentratedPosition(walletAddress: string, lpMintAddress: string): Promise<any> {
// We use a different GoldRush endpoint for V3-style positions
const resp = await client.DEXService.getLpPosition(
"solana-mainnet",
walletAddress,
lpMintAddress
);
if (resp.error || resp.data.items.length === 0) {
console.error(`Failed to get CLMM position: ${lpMintAddress}`);
return null;
}
const pos = resp.data.items[0];
// This endpoint provides the exact data we need, decoded.
return {
currentAmountA: parseFloat(pos.token_0.amount),
currentAmountB: parseFloat(pos.token_1.amount),
unclaimedFeesA: parseFloat(pos.fee_unclaimed_0),
unclaimedFeesB: parseFloat(pos.fee_unclaimed_1)
};
}
Now, we can write a different PnL function for these positions.
// Add this to src/monitor.ts
async function calculateConcentratedPnL(
position: any,
currentPrices: Map<string, number>
): Promise<any> {
// 1. Get the "HODL" value (same as before)
const hodlValue = calculateHodlValue(position, currentPrices);
// 2. Get the "Current" value from the specialized endpoint
const clmmState = await fetchConcentratedPosition(WALLET, position.lp_mint_address);
if (!clmmState) return { error: "Failed to fetch CLMM state" };
const priceA = currentPrices.get(position.token_a_address) || 0;
const priceB = currentPrices.get(position.token_b_address) || 0;
// 3. Calculate Current Value (Assets + Fees)
const currentAssetValue = (clmmState.currentAmountA * priceA) + (clmmState.currentAmountB * priceB);
const feesValue = (clmmState.unclaimedFeesA * priceA) + (clmmState.unclaimedFeesB * priceB);
const totalCurrentValue = currentAssetValue + feesValue;
// 4. Calculate PnL vs HODL
const netPnlVsHodl = totalCurrentValue - hodlValue;
return {
hodlValue,
currentLpValue: currentAssetValue,
feesValue,
totalCurrentValue,
netPnlVsHodl,
ilAmount: hodlValue - currentAssetValue // IL is just (HODL - Current Assets), *excluding* fees
};
}By using Covalent's specialized endpoints, we have completely bypassed the need to implement the complex sqrtPriceX96 and tick math ourselves.
Let's put it all together. We will now write the main function for src/monitor.ts that reads from our DB and runs all the calculations.
// Add this to the bottom of src/monitor.ts
/**
* MAIN EXECUTION FUNCTION
*/
async function runMonitor() {
const db = await initDB();
const positions = await db.all("SELECT * FROM positions WHERE is_active = 1");
if (positions.length === 0) {
console.log("No active positions found in database. Run 'npm run ingest' first.");
return;
}
console.log(`Monitoring ${positions.length} active LP positions...`);
// 1. Collect all unique tokens we need prices for
const allTokens = new Set<string>();
positions.forEach(p => {
allTokens.add(p.token_a_address);
allTokens.add(p.token_b_address);
});
// 2. Fetch all prices in one batch
const currentPrices = await fetchCurrentPrices(Array.from(allTokens));
// 3. Process each position
const reports = [];
for (const pos of positions) {
console.log(`\nProcessing: ${pos.token_a_symbol}/${pos.token_b_symbol} (Deposited ${new Date(pos.timestamp * 1000).toLocaleDateString()})`);
let report: any;
if (pos.protocol === "Raydium") { // Standard AMM
const poolState = await fetchCurrentPoolState(pos.pool_address);
if (!poolState) continue;
const { hodlValue, currentLpValue, impermanentLossUSD, impermanentLossPct } =
calculateImpermanentLoss(pos, poolState, currentPrices);
report = {
Pair: `${pos.token_a_symbol}/${pos.token_b_symbol}`,
"HODL Value": hodlValue.toFixed(2),
"Current LP Value": currentLpValue.toFixed(2),
"IL ($)": impermanentLossUSD.toFixed(2),
"IL (%)": impermanentLossPct.toFixed(2),
"Total PnL ($)": (currentLpValue - ((pos.token_a_initial_amount * pos.token_a_initial_price) + (pos.token_b_initial_amount * pos.token_b_initial_price))).toFixed(2)
};
} else if (pos.protocol === "Orca") { // Concentrated Liquidity (CLMM)
// (Assuming 'Orca' was stored during ingestion for a CLMM pool)
const pnl = await calculateConcentratedPnL(pos, currentPrices);
report = {
Pair: `${pos.token_a_symbol}/${pos.token_b_symbol}`,
"HODL Value": pnl.hodlValue.toFixed(2),
"Current LP Value": pnl.currentLpValue.toFixed(2),
"Fees ($)": pnl.feesValue.toFixed(2),
"Total PnL ($)": pnl.netPnlVsHodl.toFixed(2),
"IL ($)": pnl.ilAmount.toFixed(2)
};
}
if (report) reports.push(report);
}
// 4. Display Final Report
console.log("\n--- REAL-TIME LP PROFITABILITY REPORT ---");
console.table(reports);
}
runMonitor();
When you run your new monitor.ts script, it will read your database, fetch live data, and print a complete PnL report to your console.

Report Analysis:
RAY/USDC: We have $5.10 of IL (1.02%), but our total PnL is +$65.99. This is a winning position! The fees we earned ($71.09) crushed our impermanent loss.
SOL/USDC: We have negative $30.20 of IL (which is a gain vs. HODL, or -3.18%). This happens if the price returned closer to our entry. Our total PnL is +$23.86.
We have successfully built the "Brain" of our monitor. We now have a single script that can, for any position in our database, instantly calculate its true PnL against the "HODL" alternative.
What we have built:
Live Data Engine: Functions to fetch real-time prices and pool reserves from GoldRush.
IL Calculator: A set of TypeScript functions that implement the AMM math.
True PnL: Logic that goes beyond IL to include fees (implicitly for AMMs, explicitly for CLMMs).
CLMM Solver: A solution for handling concentrated liquidity pools by using Covalent's specialized endpoints, saving us from implementing the complex math ourselves.
What is missing?
Right now, this is a manual tool. You have to run the script to see your PnL. This is not automation.
In Part 3, we will build the "Hands and Voice." We will:
Wrap this script in a loop (e.g., run every 5 minutes).
Add a Threshold Alert: If IL (%) > 5% or Total PnL ($) < $0, send an alert.
Implement Discord/Email notifications to "scream at you" when your strategy is failing.
Discuss advanced triggers for automated rebalancing or withdrawal.