Getting a Telegram message the moment a known pump.fun deployer launches a new token is more useful than a dashboard you have to remember to check. Polling an API on a schedule introduces latency and burns API quota. A webhook-driven Telegram bot solves both problems: MadeOnSol pushes the event to your server, your server formats and forwards it, and you get the alert in under a second — on your phone.
This guide builds a complete, production-ready Telegram alert bot that fires on deployer:alert and deployer:bond events. By the end you will have a running Node.js server that verifies signatures, filters for elite deployers, and sends formatted Telegram messages.
Prerequisites
- Node.js 18+
- A MadeOnSol PRO or ULTRA API key — webhooks are not available on the BASIC tier. Get a key at madeonsol.com/developer. PRO is $49/month with up to 3 registered webhooks.
- A Telegram account to create the bot
- A publicly reachable HTTPS server — Telegram cannot push to localhost. For local development, use ngrok to create a tunnel. For production, any VPS with a domain works.
Architecture
The data flow is straightforward:
MadeOnSol API → POST /webhook → Express server → Verify HMAC → Filter deployer tier → Telegram Bot API
MadeOnSol signs every webhook payload with HMAC-SHA256. Your server verifies the signature before acting on the payload. This prevents spoofed requests from triggering fake alerts.
Step 1: Create a Telegram Bot
Open Telegram and start a conversation with @BotFather. Run these commands:
/newbot
BotFather will ask for a name and username. After that, it gives you a bot token — a string like 7123456789:AAHdqTcvCH1vGWJxfSeofSs0K_h3-4EVzMA. Save this.
Next, get your chat ID. Start a conversation with your new bot by sending it any message. Then open this URL in a browser (replace YOUR_TOKEN):
https://api.telegram.org/botYOUR_TOKEN/getUpdates
Look for "chat":{"id":...} in the JSON response. That number is your chat ID. For group chats, the ID will be negative (e.g., -1001234567890).
Store both in your environment:
TELEGRAM_BOT_TOKEN=7123456789:AAHdqTcvCH1vGWJxfSeofSs0K_h3-4EVzMA
TELEGRAM_CHAT_ID=123456789
MADEONSOL_API_KEY=msk_your_key_here
WEBHOOK_SECRET=choose_a_random_secret_string
Step 2: Set Up the Express Server
Initialize the project:
mkdir deployer-alert-bot && cd deployer-alert-bot
npm init -y
npm install express dotenv
npm install -D @types/express @types/node typescript ts-node
Create src/server.ts:
import express, { Request, Response } from 'express';
import crypto from 'crypto';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
// IMPORTANT: Use raw body for HMAC verification — parsed JSON won't match the signature
app.use('/webhook', express.raw({ type: 'application/json' }));
app.use(express.json());
const PORT = process.env.PORT || 4000;
app.post('/webhook', async (req: Request, res: Response) => {
// Verify signature first — reject anything that doesn't match
const isValid = verifySignature(req.body, req.headers['x-madeonsol-signature'] as string);
if (!isValid) {
console.warn('Invalid signature — rejecting request');
return res.status(401).send('Unauthorized');
}
const payload = JSON.parse(req.body.toString());
await handleWebhookEvent(payload);
res.status(200).send('OK');
});
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});
Step 3: Register the Webhook with MadeOnSol
Before events will arrive, you need to tell MadeOnSol where to send them. Make a POST request to https://madeonsol.com/api/v1/webhooks:
// scripts/register-webhook.ts
import dotenv from 'dotenv';
dotenv.config();
async function registerWebhook() {
const response = await fetch('https://madeonsol.com/api/v1/webhooks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.MADEONSOL_API_KEY}`,
},
body: JSON.stringify({
url: 'https://your-server.example.com/webhook',
secret: process.env.WEBHOOK_SECRET,
events: ['deployer:alert', 'deployer:bond'],
enabled: true,
}),
});
const data = await response.json();
console.log('Webhook registered:', data);
}
registerWebhook().catch(console.error);
Run this once: npx ts-node scripts/register-webhook.ts
The response includes a webhook ID — save it if you need to update or delete the webhook later.
Step 4: Verify the HMAC Signature
Every incoming request from MadeOnSol includes an X-MadeOnSol-Signature header containing an HMAC-SHA256 hex digest of the raw request body, keyed with your webhook secret. Never skip this check in production — it is the only thing preventing arbitrary POST requests from triggering your bot.
function verifySignature(rawBody: Buffer, signature: string): boolean {
if (!signature) return false;
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(rawBody)
.digest('hex');
// Use timingSafeEqual to prevent timing attacks
try {
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
} catch {
return false;
}
}
Two important details:
- Use
express.raw() on the webhook route, not express.json(). Once the body is parsed to an object, the byte-for-byte match against the signature will fail.
- Use
crypto.timingSafeEqual instead of === to prevent timing-based attacks.
Step 5: Format and Send the Telegram Message
Add the Telegram send function:
async function sendTelegram(text: string): Promise<void> {
const url = `https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`;
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: process.env.TELEGRAM_CHAT_ID,
text,
parse_mode: 'HTML',
disable_web_page_preview: true,
}),
});
}
Now write the event handler that formats deployer events:
interface DeployerAlertPayload {
event: 'deployer:alert' | 'deployer:bond';
data: {
token_address: string;
token_name: string;
token_symbol: string;
deployer_address: string;
deployer_tier: 'elite' | 'good' | 'moderate' | 'rising' | 'cold';
deployer_win_rate: number;
deployer_total_launches: number;
market_cap_usd: number;
kol_buys: number;
timestamp: string;
};
}
async function handleWebhookEvent(payload: DeployerAlertPayload): Promise<void> {
const { event, data } = payload;
if (event === 'deployer:alert' || event === 'deployer:bond') {
await handleDeployerEvent(event, data);
}
}
async function handleDeployerEvent(
event: string,
data: DeployerAlertPayload['data']
): Promise<void> {
const tierEmoji: Record<string, string> = {
elite: '[ELITE]',
good: '[GOOD]',
moderate: '[MOD]',
rising: '[RISING]',
cold: '[COLD]',
};
const eventLabel = event === 'deployer:bond' ? 'Bonded' : 'New Launch';
const tier = tierEmoji[data.deployer_tier] || data.deployer_tier.toUpperCase();
const mcap = data.market_cap_usd
? `$${(data.market_cap_usd / 1000).toFixed(1)}k`
: 'N/A';
const message = [
`<b>${tier} Deployer — ${eventLabel}</b>`,
``,
`Token: <b>${data.token_name} (${data.token_symbol})</b>`,
`CA: <code>${data.token_address}</code>`,
``,
`Deployer Win Rate: <b>${(data.deployer_win_rate * 100).toFixed(1)}%</b>`,
`Total Launches: ${data.deployer_total_launches}`,
`Market Cap: ${mcap}`,
`KOL Buys: ${data.kol_buys}`,
``,
`<a href="https://pump.fun/coin/${data.token_address}">pump.fun</a> | <a href="https://dexscreener.com/solana/${data.token_address}">DexScreener</a>`,
].join('\n');
await sendTelegram(message);
console.log(`Alert sent for ${data.token_symbol} (${data.deployer_tier} deployer)`);
}
Step 6: Filter for Elite Deployers Only
If you want to reduce noise and only alert on the highest-confidence deployers, filter by tier before sending:
const ALERT_TIERS: Set<string> = new Set(['elite', 'good']);
async function handleDeployerEvent(
event: string,
data: DeployerAlertPayload['data']
): Promise<void> {
// Drop anything below your threshold
if (!ALERT_TIERS.has(data.deployer_tier)) {
console.log(`Skipping ${data.token_symbol} — tier: ${data.deployer_tier}`);
return;
}
// ... rest of formatting and send
}
Deployer tiers from the MadeOnSol API, ranked highest to lowest: elite, good, moderate, rising, cold. Elite deployers have the strongest historical track records. Starting with just elite will give you a low-volume, high-signal feed.
You can also filter on deployer_win_rate directly if you want finer control:
if (data.deployer_win_rate < 0.60) return; // Require 60%+ win rate
Step 7: Deploy to a Public Server
MadeOnSol needs a publicly reachable HTTPS URL to deliver webhooks. Your options:
For local development — ngrok:
npx ngrok http 4000
ngrok gives you a temporary https://xxxx.ngrok.io URL. Use that when registering the webhook. Note that the URL changes every time you restart ngrok (on the free tier), so you will need to re-register the webhook.
For production — any VPS with nginx:
Set up nginx as a reverse proxy in front of your Node server. Your webhook URL becomes https://yourdomain.com/webhook. Make sure your TLS certificate is valid — MadeOnSol will not deliver to endpoints with invalid certificates.
A minimal PM2 setup to keep the process alive:
npm install -g pm2
pm2 start dist/server.js --name deployer-bot
pm2 save
pm2 startup
Testing Your Webhook
Once deployed, trigger a test delivery from the MadeOnSol API to verify your server is receiving and processing payloads correctly:
curl -X POST https://madeonsol.com/api/v1/webhooks/test \
-H "Authorization: Bearer msk_your_key_here" \
-H "Content-Type: application/json" \
-d '{"webhook_id": "your_webhook_id", "event": "deployer:alert"}'
This sends a synthetic payload to your registered URL. Check your server logs to confirm receipt, and check Telegram to confirm the message arrived.
Troubleshooting: Auto-Disable Behavior
MadeOnSol will automatically disable a webhook after 10 consecutive delivery failures. A failure is any response with a non-2xx status code, or a timeout after 10 seconds.
If your bot goes offline for maintenance or crashes, the webhook will be disabled and you will stop receiving events. To re-enable:
curl -X PATCH https://madeonsol.com/api/v1/webhooks/YOUR_WEBHOOK_ID \
-H "Authorization: Bearer msk_your_key_here" \
-H "Content-Type: application/json" \
-d '{"enabled": true}'
MadeOnSol retries failed deliveries at 5 seconds, then 30 seconds, then 2 minutes before counting a delivery as failed. This means transient server restarts (under 5 seconds) are unlikely to cause missed events.
To check current webhook status and see recent delivery history:
curl https://madeonsol.com/api/v1/webhooks/YOUR_WEBHOOK_ID \
-H "Authorization: Bearer msk_your_key_here"
Complete Server Code
Here is the full src/server.ts with all pieces assembled:
import express, { Request, Response } from 'express';
import crypto from 'crypto';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
app.use('/webhook', express.raw({ type: 'application/json' }));
app.use(express.json());
const ALERT_TIERS = new Set(['elite', 'good']);
function verifySignature(rawBody: Buffer, signature: string): boolean {
if (!signature) return false;
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET!)
.update(rawBody)
.digest('hex');
try {
return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
} catch {
return false;
}
}
async function sendTelegram(text: string): Promise<void> {
await fetch(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: process.env.TELEGRAM_CHAT_ID,
text,
parse_mode: 'HTML',
disable_web_page_preview: true,
}),
});
}
app.post('/webhook', async (req: Request, res: Response) => {
const sig = req.headers['x-madeonsol-signature'] as string;
if (!verifySignature(req.body, sig)) {
return res.status(401).send('Unauthorized');
}
const payload = JSON.parse(req.body.toString());
const { event, data } = payload;
if ((event === 'deployer:alert' || event === 'deployer:bond') && ALERT_TIERS.has(data.deployer_tier)) {
const eventLabel = event === 'deployer:bond' ? 'Bonded' : 'New Launch';
const mcap = data.market_cap_usd ? `$${(data.market_cap_usd / 1000).toFixed(1)}k` : 'N/A';
const tierLabel = data.deployer_tier === 'elite' ? '[ELITE]' : '[GOOD]';
const message = [
`<b>${tierLabel} Deployer — ${eventLabel}</b>`,
``,
`Token: <b>${data.token_name} (${data.token_symbol})</b>`,
`CA: <code>${data.token_address}</code>`,
``,
`Win Rate: <b>${(data.deployer_win_rate * 100).toFixed(1)}%</b>`,
`Launches: ${data.deployer_total_launches}`,
`Market Cap: ${mcap}`,
`KOL Buys: ${data.kol_buys}`,
``,
`<a href="https://pump.fun/coin/${data.token_address}">pump.fun</a> | <a href="https://dexscreener.com/solana/${data.token_address}">DexScreener</a>`,
].join('\n');
await sendTelegram(message);
console.log(`[${new Date().toISOString()}] Alert sent: ${data.token_symbol} (${data.deployer_tier})`);
}
res.status(200).send('OK');
});
app.listen(process.env.PORT || 4000, () => {
console.log(`Webhook server listening on port ${process.env.PORT || 4000}`);
});
Next Steps
This bot handles deployer:alert and deployer:bond. You can extend it to also handle kol:trade and kol:coordination events with the same pattern — just add cases in the event handler.
If you want to go beyond alerts and add trade execution, combine this webhook server with the Jupiter swap API. The deployer alert gives you the token address; Jupiter handles the swap. That wiring is covered in How to Build a Solana Trading Bot.
See the MadeOnSol API docs for full webhook payload schemas, rate limits, and tier comparison.