TweetStream

Send TweetStream tweets to Discord with webhooks

Send filtered tweets to any Discord channel through webhooks. No bot user or OAuth required.

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:

bash
npm install ws

Python:

bash
pip install websockets aiohttp

3. Forward tweets (Node.js)

This minimal Node.js worker sends every post from the stream as a Discord embed.

typescript
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.

python
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.

typescript
// 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, and author.
  • Keep the webhook URL in a secret manager. Rotating it means regenerating the integration in Discord.
  • To route alerts by tracked handle, set discordWebhook on each account in the dashboard. TweetStream will route matching tweets without a worker process.

Put the live feed to work

Start with the X accounts that matter, then route events to your bots, alerts, and trading workflows.