Why Discord webhooks
Discord webhooks accept a single HTTP POST and show the body as a native embed. You do not need to manage a Discord bot user, OAuth flow, or gateway connection.
TweetStream's filtered stream keeps the worker small and focused on translating WebSocket events into Discord webhook requests.
1. Get a Discord webhook URL
In the target channel, open Edit Channel → Integrations → Webhooks → New Webhook. The webhook URL contains the channel ID and a secret token, so treat it as a credential.
2. Install worker dependencies
These examples assume a new project. Install their dependencies before you run the code.
Node.js:
npm install wsPython:
pip install websockets aiohttp3. Forward tweets (Node.js)
This minimal Node.js worker sends every post from the stream as a Discord embed.
import WebSocket from "ws";
const API_KEY = process.env.TWEETSTREAM_API_KEY!;
const DISCORD_WEBHOOK_URL = process.env.DISCORD_WEBHOOK_URL!;
const WS_URL = "wss://ws.tweetstream.io/ws";
const PROTOCOLS = ["tweetstream.v1", `tweetstream.auth.token.${API_KEY}`];
function normalizeHandle(handle: string) {
return handle.replace(/^@/, "");
}
function postLabel(tweet: { author?: { platform?: string } }) {
if (tweet.author?.platform === "binance_square") return "New Binance Square post";
if (tweet.author?.platform === "truth_social") return "New Truth";
return "New X post";
}
function postUrl(tweet: { author?: { handle?: string; platform?: string }; link?: string; tweetId: string }) {
const handle = normalizeHandle(tweet.author?.handle ?? "");
if (tweet.author?.platform === "binance_square") {
return /^\d+$/.test(tweet.tweetId)
? `https://www.binance.com/en/square/post/${tweet.tweetId}`
: undefined;
}
if (tweet.author?.platform === "truth_social") {
return tweet.link ?? `https://truthsocial.com/@${handle}/posts/${tweet.tweetId}`;
}
return tweet.link ?? `https://x.com/${handle}/status/${tweet.tweetId}`;
}
async function forwardToDiscord(embed: Record<string, unknown>) {
const res = await fetch(DISCORD_WEBHOOK_URL, {
body: JSON.stringify({ embeds: [embed] }),
headers: { "content-type": "application/json" },
method: "POST",
});
if (!res.ok) {
console.error("discord webhook failed", res.status, await res.text());
}
}
function connect() {
const ws = new WebSocket(WS_URL, PROTOCOLS);
ws.on("message", async (raw) => {
const env = JSON.parse(raw.toString());
if (env.t !== "tweet" || env.op !== "content") return;
const tweet = env.d;
const handle = tweet.author?.handle ?? "unknown";
await forwardToDiscord({
author: { name: handle.startsWith("@") ? handle : `@${handle}` },
description: tweet.text,
title: postLabel(tweet),
timestamp: new Date(tweet.createdAt ?? Date.now()).toISOString(),
url: postUrl(tweet),
});
});
ws.on("close", () => {
console.warn("socket closed, reconnecting in 5s");
setTimeout(connect, 5_000);
});
ws.on("error", (err) => {
console.error("socket error", err);
ws.close();
});
}
connect();3b. Forward tweets (Python)
The Python version uses websockets and aiohttp.
import asyncio
import json
import os
import aiohttp
import websockets
API_KEY = os.environ["TWEETSTREAM_API_KEY"]
DISCORD_WEBHOOK_URL = os.environ["DISCORD_WEBHOOK_URL"]
WS_URL = "wss://ws.tweetstream.io/ws"
SUBPROTOCOLS = ["tweetstream.v1", f"tweetstream.auth.token.{API_KEY}"]
def normalize_handle(value):
return (value or "").removeprefix("@")
def post_label(tweet):
platform = tweet.get("author", {}).get("platform", "twitter")
if platform == "binance_square":
return "New Binance Square post"
if platform == "truth_social":
return "New Truth"
return "New X post"
def post_url(tweet, handle):
platform = tweet.get("author", {}).get("platform", "twitter")
if platform == "binance_square":
post_id = str(tweet["tweetId"])
return f"https://www.binance.com/en/square/post/{post_id}" if post_id.isascii() and post_id.isdigit() else None
if platform == "truth_social":
return tweet.get("link") or f"https://truthsocial.com/@{handle}/posts/{tweet['tweetId']}"
return tweet.get("link") or f"https://x.com/{handle}/status/{tweet['tweetId']}"
async def forward(session, tweet):
handle = normalize_handle(tweet["author"].get("handle")) or "unknown"
embed = {
"title": post_label(tweet),
"description": tweet["text"],
"url": post_url(tweet, handle),
"author": {"name": f"@{handle}"},
}
async with session.post(DISCORD_WEBHOOK_URL, json={"embeds": [embed]}) as resp:
if resp.status >= 300:
print("discord webhook failed", resp.status, await resp.text())
async def run():
async with aiohttp.ClientSession() as session:
async with websockets.connect(WS_URL, subprotocols=SUBPROTOCOLS) as ws:
async for raw in ws:
env = json.loads(raw)
if env["t"] == "tweet" and env["op"] == "content":
await forward(session, env["d"])
asyncio.run(run())4. Filter on detected tokens (optional)
Listen for tweet/meta envelopes if you want alerts only after TweetStream detects a ticker, contract address, or DEX URL.
// Only forward tweets that mention tracked tickers or contract addresses.
ws.on("message", async (raw) => {
const env = JSON.parse(raw.toString());
if (env.t !== "tweet") return;
// tweet/meta carries the detection payload.
if (env.op === "meta") {
const tokens = env.d.detected?.tokens ?? [];
if (tokens.length === 0) return;
await forwardToDiscord({
title: `${tokens.length} token${tokens.length > 1 ? "s" : ""} detected`,
description: tokens
.map((t: { symbol: string; priceUsd?: number }) =>
t.priceUsd ? `$${t.symbol} — $${t.priceUsd.toFixed(6)}` : `$${t.symbol}`,
)
.join("\n"),
});
}
});Discord limits and deployment notes
- Discord webhooks allow roughly 30 requests per minute per webhook. Batch or throttle if your stream exceeds that.
- For easier-to-read alerts, use Discord embed fields such as
title,description,url, andauthor. - Keep the webhook URL in a secret manager. Rotating it means regenerating the integration in Discord.
- To route alerts by tracked handle, set
discordWebhookon each account in the dashboard. TweetStream will route matching tweets without a worker process.