# TweetStream:面向交易系统的实时 X/Twitter WebSocket 数据流 TweetStream 通过 WebSocket 向加密货币和预测市场系统发送受监控的 X/Twitter 帖子。同一连接还可以传送来自 18 家出版物的新闻、选定 Binance Square 账号的帖子、回复和引用、pump.fun callout 和慈善机构通知。Pro、Scale 和 Ultra Speed 包含支持的 Donald Trump 和 White House Truth Social 帖子;Minimum 不包含 Truth Social。 ## 从这里开始 - [完整 LLM 上下文](/zh-cn/docs/llms-full.txt): Markdown 格式的全部公开开发者文档。 - [交互式文档](/zh-cn/docs): 用于浏览和搜索的开发者文档。 - [OpenAPI](/zh-cn/docs/openapi.json): REST API 的 OpenAPI 3.1 契约。 - [AsyncAPI](/zh-cn/docs/asyncapi.json): WebSocket v1 的 AsyncAPI 3.0 契约。 - [英文根路径别名](/llms-full.txt): 英文完整文档的稳定别名。 - [机器可读价格](/pricing.md):当前 Minimum、Pro、Scale、Ultra Speed 和试用信息。 - [X/Twitter 服务端检测数据](/zh-cn/speedcheck):滚动生产环境 p25、p50、p95、样本量、UTC 时间范围、各账号结果和测量边界。 - [JSON 格式的 X/Twitter 速度数据](/speedcheck.json):同一滚动报告的结构化数据。 - [CSV 格式的 X/Twitter 速度数据](/speedcheck.csv):同一报告的汇总和各账号数据行。 ## 交易用例 - [Meme 币和加密货币 Twitter 提醒](/zh-cn/crypto-twitter-alerts):监控账号事件,包含代币检测、合约提取、OCR 和可用的实时价格信息。 - [指定账号 Twitter WebSocket API](/zh-cn/twitter-websocket-api):向交易机器人发送账号事件、OCR、代币检测和可用的实时价格信息。 - [Polymarket Twitter 提醒](/zh-cn/polymarket-twitter-alerts):向预测市场策略发送 X 事件;Pro、Scale 和 Ultra Speed 还可发送支持的 Truth Social 帖子。 - [Polymarket 机器人指南](/zh-cn/blog/polymarket-twitter-alert-bot):将社交事件、Polymarket 市场数据和执行风险规则组合起来。 - [Kalshi 交易机器人指南](/zh-cn/blog/kalshi-trading-bot-social-signals):将指定社交事件与 Kalshi 官方市场数据组合,风险和执行仍由你的系统负责。 - [社交替代数据指南](/zh-cn/blog/social-events-alternative-trading-data):比较指定账号事件、新闻 API、情绪产品和抓取方案。 - [Twitter 到交易延迟指南](/zh-cn/blog/twitter-to-trade-latency-measurement):用一致边界测量发布、接收、决策、风险、订单和成交时间。 - [Truth Social API 访问指南](/zh-cn/truth-social-api):说明机构 Truth API 与 TweetStream 对 Donald Trump 和 White House 帖子的有限支持。 - [Trump Truth Social 提醒](/zh-cn/trump-truth-social-alerts):通过 WebSocket 和 Discord 接收 Pro、Scale 和 Ultra Speed 支持的 Donald Trump 和 White House 帖子。 - [竞品对比](/zh-cn/alternatives):与 uTrack、Bark.gg、TwitterAPI.io 及其他 API 方案的当前对比。 ## 产品信息 - `tweet/content` 提供首个可用帖子,包括文本、作者身份和时间戳;媒体与引用信息在可用时提供。 - `tweet/update` 可以补充引用信息或完整文本。按 `(platform, tweetId)` 更新已有帖子。 - `tweet/meta` 可以随后提供 OCR、代币与合约检测、价格、交易所和预测市场信息。 - 同一个 WebSocket 可以发送 X/Twitter 事件、18 家媒体的新闻、指定币安广场帖子、pump.fun 喊单和慈善项目提醒。 - Minimum 不提供 History API。Pro、Scale 和 Ultra Speed 提供完整 History 回放。 - Minimum 和 Pro 试用期为三天。Ultra Speed 试用期为 24 小时。 - TweetStream 不提供 Polymarket 价格或订单执行。请使用 Polymarket API,并在策略中组合两个数据流。 - TweetStream 独立运营,与 X Corp、Truth Social、TMTG、Donald J. Trump 或 Polymarket 无关联。 ## 文档页面 ### 开始 - [概览](/zh-cn/docs): 连接 TweetStream,查找实时协议、事件、REST 和完整示例。 - [快速开始](/zh-cn/docs/quickstart): 打开已认证的 TweetStream WebSocket 并查看第一条事件。 - [生产环境消费者](/zh-cn/docs/production-consumer): 构建可长期运行的 TweetStream 消费者,包含分发、重连和检查点。 ### WebSocket 协议 - [连接、认证与心跳](/zh-cn/docs/connection-auth-heartbeat): 选择 WebSocket 端点,完成认证并保持连接响应。 - [交付与恢复](/zh-cn/docs/envelope-delivery-recovery-errors): 分发消息信封,在断开后恢复,并处理公开的 WebSocket 错误。 ### 事件参考 - [事件参考](/zh-cn/docs/payloads-detections-ocr): 浏览所有 TweetStream 事件类别,并打开对应的载荷与集成指南。 - [社交内容](/zh-cn/docs/social-content): 通过 tweet 消息信封接收 Twitter、Truth Social 和币安广场内容。 - [元数据与生命周期](/zh-cn/docs/tweet-updates-lifecycle): 替换实时元数据快照并合并推文更新,再应用删除、置顶和取消置顶事件。 - [资料、关注与关联账号](/zh-cn/docs/profile-follow-affiliate): 接收资料、关注、取消关注和关联账号关系变更。 - [喊单](/zh-cn/docs/callouts): 配置喊单提醒并接收 account/callout 事件。 - [FOMO](/zh-cn/docs/fomo): 监控 FOMO 用户和代币,读取观点事件,并处理观察到的代币认证。 - [慈善项目](/zh-cn/docs/charity): 开启慈善项目提醒并接收 charity/added 事件。 - [新闻](/zh-cn/docs/news): 选择媒体,接收实时新闻,并翻阅新闻 History。 - [控制消息](/zh-cn/docs/control): 使用认证和账号管理控制消息。 ### REST 参考 - [通用 History API](/zh-cn/docs/history-api): 通过 Bearer 认证的 History API 查询已存账号事件。 - [账号与设置](/zh-cn/docs/account-management): 通过已认证的 REST 端点添加、移除、查看和配置账号。 ### 示例 - [消费者示例](/zh-cn/docs/examples): 从完整的 TypeScript 和 Python TweetStream 消费者示例开始。 ## 公开产品和购买页面 - [/](/zh-cn) - [/about](/zh-cn/about) - [/speedcheck](/zh-cn/speedcheck) - [/contact](/zh-cn/contact) - [/changelog](/zh-cn/changelog) - [/faq](/zh-cn/faq) - [/features](/zh-cn/features) - [/pricing](/zh-cn/pricing) - [/security](/zh-cn/security) - [/binance-square-alerts](/zh-cn/binance-square-alerts) - [/features/charity-alerts](/zh-cn/features/charity-alerts) - [/news-monitoring-for-trading-bots](/zh-cn/news-monitoring-for-trading-bots) - [/features/pumpfun-callout-alerts](/zh-cn/features/pumpfun-callout-alerts) - [/crypto-twitter-alerts](/zh-cn/crypto-twitter-alerts) - [/polymarket-twitter-alerts](/zh-cn/polymarket-twitter-alerts) - [/social-media-alerts-for-trading-bots](/zh-cn/social-media-alerts-for-trading-bots) - [/trump-truth-social-alerts](/zh-cn/trump-truth-social-alerts) - [/truth-social-alerts-for-traders](/zh-cn/truth-social-alerts-for-traders) - [/truth-social-api](/zh-cn/truth-social-api) - [/alternatives](/zh-cn/alternatives) - [/alternatives/twitter-api](/zh-cn/alternatives/twitter-api) - [/alternatives/tweetcatcher](/zh-cn/alternatives/tweetcatcher) - [/alternatives/utrack](/zh-cn/alternatives/utrack) - [/alternatives/1322](/zh-cn/alternatives/1322) - [/alternatives/bark-gg](/zh-cn/alternatives/bark-gg) - [/alternatives/twitterapi-io](/alternatives/twitterapi-io) - [/alternatives/xanguard](/zh-cn/alternatives/xanguard) - [/solutions](/zh-cn/solutions) - [/solutions/twitter-alerts-crypto-traders](/zh-cn/solutions/twitter-alerts-crypto-traders) - [/solutions/twitter-to-discord-alerts](/zh-cn/solutions/twitter-to-discord-alerts) - [/solutions/websocket-twitter-api-trading-bots](/zh-cn/solutions/websocket-twitter-api-trading-bots) - [/solutions/realtime-twitter-account-monitoring](/zh-cn/solutions/realtime-twitter-account-monitoring) - [/solutions/discord-crypto-alerts](/zh-cn/solutions/discord-crypto-alerts) - [/docs](/zh-cn/docs) - [/docs/quickstart](/zh-cn/docs/quickstart) - [/docs/production-consumer](/zh-cn/docs/production-consumer) - [/docs/connection-auth-heartbeat](/zh-cn/docs/connection-auth-heartbeat) - [/docs/envelope-delivery-recovery-errors](/zh-cn/docs/envelope-delivery-recovery-errors) - [/docs/payloads-detections-ocr](/zh-cn/docs/payloads-detections-ocr) - [/docs/social-content](/zh-cn/docs/social-content) - [/docs/tweet-updates-lifecycle](/zh-cn/docs/tweet-updates-lifecycle) - [/docs/profile-follow-affiliate](/zh-cn/docs/profile-follow-affiliate) - [/docs/callouts](/zh-cn/docs/callouts) - [/docs/fomo](/zh-cn/docs/fomo) - [/docs/charity](/zh-cn/docs/charity) - [/docs/news](/zh-cn/docs/news) - [/docs/control](/zh-cn/docs/control) - [/docs/history-api](/zh-cn/docs/history-api) - [/docs/account-management](/zh-cn/docs/account-management) - [/docs/examples](/zh-cn/docs/examples) - [/features/history-replay](/features/history-replay) - [/features/account-management](/features/account-management) - [/integrations](/integrations) - [/integrations/python-websockets](/integrations/python-websockets) - [/integrations/discord-webhooks](/integrations/discord-webhooks) - [/blog](/zh-cn/blog) - [/blog/twitter-api-pricing](/zh-cn/blog/twitter-api-pricing) - [/blog/twitter-streaming-api](/zh-cn/blog/twitter-streaming-api) - [/blog/twitter-to-discord-webhook](/zh-cn/blog/twitter-to-discord-webhook) - [/blog/dexscreener-alerts](/zh-cn/blog/dexscreener-alerts) - [/blog/does-twitter-use-websockets](/zh-cn/blog/does-twitter-use-websockets) - [/blog/best-crypto-twitter-monitoring-tools](/zh-cn/blog/best-crypto-twitter-monitoring-tools) - [/blog/build-real-time-twitter-dashboard](/zh-cn/blog/build-real-time-twitter-dashboard) - [/blog/crypto-rug-pull-twitter-signals](/zh-cn/blog/crypto-rug-pull-twitter-signals) - [/blog/kalshi-trading-bot-social-signals](/zh-cn/blog/kalshi-trading-bot-social-signals) - [/blog/monitor-competitor-twitter-crypto](/zh-cn/blog/monitor-competitor-twitter-crypto) - [/blog/node-twitter-websocket-client](/zh-cn/blog/node-twitter-websocket-client) - [/blog/polymarket-twitter-alert-bot](/zh-cn/blog/polymarket-twitter-alert-bot) - [/blog/pumpfun-twitter-alerts](/zh-cn/blog/pumpfun-twitter-alerts) - [/blog/python-twitter-websocket-client](/zh-cn/blog/python-twitter-websocket-client) - [/blog/social-events-alternative-trading-data](/zh-cn/blog/social-events-alternative-trading-data) - [/blog/solana-memecoin-twitter-alerts](/zh-cn/blog/solana-memecoin-twitter-alerts) - [/blog/track-crypto-twitter-accounts](/zh-cn/blog/track-crypto-twitter-accounts) - [/blog/twitter-account-activity-api-alternative](/zh-cn/blog/twitter-account-activity-api-alternative) - [/blog/twitter-api-rate-limits](/zh-cn/blog/twitter-api-rate-limits) - [/blog/twitter-cashtag-monitoring](/zh-cn/blog/twitter-cashtag-monitoring) - [/blog/twitter-data-storage-for-trading-bots](/zh-cn/blog/twitter-data-storage-for-trading-bots) - [/blog/twitter-keyword-alerts-for-crypto](/zh-cn/blog/twitter-keyword-alerts-for-crypto) - [/blog/twitter-notification-bot](/zh-cn/blog/twitter-notification-bot) - [/blog/twitter-push-notifications](/zh-cn/blog/twitter-push-notifications) - [/blog/twitter-to-discord-bot](/zh-cn/blog/twitter-to-discord-bot) - [/blog/twitter-to-telegram-alerts](/zh-cn/blog/twitter-to-telegram-alerts) - [/blog/twitter-to-trade-latency-measurement](/zh-cn/blog/twitter-to-trade-latency-measurement) - [/blog/twitter-webhook-api](/zh-cn/blog/twitter-webhook-api) - [/blog/twitter-websocket-reconnect-backoff](/zh-cn/blog/twitter-websocket-reconnect-backoff) - [/blog/websocket-vs-webhook-twitter-alerts](/zh-cn/blog/websocket-vs-webhook-twitter-alerts) - [/twitter-websocket-api](/zh-cn/twitter-websocket-api) ## 完整公开文档 ## TweetStream 开发者文档 先完成连接,再按需查阅 WebSocket 协议、事件、REST API 和完整消费者示例。 Canonical URL: https://tweetstream.io/zh-cn/docs ### 开始接收事件 - [快速开始](https://tweetstream.io/zh-cn/docs/quickstart): 打开一个已认证的连接并输出事件。 - [生产环境消费者](https://tweetstream.io/zh-cn/docs/production-consumer): 加入事件分发、重连和检查点。 - [事件参考](https://tweetstream.io/zh-cn/docs/payloads-detections-ocr): 查找每种事件操作的载荷。 - [REST 参考](https://tweetstream.io/zh-cn/docs/history-api): 回放已存事件并管理账号设置。 ### 选择基础 URL | 接口 | URL | 用途 | | --- | --- | --- | | WebSocket(美国) | wss://ws-iad.tweetstream.io/ws | 用于美国境内的连接 | | WebSocket(全球) | wss://ws-global.tweetstream.io/ws | 用于美国以外的连接 | | REST | https://api.tweetstream.io | /api/history、/api/me 和账号管理的基础域名 | | Dashboard | https://tweetstream.io/dashboard | API key、监控列表、账单和 Discord 路由 | ### 选择参考文档 - WebSocket 协议页说明连接与恢复行为。 - 事件页说明可区分的载荷结构。 - REST 页说明 History、账号管理和设置。 - 如需完整消费者,请使用示例页,而不是只复制零散片段。 ### 使用机器可读契约 - [OpenAPI 3.1](https://tweetstream.io/zh-cn/docs/openapi.json): 根据公开端点契约生成 REST 客户端。 - [AsyncAPI 3.0](https://tweetstream.io/zh-cn/docs/asyncapi.json): 查看所有 WebSocket 通道、操作和载荷 schema。 - [LLM 上下文](https://tweetstream.io/zh-cn/docs/llms-full.txt): 以 Markdown 格式加载完整公开文档。 ## WebSocket 快速开始 添加一个监控账号,打开服务端 WebSocket,然后输出每个 v1 消息信封。 Canonical URL: https://tweetstream.io/zh-cn/docs/quickstart ### 运行 5 分钟连通性测试 添加一个监控账号,复制示例并运行。看到 `TweetStream connected` 表示认证成功。该账号发布内容后会收到事件。 #### 运行 TypeScript 连通性测试 ```typescript // bun add ws // TWEETSTREAM_API_KEY=ts_... bun run smoke.ts import WebSocket from "ws"; const apiKey = process.env.TWEETSTREAM_API_KEY; if (!apiKey) { throw new Error("Missing TWEETSTREAM_API_KEY"); } const ws = new WebSocket("wss://ws-global.tweetstream.io/ws", [ "tweetstream.v1", `tweetstream.auth.token.${apiKey}`, ]); ws.on("open", () => { console.log("TweetStream connected"); }); ws.on("message", (raw) => { const event = JSON.parse(raw.toString()); console.log(event.t, event.op, event.d); }); ws.on("close", (code, reason) => { console.log("TweetStream closed", code, reason.toString()); }); ws.on("unexpected-response", (_request, response) => { console.error("Connection rejected", response.statusCode, response.statusMessage); response.resume(); process.exitCode = 1; }); ws.on("error", (error) => { console.error("WebSocket error", error.message); }); ``` ### 分发操作 | 类别 | 操作 | 含义 | | --- | --- | --- | | tweet | content | 原始发帖、回复、引用、转推,或支持的 Truth Social 帖子内容 | | tweet | meta | 为已知帖子添加 OCR、代币检测、CEX 和预测市场数据 | | tweet | update | 更新已知帖子的字段 | | tweet | delete, pin, unpin | 将已知帖子标记为已删除、已置顶或已取消置顶 | | account | profile_update, follow, unfollow, affiliate_update | 被监控账号的状态变化 | | charity | added | 新增慈善项目。可在控制台开启提醒 | | news | article | 来自已开启媒体的文章 | | control | twitter_handles_result | WebSocket 账号管理命令的结果 | ### 让消费者可靠运行 - [连接详情](https://tweetstream.io/zh-cn/docs/connection-auth-heartbeat): 选择端点,并了解认证和心跳行为。 - [生产环境消费者](https://tweetstream.io/zh-cn/docs/production-consumer): 加入重连、事件分发和恢复检查点。 ## 构建生产环境消费者 分发已知操作,通过退避策略重连,并确保每个副作用都可安全重复。 Canonical URL: https://tweetstream.io/zh-cn/docs/production-consumer ### 连接 美国境内使用 `wss://ws-iad.tweetstream.io/ws`,其他地区使用 `wss://ws-global.tweetstream.io/ws`。服务端返回 `tweetstream.v1`,不会回显携带认证 token 的协议。示例通过指数退避重连。短连接会继续增加等待时间,稳定连接 30 秒后重置。 #### 使用 Node.js 连接 ```typescript import WebSocket from "ws"; type StreamEvent = { t?: string; op?: string; d?: { author?: { handle?: string }; detected?: unknown; text?: string; }; }; const apiKey = process.env.TWEETSTREAM_API_KEY; if (!apiKey) { throw new Error("Missing TWEETSTREAM_API_KEY"); } let retry = 0; let healthyTimer: ReturnType | undefined; let reconnectTimer: ReturnType | undefined; function scheduleReconnect(reason: string) { if (reconnectTimer) return; const delayMs = Math.min(30_000, 1_000 * 2 ** retry) + Math.floor(Math.random() * 500); retry += 1; console.warn(`Reconnecting in ${delayMs}ms: ${reason}`); reconnectTimer = setTimeout(() => { reconnectTimer = undefined; connect(); }, delayMs); } function connect() { const ws = new WebSocket("wss://ws-global.tweetstream.io/ws", [ "tweetstream.v1", `tweetstream.auth.token.${apiKey}`, ]); ws.on("open", () => { if (reconnectTimer) clearTimeout(reconnectTimer); reconnectTimer = undefined; healthyTimer = setTimeout(() => { retry = 0; healthyTimer = undefined; }, 30_000); console.log("TweetStream connected"); }); ws.on("message", (raw) => { const event = JSON.parse(raw.toString()) as StreamEvent; if (event.t === "tweet" && event.op === "content") { const tweet = event.d; console.log(tweet?.author?.handle, tweet?.text); } if (event.t === "tweet" && event.op === "meta") { console.log("enrichment", event.d?.detected); } }); ws.on("close", (code, reason) => { if (healthyTimer) clearTimeout(healthyTimer); healthyTimer = undefined; scheduleReconnect(`close ${code}: ${reason.toString()}`); }); ws.on("unexpected-response", (_request, response) => { console.error("Connection rejected", response.statusCode, response.statusMessage); response.resume(); if (response.statusCode === 429 || response.statusCode === 503) { scheduleReconnect(`HTTP ${response.statusCode}`); return; } process.exitCode = 1; }); ws.on("error", (error) => { console.error("WebSocket error", error.message); }); } connect(); ``` ### 准备生产环境 - 连接关闭或发生网络错误后,使用退避策略重连。 - 只连接文档列出的 TweetStream 端点,并使用上文说明的认证方式。路由由 TweetStream 处理,不需要基础设施专用 Header。 - 使用 `(platform, tweetId)` 作为帖子键,而不是消息信封 id。content 从 `author.platform` 读取 `platform`,后续操作从 `d.platform` 读取。 - 旧版操作缺少 `d.platform` 时,使用其中的 author platform;如果没有,则按 `twitter` 处理。 - 幂等应用每个 `tweet/update`。更新包含 `ref` 时,用该快照替换已存引用,并沿 `ref.subtweet` 读取下一条被引用的帖子。 - 只有对完整消息信封计算指纹后,才丢弃完全相同的重放。 - 把 `meta` 视为某条已路由推文的迟到富化信息。 - 将回执和投影存入带索引的持久化存储。根据回放窗口和存储预算制定保留或归档策略;不要删除待处理任务。 - 记录操作失败时不要写入 payload,并通过退避策略重试。使用相同的回执 ID 作为幂等键。 - 跟踪活跃 WebSocket 连接数,避免超过套餐限制。 ### 测量信号延迟 在消息回调开始时记录本地接收时间。对于 X/Twitter 事件,解码 tweet snowflake 时间戳,测量从发布到接收的时间。主机时钟必须与 NTP 同步,wall-clock 结果才有意义。接收后的处理阶段使用单调时钟。 - 报告监控列表、客户端区域、UTC 测试时段、样本量、p50 和 p95。 - 记录重连、缺失事件、时钟同步状态和每条排除规则。 - 单独报告冷启动和重连后的样本,不要将它们混入热路径结果。 - 只有发布点、接收点、地理位置、样本和百分位边界一致时,才比较结果。 | 阶段 | 起点 | 终点 | | --- | --- | --- | | 发布到接收 | X snowflake 时间戳 | WebSocket 回调开始时记录的本地时间 | | 接收到决策 | 本地 WebSocket 接收 | 策略与风控决策就绪 | | 决策到交易场所确认 | 通过风控的决策 | 单独的交易场所响应或拒绝 | #### 测量 Snowflake 延迟 ```typescript type ContentEvent = { t?: string; op?: string; d?: { tweetId?: string; }; }; const TWITTER_EPOCH_MS = 1_288_834_974_657n; function tweetIdToTimestampMs(tweetId: string) { const id = BigInt(tweetId); return Number((id >> 22n) + TWITTER_EPOCH_MS); } function measureSnowflakeLatency(tweetId: string, arrivedAtMs: number) { const tweetedAtMs = tweetIdToTimestampMs(tweetId); return arrivedAtMs - tweetedAtMs; } ws.on("message", (raw) => { const arrivedAtMs = Date.now(); const event = JSON.parse(raw.toString()) as ContentEvent; const tweetId = event.d?.tweetId; if (event.t === "tweet" && event.op === "content" && tweetId) { console.log("publication-to-receipt ms", measureSnowflakeLatency(tweetId, arrivedAtMs)); } }); ``` ## 连接、认证与心跳 使用文档列出的区域端点,在升级连接时认证,并保留原生 ping 和 pong 处理。 Canonical URL: https://tweetstream.io/zh-cn/docs/connection-auth-heartbeat ### 选择基础 URL | 接口 | URL | 用途 | | --- | --- | --- | | WebSocket(美国) | wss://ws-iad.tweetstream.io/ws | 用于美国境内的连接 | | WebSocket(全球) | wss://ws-global.tweetstream.io/ws | 用于美国以外的连接 | | REST | https://api.tweetstream.io | /api/history、/api/me 和账号管理的基础域名 | | Dashboard | https://tweetstream.io/dashboard | API key、监控列表、账单和 Discord 路由 | ### 认证请求 实时客户端使用 WebSocket 子协议认证,REST 请求使用 Bearer token。API key 只能保存在服务端,绝不能放入公开的浏览器代码。 | 上下文 | Header 或协议 | 说明 | | --- | --- | --- | | WebSocket | tweetstream.v1 + tweetstream.auth.token. | 首选的实时认证方式 | | WebSocket fallback | Authorization: Bearer or ?apiKey= | 优先使用 Authorization;只有运行时无法设置 Header 时才使用查询参数认证 | | REST | Authorization: Bearer | Pro 或 Scale 的 History 请求使用标准 key。有效基础套餐叠加当前 Ultra 时,标准 key 可请求完整 History;当前 Ultra key 可请求指定账号的完整 History。账号管理和 /api/me 可使用任一 key | #### 认证服务端连接 ```typescript const socket = new WebSocket("wss://ws-global.tweetstream.io/ws", [ "tweetstream.v1", `tweetstream.auth.token.${process.env.TWEETSTREAM_API_KEY}`, ]); socket.addEventListener("message", (event) => { const message = JSON.parse(event.data); console.log(message.t, message.op, message.d); }); ``` ### 连接 美国境内使用 `wss://ws-iad.tweetstream.io/ws`,其他地区使用 `wss://ws-global.tweetstream.io/ws`。服务端返回 `tweetstream.v1`,不会回显携带认证 token 的协议。示例通过指数退避重连。短连接会继续增加等待时间,稳定连接 30 秒后重置。 #### 使用 Node.js 连接 [使用首次出现的完整示例。](https://tweetstream.io/zh-cn/docs/production-consumer#connect-connect-with-node-js) ### 使用 Python 连接 使用任何能发送这两个子协议的 WebSocket 运行时。正常关闭和传输错误都会通过指数退避重连。短连接会继续增加等待时间,稳定连接 30 秒后重置。 #### Python 重连客户端 ```python import asyncio import json import os import websockets API_KEY = os.environ["TWEETSTREAM_API_KEY"] URI = "wss://ws-global.tweetstream.io/ws" PROTOCOLS = ["tweetstream.v1", f"tweetstream.auth.token.{API_KEY}"] async def main(): loop = asyncio.get_running_loop() retry = 0 while True: connected_at = None reason = "connection closed" try: async with websockets.connect(URI, subprotocols=PROTOCOLS) as ws: connected_at = loop.time() async for raw in ws: event = json.loads(raw) if event["t"] == "tweet" and event["op"] == "content": tweet = event["d"] print(tweet.get("author", {}).get("handle"), tweet.get("text")) except Exception as error: reason = str(error) if connected_at is not None and loop.time() - connected_at >= 30: retry = 0 wait = min(30, 2 ** retry) retry = min(retry + 1, 5) print(f"reconnecting in {wait}s after {reason}") await asyncio.sleep(wait) asyncio.run(main()) ``` ### 处理心跳和断开 TweetStream 每 30 秒发送一次原生 WebSocket `ping`。标准 Node.js 和 Python 客户端会自动回复 `pong`。服务端会关闭无响应的连接。重连后,使用 History API 回补已存内容、资料、关注和关联账号事件。 ### 处理限制和重试 套餐设定活跃 WebSocket、监控账号和 History API 限制。收到 `429` 时暂停对应流程;如果响应包含 `retryAfterSeconds`,按其等待,否则使用常规退避。 | 接口 | 限制信号 | 推荐处理 | | --- | --- | --- | | WebSocket | 连接升级时返回 429 | 关闭不用的连接,或升级到更多连接数的套餐 | | History API | 限流时返回 `retryAfterSeconds` | 等待后再回放下一个窗口 | | 监控账号 | /api/me 返回套餐用量 | 批量添加前检查 count 和 limit | ## 消息信封、交付、恢复与错误 按类别和操作分发 v1 消息信封,容忍重复,并在支持回放时使用已存 History。 Canonical URL: https://tweetstream.io/zh-cn/docs/envelope-delivery-recovery-errors ### 分发消息信封 每个实时消息信封先按 `t` 分发,再按 `op` 分发。处理程序必须幂等,因为历史回放可能返回已实时处理的事件。 #### 定义消息信封 ```typescript type VerifiedType = 'blue' | 'business' | 'government' | 'verified' | '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' | 'binance_square'; profileImage?: string; url?: string; verifiedLabel?: TweetVerifiedLabel; verifiedType?: VerifiedType; }; type Media = { url: string; } & ( | { type: 'video'; // Every video is a progressive MP4. A public still poster is included when available. thumbnail?: string; } | { type?: 'image' | '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; subtweet?: 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 TweetMeta = { tweetId: string; // Pair with tweetId when merging enrichment. Omitted means twitter on legacy frames. platform?: 'twitter' | 'truth_social' | 'binance_square'; 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'>; }>; }; }; type TweetUpdate = { tweetId: string; // Pair with tweetId. On legacy frames, fall back to author.platform, then twitter. platform?: 'twitter' | 'truth_social' | 'binance_square'; article?: TweetArticle; kind?: TweetContentKind; translatedText?: string; author?: TweetAuthor; media?: Media[]; mentions?: TweetMention[]; poll?: TweetPoll; receivedAt?: number; urls?: TweetUrl[]; ref?: TweetReference; } & ( | { text?: string; textUpdateType?: never; } | { text: string; // Completes an earlier truncated rendering. This is not an edit signal. textUpdateType: 'completion'; } ); type TweetDeleteEvent = { tweetId: string; // On legacy frames, fall back to author.platform, then twitter. platform?: 'twitter' | 'truth_social' | 'binance_square'; eventId?: string; deletedAt?: number; receivedAt?: number; author?: TweetAuthor; text?: string; }; type TweetPinEvent = { tweetId: string; // On legacy frames, fall back to tweet.author.platform or author.platform, then twitter. platform?: 'twitter' | 'truth_social' | 'binance_square'; eventId: string; observedAt: number; receivedAt?: number; action: 'pin'; author: TweetAuthor; text?: string; tweet?: TweetContent; }; type TweetUnpinEvent = { tweetId: string; // On legacy frames, fall back to tweet.author.platform or author.platform, then twitter. platform?: 'twitter' | 'truth_social' | 'binance_square'; eventId: string; observedAt: number; receivedAt?: number; action: 'unpin'; author: TweetAuthor; text?: string; tweet?: TweetContent; }; 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'; eventId: string; observedAt: number; receivedAt?: number; actor: AccountEventActor; target: AccountEventActor & { handle: string; }; }; type UnfollowEvent = { kind: 'UNFOLLOW'; eventId: string; observedAt: number; receivedAt?: number; actor: AccountEventActor; target: AccountEventActor & { handle: string; }; }; type AffiliateAccountIdentity = AccountEventActor & { id: string; }; type AffiliateUpdateEvent = { action: 'added' | 'removed'; eventId: string; observedAt: number; receivedAt?: number; organization: AffiliateAccountIdentity; member: AffiliateAccountIdentity; }; type CalloutCaller = { username: string; image?: string; }; type CalloutReply = { caller: CalloutCaller; replyId: string; parentReplyId?: string; rootReplyId?: string; text?: string; mediaUrl?: string; }; type CalloutEvent = { platform: 'pump_fun'; caller: CalloutCaller; kind?: 'callout' | 'author_update' | 'reply' | 'repost'; text?: string; mediaUrl?: string; // Required for replies and author updates. replyId?: string; parentReplyId?: string; rootReplyId?: string; reference?: { caller: CalloutCaller; text?: string; mediaUrl?: string; reply?: CalloutReply; }; position?: { amountHeld?: string; valueUsd?: string; costBasisUsd?: string; pnlUsd?: string; pnlPercentage?: string; realizedPnlUsd?: string; }; token: { address: string; chainId?: string; symbol?: string; name?: string; image?: string; }; url?: string; marketCapUsd?: number; // SOL, exact decimal. calloutPrice?: string; // USD, exact decimal. calloutPriceUsd?: string; multiple?: number; // SOL peak, exact decimal. maxPriceSol?: string; // USD peak, exact decimal. maxPriceUsd?: string; calloutId: string; createdAt: number; receivedAt?: number; }; type CalloutUpdateEvent = CalloutEvent & { // Original envelope id, not calloutId. updateOf: string; }; type FomoCoin = { address: string; networkId: number; name?: string; symbol?: string; image?: string; metadataObservedAt?: number; priceUsd?: string; marketCapUsd?: string; liquidityUsd?: string; volume24hUsd?: string; change24hPercent?: string; }; type FomoThesisEvent = { platform: 'fomo'; coin: FomoCoin; author: { id: string; username: string; verified: boolean; displayName?: string; avatar?: string; twitter?: string; }; thesis: { id: string; text: string; createdAt: number; likes: number; replies: number; tradeId?: string; parentId?: string; segments?: Array<{ text: string; link?: string }>; }; position?: { tokenAmount?: string; usdValue?: string; unrealizedPnlUsd?: string; realizedPnlUsd?: string; unrealizedPnlPercent?: string; realizedPnlPercent?: string; closedAt?: number; }; observedAt?: number; receivedAt?: number; }; type FomoTokenVerifiedEvent = { platform: 'fomo'; coin: FomoCoin; observedAt: number; receivedAt?: number; }; type CharityAddedEvent = { id: string; name: string; slug: string; enabled: boolean; taxId?: string; location?: { city?: string; state?: string; country?: string; }; observedAt: number; }; 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; }; type NewsSource = { id: string; name: string; }; type NewsMedia = { url: string; type?: 'image' | 'video'; caption?: string; }; type NewsArticle = { source: NewsSource; url: string; title: string; publishedAt: string; receivedAt: number; modifiedAt?: string; primaryCategory?: string; categories: string[]; author?: string; keywords: string[]; description?: string; summary?: string; media: NewsMedia[]; content?: string; language?: string; copyright?: string; }; type EnvelopeBase< TFamily extends 'tweet' | 'account' | 'charity' | 'control' | 'news', TOp extends string, TPayload extends object, > = { v: 1; t: TFamily; op: TOp; id?: string; ts: number; d: TPayload; }; type TweetContentEnvelope = EnvelopeBase<'tweet', 'content', TweetContent>; type TweetMetaEnvelope = EnvelopeBase<'tweet', 'meta', TweetMeta>; type TweetUpdateEnvelope = EnvelopeBase<'tweet', 'update', TweetUpdate>; type TweetDeleteEnvelope = EnvelopeBase<'tweet', 'delete', TweetDeleteEvent>; type TweetPinEnvelope = EnvelopeBase<'tweet', 'pin', TweetPinEvent>; type TweetUnpinEnvelope = EnvelopeBase<'tweet', 'unpin', TweetUnpinEvent>; type AccountProfileEnvelope = EnvelopeBase<'account', 'profile_update', ProfileUpdateEvent>; type AccountFollowEnvelope = EnvelopeBase<'account', 'follow', FollowEvent>; type AccountUnfollowEnvelope = EnvelopeBase<'account', 'unfollow', UnfollowEvent>; type AccountAffiliateEnvelope = EnvelopeBase< 'account', 'affiliate_update', AffiliateUpdateEvent >; type AccountCalloutEnvelope = EnvelopeBase<'account', 'callout', CalloutEvent> & { id: string; }; type AccountCalloutUpdateEnvelope = EnvelopeBase<'account', 'callout_update', CalloutUpdateEvent> & { id: string; }; type AccountThesisEnvelope = EnvelopeBase<'account', 'thesis', FomoThesisEvent> & { id: string; }; type AccountTokenVerifiedEnvelope = EnvelopeBase<'account', 'token_verified', FomoTokenVerifiedEvent> & { id: string; }; type AccountEnvelope = | AccountProfileEnvelope | AccountFollowEnvelope | AccountUnfollowEnvelope | AccountAffiliateEnvelope | AccountCalloutEnvelope | AccountCalloutUpdateEnvelope | AccountThesisEnvelope | AccountTokenVerifiedEnvelope; type CharityAddedEnvelope = EnvelopeBase<'charity', 'added', CharityAddedEvent> & { id: string; }; type EmptyPayload = Record; type AuthPingCommand = { op: 'auth_ping' }; type AuthPongEnvelope = EnvelopeBase<'control', 'auth_pong', EmptyPayload>; type TwitterHandlesResultEnvelope = EnvelopeBase< 'control', 'twitter_handles_result', TwitterHandlesResult >; type ControlEnvelope = AuthPongEnvelope | TwitterHandlesResultEnvelope; type NewsEnvelope = EnvelopeBase<'news', 'article', NewsArticle> & { id: string; }; type TweetStreamEnvelope = | TweetContentEnvelope | TweetMetaEnvelope | TweetUpdateEnvelope | TweetDeleteEnvelope | TweetPinEnvelope | TweetUnpinEnvelope | AccountEnvelope | CharityAddedEnvelope | NewsEnvelope | ControlEnvelope; // Feed this router only values returned by a complete runtime decoder. // See the reference consumer linked below for the executable decoder. type DecodedEnvelope = | { kind: 'known'; event: TweetStreamEnvelope } | { kind: 'unknown' }; type ProtocolHandlers = { tweetContent: (event: TweetContentEnvelope) => void; tweetMeta: (event: TweetMetaEnvelope) => void; tweetUpdate: (event: TweetUpdateEnvelope) => void; tweetDelete: (event: TweetDeleteEnvelope) => void; tweetPin: (event: TweetPinEnvelope) => void; tweetUnpin: (event: TweetUnpinEnvelope) => void; accountProfileUpdate: (event: AccountProfileEnvelope) => void; accountFollow: (event: AccountFollowEnvelope) => void; accountUnfollow: (event: AccountUnfollowEnvelope) => void; accountAffiliateUpdate: (event: AccountAffiliateEnvelope) => void; accountCallout: (event: AccountCalloutEnvelope) => void; accountCalloutUpdate: (event: AccountCalloutUpdateEnvelope) => void; accountThesis: (event: AccountThesisEnvelope) => void; accountTokenVerified: (event: AccountTokenVerifiedEnvelope) => void; charityAdded: (event: CharityAddedEnvelope) => void; newsArticle: (event: NewsEnvelope) => void; authPong: (event: AuthPongEnvelope) => void; twitterHandlesResult: (event: TwitterHandlesResultEnvelope) => void; // Count and discard unknown input without logging its raw payload. unknown: () => void; }; function assertNever(value: never): never { throw new Error('Unhandled validated envelope'); } function route(decoded: DecodedEnvelope, handlers: ProtocolHandlers): void { if (decoded.kind === 'unknown') { handlers.unknown(); return; } const event = decoded.event; switch (event.t) { case 'tweet': switch (event.op) { case 'content': handlers.tweetContent(event); return; case 'meta': handlers.tweetMeta(event); return; case 'update': handlers.tweetUpdate(event); return; case 'delete': handlers.tweetDelete(event); return; case 'pin': handlers.tweetPin(event); return; case 'unpin': handlers.tweetUnpin(event); return; default: return assertNever(event); } case 'account': switch (event.op) { case 'profile_update': handlers.accountProfileUpdate(event); return; case 'follow': handlers.accountFollow(event); return; case 'unfollow': handlers.accountUnfollow(event); return; case 'affiliate_update': handlers.accountAffiliateUpdate(event); return; case 'callout': handlers.accountCallout(event); return; case 'callout_update': handlers.accountCalloutUpdate(event); return; case 'thesis': handlers.accountThesis(event); return; case 'token_verified': handlers.accountTokenVerified(event); return; default: return assertNever(event); } case 'charity': handlers.charityAdded(event); return; case 'news': handlers.newsArticle(event); return; case 'control': switch (event.op) { case 'auth_pong': handlers.authPong(event); return; case 'twitter_handles_result': handlers.twitterHandlesResult(event); return; default: return assertNever(event); } default: return assertNever(event); } } ``` ### 分发操作 | 类别 | 操作 | 含义 | | --- | --- | --- | | tweet | content | 原始发帖、回复、引用、转推,或支持的 Truth Social 帖子内容 | | tweet | meta | 为已知帖子添加 OCR、代币检测、CEX 和预测市场数据 | | tweet | update | 更新已知帖子的字段 | | tweet | delete, pin, unpin | 将已知帖子标记为已删除、已置顶或已取消置顶 | | account | profile_update, follow, unfollow, affiliate_update | 被监控账号的状态变化 | | charity | added | 新增慈善项目。可在控制台开启提醒 | | news | article | 来自已开启媒体的文章 | | control | twitter_handles_result | WebSocket 账号管理命令的结果 | ### 按事件流处理交付 消息信封没有确认、恢复 token 或序列号。不要根据 `ts` 或 `id` 推断顺序或恰好一次交付。 处理程序必须幂等,并保留足够的本地状态,以识别应用已经处理的事件。 ### 重连后回补 固定 `startDate` 和 `endDate`,在其他筛选条件不变的情况下持续使用 `nextCursor`,直到返回 null。以幂等方式处理每行。如果中断后的回放需要继续而不是重启,请一起保存时间范围和 cursor。 History 返回已存事件,不保证完整性或保留期限。`AFFILIATE` 返回已存的关联账号变更,不是当前列表快照。delete、pin 和 unpin 仍只在实时流中发送。 #### 回放恢复窗口 ```typescript type HistoryRow = { time: string; tweetId: string; }; type HistoryPage = { data: Array; metadata: { nextCursor: string | null }; }; async function replayAfterReconnect( checkpoint: string, processEvent: (event: HistoryRow) => Promise, saveCheckpoint: (eventTime: string) => Promise, ) { const apiKey = process.env.TWEETSTREAM_API_KEY; if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY"); const url = new URL("https://api.tweetstream.io/api/history"); const replayEnd = new Date().toISOString(); url.searchParams.set("handles", "marketdesk"); url.searchParams.set("startDate", checkpoint); url.searchParams.set("endDate", replayEnd); url.searchParams.set("limit", "1000"); url.searchParams.set("type", "TWEET"); const pages: Array> = []; let cursor: string | null = null; do { if (cursor) url.searchParams.set("cursor", cursor); const response = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` }, }); if (!response.ok) { throw new Error(`Replay failed (${response.status}): ${await response.text()}`); } const page = (await response.json()) as HistoryPage; pages.push(page.data); cursor = page.metadata.nextCursor; } while (cursor); for (const event of pages.reverse().flatMap((page) => [...page].reverse())) { await processEvent(event); } await saveCheckpoint(replayEnd); } ``` ### 修复握手错误 WebSocket 连接升级失败时,会在连接被接受前返回 JSON。客户端可以安全记录该响应。 | 状态 | 可能原因 | 修复方式 | | --- | --- | --- | | 400 | WebSocket 协议 header 缺失或无效 | 发送 `tweetstream.v1` 和有效认证 token 协议 | | 401 | API key 缺失、为空或无效 | 在控制台重新生成或轮换 API key | | 403 | 订阅状态不允许实时流 | 开始试用、续费或升级账号 | | 429 | 活跃 WebSocket 连接数达到限制 | 关闭旧连接或升级套餐 | | 503 | 连接限制器暂时不可用 | 使用退避策略重试 | #### 读取连接升级拒绝响应 ```json { "error": "WebSocket connection limit reached (3/3 active). Close an existing connection and retry.", "status": 429 } ``` ## 事件参考 找到事件类别,再打开完整载荷与集成指南。 Canonical URL: https://tweetstream.io/zh-cn/docs/payloads-detections-ocr ### 推文内容 将 `tweet/content` 作为首个可用帖子,再按 `author.platform` 分流。 - [打开社交内容参考](https://tweetstream.io/zh-cn/docs/social-content): 比较 Twitter、Truth Social 和币安广场载荷。 #### 按平台分流 帖子键使用 `(author.platform, tweetId)`。旧消息没有 platform 时按 `twitter` 处理。 #### 读取引用 引用帖子及附件保留在 `ref` 中。每个嵌套引用都有自己的 `tweetId`。 ### 富化元数据 每次收到 `tweet/meta` 快照时,替换 `(platform, tweetId)` 对应的元数据。不要保留新快照中缺少的字段。 - [打开 tweet/meta 参考](https://tweetstream.io/zh-cn/docs/tweet-updates-lifecycle#meta): 读取检测、OCR、代币、交易所和市场字段。 ### 推文更新 只更新 `tweet/update` 中存在的字段,不要替换整个帖子。 - [打开 tweet/update 参考](https://tweetstream.io/zh-cn/docs/tweet-updates-lifecycle#update): 应用文本补全、引用、媒体、投票和文章。 #### 匹配帖子 使用 `(platform, tweetId)` 匹配每条更新。 #### 替换引用快照 更新包含 `ref` 时,用该快照替换已存引用。 #### 补全可选字段 事件中存在的投票、文章和媒体值会完整替换对应字段。 ### 推文生命周期 将删除、置顶和取消置顶应用到已存帖子,不要覆盖原内容。 - [打开生命周期事件参考](https://tweetstream.io/zh-cn/docs/tweet-updates-lifecycle#lifecycle-events): 复制完整的删除、置顶和取消置顶载荷。 ### 账号事件 在 `account` 类别下分发资料、关注、取消关注和关联账号变更。 - [打开账号事件参考](https://tweetstream.io/zh-cn/docs/profile-follow-affiliate): 复制每种账号事件及其存储键。 #### 分发账号操作 按 `op` 分流。遇到未知操作时忽略消息,不要关闭 WebSocket。 #### 存储关联账号关系 将操作应用到 `(organization.id, member.id)`,`eventId` 仅用于识别完全相同的重放。 #### 回放已存变更 在通用 History API 中使用 `type=PROFILE`、`FOLLOW` 或 `AFFILIATE`。 ### 喊单 已选择账号的结构化 `account/callout` 事件通过同一个 WebSocket 发送。 - [打开喊单事件参考](https://tweetstream.io/zh-cn/docs/callouts): 配置账号并读取喊单者、代币、价格和时间字段。 #### 选择账号 在喊单标签页添加 pump.fun 用户名、Solana 钱包或个人主页链接。 #### 分发事件 处理初始 `account/callout` 内容和后续 `account/callout_update` 补充数据。先读取账号和代币地址,再处理可选字段。 #### 使用标识符 按信封 `id` 去重。将补充数据合并到 `d.updateOf` 指向的条目,包括嵌套字段,不再发送提醒。 ### 慈善项目事件 启用慈善项目提醒后,会收到仅实时发送的 `charity/added` 事件。 - [打开慈善项目事件参考](https://tweetstream.io/zh-cn/docs/charity): 复制完整事件及可选地点字段。 ### 新闻文章 已启用媒体发送 `news/article`;已存文章使用独立的新闻 History API。 - [打开新闻事件与 History](https://tweetstream.io/zh-cn/docs/news): 选择媒体、接收文章,并使用 `nextCursor` 继续翻页。 #### 选择媒体 筛选实时事件前,先读取并更新已启用媒体集合。 #### 读取实时文章 使用消息信封 `id` 实现幂等处理,并随文章保存 `source.id`。 #### 翻阅 History 将 `nextCursor` 原样传回,直到它为 null。 ### 事件顺序 维护幂等的本地投影。WebSocket 消息信封不含序列号。 #### 接收内容 保存第一条可用的内容事件。 #### 替换富化数据 按帖子键替换每个实时元数据快照。 #### 应用生命周期事件 将生命周期操作应用到同一帖子。 ### 控制消息 使用 `auth_pong` 记录连接状态,使用 `twitter_handles_result` 读取命令结果。 - [打开控制消息参考](https://tweetstream.io/zh-cn/docs/control): 复制认证和账号管理消息。 ### 账号管理通道 通过账号管理 WebSocket 或 REST API 关注或取消关注受监控的 Twitter 账号。 - [读取命令结果](https://tweetstream.io/zh-cn/docs/control#twitter-handles-result): 处理部分成功、幂等成功和失败行。 - [使用 REST 管理账号](https://tweetstream.io/zh-cn/docs/account-management): 添加、移除、查看和配置受监控账号。 ## 社交内容事件 社交内容使用 `tweet/content`。先读取 `author.platform`,再处理平台专属字段。 Canonical URL: https://tweetstream.io/zh-cn/docs/social-content ### tweet/content 按 `t="tweet"` 和 `op="content"` 分发内容事件,再以 `(author.platform, tweetId)` 作为帖子键。 #### 推文内容类型 ```typescript type VerifiedType = 'blue' | 'business' | 'government' | 'verified' | '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' | 'binance_square'; profileImage?: string; url?: string; verifiedLabel?: TweetVerifiedLabel; verifiedType?: VerifiedType; }; type Media = { url: string; } & ( | { type: 'video'; // Every video is a progressive MP4. A public still poster is included when available. thumbnail?: string; } | { type?: 'image' | '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; subtweet?: 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; }; ``` #### 推文内容事件 ```json { "v": 1, "t": "tweet", "op": "content", "id": "1234567890", "ts": 1702500000130, "d": { "tweetId": "1234567890", "kind": "reply", "text": "Could this launch today?", "createdAt": 1702500000000, "author": { "handle": "@marketdesk", "platform": "twitter" } } } ``` ### Twitter Twitter 内容可以是发帖、回复、引用或转推。引用和媒体在可用时提供。 ### Truth Social Truth Social 为唐纳德·特朗普(`@realDonaldTrump`)和白宫(`@WhiteHouse`)提供可选的实时推送。符合条件时,可在控制台的账号标签页中开启。 内容使用 tweet 消息信封,`author.platform="truth_social"`,类型可以是发帖、回复、引用或转推。观察到时可能收到元数据、更新、删除和置顶事件。不能通过通用账号管理添加其他 Truth Social 账号,也不能使用 History 回放。 #### Truth Social 内容事件 ```json { "v": 1, "t": "tweet", "op": "content", "id": "tweet-content", "ts": 1772000001060, "d": { "tweetId": "tweet-content", "text": "hello", "createdAt": 1772000001000, "receivedAt": 1772000001050, "kind": "post", "author": { "id": "truth-author", "handle": "@realDonaldTrump", "name": "Donald J. Trump", "platform": "truth_social", "url": "https://truthsocial.com/@realDonaldTrump" } } } ``` ### 币安广场 已启用的目录账号使用 `author.platform="binance_square"`。内容仅实时发送,不占用监控 Twitter 账号额度。 #### 币安广场内容事件 ```json { "v": 1, "t": "tweet", "op": "content", "id": "square-content", "ts": 1772000001060, "d": { "tweetId": "square-content", "text": "Weekly market outlook is live.", "createdAt": 1772000001000, "receivedAt": 1772000001050, "kind": "post", "author": { "id": "square-author", "handle": "@marketresearch", "name": "Market Research", "platform": "binance_square", "url": "https://www.binance.com/en/square/profile/marketresearch" } } } ``` ### 引用与媒体 回复、引用和转推可能包含 `ref`。每个嵌套引用都是独立帖子,附件从对应引用中读取。 ## 推文元数据、更新与生命周期 按 `(platform, tweetId)` 替换实时元数据快照并合并更新,再应用生命周期操作,不要替换已存内容。 Canonical URL: https://tweetstream.io/zh-cn/docs/tweet-updates-lifecycle ### tweet/meta 实时元数据 payload 是对应帖子的最新快照。请替换之前的元数据,以清除省略的字段。History 返回对应记录中保存的富化数据。恢复时,它只能填充空的本地元数据,不能覆盖实时快照。 #### 推文元数据类型 ```typescript type TweetMeta = { tweetId: string; // Pair with tweetId when merging enrichment. Omitted means twitter on legacy frames. platform?: 'twitter' | 'truth_social' | 'binance_square'; 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", "platform": "twitter", "ocr": { "text": "Chart showing SOL breakout at $100" }, "detected": { "tokens": [ { "symbol": "SOL", "name": "Solana", "priceUsd": 98.50, "sources": ["text", "ocr"] } ] } } } ``` ### tweet/update 将存在的字段应用到已存帖子。更新中的引用会替换已存引用快照。 #### 推文更新类型 ```typescript type VerifiedType = 'blue' | 'business' | 'government' | 'verified' | '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' | 'binance_square'; profileImage?: string; url?: string; verifiedLabel?: TweetVerifiedLabel; verifiedType?: VerifiedType; }; type Media = { url: string; } & ( | { type: 'video'; // Every video is a progressive MP4. A public still poster is included when available. thumbnail?: string; } | { type?: 'image' | '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; subtweet?: 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; // Pair with tweetId. On legacy frames, fall back to author.platform, then twitter. platform?: 'twitter' | 'truth_social' | 'binance_square'; article?: TweetArticle; kind?: TweetContentKind; translatedText?: string; author?: TweetAuthor; media?: Media[]; mentions?: TweetMention[]; poll?: TweetPoll; receivedAt?: number; urls?: TweetUrl[]; ref?: TweetReference; } & ( | { text?: string; textUpdateType?: never; } | { text: string; // Completes an earlier truncated rendering. This is not an edit signal. textUpdateType: 'completion'; } ); ``` #### 推文更新事件 ```json { "v": 1, "t": "tweet", "op": "update", "id": "1234567890", "ts": 1702500000180, "d": { "tweetId": "1234567890", "platform": "twitter", "ref": { "type": "reply", "tweetId": "1234567880", "text": "Mainnet is ready for launch." } } } ``` #### 递归引用更新 ```json { "v": 1, "t": "tweet", "op": "update", "id": "1234567890", "ts": 1702500000240, "d": { "tweetId": "1234567890", "platform": "twitter", "ref": { "type": "reply", "tweetId": "1234567880", "text": "Mainnet is ready for launch.", "author": { "handle": "@projectteam", "name": "Project Team" }, "subtweet": { "type": "quote", "tweetId": "1234567800", "text": "Launch proposal and contract details.", "author": { "handle": "@projectteam", "name": "Project Team" } } } } } ``` ### 生命周期事件 生命周期操作会更新已知帖子,不会替换已存内容。 #### 推文生命周期类型 ```typescript type VerifiedType = 'blue' | 'business' | 'government' | 'verified' | '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' | 'binance_square'; profileImage?: string; url?: string; verifiedLabel?: TweetVerifiedLabel; verifiedType?: VerifiedType; }; type Media = { url: string; } & ( | { type: 'video'; // Every video is a progressive MP4. A public still poster is included when available. thumbnail?: string; } | { type?: 'image' | '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; subtweet?: 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; // On legacy frames, fall back to author.platform, then twitter. platform?: 'twitter' | 'truth_social' | 'binance_square'; eventId?: string; deletedAt?: number; receivedAt?: number; author?: TweetAuthor; text?: string; }; type TweetPinEvent = { tweetId: string; // On legacy frames, fall back to tweet.author.platform or author.platform, then twitter. platform?: 'twitter' | 'truth_social' | 'binance_square'; eventId: string; observedAt: number; receivedAt?: number; action: 'pin'; author: TweetAuthor; text?: string; tweet?: TweetContent; }; type TweetUnpinEvent = { tweetId: string; // On legacy frames, fall back to tweet.author.platform or author.platform, then twitter. platform?: 'twitter' | 'truth_social' | 'binance_square'; eventId: string; observedAt: number; receivedAt?: number; action: 'unpin'; author: TweetAuthor; text?: string; tweet?: TweetContent; }; ``` #### tweet/delete 观察到此事件时,将帖子标记为已删除。 ##### 推文删除事件 ```json { "v": 1, "t": "tweet", "op": "delete", "id": "tweet-delete", "ts": 1772000001060, "d": { "eventId": "delete-1", "tweetId": "tweet-delete", "platform": "twitter", "author": { "id": "10228272", "handle": "@TeamYouTube" }, "deletedAt": 1772000001040, "receivedAt": 1772000001050, "text": "deleted" } } ``` #### tweet/pin 将帖子标记为已置顶,并保留附带的帖子快照。 ##### 推文置顶事件 ```json { "v": 1, "t": "tweet", "op": "pin", "id": "tweet-pin", "ts": 1772000001060, "d": { "action": "pin", "author": { "id": "10228272", "handle": "@TeamYouTube" }, "eventId": "pin-1", "observedAt": 1772000001060, "tweetId": "tweet-pin", "platform": "twitter", "receivedAt": 1772000001050, "text": "pinned" } } ``` #### tweet/unpin 移除对应帖子的置顶状态。 ##### 推文取消置顶事件 ```json { "v": 1, "t": "tweet", "op": "unpin", "id": "tweet-unpin", "ts": 1772000001060, "d": { "action": "unpin", "author": { "id": "10228272", "handle": "@TeamYouTube" }, "eventId": "unpin-1", "observedAt": 1772000001060, "tweetId": "tweet-unpin", "platform": "twitter", "receivedAt": 1772000001050, "text": "unpinned" } } ``` ## 资料、关注与关联账号事件 账号事件包含观察到的资料、关注和关联账号关系变更。 Canonical URL: https://tweetstream.io/zh-cn/docs/profile-follow-affiliate ### 账号事件类型 按 `op` 分流。遇到未知账号操作时忽略消息,不要关闭 WebSocket。 #### 账号事件契约 ```typescript type VerifiedType = 'blue' | 'business' | 'government' | 'verified' | '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' | 'binance_square'; 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'; eventId: string; observedAt: number; receivedAt?: number; actor: AccountEventActor; target: AccountEventActor & { handle: string; }; }; type UnfollowEvent = { kind: 'UNFOLLOW'; eventId: string; observedAt: number; receivedAt?: number; actor: AccountEventActor; target: AccountEventActor & { handle: string; }; }; type AffiliateAccountIdentity = AccountEventActor & { id: string; }; type AffiliateUpdateEvent = { action: 'added' | 'removed'; eventId: string; observedAt: number; receivedAt?: number; organization: AffiliateAccountIdentity; member: AffiliateAccountIdentity; }; ``` ### account/profile_update 只应用 `changes` 中存在的字段。字段缺失表示没有变化。 #### 资料更新事件 ```json { "v": 1, "t": "account", "op": "profile_update", "id": "profile-1", "ts": 1772000001060, "d": { "actor": { "id": "10228272", "handle": "@TeamYouTube", "name": "TeamYouTube", "profileImage": "https://pbs.twimg.com/media/avatar.jpg", "websiteUrl": "https://youtube.com" }, "changes": { "handle": "@TeamYouTube", "name": "Team YouTube", "websiteUrl": "https://youtube.com/new" }, "eventId": "profile-1", "kind": "PROFILE", "observedAt": 1772000001060, "previous": { "handle": "@OldTeam" }, "receivedAt": 1772000001050 } } ``` ### account/follow 使用稳定的发起账号和目标账号标识符存储观察到的关系。 #### 关注事件 ```json { "v": 1, "t": "account", "op": "follow", "id": "follow-1", "ts": 1772000001060, "d": { "actor": { "id": "10228272", "handle": "@TeamYouTube" }, "eventId": "follow-1", "kind": "FOLLOW", "observedAt": 1772000001060, "target": { "id": "4398626122", "handle": "@OpenAI" }, "receivedAt": 1772000001050 } } ``` ### account/unfollow 使用相同标识符移除观察到的关系。 #### 取消关注事件 ```json { "v": 1, "t": "account", "op": "unfollow", "id": "unfollow-1", "ts": 1772000001060, "d": { "actor": { "id": "10228272", "handle": "@TeamYouTube" }, "eventId": "unfollow-1", "kind": "UNFOLLOW", "observedAt": 1772000001060, "target": { "id": "4398626122", "handle": "@OpenAI" }, "receivedAt": 1772000001050 } } ``` ### account/affiliate_update 以 `(organization.id, member.id)` 作为关系键,幂等地应用 `added` 或 `removed` 操作。`eventId` 仅用于识别完全相同的重放。 关键词过滤不适用。使用 `GET /api/history?type=affiliate` 回放已存变更。History 不是当前列表快照,也不会回补启用此功能前的变更。 #### 关联账号更新事件 ```json { "v": 1, "t": "account", "op": "affiliate_update", "id": "affiliate-update-1", "ts": 1772000001060, "d": { "action": "added", "organization": { "id": "10228272", "handle": "@TeamYouTube", "name": "TeamYouTube", "profileImage": "https://pbs.twimg.com/media/avatar.jpg", "websiteUrl": "https://youtube.com" }, "member": { "id": "4398626122", "handle": "@OpenAI", "name": "OpenAI", "profileImage": "https://pbs.twimg.com/media/member-avatar.jpg" }, "eventId": "affiliate-update-1", "observedAt": 1772000001060, "receivedAt": 1772000001050 } } ``` ## 喊单事件 通过 WebSocket 接收 pump.fun 喊单、作者更新、回复和转发。先展示初始内容,再合并可选的补充数据。 Canonical URL: https://tweetstream.io/zh-cn/docs/callouts ### 选择喊单账号 在喊单标签页中使用 pump.fun 用户名、Solana 钱包或个人主页链接添加账号。标签可选。 - 账号使用 Solana 钱包,与代币所在链无关。 - Minimum:10 个账号。Pro、Scale 和 Ultra Speed:100 个。等待添加的账号也计入上限。 ### account/callout 收到内容即可展示,不必等待补充数据。`d.platform` 为 `pump_fun`,喊单者用户名和代币地址必有。 可选的 `d.token.chainId` 使用 CAIP-2:Solana 主网为 `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`;EVM 链如 `eip155:56`、`eip155:999`、`eip155:4663` 或 `eip155:8453`。缺失时表示未指定链。 文本、媒体、头像、持仓、代币详情和价格均可选。`d.caller.image` 是账号头像,`d.token.image` 是代币图片。 - 可选的 `d.kind`:`callout`、`author_update`、`reply` 或 `repost`。作者更新是原作者的新帖子,不是元数据补充。 - `d.caller` 是被跟踪的行为发起者。`d.reference` 保留原作者、文本和媒体;`d.reference.reply` 保留被转发的回复及其作者。两者都不是转发者。 - `replyId` 标识回复或作者更新。可选的 `parentReplyId` 和 `rootReplyId` 保留回复关系;`calloutId` 标识原始喊单。 - 价格使用精确十进制字符串:`calloutPrice` 和 `maxPriceSol` 以 SOL 计价,`calloutPriceUsd` 和 `maxPriceUsd` 以 USD 计价。请保留字符串或使用十进制库。USD 价格存在不代表 SOL 价格也存在。 - `marketCapUsd` 和 `multiple` 是可选数值。持仓值使用精确十进制字符串:`amountHeld` 为代币数量,`pnlPercentage` 为百分比,`*Usd` 字段为 USD 金额。 #### 喊单事件类型 ```typescript type CalloutCaller = { username: string; image?: string; }; type CalloutReply = { caller: CalloutCaller; replyId: string; parentReplyId?: string; rootReplyId?: string; text?: string; mediaUrl?: string; }; type CalloutEvent = { platform: 'pump_fun'; caller: CalloutCaller; kind?: 'callout' | 'author_update' | 'reply' | 'repost'; text?: string; mediaUrl?: string; // Required for replies and author updates. replyId?: string; parentReplyId?: string; rootReplyId?: string; reference?: { caller: CalloutCaller; text?: string; mediaUrl?: string; reply?: CalloutReply; }; position?: { amountHeld?: string; valueUsd?: string; costBasisUsd?: string; pnlUsd?: string; pnlPercentage?: string; realizedPnlUsd?: string; }; token: { address: string; chainId?: string; symbol?: string; name?: string; image?: string; }; url?: string; marketCapUsd?: number; // SOL, exact decimal. calloutPrice?: string; // USD, exact decimal. calloutPriceUsd?: string; multiple?: number; // SOL peak, exact decimal. maxPriceSol?: string; // USD peak, exact decimal. maxPriceUsd?: string; calloutId: string; createdAt: number; receivedAt?: number; }; type CalloutUpdateEvent = CalloutEvent & { // Original envelope id, not calloutId. updateOf: string; }; ``` #### 喊单事件 ```json { "v": 1, "t": "account", "op": "callout", "id": "callout.0123456789abcdef0123", "ts": 1772000001060, "d": { "platform": "pump_fun", "caller": { "username": "CryptoCalls", "image": "https://cdn.example/avatar.png" }, "kind": "callout", "text": "Watching this token.", "mediaUrl": "https://cdn.example/chart.png", "token": { "address": "So11111111111111111111111111111111111111112", "chainId": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" }, "url": "https://pump.fun/callouts/So11111111111111111111111111111111111111112/9ab3177c-7b35-4d99-9cca-8b426f74a270", "marketCapUsd": 156230.75, "calloutPriceUsd": "0.00018750", "multiple": 2.5, "calloutId": "9ab3177c-7b35-4d99-9cca-8b426f74a270", "createdAt": 1772000001000, "receivedAt": 1772000001050 } } ``` #### 转发其他作者的回复 ```json { "v": 1, "t": "account", "op": "callout", "id": "callout.repost-example", "ts": 1772000003060, "d": { "platform": "pump_fun", "kind": "repost", "caller": { "username": "MarketReader" }, "token": { "address": "So11111111111111111111111111111111111111112" }, "calloutId": "9ab3177c-7b35-4d99-9cca-8b426f74a270", "createdAt": 1772000003000, "reference": { "caller": { "username": "CryptoCalls" }, "text": "Watching this token.", "reply": { "caller": { "username": "ReplyAuthor" }, "replyId": "reply-example", "text": "Watching the volume too." } } } } ``` ### account/callout_update 将可选详情合并到原事件,不再触发提醒。 - 按本次信封的 `id` 去重。`d.updateOf` 指向原始信封 `id`,不是 `calloutId`。 - 仅合并本次提供的字段,包括嵌套的 `caller`、`token`、`position` 和 `reference` 字段。缺失字段不删除旧值。 - 找不到原事件时,存储或忽略更新,不要把它当作新喊单。 - 更新可选,即使没有更新也应展示初始内容。 #### 喊单补充数据 ```json { "v": 1, "t": "account", "op": "callout_update", "id": "callout_update.556abf0021a7ff937a1a804ec0a0c245d497bcc6090af66179e74ce03c0970e0", "ts": 1772000002060, "d": { "platform": "pump_fun", "updateOf": "callout.0123456789abcdef0123", "caller": { "username": "CryptoCalls" }, "kind": "callout", "token": { "address": "So11111111111111111111111111111111111111112", "symbol": "ABC", "name": "Alpha Beta", "image": "https://cdn.example/token.png" }, "calloutPriceUsd": "0.00018750", "calloutPrice": "0.00000125", "maxPriceSol": "0.00000450", "maxPriceUsd": "0.00067500", "calloutId": "9ab3177c-7b35-4d99-9cca-8b426f74a270", "createdAt": 1772000001000 } } ``` ### 使用标识符和时间戳 回复和转发有各自的信封 ID,但共享原始 `calloutId`。按信封 `id` 去重。 - `d.createdAt`:动态创建时间。 - `d.receivedAt`:可选的 TweetStream 接收时间。 - 信封 `ts`:准备发送的时间。 ## FOMO 观点与代币认证 在 FOMO 标签页监控用户和代币,通过现有 WebSocket 接收观点和观察到的代币认证。 Canonical URL: https://tweetstream.io/zh-cn/docs/fomo ### 选择用户和代币 - Minimum 包含 10 个 FOMO 用户和 10 个 FOMO 代币;Pro、Scale 和 Ultra Speed 包含 100 个用户和 100 个代币。这两项额度各自独立,也不占用 X 监控额度。 - 监控用户可接收其发布的观点。监控代币可接收任何作者对该代币的观点,以及首次观察到的认证。 - 在控制台的 FOMO 标签页管理两个列表。FOMO 事件仅实时发送,History 不提供回放。 ### 通过 REST 管理列表 使用 Bearer API key 认证请求 `/api/callouts`。管理时将 `platform` 设为 `fomo_user` 或 `fomo_coin`;两者的事件都使用 `d.platform: fomo`。 - `GET` 返回 `callers`、`pending` 和分平台的 `limits`。请读取 `limits` 获取当前额度;没有有效且符合条件的套餐时,各项均为 `0`。旧的顶层 `limit` 始终为 `10`。 - `PUT` 接受 `{ platform, input, label? }`。用户可用 FOMO 用户名或个人主页 URL。代币请用 FOMO 代币 URL 或 `network:address`;单独的合约地址必须能确定唯一网络。响应包含 `caller`;修改后重新获取列表。 - `DELETE` 接受 `{ platform, callerId }` 移除监控项,或 `{ platform, intentId }` 取消待处理项。使用 `GET` 返回的标识符;成功时返回 `removed: true`。 ### account/thesis 读取 `d.author`、`d.thesis` 和 `d.coin`。`d.platform` 为 `fomo`。持仓和代币元数据在可用时提供。使用 `thesis.id` 标识观点,使用 `(coin.networkId, coin.address)` 标识代币。 #### FOMO 事件类型 ```typescript type FomoCoin = { address: string; networkId: number; name?: string; symbol?: string; image?: string; metadataObservedAt?: number; priceUsd?: string; marketCapUsd?: string; liquidityUsd?: string; volume24hUsd?: string; change24hPercent?: string; }; type FomoThesisEvent = { platform: 'fomo'; coin: FomoCoin; author: { id: string; username: string; verified: boolean; displayName?: string; avatar?: string; twitter?: string; }; thesis: { id: string; text: string; createdAt: number; likes: number; replies: number; tradeId?: string; parentId?: string; segments?: Array<{ text: string; link?: string }>; }; position?: { tokenAmount?: string; usdValue?: string; unrealizedPnlUsd?: string; realizedPnlUsd?: string; unrealizedPnlPercent?: string; realizedPnlPercent?: string; closedAt?: number; }; observedAt?: number; receivedAt?: number; }; type FomoTokenVerifiedEvent = { platform: 'fomo'; coin: FomoCoin; observedAt: number; receivedAt?: number; }; ``` #### 观点事件 ```json { "v": 1, "t": "account", "op": "thesis", "id": "fomo:thesis:thesis-1", "ts": 1772000001060, "d": { "platform": "fomo", "coin": { "address": "So11111111111111111111111111111111111111112", "networkId": 1399811149, "name": "Example Token", "symbol": "EXM", "image": "https://cdn.example/token.png", "metadataObservedAt": 1772000000900, "priceUsd": "0.00000123456789012345", "marketCapUsd": "123456.7890123456789" }, "author": { "id": "user-1", "username": "SignalDesk", "displayName": "Signal Desk", "avatar": "https://cdn.example/avatar.png", "twitter": "signaldesk", "verified": true }, "thesis": { "id": "thesis-1", "text": "Watching liquidity", "createdAt": 1772000001000, "likes": 2, "replies": 1, "tradeId": "trade-1", "segments": [ { "text": "Watching liquidity", "link": "https://example.com/thesis" } ] }, "position": { "tokenAmount": "549351.552212985712345", "usdValue": "12.34567890123456789", "realizedPnlUsd": "0" }, "receivedAt": 1772000001050 } } ``` ### account/token_verified 此事件仅包含代币信息,表示 TweetStream 首次观察到被监控代币的认证,不包含作者、观点或持仓。`observedAt` 是观察时间,不是认证发生的时间。认证不代表安全保证或投资建议。 #### 观察到代币认证 ```json { "v": 1, "t": "account", "op": "token_verified", "id": "fomo:verified:1399811149:coin:1772000001000", "ts": 1772000001060, "d": { "platform": "fomo", "coin": { "address": "So11111111111111111111111111111111111111112", "networkId": 1399811149, "symbol": "EXM" }, "observedAt": 1772000001000, "receivedAt": 1772000001050 } } ``` ### 读取时间戳和市场数值 - 时间戳使用 Unix 毫秒。`thesis.createdAt` 是观点创建时间;可选的 `receivedAt` 是 TweetStream 接收时间;信封 `ts` 是准备发送的时间。 - 代币市场数值和持仓金额使用十进制字符串。代币市场数值附带 `metadataObservedAt`,表示该次观察的数据,不保证是当前报价。 - 按信封 `id` 幂等处理完全相同的重放。可选字段缺失表示不可用,不代表零。公布的 X 延迟数据不适用于 FOMO。 ## 慈善项目事件 在设置中开启慈善项目提醒,并接收实时 `charity/added` 事件。 Canonical URL: https://tweetstream.io/zh-cn/docs/charity ### charity/added 慈善项目事件仅实时发送。使用消息信封 `id` 忽略完全相同的重放。 #### 慈善项目新增类型 ```typescript type CharityAddedEvent = { id: string; name: string; slug: string; enabled: boolean; taxId?: string; location?: { city?: string; state?: string; country?: string; }; observedAt: number; }; ``` #### 慈善项目新增事件 ```json { "v": 1, "t": "charity", "op": "added", "id": "4ffccf6e-66d4-555a-af5d-ceb36c10e3f0", "ts": 1772000001060, "d": { "id": "4ffccf6e-66d4-555a-af5d-ceb36c10e3f0", "name": "Council on Foreign Relations", "slug": "council-on-foreign-relations", "enabled": true, "taxId": "13-1628168", "location": { "city": "New York City", "state": "NY", "country": "US" }, "observedAt": 1772000001060 } } ``` ## 新闻事件与 History 选择媒体,实时接收 `news/article`,并通过独立的游标式新闻 History API 获取已存文章。 Canonical URL: https://tweetstream.io/zh-cn/docs/news ### 选择媒体 使用控制台或 `GET` 和 `PATCH /api/news/sources` 查看并修改已启用的媒体。 #### 新闻来源响应 ```json { "sources": [ { "id": "bbc-news", "name": "BBC News", "icon": "/news-sources/bbc.png", "available": true, "enabled": true }, { "id": "dexerto", "name": "Dexerto", "icon": "/news-sources/dexerto.png", "available": false, "enabled": false } ] } ``` ### news/article 已启用媒体的文章会通过其他 TweetStream 事件所用的同一 WebSocket 发送。 #### 新闻文章类型 ```typescript type NewsSource = { id: string; name: string; }; type NewsMedia = { url: string; type?: 'image' | 'video'; caption?: string; }; type NewsArticle = { source: NewsSource; url: string; title: string; publishedAt: string; receivedAt: number; modifiedAt?: string; primaryCategory?: string; categories: string[]; author?: string; keywords: string[]; description?: string; summary?: string; media: NewsMedia[]; content?: string; language?: string; copyright?: string; }; ``` #### 新闻文章事件 ```json { "v": 1, "t": "news", "op": "article", "id": "news_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "ts": 1787786858123, "d": { "source": { "id": "bbc-news", "name": "BBC News" }, "url": "https://www.bbc.com/news/articles/example", "title": "Markets open after the holiday", "publishedAt": "2026-08-26T23:27:38.000Z", "receivedAt": 1787786858123, "primaryCategory": "Business", "categories": ["Business"], "author": "BBC News", "keywords": ["markets"], "description": "A brief description supplied by the publication.", "summary": "Markets reopened after the holiday.", "media": [ { "url": "https://ichef.bbci.co.uk/news/example.jpg", "type": "image", "caption": "A market floor" } ], "content": "The stored article text, when available.", "language": "en", "copyright": "BBC" } } ``` ### 新闻 History API `GET /api/news/history` 按 `(receivedAt DESC, id DESC)` 顺序返回已启用媒体的已存文章。`limit` 默认为 50,取值范围为 1 至 100。 使用 `nextCursor` 继续翻页。达到响应大小限制时,一页可能少于指定数量。这不代表任何保留期限或历史完整性保证。 #### 新闻 History 类型 ```typescript type NewsSource = { id: string; name: string; }; type NewsMedia = { url: string; type?: 'image' | 'video'; caption?: string; }; type NewsArticle = { source: NewsSource; url: string; title: string; publishedAt: string; receivedAt: number; modifiedAt?: string; primaryCategory?: string; categories: string[]; author?: string; keywords: string[]; description?: string; summary?: string; media: NewsMedia[]; content?: string; language?: string; copyright?: string; }; type HistoricalNewsArticle = { id: string; source: NewsSource; url: string; title: string; publishedAt: string; receivedAt: string; modifiedAt: string | null; primaryCategory: string | null; categories: string[]; author: string | null; keywords: string[]; description: string | null; summary: string | null; media: NewsMedia[]; content: string | null; language: string | null; copyright: string | null; }; type NewsHistoryResult = { data: HistoricalNewsArticle[]; metadata: { count: number; nextCursor: string | null; sourceId?: string; }; }; ``` #### 获取新闻 History ```typescript const apiKey = process.env.TWEETSTREAM_API_KEY; if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY"); const url = new URL( "https://api.tweetstream.io/api/news/history", ); url.searchParams.set("sourceId", "bbc-news"); url.searchParams.set("limit", "1"); const response = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}`, }, }); if (!response.ok) { throw new Error(`News history failed (${response.status}): ${await response.text()}`); } const page = await response.json(); console.log(page.data); if (page.metadata.nextCursor) { url.searchParams.set("before", page.metadata.nextCursor); } ``` #### 新闻 History 响应 ```json { "data": [ { "id": "news_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "source": { "id": "bbc-news", "name": "BBC News" }, "url": "https://www.bbc.com/news/articles/example", "title": "Markets open after the holiday", "publishedAt": "2026-08-26T23:27:38.000Z", "receivedAt": "2026-08-26T23:27:38.123Z", "modifiedAt": null, "primaryCategory": "Business", "categories": ["Business"], "author": "BBC News", "keywords": ["markets"], "description": "A brief description supplied by the publication.", "summary": "Markets reopened after the holiday.", "media": [ { "url": "https://ichef.bbci.co.uk/news/example.jpg", "type": "image", "caption": "A market floor" } ], "content": "The stored article text, when available.", "language": "en", "copyright": "BBC" } ], "metadata": { "count": 1, "nextCursor": "v1.MTc4Nzc4Njg1ODEyMwpuZXdzXzAxMjM0NTY3ODlhYmNkZWYwMTIzNDU2Nzg5YWJjZGVmMDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY", "sourceId": "bbc-news" } } ``` ## 控制消息 控制消息用于检查连接认证并返回账号管理命令结果。 Canonical URL: https://tweetstream.io/zh-cn/docs/control ### 发送 auth_ping 应用需要检查认证时,发送 `{ "op": "auth_ping" }`。这是客户端消息,不是服务端事件。 #### 认证 ping ```json { "op": "auth_ping" } ``` ### control/auth_pong 认证检查成功时,会返回空的 data 对象。 #### 认证 pong ```json { "v": 1, "t": "control", "op": "auth_pong", "ts": 1772000001060, "d": {} } ``` ### control/twitter_handles_result 读取每一行结果。同一响应中可能同时包含幂等成功和逐行失败。 #### 账号处理结果类型 ```typescript 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; }; ``` #### 账号处理结果事件 ```json { "v": 1, "t": "control", "op": "twitter_handles_result", "ts": 1772000001060, "d": { "action": "follow", "error": null, "requestId": "req-1", "results": [ { "input": "@one", "state": "added" } ] } } ``` ### 通过 WebSocket 管理账号 长期运行的后端连接可使用账号管理子协议添加或移除监控账号,无需另发 REST 请求。 #### 通过 WebSocket 关注账号 ```typescript 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); } }); ``` ## 通用 History API 通过通用 History API 回放已存内容、资料、关注和关联账号事件。 Canonical URL: https://tweetstream.io/zh-cn/docs/history-api ### 请求历史记录 使用 `GET /api/history` 在重连后回放已存事件,或检查指定时间窗口。 #### 检查权限 Pro、Scale 和 Ultra Speed 可以请求所有 History 事件类型。标准 key 查询基础套餐的监控账号;Ultra key 查询指定的 Ultra 账号。支持的类型为 `TWEET`、`PROFILE`、`FOLLOW` 和 `AFFILIATE`。delete、pin 和 unpin 仍只在实时流中发送。 #### 设置筛选条件 不提供账号筛选时,History 会查询该 key 当前监控的所有有效账号。筛选条件只能包含该 key 当前监控的账号。 | 参数 | 必填 | 说明 | | --- | --- | --- | | handle, handles, handle[], handles[] | 否 | 单个账号、重复参数或逗号分隔的多个账号 | | startDate | 否 | ISO datetime 下限 | | endDate | 否 | ISO datetime 上限 | | limit | 否 | 默认 100,最大 1000 | | type | 否 | TWEET、PROFILE、FOLLOW 或 AFFILIATE,默认为 TWEET | | cursor | 否 | 上一页返回的不透明 `nextCursor`;其他筛选条件必须保持不变 | ##### History 请求 ```typescript const apiKey = process.env.TWEETSTREAM_API_KEY; if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY"); const response = await fetch( "https://api.tweetstream.io/api/history?limit=25&type=TWEET", { headers: { Authorization: `Bearer ${apiKey}`, }, }, ); if (!response.ok) { throw new Error(`History failed (${response.status}): ${await response.text()}`); } const page = await response.json(); console.log(page.data); ``` ### 在有界时间范围内翻页 History 每次请求最多返回 1000 行,并按稳定的新到旧顺序排列。 #### 固定起止时间 记录一个 `endDate`,选择 `startDate`,并在此次回放的每一页中保持两者不变。 #### 使用 nextCursor 继续 将返回的每个 `nextCursor` 与相同的 type、账号和日期范围一起发送。cursor 为 null 时,此时间范围已结束。以幂等方式处理每行,并在恢复期间继续处理实时生命周期事件,因为 History 会分别回放已存事件类别,不会回放仅实时发送的状态变更。 ### 读取响应 先读取数据行和请求元数据,再推进回放窗口。 #### 读取数据行和元数据 结果按稳定的新到旧顺序遍历。`metadata.count` 是本页行数,`metadata.nextCursor` 用于继续相同的筛选查询,cursor 为 null 时结果集结束。较早保存的 `FOLLOW` 行可能带有空 `target`;这表示目标身份不可用,仍应继续处理该事件。 ##### History 响应类型 ```typescript type VerifiedType = 'blue' | 'business' | 'government' | 'verified' | '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'; eventId: string; observedAt: number; receivedAt?: number; actor: AccountEventActor; target: AccountEventActor; }; type UnfollowEvent = { kind: 'UNFOLLOW'; eventId: string; observedAt: number; receivedAt?: number; actor: AccountEventActor; target: AccountEventActor; }; type AffiliateAccountIdentity = AccountEventActor & { id: string; }; type AffiliateUpdateEvent = { action: 'added' | 'removed'; eventId: string; observedAt: number; receivedAt?: number; organization: AffiliateAccountIdentity; member: AffiliateAccountIdentity; }; type TweetMeta = { tweetId: string; // Pair with tweetId when merging enrichment. Omitted means twitter on legacy frames. platform?: 'twitter' | 'truth_social'; 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'>; }>; }; }; type HistoryMedia = { 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 HistoryTweetArticle = { description?: string; id?: string; publishedAt?: number; text?: string; thumbnail?: string; title?: string; updatedAt?: number; url?: string; }; type HistoryTweetPollChoice = { id?: string; image?: string; label?: string; votes?: number; }; type HistoryTweetPoll = { choices: HistoryTweetPollChoice[]; endsAt?: number; totalVotes?: number; updatedAt?: number; }; type TweetContentKind = 'post' | 'reply' | 'quote' | 'retweet'; type HistoryTweetReference = { article?: HistoryTweetArticle; type: 'reply' | 'quote' | 'retweet'; tweetId?: string; text?: string; translatedText?: string; author?: TweetAuthor; media?: HistoryMedia[]; poll?: HistoryTweetPoll; quoted?: HistoryTweetReference; subtweet?: HistoryTweetReference; }; type TweetContent = { tweetId: string; kind: TweetContentKind; // Original tweet text when the stored content includes both original and translated text. text: string; translatedText?: string; createdAt: number; author: TweetAuthor; article?: HistoryTweetArticle; link?: string; media?: HistoryMedia[]; mentions?: TweetMention[]; poll?: HistoryTweetPoll; // Epoch ms from the realtime payload when the stored content includes it. receivedAt?: number; urls?: TweetUrl[]; ref?: HistoryTweetReference; }; type HistoricalContent = | TweetContent | ProfileUpdateEvent | FollowEvent | UnfollowEvent | AffiliateUpdateEvent; type HistoricalTweetResponse = { tweetId: string; twitterId: string; twitterHandle: string | null; body: string; time: string; // ISO TweetStream receipt time for the historical event. receivedTime: string; link: string; messageType: 'TWEET' | 'PROFILE' | 'FOLLOW' | 'AFFILIATE'; content: HistoricalContent; meta?: TweetMeta; }; type HistoryResult = { data: HistoricalTweetResponse[]; metadata: { count: number; nextCursor: string | null; handle?: string; handles?: string[]; startDate?: string; endDate?: string; type?: 'TWEET' | 'PROFILE' | 'FOLLOW' | 'AFFILIATE'; }; }; ``` ##### History 响应 ```json { "data": [ { "tweetId": "account:affiliate:aff_01JQ8YQ5K8B8QKH6M0P8A1V2WX", "twitterId": "123", "twitterHandle": "organization", "body": "Added New Member (@newmember) to affiliate list", "time": "2025-04-09T00:00:00.000Z", "receivedTime": "2025-04-09T00:00:00.123Z", "link": "https://x.com/newmember", "messageType": "AFFILIATE", "content": { "action": "added", "eventId": "aff_01JQ8YQ5K8B8QKH6M0P8A1V2WX", "observedAt": 1744156800000, "receivedAt": 1744156800123, "organization": { "id": "123", "handle": "@organization", "name": "Organization", "verifiedType": "business" }, "member": { "id": "456", "handle": "@newmember", "name": "New Member", "profileImage": "https://pbs.twimg.com/profile_images/newmember_normal.jpg" } } } ], "metadata": { "count": 1, "nextCursor": null, "type": "AFFILIATE" } } ``` #### 映射关联账号数据行 对于 `AFFILIATE`,顶层账号是 organization,`content` 与实时 `affiliate_update` 载荷一致。 ### 处理错误 | 状态 | 响应 body | 含义 | | --- | --- | --- | | 400 | { "error": "Invalid query parameters" } | 日期、limit、type 或账号格式错误 | | 400 | { "error": "Invalid handle provided", "handle": "..." } | 账号校验失败 | | 400 | { "error": "startDate must be before endDate" } | 日期范围顺序错误 | | 400 | { "error": "Invalid cursor" } | cursor 格式错误或与其他筛选条件不匹配 | | 401 | { "error": "Missing or invalid API key" } | Bearer token 缺失或格式错误 | | 401 | { "error": "Invalid API key" } | Bearer token 格式正确,但不是有效 API key | | 403 | { "error": "History is available on Pro, Scale, and Ultra Speed" } | 套餐不包含请求的 History 类型 | | 403 | { "error": "Your subscription is not active", "message": "Please ensure your subscription is active", "status": "PAST_DUE" } | 订阅状态不允许使用 History | | 403 | { "error": "Handle ... is not among your tracked accounts" } | 请求回放的账号不在监控列表中 | | 429 | { "error": "Too many history requests", "retryAfterSeconds": 60 } | 超过请求频率限制 | ## 账号与设置 通过已认证的 REST 端点和账号设置管理监控账号及可选事件来源。 Canonical URL: https://tweetstream.io/zh-cn/docs/account-management ### 添加和移除账号 从后端调用 REST 端点来修改监控列表。 #### 发送账号 handle 在 `accounts` 中发送一个 handle 或 handle 数组。开头的 @ 可以省略,匹配不区分大小写。 ##### 添加账号 ```typescript const apiKey = process.env.TWEETSTREAM_API_KEY; if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY"); const response = await fetch("https://api.tweetstream.io/api/add-account", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ accounts: ["marketdesk", "realDonaldTrump"], }), }); if (!response.ok && response.status !== 207) { throw new Error(`Add failed (${response.status}): ${await response.text()}`); } console.log(await response.json()); ``` ##### 移除账号 ```typescript const apiKey = process.env.TWEETSTREAM_API_KEY; if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY"); const response = await fetch("https://api.tweetstream.io/api/remove-account", { method: "DELETE", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ accounts: "marketdesk", }), }); if (!response.ok && response.status !== 207) { throw new Error(`Remove failed (${response.status}): ${await response.text()}`); } console.log(await response.json()); ``` #### 选择凭证范围 标准 API key 更新标准监控账号列表。有效的 Ultra key 更新 Ultra 选择,并执行其已付费账号上限。 #### 读取每条结果 响应包含每个 handle 的处理结果。重试前应读取每行状态,而不能只看 HTTP 状态。 ##### 命令结果 ```json { "action": "follow", "requestId": "8b4f9c9c-9e7b-4a0c-9c7d-2d4d6f0a9a25", "error": null, "results": [ { "input": "marketdesk", "state": "added" }, { "input": "realDonaldTrump", "state": "added" } ], "summary": { "failed": 0, "succeeded": 2, "total": 2 } } ``` ### 检查当前用量 使用标准或 Ultra API key 调用 `/api/me`。此私有响应不会缓存。 #### 读取套餐和用量字段 响应包含基础套餐用量和附加的 Ultra Speed 详情。 | 字段 | 类型 | 说明 | | --- | --- | --- | | credentialScope | standard 或 ultra_speed | 本次 Bearer key 的权限范围 | | plan | BASIC、ELITE 或 ENTERPRISE | 运行时套餐枚举 | | trackedAccounts | object | 数量、限制和规范化 handles | | websocket | object | 当前活跃连接数和套餐限制 | | stripe | object | 订阅状态和账期字段;标识符应视为私有 | | ultraSpeed | object 或 null | Ultra 状态、账期、限制、独立 WebSocket 用量和取消时间 | #### 查看响应 ##### 账号用量请求 ```typescript const apiKey = process.env.TWEETSTREAM_API_KEY; if (!apiKey) throw new Error("Missing TWEETSTREAM_API_KEY"); const response = await fetch("https://api.tweetstream.io/api/me", { headers: { Authorization: `Bearer ${apiKey}`, }, }); if (!response.ok) { throw new Error(`Account status failed (${response.status}): ${await response.text()}`); } console.log(await response.json()); ``` ##### 账号用量响应 ```json { "credentialScope": "standard", "plan": "ELITE", "trackedAccounts": { "count": 2, "limit": 250, "handles": ["marketdesk", "realDonaldTrump"] }, "websocket": { "count": 1, "limit": 10 }, "stripe": { "subscriptionStatus": "ACTIVE", "customerId": "[redacted]", "hasCustomer": true, "subscriptionId": "[redacted]", "currentPeriodStart": "2026-06-30T00:00:00.000Z", "currentPeriodEnd": "2026-07-30T00:00:00.000Z", "canceledAt": null }, "ultraSpeed": { "active": true, "status": "ACTIVE", "billingCycle": "MONTHLY", "paymentRail": "STRIPE_CARD", "accountLimit": 25, "websocket": { "count": 1, "limit": 5 }, "currentPeriodEnd": "2026-07-30T00:00:00.000Z", "cancelAtPeriodEnd": false, "canceledAt": null } } ``` ### 跟踪关联账号列表变更 已监控的企业账号在有效或试用中的 Pro、Scale 和 Ultra 套餐上默认关闭关联账号列表变更。 #### 开启或关闭提醒 使用 `POST /api/affiliate-alerts`、`DELETE /api/affiliate-alerts` 或控制台。将已监控的 handle 作为 `account` 发送,开头的 @ 可以省略。重复请求是安全的。成功时返回 `{ account, affiliateAlertsEnabled }`。 ##### 修改提醒设置 ```typescript async function setAffiliateAlerts(account: string, enabled: boolean) { const response = await fetch("https://api.tweetstream.io/api/affiliate-alerts", { method: enabled ? "POST" : "DELETE", headers: { Authorization: `Bearer ${process.env.TWEETSTREAM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ account }), }); if (!response.ok) { throw new Error(await response.text()); } return await response.json(); } console.log(await setAffiliateAlerts("marketdesk", true)); console.log(await setAffiliateAlerts("marketdesk", false)); ``` #### 处理设置错误 开启提醒时,TweetStream 会检查账号是否仍符合企业账号资格。检查失败时返回 `400` 和 `{ error: "This account is not a business account." }`。 | 状态 | 出现时机 | 处理方式 | | --- | --- | --- | | 200 | 已保存的设置与请求一致 | 读取 `affiliateAlertsEnabled` | | 400 | 请求 body 无效或账号不是企业账号 | 修正请求 | | 401 | Bearer key 缺失或无效 | 发送有效的 API key | | 403 | 订阅或功能权限未生效 | 检查套餐权限 | | 404 | 此 API key 未监控该 handle | 检查已监控 handle | | 500 | TweetStream 无法确认资格 | 稍后重试 | #### 保存和回放变更 变更会发送到 WebSocket、控制台实时流和 Discord 通知路由。关键词过滤不适用。按 `(organization.id, member.id)` 保存关系,`eventId` 仅用于完全相同的重放去重。使用 `GET /api/history?type=affiliate` 回放已记录变化。不提供当前列表快照或功能上线前回补。 ### 接收币安广场帖子 币安广场账号默认关闭。可在控制台或通过此 API 开启。它们与 X 监控账号分开管理,不占用 X 账号额度。 #### 列出和修改设置 `GET /api/binance-square` 返回已保存的设置,以及 `displayName`、`squareHandle`、`profileUrl`、`avatarUrl` 和 `enabled`。`POST /api/binance-square` 开启账号;`DELETE /api/binance-square` 关闭账号。将广场 handle 放在 `account` 中,开头的 @ 可以省略。重复请求是安全的。 - `profileUrl` 是规范地址。没有头像时,`avatarUrl` 为 `null`。 - 设置属于 TweetStream 用户,因此生效的 Standard 和 Ultra 凭证会共享它们。 - 保存成功时返回 `{ account, binanceSquareEnabled }`,其中包含规范 handle。 ##### 列出币安广场账号 ```typescript const response = await fetch( "https://api.tweetstream.io/api/binance-square", { headers: { Authorization: `Bearer ${process.env.TWEETSTREAM_API_KEY}`, }, }, ); if (!response.ok) { throw new Error(await response.text()); } const catalog = await response.json(); console.log(catalog); // { // accounts: [{ // avatarUrl: "https://bin.bnbstatic.com/static/content/live-admin-api/images/chVikg58jFQ6ScXcVmWNmj.png", // displayName: "币安Binance华语", // enabled: true, // profileUrl: "https://www.binance.com/en/square/profile/Vpo7Qwqy63rk7_Km3zYYaQ", // squareHandle: "binancezh" // }], // canManage: true // } ``` ##### 开启或关闭帖子 ```typescript async function setBinanceSquarePosts(account: string, enabled: boolean) { const response = await fetch("https://api.tweetstream.io/api/binance-square", { method: enabled ? "POST" : "DELETE", headers: { Authorization: `Bearer ${process.env.TWEETSTREAM_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ account }), }); if (!response.ok) { throw new Error(await response.text()); } return await response.json(); } console.log(await setBinanceSquarePosts("binancezh", true)); // { account: "binancezh", binanceSquareEnabled: true } ``` #### 处理实时内容 已开启账号的帖子、回复和引用会通过 WebSocket 和适用的 Discord 路由发送。广场内容不会存入 History,因此重连后不会补发错过的帖子。关键词过滤仍然生效。Discord 默认使用全局 webhook,适用账号专属路由时除外。 - 内容使用 `tweet/content`,`author.platform` 为 `binance_square`,`kind` 为 `post`、`reply` 或 `quote`。 - 后续推文操作的 `d.platform` 为 `binance_square`;使用 `(platform, tweetId)` 作为帖子键。 - 有引用上下文时,回复和引用会包含 `ref`。帖子和个人主页链接使用币安规范地址。 ##### 币安广场内容事件 ```json { "v": 1, "t": "tweet", "op": "content", "id": "358051617962575", "ts": 1772000001100, "d": { "tweetId": "358051617962575", "kind": "quote", "text": "Example Binance Square quote", "createdAt": 1772000001000, "receivedAt": 1772000001100, "link": "https://www.binance.com/en/square/post/358051617962575", "author": { "id": "Vpo7Qwqy63rk7_Km3zYYaQ", "handle": "@binancezh", "platform": "binance_square", "url": "https://www.binance.com/en/square/profile/Vpo7Qwqy63rk7_Km3zYYaQ" }, "ref": { "type": "quote", "tweetId": "358012920288073" } } } ``` #### 处理设置错误 | 状态 | 出现时机 | 处理方式 | | --- | --- | --- | | 200 | 已列出目录或保存设置 | 读取响应 body | | 400 | 请求 body 无效或账号不在目录中 | 修正请求 | | 401 | Bearer key 缺失或无效 | 发送有效的 API key | | 403 | API 凭证未生效 | 检查订阅状态 | | 500 | TweetStream 无法保存此设置 | 稍后重试 | ### 读取账号处理结果 | 状态 | 出现时机 | 推荐处理 | | --- | --- | --- | | added | 账号已加入监控列表 | 视为成功 | | already_following | 账号已经在监控中 | 作为逐行幂等结果处理 | | removed | 账号已移除 | 视为成功 | | not_following | 账号原本没有被监控 | 作为逐行幂等结果处理 | | invalid_input, duplicate, not_found, failed | 输入无效、输入重复、账号不存在或操作失败 | 展示逐行 message,仅在合适时重试 | ### 读取 REST 状态码 Add 和 remove 端点为每个 handle 返回一行结果。HTTP 状态说明批处理结果,每行 result 说明对应 handle 的结果。 | 状态 | 出现时机 | 说明 | | --- | --- | --- | | 200 | 没有任何行失败,包括幂等成功结果 | `summary.failed` 为 0;`already_following` 和 `not_following` 都属于成功结果 | | 207 | 部分行成功,部分行失败 | 重试前读取 `results` 和 `summary` | | 400 | 请求 body 无效,或每一行都因非临时原因失败 | 重试前先修正请求或逐行错误 | | 503 | 每一行都因临时原因失败 | 使用退避策略重试整批请求 | ### 检查套餐限制 - Minimum:试用后 50 个监控账号和 3 个 WebSocket 连接。 - Trial:3 天内 5 个监控账号和 1 个 WebSocket 连接。 - Pro:250 个监控账号和 10 个 WebSocket 连接。 - Scale:可在定价页自助配置更高的监控账号和 WebSocket 限制。 - Pro、Scale 和 Ultra Speed 提供完整 History 回放。 ## 消费者示例 复制完整消费者,再用你的幂等应用处理程序替换控制台输出。 Canonical URL: https://tweetstream.io/zh-cn/docs/examples ### 构建参考消费者 组合使用以下示例。先使用完整分发器,再添加一种重连客户端、持久恢复和账号管理。 - [分发所有操作](https://tweetstream.io/zh-cn/docs/examples#complete-router): 验证并分发全部 15 种服务端操作。 - [使用 Node.js 连接](https://tweetstream.io/zh-cn/docs/examples#connect): 认证、区分升级失败并自动重连。 - [使用 Python 连接](https://tweetstream.io/zh-cn/docs/examples#python): 使用 Python 运行相同的连接循环。 - [使用 History 恢复](https://tweetstream.io/zh-cn/docs/examples#backfill): 用不透明 cursor 回放已存数据页。 - [查询 History](https://tweetstream.io/zh-cn/docs/examples#request): 设置有界时间范围和固定筛选条件。 - [管理账号](https://tweetstream.io/zh-cn/docs/examples#add-remove): 添加和移除受监控的 Twitter handle。 ### 分发所有服务端操作 此 TypeScript 分发器覆盖全部 15 种服务端操作。它会先验证输入,并在遇到未知操作时忽略该消息,而不是关闭 WebSocket。 让每个处理程序保持简短。在调用策略、通知或执行边界前,先保存回执和本地投影。 #### 完整 TypeScript 分发器 [使用首次出现的完整示例。](https://tweetstream.io/zh-cn/docs/envelope-delivery-recovery-errors#envelope-define-the-envelope) ### 运行 5 分钟连通性测试 添加一个监控账号,复制示例并运行。看到 `TweetStream connected` 表示认证成功。该账号发布内容后会收到事件。 #### 运行 TypeScript 连通性测试 [使用首次出现的完整示例。](https://tweetstream.io/zh-cn/docs/quickstart#smoke-test-run-the-typescript-smoke-test) ### 连接 美国境内使用 `wss://ws-iad.tweetstream.io/ws`,其他地区使用 `wss://ws-global.tweetstream.io/ws`。服务端返回 `tweetstream.v1`,不会回显携带认证 token 的协议。示例通过指数退避重连。短连接会继续增加等待时间,稳定连接 30 秒后重置。 #### 使用 Node.js 连接 [使用首次出现的完整示例。](https://tweetstream.io/zh-cn/docs/production-consumer#connect-connect-with-node-js) ### 使用 Python 连接 使用任何能发送这两个子协议的 WebSocket 运行时。正常关闭和传输错误都会通过指数退避重连。短连接会继续增加等待时间,稳定连接 30 秒后重置。 #### Python 重连客户端 [使用首次出现的完整示例。](https://tweetstream.io/zh-cn/docs/connection-auth-heartbeat#python-python-reconnecting-consumer) ### 重连后回补 固定 `startDate` 和 `endDate`,在其他筛选条件不变的情况下持续使用 `nextCursor`,直到返回 null。以幂等方式处理每行。如果中断后的回放需要继续而不是重启,请一起保存时间范围和 cursor。 History 返回已存事件,不保证完整性或保留期限。`AFFILIATE` 返回已存的关联账号变更,不是当前列表快照。delete、pin 和 unpin 仍只在实时流中发送。 #### 回放恢复窗口 [使用首次出现的完整示例。](https://tweetstream.io/zh-cn/docs/envelope-delivery-recovery-errors#backfill-replay-a-recovery-window) ### 请求 History 重连后或查看特定时间范围时,使用 `GET /api/history` 回放已存事件。 #### 检查权限 Pro、Scale 和 Ultra Speed 可以请求所有 History 事件类型。标准 key 查询基础套餐的监控账号;Ultra key 查询指定的 Ultra 账号。支持的类型为 `TWEET`、`PROFILE`、`FOLLOW` 和 `AFFILIATE`。delete、pin 和 unpin 仍只在实时流中发送。 #### 设置筛选条件 不提供账号筛选时,History 会查询该 key 当前监控的所有有效账号。筛选条件只能包含该 key 当前监控的账号。 | 参数 | 必填 | 说明 | | --- | --- | --- | | handle, handles, handle[], handles[] | 否 | 单个账号、重复参数或逗号分隔的多个账号 | | startDate | 否 | ISO datetime 下限 | | endDate | 否 | ISO datetime 上限 | | limit | 否 | 默认 100,最大 1000 | | type | 否 | TWEET、PROFILE、FOLLOW 或 AFFILIATE,默认为 TWEET | | cursor | 否 | 上一页返回的不透明 `nextCursor`;其他筛选条件必须保持不变 | ##### History 请求 [使用首次出现的完整示例。](https://tweetstream.io/zh-cn/docs/history-api#request-filters-history-request) ### 添加和移除账号 从后端调用 REST 端点来修改监控列表。 #### 发送账号 handle 在 `accounts` 中发送一个 handle 或 handle 数组。开头的 @ 可以省略,匹配不区分大小写。 ##### 添加账号 [使用首次出现的完整示例。](https://tweetstream.io/zh-cn/docs/account-management#add-remove-input-add-accounts) ##### 移除账号 [使用首次出现的完整示例。](https://tweetstream.io/zh-cn/docs/account-management#add-remove-input-remove-an-account) #### 选择凭证范围 标准 API key 更新标准监控账号列表。有效的 Ultra key 更新 Ultra 选择,并执行其已付费账号上限。 #### 读取每条结果 响应包含每个 handle 的处理结果。重试前应读取每行状态,而不能只看 HTTP 状态。 ##### 命令结果 [使用首次出现的完整示例。](https://tweetstream.io/zh-cn/docs/account-management#add-remove-result-command-result)