DEV Community

Aleksander Sekowski
Aleksander Sekowski

Posted on

hp=0 in SupplyChain 1.1 Marks Custody Hops. ads.txt Checks on Them False-Fail.

A CTV bid request arrives with a three-node source.schain. The middle node names an SSAI platform the buyer has never heard of. Your supply-path optimizer flags it: sid not in sellers.json, ads.txt lookup fails, chain rejected.

The partner adopted SupplyChain 1.1 early. That middle node carries hp: 0. It was never supposed to pass ads.txt. Your reader treats it like a payment hop because that is what six years of 1.0 chains trained everyone to assume.

What hp was supposed to mean

The hp field on a SupplyChainNode is an integer: 1 if this node is involved in the flow of payment, 0 if not. Required on every node.

SupplyChain 1.0 then adds the sentence that shaped every implementation since: for version 1.0, this property should always be 1. The flag became a boolean with one legal value. Builders hardcoded hp: 1. Validators asserted it. Buy-side audit walked every node against sellers.json and ads.txt because, in a 1.0 chain, every node is a seller account.

The result is a chain that describes money and nothing else. A request that crossed a header-bidding wrapper, a publisher ad server, an SSAI stitcher, and two SDKs might disclose two nodes. Both are payment nodes. Four systems took custody of the payload. None of them are named.

SupplyChain 1.1 (IAB Tech Lab public comment, June 2026) keeps the same nodes array and lets custody-taking systems append themselves with hp: 0. Prebid, ad servers, SSAI platforms, SDKs: the plumbing everybody runs and nobody currently discloses.

One clause in the proposal matters for anyone running authorization checks: hp=0 nodes are not expected to pass ads.txt validation. Referencing a sellers.json entry for a non-payment handler is strongly recommended, not required. A custody node is disclosure, not an authorization claim.

The chain before and after

A publisher ad server, an SSAI platform, and an exchange on the same impression:

"source": {
  "schain": {
    "ver": "1.0",
    "complete": 1,
    "nodes": [
      { "asi": "pubadserver.example", "sid": "8842", "hp": 1 },
      { "asi": "exchange.example", "sid": "pub-42", "hp": 1 }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Same path under 1.1:

"source": {
  "schain": {
    "ver": "1.1",
    "complete": 1,
    "nodes": [
      { "asi": "pubadserver.example", "sid": "8842", "hp": 1 },
      { "asi": "ssai.example", "sid": "net-19", "hp": 0 },
      { "asi": "exchange.example", "sid": "pub-42", "hp": 1 }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Three consequences fall out immediately. A supply-path model that scores by chain length now sees a longer path for the same inventory and will penalize the more honest disclosure unless it filters on hp first. Duplicate bid detection gets better: the same request arriving through two routes now carries evidence of which systems it crossed. An authorization check that walks all three nodes against ads.txt finds one that was never supposed to be there.

That third outcome is the bug I keep seeing in production readers, not in the payload.

What your validator actually checks today

Under SupplyChain 1.0 rules, a node with hp: 0 triggers openrtb.schain.node.hp_unexpected:

$ rtblint validate bid-request.json
warning  openrtb.schain.node.hp_unexpected
         hp is 0; version 1.0 of the SupplyChain object expects every node
         on the declared path to be marked as part of the payment flow
         with hp set to 1.
         path: source.schain.nodes[0].hp
Enter fullscreen mode Exit fullscreen mode

That warning is correct for a chain that declares ver: "1.0". It is the wrong verdict for a chain that declares ver: "1.1" and uses hp as real data.

The structural checks survive the migration: asi and sid required, adjacent duplicate nodes still fail (openrtb.schain.duplicate_node), complete still means something (though new enumerations are proposed, so read the final text before branching on it). What breaks is anything that assumed every node resolves in sellers.json or passes ads.txt.

When you run resolution with a local cache, RTBlint skips sellers.json lookup on hp=0 nodes entirely. Only payment hops get openrtb.resolve.sid_not_in_sellers. That matches the spec intent: authorization applies to the payment path, not to custody disclosure.

How you catch it before a partner ships 1.1

Paste the bid request into the OpenRTB tester. It flags structural schain issues with the JSON path attached, including chains left in the pre-2.6 source.ext.schain location (openrtb.field.moved) and misspelled node fields (openrtb.field.undefined).

For a corpus of what your stack actually emits:

$ rtblint validate --resolve --cache ./trust-cache/ ./outbound/*.json
Enter fullscreen mode Exit fullscreen mode

The same checks run as MCP tools an agent can call on its own output, and over gRPC on openadtech.rtblint.v1. Agentic stacks (AAMP buyer agents, ARTF hosts) still end the hop as an OpenRTB bid request. The agent handshake succeeding does not mean source.schain matches the reader on the other side.

Grep your codebase for the habits that will false-fail honest 1.1 chains:

hp.*[:=]\s*1          # hardcoded payment claims
"1\.0"                # ver comparisons that silently drop 1.1
schain.*length        # hop-count policies without an hp filter
adsTxt|ads_txt        # authorization scoped chain-wide
Enter fullscreen mode Exit fullscreen mode

Every hit is a decision, not a mechanical find-and-replace.

Where to go next

The SupplyChain 1.1 analysis walks the check-by-check split between 1.0 assumptions and 1.1 semantics: which rules survive, which ones were wearing a 1.0 assumption as clothing.

The migration checklist separates sell-side builder steps from buy-side reader steps. If you read chains for SPO or verification, the reader half is the one that prevents manufactured violations on hp=0 nodes.

Field definitions and the 2.5 vs 2.6 placement (source.schain vs source.ext.schain) live in the schain reference. Overclaiming completeness is its own failure mode: openrtb.schain.incomplete fires when complete: 1 does not match what the nodes actually disclose.

For how schain sits beside ads.txt, sellers.json, and the newer trust artifacts, the supply chain trust stack guide maps which file answers which question. None of them cross-check each other automatically; schain records the path, the other files authorize it.

The short version

hp=0 is not a broken seller ID. It is a custody hop that never touches payment. Running ads.txt or mandatory sellers.json resolution on those nodes rejects good supply. Split hop counting from money counting. Accept ver: "1.1" or reject it loudly. The silent third option, dropping an unrecognized version and proceeding without the chain, is worse than either.

RTBlint is independent of IAB Tech Lab. The rule ids above cite the SupplyChain object spec and the 1.1 public comment proposal because that is where the requirements live.

Top comments (0)