载荷与事件
TweetStream 发送紧凑的 JSON envelope。content 承载社交事件;meta 承载富化;生命周期和账号事件使用独立 operation。
推文内容
`tweet/content` 是多数 consumer 首先处理的事件。`author.platform` 为 `twitter` 或 `truth_social`。当 `partial` 为 true 时,上游只提供了截断文本;TweetStream 会移除指向同一推文的续读链接,而不会把它作为正文展示。
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 TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
type TweetReference = {
type: 'reply' | 'quote' | 'retweet';
tweetId?: string;
text?: string;
translatedText?: string;
author?: TweetAuthor;
media?: Media[];
// True when the upstream only supplied truncated text.
partial?: boolean;
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;
link?: string;
media?: Media[];
mentions?: TweetMention[];
// True when text is known to be truncated.
partial?: boolean;
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` 携带已知 tweetId 的增量更新。把有定义的字段合并到现有记录,并保留 update 载荷中缺失的早期字段。带有 `partial: true` 的更新表示文本已被截断;之后不带该标记的文本更新会用完整文本替换它。翻译可能随 `tweet/content` 或 `tweet/update` 以 `translatedText` 到达。
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 TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
type TweetReference = {
type: 'reply' | 'quote' | 'retweet';
tweetId?: string;
text?: string;
translatedText?: string;
author?: TweetAuthor;
media?: Media[];
// True when the upstream only supplied truncated text.
partial?: boolean;
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;
link?: string;
media?: Media[];
mentions?: TweetMention[];
// True when text is known to be truncated.
partial?: boolean;
receivedAt?: number;
urls?: TweetUrl[];
ref?: TweetReference;
};
type TweetUpdate = {
tweetId: string;
kind?: TweetContentKind;
text?: string;
translatedText?: string;
author?: TweetAuthor;
media?: Media[];
mentions?: TweetMention[];
partial?: boolean;
receivedAt?: number;
urls?: TweetUrl[];
ref?: TweetContent['ref'];
};
{
"v": 1,
"t": "tweet",
"op": "update",
"id": "1234567890",
"ts": 1702500001800,
"d": {
"tweetId": "1234567890",
"translatedText": "New project is live. Contract address: 9xQeWvG816bUx9EP..."
}
}
生命周期事件
删除、置顶和取消置顶都是显式事件。它们应该更新下游状态,而不是覆盖原始 content 载荷。
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 TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet';
type TweetReference = {
type: 'reply' | 'quote' | 'retweet';
tweetId?: string;
text?: string;
translatedText?: string;
author?: TweetAuthor;
media?: Media[];
// True when the upstream only supplied truncated text.
partial?: boolean;
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;
link?: string;
media?: Media[];
mentions?: TweetMention[];
// True when text is known to be truncated.
partial?: boolean;
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;
handle?: string;
name?: string;
normalizedHandle?: string;
profileImage?: string;
twitterId?: 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<{ handle?: string; input: string; state: string }>;
};
};
const ws = new WebSocket("wss://ws.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);
}
});