A single data point rarely tells the full story on Solana memecoins. A 30% price drop could mean a healthy correction before continuation, a whale dumping to reload lower, or the start of a terminal decline. The difference between these scenarios often shows up in other signals — what KOLs are doing, whether smart wallets are exiting or accumulating, and whether the drop triggers recovery patterns.
A proper alert system combines multiple signal types and evaluates them together. This guide builds one using three MadeOnSol data streams: price alert webhooks, KOL coordination WebSocket events, and wallet tracker notifications. If you're new to the SDK, our overview of building Solana trading tools with the MadeOnSol SDK covers the setup these examples assume.
Why Combine Signals Instead of Using One
It is tempting to build an alert system around a single trigger — a price drop, a KOL sell, a whale exit — because each one is simple to wire up on its own. The problem is that each signal in isolation has a high false-positive rate on memecoins. Price moves constantly, KOLs rotate in and out of positions for reasons that have nothing to do with the token's trajectory, and individual wallets rebalance. Acting on any one of them by itself means reacting to noise most of the time.
The approach in this guide treats signals as evidence rather than commands. No single event triggers an action directly. Instead, each stream writes into a shared per-token state object, and a separate evaluation step looks at the combination of recent events to decide whether the evidence is strong enough to act. A price dip with three KOLs exiting in the same window is a different event than a price dip on its own, even though the price data is identical. The whole point of running three streams is to let them confirm or contradict each other.
This also keeps the system honest about latency. Price alerts and the two WebSocket channels each push events the moment they happen, so the state object reflects what is true right now rather than what was true at your last poll. The cost is that you maintain a webhook receiver plus a live socket and reason about events that arrive out of order — which is what the architecture and reconnect logic below are designed to absorb. If you only ever want one of these signals, you do not need this architecture; a single webhook receiver (covered in the dip-buying webhook guide) is simpler and sufficient.
The Three Signal Types
1. Price Dip Alerts
Price alerts fire when a token's market cap drops below a threshold you define. You create them via the API with a drop_pct parameter, and they deliver via webhook or WebSocket. Each alert captures the token's current MC as a baseline and watches for deviations.
Useful for: detecting rapid price movements that require immediate decisions.
2. KOL Coordination Events
The kol:coordination WebSocket channel (on wss://madeonsol.com/ws/v1/stream) streams real-time coordination signals — when multiple tracked KOLs are buying or selling the same token within a short window. The exited_count field tells you how many KOLs have sold, and entered_count tells you how many have bought.
Useful for: understanding whether smart money is bailing or loading up, independent of price action.
3. Wallet Tracker Events
The wallet_tracker:events channel (ULTRA, same socket) lets you track specific wallets (your own, known whales, deployer wallets) and receive push notifications when they execute trades. Each event includes the token, direction (buy/sell), and size. For the full REST and webhook surface behind this stream, see our guide on how to monitor Solana wallet activity with the Wallet Tracker API.
Useful for: tracking specific addresses you trust or fear, and detecting portfolio-relevant moves in real time.
Architecture
The system runs three signal sources over two transports — a webhook receiver for price alerts and one multiplexed WebSocket subscribed to two channels — and funnels all events into a single handler that evaluates the combined signal state:
Price Alerts (webhook POST) ──┐
KOL Coordination (WS channel kol:coordination) ──┼──▶ Unified Alert Handler ──▶ Action
Wallet Tracker (WS channel wallet_tracker:events) ┘
All three streams update a shared state object per token. The handler evaluates the combined state on every new event and decides whether to alert, buy, sell, or wait.
The shared state map is the heart of the design. Each stream is a producer that knows nothing about the others; it only updates its slice of the per-token record and then calls evaluateSignals. This decoupling matters because the producers are independent — the webhook receiver and the WebSocket can drop and reconnect at different times, and each channel handler only ever writes its own slice. Keeping each producer narrow — the KOL handler only touches kolActivity, the wallet handler only appends to walletEvents — means a reconnect on one stream never corrupts the state the others depend on. The setTimeout(connectStream, 5000) reconnect on the close event is deliberate: the WebSocket carrying the KOL tracker and wallet channels will drop periodically, and a five-second backoff re-establishes it (and re-subscribes) without hammering the server.
Setting Up the Streams
import MadeOnSol from "madeonsol";
import WebSocket from "ws";
import express from "express";
const client = new MadeOnSol({ apiKey: process.env.MADEONSOL_API_KEY! });
// Shared state per token
interface TokenState {
lastPriceDrop: { dropPct: number; mcUsd: number; at: Date } | null;
kolActivity: { entered: number; exited: number; at: Date } | null;
walletEvents: Array<{ wallet: string; direction: "buy" | "sell"; at: Date }>;
}
const state = new Map<string, TokenState>();
function getState(mint: string): TokenState {
if (!state.has(mint)) {
state.set(mint, { lastPriceDrop: null, kolActivity: null, walletEvents: [] });
}
return state.get(mint)!;
}
Connect the KOL Coordination and Wallet Tracker Channels
Both push feeds ride the same multiplexed socket, wss://madeonsol.com/ws/v1/stream. You never put your API key on the WebSocket URL: mint a short-lived stream token from POST /api/v1/stream/token (24h expiry, minted with your msk_ key), connect with ?token=, then send a subscribe message naming the channels you want. Nothing is delivered until you subscribe.
async function getStreamToken(): Promise<{ token: string; ws_url: string }> {
const res = await fetch("https://madeonsol.com/api/v1/stream/token", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.MADEONSOL_API_KEY}` },
});
return res.json(); // { token, ws_url: "wss://madeonsol.com/ws/v1/stream" }
}
async function connectStream() {
const { token, ws_url } = await getStreamToken();
const ws = new WebSocket(`${ws_url}?token=${token}`);
ws.on("open", () => {
// kol:coordination is PRO+, wallet_tracker:events is ULTRA (subscriber-scoped)
ws.send(JSON.stringify({
type: "subscribe",
channels: ["kol:coordination", "wallet_tracker:events"],
}));
});
ws.on("message", (data) => {
const event = JSON.parse(data.toString());
if (event.type === "kol:coordination") {
const ts = getState(event.token_mint);
ts.kolActivity = {
entered: event.entered_count,
exited: event.exited_count,
at: new Date(),
};
evaluateSignals(event.token_mint);
}
if (event.type === "wallet_tracker:event") {
const ts = getState(event.token_mint);
ts.walletEvents.push({
wallet: event.wallet,
direction: event.direction,
at: new Date(),
});
// Keep last 50 events per token
if (ts.walletEvents.length > 50) ts.walletEvents.shift();
evaluateSignals(event.token_mint);
}
});
ws.on("close", () => setTimeout(connectStream, 5000));
}
One socket is enough here — PRO allows 2 concurrent connections and ULTRA 3, but a single connection can carry every channel you subscribe to (kol:trades, deployer:alerts, price_alert:events, token:prices, and so on).
Price Alert Webhook Receiver
const app = express();
app.use(express.json());
app.post("/webhook/price", (req, res) => {
// HMAC verification omitted for brevity — see dip-buying webhook guide
const { token_mint, drop_pct_actual, current_mc_usd, event } = req.body;
if (event === "price_alert:dip") {
const ts = getState(token_mint);
ts.lastPriceDrop = { dropPct: drop_pct_actual, mcUsd: current_mc_usd, at: new Date() };
evaluateSignals(token_mint);
}
if (event === "price_alert:recovery") {
handleRecovery(token_mint, req.body);
}
res.status(200).send("OK");
});