Discord is where most crypto trading communities actually operate. Putting deployer alerts into a dedicated Discord channel means your team sees them in the same place they discuss trades — no separate app to check, no polling dashboard to refresh. The alert arrives as a rich embed with color coding, links, and KOL co-buy data, and it is pushed the moment the on-chain event fires.
This guide covers two approaches:
- Discord webhook URL (simple, no bot account needed) — a channel-specific URL you POST formatted embeds to
- Discord.js bot (more control, can respond to slash commands, manage permissions)
We will focus on approach one because it is faster to set up and handles this use case without the overhead of managing a bot application. Approach two is mentioned where relevant.
Prerequisites
- A Discord server where you have Manage Webhooks permission
- Node.js 18+
- A MadeOnSol PRO or ULTRA API key — webhook delivery requires a paid tier. Free BASIC keys cover REST polling only. Get your key at madeonsol.com/developer.
- A server with a public HTTPS URL to receive MadeOnSol's outbound webhook POSTs
Step 1: Create a Discord Webhook in Your Channel
In Discord, open the channel where you want alerts posted. Go to Edit Channel (gear icon) → Integrations → Webhooks → New Webhook.
Give it a name (e.g., "Deployer Alerts") and optionally a custom avatar. Click Copy Webhook URL — you will get a URL like:
https://discord.com/api/webhooks/1234567890123456789/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Store this in your environment as DISCORD_WEBHOOK_URL. Treat it like a password — anyone with this URL can post to your channel.
Step 2: Set Up the Express Server
mkdir deployer-discord-bot && cd deployer-discord-bot
npm init -y
npm install express dotenv
npm install -D @types/express @types/node typescript ts-node
Create your .env file:
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/your/url
MADEONSOL_API_KEY=msk_your_key_here
WEBHOOK_SECRET=choose_a_random_string
PORT=4000
Create src/server.ts with the raw body middleware on the webhook route — this is required for HMAC verification:
import express, { Request, Response } from 'express';
import crypto from 'crypto';
import dotenv from 'dotenv';
dotenv.config();
const app = express();
// Raw body on webhook route — must come before express.json()
app.use('/webhook', express.raw({ type: 'application/json' }));
app.use(express.json());
app.post('/webhook', async (req: Request, res: Response) => {
const sig = req.headers['x-madeonsol-signature'] as string;
if (!verifySignature(req.body, sig)) {
console.warn('Signature mismatch — dropping request');
return res.status(401).send('Unauthorized');
}
const payload = JSON.parse(req.body.toString());
await handleEvent(payload);
res.status(200).send('OK');
});
app.listen(process.env.PORT || 4000, () => {
console.log(`Server listening on port ${process.env.PORT || 4000}`);
});
Step 3: Register Deployer Events with MadeOnSol
Tell MadeOnSol to deliver deployer:alert and deployer:bond events to your server. The deployer:alert event fires when a tracked deployer creates a new token. The deployer:bond event fires when one of their tokens hits the bonding curve graduation threshold.
// scripts/register-webhook.ts
import dotenv from 'dotenv';
dotenv.config();
async function register() {
const res = 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 res.json();
console.log(JSON.stringify(data, null, 2));
}
register().catch(console.error);
Run it once with npx ts-node scripts/register-webhook.ts. Save the returned webhook ID — you will need it to test, update, or disable the webhook.
Step 4: Verify the HMAC Signature
MadeOnSol signs every webhook payload with HMAC-SHA256 using your webhook secret. The signature is in the X-MadeOnSol-Signature header as a hex string.
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;
}
}
The critical detail: you must compute the HMAC over the raw request bytes, not a re-serialized JSON object. That is why the express route uses express.raw(). If you parse to JSON first and then re-stringify, whitespace and key ordering differences will break the comparison.
Step 5: Format Discord Embed Messages with Tier Color Coding
Discord webhooks accept a JSON body with an embeds array. Each embed can have a color (as a decimal integer), fields, a title, and a URL. This is much more readable than plain text.
Color coding by deployer tier makes it easy to scan the alert feed at a glance:
const TIER_COLORS: Record<string, number> = {
elite: 0xFFD700, // Gold
good: 0x00C851, // Green
moderate: 0xFFAA00, // Amber
rising: 0x33B5E5, // Blue
cold: 0x9E9E9E, // Grey
};
const TIER_LABELS: Record<string, string> = {
elite: 'Elite',
good: 'Good',
moderate: 'Moderate',
rising: 'Rising',
cold: 'Cold',
};
Build the embed:
interface DeployerData {
token_address: string;
token_name: string;
token_symbol: string;
deployer_address: string;
deployer_tier: string;
deployer_win_rate: number;
deployer_total_launches: number;
market_cap_usd: number;
kol_buys: number;
timestamp: string;
}
function buildEmbed(event: string, data: DeployerData) {
const isElite = data.deployer_tier === 'elite';
const color = TIER_COLORS[data.deployer_tier] ?? 0x9E9E9E;
const tierLabel = TIER_LABELS[data.deployer_tier] ?? data.deployer_tier;
const eventLabel = event === 'deployer:bond' ? 'Token Bonded' : 'New Token Launch';
const mcap = data.market_cap_usd
? `$${(data.market_cap_usd / 1000).toFixed(1)}k`
: 'Unknown';
return {
title: `${tierLabel} Deployer — ${eventLabel}`,
url: `https://pump.fun/coin/${data.token_address}`,
color,
fields: [
{
name: 'Token',
value: `**${data.token_name}** (${data.token_symbol})`,
inline: true,
},
{
name: 'Market Cap',
value: mcap,
inline: true,
},
{
name: 'KOL Buys',
value: String(data.kol_buys),
inline: true,
},
{
name: 'Deployer Win Rate',
value: `${(data.deployer_win_rate * 100).toFixed(1)}%`,
inline: true,
},
{
name: 'Total Launches',
value: String(data.deployer_total_launches),
inline: true,
},
{
name: 'Tier',
value: tierLabel,
inline: true,
},
{
name: 'Contract Address',
value: `\`${data.token_address}\``,
inline: false,
},
{
name: 'Links',
value: [
`[pump.fun](https://pump.fun/coin/${data.token_address})`,
`[DexScreener](https://dexscreener.com/solana/${data.token_address})`,
`[Birdeye](https://birdeye.so/token/${data.token_address})`,
].join(' | '),
inline: false,
},
],
footer: {
text: `MadeOnSol Deployer Hunter • ${new Date(data.timestamp).toUTCString()}`,
},
};
}
Step 6: POST to the Discord Webhook URL
async function sendDiscordEmbed(embed: object): Promise<void> {
const response = await fetch(process.env.DISCORD_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ embeds: [embed] }),
});
if (!response.ok) {
const text = await response.text();
console.error(`Discord webhook failed: ${response.status} — ${text}`);
}
}
Discord's webhook API accepts up to 10 embeds per request. For this use case, one embed per alert is cleaner.
Adding KOL Co-Buy Data
The deployer:alert payload includes a kol_buys field — the number of tracked KOL wallets that have already bought the token. This is high-signal data: if three elite KOLs bought a token within seconds of launch, that is worth highlighting separately.
Extend the embed conditionally:
if (data.kol_buys >= 3) {
embed.fields.push({
name: 'KOL Signal',
value: `${data.kol_buys} KOLs already in — early coordination detected`,
inline: false,
});
}
You can also add a @here mention in the webhook body for high-KOL-count events:
await fetch(process.env.DISCORD_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: data.kol_buys >= 3 ? '@here High KOL activity' : null,
embeds: [embed],
}),
});
Filtering: Elite and Good Tiers Only
Moderate and cold deployers launch frequently. Without filtering, you will get noise that drowns out the useful signals. Add a tier gate before building the embed:
const ALLOWED_TIERS = new Set(['elite', 'good']);
async function handleEvent(payload: { event: string; data: DeployerData }) {
const { event, data } = payload;
if (event !== 'deployer:alert' && event !== 'deployer:bond') return;
if (!ALLOWED_TIERS.has(data.deployer_tier)) {
console.log(`Skipping ${data.token_symbol} — tier: ${data.deployer_tier}`);
return;
}
const embed = buildEmbed(event, data);
await sendDiscordEmbed(embed);
console.log(`[${new Date().toISOString()}] Sent: ${data.token_symbol} (${data.deployer_tier})`);
}
If you want separate channels for elite vs. good alerts, keep two Discord webhook URLs and route based on tier.
Complete Server Code
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 TIER_COLORS: Record<string, number> = {
elite: 0xFFD700,
good: 0x00C851,
moderate: 0xFFAA00,
rising: 0x33B5E5,
cold: 0x9E9E9E,
};
const ALLOWED_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;
}
}
function buildEmbed(event: string, data: any) {
const color = TIER_COLORS[data.deployer_tier] ?? 0x9E9E9E;
const eventLabel = event === 'deployer:bond' ? 'Token Bonded' : 'New Token Launch';
const mcap = data.market_cap_usd ? `$${(data.market_cap_usd / 1000).toFixed(1)}k` : 'Unknown';
const tierLabel = data.deployer_tier.charAt(0).toUpperCase() + data.deployer_tier.slice(1);
return {
title: `${tierLabel} Deployer — ${eventLabel}`,
url: `https://pump.fun/coin/${data.token_address}`,
color,
fields: [
{ name: 'Token', value: `**${data.token_name}** (${data.token_symbol})`, inline: true },
{ name: 'Market Cap', value: mcap, inline: true },
{ name: 'KOL Buys', value: String(data.kol_buys), inline: true },
{ name: 'Win Rate', value: `${(data.deployer_win_rate * 100).toFixed(1)}%`, inline: true },
{ name: 'Launches', value: String(data.deployer_total_launches), inline: true },
{ name: 'Tier', value: tierLabel, inline: true },
{ name: 'CA', value: `\`${data.token_address}\``, inline: false },
{
name: 'Links',
value: `[pump.fun](https://pump.fun/coin/${data.token_address}) | [DexScreener](https://dexscreener.com/solana/${data.token_address})`,
inline: false,
},
],
footer: { text: `MadeOnSol • ${new Date(data.timestamp).toUTCString()}` },
};
}
async function sendDiscordEmbed(embed: object, content?: string): Promise<void> {
const res = await fetch(process.env.DISCORD_WEBHOOK_URL!, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: content ?? null, embeds: [embed] }),
});
if (!res.ok) console.error(`Discord error: ${res.status} ${await res.text()}`);
}
app.post('/webhook', async (req: Request, res: Response) => {
if (!verifySignature(req.body, req.headers['x-madeonsol-signature'] as string)) {
return res.status(401).send('Unauthorized');
}
const { event, data } = JSON.parse(req.body.toString());
if ((event === 'deployer:alert' || event === 'deployer:bond') && ALLOWED_TIERS.has(data.deployer_tier)) {
const embed = buildEmbed(event, data);
const mention = data.kol_buys >= 3 ? '@here High KOL activity' : undefined;
await sendDiscordEmbed(embed, mention);
console.log(`[${new Date().toISOString()}] Sent: ${data.token_symbol} (${data.deployer_tier})`);
}
res.status(200).send('OK');
});
app.listen(process.env.PORT || 4000, () => console.log(`Listening on ${process.env.PORT || 4000}`));
Testing and Production Considerations
Test your webhook before going live:
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 triggers a synthetic delivery. You should see the embed appear in your Discord channel within a few seconds.
MadeOnSol retry schedule: failed deliveries are retried at 5 seconds, 30 seconds, and 2 minutes. After 10 consecutive failures, the webhook is auto-disabled. Re-enable it via PATCH /api/v1/webhooks/:id with { "enabled": true }.
Discord rate limits: Discord's webhook API allows roughly 30 POST requests per minute per webhook URL. This is generous for deployer alerts, but if you register the same URL across multiple event types and have high trade volume, you may need to queue outbound requests.
Approach two — Discord.js bot: if you need slash commands (/setfilter, /pause, /resume), per-user DM alerts, or role-based access to configure the bot, switch to a Discord.js application bot. The MadeOnSol webhook handler remains identical — just replace the fetch to Discord's webhook URL with a channel.send({ embeds: [embed] }) call from a Discord.js TextChannel.
See the full API reference and webhook payload schemas at madeonsol.com/developer.