Skip to content
Charles Shao
Go back

供应链透明度(上):ads.txt / app-ads.txt / sellers.json 把卖方授权变成可交叉核验的声明

views

程序化广告的供应链天生可被任意插队、冒名。一次曝光从媒体流向买方,中途可能经过多个 SSP、ADX 与转售中介;而 OpenRTB bid request 里描述库存归属的字段——媒体域名 site.domain、App 包名 app.bundle、卖方身份——历史上全由上游自报。自报即可伪造:把 domain 改成 nytimes.com,垃圾流量便能卖出优质库存的价,这是域名伪造(domain spoofing);把未授权库存层层转卖、洗白来源,则是非法转售 / 库存洗钱(inventory laundering)

IAB Tech Lab 的供应链标准,本质是给这条链逐层盖上可核验的图章:媒体声明谁被授权卖、中介声明每个账号背后是谁、每条请求在运行时记录真实经过的每一跳。这套标准分两类——三份静态声明ads.txt / app-ads.txt / sellers.json)与一个运行时对象SupplyChain / schain)。本篇讲透三份静态声明,即「谁被授权卖」;运行时的 schain 与买方交叉核验闭环见下篇《OpenRTB SupplyChain(schain)》

供应链透明度四件套总览:一条从左到右的供应链——媒体 Publisher(网站 / App,绿色)→ SSP / Exchange A(中介,蓝色)→ ADX B(转售中介,紫色)→ DSP 买方(竞价 + 核验,琥珀色),中介之间的箭头标注 schain 逐跳追加 node;每个节点上方挂着它发布的标准:媒体上方是 ads.txt / app-ads.txt,两个中介上方是 sellers.json,买方上方是交叉核验;下方两段说明——三份「声明」(ads.txt/app-ads.txt 媒体声明谁被授权卖、sellers.json 中介声明每个 account_id 背后卖方是谁、SupplyChain 运行时逐跳记录真实中介)加一个「运行时对象」;买方交叉核验:schain 每一跳的 (asi,sid) 查该 asi 的 sellers.json 得卖方域名、再回查该域名 ads.txt 是否授权,三者对得上且 complete=1 才是可信路径 四件套分两类:三份静态声明(ads.txt/app-ads.txt 由媒体发、sellers.json 由中介发,本篇主角)+ 一个运行时对象(schain 随每条 bid request 逐跳生长,见下篇)。声明回答「谁被授权参与」,运行时回答「谁实际参与了」;买方把「实际」和「被授权」一对,才知道这条路径可不可信。

TL;DR

Table of contents

Open Table of contents

一、问题定义:域名伪造与非法转售

在展开标准之前,先明确它们要防的是什么。OpenRTB bid request 里描述「库存归属」的关键字段有:

site.domain / site.page      # Web:媒体域名 / 页面 URL
app.bundle / app.storeurl    # App:包名 / 商店链接
publisher.id / publisher.domain

问题在于:这些字段沿途均由上游填写,买方无从验证真伪。由此产生两类典型欺诈:

两者的共同根因:供应链上缺少一份「谁被授权代表谁」的可核验记录。IAB 标准正是逐个补上这条记录——先补「声明」(谁被授权,本篇),再补「运行时」(实际走了哪条路,下篇),最后由买方交叉核验把两者对上。

库存洗钱的典型链路。 把「一次曝光机会」视作一批货,洗钱即将来路不明 / 未授权的货倒手数次、洗白来源,再以「正规」名义售出——与洗钱同理,只是标的是库存:

  1. 作弊 / 未授权流量源 X,实际持有的是垃圾位、机器人流量,或未获纽约时报授权销售 nytimes.com 库存;
  2. X 将其转售给 SSP-A,并声称「这是 nytimes.com 的库存」;
  3. SSP-A(疏于核查或有意为之)转售给 ADX-B,ADX-B 再转售给 SSP-C,层层倒手;
  4. 抵达买方(DSP)时,bid request 上 site.domain = nytimes.com,呈现为一条正规、优质的路径

买方于是以优质库存的价格买到了假货 / 盗卖货。它与域名伪造的区别在于:伪造是直接篡改字段(假冒身份),洗钱是借一串中间商倒手藏匿未授权来源(无证倒卖 + 隐藏路径),后者更为隐蔽。

这个例子贯穿两篇:本篇的三份声明先锁定「谁被授权」(洗钱链中的第一手 X 并不在 nytimes.com/ads.txt 内);下篇的 schain 让「经了几手」可见、由交叉核验逐跳查出「哪一手未授权」——回查即穿帮。

二、ads.txt:媒体的授权卖方清单

ads.txt(Authorized Digital Sellers) 是其中最早、也最简单的一块:媒体在自己域名的根目录放置一个纯文本文件——https://example.com/ads.txt——公开声明**「哪些广告系统、用哪个账号,被授权销售我的库存」**。买方与验证方爬取它,即可判断一条 bid request 声称的卖方是否为媒体所认可。

每一行是一条授权记录,逗号分隔,最多四个字段:

ads.txt 一行的四个字段解剖 + app-ads.txt 的域名解析:顶部是一整行原始记录 google.com, pub-0000000000000000, DIRECT, f08c47fec0942fa0;下方拆成四个字段卡片——① 广告系统域名 asi(google.com,即 SSP/ADX)② 卖方账号 ID(pub-0000…,等于 sellers.json 的 seller_id)③ 账户关系(DIRECT / RESELLER)④ 认证机构 ID(TAG-ID,可选);中部旁注说明 DIRECT 是直接账户、RESELLER 是授权第三方转售,买方把全网 ads.txt 抓下来构建「域名→授权(asi,sid)集合」索引;底部是 app-ads.txt 的解析流水线:bundle id(com.foo.bar)→ 应用商店 listing → 开发者官网域名 developer.com → developer.com/app-ads.txt,并说明 App 没有 web root 靠开发者域名反查、这层映射依赖爬商店是最脏的一环 一行 ads.txt 的四个字段:广告系统域名 · 卖方账号 ID · 账户关系 · 认证机构 ID。第①②字段正是后面 schain 里 node 的 asisid,也是 sellers.json 的对账键——三份声明在这里第一次咬合。

四个字段:

字段含义说明
广告系统域名被授权的 SSP/ADX 域名对应 schain node 的 asi、sellers.json 的发布域名
卖方账号 ID媒体在该系统里的账号对应 schain node 的 sid、sellers.json 的 seller_id
关系类型DIRECTRESELLERDIRECT=直接账户;RESELLER=授权第三方代卖
认证机构 IDTAG-ID(可选)用于进一步确认广告系统身份

前两个字段——asisid——是贯穿整套标准的「对账键」,此处先明确(sellers.json 与下篇 schain 会反复用到):

因此单独的 sid 无意义,必须成对使用:(asi, sid) = 「在 asi 这个广告系统内、编号为 sid 的卖方账号」,在全网范围内唯一锁定一条「谁通过谁销售」的授权关系。它在多份声明中处处一致,才算对得上账:

ads.txt 一行     : asi = 广告系统域名, sid = 卖方账号ID
sellers.json 一条: asi = 发布该文件的中介域名, sid = seller_id
schain 一个 node : asi = node.asi,        sid = node.sid   (见下篇)

DIRECTRESELLER 是理解供应链形状的关键:

以一条真实的 DIRECT 记录为例,将四个字段落到业务含义上:

google.com, pub-3469257745550756, DIRECT, f08c47fec0942fa0

一个常见误解需澄清:Google 在这条链里是「销售库存的交易所」,而非「填充广告的广告主」。媒体将库存授权 google.com 销售,Google 发起拍卖、撮合众多广告主/买方竞价,由出价最高者的广告填充该展示位;Google 扣除技术费后将余款直接结算给媒体。即:sid 回答「这是谁的账号」,asi 回答「谁在代其销售」,DIRECT/RESELLER 回答「隔了几层」——填充进来的广告来自拍卖胜出的需求方,与 asi 无关。

第④字段的认证机构 ID(上例中的 f08c47fec0942fa0)需单独说明:它是广告系统(asi)在第三方认证机构登记的全球唯一身份编号,实践中该机构基本仅有一家——TAG(Trustworthy Accountability Group),故它几乎总是一个 TAG-ID。要点有三:

ads.txt 还支持几个变量行,用于更复杂的所有权场景:

CONTACT=ops@example.com          # 联系方式
SUBDOMAIN=news.example.com       # 指向子域名自己的 ads.txt
OWNERDOMAIN=example.com          # 声明真正的所有者域名
MANAGERDOMAIN=ssp.com            # 声明代运营变现的一方

一句话本质:ads.txt 把「谁能销售我的库存」从媒体的私下约定,变成一份任何人都能抓取、核对的公开清单。买方的用法极简:请求中声称的卖方 (asi, sid) 不在清单内 → 判定为伪造或未授权。伪造者可篡改 bid request 中的字段,却改不了 nytimes.com 根目录下那份由纽约时报自身控制的 ads.txt——信任锚点从「上游自报」转移到「媒体自身掌控的公开文件」,这正是整套设计的支点。

三、app-ads.txt:面向 App / CTV 的同一机制

ads.txt 依赖「域名根目录」这个天然锚点工作,但 App 与 CTV/OTT 没有 web root——com.foo.bar 这样的 bundle id 并非可拼接 /ads.txt 的地址。app-ads.txt 解决的正是这一定位问题,文件格式与 ads.txt 完全一致,唯一差别在于如何找到该文件

  1. 从 bid request 拿到 app.bundle(包名)或 app.storeurl
  2. 用它去应用商店(App Store / Google Play)查到这个 App 的 listing;
  3. 从 listing 里读出开发者声明的官网域名(developer website);
  4. 到该域名根目录取 https://developer.com/app-ads.txt

即:App 的授权信任锚点,转移到了「开发者在商店中声明的官网」——因为商店 listing 是开发者身份的可信来源,无法冒充。此处的 listing 即在商店中打开某个 App 所见的详情页(图标、截图、评分,以及关键的**「开发者网站 / Developer Website」字段**);开发者上架时须通过商店审核并登记该官网,攻击者无法篡改——这正是「信任锚点」的由来。

「从 listing 读出开发者域名」的具体做法因平台而异:

取得官网链接后还须归一化为根域名再拼接路径:去除 www、路径与 query,按 public suffix list 取 eTLD+1(https://www.developer.com/x?utm=..developer.com),最终取 https://developer.com/app-ads.txt

这条链看似直白,工程上却是这套标准中最棘手的一环bundle id → 开发者域名 的映射需持续爬取各大应用商店并解析页面,而商店结构会变动、存在反爬与地区差异;一旦 App 更换开发者、或 bundle 被仿冒,映射便可能出错。多数买方或自行维护这张映射表,或采购第三方数据。

一个反常识但重要的判断:许多 App 并无 app-ads.txt,且属正常。app-ads.txt 仅对「将库存投放开放程序化市场、经第三方 SSP 转售」的媒体才有意义。新闻聚合类 App(挂载数十个 SSP 变现)会有一份上千行的 app-ads.txt;而订阅 / 内购型(如 Discord)、围墙花园(如 TikTok——广告仅在自家闭环买卖、不走 OpenRTB)、纯工具类(如各类卖家后台)均不参与开放供应链,自然不发布该文件。因此「查不到 app-ads.txt」通常意味着该 App 不做开放程序化,而非欺诈——核验时应归为「降权 / 不适用」而非「拒绝」。反之,若一条 bid request 声称是某围墙花园 App 的库存、却带着一串外部 SSP 的 schain,才是应予警惕的信号

CTV 场景更复杂:设备、渠道、App 形态碎片化,app.bundle 口径各家不一,app-ads.txt 的覆盖率与准确率均低于 Web。这也是 CTV 至今仍为欺诈重灾区的原因之一。

四、sellers.json:中介的卖方声明(ads.txt 的镜像)

ads.txt/app-ads.txt 是从媒体出发的声明——「谁能卖我」。但买方收到 bid request 时,直接对话的是中介(SSP/ADX),请求携带的是「某广告系统的某个 seller_id」。买方还需要反方向的一份声明:这个 seller_id 究竟是谁? 这便是 sellers.json

每个中介在自身域名根目录发布 https://ssp.com/sellers.json,逐条声明其系统内每个账号背后的实体:

{
  "sellers": [
    {
      "seller_id": "540123",
      "name": "The New York Times",
      "domain": "nytimes.com",
      "seller_type": "PUBLISHER"
    },
    {
      "seller_id": "889001",
      "name": "SomeReseller Inc",
      "domain": "somereseller.com",
      "seller_type": "INTERMEDIARY"
    }
  ]
}

一条 seller entry 即中介为「系统内某个账号」签发的一张身份卡片,四个核心字段各回答一个不同的问题:

另有两个可选标记:is_confidential(允许中介出于商业保密隐藏 name/domain,代价是该路径无法完全核验、只能降权)、is_passthrough(标记该节点仅为透传、并非真正的卖方账户)。

顶层还携带中介自身的身份信息:contact_emailversion,以及 identifiers(常含该中介的 TAG-ID,可与 ads.txt 第④字段相互印证;有时还含 DUNS 企业编码)。

如何获取:sellers.json 的位置完全由 asi 决定——https://{asi}/sellers.json,asi 为何即据此拼接,无需像 app-ads.txt 那样翻译域名。买方从抓取到的所有 ads.txt 中收集全部 asi,逐个拉取整份 sellers.json 备用。(买方如何把三份声明抓全、清洗、编译成可在竞价热路径上查询的索引,见下篇的工程章节。)

ads.txt 与 sellers.json 是方向相反、必须互相印证的一对:

ads.txt 与 sellers.json 的双向对账:左侧绿色面板是媒体 ads.txt(nytimes.com),列出三行——openx.com 540123 DIRECT、pubmatic.com 771 RESELLER、appnexus.com 3722 RESELLER;右侧蓝色面板是中介 sellers.json(openx.com),列出两条 entry——seller_id 540123(name NYT、域名 nytimes.com、类型 PUBLISHER)与 seller_id 889001(SomeReseller、INTERMEDIARY);一条绿色箭头把左侧 openx 540123 那行与右侧 seller_id 540123 的 entry 连起来标注「对账」;底部旁注强调两份声明方向相反必须互相印证,ads.txt 从媒体说 openx 的 540123 能直接卖我、sellers.json 从 openx 说 540123 就是 nytimes.com,对得上卖方身份才成立,任一对不上都是伪造或非法转售信号 同一个 540123,在媒体的 ads.txt 里被声明为「openx 可 DIRECT 卖我」,在 openx 的 sellers.json 里被声明为「就是 nytimes.com 这个 PUBLISHER」。两个方向对上,卖方身份才成立;若 sellers.json 里 540123 指向的是别的域名,或 ads.txt 里根本没这条,就是可疑路径。

一句话本质:ads.txt 是媒体侧的「谁能卖我」,sellers.json 是中介侧的「这个账号是谁」。单份声明易于造假,两份对向声明互相印证则难——这正是从「信任」走向「可核验」的核心手法。而把「实际经过了哪些中介」这条运行时路径接入、完成完整核验闭环,则须依靠下篇的 schain

五、常见误区 ↔ 正解

常见误区正解
ads.txt 能「阻止」欺诈它只提供可核验的授权清单;真正的拦截靠买方核验并拒绝。媒体部署了、买方不核验,等于无效
ads.txt 和 sellers.json 是一回事方向相反:ads.txt 媒体侧「谁能卖我」,sellers.json 中介侧「这个账号是谁」,二者对账才成立
app-ads.txt 就是把 ads.txt 换个名格式相同,但定位机制不同——靠 bundle→商店→开发者域名 反查,这层映射是工程难点
RESELLER 路径都是可疑的RESELLER 是合法授权的转售;可疑的是「对不上任何授权」的路径。只是买方通常更偏好更短的 DIRECT
App 没有 app-ads.txt 就有问题未必。订阅制 / 围墙花园(TikTok)/ 工具类本就不走开放程序化,没有很正常;只对参与开放转售的媒体才有意义
认证机构 ID(TAG-ID)是硬核验键它绑 asi、可选、且现实中抄错率高;核验主键是 (asi, sid),TAG-ID 只做辅助,不因它对不上而硬拒
部署了标准就 100% 干净覆盖不全、confidential、CTV 口径混乱、映射滞后——核验永远是尽力而为,用降权兜住灰色地带

程序化供应链的这三份声明,看似只是「两份文本文件加一个 JSON」,本质却是一次把信任换成核验的系统改造:媒体以 ads.txt/app-ads.txt 掌控「谁能代表我」,中介以 sellers.json 公开「系统内每个账号是谁」,买方将两个方向相互印证,域名伪造与非法转售中授权这一半便无处藏身。理解了那个反复出现的对账键 (asi, sid) 如何在两份声明中咬合,再进一步看 schain 如何把「实际路径」接入,便理解了整个 AdTech 透明度运动的缩影——将每一个「信任来的」环节,逐一换成「可核验的」环节


延伸阅读

一手资料与规范:


views
Share this post on:

Previous Post
供应链透明度(下):OpenRTB schain 用运行时逐跳记录做供应链核验
Next Post
Last Look 深挖:顺序二价下的偷看反超,与一价统一竞价如何堵住它