Blog

ads.txt, app-ads.txt and sellers.json Validation: A Guide for Exchange Operators

ads.txt, app-ads.txt and sellers.json from the consuming side: crawl schedules, parse edge cases, schain validation and what these files cannot prove.

19 min read By the Floxis engineering team

Four checks on an inbound request, three spoof shapes caught and one that passes all four

Nearly everything written about ads.txt and sellers.json is addressed to the publisher: put a text file on your site, list your sellers, done. Almost nothing is written for the party who has to consume and enforce these files — the operator who decides what a fetch failure means, how a malformed line parses, whether an incomplete supply chain clears, and what their own sellers.json admits. The publisher’s job takes an afternoon. Yours is an always-on system with real failure modes.

Our ad fraud prevention guide places authorized-seller checks at Gate 1 of four — the cheapest place to refuse a fraudulent request. This article is the machinery behind that gate: what each file actually asserts, how to crawl and parse them, how to validate an inbound supply chain object, and what to publish yourself. It closes with one bid request walked through every check, showing where a spoof is caught — and where it is not.

Table of Contents

What each file asserts, and what it does not

The three standards form a triangle, and each side carries a narrow assertion. Enforcement goes wrong when an operator treats any of them as asserting more.

File Published by What it asserts What it cannot assert
ads.txt / app-ads.txt The publisher or app developer “Account 12345 on system X may sell inventory of this domain or app” That a given bid request genuinely originates from this domain or app
sellers.json Each advertising system seller_id 12345 on my system is this legal entity — a publisher, an intermediary, or both” That the entity’s traffic is real, or that the entity is honest
SupplyChain object (schain) Every reseller, in the request itself “This request passed through these accounts, in this order” Anything — it is self-reported until cross-checked against the other two

Joined together, the three prove something genuinely useful: that the path the request claims is authorized for the declared domain, hop by hop, each hop resolving to a named legal entity. That eliminates the crude spoofing that made these standards necessary — an unknown seat stamping premium domains onto junk requests.

Now the limitation, stated as plainly as it deserves: ads.txt validates that a seller is authorized for the declared domain — it cannot detect that the declaration itself is a lie. A spoofer riding an authorized reseller’s real seller ID passes every check in this article, because every file agrees the path is allowed. The app-ads.txt specification concedes as much in its limitations section: these standards cannot stop a platform from misrepresenting an unauthorized publisher ID as an authorized one. Only render-side measurement or log-level correlation with the publisher’s own records exposes that lie — Gates 3 and 4 in the fraud guide’s terms. Run these checks because they turn spoofing from an open door into an edge case; measure downstream because an edge case is not zero.

DIRECT vs RESELLER: why one word matters commercially

The ads.txt 1.1 specification defines field #3 precisely. DIRECT asserts the publisher — the content owner — directly controls the account in field #2 on the system in field #1, usually meaning a direct contract. RESELLER asserts the publisher authorized another entity to control that account and resell through that system. The field is case-insensitive when you read it.

The distinction is commercial before it is technical. DIRECT means money flows from the named system to the publisher; RESELLER means at least one more hop and one more margin in between. Buyers running supply path optimization increasingly bid the direct path and prune the rest — so the DIRECT/RESELLER mix through your exchange is your demand’s quality perception of you. It is also where spoofers hide: a large reseller’s seller ID appears in thousands of ads.txt files, far better camouflage than a seat that appears in three.

For a validator, the field earns its keep in coherence checks. A DIRECT line should correspond to a sellers.json entry of type PUBLISHER (or BOTH) and, in a complete schain, to the first node; a RESELLER line to an INTERMEDIARY or BOTH entry at a later hop. These are conventions the specs encourage, not hard rules — treat a mismatch as a flag to score, not an automatic refusal. Field #4, the certification authority ID, is optional and superseded by sellers.json’s identifiers; read it if present, require it never.

Crawling in practice: schedules, redirects and caching

The auction path must never fetch anything — authorization lookups hit in-memory data, as the RTB exchange setup guide lays out stage by stage. The crawler is an offline system feeding a lookup table, and the spec is unusually concrete about its behavior.

  • Where to fetch. /ads.txt at the root domain — public suffix plus one label, derived with the Public Suffix List, never naive dot-splitting. Crawl subdomains only when the root file declares them with subdomain=. Prefer HTTPS.
  • Content type is a gate. ads.txt must be served as text/plain; any other content type is an error and the content is ignored. That one rule blocks the web’s commonest false positive — a server returning its HTML error page with a 200. Same story for sellers.json with application/json.
  • Redirects. Follow 301, 302 and 307. Within the original root domain, any number is fine. Off the root domain, exactly one hop is allowed — the spec’s mechanism for delegating the file to a third-party host — and if that location redirects again, the fetch is an error.
  • Caching and schedule. Honor cache-control and Expires; absent both, the spec’s default expiry is seven days. Treat that as a ceiling, not a target: a lapsed entry is indistinguishable from a spoof at bid time, so re-crawl active supply far more often than the idle tail. Detect change cheaply via Last-Modified plus a checksum of the prior contents.
  • Scope. Crawl the domains and apps in your bid stream, not the web. Your request logs are the crawl frontier.
  • Size. The spec sets no limit; real files range from three lines to tens of thousands. Cap generously, but never silently truncate — a truncated ads.txt is missing authorizations, so truncation manufactures false refusals. Oversize or obviously corrupted content means ignoring the whole file and keeping the last good copy, never enforcing against half of it.

What should your exchange do when an ads.txt fetch fails?

Failing open versus failing closed is a real decision, and the specs only decide part of it. The baseline: a 404 on ads.txt means no declarations exist — no policy, nothing to enforce, no seller can be called unauthorized. A 401 means seek contact with the site. Any other HTTP error where you previously found data means keep using the last successfully retrieved data set. sellers.json is stricter in your favor: on a 404 or any other error, use the last successful data. Timeouts are not HTTP responses at all; the specs are silent, so that policy is yours.

A workable policy distinguishes three states. Never had a file: you cannot enforce authorization, so the domain competes on your other signals — invalid-traffic scoring, floors, outcome metrics. Refusing every domain without an ads.txt fails closed on a long tail of legitimate sites and is usually commercial self-harm; running them unenforced and saying so in reporting is honest. Had a file, now unreachable: serve last-known-good with a staleness ceiling, retry with backoff, and alert when a high-volume domain goes stale — then degrade its score rather than pretending week-old authorizations are current. Had a file, now a clean 404: the spec says treat it as no declarations, but act on a confirmation re-crawl rather than one observation — publishers break their own sites constantly. Whatever you choose, instrument every refusal with its reason: “blocked, seller not in ads.txt” and “unenforceable, no ads.txt present” are different facts, and the distinction is the conversation the day a partner asks why volume dropped.

Parse edge cases a validator must survive

The format is deliberately forgiving to publishers, so the burden of interpretation lands on you. All of the following comes straight from the spec, and all of it appears in the wild.

  • Comments. # starts a comment anywhere on a line, including after a record.
  • Whitespace and line endings. Ignore spaces and tabs around fields; accept CR, LF and CRLF in any mixture.
  • Duplicates. Not forbidden — deduplicate. A pair listed as both DIRECT and RESELLER is authorized either way; flag it as publisher hygiene, not failure.
  • Account IDs. Field #2 can be a string or an integer, and the spec is silent on case. Compare exactly; log near-misses so integration bugs surface as reports, not silent refusals.
  • Extension fields. Anything after a ; is an extension — preserve it, break on none of it.
  • Variables. Any VARIABLE=VALUE line is a declaration. Store the ones you do not recognize. Repeated variables are all stored — except OWNERDOMAIN, where only the first counts.
  • The empty file. A file with no seller records must carry the placeholder record (placeholder.example.com, placeholder, DIRECT, placeholder); a bare empty file was deprecated as a signal in 2020.
  • Corruption. Obviously malformed content means ignoring the entire file — which, with last-known-good, makes a publisher’s botched deploy cost them nothing for a cycle.

Two variables added in ads.txt 1.1 make cross-file joins possible. OWNERDOMAIN names the entity owning the domain; it should equal the domain on that publisher’s sellers.json entries, and — the load-bearing part — the first node of a complete schain should resolve to a sellers.json entry matching it. Absent, assume the domain itself. MANAGERDOMAIN names an exclusive monetization partner, optionally per country and at most one per country; inventory the manager monetizes is expected to show the manager as the originating node. INVENTORYPARTNERDOMAIN delegates authorization to another party’s ads.txt — follow it exactly one hop, ignore the directive if found in the referenced file, and note it leads to ads.txt even when discovered in an app-ads.txt.

app-ads.txt: discovery runs through the store listing

For web inventory, the declared domain tells you where to crawl. For apps it does not — com.example.game is not a hostname, and inventing one from it was an early source of spoofable garbage. The app-ads.txt specification routes discovery through the app store listing, and every link in the chain is a check.

  1. Start from app.storeurl in the bid request — required for inventory to be treated as authorized under the spec. An app request with no store URL is an app you cannot verify.
  2. Crawl the store listing page, which participating stores annotate with three meta tags: appstore:developer_url, appstore:bundle_id, appstore:store_id. The listing’s IDs cross-check the IDs declared in the request — a spoofed bundle riding a real store URL fails here.
  3. Canonicalize the developer URL: take the hostname, keep at most the first two labels before the public suffix, strip a leading www. or m., append /app-ads.txt, try HTTPS then HTTP. If the file is absent on a subdomain, fall back to its parent.
  4. Parse as ads.txt, with one exception: the subdomain= directive is unused in app-ads.txt and must be ignored.

The spec’s operational guidance is specific: crawl a given store listing no more than weekly, honor the store’s robots.txt, and only crawl listings for apps actually sending you traffic. Because one app has unboundedly many store URL variants, index by store domain plus bundle or store ID — itunes.apple.com:343200656 → rovio.com is the spec’s own example — never by full URL. When a developer moves domains, the spec suggests the old file stay up around thirty days; mirror that tolerance rather than flapping an app’s authorization on moving day.

Publishing your own sellers.json

You are not only a consumer. Every system named in an ads.txt file or an schain node is expected to publish /sellers.json at its root domain — and as an exchange you are such a system, judged by buyers’ crawlers on the standards you apply upstream. The sellers.json specification keeps the shape short: required version ("1.0") and sellers array; optional contact_email, contact_address, and an identifiers array for your TAG ID or DUNS number.

The rules that matter operationally:

  • Every seller you represent must be listed — even confidential ones. Omitting a seller is not an option the spec offers; hiding their identity is.
  • One legal entity per seller_id, ever. Entities may hold several IDs — the spec’s own sample splits one company across regional IDs, disambiguated with the comment field — but never the reverse.
  • seller_type is the graph’s edge label. PUBLISHER: inventory on properties the entity owns, paid directly by you. INTERMEDIARY: not owned, or not paid directly. BOTH: both under one account. As an operator running managed supply and reselling other exchanges’ supply, either mark accounts BOTH or — cleaner for buyers — split the business lines into separate seller IDs.
  • For INTERMEDIARY and BOTH entries, domain must be the root domain of that seller’s own sellers.json. This pointer lets a buyer walk the graph from your file into your upstream’s. Break it and your paths dead-end.

Confidential entries are the judgment call. is_confidential: 1 lets you omit name and domain, and legitimate reasons exist — sensitive terms, an unannounced relationship. But a confidential row is a dead end for every buyer crawler that reaches it, and sophisticated demand discounts what it cannot resolve. Use it sparingly: a mostly-anonymous sellers.json reads as an exchange with something to hide.

Validating the schain on inbound requests

The SupplyChain object is where the static files meet the live request. Find it at source.schain in OpenRTB 2.6 and later, source.ext.schain in 2.5 — accept both — and as a serialized schain= parameter on tag and VAST endpoints, per the spec’s serialization appendix. Validation runs in layers, cheapest first.

Structural. complete, nodes and ver are required on the object; asi, sid and hp on every node, with rid, name and domain optional (name/domain should be absent when the seller exists in the system’s sellers.json). In version 1.0, hp must be 1 on every node; asi values are bare root domains, and sid should stay within 64 characters. Empty nodes, missing ver, hp: 0 — all malformed, and a malformed chain from a direct partner is an integration bug to raise, not a shape to tolerate.

Ordering and transport. In a complete chain the first node is the originating system and seller — the owner’s side of the inventory. The last node is, per the spec, the entity sending this bid request. That yields the most underused check in the ecosystem: the last node’s asi must be the system domain of the integration the request physically arrived on, and its sid must match the request’s publisher.id. A fabricated chain is usually a perfect copy of a real one — perfect except that it arrived on the wrong wire. Validate the chain against the connection, not just the files.

Cross-file. Every node’s (asi, sid) pair should appear in the declared domain’s ads.txt or app-ads.txt — the first hop typically DIRECT, later hops RESELLER — and resolve in that system’s sellers.json to an entry whose seller_type fits its position. In a complete chain, the first node’s sellers.json domain should match the site’s OWNERDOMAIN. Checking only the asi and ignoring the sid is the common shortcut, and exactly the gap a fraudster with their own account inside an authorized reseller walks through.

Your own conduct. Reselling onward permits exactly one operation: copy the inbound chain unchanged, preserve complete, append your node — your domain as asi, the seller account you pay as sid. If the inbound request had no schain, create one with complete: 0 and your node alone. Forwarding a chain without inserting yourself is explicitly invalid; so is repairing upstream nodes, reordering, or promoting complete from 0 to 1. Your standing in every buyer’s validator depends on never editing history.

Is an incomplete supply chain a reason to drop the request?

It is tempting to treat complete: 0 as an admission of guilt, and worth resisting with the spec open. An incomplete chain is the honest state when an upstream hop does not support the SupplyChain object: the spec instructs a reseller receiving schain-less inventory to create the object, set complete to 0, and add only its own node. A hard rule dropping every incomplete chain punishes the partner who told the truth — while rewarding the one who quietly stamps complete: 1 on a chain they cannot vouch for. Fabricated completeness is partially testable: a genuinely complete chain’s first node should resolve, via its system’s sellers.json, to a PUBLISHER-type entry matching the declared site’s OWNERDOMAIN. A “complete” chain whose first node resolves to an intermediary — or to nothing — is a far stronger negative signal than complete: 0 ever was.

The workable policy is economic rather than binary. Route and price incomplete chains as what they are — less verifiable inventory much of your demand will discount, since buyers’ validators run these same checks. Require completeness contractually where you have leverage: a direct SSP integration has no excuse for complete: 0 on owned inventory. And measure the refusal path: if you decline incomplete chains for some demand, count the drops by partner and reason — a partner deserves to hear “volume will fall when we tighten schain policy” before it happens, with data.

A worked example: one bid request, every check in order

Your exchange — call it yourexchange.com — has a server-to-server integration with the SSP midssp.com. A request arrives on that integration:

{
  "id": "req-8842",
  "site": {
    "domain": "gazettedaily.com",
    "publisher": { "id": "7731" }
  },
  "source": {
    "schain": {
      "ver": "1.0",
      "complete": 1,
      "nodes": [
        { "asi": "firstssp.com", "sid": "88231", "hp": 1 },
        { "asi": "midssp.com",   "sid": "7731",  "hp": 1 }
      ]
    }
  }
}

The claim: Gazette Daily’s inventory, sold through its direct system firstssp.com under account 88231, resold by midssp.com under account 7731. The checks, in pipeline order:

  1. Structuralver, complete, nodes present; both nodes carry asi, sid, hp: 1; domains are bare roots. Pass.
  2. Transport — the request arrived on the midssp.com integration; the last node’s asi is midssp.com and its sid 7731 equals publisher.id. The chain matches the wire. Pass.
  3. ads.txt — your crawler’s current copy of gazettedaily.com/ads.txt contains firstssp.com, 88231, DIRECT and midssp.com, 7731, RESELLER. Every hop’s pair is authorized for the declared domain. Pass.
  4. sellers.jsonfirstssp.com/sellers.json resolves 88231 to Gazette Daily Media, type PUBLISHER, domain: gazettedaily.com, matching OWNERDOMAIN. midssp.com/sellers.json resolves 7731 to FirstSSP Inc, type INTERMEDIARY, domain: firstssp.com — where firstssp’s own sellers.json lives, so the graph walks end to end. Pass.
  5. Coherence — DIRECT line ↔ PUBLISHER first node; RESELLER line ↔ INTERMEDIARY hop; complete: 1 anchored on a publisher-owned account. Pass. The request proceeds to the auction.

Now the spoofs. Variant A — crude: a fraudster’s own seat declares gazettedaily.com with its own node (shadyssp.com, 4412). No such line in Gazette’s ads.txt; refused at check 3. Variant B — the copied chain: the fraudster fabricates the exact two-node chain above but sends it over their own integration. Checks 3 and 4 pass — the files really do authorize that path — but check 2 fails: the last node claims midssp.com and the request arrived on a different wire. This variant gets through validators that check files but not transport. Variant C — the account inside the reseller: the fraudster opens a real account 9912 at midssp and sends spoofed Gazette traffic through it. Chain and transport are genuine — but midssp.com, 9912 appears nowhere in Gazette’s ads.txt, so check 3 refuses it, provided you validate the seller ID and not merely the domain. Variant D — the one that slips through: traffic mislabeled as gazettedaily.com inside the authorized accounts — upstream of firstssp, or by a compromised or dishonest party along the legitimate path, riding the real 88231/7731 IDs. Every check passes, because every file truthfully says this path is authorized; what is false is the impression itself, and no published file can see that. Catching it takes render-side verification or log-level correlation against the publisher’s own delivery records — which is why the fraud guide insists Gate 1 is necessary and nowhere near sufficient.

How validation data feeds supply path optimization

Everything your validator touches is also the raw material of supply path optimization — one dataset, two read patterns. Each validated request contributes an observation: this domain reaches me through this sequence of (asi, sid) hops. Aggregate the observations and the questions answer themselves. How many distinct paths deliver the same publisher’s inventory? Which are one hop and DIRECT-anchored, and which wander through three intermediaries — each an unauditable margin and another place for a declaration to change? Which resellers add unique supply, and which duplicate paths you already have shorter? Cutting redundant long paths improves your economics and your fraud posture at once — and your buyers’ SPO tooling is attempting the same pruning from outside, with less data than you hold.

The mirror image matters as much: your published sellers.json, and the nodes you append, are inputs to other people’s SPO. Buyers walking the graph decide whether paths through your exchange look short, resolvable and honestly labeled — or opaque. Stable seller IDs, resolvable domains on every intermediary entry, sparing confidentiality and chains you never edit are not compliance chores; they are how you stay on the preferred side of demand’s pruning decisions.

The practical obstacle to all of this is not difficulty. It is that authorization drift is invisible and never urgent. Nothing pages you when a publisher removes your line or a seller ID stops resolving. The inventory keeps arriving, keeps being offered, and keeps looking entirely normal — right up until a buyer’s supply-path team asks a question you cannot answer. The only real defence is checking on a schedule, which is precisely the kind of task that slides in a busy quarter. So the thing worth having is not a better report; it is a lower cost of asking. When you can simply ask what you are currently carrying that is not authorized to be sold, the audit stops being a project you schedule and becomes a question you ask on a Tuesday.

Key takeaways

Point Details
Validate the declaration, then distrust it ads.txt proves a seller is authorized for the declared domain, never that the declaration is true — pair Gate 1 with render-side or log-level measurement
Check the sid, not just the domain A fraudster with an account inside an authorized reseller passes every domain-only check; the seller ID catches them
Validate the chain against the wire The last schain node must match the integration the request arrived on and its publisher.id — copied chains fail here and almost nowhere else
Fetch failures have defined semantics ads.txt 404 = no declarations; other errors = last-known-good; sellers.json errors = always last-known-good; timeouts are your policy — make it explicit
Parse liberally, enforce exactly Comments, whitespace, duplicates and unknown variables must not break you; malformed files are ignored whole, never half-enforced
Incomplete beats fabricated complete: 0 is the honest state for schain-less upstream supply; price it, require completeness contractually, test “complete” claims against the first node
Your own files are the other half Publish a sellers.json buyers can walk — one entity per ID, resolvable intermediary domains — and never edit an inbound chain beyond appending your node

Validation built into Floxis

Everything above is work your exchange must do on every request, against data that changes daily — which is why Floxis, the white-label RTB exchange you run under your own brand, builds it into the platform. A built-in ads.txt crawler keeps the authorized-seller data you enforce against current rather than imported-last-quarter. sellers.json and full supply-chain (schain) validation run on the request path, with supply-path transparency and SPO analysis over the paths your traffic actually takes. Allow and block lists apply across domain, bundle, publisher, creative ID, IP, device and ad markup, so every policy decision in this guide has an enforcement dimension waiting for it. And because every request, bid, win and drop lands in an exportable log-level ledger with its reason attached, the day a partner asks why volume moved, the answer is a query rather than a negotiation.

To see the validation pipeline against your own supply mix, request a technical walkthrough.

Sources and specifications