🐋WhaleWatch
24h 扫描
说明
TG 频道登录
🔌API 参考手册 · 订阅方接入

Signals API 接入文档

持有 API key 的订阅方按本文接入:鉴权、tier、字段契约与失败语义。

基址 https://whalewatch.wired.fund
章节
  1. 1. 快速开始
  2. 2. 鉴权
  3. 3. Tier:realtime 与 delayed
  4. 4. 订阅范围与当前开放状态
  5. 5. GET /api/signals —— 请求参数
  6. 6. 核心概念:信号(事件) vs 视图
  7. 7. 信号 · ① 原始事件(bus[])
  8. 8. 信号 · ② 策略事件(strategies)
  9. 9. 视图(拉取展示,非信号)
  10. 10. POST <你的端点> —— Webhook 推送(realtime tier 专属)
  11. 11. 失败语义
  12. 12. 频率、拓扑与现价
  13. 13. 公开端点(无需 key)
  14. 14. GET /api/market-card/{cid} —— 按需查询 · 市场深度卡
  15. 15. 常见问题
  16. 16. 变更记录
  17. 17. 账号端点(Telegram 登录)
端点
  1. GET/api/signals/list
  2. GET/api/signals
  3. POST<你的端点>
  4. GET/api/record
  5. GET/api/health
  6. GET/api/continuity
  7. GET/api/dataset/record.csv
  8. GET/embed/record · /embed/status
  9. GET/api/pulse
  10. GET/api/calibration
  11. GET/api/market-card/{cid}
  12. GET/api/auth/login?c=<code>
  13. POST/api/auth/logout
  14. GET/api/auth/telegram
面向持有 API key 的订阅方。key 由运营者签发,明文只显示一次(库中仅存sha256),丢失只能重新签发。本文覆盖两类端点:信号(推/拉事件与视图)与按需查询(点名一个市场现算答案)——两类的可靠性承诺不同,见下表。设计取舍与口径修订史见内部契约docs/signals-api.md,本文只讲怎么用。

基址:https://whalewatch.wired.fund

端点方法鉴权缓存用途
/api/signalsGETAPI key30s主 feed:信号(事件)+ 视图
/api/signals/listGETAPI key30s名录:这把 key 实际收得到哪些信号(§4.1)
/api/market-card/{cid}GETAPI key30s单市场深度卡(realtime + market 范围)
/api/recordGET无(公开)60s已公开发布信号的战绩与逐日存证链(§13)
/api/healthGET无(公开)无引擎存活探针(200 / 503)
/api/continuityGET无(公开)无数据连续性 · 30 天起算时钟(§13)
/api/dataset/record.csvGET无(公开)300s已发布信号全量台账 CSV 数据集(§13)
/api/pulseGET无(公开)300s市场脉搏日榜(§13)
/api/calibrationGET无(公开)600s市场校准研究(§13)
/embed/record /embed/statusGET无(公开)60s可嵌入 HTML 卡片:战绩卡 / 状态徽章(§13)
/api/auth/loginGET一次性登录码无消费 Telegram bot 签发的登录码,置会话(§17)
/api/auth/telegramGETHMAC 签名验证无Telegram Login Widget 回调(§17)
/api/auth/logoutPOST会话 cookie无登出:吊销会话并清 cookie(§17)
/api/me/subscriptionGET PUT DELETE会话 cookie无读写「我的」信号订阅(§17)
/api/me/keysGET POST DELETE会话 cookie无自助领取 / 吊销 API key(§17)
/api/me/webhooksGET POST DELETE会话 cookie无自助登记 / 启停 webhook 端点(§17)
webhook(你的端点)POSTHMAC 签名验证—事件推送(realtime tier 专属)

本文的端点分两类,它们的可靠性承诺不同,别把一类的经验套到另一类上:

类别端点承诺
信号/api/signals、/api/signals/list、webhook、/api/record、/api/continuity、/api/dataset/record.csv、/api/pulse、/api/calibration零上游调用——全部字段来自已持久化状态,你的请求不会失败于上游抖动
按需查询/api/market-card/{cid}按需向 Polymarket 取数——会背压(429),也会受上游波动影响
账号/api/auth/login、/api/auth/telegram、/api/auth/logout、/api/me/*零上游调用——只读写本地会话/订阅表;不返回任何信号数据

深度卡不是信号:它没有事件 id、不可被推送、也不是任何事件的折叠。它是你点名一个市场、我们现算一份答案。接入前请读完 §14。

一条跨类别的恒定承诺:

  • 字段只增不改:既有字段名称与语义不变;解析时请忽略未知字段。

1. 快速开始

cURLcurl -s -H "x-feed-token: $WHALEWATCH_KEY" \
  "https://whalewatch.wired.fund/api/signals?windowHours=24" | jq
Nodeconst KEY = process.env.WHALEWATCH_KEY;

async function pull() {
  const res = await fetch(
    "https://whalewatch.wired.fund/api/signals?windowHours=24",
    { headers: { "x-feed-token": KEY }, signal: AbortSignal.timeout(15000) },
  );
  if (res.status === 401) throw new Error("key 无效或已被吊销");
  if (res.status === 403) throw new Error("服务端未开放 feed,联系运营者");
  const feed = await res.json();
  if (!feed.healthy) return null; // 故障 ≠ 没信号,见 §11
  return feed;
}

setInterval(() => void pull(), 60_000); // 推荐节奏:1 次/分钟

接入前先调一次 GET /api/signals/list(同一把 key),确认你这把 key 实际能收到哪些信号——省掉「接口通、返回 200、就是没数据」的排查。见 §4.1。


2. 鉴权

两种写法等价:

HTTPx-feed-token: <YOUR_API_KEY>
HTTPauthorization: Bearer <YOUR_API_KEY>

key 形如 wlk_ + 32 字符 base64url。

状态含义处理
401key 缺失、错误或已吊销核对 header;联系运营者
403服务端尚未开放 feed联系运营者
200 + 响应含 error服务端内部异常见 §11,不要当成没信号

401/403 的响应体是 { "error": "<说明>" },没有 feed 结构。/api/signals 不做请求数限流(30s 缓存即保护);/api/record 限流见 §13。


3. Tier:realtime 与 delayed

tier语义
realtime实时;可挂 webhook 推送(§10)
delayed整个 feed 以 now − delayedMin 构建(当前部署延迟 30 分钟)

延迟层不删减字段,只是时间平移:晚于基准时刻的事件不可见、晚于基准时刻的结算仍显示为进行中;updatedAt 即数据基准时刻(已时移),展示「截至 HH:MM」以它为准。唯一例外:healthy/staleLoops 永远按真实时间评估。


4. 订阅范围与当前开放状态

签发 key 时可限定订阅范围,过滤在服务端执行:

类型对应的信号出现在
strategy② 策略事件strategies 段 + webhook
large① 大额成交事件bus[] + webhook(勾选)
consensus① 聪明钱共识事件bus[] + webhook(勾选)
discovery① 钱包发现事件bus[] + webhook(勾选)
market—(非事件,属按需查询类)/api/market-card/{cid},仅 realtime(§14)
  • 未限定 = 不限,拿全部类型。
  • key 不含 strategy 时 strategies 段是空结构(形状不变,不必判空)。
  • 视图字段(active[]/settled[]/record30d,§9)不受范围约束,任何有效 key 都能拿到。

当前开放状态(本页渲染时按运营开关实时生成):

你这把 key 现在实际收得到什么· 打开本页时查库生成 · 非手写快照
✅
核心端点
运行中
active · settled · record30d
📈
strategies
12 档对外发布
保守 · 激进 · 重仓共识 · 首发共识 · 巨鲸 · 超级巨鲸 · 巨鲸精英 · 一边倒分歧 · 早期赢家跟投 · 相对冲击 · 赛前巨鲸 · 滚球巨鲸
🚚
bus[] sourceType
large · consensus · discovery
三类全开 —— bus[] 会出现全部 sourceType
🔗
存证链最新一条
2026-10-10
见 /api/record 的 digest

标「未开启」的事件类型不会产出任何数据(bus[] 中无该类型条目);strategies 只含已放开推送的档位——遍历请以 recordByStrategy 的键为准,不要写死档数。

4.1 GET/api/signals/list —— 机读版名录

上面那张状态表是给人看的、且只讲全局开关。你这把 key 实际能收到什么(全局开关 ∩ 你的订阅范围)由本端点回答,全 ASCII、可直接进代码:

JSON{
  "updatedAt": 1755412800, // 响应时刻(unix 秒)
  "tier": "delayed", // realtime | delayed
  "signals": {
    "bus": [
      // ① 原始事件(§7)
      { "type": "large", "threshold": 50000 },
      { "type": "large", "threshold": 500000 },
      { "type": "consensus", "threshold": 2 },
    ],
    "strategy": [
      // ② 策略事件(§8)
      {
        "code": "mega_whale",
        "source": "heavy",
        "events": ["entry", "settle"],
      },
      {
        "code": "first_mover_consensus",
        "source": "consensus",
        "events": ["entry", "settle"],
      },
    ],
  },
}
字段说明
bus[].type与 bus[] 条目的 sourceType 同值
bus[].threshold该档下限;语义随类型:large=USD,consensus=钱包数,discovery=评分
strategy[].code档位码,认档用它(§8.3);null = 运营手工建、未登记码
strategy[].source检测器族(§8.3)
strategy[].events该档会发出的事件种类,当前为买入 entry 与兑现 settle。开放集:新动作种类(如卖出)上线前会先出现在这里——按值分发、未知值跳过(§8.4/§10)

三条口径:

  • 列出来的就是收得到的。 运营没开、或不在你订阅范围内的,一律不出现——两段都可能是 []。名录不解释「为什么没有」,那是运营者的事。
  • 同一 type 可有多档(如大额 ≥$50k 与 ≥$500k 各占一行)。bus[] 按最低启用档入账,按档过滤请自行筛 payload 数值(§7)。
  • 不含中文展示名,也不含 id。 展示名会被运营改动、id 是部署本地行号(§8.3),两者都不该进你的代码。

鉴权、401/403 与 /api/signals 完全一致,所以本端点也可以当「我的 key还有效吗」的探针。内部异常返 503——与 §11 刻意相反:feed 空了只是「今天没信号」,名录空了却等于「你的 key 被削了范围」,那个谎撒不起。


5. GET/api/signals —— 请求参数

主 feed。响应结构见 §6–§9。

参数类型取值默认说明
windowHoursnumber6 / 12 / 24 / 4824非法值静默回落默认值

作用于 active[] 与 bus[];不影响 strategies.active(固定 48h)、settled(固定 3 天)、record30d(固定 30 天)。


6. 核心概念:信号(事件) vs 视图

判据:触发后发出的才是信号。 事件不可变、逐条、有稳定 id——推送与计数都以它为准。视图是事件的折叠/汇总,回答「现在该看什么」,只用于渲染。

信号(事件)视图
性质不可变,逐条,有稳定 id折叠快照,随事件更新
用途触发、推送、计数渲染、看战绩
管线TG / webhook / API无(仅拉取展示)

响应顶层字段归类:

字段归类内容详见
bus[]信号 · ① 原始事件大额成交 / 聪明钱共识 / 钱包发现§7
strategies信号 · ② 策略事件(events)+ 视图(active/settled/recordByStrategy)25 档买入/兑现动作流§8
active[]视图① 大额/共识事件按市场×方向折叠§9.1
settled[]视图已结算折叠条目(与 active 同构)§9.2
record30d视图① 已结算事件的 30 天战绩汇总§9.3
updatedAt 等元信息见 §6.1—

推荐消费模式:事件做触发,视图做渲染。 webhook 收到事件后,拿conditionId + outcome 去视图取当前折叠状态展示。不挂 webhook 的轮询方,策略侧请以 strategies.events[](§8.4)为触发流——它与 webhook 推送体同构、同幂等键,买入与兑现都在里面。三条纪律:

  • 幂等去重键是 (id, event),event 值域 entry / settle / bus;
  • 同一共识组每次升级(2 人 → 3 人)是一条新事件——统计共识个数请按(conditionId, outcome) 归并,或直接用视图(已折叠);
  • 不要跨形态相加:同一笔市场行为会同时出现在事件与视图里。

6.1 元信息与通用格式

JSON{
  "updatedAt": 1755412800, // 数据基准时刻(延迟层已时移)
  "windowHours": 24,
  "heavyMinUsd": 50000, // 视图 heavy 门槛(常量)
  "delayedMin": 0, // 0 = realtime;30 = 延迟 30 分钟
  "healthy": true, // 引擎健康位(永远按真实时间)
  "staleLoops": [], // 停跳循环名;healthy=false 时非空
}
约定说明
时间戳unix 秒(UTC),不是毫秒
价格0–1 小数 = 隐含概率 = 每份合约 USDC 价
金额USD 数值
null不适用或未知;字段本身恒在(含 §11 失败响应)
IDconditionId = 0x… 市场 ID;asset = CLOB token id(十进制串)
数组/排序空为 [];各列表新在前

三个时刻,务必分清:

字段含义
formationTs信号在客观世界成立的时刻
emittedAt我们检测到并发布的时刻(存证锚点)
updatedAt本次响应的数据基准时刻

emittedAt − formationTs = 检测延迟,公开它让你自己判断来不来得及跟。


7. 信号 · ① 原始事件(bus[])

全站原始事件的不可变台账。窗口 = windowHours,按 emittedAt 倒序,最多 200 条。

TypeScriptinterface BusSignal {
  id: number; // 幂等键的一半(配合 event="bus")
  sourceType: "large" | "consensus" | "discovery";
  dedupKey: string;

  // ——— 市场身份(discovery 无市场,一律 null)———
  conditionId: string | null;
  title: string | null;
  slug: string | null; // 单市场页;eventSlug 只能落到事件页
  eventSlug: string | null;
  category: string | null; // 如 "Sports"
  subcategory: string | null; // 如 "NBA";无/未知 = null

  // ——— 方向 ———
  outcome: string | null; // 如 "Yes"
  outcomeIndex: number | null;
  asset: string | null; // CLOB token id

  // ——— 金额(跨类型同名同义)———
  netUsd: number | null; // large=名义额,consensus=总净买
  avgPrice: number | null; // 成本基准:large=成交价,consensus=组级 USD 加权均价
  walletCount: number | null; // large 恒 1(一笔成交=一个钱包)
  // ——— 谁买的(与 walletCount 同源,数字与列表不打架)———
  wallets: { wallet: string; netUsd: number; avgPrice: number }[] | null;

  payload: Record<string, unknown>; // 原始载荷,形状随 sourceType,见下
  emittedAt: number;
}

这些顶层字段与 active[] 的 Signal(§9)同名同义 —— 同一套解析器可以同时吃 bus[] 和 active[],不必先 switch (sourceType) 再决定读哪个键。

wallets 的三种取值,按类型:

sourceTypewallets
large单元素——一笔成交就是一个钱包,金额/价即该笔的名义额与成交价
consensus全量参与钱包,按净买降序(顺序即信息,勿重排)
discoverynull——没有仓位,地址在 payload.address
null 而非 []:空数组会把「不知道」谎报成「零个钱包」。2026-08-21 之前入账的 consensus 事件载荷里没有这份明细,同样为 null——与outcomeIndex/asset 一样,一天之后全量数据都齐。

payload 保留原始载荷,字段一个没少(additive,既有消费方零改动):

sourceType事件含义payload 字段(中文名)
large单笔大额成交(含白名单与非白名单)usd 名义额 · side 买卖向("BUY"|"SELL"|null)· outcome 方向 · outcomeIndex · asset · price 成交价 · wallet 钱包 · slug/eventSlug
consensus≥N 个白名单钱包同向共识outcome 方向 · outcomeIndex · asset · walletCount 钱包数 · totalNetUsd 总净买 · avgBuyPrice 组级加权均价 · wallets 钱包明细(wallet/netUsd/avgBuyPrice)· slug/eventSlug
discovery新钱包通过准入进白名单池address 地址 · score 评分(0-100) · source 发现渠道
payload.usd(large)与 payload.totalNetUsd(consensus)是同一语义的两个历史名字,顶层的 netUsd 已统一;payload.price(large)与payload.avgBuyPrice(consensus)同理,顶层 avgPrice 已统一;payload.wallets[].avgBuyPrice 同理,顶层 wallets[].avgPrice 才是归一后的名字。新接入请读顶层字段。outcomeIndex/asset 自 2026-08-19 起、wallets 与 avgBuyPrice 自2026-08-21 起写入载荷,此前入账的事件为 null;bus[] 窗口最长 48h,一天之后全量数据都齐。
consensus 的 avgPrice 请勿自行从 wallets[] 重算。 源侧给的是按份额USD 加权的组级成本(totalNetUsd / Σ(netUsd / avgBuyPrice)),不是各钱包均价的算术平均——两者能差近 1¢,而追高闸门的红线只有 10¢。顶层 avgPrice与 active[] 的同名字段读的是同一个源字段,口径保证一致。

要点:

  • 阈值分档:运营者可为同一类型配多档定义(如「大额 ≥$50k / 巨额≥$500k」)。bus[] 按最低启用档入账;按档过滤请自行筛 payload 数值,或让运营者把你的 webhook 配成只订某一档(§10)。
  • large 不等于聪明钱:它是流水,不带判断;白名单身份不在 payload 里。
  • discovery.source 取值:leaderboard 全球榜 / category:<分类> 分类榜 / discovered:echo|splitter|insider|early_winner 四条发现渠道。

8. 信号 · ② 策略事件(strategies)

25 档纸面策略的买入/兑现动作。需 key 范围含 strategy。

只轮询的消费方请以 events[] 为事件源(§8.4):买入(entry)与兑现(settle)在里面是一等对称动作,逐条与 webhook 推送体同构。active[]/ settled[] 是折叠视图——active[] 的行在结算后消失(它回答「现在还能行动什么」,不是「发生过什么」),settled[] 只保留近 3 天、最多 20 条。拿视图当事件流,就会得出「只有买入、没有兑现」的错觉。

TypeScriptinterface StrategyFeed {
  active: StrategyFeedSignal[]; // 视图:近 48h 触发、未结算(结算后行消失)
  settled: StrategyFeedSettled[]; // 视图:近 3 天结算,最多 20 条
  events: SignalEventV1[]; // 事件:买入+兑现动作流,webhook 载荷的拉取镜像(§8.4)
  recordByStrategy: Record<
    string, // 键 = strategy id 字符串(部署本地,别写死;认档用 code,见 §8.3)
    {
      code: string | null; // 跨部署稳定的档位码 ← 认档用它
      name: string;
      source: string;
      record: SignalRecord; // 视图:30d 汇总
    }
  >;
}

8.1 active[] 字段

字段类型中文名说明
idnumber事件 IDwebhook 去重键的一半
strategyobject档位{id, code, name, source};认档用 code
conditionIdstring市场 ID—
titlestring市场问题英文原文
slugstring市场短名拼单市场页链接
eventSlugstring事件短名拼事件页链接(一个事件下可挂几十个市场)
categorystring | null一级分类如 Sports
subcategorystring | null二级分类如 NBA;无为 null
outcomestring买入方向反向档已是翻转后的方向
outcomeIndexnumber | null方向序号—
assetstring | null代币 ID用它订实时价(§12)
formationTsnumber形成时刻语义随 source,见 §8.3
referencePricenumber | null聪明钱成本—
walletCountnumber | null钱包数heavy 恒 1
totalNetUsdnumber | null总净买USD
entryPricenumber | null纸面进场价我们的模拟买入价
sizeUsdnumber | null纸面额默认 500
emittedAtnumber发布时刻减 formationTs = 检测延迟

entryPrice − referencePrice = 追价成本(实测红线 10¢)。

8.2 settled[] 字段

字段类型中文名说明
idnumber事件 ID与 active 同一台账
strategyIdnumber档位 ID部署本地,见 §8.3
strategyCodestring | null档位码跨部署稳定 ← 认档用它
strategyNamestring档位名中文展示名
conditionIdstring市场 ID—
titlestring市场问题—
outcomestring买入方向—
entryPricenumber | null进场价纸面
exitPricenumber | null退出价结算价
wonboolean | null是否盈利null = 平局,不进胜率分母
realizedPnlnumber | null已实现盈亏USD,纸面
settledAtnumber结算时刻—

纸面口径:以上是模拟跟单数字(真实数据 · 模拟策略),展示必须携带「研究用途模拟信号 · 非投资建议 · 只读非托管」。

8.3 检测器族与 25 档

strategy.source = 档位所属检测器族:

source中文名在检测什么formationTs 语义
consensus多钱包共识N 个白名单钱包净买同一结果第 N 人到位时刻
heavy单笔巨额单个白名单钱包单笔 BUY 达标那一笔成交时刻
lopsided一边倒分歧有分歧但一边明显占优倾斜跨线时刻
resolved分歧解除少数边开始净卖(认输)识别到认输那一轮
lone_wolf高分独狼高评分钱包净买达标净买跨线时刻
early_winner早期赢家早期赢家渠道钱包净买达标净买跨线时刻
confluence信号共振≥2 个检测族、≥2 个不同钱包先后看多同一结果条件补齐时刻的下界(第 N 个族到场)
cohort_follow同批新钱包≥3 个同批出生的新钱包同向净买达标组内最后一笔成交时刻
code(认档用它)档名source反向
conservative_consensus保守consensus—
aggressive_consensus激进consensus—
elite_consensus精英共识consensus—
heavy_consensus重仓共识consensus—
first_mover_consensus首发共识consensus—
whale_follow巨鲸heavy—
mega_whale超级巨鲸heavy—
elite_whale巨鲸精英heavy—
lopsided_majority一边倒分歧lopsided—
standoff_resolved分歧解除resolved—
high_score_lone_wolf高分独狼lone_wolf—
early_winner_follow早期赢家跟投early_winner—
contrarian_minority逆势少数边lopsided对照
inverse_whale_follow反巨鲸heavy✓
inverse_mega_whale反超级巨鲸heavy✓
inverse_elite_whale反巨鲸精英heavy✓
inverse_standoff_resolved反分歧解除resolved✓
inverse_high_score_lone_wolf反高分独狼lone_wolf✓
inverse_early_winner_follow反早期赢家early_winner✓
relative_impact相对冲击heavy—
signal_confluence信号共振confluence—
fresh_wallet_cohort同批新钱包cohort_follow—
whale_follow_pregame赛前巨鲸heavy—
whale_follow_inplay滚球巨鲸heavy—
skill_consensus技能共识consensus—

各档的具体触发阈值不对外公开(2026-09-14 起)。source 说明这一档属于哪个检测器族、reverse 说明它是不是对照档。这些足够你把信号归类、把正向档与它的对照档配对读;逐档门槛的确切数值不在对外契约内,也不会出现在任何响应里。2026-09-23 新增的赛前 / 滚球巨鲸与技能共识各有一个身份字段——phase(pregame开赛前 / inplay 滚球,按开赛时间切分)与 pool(skill = 只数技能池钱包)——但 phase / pool 只出现在站内 /api/follow 的 params 里(不在本契约内);契约消费方请用 code(whale_follow_pregame / whale_follow_inplay / skill_consensus)识别这几档。

反向 = ✓ 的档与被它镜像的那档共用同一套检测参数,只是信号触发时买相反一边——所以两者必须成对读:正向档显著而对照档不显著,结论才立得住。逆势少数边(标「对照」)是 一边倒分歧 的对照组,跟的是同一批市场的少数边。

各档持仓会重叠,战绩不可跨档相加。当前对外放开的档见 §4 实时状态表。

⚠️ 认档请用 code,别用 id

三个字段能标识一档,但只有 code 该被写进你的代码:

字段跨部署稳定可硬编码说明
code✅✅ 就用它ASCII、每档唯一、冻结(发布后永不改名)
name✅⚠️ 可但不建议中文展示名;运营改一次文案你就断了
id❌❌ 绝对不要数据库自增行号,换个部署就是另一档
source✅❌ 不唯一检测器族,一族挂多档(如 heavy)

id 为什么不能用:它是自增行号,取决于这个库在哪个种子版本上建起来的。全新安装的库里「超级巨鲸」是 7;而从早期版本一路升级上来的库里,同一档可能是 9,7 反倒是「首发共识」。硬编码 strategyId === 7 不会报错,只会静默地把另一档的信号当成你要的那档——这两档分属不同检测器族(heavy 与 consensus),触发条件毫不相干。

id 唯一的正当用途是同一次响应内的分组键(recordByStrategy 的键就是它);出了这次响应就别留着它。

code 可能为 null——那是运营手工建的、尚未登记档位码的档。此时只能退回name 认,或干脆跳过:上表 25 档都有 code。

下面这张表是本部署此刻的 id ↔ code ↔ 档名对照,/api-docs 页面按当前库实时生成(源文件里为空)。排查「我收到的 id 是哪一档」时看它:

strategies[].id ↔ 档名· 打开本页时查库生成
本部署 idcode · 认档用它档名
1conservative_consensus保守
2aggressive_consensus激进
6heavy_consensus重仓共识
7first_mover_consensus首发共识
8whale_follow巨鲸
9mega_whale超级巨鲸
10elite_whale巨鲸精英
11lopsided_majority一边倒分歧
14early_winner_follow早期赢家跟投
66relative_impact相对冲击
69whale_follow_pregame赛前巨鲸
70whale_follow_inplay滚球巨鲸
⚠️ 只列已对外发布的档;左列 id 只对本部署有效,硬编码请用 code(为什么见 §8.3)。

8.4 events[] — 动作流(webhook 载荷的拉取镜像,2026-08-31 起)

量化信号流的铁律是「告诉订阅方我们刚做了什么」。买入与兑现在这里是一等对称动作:策略开仓出一条 event: "entry",市场结算平仓出一条event: "settle"——同一台账行结算后就是两条事件,与 /follow 策略详情「操作历史」的时间线同一语义。

逐条结构 = §10「事件体」的 SignalEventV1,一字不差(服务端就是同一个构造函数产出)。webhook 与轮询因此可以共用同一套解析器与同一个幂等去重键(id, event);从轮询迁到 webhook(或反之)零改动。

口径规则
窗口entry 按 emittedAt、settle 按 settle.settledTs,各取近 48h;无条数上限(窗口即上限,不做静默截断)
排序按事件自身时刻倒序;同刻 settle 排 entry 前(同刻时兑现在时间线上更「新」);再按 id 倒序
延迟层delayed tier 整段时移:结算晚于时移后基准的兑现「尚未发生」,不出现(该信号在 active[] 里也仍是行动项——两个口径互洽)
档位与 active[] 相同,只含运营已放开推送(push_enabled)的档
与推送异webhook 的 settle 只跟着「该通道发过的 entry」走;events[] 照的是台账,不看投递状态——新订阅方第一次轮询就能拿到完整近 48h
前向兼容event 值域是开放集(将来可能新增动作种类,如卖出):未知值跳过别报错,新值上线前先出现在名录 events[]——纪律全文见 §10

消费建议:用 (id, event) 做幂等去重后,entry 当开仓信号、settle 当平仓/对账信号。老买入(超 48h)的新结算只出 settle 一条,entry 上下文(进场价等)已内嵌在事件体的 paper 块里,不需要回查。


9. 视图(拉取展示,非信号)

对 ① 大额/共识事件的折叠与汇总。规则固定(非配置),数据请求时现算。

9.1 active[] — 进行中(折叠视图)

按市场×方向折叠:同一仓位多笔成交只出一条;共识升级原地更新金额但formationTs 保持最初形成时刻;两侧都有聪明钱时合并为一条 split。

三种 kind:

kind判据(固定)读法
consensus≥2 白名单钱包净买同一结果最强方向
split两个对立结果上都有白名单钱包警告,无方向(outcome 恒 null)
heavy单个白名单钱包单笔 ≥heavyMinUsd单人观点;已有共识时被抑制
字段类型中文名说明
keystring去重键<conditionId>|<outcome>;split 为 conditionId
kindSignalKind种类见上表
conditionIdstring市场 ID—
titlestring市场问题—
slugstring市场短名缺失时空串
eventSlugstring事件短名别拿它当市场链接
categorystring | null一级分类—
subcategorystring | null二级分类—
formationTsnumber形成时刻判断新鲜度用它
outcomestring | null方向split 恒 null
outcomeIndexnumber | null方向序号—
assetstring | null代币 ID—
walletCountnumber钱包数split 为两侧之和
netUsdnumber净买入split 为两侧之和
avgPricenumber成本基准split 恒 0
wallets{wallet,netUsd,avgPrice}[]钱包明细按净买降序
sides数组(仅 split)双边明细每侧同 wallets 外加 outcome/asset

9.2 settled[] — 已结算(认账视图)

近 3 天,同一市场×方向取最新一条,最多 20 条。与 active[] 同构:身份/仓位字段同名同义(可用 key 对上号、复用同一卡片组件),差别只在末尾三项:

字段类型中文名说明
entryPricenumber进场价等同 active 的 avgPrice
wonboolean是否命中—
settledAtnumber结算时刻—

同一条信号可同时出现在 active[] 与 settled[](窗口口径不同)——同key 是同一笔仓位的两个阶段。

9.3 record30d — 30 天战绩(汇总视图)

⚠️ 五个字段全是「条数」量纲,不是百分比。

字段中文名说明
settled已判定条数分母
wins命中条数分子
implied市场预期命中条数Σ 各信号赢面的隐含概率
excess超额条数wins − implied
sd噪音标准差√Σ p(1−p),判断 excess 是否显著的唯一尺子

示例:{"settled":1799,"wins":1066,"implied":1051.3,"excess":14.7,"sd":19.3} ——市场预期中 1051.3 条,实际多中 14.7 条,噪音 σ=19.3,在运气范围内。

展示要求:命中数旁必印 implied;|excess| < 2×sd 必须写「仍在运气范围内」;settled < 5 标「样本不足」;禁止单日胜率/连对天数类表述。

recordByStrategy(§8)量纲与展示要求同此。两份战绩口径不同(①动向 vs ②各档纸面),不可比也不可加。


10. POST<你的端点> —— Webhook 推送(realtime tier 专属)

由运营者代为登记你的端点:接收 URL + ≥16 字符 HMAC secret + 勾选的推送类型。规则:

  • 不勾 = 仅 ② 策略事件(历史默认);① 各类型须显式勾选;
  • ① 类型可按定义细分订阅(如只订「巨额 ≥$500k」档),事件体不变、只是子集;
  • 勾选须在 key 订阅范围内,越界登记当场被拒;
  • ① 类型的全局开关(§4)仍是产出前提。

请求头

HTTPPOST <你的 URL>
content-type: application/json
x-signature: sha256=<hex hmac-sha256(secret, 原始 body)>
x-signal-id: <事件 id>
x-signal-event: entry | settle | bus

事件体

② 策略事件 SignalEventV1(event: "entry" | "settle"):

TypeScriptinterface SignalEventV1 {
  v: 1;
  id: number; // strategy_signals.id
  event: "entry" | "settle";
  emittedAt: number;
  // 按 code 分派;id 是部署本地行号、name 是中文展示名(§8.3)
  strategy: {
    id: number;
    code: string | null;
    name: string;
    source: string;
  };
  market: {
    conditionId: string;
    title: string;
    slug: string;
    eventSlug: string;
    category: string | null;
    subcategory: string | null;
    outcome: string;
    outcomeIndex: number | null;
    asset: string | null;
  };
  signal: {
    formationTs: number;
    referencePrice: number | null;
    walletCount: number | null;
    totalNetUsd: number | null;
  };
  paper: {
    entryPrice: number | null;
    sizeUsd: number | null;
    chaseCents: number | null; // (entry − reference) × 100
    latencySec: number; // emittedAt − formationTs
  };
  record: SignalRecord | null; // 量纲见 §9.3
  settle: {
    settledTs: number;
    exitPrice: number | null;
    won: boolean | null;
    realizedPnl: number | null;
  } | null; // entry 时为 null
  notice: string;
}

⚠️ 前向兼容纪律:event 值域是开放集,未知值必须跳过,不能报错。今天是 entry/settle,将来可能新增动作种类(例如主动卖出 exit——届时会带自己的数据块,模式同 settle 块的「非该事件时为 null」)。三条义务:

  • 未知 event 值:跳过并记日志,别抛错。 尤其 webhook 端:回 4xx =永久拒收(§「投递语义」,按毒消息处理不再重试)——用严格枚举校验未知值再拒收,等于让每一种未来动作对你静默丢失。
  • 别把 event 写成硬闸。 照抄本结构做校验时,event 请当 string 收、已知值分发、未知值走丢弃分支(与下方 strategy.code 的 default 分支同一纪律);不要用 z.enum(["entry","settle"]) 这类封闭枚举直接拒收。
  • 新动作先进名录,再进通道。 上新动作种类前,你名录里该档的events[](§4.1)会先出现新值——想第一时间感知,diff 名录即可,不必盯公告。

⚠️ handler 里请按 strategy.code 分派。 strategy.id 与 §8 的是同一个值,同样是部署本地的自增行号——switch (ev.strategy.id) 写成数字的话,换一个部署、或本部署重建过库,同一个 case 就会静默地接到另一档的信号。

Nodeswitch (ev.strategy.code) {
  case "mega_whale":
    /* 单笔 BUY ≥$150k */ break;
  case "inverse_mega_whale":
    /* 同一笔单,买对面 */ break;
  default: /* 未知档位码 = 我们新加了档;忽略即可,别抛错 */
}

连通性测试事件(§10 的 action:"test")的 id、strategy.id 均为 0,strategy.code 为 null——三个哨兵任取其一都能识别并丢弃。

① 原始事件 BusEventV1(event: "bus"):

TypeScriptinterface BusEventV1 {
  v: 1;
  event: "bus";
  id: number; // bus_signals.id
  bus: BusSignal; // 与 §7 的 bus[] 单条完全同形
  notice: string;
}

「完全同形」不是承诺而是事实:推拉两条路径嵌的是同一份 zod schema(lib/signalBus.ts 的 BusSignalSchema),并有一条从真实数据现算字段集的回归测试钉着 —— 给 bus[] 加字段而漏掉 webhook 这条路,测试会先红。

幂等去重键 (id, event)——两类事件 id 来自不同表,但 event 不同,二元组永不碰撞。

验签(Node.js)

Nodeimport { createHmac, timingSafeEqual } from "node:crypto";

// 必须用原始 body 字符串验签,不能 parse 后再 stringify。
function verify(rawBody, header, secret) {
  const expected =
    "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(header ?? "");
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

投递语义

事项行为
交付保证at-least-once,按 (id, event) 去重
超时5 秒;请先回 2xx 再做重活
成功任意 2xx
4xx永久拒收,该条不再重试
5xx/网络瞬态,30 秒节奏重试
熔断连续失败 10 次停用端点并通知运营者
补发窗口entry 6 小时 / settle 7 天 / bus 1 小时
bus 特有不回灌(只投登记后的事件);每端点每轮最多 10 条
引擎停跳投递整体冻结

11. 失败语义

服务端内部异常时不返回 5xx,而是 200 + 空 feed + healthy: false + error 字段。字段集合与成功响应一致,你不需要为失败分支准备另一套类型。

healthy === false 时必须:顶部提示中断;冻结时间戳(「3 分钟前」改「截至 HH:MM」);隐藏「去交易」等行动入口。


12. 频率、拓扑与现价

  • 每分钟拉 1 次即可(服务端缓存 30s)。
  • 本服务不承接终端 App 直连。正确拓扑:
WhaleWatch ──1 次/分钟──▶ 你的后端(缓存+你的鉴权)──▶ 你的客户端
  • key 放服务端,不下发客户端。
  • 现价不从本接口取:用 asset 直连wss://ws-subscriptions-clob.polymarket.com。本服务只给成本基准(avgPrice / referencePrice);追高闸门 = 实时价 − 成本,红线 10¢。

13. 公开端点(无需 key)

GET/api/record

已公开发布信号的战绩与存证(分母只含发过的信号,与 §8 的全量纸面履历口径不同,不可混用)。

TypeScriptinterface RecordFeed {
  updatedAt: number;
  strategies: {
    id: number; // 部署本地自增行号,别硬编码(§8.3)
    code: string | null; // 跨部署稳定的档位码 ← 认档用它
    name: string;
    source: string;
    pushedCount: number; // 已发布总数(含未结算)
    record: SignalRecord; // 量纲见 §9.3
    realizedPnl: number | null; // 与 record 同一批行的纸面盈亏合计,美元
    //   ↑ 咬 record 的同一个行集合(30d 窗、已发布、已结算、有入场价)——
    //     两个数并排展示,出自不同分母就是两本账。
    //     null = 判不了,不是 0:行集合为空,或其中任一行缺 realized_pnl;
    //     少一行的和是错的和,不是「部分的和」。
    settledRecent: {
      id: number;
      conditionId: string;
      title: string;
      outcome: string;
      entryPrice: number | null;
      exitPrice: number | null;
      won: boolean | null;
      realizedPnl: number | null;
      settledAt: number;
    }[]; // 最多 10 条
  }[];
  digest: { day: string | null; tail: string | null }; // 存证链尾
  digests: {
    // 逐日存证行,最近 30 天,按 day 倒序(2026-09-07 追加,additive)。
    day: string; // 该批信号所属的 UTC 日
    digest: string; // 该日链式 sha256
    prev: string; // 该日复算的起点(= 表里上一条的 digest;首条为 "genesis")
    count: number; // 进入该日摘要的已发布信号条数
    paramsDigest: string | null; // 规则集指纹,见下
    createdAt: number;
  }[];
}

digest 为每 UTC 日的链式 sha256 摘要链尾,可复算验证「先发布后结算、未删改」。限流:每 IP 60 次/分钟,超限 429。

怎么复算(2026-09-07 起真的可以执行了)。 逐条 preimage 是前值|id|档名|市场|方向|发布时刻|入场价(无入场价写字面量 null),按id 升序链式 sha256,起点取该日的 prev。所需的 id 由/api/dataset/record.csv 的 signal_id 列提供——那一列同批追加,此前公开导出里根本没有 id,于是那句「可复算验证」谁也执行不了。

现成两条路:npx tsx scripts/verify-digest.ts <baseUrl>,或 /record 页「自己验一遍存证链」折叠块(浏览器 WebCrypto,服务端零参与——由被验证方跑的验证器证明不了任何事)。

摘要在 06:00 UTC 结算,不是零点:投递有延迟档,23:50 发出的信号可能在次日 00:20 才 sent。过了 entry 补发窗口(6h)昨日成员集结构上冻结,复算才是确定的。因此 count 与导出行数不一致时,方向有意义:导出行数少于count = 有行被删(要报的那个方向);多于 count = 摘要算完后才投递成功(2026-09-07 之前的历史遗留,之后不该再出现)。

paramsDigest 是规则集指纹:全部档位参数 + 事件线阈值档 + 每一个进过config_history 的运营设置,规范化后取 sha256。它刻意不链式——同一套规则每天必须给出同一个值,读者才看得出「这天规则动过」。它能证明的只有「参数在哪一天变过」;参数是什么不公开(阈值是一套知道了就能规避的规则集,与可验证的战绩是两回事),完成核对需运营者出示原值。null = 该日无记录。

GET/api/health

200 = 全部引擎循环正常心跳;503 = 有停跳。适合挂 uptime 监控。返回 {ok, nowSec, loops[], staleLoops[], startedAt}。

GET/api/continuity

数据连续性 · 30 天起算时钟。逐 UTC 日重建监控的覆盖/断档史(60 天窗)并给出当前不间断覆盖段的读数——README 路线图承诺「30 个不间断交易日后重推所有阈值」,这条端点就是那口钟,起算日由数据自己说话(当前连续段的第一天),不靠人工宣布。/status 页的连续性区渲染的就是本响应。

判定材料是共识循环逐轮落库的实测时间戳(每 5 分钟一轮,窗口拉取成功才计)。相邻两轮间隔超过 tolSec(20 分钟,与 /api/health 判停跳同一把尺)记断档;跨午夜的断档两天都不计入;记录起点日若从中途开跑记 partial 不计入。全部判定取保守方向——这口钟宁可少计一天,不能多计一天。

TypeScriptinterface ContinuityReport {
  gateDays: number; // 30
  tolSec: number; // 1200 —— 断档容忍阈值(秒)
  recordStartDay: string | null; // 全表最早记录所在 UTC 日;null = 从未有记录
  days: {
    day: string; // UTC yyyy-mm-dd
    status: "covered" | "gap" | "partial" | "pre" | "pending";
    cycles: number; // 当日落库轮次(满覆盖 ≈ 288)
    maxGapSec: number; // 触碰本日的最长断档;0 = 无
  }[]; // 旧 → 新共 61 条,最后一条恒为今天(pending)
  streakDays: number; // 截至昨天的连续覆盖日数 = 时钟读数
  streakStartDay: string | null; // 起算日(UTC)
  streakClipped: boolean; // 连续段打满 60 天展示窗,真实值可能更长
  todayCoveredSoFar: boolean; // 今天到目前为止无断档
  gateReached: boolean; // streakDays ≥ gateDays
}

GET/api/dataset/record.csv

已发布信号的全量结算台账 CSV(与 /api/record 同一分母:存在 sent entry投递的信号;未结算行也导出,won 留空——分母诚实)。逐行字段:emitted_at_utc, formation_at_utc, strategy_code, strategy_name, condition_id, outcome, title, entry_price, settled, won, exit_price, realized_pnl, settled_at_utc, signal_id。头三行是 # 注释(license / 生成时刻与行数 /完整性说明),pandas 用 pd.read_csv(url, comment="#") 读。

signal_id 于 2026-09-07 追加在末尾(按位置解析的消费方不受影响):存证链以它为 preimage 输入,缺这一列时链无法复算。

许可 CC BY 4.0,署名 whalewatch.wired.fund。防篡改校验走 /api/record 的逐日 sha256 存证链(digests[],复算方法见 §13 那一节)——CSV 是便利导出,不是存证载体。限流:每 IP 6 次/分钟。

GET/embed/record · GET/embed/status

可嵌入 HTML 卡片(iframe 用):/embed/record 是已发布信号记分卡(数据与/record 页同源),/embed/status 是引擎状态 + 连续性时钟徽章。自包含HTML、零脚本、60s 缓存、noindex,固定携带署名回链。?theme=dark 得深色版。复制即用的嵌入代码在 /record 与 /status 页的「嵌入此卡」折叠块里。

HTML<iframe
  src="https://whalewatch.wired.fund/embed/status"
  width="360"
  height="96"
  style="border:0"
  loading="lazy"
  title="WhaleWatch status"
></iframe>

GET/api/pulse

市场脉搏:market_daily 每日聚合(UTC 收盘后重建昨日 24h 窗口)之上的两份读物——异常市场日榜(四个可解释分量:量能异动/单边度/鲸鱼占比/日内价移,加权合成 0–100 分,分量逐项返回)与小单 vs 鲸鱼方向分歧($2k–10k 桶与≥$50k 桶的净买方向背离,双边材料性门槛 $5k/$50k)。latestDay/dayCount自述数据新鲜度与底座厚度;truncated 为真时该日数字是下界。口径细节见/pulse 页脚注。

2026-08-28 起 payload 追加 conviction 键(确信指数,additive,按「忽略未知字段」纪律老消费方零影响):品类×日的激辩度 0–100(高 = 激辩/恐慌,低 = 确信,VIX 语义),categories[] 每项含 key(gamma 一级分类原值,空串 =未分类桶)、score、四分量 components(contest 阵营对峙 / divergence小单鲸鱼对立 / priceMove 价格动荡 / volSurge 量能异动,各 0–1)、volumeUsd/markets、volBaselineDays(<3 = 量能分量用的是横截面分位)与≤30 天的逐日 series。品类日总量 <$10k 不给分;服务端现算失败时该键为null——消费方必须判空。

2026-08-28(同日第二批)再追加三处 additive:top[] 每行新增washRatio(同钱包当日买卖配对量 ×2 ÷ 总量,判定材料上线前的日份为null);顶层新增 ghosts[](无鲸异动:价移 ≥10¢ 且当日单笔最大<$10k 的市场,含 moveCents/maxFillUsd/washRatio,材料未采集的老日份永不进榜)与 washTop[](洗量榜:washRatio ≥ 0.2 且量 ≥$10k,含washUsd 单腿配对量——中性结构描述,非操纵指控)。

GET/api/calibration

市场校准研究:按 10¢ 赔率带对比「市场隐含概率」与「实际发生率」,整体 +一级分类分组(样本 ≥30 才成组)。这不是本站信号的战绩——样本是 alert时点的市场价格观察。置信区间按市场数聚簇(同市场多条 alert 是同一次随机事件的复制品)。选择偏差声明见 /calibration 页——引用本数据请带上它。

MCP Server(AI agent 接入)

仓库自带 MCP(Model Context Protocol)server,把上述端点暴露给 Claude Code / Claude Desktop / 任何 MCP 客户端:

Shellclaude mcp add whalewatch -e WHALEWATCH_API_KEY=<你的key> -- npx -y whalewatch-mcp

公开工具(get_health / get_continuity / get_record)无需 key;信号工具(get_signals / list_signals / get_market_card)读 WHALEWATCH_API_KEY。自托管部署用 WHALEWATCH_BASE_URL 指向自己的基址。工具面与本文端点 1:1,不发明新语义。


14. GET/api/market-card/{cid} —— 按需查询 · 市场深度卡

这是与「信号」并列的另一类端点,不是 /api/signals 的一部分——它没有事件id、不可被推送、也不是任何事件的折叠(§6 的判据)。你点名一个市场,我们现算一份答案。路径里的 {cid} 就是市场的 conditionId(0x…)。

需 key 范围含 market,且 realtime tier 专属。回答的是「用户正要在这个市场下单,此刻盘面长什么样」:谁在买、多强、成本多少、有没有分歧、我们历史上在这里发过什么信号、准不准。

一张延迟 30 分钟的盘面回答不了「我现在该不该进」 ——所以延迟档拿不到它,这是范围问题不是字段阉割(delayed key 的 /api/signals 字段仍一个不少)。

与「信号」类端点的根本差别(先读这一段)

/api/signals本端点
数据来源全部已持久化状态,零上游调用按需打上游(成交窗口)
突发流量永远挤不占引擎预算受全局预算约束,会背压
429不会会,且是正常工作状态
稳定性依赖只依赖我们自己额外依赖 Polymarket 公开 API

响应

TypeScriptinterface MarketCardResponse {
  card: MarketCard; // identity / meta / brief / freshFlow / history / window / pulse
  builtAt: number; // 本卡数据的基准时刻(unix 秒)
  staleSec: number; // 响应时刻 − builtAt
  live: boolean; // true = 新鲜期内;false = 预算耗尽,发的是陈旧窗口重算的卡
  healthy: boolean; // 引擎健康位,与 /api/signals 同义
  notice: string; // 研究用途 · 非投资建议 · 只读非托管
}

card.brief 三段:classification(consensus / disagreement / none,与全站同一套判据与门槛)、smartFlow(按结果分组的聪明钱留存敞口,逐结果给totalExposureUsd / totalNetShares,逐钱包给 exposureUsd / netShares / avgBuyPrice / scoreBand / winRate / isMarketMaker)、accum(拆单建仓组)。另有 freshFlow(≤7 天新钱包的大额买入)与 history(我们在这个市场发过的告警 + 验证结论)。

逐结果的两个合计都在截断之前求和。wallets 每个结果最多给 8 个(按敞口降序),但 totalExposureUsd / totalNetShares 始终是该结果全部聪明钱的和 —— 一边超过 8 个钱包时,合计不会缩水成「你看到的这几行之和」。

card.pulse:市场脉搏视角(2026-08-31 新增)

TypeScriptinterface MarketCardPulse {
  day: string; // 榜单判定覆盖的 UTC 日
  category: string | null; // Polymarket 分类(空串归一为 null)
  subcategory: string | null;
  boards: ("anomaly" | "divergence" | "ghost" | "wash")[]; // 该日上了哪些榜
  anomalyScore: number | null; // 0–100,仅进入当日异常日榜前 10 才有值
}

两类信息,分工是重点:category / subcategory 是 Polymarket 对市场的分类(这是什么市场),boards 是 我们在脉搏日榜上给它的评价(我们发现它怎么了),判定与 /api/pulse(§13)同一套门槛、同一个实现,两处不会给出不同答案。

pulse 为 null = market_daily 里没有这个市场的任何一天(底座还没覆盖到它),不是「它很正常」。boards 为空数组才是「那天它一个榜都没上」。

⚠️ 时间口径与 card 其余字段不同,不要混读。 brief / freshFlow / window 说的是此刻那个成交窗口;pulse 说的是 pulse.day 那个已收盘的完整 UTC 日。今天盘中刚异动起来的市场,今天不会有 boards —— 这是设计,不是延迟。
anomalyScore 只覆盖当日异常日榜前 10(榜在数据层就封了 10 条)。排到第11 的市场拿到的是 null,含义是「我们没算到那么远」而不是「它不异常」。本段全部来自本地 market_daily,不打上游 —— 它不会让这个端点更容易吃到429。

⚠️ brief.settled:市场结算后敞口一律为 0

brief.settled 为 true 时,smartFlow 里所有 exposureUsd / totalExposureUsd 都是 0,这不是「没人押」而是「已经没有人还持有」。

这是一次可观察到的行为变更,我们主动讲明。 本文开头承诺「字段只增不改」,settled / totalNetShares 是纯新增没有问题,但 exposureUsd 在已结算市场上的取值确实变了(原先给的是结算前的水位)。我们把它算作修正而非重定义:这个字段的定义一直是「留存敞口」,而旧行为并不满足这个定义——它在一个持仓为零的钱包上报出过 $1,829,963。若你的系统把已结算市场的 exposureUsd 落库或用于归因,请按下表重新对齐。

原因是上游的一个硬事实:Polymarket 的赎回是 /activity 上的独立REDEEM 事件,永远不出现在成交流水里。所以「还持有多少」这个量,在市场结算之后无法从买卖推算——净股数会永久冻结在结算前的水位。我们实测过它的代价:一个钱包买入 3,339,219 股、卖出 139,219 股,随后一次性赎回 3,200,000 股,Polymarket 自己的持仓接口返回空,而按买卖推算出来的敞口是 $1,829,963。输的一边同理虚高(结算价 0,成本价照记)。

与其发一个我们知道是假的数字,不如归零并把 settled 明确告诉你。判据是closed 且不在 UMA 争议中(争议期赎回被卡住,仓位可能真在,故不归零)。

结算后仍然可用的字段:netShares / totalNetShares / avgBuyPrice 照旧,它们是窗口内的成交事实,结算改变不了;classification 的金额也照旧,它表达的是「窗口内投入了多少」而非「现在还押着多少」。一句话分界:

口径含义结算后
exposureUsd(敞口)现在还持有归零
netShares / 分类金额窗口内投入照旧

为什么给的是 scoreBand 而不是原始分

scoreBand 取值 "high" / "mid" / "low" / null(未知)。

原始评分是我们内部模型的输出,会随模型迭代漂移。把一个连续值写进对外契约,等于承诺它的语义永不变——那样我们每调一次模型,对你就是一次无声的破坏性变更。分档是稳定语义:模型怎么调,都不改变「这个钱包算强」这件事。分档边界的改动才算破坏性变更,而那是件明确、罕见、会公告的事。

winRate 照给原值:它是实测统计(逐仓盈亏聚合,已处理持有到归零的幸存者偏差),不是模型输出,没有随版本漂移的问题。

状态判据

情形返回
数据在新鲜期内200,live: true,staleSec 很小
预算耗尽,但缓存窗口在闸内200,live: false + staleSec
预算耗尽且无缓存 / 超陈旧闸429 + Retry-After
引擎停跳预算归零 → 多为上面两行,healthy: false

429 是背压,不是故障。 它意味着「此刻不能诚实地回答你」,请按Retry-After 退避后重试——立刻重试只会把背压变成雪崩。429 的响应体不含card,所以不会被误读成「这个市场没有信号」。

超过陈旧闸我们宁可拒绝,也不发旧卡。 卡片说「3 个聪明钱刚买了 YES」,若其中2 个在这几分钟里已经卖了,那张卡不是「不够新」,是错的,而且错在会让人亏钱的方向上。

⚠️ 这条端点的依赖风险,必须知道

/api/signals 只读我们自己的库;本端点按需向 Polymarket 的公开 API 取数,而那些接口无版本、会静默变更。实测有过:/activity 的 limit 上限从 1000悄悄降到 500,没有公告,直接把依赖它的页面全打挂。

所以:本端点的可用性含有一段我们控制不了的部分。把它接进你的关键路径前,请准备好降级显示(例如仅用 /api/signals 的信号做提示),不要让一张卡片拿不到就阻断用户的操作。


15. 常见问题

bus 一直是空数组? 看 §4 实时状态表——类型未开启就是预期行为,不是故障。

active 是空的? 先看 healthy;为 true 且 updatedAt 新鲜就是窗口内没有达标信号,放大 windowHours 再看。

implied 是 1051.3,是百分比吗? 不是,record 五件套全是条数(§9.3)。

recordByStrategy 用档名取不到? 键是 strategy id 字符串。想按档位索引就遍历一遍自己转成 code → record(值里有 .code)——别把某个具体 id数字写进代码,它是部署本地的(§8.3)。

想用 webhook 收聪明钱动向? 订 ① 的 consensus/large 类型—— active[] 是折叠视图,没有稳定逐事件 id,不作为推送对象。

能拿历史数据吗? 只有滚动窗口与 30 天汇总;长期记录见公开页/record。

key 何时失效? 仅运营者吊销时,立即 401,挂其上的 webhook 同时失效。


16. 变更记录

  • 2026-09-23 策略中心第五批(19 → 25 档):新增 6 档(relative_impact / signal_confluence / fresh_wallet_cohort / whale_follow_pregame / whale_follow_inplay / skill_consensus),全部推送关、目前未在公开页展示。本契约内可见的变化只有一处,为 additive:strategy.source 新增两个取值confluence、cohort_follow——按枚举写死 source 的客户端需要放行。phase / pool 两个身份字段只出现在站内 /api/follow 的 params 里(不在本契约内);契约消费方请用 code 识别这几档。原有档位参数一个字节未改;但参数指纹(/api/record 的 paramsDigest)是对全部档的全局指纹,部署当天会与前一天不同——这表示「这一天新增了档」,不是老档被改了。
  • 2026-09-17 档位到期与权益配置表上线(§17):paid/dev 现在可以带到期时刻,到期后系统自动降为 free;自助 key(/api/me/keys)与 webhook(/api/me/webhooks)的「本档是否提供」与数量上限,从硬编码常量改为运营者在 /manage 维护的档位权益表,出厂值与旧常量逐项相等(免费 0、付费 5、开发者 5)。唯一不是零行为变化的例外:此前 webhook 登记上限是不分档位的一刀切 5 个,现在免费档出厂上限为 0——任何「曾是付费/开发者、自助领过 key,后被降档(人工降级或到期)但 key 未吊销」的订阅方,从今天起无法再登记新端点;已登记的端点不受影响,照常投递。/api/follow(站内网页接口,不在本契约内)同批开始按登录身份裁剪仓位历史深度,与本文档覆盖的 API key 端点无关。
  • 2026-09-15 新增 /api/me/webhooks(§17):webhook 端点自助登记,此前必须由管理员代配。同时收紧自助 key 的默认范围:此前不传 bus_types 落库成 NULL(= 全部),把唯一会打上游的 market 也默认发了出去;现在自助签发只授予可推送的事件类型,market 须由管理员显式发放。已签发的存量 key 范围不变。
  • 2026-09-15 新增 /api/auth/telegram(§17):Telegram Login Widget 回调,网页端一次点击即可登录,不必切到 Telegram。bot 的 /login 命令保留为兜底(Widget 被扩展拦截、域名未绑、或用户本就在 Telegram 里时)。两条路径共用同一落点,同一个 Telegram 账号不会分裂成两个用户。
  • 2026-09-15 新增 self 端点 /api/me/subscription、/api/me/keys(§17)。订阅者用户体系批次 3:自助订阅与 API key 领取。存量 key 的 tier、范围与行为均未变,只是在库里多认了一个归属(迁到同名的开发者账号名下)。
  • 2026-09-15 新增账号端点 /api/auth/login、/api/auth/logout(§17)。订阅者用户体系批次 1:Telegram 登录。二者都不返回信号数据,现有 API key 鉴权与所有既有端点行为不变。
日期变更
2026-09-14§8.3 的档位表删去「触发条件」列:各档的具体触发阈值(钱包数 / 每钱包金额 / 单笔门槛 / 主导边占比 / $ per signal / 偏离护栏 / 退出规则)不再对外公开。同批把 source 表里「主导边占比 ≥70%」改为不含数值的描述。对外字段与端点零变更——本文件此前也从未在任何响应里下发这些阈值(/api/signals、/api/record、/api/signals/list 只从 params_json 取 source,每日参数指纹 paramsDigest 只发 sha256、不发原文),所以这一条只影响文档可读到的内容,不影响任何消费方的解析。归类与配对读法照旧靠 source + reverse,两者都保留
2026-09-07GET /api/record 追加 digests[](§13):逐日存证行(day/digest/prev/count/paramsDigest),最近 30 天。additive、零上游。配合同批给 /api/dataset/record.csv 末尾追加的 signal_id 列,「按 id 升序复算 sha256 即可验证」这句承诺第一次可被执行——此前公开导出里没有 id,历史摘要也只存在于 TG 频道消息里。同批把摘要结算时刻从 UTC 零点推迟到 06:00(过 entry 补发窗口,昨日成员集结构上冻结,复算才确定)。老消费方零影响
2026-09-07GET /api/dataset/record.csv 末尾追加 signal_id 列(§13):additive,列序不变,按列名解析的消费方零影响
2026-09-04GET /api/record 的 strategies[] 追加 realizedPnl(§13):该档 30d 窗内已发布且已结算信号的纸面盈亏合计(美元)。additive、零上游(与 record 同一条 SQL、同一批行取出)。⚠️ 口径咬死 record:同样只含有入场价的行(无价=无基准=不可评级,两边账都不进),所以它与 record.wins/settled 永远同分母。null = 判不了不是 0——行集合为空,或其中任一行缺 realized_pnl;少一行的和是错的和,不是部分的和。老消费方零影响
2026-09-04文档编排:§5 / §10 / §14 的小节标题改为带端点写法(GET /api/signals / POST <你的端点> / GET /api/market-card/{cid})——/api-docs 左栏「端点」索引只认标题里的端点写法,改后它自动收全主 feed、webhook 与市场深度卡,清单仍只有本文件这一份。端点总览表同步补上 GET /api/pulse(300s)与 GET /api/calibration(600s)两行。端点、字段、语义零变更。
2026-08-31GET /api/signals 的 strategies 追加 events[](§8.4):买入+兑现动作流,逐条 = §10 SignalEventV1(服务端同一构造器产出——拉/推同构、同 (id,event) 幂等键),entry 按 emittedAt、settle 按 settledTs 各 48h 窗、无静默截断。此前只轮询的消费方拿不到与买入对等的兑现动作(active[] 结算后行消失、settled[] 是 3d/20 条战绩视图)。GET /api/signals/list 的 strategy[] 同步追加 events: ["entry","settle"] 声明事件种类。并立前向兼容纪律(§10):event 值域为开放集(将来可能加卖出等动作),消费方须跳过未知值、不得用封闭枚举拒收,新值上线前先进名录。全部 additive,老消费方零影响
2026-08-31GET /api/market-card/{cid} 的 card 追加 pulse 键(§14):该市场的分类与市场脉搏日榜身份(boards[] = 异常/分歧/无鲸/洗量,anomalyScore)——additive、零上游(纯 market_daily 本地读),底座未覆盖该市场时为 null。⚠️ 时间口径与 card 其余字段不同:其余是此刻的成交窗口,pulse 是 pulse.day 那个已收盘 UTC 日的判定
2026-08-28GET /api/pulse 再追加 ghosts[](无鲸异动)、washTop[](洗量榜)与 top[].washRatio(§13):全部 additive,判定材料(wash_usd/max_fill_usd)自当日起采集,老日份为 null/不进榜
2026-08-28GET /api/pulse payload 追加 conviction 键(§13):品类×日确信指数(激辩度 0–100,四分量逐项返回,≤30 天逐日序列)——additive 字段,老消费方按「忽略未知字段」零影响;现算失败降级 null
2026-08-27新增公开端点 GET /api/pulse 与 GET /api/calibration(§13):市场脉搏(异常日榜 + 小单vs鲸鱼分歧,market_daily 每日聚合)与市场校准(赔率带隐含 vs 实际,聚簇 CI)——零上游
2026-08-27新增公开数据集 GET /api/dataset/record.csv(§13):已发布信号全量台账,CC BY 4.0,未结算行含(won 空)——分母与 /api/record 同一口径
2026-08-27新增可嵌入卡片 GET /embed/record /embed/status(§13):自包含 HTML、零脚本、60s 缓存、noindex、带署名回链,?theme=dark 深色版
2026-08-27新增 MCP Server(§13):mcp/server.ts 把全部端点 1:1 暴露给 MCP 客户端;公开工具免 key,信号工具读 WHALEWATCH_API_KEY
2026-08-27新增公开端点 /api/continuity(§13):数据连续性 · 30 天起算时钟——逐 UTC 日的覆盖/断档条带与当前连续覆盖读数,判定与 /api/health 停跳阈值同一把尺,零上游,起算日由数据自己说话
2026-08-27新增 /api/signals/list 信号名录(§4.1):按 ①原始/②策略两大类列出这把 key 实际收得到的信号,全 ASCII(type+threshold / code+source),内部异常返 503 而非空名录
2026-08-21② strategy 增 code:跨部署稳定的 ASCII 档位码(如 mega_whale),认档改用它。id 是部署本地行号、name 是中文名,都别硬编码
2026-08-21② strategy.id 澄清为部署本地自增行号、跨部署会变:§8.3 删掉会被误读成 id 的行序号列并增实时对照表,§8.2/§10/§13/§15 同步补认档口径
2026-08-21新增 /api/market-card/{cid} 市场深度卡——本文首个会打上游的端点,自成「按需查询」一类(realtime + market 范围),含 429 背压语义
2026-08-21① bus[] 增 wallets 钱包明细(与 active[] 同名同义;discovery 恒 null)——webhook 同步生效
2026-08-19文档重写为使用者参考版(理由与修订史移至内部契约)。信号=事件(①原始/②策略)与视图分立为本文骨架
2026-08-19① 支持多档信号定义(同类型不同阈值),webhook 可按档订阅;settled[] 补齐与 active[] 同构字段;active[] 增 slug
2026-08-18webhook 支持 ① 类型;失败响应字段集合与成功一致;record 量纲修正(条数);开放状态改实时生成
2026-08-13strategies 段、delayed tier、多租户 key、webhook、/api/record 与存证链、bus[] 与订阅范围

17. 账号端点(Telegram 登录)

订阅者用 Telegram 登录本站网页,与 API key 是两套独立凭证:API key 用于程序化拉取信号(§2),会话 cookie 只用于网页。持有其中一个不会自动获得另一个。

登录码由 Telegram bot 签发,只投递到本人私聊;网页侧不签发任何码。这个方向是安全设计而非实现细节:反过来做(网页签发、任意点击者认领)存在会话固定攻击 ——攻击者签发码并诱导受害者点击,即可换出受害者的会话。

GET/api/auth/login?c=<code>

消费一次性登录码。成功 → 302 到 /me 并下发 HttpOnly 会话 cookie;失败 → 302 回 /login?e=<reason>,reason 四种:

reason含义用户该做什么
missing没带 c 参数回 Telegram 发送 /login
unknown码不存在同上
expired超过 10 分钟有效期同上
consumed码已被使用过(一次性)同上

码的有效期 10 分钟、一次性;同一 Telegram 账号再次 /login 会使旧码立即失效。

POST/api/auth/logout

吊销当前会话并清 cookie,返回 {"ok":true}。未携带 cookie 也返回 200(登出是幂等的)。两件事都做:只清 cookie 而不吊销,token 在服务端仍然有效。

GET|PUT|DELETE /api/me/subscription

「我的」信号订阅。归属只来自会话 cookie —— 请求体里没有、也不接受任何用户 id。

GET 返回 { kinds, active, freeTierOpen };PUT 接受 { kinds: { <类型>: boolean } },返回服务端裁剪后的实际结果(下述);DELETE 退订(幂等)。

两道裁剪,任何写入路径都会过:

规则作用范围行为
ops 封禁所有档位运维通知永不对外,请求体里带 ops:true 也会被剥掉
免费层白名单仅 free只有运营者在 /manage 打开的类型可订阅;默认全关 → 返回 402

裁剪是静默的:越界项被丢弃而不报错(页面上根本看不到这些选项,能提交上来的只有伪造请求)。因此请以 PUT 的返回值为准,而不是你提交的内容。

GET|POST|DELETE /api/me/keys

自助 API key。能否领取、以及能同时持有几把有效 key,由档位权益决定——运营者在 /manage 维护,出厂值:免费档 0 把(不发放)、付费档 5 把、开发者档 5 把,随时可调,无需重新部署。tier 不可自选,当前自助签发一律为 realtime。本档不提供时返回 403;已达上限时返回 409。

付费与开发者档可以带到期时刻:到期后系统自动降为免费档——零延迟私聊投递随之停止、不能再申领新 key。已签发的 key 不吊销,与人工降级同一语义,续费或重新升级即可恢复。

POST 返回的明文 key 只显示这一次。DELETE?id= 只能吊销自己名下的 key,不属于自己时返回 404(不泄露「这个 id 存在」)。

GET/api/auth/telegram

Telegram Login Widget 的回调端点。用户在网页上点击授权 → Telegram 把带签名的参数重定向到这里 → 验签通过即置会话cookie 并 302 到 /me。

与 bot 发码是两条并存路径,不是替换:

路径适用特点
Login Widget生产(域名已在 BotFather /setdomain 绑定)全程不离开浏览器,一次点击
bot /login本地开发、Widget 被扩展拦截、用户本就在 Telegram 里需在 TG 内点一次命令

两者共用 upsertUserByTgChat —— 回调里的 id 就是 tg_chat_id,同一个 Telegram账号无论从哪条路径进来都是同一个用户。

验签遵循官方算法(SHA256(bot_token) 作密钥对排序后的字段串做 HMAC),并额外要求 auth_date 在 5 分钟内。官方示例用 24 小时,那是给"验证身份"的宽松场景;这里是登录,回调 URL 带着完整签名,会落进浏览器历史与日志,放一天等于给了一天的重放窗口。

失败一律 302 回 /login?e=…,且只区分 expired(可重试)与 unknown——不向可能的攻击者反馈"你差在哪一步"。

GET|POST|DELETE /api/me/webhooks

自助管理 webhook 端点。端点挂在你自己的 API key 上,所以每个操作都会顺「端点 → key → 归属」查回去;不属于你的一律 404(不透露该 id 是否存在)。

登记时的四道闸:

闸规则
传输回调地址必须是 https —— 签名保护的是内容完整性,不是传输机密性,而事件体含市场、钱包与金额
key 归属只能用自己名下、未吊销的 key
tier仅 realtime(延迟数据请用拉取 API)
范围端点勾选的类型必须落在该 key 的范围内,越界当场报错而不是静默丢弃——「登记成功、永远不投」是最难排查的配置错误

签名密钥由服务端生成(让调用方自定义等于允许弱密钥,而它是 HMAC 的全部强度来源),只在登记响应里出现一次,列表接口不回显。丢了就删掉重建。

端点上限由档位权益决定(运营者在 /manage 维护,出厂值:付费档 5 个、开发者档 5 个、免费档 0 个)。已登记的端点不受降档或到期影响,照常投递——投递只认端点与 key 自身状态,从不查当前档位;但降为免费档后无法再登记新端点,即使名下仍有未吊销的 key,升级或续费即可恢复登记权限。

自助 key 的范围

POST /api/me/keys 的 scopes 省略时授予全部可推送事件类型,但永远不含market(市场深度卡)。market 是唯一会按需查询上游 Polymarket 的能力,自助发放等于把引擎自己依赖的共享调用预算敞开——需要它请联系管理员单独签发。