DEV Community

Lambo Poewert
Lambo Poewert

Posted on

# How I Built a Real-Time Token Price Engine on Solana Without Any Third-Party APIs

How I Built a Real-Time Token Price Engine on Solana Without Any Third-Party APIs

I run MadeOnSol, a Solana trading intelligence platform. Last week I shipped a feature that changed everything about how we handle data: self-calculated token prices and market caps, derived entirely from our own gRPC streams.

No Dexscreener API. No Birdeye. No CoinGecko. Just on-chain data from our own infrastructure.

Here's exactly how it works, the problems I hit, and what I learned.

Why Build This

We track 1,050+ KOL (Key Opinion Leader) wallets on Solana in real-time. When a KOL buys a token, we detect it in under 2 seconds. But we were displaying trades without market cap context — "KOL bought 5 SOL of $TOKEN" tells you nothing without knowing if the token is at $2K or $200K market cap.

We could have called the Dexscreener API, but:

  • Rate limits would throttle us during high-volume periods
  • Extra latency on every trade (network round trip to their API)
  • External dependency = single point of failure
  • Can't customize the pricing model (VWAP, outlier rejection, etc.)
  • Costs money at scale

We already had gRPC streams processing every swap across 11 DEXes on Solana. The price data was flowing through our system — we just weren't capturing it.

The Architecture

Solana Validators
    │
    ▼
gRPC Streams (Frankfurt + New York)
    │
    ▼
DEX Swap Handler (existing)
    │
    ├── KOL trade detection (existing)
    ├── Deployer monitoring (existing)
    └── NEW: Price calculation
            │
            ├── In-memory Map (instant reads)
            └── PostgreSQL (persistence, 5-sec batch writes)
Enter fullscreen mode Exit fullscreen mode

One gRPC stream in, price data for every actively traded token out. No new infrastructure.

The Pricing Method: Balance Diffs, Not Instruction Parsing

This was the key insight and the biggest lesson of the project.

My first attempt parsed each DEX program's swap instruction data to extract amounts. Raydium has one format, Orca another, Jupiter another. Pump.fun bonding curve swaps are completely different. I was maintaining 11 separate parsers and getting ~65% accuracy.

The fix: stop reading the instruction, start reading the result.

Every Solana transaction includes preTokenBalances and postTokenBalances — the token balances of every account before and after the transaction. The difference is what actually moved.

// Before: parse DEX-specific instruction data (fragile, per-DEX)
const amount = decodeRaydiumSwapInstruction(ix.data);

// After: measure what actually changed (universal)
const pre = transaction.meta.preTokenBalances;
const post = transaction.meta.postTokenBalances;
const diff = post.uiAmount - pre.uiAmount; // actual amount moved
Enter fullscreen mode Exit fullscreen mode

This works for every DEX because it measures the outcome, not the instruction. Concentrated liquidity (Orca Whirlpool, Raydium CLMM), multi-hop routes (Jupiter), bonding curves (Pump.fun) — all produce the same standardized balance changes.

Hybrid Approach: Balance Diffs + Pool State

Pure balance diffs give you the executed price including slippage. For thin-liquidity tokens, that can differ from the "true" mid-market price.

Our mc-tracker already had pool-state math for some DEXes — reading AMM reserves and calculating price from the constant product formula. This was getting 87% accuracy for Raydium and Orca.

The final approach is hybrid:

  • Raydium / Orca: pool-state math (87% accuracy, better for mid-price)
  • PumpSwap / Meteora / Pump.fun: executed price from balance diffs (more reliable for these DEXes)
  • Jupiter: balance diffs always (aggregator wraps inner DEXes, balance diff captures net result)

We run both methods and use whichever is more accurate per DEX.

VWAP Over Last Price

Using the last trade's price for market cap is dangerous. One small trade at a crazy price distorts everything.

We use VWAP (Volume Weighted Average Price) over a 5-minute rolling window:

function updateVWAP(token, price, volume) {
  token.trades_5min.push({ price, volume, time: Date.now() });

  // Evict trades older than 5 minutes
  token.trades_5min = token.trades_5min
    .filter(t => t.time > Date.now() - 300000);

  // Volume-weighted average
  const totalValue = token.trades_5min
    .reduce((sum, t) => sum + t.price * t.volume, 0);
  const totalVolume = token.trades_5min
    .reduce((sum, t) => sum + t.volume, 0);

  token.vwap = totalValue / totalVolume;
}
Enter fullscreen mode Exit fullscreen mode

VWAP naturally weighs large trades more than small ones. A $0.01 dust trade barely moves the average, while a 50 SOL trade dominates it.

Outlier Rejection

Even with VWAP, we add explicit outlier rejection:

if (existingVWAP > 0) {
  const deviation = Math.abs(newPrice - existingVWAP) / existingVWAP;

  if (deviation > 0.50) {
    // Price moved 50%+ from VWAP — suspicious
    // Include in VWAP (gets diluted by volume)
    // But DON'T update last_price yet
    token.pendingPrice = newPrice;
    return;
  }

  if (token.pendingPrice) {
    // Next trade confirms or reverts the move
    const confirmDev = Math.abs(newPrice - token.pendingPrice) / token.pendingPrice;
    if (confirmDev < 0.20) {
      // Move confirmed by second trade
      token.lastPrice = newPrice;
      token.pendingPrice = null;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This prevents one flash trade from showing a 10x market cap on our frontend. The price needs a second trade to confirm before we update the display price.

The WSOL Problem

This was the bug that took hours to find.

Some DEXes (like PumpSwap) route through Wrapped SOL (WSOL), while others use native SOL transfers. My original code only checked native SOL balance changes (meta.preBalances[0]). For PumpSwap, the SOL moved via WSOL token transfers — invisible to the native balance check.

The fix checks both:

function getSOLAmount(transaction, balanceChanges) {
  // Check WSOL token balance change first
  const wsolChange = balanceChanges.get(WSOL_MINT);
  if (wsolChange) return Math.abs(wsolChange);

  // Fall back to native SOL balance change
  const nativeDiff = (
    postBalances[0] - preBalances[0] + transaction.meta.fee
  ) / 1e9;
  return Math.abs(nativeDiff);
}
Enter fullscreen mode Exit fullscreen mode

This single fix jumped PumpSwap accuracy from 62% to the same range as our other DEXes.

Token Supply: The One RPC Call We Can't Avoid

Price comes from gRPC for free. But market cap = price × supply, and total supply isn't in the swap transaction. We need one RPC call per new token.

Our approach:

  • First swap detected → queue getTokenSupply() RPC call
  • Rate limited at 20/sec (our RPC plan allows 200/sec)
  • Cache supply in Map + database
  • Poll every 30 minutes for active tokens (burn detection)
  • Burns also caught in real-time via our Token Program gRPC stream

For Pump.fun tokens specifically, supply starts at 1 billion. But devs can burn tokens before bonding, so we still fetch actual supply rather than hardcoding.

In-Memory Map + Database Persistence

Every price update goes to an in-memory JavaScript Map first. API requests read from the Map — instant, no database query.

The Map batch-writes to PostgreSQL every 5 seconds via a single multi-row UPSERT. This means:

  • Real-time reads: instant (Map)
  • Database writes: one query every 5 seconds (efficient)
  • Server restart: load last 7 days from database into Map on startup

The Map holds all tokens traded in the last 7 days. At ~80,000 tokens, that's roughly 25-50MB of RAM including trade windows for VWAP. On a 64GB server, negligible.

Pruning

Most tokens die within hours. Without pruning, the Map and database grow forever.

  • Map: evict tokens with no trades in 7 days (hourly cron)
  • Database: delete tokens with no trades in 90 days (daily pg_cron)
  • If a pruned token trades again, it re-enters automatically on the next swap

What This Unlocked

With self-calculated market caps, every feature gets richer:

  • KOL trade feed: "Cupsey bought at $4.2K MC" instead of just "bought 5 SOL"
  • Deployer alerts: "Elite deployer launched, currently $3K MC"
  • Scout leaderboard: "Scout bought at $2K MC, followers entered at $45K MC"
  • Coordination detection: "3 KOLs piled in at $8K MC"
  • PnL calculations: entry and exit MC from our own data

None of this was possible when we called external APIs for pricing.

Performance

Solana does 300-1,500 DEX swaps per second across our 11 monitored programs. Our price engine handles this comfortably:

  • Map.set() per swap: nanoseconds
  • VWAP calculation: microseconds (sum and division)
  • Database batch write: ~50-100ms every 5 seconds
  • RPC supply fetches: ~45/sec out of 200/sec limit
  • Total RAM for price engine: ~25-50MB

The price engine runs alongside our KOL tracker, deployer scorer, and alpha wallet system on a single dedicated server (Xeon E-2176G, 64GB RAM, NVMe RAID1).

Accuracy

Where we landed after the hybrid approach:

  • Raydium / Orca: ~87% within 1% of Dexscreener (pool-state math)
  • PumpSwap: significantly improved after WSOL fix (balance diffs)
  • Jupiter: accurate (balance diffs capture net multi-hop result)
  • Pump.fun bonding curve: 4-6% deviation (expected — thin liquidity means executed price differs from mid-price)

Still improving Meteora DAMM and fine-tuning edge cases. But for our use case — showing MC context on KOL trades and deployer alerts — this accuracy is more than sufficient.

Key Lessons

  1. Measure the result, not the instruction. Balance diffs are universal across all DEXes. Instruction parsing is fragile and per-DEX.

  2. Hybrid beats pure. Pool-state math is better for some DEXes, executed price for others. Use both.

  3. WSOL vs native SOL will bite you. Every Solana price feed has hit this bug. Check both.

  4. VWAP > last price. One dust trade shouldn't move your market cap. Volume-weight everything.

  5. Outlier rejection prevents embarrassment. A flash trade showing 100x MC on your frontend is worse than showing no MC.

  6. In-memory first, database second. Real-time reads from Map, async batch writes to Postgres. Best of both worlds.

  7. You probably already have the data. If you're processing Solana transactions for any reason, you already have everything needed for pricing. You just need to extract it.


The entire price engine runs on the same server as everything else, adds ~25-50MB of RAM, and uses 22% of our RPC rate limit. No new infrastructure, no external API costs, no rate limit anxiety.

If you're building on Solana and relying on third-party APIs for pricing, consider whether you already have the swap data flowing through your system. You probably do.

madeonsol.com | API Docs | Developer Portal

Top comments (0)