On a recent load of 9gag.com in an ordinary browser with an adblocker running, 88 of the 194 resources on the page came from a domain called amidstdeploy.com — a name that belongs to no advertiser and appears in no ad-tech vendor list. Scripts, bid requests, tracking pixels, iframe documents, video segments. On a colder cache, 80%.
It is an ad-recovery proxy, and for that visitor it is 9GAG's ad stack. This is a teardown of how it works, measured on a live page load on 19 August 2026. We build a competing product; the last section is where we say what we do differently.
What Ad-Shield is
Ad-Shield is a Korean-founded adblock-recovery vendor, operating since around 2022, whose pitch to publishers is to "measure and monetize dark traffic" — its term for the ad-blocking audience that never shows up in analytics, never sees an adblock wall, and is simply invisible on the revenue line. On its own site the company claims 500+ publishers, 15 billion+ recovered ad requests, and that 80%+ of recovered revenue is net new; it lists Freestar, PMC, Publift and Times of India among partners and customers, and is a Google Certified Publishing Partner and registered IAB TCF vendor.
That is the marketing. What follows is the machine underneath it.
How we know 9GAG runs it
Two ways, both of which take a few seconds.
The recovery SDK announces itself as a global. Open the console on 9gag.com and type one word:
window.adshield
// → { loaded: true, isAdBlockerDetected: true, exportSnapshot: ƒ, cmd: Array(0) }
loaded: true means the runtime is live on the page. isAdBlockerDetected: true means it has profiled this visitor and concluded they block ads — the flag that gates everything else. There is even a diagnostic hook: window.adshield.exportSnapshot() returns a base64-encoded JSON dump of the page's entire GPT and Prebid state, slot by slot, targeting key by targeting key.
And the entry point is inline in 9GAG's server-rendered HTML, before anything else on the page:
<script async id="fHbbIn" data-sdk="l/1.2.13" data-cfasync="false" nowprocket
src="https://html-load.com/app.js" charset="UTF-8"
onload="((t,e,o,r)=>{ …9,229 characters of obfuscated fallback logic… })">
9.4 KB, in the <head>. data-cfasync="false" and nowprocket opt it out of Cloudflare Rocket Loader and WP Rocket — recovery code has to run first and unmodified, so it explicitly refuses to be deferred, bundled or rewritten by performance plugins.
Where the proxy actually runs
amidstdeploy.com is a thin, disposable address in front of a serious piece of infrastructure. The DNS and TLS records tell you the whole story:
| Signal | What it shows |
|---|
| The shard hostnames | all CNAME to the vendor's core zone, which resolves to 104.18.20.31 / 104.18.21.31 |
| Whois on those IPs | CLOUDFLARENET — Cloudflare, Inc. (AS13335) |
| Nameservers on the core zone | Cloudflare's |
| Nameservers on the disposable domain | GoDaddy's — registered and pointed separately |
| TLS certificate | a separate single-hostname certificate per shard, issued by Google Trust Services |
| Response headers | server: cloudflare, cf-ray, cf-cache-status on every proxied response |
So: it runs on Cloudflare, and the shape of it is specific. A per-shard certificate issued automatically, on a domain whose registrar DNS points at a Cloudflare zone it doesn't belong to, is the signature of Cloudflare's SaaS custom-hostname product — the mechanism for attaching arbitrary third-party hostnames to one origin, with certificates issued on demand.
That is not an incidental detail. It is what makes the address disposable. Standing up a new front door costs one DNS record and an automatically issued certificate; the workers, the cache and the routing never move. The domain is the cheap, replaceable part, and the architecture is built around that being true.
Three more structural properties are visible from the traffic:
It is a family, not a hostname. Traffic spread across twelve hostnames in a single pageview — ten numbered shards, 0. through 9., plus two apex hosts. That is not decoration: browsers cap concurrent connections per host, and several hundred ad requests down a single hostname would serialise into a stall. Ten shards turn one queue into ten.
It is configured per publisher. Before any ad traffic moves, the client fetches …/script/9gag.com.js?hash=-1862713072 — a per-publisher config keyed by domain, versioned by hash. One edge deployment, one config surface, every customer.
It only engages for visitors who are blocking. We loaded the same page with no adblocker as a control: the SDK loaded, concluded there was nothing to recover, and routed zero requests through the proxy. Every ad call went straight to Google. A proxy that runs unconditionally pays a hop for the ~70% of visitors who don't block anything, and stamps a permanent signature on every pageview.
The URL grammar
Every proxied request has the same shape:
https://8.stg.deck.amidstdeploy.com/session/eba/sj0/zq4/9gag.com/tyo/qwhvi331tyxywyw1vcnvy79yrcssc…
│ │ │ └ upstream chain │ │ └ encoded target URL
│ └ rotating domain └ route prefix │ └ payload kind
└ shard 0–9 └ publisher
Four decisions are visible in that path, and each is a real engineering choice:
The publisher domain is in the path, not the hostname. …/9gag.com/… means a single edge deployment serves every customer, and per-publisher policy is a path lookup rather than a separate deployment per site.
A kind code classifies the payload before anything is parsed. In our session: tyo for images, pixels and XHR beacons (59×), zao for iframe documents (16×), ijk for scripts (11×), plus separate codes for stylesheets. The worker knows what transformation a response needs before it touches the body — a script needs URL rewriting inside it, a pixel needs none.
The upstream chain is encoded, not just the target. Those two-to-four opaque segments before the publisher name track where in the frame tree the request came from. Ad tech is nested — an ad in an iframe loads another iframe that fires a pixel that redirects to a bidder — and the proxy has to know the whole ancestry to rewrite the next hop correctly.
The target URL is folded into the path, never a query parameter. A query string is the easiest thing in the world for a filter list to pattern-match. Path segments look like ordinary CDN routing. The cost is length: the longest URL we captured was 15,881 characters — an entire bid request encoded into a path.
Decoding one: what that opaque token really says
The token is not encrypted. It is obfuscated with a character-substitution cipher, and the design goal is narrower than secrecy: the encoded URL must contain no substring — doubleclick, googlesyndication, /gpt.js — that a filter list can match. Anything beyond that is wasted CPU on the edge and in the browser.
The scheme, recovered by comparing 47 request pairs where the original URL and the proxied URL were both observable on the page:
- Every token opens with the same fixed four-character header,
qwhv.
- The rest is the entire absolute URL, scheme included, run through a permuted alphabet: 32 plaintext characters each map to a single character, and everything outside that set maps to
y plus one character — y is reserved as the escape prefix and never appears alone.
- Before encoding, the proxy appends its own control parameter to the URL.
There is no key, no salt and no per-request state: the same input always produces the same output, which is precisely what makes the results cacheable at the edge.
A worked example. This is a real script request captured on the page:
https://0.stg.…/session/79x/ti9/rby/c6z/9gag.com/ijk/qwhvi331tyxywywtnq5gn15mvy7tyrcyry7s5mznqzfqbyr7n3yw3vcywutywc13yrut…
Take the token, drop the qwhv header, and apply the substitution:
| plaintext | h | t | t | p | s | : | / | / | s | e | c | u | r | e | p | u | b | a | d | s | . | g | . | d |
|---|
| cipher | i | 3 | 3 | 1 | t | yx | yw | yw | t | n | q | 5 | g | n | 1 | 5 | m | v | y7 | t | yr | c | yr | y7 |
Carry on to the end of the token and it resolves to:
https://securepubads.g.doubleclick.net/tag/js/gpt.js?as-extra=%7B%22configIndex%22%3A0%7D
That is Google Publisher Tag — the single most-blocked script on the open web — plus a control parameter the proxy appended for its own use (as-extra={"configIndex":0}).
The mapping is stable enough to run backwards. Re-encoding the original URLs we had captured reproduced the proxy's own tokens byte-for-byte in 41 of 47 cases, which is as strong a confirmation of a scheme as black-box work gets. A sample of what the other tokens on the page decode to:
https://securepubads.g.doubleclick.net/pagead/managed/js/gpt/m202608130101/pubads_impl.js
https://cdn.confiant-integrations.net/gptprebidnative/202608131959/wrap.js
https://hb-9gag.opentrade.ascendeum.com/asc_prebid.js
https://cdn.id5-sync.com/api/1.0/id5PrebidModule.js
https://c.amazon-adsystem.com/aax2/apstag.js
Ad server, ad-quality vendor, header-bidding wrapper, identity module, Amazon's bidder. The whole stack, one substitution away.
What actually flows through it
Not scripts. Everything.
| Dimension | What we measured in one pageview |
|---|
| Methods | 520 GET, 192 POST, 1 OPTIONS — the POSTs are live header-bidding auction calls |
| Statuses | 548 × 200, 139 × 204 (beacons), 5 × 206 (video byte ranges), 5 × 4xx/5xx |
| Content types | image/gif pixels, application/json bid responses, JavaScript, text/html iframe documents, video/mp2t HLS segments |
| Initiators | 564 script-initiated, 95 parser-initiated, 53 other |
| Redirects | 302 and 307 chains followed and rewritten in flight |
A 204 beacon and a 206 video range are not the same problem. One is a fire-and-forget pixel where latency is irrelevant and the body is empty; the other is a partial-content request where Range headers and byte offsets must survive the round trip or the video stalls. Sitting between an ad server and a browser means implementing enough of HTTP that nothing in the ad stack notices it is talking to a middlebox — including redirect chains, where each hop's Location has to be rewritten back into the proxy's own address space before the browser follows it.
The single OPTIONS request is the tell for the next problem.
The CORS problem it invents for itself
Once you re-host an ad stack, you have also rewritten every origin in it. Frames that used to be safeframe.googlesyndication.com are now iframe documents on a proxy shard. Scripts that expected to call a bidder from a Google-origin frame now call a different shard from a proxy-origin frame. Every same-origin assumption in the ad stack is broken, and the browser enforces those assumptions.
The proxy's answer is to make CORS say yes to whatever it invented. Across proxied responses in one pageview:
Access-Control-Allow-Origin | Count | What it means |
|---|
* | 394 | ordinary public assets |
https://9gag.com | 202 | credentialed calls from the publisher page — exact origin required |
https://2.stg…, https://8.stg… | 61 | calls between proxied frames, origin echoed back |
null | 14 | sandboxed/opaque-origin frames |
That third row is the interesting one. Requests originating inside a proxied iframe get that iframe's proxy origin echoed back — one shard authorising another, in a frame tree that only exists because the proxy built it. The Referer on those requests points at a sibling shard rather than at 9GAG.
Cookies, re-encoded
Header bidding without identity is worth a fraction of header bidding with it. But ad-vendor cookies live on domains the browser will never send to a proxy hostname. So the proxy carries its own:
cookie: img_fmjxcd5fbhj1s5tj1575t5scjxsvj9jzj9juj0u=AHWqTUl7b4nbBM4ndYDh3h7Yh…
img_fmjxcd5fbhj1s5tj1575t5scjxsvj9jzj9jkz9=AdutNiZnIp4B0yNkEESQ_N1E9…
img_fmjxcd5fbhj1s5tj1575t5scjxsvj9jzj9j0jajuj0=NO_DATA
Ad-vendor cookies are re-encoded under opaque names, scoped to the proxy domain and mapped back at the edge. Three things fall out of that design at once: the ad server still gets the identity it needs to bid; the publisher's own first-party cookie jar is never touched; and nothing sitting on the proxy domain looks like a recognisable tracking cookie to a filter list. Every serious recovery proxy converges on some version of this, ours included.
Caching and deployment
Proxied responses come back through Cloudflare with zstd compression and a pair of headers describing the edge's own behaviour: a cache status of HIT or MISS — 342 hits to 166 misses in one pageview, so roughly two-thirds of the ad stack is served without a round trip to the real ad server — and a version stamp.
That version stamp is the surprise. We counted eleven different build numbers inside a single pageview, spanning a range of consecutive versions. The shards are not one deployment behind one hostname; they are independently deployed workers rolling forward continuously, and a page can be served by several generations at once.
Upstream cache-control is mostly passed through untouched — a no-cache, must-revalidate from the ad server stays that way on the proxy — so caching decisions stay with the origin that made them.
Why the page never notices
A proxy this thorough creates one last problem: the ad stack reads its own URLs back. Google Publisher Tag inspects the script tag it was loaded from. Prebid compares origins. Ad-quality vendors check where creatives came from. Point those reads at a proxy hostname and code paths silently diverge — slots stay empty, viewability never reports, refresh never fires.
So the client half of the system lies, consistently. On the live page, the same <script> element answers two different ways depending on how you ask:
script.src // → "https://securepubads.g.doubleclick.net/tag/js/gpt.js"
script.outerHTML // → '<script src="https://0.stg.deck.amidstdeploy.com/session/79x/ti9/rby/c6z/9gag.com/ijk/qwhvi331…">'
The property getter returns the original; the DOM holds the proxy URL. Writes go to the proxy, reads come back rewritten. The patched primitives are the ones that matter — fetch, Element.setAttribute, the innerHTML setter, the script src accessor — and an inline shim snapshots the native versions of roughly thirty DOM APIs before any page code runs, so the layer always has clean originals to call through to. Google's own code, on a page where every one of its URLs has been swapped, sees nothing but ordinary Google URLs.
The result: all three GPT slots filled — a 970×250 billboard and two 336×280 units. Prebid ran a live auction through the tunnel and both sidebar slots were won by the amx bidder at $0.13 and $0.08 CPM, at refresh iteration 3. Not "an ad appeared": a functioning header-bidding stack, auctioning and refreshing, entirely over a proxy.
What this means if you are the publisher
Strip the vendor name off and an ad-recovery proxy has to do five things. Any one of them done badly costs fill:
- Carry the whole stack, not just scripts — auctions, pixels, iframes, video, redirects, preflights.
- Re-scope identity, so bidders still recognise the user and the publisher's cookie jar stays clean.
- Rebuild the frame tree's origins and satisfy CORS for frames that did not exist a moment ago.
- Stay transparent to the page, so every read the ad stack performs returns what it expects.
- Keep a supply of addresses, because the hostname is the disposable part and always will be.
AdUnblock does all five, on your site, without you operating any of it. The install is one script tag carrying your account code. From there: nothing is installed and no proxy traffic is sent until adblocking is confirmed on that pageview, so visitors who don't block anything are untouched and your page behaves exactly as it does today. When a visitor does block, their ad stack is carried end to end — auctions, pixels, iframes, video, redirects — with ad identity re-scoped so bidders still recognise the user and your own first-party cookies stay untouched. Your GPT ad-unit paths, sizes, targeting, refresh and header-bidding configuration stay exactly as you set them. We move the transport, not the setup.
Where we do it differently
Ad-Shield is a serious product and the proxy above is good engineering. There are four places we made different choices — described here by what they get you, because the mechanics of how a recovery stack stays reachable are the one thing no operator should publish. Anything written down here is something a filter-list maintainer reads too.
1. The address supply is the product, and it is operated. Both systems agree the hostname is disposable. The difference is who does the disposing, and when. Ours moves continuously and automatically, on a schedule that does not wait for something to break — so the discovery cost never lands on one of your visitors. Recovery that quietly degrades over a few weeks and recovery that holds are the same code with different operations behind it; that operations work is what you are actually buying.
2. Your site doesn't share fate with every other customer. A single shared proxy domain is efficient right up until the moment it is listed — and then every publisher behind it feels the same thing on the same day. Our estate is deliberately partitioned so that one host going down affects the sites on that host, not the whole customer base. It costs us more to run. It means your recovery rate doesn't depend on a stranger's traffic.
3. We chase the reads other stacks miss. A transparency layer that is almost right is worse than it sounds: the ad renders, the slot fills, and some vendor-specific check fails anyway, leaving a loading skeleton over a paid impression or silently killing refresh. Every one of these we find gets fixed against the real vendor code path and locked down with a permanent regression test, and the whole client is re-verified on live publisher pages before it ships. Fill rate is decided by these edge cases, not by the big architecture.
4. Reporting doesn't depend on the blocked client phoning home. A client script that gets blocked cannot report that it was blocked — exactly the failure mode you most need to see. Our impression, detection and click numbers are recorded server-side, so your dashboard shows what actually happened on blocked pageviews, including the ones where our own client never got to run.
One caveat worth stating plainly
This is a snapshot dated 19 August 2026. Ad-Shield ships continuously — eleven build numbers inside one pageview — and the hostname carrying 9GAG's ads today is by design not the one carrying them next quarter. That impermanence isn't a footnote to the story; it is the story. An ad-recovery proxy is not something you install once. It is something somebody has to keep alive, every week, for as long as your site serves ads.
Everything here was observed from a public page as any visitor sees it — no accounts, no exploitation, no access to anyone's systems. If you run an ad-recovery stack and think we've read something wrong, we would genuinely like to hear it.