Real-Time Solana Wallet Alerts via Webhooks: Integration Guide (2026)
Deep dive into configuring and consuming MadeOnSol wallet_tracker:event webhooks for real-time Solana wallet notifications. Covers payload structure, event types, filtering, signature verification, and retry handling.
MadeOnSol··Updated · 13 min read
Disclosure: This article contains affiliate links. If you sign up through them, MadeOnSol may earn a commission at no extra cost to you. This never affects our rankings, ratings, or reviews.
Building a product on Solana or Robinhood Chain data?
Skip the Geyser pipeline — embed KOL flow, deployer reputation, and the all-DEX firehose over REST, WebSocket, or webhooks. The Business tier (€400/mo) is the self-serve embed license at 500k calls/day — Enterprise adds white-label & redistribution.
Building a product on Solana or Robinhood Chain data?
MadeOnSol isn't an RPC or another generic token API — it's Solana & Robinhood Chain memecoin intelligence that's painful to build in-house, pulled from dual-region gRPC shred streams and our own Robinhood Chain node, pushed to your app as events land.
Signals you'd otherwise build
KOL & smart-money flow from 2,000+ labeled wallets, deployer reputation, coordination clusters, linked-wallet entity resolution, and an all-DEX firehose.
Register your endpoint with POST /api/v1/webhooks. The server generates the signing secret and returns it once, as webhook.secret in the response. Store it (for example as WEBHOOK_SECRET); you cannot read it back later.
import express from 'express';
import crypto from 'crypto';
const app = express();
// IMPORTANT: raw body for this route, and no app.use(express.json()) in front of it
app.post(
'/webhook/wallet-tracker',
express.raw({ type: 'application/json' }),
(req, res) => {
// Step 1: Verify the signature before doing anything else
const ok = verifyMadeOnSol(
req.body,
req.get('X-MadeOnSol-Timestamp'),
req.get('X-MadeOnSol-Signature'),
process.env.WEBHOOK_SECRET!,
);
if (!ok) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Step 2: Acknowledge immediately, do not block on processing
res.status(200).json({ received: true });
// Step 3: Process asynchronously
const payload = JSON.parse(req.body.toString('utf8'));
handleEvent(payload).catch(console.error);
}
);
The ordering here is deliberate:
Verify the signature first to reject forged requests before touching the payload
Return 200 immediately. Each delivery attempt times out after 10 seconds, and a timeout is retried
Process asynchronously so errors in your handler do not affect delivery acknowledgement
Signature Verification
Every delivery carries two headers you need:
X-MadeOnSol-Timestamp: unix time in milliseconds, generated fresh for every delivery attempt
X-MadeOnSol-Signature: the lowercase hex HMAC-SHA256 of the string `${timestamp}.${rawBody}`, keyed with your webhook secret exactly as the API returned it. The header has no algorithm prefix, and the secret is used as a plain string (do not hex-decode it)
Compute the HMAC over the exact bytes you received, never over re-serialized JSON:
// rawBody: the exact request bytes (never re-serialized JSON)
// secret: the webhook secret exactly as the API returned it (do not hex-decode)
function verifyMadeOnSol(rawBody: Buffer | string, timestamp: string | undefined, signature: string | undefined, secret: string): boolean {
// X-MadeOnSol-Timestamp: unix milliseconds, fresh on every retry
if (typeof timestamp !== "string" || !/^\d{1,16}$/.test(timestamp)) return false;
// Replay window: your choice, the server does not enforce one
if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60_000) return false;
// X-MadeOnSol-Signature: 64 hex characters, bare (no algorithm prefix)
if (typeof signature !== "string" || !/^[0-9a-f]{64}$/i.test(signature)) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest();
// Both sides are exactly 32 bytes here, so this compares in constant time and cannot throw
return crypto.timingSafeEqual(expected, Buffer.from(signature, "hex"));
}
The Business tier (€400/mo, self-serve) licenses you to display MadeOnSol data inside your own product — 500k calls/day, 10 WS + 5 firehose connections. Enterprise above adds white-label & redistribution rights and custom endpoints.
Evaluate first, commit later
Test everything on a free ULTRA key — no commitment. If it fits, we scope volume or white-label pricing.
Polling the Wallet Tracker API on a schedule works well for many use cases — batch analytics, daily reports, backfill jobs. But when you need to react to a swap as it happens rather than minutes later, polling falls short. Webhooks invert the model: instead of asking "what happened recently?" on a timer, you register an endpoint and the API notifies you the moment an event is indexed.
This guide is a complete reference for working with the wallet_tracker:event webhook from the Solana API. It covers everything from initial configuration to production-grade payload handling, including signature verification, event type differences, filtering strategies, and retry semantics. For the broader context of building Solana notification systems, see the Solana alerts and notifications guide.
The Wallet Tracker (the watchlist, the REST trade feed and this webhook) needs a PRO plan or higher; a BASIC key gets a 403 on every /wallet-tracker/* route. See pricing for the plans. New to the Wallet Tracker entirely? Start with the setup tutorial, and pick your watchlist with the KOL smart-money guide.
How Wallet Tracker Webhooks Work
When you configure a webhook URL on your account, the API starts pushing wallet_tracker:event payloads to that endpoint for every swap and transfer made by any wallet in your watchlist. The flow looks like this:
A wallet in your watchlist submits a transaction on Solana
The transaction is confirmed and indexed by the MadeOnSol pipeline
The API constructs a webhook payload and sends a POST request to your registered URL
Your server responds with a 2xx status to acknowledge receipt
If the delivery fails with a 5xx, a 429, a network error or a timeout, the API retries with backoff
The key property to understand: webhooks fire per event, not per poll cycle. If a tracked wallet makes 10 swaps in 30 seconds, you receive 10 separate webhook calls. This is what makes webhooks useful for alert bots — there is no batching delay.
The full endpoint reference, including how to register and update your webhook URL, is in the API docs.
The Webhook Payload Structure
Every wallet_tracker:event webhook delivery is a JSON POST with the same three-field envelope that every MadeOnSol webhook uses:
Unix seconds when MadeOnSol indexed the transaction (normally within a second or two of the chain)
slot
number | null
Solana slot number
replayed
boolean
true when the event was recovered after a stream reconnect rather than seen live
ts
ISO 8601
block_time as an ISO string
Swap events can also carry four optional market-context fields when they are available for the token: mc_change_pct, volume_usd, mev_volume_pct and history_age_seconds. Treat all four as possibly absent. The payload does not include a token symbol or a USD price; look those up from the token endpoints if you need them.
Swap vs Transfer Events
The two event types require different handling logic.
Swap Events (event_type: "swap")
Swap events are DEX trades — the wallet exchanged one token for another through a program like Jupiter, Raydium, or Orca. These events always have an action field ("buy" or "sell"), a sol_amount, and a token_amount.
A "buy" means the wallet spent SOL (or a SOL-denominated token) to acquire a token. A "sell" means the opposite. The sol_amount is normalised to SOL value — even if the underlying trade was USDC → TOKEN, the API converts the USDC value to its SOL equivalent so you have a consistent unit for size filtering.
Swap events are the primary signal source for most use cases — copy trading, whale alerts, KOL tracking. To decide which wallets are worth registering in the first place, our Solana wallet win rate leaderboard API guide covers pulling ranked, performance-filtered addresses to populate your watchlist.
Transfer Events (event_type: "transfer")
Transfer events are native SOL movements into or out of a tracked wallet with no token balance change, so no DEX trade is involved. They have action: null, token_mint: null and token_amount: null. The sol_amount is the SOL that moved, and counterparty is the other wallet when MadeOnSol can match it. SPL token transfers without a swap are not reported as events.
Transfers are useful for:
Funding detection: a wallet receiving fresh SOL right before it starts trading
Portfolio tracking: a whale moving SOL to or from a cold wallet or an exchange
Cash-out signals: SOL leaving a trader's wallet after a run of sells
For most trading signal use cases you will filter to event_type: "swap" only and ignore transfers.
The 5 minute window is a check you choose to apply on your side; MadeOnSol does not enforce one. Because every retry is signed again with a new timestamp, the window never rejects a legitimate retry. For the same check in Python, see verifying MadeOnSol webhook signatures in Node.js and Python.
Do not skip this step in production. Anyone who can POST to your endpoint can forge events if you do not verify the signature.
Idempotent Event Processing
The API retries failed deliveries, and a retry resends the same body, so your handler may see the same event more than once. The envelope has no event id; key on the payload's own identity instead. For wallet_tracker:event that is the transaction signature plus the tracked wallet:
// In-memory for development; use Redis or a unique DB column in production
const processed = new Set<string>();
async function handleEvent(payload: any) {
const { event, data } = payload;
if (event !== 'wallet_tracker:event') return;
// Idempotency check: one event per (transaction, tracked wallet)
const key = `${data.tx_signature}:${data.wallet_address}`;
if (processed.has(key)) {
console.log(`Duplicate event ${key}, skipping`);
return;
}
processed.add(key);
// Route to handler based on event type
if (data.event_type === 'swap') {
await handleSwap(data);
} else if (data.event_type === 'transfer') {
await handleTransfer(data);
}
}
Filtering Strategies
Receiving every event is not the same as using every event. Good filtering is what makes a webhook integration useful rather than overwhelming.
Filter by Event Type
For trading signal use cases, restrict to swaps only:
if (data.event_type !== 'swap') return;
Filter by SOL Size
Small trades are usually noise — test positions, micro-rebalancing, automated dust consolidation:
const MIN_SOL = 5; // adjust to your use case
if (data.sol_amount < MIN_SOL) return;
If you are building a copy-trading feed, you only want buys. If you are building a portfolio exit tracker, you only want sells:
if (data.action !== 'buy') return; // for copy trading
// or
if (data.action !== 'sell') return; // for exit tracking
Stablecoins and Wrapped SOL Are Already Excluded
You do not need a stablecoin filter. MadeOnSol treats SOL, wSOL, USDC and USDT as the quote side of a swap, so token_mint on a swap is always the other token. A USDC- or USDT-quoted swap still arrives, with sol_amount converted to its SOL equivalent. If you want to skip specific tokens, keep your own deny list:
const IGNORED_MINTS = new Set<string>([
// mints you never want alerts for
]);
if (data.token_mint && IGNORED_MINTS.has(data.token_mint)) return;
Enrichment Before Filtering
Sometimes you want to filter on data that is not in the webhook payload — for example, only alerting on tokens below a certain market cap. In that case, use the event as a trigger to fetch enriched data, then decide whether to act:
async function handleSwap(data: any) {
// Fetch token metadata from a price API
const meta = await fetchTokenMetadata(data.token_mint);
// Only alert on tokens with market cap under $5M
if (meta.market_cap_usd > 5_000_000) return;
await sendAlert(data, meta);
}
Retry Semantics and Failure Handling
Understanding retry behaviour helps you design a robust handler.
Each delivery is tried up to 4 times: immediately, then after about 5 seconds, 30 seconds and 2 minutes (each delay has some random jitter). The API retries when your endpoint:
Returns any 5xx status code
Returns 429 (Too Many Requests)
Does not answer within 10 seconds, or the connection fails
The API does not retry a 3xx or any other 4xx (including 400 and 401); those count as a failed delivery straight away. It also does not retry a 2xx. This is why you must acknowledge before processing: if processing fails after you return 200, you will not get a retry. Handle processing errors with your own retry queue.
After 10 consecutive failed deliveries (a delivery counts once, after its retries are used up) the webhook is disabled automatically and the account owner is emailed. A successful delivery resets the counter. Re-enable it with PATCH /api/v1/webhooks/{id} and the body {"is_active": true}, which also resets the failure counter.
For production deployments, a dead-letter queue pattern works well:
const deadLetterQueue: any[] = [];
async function handleEvent(payload: any) {
try {
await processPayload(payload);
} catch (err) {
console.error('Event processing failed — adding to dead letter queue:', err);
deadLetterQueue.push({ payload, failedAt: new Date(), error: String(err) });
// Optionally: persist to DB, send to Slack, etc.
}
}
Testing Your Webhook Integration
Before pointing the API at production infrastructure, test with a local tunnel:
# Using ngrok
ngrok http 3010
# Your webhook URL will be something like:
# https://abc123.ngrok.io/webhook/wallet-tracker
Register this URL with POST /api/v1/webhooks as shown above, then send yourself a signed sample delivery without waiting for a real trade:
The test delivery uses the same headers, signature and envelope as a real one, with a sample payload for the webhook's first subscribed event and "_test": true inside data, so your verification code is exercised end to end. After that, add a wallet to your watchlist and wait for its next trade; the API typically indexes transactions within a few seconds.
Production Checklist
Before going live with a webhook integration:
Signature verification enabled and tested (timestamp header in milliseconds, hex signature with no prefix)
Raw body middleware used (not express.json()) — parsed JSON loses the raw bytes needed for HMAC
Handler acknowledges 200 before processing
Idempotency check on data.tx_signature + data.wallet_address
Dead letter queue or error alerting for failed processing
HTTPS endpoint with a valid certificate (required by the API)
Appropriate filters applied to reduce noise
Tested with POST /api/v1/webhooks/test and with at least one real event from a tracked wallet
Turning these events into an activity feed for end users, with token and counterparty context, is covered on the portfolio trackers solution.
For more on building real-time Solana notification systems beyond wallet tracking, the Solana alerts and notifications guide covers price alerts, token launch notifications, and on-chain event monitoring. The API docs have the complete webhook configuration reference, and our overview of wallet tracking infrastructure shows where webhooks sit next to REST and streaming delivery.