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.
Quick start
- 1Check a domain. Paste any domain on the home page. You get a grade, the parsed file, and every problem found — no sign-up.
- 2Build a file. The builder turns a checklist of ad networks plus your publisher IDs into a valid file. Copy or download it.
- 3Put it somewhere. Upload it to your own site at
/app-ads.txt, or let us host it on ayourname.bidlint.e91.insubdomain. - 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.
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.
| CHECK | POINTS | WHAT IT MEANS |
|---|---|---|
| Served over HTTP 200 | 20 | The file is reachable at all. |
| Correct Content-Type | 10 | text/plain, as the spec requires. |
| Redirects within spec | 10 | At most one redirect off your domain. |
| Crawlers allowed by robots.txt | 10 | robots.txt doesn't block the file. |
| Every line parses | 20 | Scaled by the share of broken lines. |
| Publisher IDs verified | 20 | Cross-checked against sellers.json. |
| Store listing points here | 25 | The listing's developer website matches. |
| Actively monitored | 5 | On a schedule, with alerts. |
| OWNERDOMAIN declared | 5 | Optional, 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 SAYS | THEIR SELLERS.JSON SAYS | RESULT |
|---|---|---|
| any | ID absent | ❌ Missing |
| any | is_confidential: 1 | 🔒 Confidential — normal, not a problem |
| DIRECT | PUBLISHER or BOTH | ✅ Matched |
| RESELLER | INTERMEDIARY or BOTH | ✅ Matched |
| DIRECT | INTERMEDIARY | ⚠️ Mismatch — change the line to RESELLER |
| RESELLER | PUBLISHER | ⚠️ Mismatch — change the line to DIRECT |
| DIRECT | a 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.
Monitoring & alerts
How often we re-check depends on what a domain is, not on a single global setting:
| DOMAIN | INTERVAL |
|---|---|
| Claimed and DNS-verified | every 6 hours |
| Claimed, not yet verified | every 24 hours |
| Hosted by us | every 24 hours, reachability only |
| Public check, unclaimed | weekly, 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.
| ENDPOINT | WHAT IT DOES |
|---|---|
| POST /api/check | Check a domain. Body: {"domain": "example.com"} |
| GET /api/report/{host} | The stored report for a domain. |
| GET /api/badge/{host}.svg | An embeddable grade badge. |
| GET /api/networks | Every ad network we know, and whether it's indexed. |
| GET /api/leaderboard | Recently 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.