Skip to content

Latest commit

 

History

147 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

self-adint — reading your own advertising profile from the demand side

The question. When an ad is auctioned for your phone, the bid request that goes out describes you — segments, identity graphs, consent state, location precision. Buyers see that description for free, before anyone pays for anything. So: what does the demand side already know about one specific person's device, and can that person read it himself?

The thesis is not "what would my impressions cost". It is that the audience profile arrives inside the bid request. You do not have to buy the impression to read what came with it.

One operator, one phone, his own data. Nobody else is observed — not incidentally, not as a "noise statistic". That constraint shapes the code, not just the intentions: see The rules the code is built to, below.


Where it stands, with the numbers that qualify it

Two threads run in parallel. docs/INDEX.md is the map; start there if you want to navigate rather than read.

Thread A — measurement. Which exchanges actually sell this device's traffic, and what is observable from where. This is where the current work is.

Thread B — access. What a person can legally demand from an exchange, and what it takes for a small buyer to get a seat.

The current measurement, stated with its size before its content

The live work is a study of Russian header bidding, chosen because the western literature on the subject covers US/EU inventory and does not contain the Russian SSPs at all — Yandex, adriver, buzzoola, betweendigital, sape, smi2, MyTarget/VK, astralab, gnezdo, hybrid, bidvol, alfasense are visible by name in our own capture and absent from the papers.

Generated 2026-08-21 12:56 UTC by tools/adint-status from the files on disk.

The sampling frame — and its rejections, which are half the result

The study runs on the ru-mobile frame, declared in ref/CANONICAL-FRAME. Which vantage a frame is built from changes its membership, so this is a choice with a reason, recorded in that file.

Vantage ru-mobilecanonical · 208 domains touched in rank order · 3 duplicate row(s) collapsed · browser egress 91.78.80.171, 91.79.81.62 · frame-stageb-ru-mobile-2026-08-19-schema3.jsonl

verdict n what it means
admit 14 in the frame — 200, HTML, and an AUCTION config: a bidder roster or a prebid global
ad-serving-only 18 Adfox/Yandex ad code present, no auction config readable — this publisher sells ads, but not by asking several bidders in the page
no-wrapper 24 page loaded, no Adfox and no prebid (re-probed at the full 45 s window)
ya-generic-only 63 a Yandex namespace but no advertising global — Metrika, not ad-tech
blocked 23 4xx/5xx — we never saw the page, so this says nothing about its wrapper
rate-limited 1 428/429 — caused by our own load, not by the site
unreachable 20 timeout or refused connection — our blindness
no-web-apex 45 DNS/TLS failure — the domain serves no page at its apex at all

Admitted: magnit.ru, pikabu.ru, rbc.ru, gismeteo.ru, hh.ru, ria.ru, iz.ru, matchtv.ru, kp.ru, aif.ru, mk.ru, sports.ru, aviasales.ru, interfax.ru.

Vantage nl-direct · 254 domains touched in rank order · browser egress 77.246.104.228 · frame-stageb-nl-direct-2026-08-19-schema3.jsonl

verdict n what it means
admit 13 in the frame — 200, HTML, and an AUCTION config: a bidder roster or a prebid global
ad-serving-only 20 Adfox/Yandex ad code present, no auction config readable — this publisher sells ads, but not by asking several bidders in the page
no-wrapper 33 page loaded, no Adfox and no prebid (re-probed at the full 45 s window)
ya-generic-only 81 a Yandex namespace but no advertising global — Metrika, not ad-tech
blocked 33 4xx/5xx — we never saw the page, so this says nothing about its wrapper
rate-limited 1 428/429 — caused by our own load, not by the site
unreachable 19 timeout or refused connection — our blindness
no-web-apex 54 DNS/TLS failure — the domain serves no page at its apex at all

Admitted: magnit.ru, pikabu.ru, ria.ru, lenta.ru, iz.ru, matchtv.ru, kp.ru, aif.ru, mk.ru, sports.ru, liveinternet.ru, sportbox.ru, playground.ru.

Captured cells

Three counts, because they are three different facts. this arm is what this arm attempted. both arms adds the other arm's load — they differ exactly when one arm refused a load the other recorded. readable additionally requires that BOTH arms saw the page, and a pair with an unseen half carries no RU-versus-NL contrast at all. Only the last number is a comparison. The two ways of not seeing are kept apart: refused is a 4xx/5xx — the publisher's server answered and declined THIS vantage — while no answer is a navigation that never returned a response at all, which is our own link and says nothing about the site.

cell arm loads refused no answer sites this arm both arms readable window hours UTC vantage verified
2026-08-19-day-2026081914 nl-direct 14 2 0 14 14 10 3 45 s 14,15 14/14
2026-08-19-day-2026081914 ru-mobile 10 0 6 10 10 10 3 45 s 14 7/10
2026-08-19-evening-2026081915 nl-direct 56 8 4 14 56 56 27 45 s 15,16,17,18 56/56
2026-08-19-evening-2026081915 ru-mobile 56 0 26 14 56 56 27 45 s 15,16,17,18 50/56
2026-08-19-evening-2026081919 nl-direct 14 2 2 14 14 14 9 45 s 19,20 14/14
2026-08-19-evening-2026081919 ru-mobile 14 0 5 14 14 14 9 45 s 19,20 12/14
2026-08-19-morning-2026081906 nl-direct 6 0 0 2 6 6 5 45 s 6 6/6
2026-08-19-morning-2026081906 ru-mobile 6 0 1 2 6 6 5 45 s 6 6/6
2026-08-19-night-2026081921 nl-direct 46 9 7 14 46 44 13 45 s 21,22,23 46/46
2026-08-19-night-2026081921 ru-mobile 48 0 23 14 48 44 13 45 s 21,22,23 41/48
2026-08-20-evening-2026082019 us-exit 14 1 0 14 14 14 13 45 s 19,20 14/14
2026-08-20-evening-2026082019 ru-mobile 14 0 0 14 14 14 13 45 s 19,20 14/14
2026-08-20-evening-2026082020 us-exit 18 2 1 13 18 10 9 45 s 20,21 18/18
2026-08-20-evening-2026082020 ru-mobile 11 0 0 11 11 10 9 45 s 20 11/11
2026-08-20-night-2026082021 us-exit 25 3 1 14 25 15 9 45 s 21,22 25/25
2026-08-20-night-2026082021 ru-mobile 15 0 5 14 15 15 9 45 s 21,22 15/15
2026-08-20-night-2026082022 us-exit 28 4 1 14 28 28 15 45 s 22,23 27/28
2026-08-20-night-2026082022 ru-mobile 28 0 10 14 28 28 15 45 s 22,23 28/28
2026-08-20-night-2026082023 us-exit 28 4 1 14 28 28 15 45 s 0,23 28/28
2026-08-20-night-2026082023 ru-mobile 28 0 10 14 28 28 15 45 s 0,23 28/28
2026-08-21-day-2026082110 nl-direct 26 3 2 14 26 26 21 45 s 10,11 25/26
2026-08-21-day-2026082110 ru-mobile 28 0 1 14 28 26 21 45 s 10,11 27/28
2026-08-21-day-2026082112 nl-direct 5 1 0 5 5 5 4 45 s 12 5/5
2026-08-21-day-2026082112 ru-mobile 28 0 1 14 28 5 4 45 s 12 27/28
2026-08-21-morning-2026082103 nl-direct 27 5 0 14 27 27 14 45 s 3,4 27/27
2026-08-21-morning-2026082103 ru-mobile 28 0 10 14 28 27 14 45 s 3,4 28/28
2026-08-21-morning-2026082104 nl-direct 28 4 0 14 28 28 17 45 s 4,5 28/28
2026-08-21-morning-2026082104 ru-mobile 28 0 10 14 28 28 17 45 s 4,5 28/28
2026-08-21-morning-2026082106 nl-direct 28 5 0 14 28 28 16 45 s 6 28/28
2026-08-21-morning-2026082106 ru-mobile 28 0 10 14 28 28 16 45 s 6 28/28
2026-08-21-morning-2026082108 nl-direct 28 6 0 14 28 28 20 45 s 8 28/28
2026-08-21-morning-2026082108 ru-mobile 28 0 2 14 28 28 20 45 s 8 28/28
2026-08-21-night-2026082100 us-exit 28 5 1 14 28 28 14 45 s 0,1 28/28
2026-08-21-night-2026082100 ru-mobile 28 0 10 14 28 28 14 45 s 0,1 28/28
2026-08-21-night-2026082102 nl-direct 28 5 0 14 28 28 15 45 s 2,3 28/28
2026-08-21-night-2026082102 ru-mobile 28 0 11 14 28 28 15 45 s 2,3 27/28

Coverage against the design: 239 of 4480 readable pairs (5.3 %) of the 14-day, two-arm schedule in §2.3 of docs/step0c-hb-ru-market-study-2026-08-18.md. That is the study's own unit: a pair is readable only when BOTH arms saw the page, and only such a pair carries a contrast. Measured in loads attempted the same work reads 901 of 8960 (10.1 %) — the larger figure is what we have SPENT, the smaller is what we can USE, and the gap is the systematic per-site blindness named in the table above, not noise. Everything measured so far is a pilot of that design, not a sample of the Russian market.

Per-site yield — the denominator under every per-site claim

Worst-seen first. readable is the n any statement about that site rests on, and it is not the same number for any two sites: it runs 0 % to 82 % across the frame. So every aggregate over readable pairs above is a site-weighted average, weighted by nothing anyone chose — a site the instrument cannot see drops out of it silently, contributing no row and no warning. attempted counts pair ids seen in ANY arm, which is the honest denominator for how much of a site the instrument can reach. The loss column keeps the two kinds apart BY ARM, because they are different claims: a refused is the publisher's server declining that vantage, a no answer is our own link failing to fetch the page. Not shown to correlate with anything about the site's wrapper, and not ruled out either.

site attempted complete readable yield lost, by arm and kind
rbc.ru 34 32 0 0 % ru-mobile no-answer 24, nl-direct refused 22, us-exit refused 10, nl-direct no-answer 1
iz.ru 34 29 8 24 % nl-direct refused 12, us-exit refused 8, ru-mobile no-answer 3
aif.ru 33 30 8 24 % ru-mobile no-answer 22
magnit.ru 36 31 11 31 % ru-mobile no-answer 19, nl-direct no-answer 1
matchtv.ru 35 30 11 31 % ru-mobile no-answer 19
pikabu.ru 36 32 12 33 % ru-mobile no-answer 18, nl-direct refused 4
gismeteo.ru 35 31 21 60 % nl-direct refused 10, ru-mobile no-answer 6
mk.ru 35 30 21 60 % nl-direct no-answer 7, ru-mobile no-answer 5
kp.ru 34 29 21 62 % nl-direct no-answer 6, ru-mobile no-answer 6
hh.ru 34 29 23 68 % ru-mobile no-answer 4, us-exit no-answer 3, nl-direct refused 2, us-exit refused 1
aviasales.ru 33 30 25 76 % ru-mobile no-answer 5
sports.ru 34 31 26 76 % ru-mobile no-answer 4, us-exit no-answer 1
interfax.ru 32 29 25 78 % ru-mobile no-answer 3, us-exit no-answer 1
ria.ru 33 30 27 82 % ru-mobile no-answer 3

These 239 readable pairs are the same 239 the coverage line counts — the table is a decomposition of that figure, not a second measurement of it. A row whose complete is high and whose readable is zero is the shape to watch: both arms wrote a load, so the pair looks complete, and one arm never saw the page, so there is no contrast inside it.

Structural zeroes — sites in the frame that can yield no readable pair. rbc.ru (34 attempted, 24 ru-mobile no-answer, 22 nl-direct refused, 10 us-exit refused, 1 nl-direct no-answer). A site here costs loads every replicate and returns no contrast, and it is absent from every aggregate above without appearing in any of them. Keeping or dropping it is a decision about the study, recorded in ref/CANONICAL-FRAME where the frame is declared — never made here, and never made by quietly dropping it, since a frame that sheds what the instrument cannot see becomes a frame defined by the instrument and then reports full coverage of itself.

Declared in ref/CANONICAL-FRAME: rbc.ru. This list is recomputed from the corpus on every run, so a site whose refusal breaks leaves it here on its own — check the declaration still describes a site that is on this list.

What a no answer was, where a control was taken

A load with no HTTP status says the navigation never returned a response — our own link, never the publisher's refusal. But that is still two facts under one word. After each pair, the same URL is fetched with plain curl from inside the SAME vantage (tools/adint-paired-run --control-curl, on by default): if that answers, the host was reachable and the full page did not finish inside the nav budget — a budget we chose. If it does not, the host was unreachable from that vantage at that moment — a hole in the frame. unattributed is loads captured before the control existed, kept in the table because a split computed over only the rows that happen to carry a control would read as a fact about the corpus while being a fact about when the control was wired. The control runs AFTER the pair, never beside it — a second fetch racing the first would compete for the very link under measurement — so each control row publishes its own after_s gap.

arm no answer host reachable (page did not finish) host unreachable unattributed
nl-direct 15 1 1 13
ru-mobile 141 7 7 127
us-exit 5 0 0 5

Some cells carry loads outside the daypart they are named for. 2026-08-19-day-2026081914 is named day but 1 evening load(s) of 24 fall outside it; 2026-08-20-evening-2026082020 is named evening but 5 night load(s) of 29 fall outside it; 2026-08-21-night-2026082102 is named night but 18 morning load(s) of 56 fall outside it. A cell takes its name from the daypart it STARTED in and keeps it for the whole run, so a long cell crosses the boundary. The design in §2.3 stratifies by daypart, so these loads are filed under a daypart they were not sampled in. They are named here rather than re-bucketed — which stratum they belong to is a decision about the study, not about this table.

The word market is not available to us yet. What we have is a design that says exactly what would earn it (docs/step0c-…), a frame built by a published rule, and the first cell of that design actually run (docs/step0d-…). Every figure below and in the documents carries its n and its coverage in the same line as the number, and no chart in this repository is captioned as though it covered more.


What we can see, and what we are blind to

This matters more than any result here, so it is on the front page rather than in a footnote.

Observable: what an ad wrapper hands the browser — which bidders a publisher's own config asks, which of them answered, which stayed silent, what price came back when one did, and the currency and floor when the request carries them.

Not observable, at all, from this vantage:

  • The server-side auction. Server-to-server bidding never touches the browser. A site we record as carrying no wrapper may be selling its inventory perfectly well.
  • Any page we did not see. An HTTP 401/403/5xx is our own access failing, and it is recorded as blocked, never as "this publisher runs no header bidding". Converting our blindness into a market fact is the one mistake this project cannot afford.
  • Anything about anyone else. By construction.

The rules the code is built to, not merely to follow

  • Only his device. A record whose device id is not the target never reaches disk — dropped in the parser before any buffer, counted as a bare integer, never as a row or an aggregate. This is why monitor-mode Wi-Fi capture is rejected outright rather than filtered afterwards.
  • UNKNOWN is a first-class verdict. A host the reference table cannot name is reported with its count, and the coverage ratio is printed on every run. LOSS is not NOT IN.
  • Absence is split by cause, always. no-bid / no-answer / blocked / unreachable / no-web-apex / no-wrapper are six different facts. Folding any two of them is how a measurement of our own latency becomes a finding about bidder selectivity.
  • A silent fallback is a fabricated success. A missing reference table raises rather than rendering every host UNKNOWN and letting the run "succeed" — a table of nothing looks exactly like a finished measurement.
  • A gate you have not seen fail is not a gate. Every property above was broken deliberately and watched go red before it was accepted. docs/REPRODUCE.md lists which mistake each gate exists to catch.
  • A vantage is part of every reading. Each row records the exit address the browser read, not the one the shell measured — on our node those differ, and a row that cannot prove its own path is discarded rather than labelled.

Layout

tools/         collectors and reporters, one concern each, every one with a --test that
               runs offline against a local fixture:
                 adint-frame-stageb    builds the sampling frame from the pinned universe
                                       and PUBLISHES the rejections with their reasons
                 adint-paired-run      runs one cell of the schedule from both vantages,
                                       barriered site by site so a pair is really a pair
                 adint-hb-capture      drives a browser, records the wrapper's auction,
                                       and proves its exit from INSIDE the browser
                 adint-hb-report       counts it; refuses to pool schemas or fold no-answer
                 adint-publish         copies observations to public-data/ through an
                                       explicit field ALLOWLIST
                 adint-aggregate       observations -> bundle-exchange table
               self-test any of them:  python3 tools/<name> --test
               (browser-driving tools need ~/.venv-browser/bin/python3; run under the
               wrong interpreter they exit 2 = n/a, never a false FAIL)

ref/           the pinned inputs: the Tranco universe, domain -> org -> exchange tables,
               and each frame produced from them
public-data/   the observations, world-readable, CC0. Written only by adint-publish
data/          HIS capture. Gitignored, and stays that way — this is the privacy boundary
docs/          design, method, fact base. See docs/INDEX.md
tests/         fixtures

This repository is deliberately not part of the operator's mesh: nothing here is a mesh organ, no cron, no reflex. It consumes the mesh (the phone body, the board, the voice channel) and never vendors it.

Licence

CC0 1.0 — public domain. Take it, fork it, use it to argue we are wrong. See LICENSE.

A public-domain dedication is a statement about rights, not about truth: every number here is published with its n and its vantage so that you can judge it rather than inherit it.

About

Reading your own advertising profile from the demand side: what an RTB bid request hands a buyer about your own phone, for free.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages