面向持有 API key 的订阅方。key 由运营者签发,明文只显示一次(库中仅存sha256),丢失只能重新签发。本文覆盖两类端点:信号(推/拉事件与视图)与按需查询(点名一个市场现算答案)——两类的可靠性承诺不同,见下表。设计取舍与口径修订史见内部契约docs/signals-api.md,本文只讲怎么用。基址:https://whalewatch.wired.fund
| 端点 | 方法 | 鉴权 | 缓存 | 用途 |
|---|---|---|---|---|
/api/signals | GET | API key | 30s | 主 feed:信号(事件)+ 视图 |
/api/signals/list | GET | API key | 30s | 名录:这把 key 实际收得到哪些信号(§4.1) |
/api/market-card/{cid} | GET | API key | 30s | 单市场深度卡(realtime + market 范围) |
/api/record | GET | 无(公开) | 60s | 已公开发布信号的战绩与逐日存证链(§13) |
/api/health | GET | 无(公开) | 无 | 引擎存活探针(200 / 503) |
/api/continuity | GET | 无(公开) | 无 | 数据连续性 · 30 天起算时钟(§13) |
/api/dataset/record.csv | GET | 无(公开) | 300s | 已发布信号全量台账 CSV 数据集(§13) |
/api/pulse | GET | 无(公开) | 300s | 市场脉搏日榜(§13) |
/api/calibration | GET | 无(公开) | 600s | 市场校准研究(§13) |
/embed/record /embed/status | GET | 无(公开) | 60s | 可嵌入 HTML 卡片:战绩卡 / 状态徽章(§13) |
/api/auth/login | GET | 一次性登录码 | 无 | 消费 Telegram bot 签发的登录码,置会话(§17) |
/api/auth/telegram | GET | HMAC 签名验证 | 无 | Telegram Login Widget 回调(§17) |
/api/auth/logout | POST | 会话 cookie | 无 | 登出:吊销会话并清 cookie(§17) |
/api/me/subscription | GET PUT DELETE | 会话 cookie | 无 | 读写「我的」信号订阅(§17) |
/api/me/keys | GET POST DELETE | 会话 cookie | 无 | 自助领取 / 吊销 API key(§17) |
/api/me/webhooks | GET POST DELETE | 会话 cookie | 无 | 自助登记 / 启停 webhook 端点(§17) |
| webhook(你的端点) | POST | HMAC 签名验证 | — | 事件推送(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" | jqNodeconst 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。
| 状态 | 含义 | 处理 |
|---|---|---|
401 | key 缺失、错误或已吊销 | 核对 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 都能拿到。
当前开放状态(本页渲染时按运营开关实时生成):
标「未开启」的事件类型不会产出任何数据(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。
| 参数 | 类型 | 取值 | 默认 | 说明 |
|---|---|---|---|---|
windowHours | number | 6 / 12 / 24 / 48 | 24 | 非法值静默回落默认值 |
作用于 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 失败响应) |
| ID | conditionId = 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 的三种取值,按类型:
sourceType | wallets |
|---|---|
large | 单元素——一笔成交就是一个钱包,金额/价即该笔的名义额与成交价 |
consensus | 全量参与钱包,按净买降序(顺序即信息,勿重排) |
discovery | null——没有仓位,地址在 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[] 字段
| 字段 | 类型 | 中文名 | 说明 |
|---|---|---|---|
id | number | 事件 ID | webhook 去重键的一半 |
strategy | object | 档位 | {id, code, name, source};认档用 code |
conditionId | string | 市场 ID | — |
title | string | 市场问题 | 英文原文 |
slug | string | 市场短名 | 拼单市场页链接 |
eventSlug | string | 事件短名 | 拼事件页链接(一个事件下可挂几十个市场) |
category | string | null | 一级分类 | 如 Sports |
subcategory | string | null | 二级分类 | 如 NBA;无为 null |
outcome | string | 买入方向 | 反向档已是翻转后的方向 |
outcomeIndex | number | null | 方向序号 | — |
asset | string | null | 代币 ID | 用它订实时价(§12) |
formationTs | number | 形成时刻 | 语义随 source,见 §8.3 |
referencePrice | number | null | 聪明钱成本 | — |
walletCount | number | null | 钱包数 | heavy 恒 1 |
totalNetUsd | number | null | 总净买 | USD |
entryPrice | number | null | 纸面进场价 | 我们的模拟买入价 |
sizeUsd | number | null | 纸面额 | 默认 500 |
emittedAt | number | 发布时刻 | 减 formationTs = 检测延迟 |
entryPrice − referencePrice = 追价成本(实测红线 10¢)。
8.2 settled[] 字段
| 字段 | 类型 | 中文名 | 说明 |
|---|---|---|---|
id | number | 事件 ID | 与 active 同一台账 |
strategyId | number | 档位 ID | 部署本地,见 §8.3 |
strategyCode | string | null | 档位码 | 跨部署稳定 ← 认档用它 |
strategyName | string | 档位名 | 中文展示名 |
conditionId | string | 市场 ID | — |
title | string | 市场问题 | — |
outcome | string | 买入方向 | — |
entryPrice | number | null | 进场价 | 纸面 |
exitPrice | number | null | 退出价 | 结算价 |
won | boolean | null | 是否盈利 | null = 平局,不进胜率分母 |
realizedPnl | number | null | 已实现盈亏 | USD,纸面 |
settledAt | number | 结算时刻 | — |
纸面口径:以上是模拟跟单数字(真实数据 · 模拟策略),展示必须携带「研究用途模拟信号 · 非投资建议 · 只读非托管」。
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 是哪一档」时看它:
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 | 单人观点;已有共识时被抑制 |
| 字段 | 类型 | 中文名 | 说明 |
|---|---|---|---|
key | string | 去重键 | <conditionId>|<outcome>;split 为 conditionId |
kind | SignalKind | 种类 | 见上表 |
conditionId | string | 市场 ID | — |
title | string | 市场问题 | — |
slug | string | 市场短名 | 缺失时空串 |
eventSlug | string | 事件短名 | 别拿它当市场链接 |
category | string | null | 一级分类 | — |
subcategory | string | null | 二级分类 | — |
formationTs | number | 形成时刻 | 判断新鲜度用它 |
outcome | string | null | 方向 | split 恒 null |
outcomeIndex | number | null | 方向序号 | — |
asset | string | null | 代币 ID | — |
walletCount | number | 钱包数 | split 为两侧之和 |
netUsd | number | 净买入 | split 为两侧之和 |
avgPrice | number | 成本基准 | split 恒 0 |
wallets | {wallet,netUsd,avgPrice}[] | 钱包明细 | 按净买降序 |
sides | 数组(仅 split) | 双边明细 | 每侧同 wallets 外加 outcome/asset |
9.2 settled[] — 已结算(认账视图)
近 3 天,同一市场×方向取最新一条,最多 20 条。与 active[] 同构:身份/仓位字段同名同义(可用 key 对上号、复用同一卡片组件),差别只在末尾三项:
| 字段 | 类型 | 中文名 | 说明 |
|---|---|---|---|
entryPrice | number | 进场价 | 等同 active 的 avgPrice |
won | boolean | 是否命中 | — |
settledAt | number | 结算时刻 | — |
同一条信号可同时出现在 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 分钟的盘面回答不了「我现在该不该进」 ——所以延迟档拿不到它,这是范围问题不是字段阉割(delayedkey 的/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-07 | GET /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-07 | GET /api/dataset/record.csv 末尾追加 signal_id 列(§13):additive,列序不变,按列名解析的消费方零影响 |
| 2026-09-04 | GET /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-31 | GET /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-31 | GET /api/market-card/{cid} 的 card 追加 pulse 键(§14):该市场的分类与市场脉搏日榜身份(boards[] = 异常/分歧/无鲸/洗量,anomalyScore)——additive、零上游(纯 market_daily 本地读),底座未覆盖该市场时为 null。⚠️ 时间口径与 card 其余字段不同:其余是此刻的成交窗口,pulse 是 pulse.day 那个已收盘 UTC 日的判定 |
| 2026-08-28 | GET /api/pulse 再追加 ghosts[](无鲸异动)、washTop[](洗量榜)与 top[].washRatio(§13):全部 additive,判定材料(wash_usd/max_fill_usd)自当日起采集,老日份为 null/不进榜 |
| 2026-08-28 | GET /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-18 | webhook 支持 ① 类型;失败响应字段集合与成功一致;record 量纲修正(条数);开放状态改实时生成 |
| 2026-08-13 | strategies 段、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 的能力,自助发放等于把引擎自己依赖的共享调用预算敞开——需要它请联系管理员单独签发。