If you are a liquidity provider (LP) on Solana—whether you are farming yields on Raydium, Orca, or Meteora—you are familiar with the "Silent Killer" of DeFi: Impermanent Loss (IL).
There are thousands of articles explaining the theory of IL. They show you the divergence curve, they explain that "if Token A pumps and Token B dumps, you lose money compared to holding," and they give you generic calculators where you manually type in numbers.
This is not one of those articles.
As a developer, manual calculators are useless. You cannot manually check 15 different positions across 3 protocols every time the market moves. You need automation. You need a system that:
Watches your wallet 24/7.
Remembers exactly when you deposited and at what price (the "Anchor").
Calculates the divergence in real-time based on live on-chain data.
Screams at you (via alerts) when your strategy is failing.
In this three-part series, we are going to ignore the generic tools and build our own bare-metal DeFi Dashboard. We will build a TypeScript-based monitoring service that tracks your specific LP positions on Solana, calculates your exact Impermanent Loss, tracks accrued fees, and gives you a "Health Score" for your portfolio.
In Part 1, we tackle the hardest engineering challenge in PnL tracking: Data Persistence. We will build the ingestion engine that detects your deposits and saves the "Initial State" to a local database, creating the baseline against which all future risk is measured.
Why is building an automated IL tracker so hard? It’s not the math (the math is actually simple). It’s the History.
To calculate Impermanent Loss, you need to compare two states of reality:
Current Reality: How many tokens do I have in the pool right now, and what are they worth?
Alternate Reality (HODL): If I had never deposited into the pool and just kept my tokens in my wallet, what would they be worth right now?
The blockchain is very good at telling you the Current Reality (via getAccountInfo or getTokenAccountBalance). It is notoriously difficult at telling you the Historical Reality quickly.
If you deposited into a RAY/USDC pool 4 months ago:
What was the price of RAY at that exact second?
Exactly how many tokens did you input? (Did you zap in? Did you swap half first?)
If your application does not persist this initial state at the moment of deposit (or fetch it reliably from history), you cannot calculate IL. You are flying blind.
We will build a lightweight "State Engine" that solves this.
The Data Source: We will use the Covalent GoldRush API to fetch historical transaction data. Why? Because parsing raw Solana instructions from a standard RPC node to find a specific "Add Liquidity" event is painful. Covalent provides decoded, human-readable logs.
The Storage: A local SQLite database. It's fast, serverless, and perfect for a personal dashboard.
The Logic: A TypeScript script that backfills your history and listens for new deposits.
Let's build the scaffolding. We will use a standard Node.js/TypeScript setup.
mkdir solana-il-monitor
cd solana-il-monitor
npm init -y
npm install typescript ts-node @types/node --save-dev
npm install @covalenthq/client-sdk sqlite3 sqlite open
npm install dotenv
npx tsc --initCreate a .env file to handle your configuration securely.
GOLDRUSH_API_KEY= #Your GoldRush API Key
WALLET_ADDRESS= # e.g., 5oA...Now, let's initialize our database. We need a table that acts as our "Ledger of Truth."
Create a file src/db.ts:
import sqlite3 from 'sqlite3';
import { open, Database } from 'sqlite';
/**
* Initializes the SQLite database and creates the positions table
* if it doesn't already exist.
*/
export async function initDB(): Promise<Database> {
const db = await open({
filename: './positions.db',
driver: sqlite3.Database
});
await db.exec(`
CREATE TABLE IF NOT EXISTS positions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tx_hash TEXT UNIQUE NOT NULL, -- The unique "Add Liquidity" tx hash
pool_address TEXT NOT NULL, -- The address of the LP pool
protocol TEXT, -- e.g., "Raydium", "Orca"
-- Token A (e.g., SOL)
token_a_address TEXT NOT NULL,
token_a_symbol TEXT,
token_a_initial_amount REAL NOT NULL,
token_a_initial_price REAL NOT NULL, -- The price at deposit (USD)
-- Token B (e.g., USDC)
token_b_address TEXT NOT NULL,
token_b_symbol TEXT,
token_b_initial_amount REAL NOT NULL,
token_b_initial_price REAL NOT NULL, -- The price at deposit (USD)
-- LP Token received
lp_mint_address TEXT,
lp_initial_amount REAL,
timestamp INTEGER NOT NULL, -- UNIX timestamp of deposit
is_active BOOLEAN DEFAULT 1 -- To mark if we've withdrawn
);
`);
console.log("💾 Database initialized: positions.db");
return db;
}
This schema is the core of our application. Notice we capture initial_amount AND initial_price. The initial_price is the most critical data point for the "HODL" side of the calculation. If you don't record the price of SOL when you deposited it, you can never calculate your divergence loss accurately. This is our "Anchor".
Now we need to populate this database. We will write an "Ingestor" script using the Covalent GoldRush API.
We are looking for transactions where the user sent tokens to a pool and received LP tokens back.
Solana transactions are notoriously complex. A single transaction might be a 10-step "Zap" that involves multiple swaps before finally depositing.
To cut through the noise, we will use the TransactionService.getAllTransactionsForAddress endpoint from GoldRush. We need this specific endpoint (rather than a simple balance check) because it provides two critical arrays:
log_events: We use this to identify the protocol (e.G., "Raydium").
transfers: A decoded list of all token movements, which shows us exactly what left our wallet and what came in.
Create src/ingestor.ts:
import { GoldRushClient } from "@covalenthq/client-sdk";
import * as dotenv from "dotenv";
import { initDB } from "./db";
import { Database } from "sqlite";
dotenv.config();
const API_KEY = process.env.GOLDRUSH_API_KEY;
const WALLET = process.env.WALLET_ADDRESS;
if (!API_KEY || !WALLET) {
console.error("Missing API Key or Wallet Address");
process.exit(1);
}
const client = new GoldRushClient(API_KEY);
/**
* Main function to scan wallet history and populate the DB.
*/
async function findDepositTransactions() {
console.log(`🕵️ Scanning transaction history for wallet: ${WALLET}...`);
console.log("This may take a moment...");
const db = await initDB();
let allTransactions = [];
let hasMore = true;
let page = 0;
// We must paginate through all transactions
while(hasMore) {
const resp = await client.TransactionService.getAllTransactionsForAddress(
"solana-mainnet",
WALLET,
{
quoteCurrency: "USD",
noLogs: false, // We NEED logs to identify protocols
pageSize: 100,
page
}
);
if (resp.error) {
console.error("API Error:", resp.error_message);
hasMore = false;
return;
}
allTransactions.push(...resp.data.items);
hasMore = resp.data.has_more;
page++;
}
console.log(`Found ${allTransactions.length} total transactions. Filtering for LP deposits...`);
for (const tx of allTransactions) {
// --- Protocol Identification Heuristic ---
// A robust system checks against a list of known Program IDs.
// For Raydium, the V4 Pool Program ID is '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'
const isRaydiumDeposit = tx.log_events.some(log =>
log.sender_address === '675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8' &&
log.decoded?.name === 'raydium_swap' // This is a simplification; you'd look for 'deposit' or 'add_liquidity' instructions
);
// For this guide, we'll use a transfer-based heuristic which is protocol-agnostic
await parseAndStore(db, tx);
}
console.log("✅ History scan complete.");
}
The code above gets the transactions. Now we need to extract the meaning.
On Solana, the transfers array (provided by Covalent) is the source of truth for our purposes. It abstracts away the complex inner instructions and just tells us: "Wallet A sent 100 USDC to Pool B."
We are looking for a specific pattern:
OUT: 2 (or more) token transfers from our wallet.
IN: 1 token transfer to our wallet (the LP token).
Add this parsing logic to src/ingestor.ts:
/**
* Parses a single transaction and stores it in the DB if it's an LP deposit.
* This is the "Anchor" logic.
*/
async function parseAndStore(db: Database, tx: any) {
// Filter transfers where sender == our wallet (Deposits)
const outgoing = tx.transfers.filter((t: any) => t.from_address === WALLET && t.transfer_type === 'OUT');
// Filter transfers where receiver == our wallet (LP Receipt)
const incoming = tx.transfers.filter((t: any) => t.to_address === WALLET && t.transfer_type === 'IN');
// --- The Heuristic ---
// A standard, non-zap LP deposit has 2 outgoing assets and 1 incoming LP token.
if (outgoing.length === 2 && incoming.length === 1) {
const tokenA = outgoing[0];
const tokenB = outgoing[1];
const lpToken = incoming[0];
// This is a weak check, but for a personal tool it's effective:
// Assume the "incoming" token is an LP token if it's not SOL/USDC/etc.
if (!['USDC', 'SOL', 'RAY'].includes(lpToken.contract_ticker_symbol)) {
// --- Historical Price Anchor ---
// Covalent's 'quote_rate' is the historical price *at the time of the transaction*.
// This is the most crucial piece of data.
const priceA = tokenA.quote_rate || 0;
const priceB = tokenB.quote_rate || 0;
// If price data is missing, we can't calculate IL.
if (priceA === 0 || priceB === 0) {
console.warn(`Skipping ${tx.tx_hash}: Missing historical price data.`);
return;
}
// Prepare data for DB
const stmt = await db.prepare(`
INSERT OR IGNORE INTO positions (
tx_hash, protocol, pool_address,
token_a_address, token_a_symbol, token_a_initial_amount, token_a_initial_price,
token_b_address, token_b_symbol, token_b_initial_amount, token_b_initial_price,
lp_mint_address, lp_initial_amount, timestamp, is_active
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
`);
try {
await stmt.run(
tx.tx_hash,
"Raydium", // Protocol identified earlier
tokenA.to_address, // The pool address is where we sent the tokens
tokenA.contract_address,
tokenA.contract_ticker_symbol,
parseFloat(tokenA.delta) / Math.pow(10, tokenA.contract_decimals),
priceA,
tokenB.contract_address,
tokenB.contract_ticker_symbol,
parseFloat(tokenB.delta) / Math.pow(10, tokenB.contract_decimals),
priceB,
lpToken.contract_address,
parseFloat(lpToken.delta) / Math.pow(10, lpToken.contract_decimals),
new Date(tx.block_signed_at).getTime() / 1000
);
console.log(`📌 Stored Anchor: ${tokenA.contract_ticker_symbol}/${tokenB.contract_ticker_symbol} from ${tx.tx_hash}`);
} catch (e) {
if (e.code !== 'SQLITE_CONSTRAINT') { // Ignore if "UNIQUE constraint failed"
console.error(`Failed to store tx ${tx.tx_hash}:`, e.message);
}
} finally {
await stmt.finalize();
}
}
}
}
// Run the ingestor
findDepositTransactions();
Before moving to complex math, we must verify our ingestion works. This is our "Single Source of Truth."
Create a simple check script src/check.ts:
import { initDB } from "./db";
(async () => {
const db = await initDB();
const positions = await db.all("SELECT * FROM positions WHERE is_active = 1");
console.log("\n📊 CURRENT TRACKED LP POSITIONS (THE ANCHOR):");
// Format for a clean console.table view
const formatted = positions.map(p => ({
Protocol: p.protocol,
Pair: `${p.token_a_symbol}/${p.token_b_symbol}`,
"Token A Qty": p.token_a_initial_amount.toFixed(4),
"Token A Price": `$${p.token_a_initial_price.toFixed(4)}`,
"Token B Qty": p.token_b_initial_amount.toFixed(4),
"Token B Price": `$${p.token_b_initial_price.toFixed(4)}`,
Date: new Date(p.timestamp * 1000).toLocaleDateString()
}));
console.table(formatted);
await db.close();
})();
Run your ingestor first, then your check script:
npx ts-node src/ingestor.ts
npx ts-node src/check.tsYour check.ts script should print a beautiful table of your LP positions, anchored to their exact historical price and quantity.

This table is the bedrock of our entire application.
We have successfully bypassed the biggest hurdle in building a custom DeFi dashboard: Memory.
Generic dashboards often guess your entry price based on averages or simple 24h changes. By building our own Ingestion Engine with Covalent, we have a source of truth that is cryptographically verified by the blockchain's own history.
What we have built:
SQLite Schema: A lightweight but robust structure to hold LP state.
History Miner: A script that finds "Add Liquidity" events in your wallet history using Covalent's getAllTransactionsForAddress.
Price Anchor: A mechanism to capture the exact asset price at the time of deposit using Covalent's historical quote_rate.
What is missing?
Right now, we have static history. We know what we did. We don't know what it's worth now.
In Part 2, we will build the Real-Time Calculation Engine. We will:
Fetch live prices and pool reserves using the GoldRush API.
Implement the actual Impermanent Loss Formula in TypeScript.
Compare our "Anchor" (Database) vs. "Reality" (Live Data) to output a real-time PnL report.
The foundation is solid. Now, let's do the math.