No code? Use the Discord bot
Any paid X plan, X plan trial, or Ultra Speed includes the TweetStream Discord bot, which tracks X accounts at standard speed up to your plan's account limit. Sign up with email or Discord, link your Discord identity, and join the TweetStream community. Then install the bot in your own server. The bot delivers X posts, and with an Instagram Tracking plan it can also post the Instagram accounts on that plan. WebSocket, other sources, and customer webhooks require the applicable paid plan or eligible trial.
See the Twitter Discord botWhy Discord webhooks
Discord webhooks accept a single HTTP POST and show the body as a native embed. The destination webhook does not need a bot or gateway connection. TweetStream account setup and access requirements are separate.
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-global.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-global.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.