载荷与事件
TweetStream 发送紧凑的 JSON envelope。content 承载社交事件;meta 承载富化;生命周期和账号事件使用独立 operation。
推文内容
`tweet/content` 是多数 consumer 首先处理的事件。`author.platform` 为 `twitter` 或 `truth_social`。
type VerifiedType = 'blue' | 'business' | 'government' | 'none';
type TweetVerifiedLabel = {
badge: string | null;
description: string;
url: string | null;
};
type TweetAuthor = {
banner?: string;
bio?: string;
followersCount?: number;
followingCount?: number;
id?: string;
joinedAt?: number;
location?: string;
metrics?: {
likes?: number;
tweets?: number;
};
// Includes a leading @ when present, for example "@elonmusk".
handle?: string;
name?: string;
platform?: 'twitter' | 'truth_social';
profileImage?: string;
url?: string;
verifiedLabel?: TweetVerifiedLabel;
verifiedType?: VerifiedType;
};
type Media = {
url: string;
type?: 'image' | 'video' | 'gif';
thumbnail?: string;
};
type TweetUrl = {
url: string;
name?: string;
tco?: string;
};
type TweetMention = {
handle?: string;
id?: string;
name?: string;
};
type TweetArticle = {
description?: string;
id?: string;
publishedAt?: number;
text?: string;
thumbnail?: string;
title: string;
updatedAt?: number;
url: string;
};
type TweetPollChoice = {
id?: string;
image?: string;
label: string;
votes?: number;
};
type TweetPoll = {
choices: TweetPollChoice[];
endsAt?: number;
totalVotes?: number;
updatedAt?: number;
};
type TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
type TweetReference = {
article?: TweetArticle;
type: 'reply' | 'quote' | 'retweet';
tweetId?: string;
text?: string;
translatedText?: string;
author?: TweetAuthor;
media?: Media[];
poll?: TweetPoll;
quoted?: TweetReference;
};
type TweetContent = {
tweetId: string;
kind: TweetContentKind;
// Original tweet text when the event includes both original and translated text.
text: string;
// Translation, present only when available.
translatedText?: string;
createdAt: number;
author: TweetAuthor;
article?: TweetArticle;
link?: string;
media?: Media[];
mentions?: TweetMention[];
poll?: TweetPoll;
receivedAt?: number;
urls?: TweetUrl[];
ref?: TweetReference;
};
{
"v": 1,
"t": "tweet",
"op": "content",
"id": "1234567890",
"ts": 1702500000130,
"d": {
"tweetId": "1234567890",
"kind": "post",
"text": "新项目已上线。合约地址:9xQeWvG816bUx9EP...",
"translatedText": "New project is live. Contract address: 9xQeWvG816bUx9EP...",
"createdAt": 1702500000000,
"receivedAt": 1702500000123,
"author": {
"id": "123456",
"handle": "@marketdesk",
"name": "Market Desk",
"platform": "twitter",
"followersCount": 125000,
"verifiedType": "business"
},
"link": "https://x.com/marketdesk/status/1234567890"
}
}
富化元数据
`tweet/meta` 以 tweetId 为 key,可能在 content 之后到达。应合并到本地同一条推文记录中,不要把它当成单独提醒,除非你的流程需要富化专用提醒。
type TweetMeta = {
tweetId: string;
ocr?: {
text: string;
};
detected?: {
tokens?: Array<{
symbol?: string;
name?: string;
contract?: string;
chain?: string;
networkId?: number;
priceUsd?: number;
sources: Array<'text' | 'ocr'>;
}>;
cex?: Array<{
exchange: 'bybit' | 'binance' | 'hyperliquid';
symbol?: string;
priceUsd?: number;
url?: string;
baseAsset?: string;
quoteAsset?: string;
sources: Array<'text' | 'ocr'>;
}>;
prediction?: Array<{
exchange: 'polymarket' | 'kalshi';
marketId?: string;
title?: string;
priceUsd?: number;
url?: string;
sources: Array<'text' | 'ocr'>;
}>;
};
};
{
"v": 1,
"t": "tweet",
"op": "meta",
"id": "1234567890",
"ts": 1702500001000,
"d": {
"tweetId": "1234567890",
"ocr": {
"text": "Chart showing SOL breakout at $100"
},
"detected": {
"tokens": [
{
"symbol": "SOL",
"name": "Solana",
"priceUsd": 98.50,
"sources": ["text", "ocr"]
}
]
}
}
}
推文更新
当有更多推文数据可用时,tweet/update 可能在 tweet/content 之后到达。引用更新通常包含 ref.tweetId、ref.text 或 ref.translatedText 之一,以及其他可用的引用字段。如果当前仅有富引用数据,更新也可能只携带完整的 ref.poll 或 ref.article,而不含标识或文本。poll 和 article 的值是完整对象。请将每次更新应用到同一条推文,替换更新中包含的字段,并保留被省略字段的现有值。
type VerifiedType = 'blue' | 'business' | 'government' | 'none';
type TweetVerifiedLabel = {
badge: string | null;
description: string;
url: string | null;
};
type TweetAuthor = {
banner?: string;
bio?: string;
followersCount?: number;
followingCount?: number;
id?: string;
joinedAt?: number;
location?: string;
metrics?: {
likes?: number;
tweets?: number;
};
// Includes a leading @ when present, for example "@elonmusk".
handle?: string;
name?: string;
platform?: 'twitter' | 'truth_social';
profileImage?: string;
url?: string;
verifiedLabel?: TweetVerifiedLabel;
verifiedType?: VerifiedType;
};
type Media = {
url: string;
type?: 'image' | 'video' | 'gif';
thumbnail?: string;
};
type TweetUrl = {
url: string;
name?: string;
tco?: string;
};
type TweetMention = {
handle?: string;
id?: string;
name?: string;
};
type TweetArticle = {
description?: string;
id?: string;
publishedAt?: number;
text?: string;
thumbnail?: string;
title: string;
updatedAt?: number;
url: string;
};
type TweetPollChoice = {
id?: string;
image?: string;
label: string;
votes?: number;
};
type TweetPoll = {
choices: TweetPollChoice[];
endsAt?: number;
totalVotes?: number;
updatedAt?: number;
};
type TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
type TweetReference = {
article?: TweetArticle;
type: 'reply' | 'quote' | 'retweet';
tweetId?: string;
text?: string;
translatedText?: string;
author?: TweetAuthor;
media?: Media[];
poll?: TweetPoll;
quoted?: TweetReference;
};
type TweetContent = {
tweetId: string;
kind: TweetContentKind;
// Original tweet text when the event includes both original and translated text.
text: string;
// Translation, present only when available.
translatedText?: string;
createdAt: number;
author: TweetAuthor;
article?: TweetArticle;
link?: string;
media?: Media[];
mentions?: TweetMention[];
poll?: TweetPoll;
receivedAt?: number;
urls?: TweetUrl[];
ref?: TweetReference;
};
type TweetUpdate = {
tweetId: string;
article?: TweetArticle;
kind?: TweetContentKind;
text?: string;
translatedText?: string;
author?: TweetAuthor;
media?: Media[];
mentions?: TweetMention[];
poll?: TweetPoll;
receivedAt?: number;
urls?: TweetUrl[];
ref?: TweetReference;
};
{
"v": 1,
"t": "tweet",
"op": "update",
"id": "1234567890",
"ts": 1702500001800,
"d": {
"tweetId": "1234567890",
"kind": "reply",
"ref": {
"type": "reply",
"tweetId": "1234567880",
"text": "Parent tweet text"
}
}
}
生命周期事件
删除、置顶和取消置顶都是显式事件。它们应该更新下游状态,而不是覆盖原始 content 载荷。置顶事件可能包含不带富内容的推文快照;如果 poll 或 article 数据可用,随后会按同一推文的顺序发送 tweet/update,并携带各自的完整对象。
type VerifiedType = 'blue' | 'business' | 'government' | 'none';
type TweetVerifiedLabel = {
badge: string | null;
description: string;
url: string | null;
};
type TweetAuthor = {
banner?: string;
bio?: string;
followersCount?: number;
followingCount?: number;
id?: string;
joinedAt?: number;
location?: string;
metrics?: {
likes?: number;
tweets?: number;
};
// Includes a leading @ when present, for example "@elonmusk".
handle?: string;
name?: string;
platform?: 'twitter' | 'truth_social';
profileImage?: string;
url?: string;
verifiedLabel?: TweetVerifiedLabel;
verifiedType?: VerifiedType;
};
type Media = {
url: string;
type?: 'image' | 'video' | 'gif';
thumbnail?: string;
};
type TweetUrl = {
url: string;
name?: string;
tco?: string;
};
type TweetMention = {
handle?: string;
id?: string;
name?: string;
};
type TweetArticle = {
description?: string;
id?: string;
publishedAt?: number;
text?: string;
thumbnail?: string;
title: string;
updatedAt?: number;
url: string;
};
type TweetPollChoice = {
id?: string;
image?: string;
label: string;
votes?: number;
};
type TweetPoll = {
choices: TweetPollChoice[];
endsAt?: number;
totalVotes?: number;
updatedAt?: number;
};
type TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
type TweetReference = {
article?: TweetArticle;
type: 'reply' | 'quote' | 'retweet';
tweetId?: string;
text?: string;
translatedText?: string;
author?: TweetAuthor;
media?: Media[];
poll?: TweetPoll;
quoted?: TweetReference;
};
type TweetContent = {
tweetId: string;
kind: TweetContentKind;
// Original tweet text when the event includes both original and translated text.
text: string;
// Translation, present only when available.
translatedText?: string;
createdAt: number;
author: TweetAuthor;
article?: TweetArticle;
link?: string;
media?: Media[];
mentions?: TweetMention[];
poll?: TweetPoll;
receivedAt?: number;
urls?: TweetUrl[];
ref?: TweetReference;
};
type TweetDeleteEvent = {
tweetId: string;
eventId: string;
deletedAt?: number;
receivedAt?: number;
author?: TweetAuthor;
text?: string;
};
type TweetPinEvent = {
tweetId: string;
eventId: string;
observedAt: number;
receivedAt?: number;
action: 'pin' | 'unpin';
author: TweetAuthor;
text?: string;
tweet?: TweetContent;
};
账号事件
资料、关注和取消关注事件使用 `account` family。可用性取决于被监控账号实际观察到的变化。
type VerifiedType = 'blue' | 'business' | 'government' | 'none';
type TweetVerifiedLabel = {
badge: string | null;
description: string;
url: string | null;
};
type TweetAuthor = {
banner?: string;
bio?: string;
followersCount?: number;
followingCount?: number;
id?: string;
joinedAt?: number;
location?: string;
metrics?: {
likes?: number;
tweets?: number;
};
// Includes a leading @ when present, for example "@elonmusk".
handle?: string;
name?: string;
platform?: 'twitter' | 'truth_social';
profileImage?: string;
url?: string;
verifiedLabel?: TweetVerifiedLabel;
verifiedType?: VerifiedType;
};
type AccountEventActor = TweetAuthor & {
websiteUrl?: string;
};
type ProfileUpdateEvent = {
kind: 'PROFILE';
eventId: string;
observedAt: number;
receivedAt?: number;
actor: AccountEventActor;
changes: {
avatar?: string;
banner?: string;
bio?: string;
handle?: string;
location?: string;
name?: string;
verifiedLabel?: TweetVerifiedLabel | null;
websiteUrl?: string | null;
};
previous?: {
avatar?: string;
banner?: string;
bio?: string;
handle?: string;
location?: string;
name?: string;
verifiedLabel?: TweetVerifiedLabel | null;
websiteUrl?: string | null;
};
};
type FollowEvent = {
kind: 'FOLLOW' | 'UNFOLLOW';
eventId: string;
observedAt: number;
receivedAt?: number;
actor: AccountEventActor;
target: AccountEventActor;
};
{
"v": 1,
"t": "account",
"op": "profile_update",
"ts": 1744156801000,
"d": {
"kind": "PROFILE",
"eventId": "profile_1",
"observedAt": 1744156800000,
"receivedAt": 1744156800123,
"actor": {
"id": "123",
"handle": "@tracked",
"name": "Tracked Account",
"profileImage": "https://pbs.twimg.com/profile_images/new-avatar_normal.jpg",
"followersCount": 125000,
"followingCount": 321,
"verifiedType": "business",
"verifiedLabel": {
"badge": "https://pbs.twimg.com/affiliation_badge.jpg",
"description": "Example Org",
"url": "https://x.com/example"
},
"websiteUrl": "https://tracked.example",
"location": "New York, NY"
},
"changes": {
"avatar": "https://pbs.twimg.com/profile_images/new-avatar_normal.jpg",
"bio": "Now watching markets 24/7",
"websiteUrl": "https://new-site.example"
},
"previous": {
"avatar": "https://pbs.twimg.com/profile_images/old-avatar_normal.jpg",
"bio": "Old bio",
"websiteUrl": "https://old-site.example"
}
}
}
典型顺序
带图片的帖子通常分阶段到达。把 `content` 当成可提醒事件,将后续 `meta` 合并进同一条本地记录,并把生命周期事件作为状态变化处理。
{
"v": 1,
"t": "tweet",
"op": "content",
"id": "2064689031777615872",
"ts": 1781095200123,
"d": {
"tweetId": "2064689031777615872",
"kind": "post",
"text": "New token live. CA: 9xQeWvG816bUx9EP...",
"createdAt": 1781095199900,
"receivedAt": 1781095200108,
"author": {
"handle": "@marketdesk",
"name": "Market Account",
"platform": "twitter"
}
}
}
{
"v": 1,
"t": "tweet",
"op": "meta",
"id": "2064689031777615872",
"ts": 1781095200340,
"d": {
"tweetId": "2064689031777615872",
"detected": {
"tokens": [
{
"symbol": "EDGE",
"contract": "9xQeWvG816bUx9EP...",
"chain": "solana",
"priceUsd": 0.0042,
"sources": ["text", "ocr"]
}
]
}
}
}
{
"v": 1,
"t": "tweet",
"op": "delete",
"id": "2064689031777615872",
"ts": 1781095300123,
"d": {
"tweetId": "2064689031777615872",
"eventId": "delete_2064689031777615872",
"deletedAt": 1781095300100
}
}
Handle 管理结果
WebSocket handle-management 命令会返回 `control/twitter_handles_result`。REST add/remove 端点为后端工作流暴露相同的逐行状态模型。
type TwitterHandlesResult = {
action: 'follow' | 'unfollow';
requestId: string | null;
results: Array<{
input: string;
state:
| 'added'
| 'already_following'
| 'invalid_input'
| 'duplicate'
| 'not_found'
| 'failed'
| 'removed'
| 'not_following';
message?: string;
}>;
error: string | null;
};
通过 WebSocket 管理 handles
当一个长期运行的后端 socket 需要添加或移除监控账号、且不想额外发 REST 请求时,使用 handle-management subprotocol。
import WebSocket from "ws";
type HandleManagementEvent = {
t?: string;
op?: string;
d?: {
error?: string | null;
results?: Array<{ input: string; state: string }>;
};
};
const ws = new WebSocket("wss://ws-global.tweetstream.io/ws", [
"tweetstream.handle-management",
`tweetstream.auth.token.${process.env.TWEETSTREAM_API_KEY}`,
]);
ws.on("open", () => {
ws.send(JSON.stringify({
type: "twitter_handles",
action: "follow",
handles: ["marketdesk", "realDonaldTrump"],
requestId: crypto.randomUUID(),
}));
});
ws.on("message", (raw) => {
const event = JSON.parse(raw.toString()) as HandleManagementEvent;
if (event.t === "control" && event.op === "twitter_handles_result") {
console.log(event.d?.results, event.d?.error);
}
});