跳到文档正文
文档

载荷与事件

TweetStream 发送紧凑的 JSON envelope。content 承载社交事件;meta 承载富化;生命周期和账号事件使用独立 operation。

推文内容

`tweet/content` 是多数 consumer 首先处理的事件。`author.platform` 为 `twitter` 或 `truth_social`。当 `partial` 为 true 时,上游只提供了截断文本;TweetStream 会移除指向同一推文的续读链接,而不会把它作为正文展示。

类型typescript
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;
};
 
示例json
{
  "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 之后到达。应合并到本地同一条推文记录中,不要把它当成单独提醒,除非你的流程需要富化专用提醒。

类型typescript
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'>;
    }>;
  };
};
 
示例json
{
  "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` 到达。

类型typescript
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'];
};
 
示例json
{
  "v": 1,
  "t": "tweet",
  "op": "update",
  "id": "1234567890",
  "ts": 1702500001800,
  "d": {
    "tweetId": "1234567890",
    "translatedText": "New project is live. Contract address: 9xQeWvG816bUx9EP..."
  }
}
 

生命周期事件

删除、置顶和取消置顶都是显式事件。它们应该更新下游状态,而不是覆盖原始 content 载荷。

类型typescript
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。可用性取决于被监控账号实际观察到的变化。

类型typescript
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;
};
 
示例json
{
  "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` 合并进同一条本地记录,并把生命周期事件作为状态变化处理。

1. 内容json
{
  "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"
    }
  }
}
 
2. 富化json
{
  "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"]
        }
      ]
    }
  }
}
 
3. 生命周期json
{
  "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 端点为后端工作流暴露相同的逐行状态模型。

类型typescript
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。

通过 WebSocket follow handlestypescript
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);
  }
});