BidLint

Docs

Everything BidLint does, and how to read what it tells you.

What this is

app-ads.txt is a plain-text file that lists which ad networks are allowed to sell your app's inventory. Buyers check it before bidding. If it's missing, malformed, or lists an ID the network doesn't recognise, those bids get rejected — quietly, with no error anywhere you'd look.

BidLint does four things: validates the file against the IAB spec, monitors it so you hear when it breaks, cross-checks your publisher IDs against each network's sellers.json, and hosts the file if you don't have a website.

Checking a domain and using the builder need no account. An account is only for hosting, monitoring, and alerts.

Quick start

  1. 1Check a domain. Paste any domain on the home page. You get a grade, the parsed file, and every problem found — no sign-up.
  2. 2Build a file. The builder turns a checklist of ad networks plus your publisher IDs into a valid file. Copy or download it.
  3. 3Put it somewhere. Upload it to your own site at /app-ads.txt, or let us host it on a yourname.bidlint.e91.in subdomain.
  4. 4Point your store listing at it. This is the step people miss — see below.

Hosting your file

Your own domain

Serve the file at https://yourdomain.com/app-ads.txt. It must return HTTP 200 with Content-Type: text/plain — the spec says any other content type is an error, and strict crawlers will ignore the file entirely.

To turn on monitoring and alerts we need to know you control the domain. Add the DNS TXT record shown on your dashboard, then hit Verify.

Hosted by us

If you have no website, pick a subdomain and we serve the file for you. App stores accept a bidlint.e91.in subdomain as your developer website. Subdomains must be at least 4 characters, letters/numbers/hyphens only; some names are reserved.

The step most people miss. Crawlers find your app-ads.txt through the developer website on your app store listing. A perfect file on a domain your listing doesn't point to is invisible. We read the listing and flag it when the two disagree — it's the heaviest single penalty in the grade.

How the grade works

The grade is deterministic and the arithmetic is shown on every public report. Checks that don't apply are skipped and contribute to neither the score nor the total, so not knowing something never helps or hurts you.

CHECKPOINTSWHAT IT MEANS
Served over HTTP 20020The file is reachable at all.
Correct Content-Type10text/plain, as the spec requires.
Redirects within spec10At most one redirect off your domain.
Crawlers allowed by robots.txt10robots.txt doesn't block the file.
Every line parses20Scaled by the share of broken lines.
Publisher IDs verified20Cross-checked against sellers.json.
Store listing points here25The listing's developer website matches.
Actively monitored5On a schedule, with alerts.
OWNERDOMAIN declared5Optional, strengthens verification.

Letters: A 90+, B 75+, C 60+, D 40+, otherwise F.

Two things override the arithmetic. If the file is unreachable, a 404, or has no valid records, it's an F — there is no partial credit for a file nobody can read. And if we haven't cross-checked your publisher IDs yet, the grade is marked provisional and capped at B, because an A claims we verified something we haven't.

The sellers.json cross-check

Every network publishes a sellers.json listing who may sell through it. We fetch each one and compare it against your file. Whether your ID exists is only the first test — the relationship has to be consistent too, and that is what actually breaks a SupplyChain object downstream.

YOUR LINE SAYSTHEIR SELLERS.JSON SAYSRESULT
anyID absent❌ Missing
anyis_confidential: 1🔒 Confidential — normal, not a problem
DIRECTPUBLISHER or BOTH✅ Matched
RESELLERINTERMEDIARY or BOTH✅ Matched
DIRECTINTERMEDIARY⚠️ Mismatch — change the line to RESELLER
RESELLERPUBLISHER⚠️ Mismatch — change the line to DIRECT
DIRECTa different seller domain⚠️ Mismatch — ask them to update it

The domain comparison only applies to DIRECT lines. A RESELLER line authorises a third party to resell your inventory, so the network legitimately lists their domain, not yours.

We can't see live bid requests, so we can't validate a real SupplyChain object. What we can check are the conditions that make one valid — the cause rather than the symptom.

Monitoring & alerts

How often we re-check depends on what a domain is, not on a single global setting:

DOMAININTERVAL
Claimed and DNS-verifiedevery 6 hours
Claimed, not yet verifiedevery 24 hours
Hosted by usevery 24 hours, reachability only
Public check, unclaimedweekly, then stops if nobody looks at it

Most checks are nearly free: we send a conditional request, and a 304 Not Modified ends it there. You get an email when something newly breaks — once per day per domain at most, so a flapping file can't spam you.

API

The public endpoints need no authentication and no key.

ENDPOINTWHAT IT DOES
POST /api/checkCheck a domain. Body: {"domain": "example.com"}
GET /api/report/{host}The stored report for a domain.
GET /api/badge/{host}.svgAn embeddable grade badge.
GET /api/networksEvery ad network we know, and whether it's indexed.
GET /api/leaderboardRecently checked domains.

Anonymous checks are limited to 5 per minute and 30 per hour per IP. A domain checked in the last 15 minutes returns the cached report rather than re-crawling.

Embedding the badge

<a href="https://bidlint.e91.in/b/yourdomain.com">
  <img src="https://bidlint.e91.in/api/badge/yourdomain.com.svg"
       alt="BidLint app-ads.txt grade">
</a>

Self-hosting

BidLint is MIT licensed and runs on a single machine: Postgres, Redis, and six small processes behind Caddy.

git clone https://github.com/bidlint/bidlint
cd bidlint
cp .env.example .env    # Postgres password, Google OAuth, DNS token
make up

The wildcard certificate for *.yourdomain needs a DNS-01 challenge, so you'll need an API token for whoever hosts your DNS — HTTP-01 cannot issue wildcards. Full detail is in the repository README.

Something unclear or wrong? Open an issue.