上篇《ads.txt / app-ads.txt / sellers.json》 探讨了三份静态声明——媒体(Publisher)通过 ads.txt 或 app-ads.txt 声明「谁被授权售卖我的库存」,而中介则通过 sellers.json 声明「系统内每个账号背后真实的卖方身份」。然而,静态声明只能回答「谁被授权」,却无法回答「一条具体的广告请求实际经过了哪些中介,且顺序如何」。为了补上这后半个问题,OpenRTB 引入了 SupplyChain 对象(schain),这正是本篇的主角。
正因如此,如果说三份静态声明是事先公布的授权表,那么 schain 则是随每条 bid request 逐跳生长的运行时记录。每个中介在转发请求前,都必须将自身作为一个 node 追加进去,从而清晰记录「我是谁,又从谁手中接手」。当买方(Buy Side)收到请求后,只需将每一跳与授权表进行比对,即可判定这条供应链是否完整、每一手转售是否均被授权。需要强调的是,本篇还将深入剖析买方侧的完整工程实现:从离线抓取三份声明并编译成内存索引,到在竞价热路径上利用 schain 完成亚毫秒级的逐跳核验。
本文是 供应链透明度与优化 系列的第 2 篇(运行时 schain)。 全系列 3 篇:
- 供应链透明度(上):ads.txt 与卖方授权声明
- 供应链透明度(下):OpenRTB schain
- 供应链优化(SPO)
一句话定位:OpenRTB 的 schain 作为随请求逐跳生长的运行时记录,配合静态声明实现了供应链的交叉核验,并为买方剔除冗余路径提供了数据支撑。
schain 是一条「谁经手了谁」的运行时账。
nodes[0] 最靠近媒体,每多一跳中介就 append 一个 node;complete=1 是完整性承诺。每个 node 的 asi/sid 正是 sellers.json 和 ads.txt 的对账键——运行时记录在这里接回上篇的静态声明。
TL;DR
- schain 是什么:OpenRTB 的
source.schain(早期在source.ext.schain),一个有序node数组,随请求逐跳生长,记录库存实际经过的每一手中介。 - node 字段:
asi(该中介域名)、sid(卖方在该系统里的账号)、hp(是否参与本次结算);顶层complete(1代表链从媒体到发出请求的交易所完整无缺)、ver。 - 两条规则:① 每个中介转发前必须把自己追加为一个
node,且nodes[0]最靠近媒体、顺序不能乱;② 只要有一跳没加进来,complete就该标0——它是一句可被证伪的承诺。 - 核验闭环:对
schain逐跳走一遍——complete==1?→ 每个node的sid在asi的sellers.json里存在?→ 从sellers.json拿到卖方domain,回查其ads.txt是否授权(asi, sid)?全过且完整,即为可信路径。 - 交叉核验才锁得住:伪造者能改
bid request的字段,但改不了媒体根目录那份自己控制的ads.txt。要让整条链自洽,得同时篡改ads.txt、sellers.json与schain且互相对得上,造假成本陡增。 - 离线工程:买方先把全网
ads.txt、app-ads.txt与sellers.json抓全、清洗,并编译成authMap、sellersMap、bundleMap三张内存索引;两大坑是「200≠ 有文件」的软 404 与「文件普遍很脏」。 - 热路径工程:核验挂在竞价漏斗很靠前的一段,纯读离线建好的三张内存索引,亚毫秒完成,失败向「降权或放行」降级;核验是尽力而为,灰色地带降权而非硬拒。
- 它还喂给 SPO:
schain暴露真实跳数,买方据此收敛冗余路径、降低 ad tech tax——透明度正是供应链优化(SPO)的前提。
Table of contents
Open Table of contents
一、schain 是什么:运行时逐跳记录
上篇介绍的 ads.txt 与 sellers.json 都是静态声明,即媒体和中介各自「事先」贴出的授权表。但这引出了一个关键问题:一条具体的 bid request 到底实际经过了哪些中介,且顺序如何?为了解答这个问题,我们需要引入 OpenRTB 里的 SupplyChain 对象(schain)。它随请求逐跳生长,忠实记录了真实的流转路径。
schain 挂载在 source.schain(OpenRTB 2.6 规范;早期实现则放在 source.ext.schain),其核心是一个有序的 node 数组。node 与顶层字段的具体含义如下:
| 字段 | 含义 |
|---|---|
asi | 广告系统标识(该中介的域名)——对应 sellers.json 发布域名与 ads.txt 的广告系统域名 |
sid | 卖方在该系统里的账号 ID——对应 sellers.json 的 seller_id 与 ads.txt 的账号 ID |
hp | 该 node 是否参与本次结算(handles payment),通常为 1 |
rid / name / domain | 请求 ID、可选的名称和域名 |
complete(顶层) | 1 代表链完整(从媒体到发出请求的交易所每一跳都在);0 代表不完整 |
ver(顶层) | schain 版本 |
正如上篇反复强调的,asi 与 sid 正是对账键:asi 是广告系统域名(据此即可拼出 https://{asi}/sellers.json),而 sid 是卖方在该系统内的账号编号。因此,schain 的每个 node 实际上代表了「在某个 asi 内、由某个 sid 经手的这一跳」。运行时记录采用的正是静态声明的同一套键,从而确保两边能够完美对齐。
在工程实现上,有几条必须遵守的规则:
- 每个中介在转发请求前,必须将自己追加为一个
node,且顺序必须正确(nodes[0]是最上游、直连媒体的那一跳)。漏加或乱序都会导致下游核验失败。 complete=1是一句可被证伪的承诺。只要有任一跳未把自己加入,理论上就应标0。当买方看到complete=0时,即知这条链存在未申报的中间跳。
需要强调的是,schain 并不替代 ads.txt 或 sellers.json,而是将它们串联起来:它给出「实际路径」,前两者给出「每一跳是否被授权」。买方只有以 schain 的每个 node 回查另外两份声明,方才完成最终的核验闭环。
这恰好回应了上篇留下的库存洗钱难题。洗钱的本质是「买方看不到经了几手、每一手是否被授权」。如今,schain 将「经了几手」变成一个公开的有序数组。若想隐去中间跳,就必须不把自己加入,此时 complete 便只能标 0,从而暴露异常;若如实全部加入,那几手转售则完全暴露在买方眼前,进而进入下文的逐跳核验。无论哪种方式,洗钱的路径都被彻底堵死。
二、把声明和运行时接上:买方交叉核验闭环
单看每个标准,它们都只是一份孤立的表;真正起作用的是买方将它们进行交叉核验。这一核验逻辑本质上是一条层层递进的决策链:
核验是一条「层层收紧」的漏斗:先看链完不完整(schain.complete),再看每一跳的卖方在不在中介的 sellers.json 里,再回查卖方域名的 ads.txt 授没授权这一跳;全过才是可信路径,还能顺带按跳数做 SPO。任一步不过,对应到「降权」或「丢弃」。
一条 bid request 的完整核验,就是对它的 schain 逐跳走一遍:
- 完整性:首先检查
schain.complete == 1是否成立。如果不完整,说明有隐藏的中间跳——保守的买方会直接拒绝,而多数买方会选择降权处理。 - 卖方存在性:对每个
node,去asi的sellers.json(即离线建好的sellersMap,详见第三节)里查sid是否存在。如果查不到,即视为未知卖方(unauthorized),直接拒绝出价。 - 授权一致性:从
sellers.json拿到该卖方的domain,回查该domain的ads.txt或app-ads.txt(即离线建好的authMap),确认其中是否有(asi, sid)这条授权记录。如果没有,则判定为域名伪造或非法转售,直接丢弃。 - 路径质量(SPO):全部通过后,还需要评估链条是否有冗余跳数。这不是「真伪」问题,而是「效率」问题。买方会据此优先走更短、更少抽成的路径(详见第五节)。
以域名伪造为例:假设一条请求声称库存来自 nytimes.com,且 schain 中带有某个 (asi, sid)。买方回查 nytimes.com/ads.txt 时,发现其中并无这条 (asi, sid) 的授权,便会判定其为伪造并丢弃。伪造者虽然可以篡改 bid request 里的字段,却改不了 nytimes.com 根目录下那份由纽约时报自身控制的 ads.txt。
不仅如此,再以上篇那条库存洗钱链(X → SSP-A → ADX-B → SSP-C)验证一遍:若 X 试图隐去自己这一跳,schain 中便少一个 node,complete 应为 0,在第 1 步即被拦下;若如实记录,则走到第 3 步,回查媒体的 ads.txt 会发现并无授权 X 对应的 (asi, sid),同样会被丢弃。无论隐去与否,洗钱链都通不过这个严密的闭环。
正因如此,这四件套单独拿出来均可被绕过或造假,但交叉核验把它们相互锁定。伪造者必须同时篡改媒体的 ads.txt、中介的 sellers.json 和请求的 schain,并使三者完全自洽,造假成本陡增到极不划算的地步。
三、买方侧工程(离线):全网抓取、解析、建索引
核验逻辑虽然清晰,但真正的工程量在于把这三份文件抓取完整、清洗干净,并编译成可在竞价热路径上 O(1) 查询的索引。这是一道典型的「离线重、在线轻」读写分离题,与频次控制工程篇属于同一套架构思路。
首先是离线抓取与定时刷新。买方需要维护以下三类全网数据:
- 全网媒体的
ads.txt(按 Web 域名划分),体量在数百万量级; - App 开发者的
app-ads.txt,外加bundle id → 开发者域名的应用商店映射关系; - 各中介的
sellers.json(按广告系统域名划分),数量较少但更新频繁。
这本质上是一个分布式爬虫、存储与调度问题。系统需要按域名分片抓取、遵守缓存头、错峰刷新(热门域名勤刷、长尾域名稀刷),同时处理超时与 404 错误,并监控覆盖率。由于文件并非一成不变,媒体会增删授权,中介也会调整 sellers.json,因此刷新周期直接决定了核验的时效与误杀率。此外,sellers.json 还常见大厂文件达数十万条、上百 MB(需用 gzip 加 ETag 增量刷新)、302 跳转至 CDN 等复杂情况。
在抓取阶段,有两个极易踩中的坑,需要特别强调:
200不等于有文件:大量站点对不存在的/app-ads.txt并不返回 404,而是返回一个 HTTP 200 的网页(如首页、SPA 路由页、软 404)。如果仅凭状态码即认定「抓到了」,会将大量 HTML 垃圾灌入索引。判定文件是否存在,必须校验content-type为纯文本(sellers.json则须为 JSON 且能解析出sellers[]数组),且内容能解析出合法记录行,否则一律视同「无」。- 文件普遍很脏,解析须容错并归一化:即便是大厂的正规
app-ads.txt,也常见域名或关系字段大小写混用(如Pubmatic.com与pubmatic.com、RESELLER与reseller)、逗号前后带空格或漏逗号、第四个字段 TAG-ID 抄错或截断,以及海量重复行。因此,解析器必须统一转小写、trim 空格、按逗号切分后校验字段数、忽略#注释与空行、按(asi,sid,关系)去重,并对畸形行跳过而非整文件报错。
其次是预构索引。把抓来的原始文件编译成热路径能 O(1) 查的结构,典型的是以下三张内存表:
authMap: (publisher_domain) → set of authorized (asi, sid) # 来自 ads.txt/app-ads.txt
sellersMap: (asi) → { sid → {domain, seller_type, confidential} } # 来自 sellers.json
bundleMap: (app.bundle) → developer_domain # 来自商店爬取
这三张表构成了核验闭环的查询面:authMap 回答「该媒体授权了哪些 (asi,sid)」,sellersMap 回答「某 asi 下某 sid 背后是谁」,而 bundleMap 将 App 请求翻译为可回查的开发者域名。它们与频次控制的本地缓存一致:离线全量灌入进程内存、定时刷新,在线纯读。下一节的逐跳核验,读的正是这三张表。
四、买方侧工程(在线):热路径逐跳核验
索引备好后,核验逻辑就要跑在十万级 QPS 的竞价热路径上。由于重活儿(全网抓取、解析、建索引)已在离线阶段做完,热路径只需纯读那三张内存索引,即可在亚毫秒内完成判断。
每当一条 bid request 进来,核验本质上就是对 schain 的一次线性扫描:
extract publisher_domain / bundle + source.schain
for each node in schain:
seller = sellersMap[node.asi]?.[node.sid]
if seller == null → 未知卖方,标记不可信
if authMap[seller.domain] not contains (node.asi, node.sid)
→ 未授权或伪造,标记不可信
if !schain.complete or 任一 node 不可信:
→ no-bid / 降权 / 不参与(按买方策略)
在热路径的工程实现上,有几个关键点需要注意:
- 索引纯内存、亚毫秒命中:
sellersMap、authMap与bundleMap必须离线全量灌进进程内存并定时刷新,在线只读。绝不能因为一次索引 miss 或网络抖动就把竞价链路拖垮,或者把真实的广告曝光误杀。 - 尽量前置:核验通常挂在竞价漏斗很靠前的请求解析与定向过滤阶段。让不可信流量尽早出局,能为后续的排序和出价省下大量算力。
- App 请求先翻译:对于 App 流量,必须先用
bundleMap把app.bundle翻译成开发者域名,再回查authMap。这层映射的缺口会直接变成 App 侧的核验盲区。 - 失败降级方向要对:遇到索引 miss、文件缺失、
confidential卖方等「查不到」的情况,应向放行或降权降级;只有「明确对不上」(如sellers.json有记录但domain对不上、ads.txt明确没授权)才走拒绝或丢弃逻辑。
需要强调的是,核验是尽力而为的,我们必须承认以下几个现实边界:
- 覆盖不全:目前仍有媒体没部署
ads.txt,有中介的sellers.json不完整,app-ads.txt的商店映射也存在缺口。因此,原则是「查不到」降权,「明确对不上」才拒绝。 - confidential 与弱域名:
sellers.json允许隐藏身份,或者domain指向*.blogspot.com、*.firebaseapp.com、*.app-ads-txt.com这类弱绑定域。这类路径无法完全核验,只能按买方的风险偏好进行降权。 - 时效窗口:文件更新与索引刷新之间必然存在滞后,可能造成短暂误判。系统应给予宽限期或软失败,切勿将偶发的对不上直接判为欺诈而硬拒。
- schain 本身可能被伪造:
node完全可以被人为编造,因此schain并非「自证清白」,必须逐跳回查另外两份声明方才作数。单凭complete=1绝对不等于可信。
五、透明度之上:供应链优化(SPO)
核验解决了「真不真」的问题,而 schain 还一并解决了「值不值」的问题。由于 schain 把真实跳数与每一跳的中介都摊开在阳光下,买方首次得以回答:同一份库存,究竟是经 3 跳中介买到的,还是经 1 跳直连(DIRECT)买到的?中间每一跳都在抽成,这正是生态图所讲的 ad tech tax。
供应链优化(SPO,Supply Path Optimization) 便是买方据此收敛路径的策略:面对同一份库存的多条可达路径,优先选择更短、更少抽成、更多 DIRECT、可核验性更高的那条,果断砍掉冗余的转售跳。这既能节省预算,让更多资金落到真实库存上,也能降低风险,因为路径越短,越难藏匿未授权环节。
正因如此,透明度与 SPO 互为因果:先有可核验的供应链(透明度),才谈得上有依据地优化供应链(SPO)。如果没有 schain,买方无从得知预算经了几手,优化也就无从着手。这恰好回应了生态图第 7 节那 15% 的 unknown delta:浪费只有被测得到,才谈得上被消除。关于 SPO 的打分、席位策略与需求方平台(DSP)的落地细节,详见专文《供应链优化(SPO)》。
六、常见误区 ↔ 正解
| 常见误区 | 正解 |
|---|---|
schain 能替代 ads.txt 或 sellers.json | 不能。schain 是运行时路径,那两者是授权声明;核验要三者交叉,缺一不可 |
complete=1 就一定可信 | 它只是一句可被证伪的承诺;仍要逐 node 回查 sellers.json 和 ads.txt 才算数 |
schain 里有 node 就代表被授权 | node 只是「自报经了这一手」;是否被授权,要拿 (asi,sid) 回查 sellers.json 与 ads.txt 才知道 |
| 核验就该「对不上就拒」 | 「查不到」(覆盖/时效/confidential)降权、「明确对不上」才拒——尽力而为,避免误杀真实库存 |
| 抓到 HTTP 200 就是有文件 | 很多站点对不存在的路径返回 200 网页或软 404;必须校验是纯文本且可解析记录(sellers.json 须是 JSON)才算数 |
schain 只是防欺诈的 | 它同时是 SPO 的数据基础:暴露真实跳数,才能收敛路径、降低 ad tech tax |
S2S 或 Header Bidding 不用管 schain | 恰恰相反——服务器到服务器的多跳场景更依赖 schain 逐跳生长来还原真实路径(见 Prebid Server) |
排障与调优口诀:交叉核验锁闭环;查不到则降权;对不上才硬拒;跳数越少越优。
静态声明回答「谁被授权」,schain 回答「谁实际参与了」。核验过关之后,跳数和每一跳的中介就可以拿来做路径收敛——详见下一篇《供应链优化(SPO)》。
延伸阅读
同系列(供应链透明度与优化):
- 供应链透明度(上):ads.txt / sellers.json:本篇的上篇——三份静态声明的字段与对账关系。
- 供应链优化(SPO):有了跳数之后,买方如何收敛路径、砍税——本篇第五节的完整展开。
跨系列相关:
- 程序化广告生态全景:母篇——第 7 节的 ad tech tax 与 15% unknown delta,正是
schain与 SPO 要解决的问题。 - Last Look 深挖:拍卖侧从「偷看」到「一价加可审计日志」,和供应链侧是同一方向。
- 程序化交易模式:RTB / PMP / PD / PG:不同交易模式下供应链的长短与可控性差别,是 SPO 的背景。
- Header Bidding:Prebid.js vs Prebid Server:
schain在多路并行竞价里怎么逐跳生长、S2S 场景为何更要靠它。 - Bidder 请求解析:
schain、domain与bundle在bid request里怎么被解析出来,供应链核验挂在漏斗的哪一段。
一手资料与规范:
- IAB Tech Lab. Supply Chain Object:
schain的node结构、complete语义与在 OpenRTB 中的挂载点。 - IAB Tech Lab. OpenRTB 2.6 Specification:
source.schain等字段的权威定义。 - IAB Tech Lab. sellers.json Specification:
schain回查所依赖的卖方身份声明。 - ISBA & PwC. Programmatic Supply Chain Transparency Study:unknown delta 与「日志对不上账」的实证来源。