# The BranditScan Protection Funnel — platform integration guide

> Turn the free BranditScan Leak Check into a native protection funnel inside
> your creator dashboard: a **"Takedowns" nav item with a live "▲ 18%" badge**,
> a **dashboard banner showing the creator's own leaked images**, and a
> **Takedowns page** where they act on it. All driven by one hidden widget
> load per creator per day, cached in your users' browsers.
>
> **No API key. No approval. No cost. No database changes.** The funnel keeps
> its state in the creator's browser and the widget token is stateless — the
> only thing you add server-side is the creator's stagename in a JS variable
> (plus one signing secret in config if you take the Self Submissions
> widget). Signups the funnel produces are credited to your affiliate slug
> ([affiliate program](https://branditscan.com/affiliate-program)).
>
> This is the pattern running in production on partner platforms since
> July 2026. Every snippet below is copy-paste complete.

---

## Contents

1. [What you're building](#1-what-youre-building)
2. [Quick start — the build, step by step](#2-quick-start)
3. [Read this first: iframe & referrer rules](#3-read-this-first)
4. [Step 1 — the funnel module (one file)](#4-step-1--the-funnel-module-one-file)
5. [Step 2 — your Takedowns page](#5-step-2--your-takedowns-page)
6. [Step 3 — the menu item](#6-step-3--the-menu-item)
7. [Step 4 — the banner (optional)](#7-step-4--the-banner-optional)
8. [Data reference — everything the widget reports](#8-data-reference)
9. [Caching & limits — why this is nearly free](#9-caching--limits)
10. [Developing locally](#10-developing-locally)
11. [Copy & design rules that tested well](#11-copy--design-rules)
12. [More placement ideas](#12-more-placement-ideas)
13. [Gotchas — the complete field list](#13-gotchas)
14. [Going deeper: the partner tier](#14-partner-tier)
15. [QA checklist](#15-qa-checklist)
16. [For AI coding agents](#16-for-ai-coding-agents)
17. [Appendix — lighter shapes & the full embed reference](#appendix--lighter-shapes--the-full-embed-reference)

---

## 1. What you're building

You get **two embeddable components** — use them together (recommended) or
either one alone:

- **Leak Check Widget** — a live piracy scanner: leaks found, traffic-loss
  %, the actual stolen images. Embed it visibly as the report page — and
  load it **hidden** as a pure data feed for your own UI. Free, no key.
- **Self Submissions Widget** — your creators file real DMCA takedowns from
  inside your page, filed and tracked by BranditScan. Needs a signing secret
  we provision same-day (section 5.2).

Every signup path is credited to you — the Leak Check embed and the signup
links in these recipes carry your `?aff=` slug, and the Self Submissions
widget attributes automatically (we link your affiliate account when
provisioning your signing secret).

**The preferred shape:** give the widgets a **dedicated section** of your
app — one page where both live — reached from a menu item, supported
(optionally) by a dashboard banner. The steps below build exactly that.

**None of it is mandatory.** A single visible iframe on any page is a valid
integration; so is the module without a banner, or a page without the nav
badge. Every lighter shape — plus the full URL-parameter reference for both
widgets — lives in the [Appendix](#appendix--lighter-shapes--the-full-embed-reference).

The Leak Check widget already computes everything a conversion funnel
needs: whether a creator's name is leaking, what % of their search traffic
pirate sites are stealing, which domains, and thumbnails of the leaked
images. It reports all of it to your page via `postMessage`.

Instead of only embedding it as a page, you also use it as a **headless,
zero-credential data feed**:

```text
any dashboard pageview
   │  cache miss (at most once per creator per day)
   ▼
hidden Leak Check iframe ──▶ bs-leak-check:result ──▶ localStorage (24h)
                                                          │
              every surface renders from the cache, instantly, zero traffic
                                                          │
        ┌────────────────────────┬────────────────────────┤
        ▼                        ▼                        ▼
  nav "Takedowns ▲18%"    dashboard banner          Takedowns page
  (every page)            (their own leaked         (two cards + widgets,
                          images, blurred)          the full report)
```

The funnel closes on two actions: **free self-submission takedowns** — the
Self Submissions widget on your Takedowns page, the preferred path (section
5.2; needs a signing secret we provision same-day, so request it when you
start building) — and a **BranditScan signup** through the embeds' own CTAs,
attributed to your `?aff=` slug automatically.

A creator who has never heard of BranditScan opens your dashboard and sees,
in your platform's own visual language: *"Sarah, we found your content on
pirate sites — +18% revenue to reclaim"*, with a strip of her own (blurred)
leaked images as proof. Every element of that message came from one hidden
widget load.

### The four states

Everything renders from `result.standing`:

| State | Meaning | Nav badge | Banner | Takedowns page |
|---|---|---|---|---|
| `at_risk` | pirate results found | red `▲ 18%` (or `6 leaks`) | personalized + leaked-image strip | leak report auto-runs |
| `good` | clean scan | none — silence is the reward | generic protection pitch | leak section hidden (saves quota) |
| `account` | already a BranditScan member | green `✓` | recolors green: "protected", no upsell | "already protected" card |
| *(no data)* | probe pending / failed | none | generic pitch | embed loads prefilled (doubles as probe) |

---

## 2. Quick start

The build is four steps, each one a paste. Do them in this order — or your
own; after step 1 the data is yours (section 8), and lighter shapes down to
a single iframe live in the [Appendix](#appendix--lighter-shapes--the-full-embed-reference).

| Step | What | Where | Section |
|---|---|---|---|
| **1** | Install the module — config + one JS file. Runs the hidden scan, caches 24h, renders everything | every logged-in page | 4 |
| **2** | Create your **Takedowns page** — two cards (submit free / go automated) + both widgets | a new page, e.g. `/takedowns` | 5 |
| **3** | Add the **menu item** — nav link + badge slot, the module fills "▲ 18%" | your nav | 6 |
| **4** | Add the **banner** *(optional)* — their leaked images, blurred, clicking into the page | dashboard / settings | 7 |

On the creator's next pageview the module probes once, caches the result,
and every hook renders — from cache, on every page, for the next 24 hours.

**Do this now, in parallel:** email
[help@branditscan.com](mailto:help@branditscan.com) for your **Self
Submissions signing secret** (section 5.2). It's provisioned same-day but it
is the one piece that isn't instant — request it when you start building and
the Takedowns page's free-submission card ships live with everything else.

---

## 3. Read this first

Two iframe rules cost integrators the most time. Get them right up front:

1. **Always set `referrerpolicy="no-referrer"`** on every BranditScan iframe
   (the hidden probe AND visible embeds) and `referrerPolicy = 'no-referrer'`
   on preloaded `<img>` elements. The scan endpoints validate the embedding
   page's referrer and reject some values (localhost and various proxy
   setups trip it) — and the failure is deceptive: the widget *renders* but
   never auto-runs and never posts a `result` (only `height`). It looks
   exactly like a broken postMessage contract. Identity comes from
   `?stagename=`, never from the referrer, so sending none is always the
   reliable configuration.
2. **Never hide the probe iframe with `display:none`, and never use
   `loading="lazy"` on it.** The widget needs real layout to boot, and a
   lazy off-screen frame never loads at all. Park it off-viewport instead
   (`position:absolute; width:2px; height:2px; left:-9999px`). The module
   below does this for you.

---

## 4. Step 1 — the funnel module (one file)

One dependency-free script (~170 lines). **Include it from your main
layout** — every page that renders the nav item or the banner needs it, so
load it globally rather than per-page. What it does: when there's no fresh
data for the creator, it loads the Leak Check widget in an invisible iframe,
silently reads the scan result the widget posts, caches it in the creator's
browser for 24 hours, and removes the iframe. Nothing is saved to your
backend or database — the data lives on the frontend, and any surface
renders from it: the nav badge, the banner, or anything custom you build.

In detail, it implements the entire data layer and all render logic:

- **cache-first**: serves `localStorage` when fresh (24h), probes only on miss;
- **verified**: a result is accepted only when the message origin, the source
  frame, AND the echoed `stagename` all match — a manual scan of someone
  else's name can never poison the logged-in creator's cache;
- **samples-aware**: the widget re-posts the result once image samples land
  (see section 8), so the probe lingers 15s and the latest verified message
  wins;
- **fail-silent**: no result within 180s → cleanup, nothing renders, the next
  pageview retries. A funnel must degrade to silence, never to an error;
- **pub/sub**: every surface listens for one `bs-leak-data` event, so the
  same file runs unmodified on every page — consumers are existence-guarded.

The `data-bs-*` hooks it targets are the same contract our future hosted SDK
will use, so upgrading later means deleting this file, not rewriting markup.

```html
<script>
/* bs-funnel.js — BranditScan protection funnel, v1.
   Config: window.BS_FUNNEL = { stagename, aff } (set before this runs). */
(function () {
  'use strict';

  var cfg = window.BS_FUNNEL || {};
  var STAGENAME = (cfg.stagename || '').trim();
  if (!STAGENAME) return;                       // viewers, no-stagename users: do nothing

  var ORIGIN = 'https://branditscan.com';
  var CACHE_KEY = 'bs-leak-check:' + STAGENAME.toLowerCase();
  var DAY = 24 * 60 * 60 * 1000;

  /* ---------- pub/sub ------------------------------------------------ */

  function publish(result) {
    window.bsLeakData = result;                 // for late-joining code
    document.dispatchEvent(new CustomEvent('bs-leak-data', { detail: result }));
  }

  /* ---------- normalization ------------------------------------------ */

  function toResult(d) {
    // Google-Images pirated thumbs first, then per-site samples as fill —
    // deduped, capped. Covers creators with big traffic loss but a clean
    // Google-Images page (their proof images live on the pirate sites).
    var thumbs = (d.thumbs || []).concat(d.samples || [])
      .filter(function (src, i, all) { return all.indexOf(src) === i; })
      .slice(0, 12);
    return {
      standing: d.standing,
      traffic: d.traffic || null,               // { pct, surface: 'web'|'images' }
      pirates: d.pirates || 0,
      domains: (d.domains || []).slice(0, 6),
      thumbs: thumbs,
      estimatedLoss: d.estimatedLoss || null,
      at: Date.now()
    };
  }

  /* ---------- verified accept (probe + visible embed share this) ----- */

  function isOwnResult(event, frame) {
    if (event.origin !== ORIGIN) return false;
    if (!frame || !frame.contentWindow || event.source !== frame.contentWindow) return false;
    var d = event.data || {};
    if (d.type !== 'bs-leak-check:result') return false;
    // Poisoning guard: only the logged-in creator's own name may be cached.
    return (d.stagename || '').toLowerCase() === STAGENAME.toLowerCase();
  }

  function accept(data) {
    var result = toResult(data);
    try { localStorage.setItem(CACHE_KEY, JSON.stringify(result)); } catch (e) { /* storage blocked — in-memory only */ }
    publish(result);                            // latest verified message wins
  }

  /* ---------- the hidden probe ---------------------------------------- */

  function embedUrl() {
    return ORIGIN + '/embed/leak-check?branding=0&border=0'
      + (cfg.aff ? '&aff=' + encodeURIComponent(cfg.aff) : '')
      + '&stagename=' + encodeURIComponent(STAGENAME);
  }

  function startProbe() {
    var frame = document.createElement('iframe');
    frame.src = embedUrl();
    frame.referrerPolicy = 'no-referrer';       // see "Read this first"
    frame.style.cssText = 'position:absolute;width:2px;height:2px;left:-9999px;top:0;border:0;';
    frame.setAttribute('aria-hidden', 'true');
    frame.setAttribute('tabindex', '-1');

    var gotFirst = false;
    function teardown() {
      window.removeEventListener('message', onMsg);
      if (frame.parentNode) frame.parentNode.removeChild(frame);
    }
    function onMsg(event) {
      if (!isOwnResult(event, frame)) return;
      accept(event.data);
      if (!gotFirst) {
        gotFirst = true;
        setTimeout(teardown, 15000);            // linger: catch the samples re-post
      }
    }
    window.addEventListener('message', onMsg);
    setTimeout(function () { if (!gotFirst) teardown(); }, 180000);  // fail silent
    document.body.appendChild(frame);
  }

  /* ---------- consumers ------------------------------------------------ */

  document.addEventListener('bs-leak-data', function (e) { renderAll(e.detail); });

  function renderAll(r) {
    if (!r || !r.standing) return;
    renderNavBadge(r);
    renderBanner(r);
    renderTakedownsPage(r);
  }

  /* Nav badge: red % pill when at risk, green check for members, nothing
     when clean. Compact by design — long badges truncate nav labels. */
  function renderNavBadge(r) {
    document.querySelectorAll('[data-bs-nav-badge]').forEach(function (slot) {
      if (slot.dataset.done) return;
      if (r.standing === 'at_risk') {
        slot.textContent = (r.traffic && r.traffic.pct > 0)
          ? '\u25B2 ' + Math.round(r.traffic.pct) + '%'
          : r.pirates + ' leaks';
        slot.className = 'bs-badge bs-badge--alert';
        slot.hidden = false;
      } else if (r.standing === 'account') {
        slot.textContent = '\u2713';
        slot.className = 'bs-badge bs-badge--ok';
        slot.hidden = false;
      }
      slot.dataset.done = '1';                  // decide once per pageview
    });
  }

  /* Banner: server-rendered generic copy is the default state; this only
     upgrades it when there is something personal to say. */
  function renderBanner(r) {
    var el = document.querySelector('[data-bs-banner]');
    if (!el || el.dataset.done) return;
    el.dataset.done = '1';

    function setText(sel, text) {
      var n = el.querySelector(sel);
      if (n) { n.textContent = text; n.hidden = false; }
    }

    if (r.standing === 'account') {
      el.classList.add('bs-banner--protected');
      setText('[data-bs-banner-headline]', 'Your content is protected');
      setText('[data-bs-banner-sub]',
        'BranditScan is watching your name 24/7 \u2014 new leaks get found and taken down for you.');
      return;
    }
    if (r.standing !== 'at_risk' || r.pirates < 1) return;   // keep the generic pitch

    var name = (el.getAttribute('data-user-name') || '').trim();
    setText('[data-bs-banner-headline]',
      (name ? name + ', we' : 'We') + ' found your content on pirate sites.');
    setText('[data-bs-banner-sub]',
      'Take it down free \u2014 takedowns are covered for every creator.');

    if (r.domains.length) {
      setText('[data-bs-banner-domains]',
        'Found on: ' + r.domains.slice(0, 2).join(' \u00B7 ')
        + (r.domains.length > 2 ? ' \u00B7 +' + (r.domains.length - 2) + ' more' : ''));
    }

    var statNum = el.querySelector('[data-bs-banner-stat]');
    var statLabel = el.querySelector('[data-bs-banner-stat-label]');
    if (statNum && statLabel) {
      if (r.traffic && r.traffic.pct > 0) {
        statNum.textContent = '+' + Math.round(r.traffic.pct) + '%';
        statLabel.textContent = 'revenue to reclaim';
      } else {
        statNum.textContent = String(r.pirates);
        statLabel.textContent = 'leaks to remove';
      }
      statNum.hidden = statLabel.hidden = false;
    }

    var strip = el.querySelector('[data-bs-banner-thumbs]');
    if (strip && r.thumbs.length) renderThumbStrip(strip, r.thumbs);
  }

  /* Leaked-image strip: preload off-DOM, render only survivors (hotlinked
     images die unpredictably), favicon-badge each tile, blur via YOUR css.
     Tiles land in a .bs-leak-track; with 4+ survivors the set is duplicated
     and the track tagged --loop, so the CSS marquee slides it seamlessly.
     Too few tiles = no duplication, no animation — a static row. */
  function renderThumbStrip(strip, urls) {
    var loaded = [];
    var pending = urls.length;
    var rendered = false;

    function makeTile(src, isClone) {
      var tile = document.createElement('span');
      tile.className = 'bs-leak-tile';
      if (isClone) tile.setAttribute('aria-hidden', 'true');
      var img = document.createElement('img');
      img.src = src;
      img.referrerPolicy = 'no-referrer';
      img.alt = '';
      tile.appendChild(img);
      try {
        var host = new URL(src).hostname.replace(/^www\./, '');
        var fav = document.createElement('img');
        fav.className = 'bs-leak-tile__favicon';
        fav.src = 'https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&size=32&url=https://' + host;
        fav.onerror = function () { fav.remove(); };
        tile.appendChild(fav);
        tile.title = host;
      } catch (e) {}
      return tile;
    }

    function render() {
      if (rendered || !loaded.length) return;
      rendered = true;
      var set = loaded.slice(0, 8);
      var track = document.createElement('div');
      track.className = 'bs-leak-track' + (set.length >= 4 ? ' bs-leak-track--loop' : '');
      set.forEach(function (src) { track.appendChild(makeTile(src, false)); });
      if (set.length >= 4) {
        set.forEach(function (src) { track.appendChild(makeTile(src, true)); });
      }
      strip.appendChild(track);
      strip.hidden = false;
    }

    urls.forEach(function (src) {
      var probe = new Image();
      probe.referrerPolicy = 'no-referrer';
      probe.onload = function () { loaded.push(src); if (--pending === 0) render(); };
      probe.onerror = function () { if (--pending === 0) render(); };
      probe.src = src;
    });
    setTimeout(render, 8000);                   // straggler cutoff: show what loaded
  }

  /* Takedowns page: fill the automation card's %, decide the leak embed
     once (a late probe result must never yank a loaded widget). */
  function renderTakedownsPage(r) {
    var pct = document.querySelector('[data-bs-pct]');
    if (pct && r.standing === 'at_risk' && r.traffic && r.traffic.pct > 0) {
      pct.textContent = '+' + Math.round(r.traffic.pct) + '%';
    }
    var frame = document.getElementById('bs-leakcheck');
    if (!frame || frame.dataset.decided || frame.getAttribute('src')) return;
    frame.dataset.decided = '1';
    var section = document.querySelector('[data-bs-leak-section]');
    if (r.standing === 'good' || r.standing === 'account') {
      if (section) section.hidden = true;       // nothing to look at; saves scan quota
    } else if (frame.dataset.srcPrefilled) {
      frame.src = frame.dataset.srcPrefilled;   // at_risk: auto-run the full report
    }
  }

  /* ---------- iframe heights + visible-embed harvesting --------------- */

  window.addEventListener('message', function (e) {
    if (e.origin !== ORIGIN) return;
    var d = e.data || {};
    var lk = document.getElementById('bs-leakcheck');
    var tw = document.getElementById('bs-takedowns');
    if (d.type === 'bs-leak-check:height' && lk && e.source === lk.contentWindow) {
      lk.style.height = Math.ceil(d.height) + 'px';
    }
    if (d.type === 'bs-takedowns:height' && tw && e.source === tw.contentWindow) {
      tw.style.height = Math.ceil(d.height) + 'px';
    }
    // The visible embed keeps refreshing the cache for the whole visit —
    // including manual re-scans of the creator's OWN name (others rejected).
    if (lk && isOwnResult(e, lk)) accept(d);
  });

  /* ---------- boot ----------------------------------------------------- */

  var cached = null;
  try { cached = JSON.parse(localStorage.getItem(CACHE_KEY) || 'null'); } catch (e) {}

  var visibleEmbed = document.getElementById('bs-leakcheck');
  if (cached && (Date.now() - cached.at) < DAY) {
    publish(cached);                            // instant, zero network
  } else if (visibleEmbed && visibleEmbed.dataset.srcPrefilled) {
    visibleEmbed.dataset.decided = '1';         // no data: the VISIBLE embed is the probe
    visibleEmbed.src = visibleEmbed.dataset.srcPrefilled;
  } else {
    startProbe();                               // any other page: hidden probe
  }
})();
</script>
```

Base CSS for the nav badge (restyle freely — class names are yours; the
banner's full CSS — card, stat, proof strip, marquee, tiles — ships with the
banner in section 7):

```css
/* nav badge */
.bs-badge { display: inline-block; margin-left: 6px; padding: 1px 7px; border-radius: 999px;
            font-size: 11px; font-weight: 700; line-height: 1.5; vertical-align: middle; }
.bs-badge--alert { background: #e5484d; color: #fff; }
.bs-badge--ok    { background: #2fbd76; color: #fff; }
```

---

## 5. Step 2 — your Takedowns page

The funnel's landing page (`/takedowns` in these samples). Composition, top
to bottom:

1. **Two cards, honestly compared** — free self-submission vs full automation;
2. **Self Submissions widget** — hidden until intent, so its iframe never
   loads for visitors who don't click;
3. **Leak Check embed** — the full report, auto-running for at-risk creators;
4. (optional) a short FAQ in your own words.

**The complete page — both cards live — is the preferred integration.** The
free-submission card is what makes the nav badge's promise real: the creator
saw "▲ 18%", clicked, and can act on it right here, for free. The widget
needs a signing secret we provision (section 5.2) — that's an email and a
same-day turnaround, not an API project, so request it when you start
building. The honest two-card split is deliberate: creators trust the paid
pitch more when the free path isn't buried, and every element of the page
reinforces the story the nav badge started.

### 5.1 The two cards

Show both ways to act, side by side. **Submit free** — the Self Submissions
widget, unlimited; smaller and mid-size creators happily stay on it forever.
**Go automated** — links out to BranditScan with your `aff` code; typically
what bigger creators pick once the free path has shown them what's out
there. The honest split converts better than an upsell-only pitch.

A live reference implementation, exactly this layout:

![The live Protection page of a partner platform — the two cards side by side above the embedded widget](https://branditscan.com/front/lp/assets/images/developers/funnel/yv-takedowns-page.png)

![The Self Submissions widget on the same page — paste box and filed takedowns with live states](https://branditscan.com/front/lp/assets/images/developers/funnel/yv-takedowns-widget.png)

How the pieces fit — **your design, your copy**; the reference above is
inspiration, not a template:

- **The "go automated" card** is a plain link out to BranditScan with your
  `aff` code — nothing to wire.
- **The "submit free" card** toggles the Self Submissions iframe below it
  (section 5.2) — hidden until that click, so it never loads for visitors
  who don't ask for it.
- **The Leak Check report loads underneath automatically** (section 5.3) —
  creators land on the page and immediately see their leaks; the module
  auto-runs it for at-risk creators and hides it for clean or
  already-protected ones.

The complete page skeleton — identical to the one in the API reference, so
the two never drift:

```html
<!-- /takedowns — two simple cards, style them your way (see the screenshots) -->
<div class="cards">

  <div class="card">                            <!-- FREE — self submission -->
    <h4>Submit takedowns — free</h4>
    <p>Paste a stolen link — a formal DMCA notice gets filed for you. Unlimited.</p>
    <a href="#takedown-widget" data-bs-open-takedowns>Submit a link</a>
  </div>

  <div class="card">                            <!-- AUTOMATED — your aff link -->
    <h4>Want it fully automated?</h4>
    <p>24/7 scanning and takedowns handled for you — reclaim up to
       <b data-bs-pct>+22%</b> of your traffic.</p>
    <a href="https://branditscan.com/register?aff=your-affiliate-slug"
       target="_blank" rel="noopener">Get protected</a>
  </div>

</div>

<!-- Self Submissions widget — hidden until the free card is clicked; the
     iframe never loads for visitors who don't ask for it. Signing secret:
     email help@branditscan.com when you START building (section 5.2). -->
<section id="takedown-widget" hidden>
  <iframe id="bs-takedowns" data-src="https://branditscan.com/embed/takedowns?t=TOKEN&funnel=off"
          referrerpolicy="no-referrer" scrolling="no" style="width:100%;border:0;height:520px;"></iframe>
</section>
<script>
  /* the free card's CTA reveals the widget and loads its iframe
     (minimal version — section 5.2's script adds #takedown-widget deep links) */
  document.addEventListener('click', function (e) {
    var t = e.target.closest('[data-bs-open-takedowns]');
    if (!t) return;
    e.preventDefault();
    var s = document.getElementById('takedown-widget');
    var f = document.getElementById('bs-takedowns');
    s.hidden = false;
    if (f && !f.getAttribute('src') && f.dataset.src) f.src = f.dataset.src;
    s.scrollIntoView();
  });
</script>

<!-- Leak Check report — loads by itself; the module decides once per pageview:
     at risk           -> auto-runs, creators see their leaks the moment they land
     clean / protected -> this WHOLE SECTION stays hidden (and those creators
                          get no nav badge either — silence is the reward)
     no data yet       -> loads prefilled and doubles as that day's probe -->
<section data-bs-leak-section>
  <iframe id="bs-leakcheck"
          data-src-prefilled="https://branditscan.com/embed/leak-check?aff=your-affiliate-slug&border=0&stagename=SARAH"
          referrerpolicy="no-referrer" scrolling="no" style="width:100%;border:0;height:600px;"></iframe>
</section>
```

The `data-bs-pct` slot is replaced with the creator's real `+{pct}%` when the
cache says `at_risk` — a personalized perk line converts far better than a
generic one. Card copy is yours: perk bullets, fit lines ("Find a leak now
and then? This is all you need."), pricing — whatever matches your product;
`branditscan.com/?aff=` works on the automated CTA too (both record the
30-day attribution cookie). If your signing secret hasn't arrived yet, ship
with the automated card only and add the free card the moment the widget is
live — never show "Submit free" before the page can actually deliver it
(copy rule 6). Treat one-card-only as a launch-day gap to close, not a
configuration.

### 5.2 Self Submissions widget — the page's centerpiece, hidden until intent

The [Self Submissions widget](https://branditscan.com/developers#self-submissions)
lets your creators file real DMCA takedowns from inside your page — formal
notices, filed and tracked by BranditScan, anonymous for the creator. This is
the preferred way to close the funnel: free, immediate action on the leaks
the badge just showed them. Signups from inside the widget — including its
built-in leak check — credit your affiliate automatically; we link your
account when provisioning the secret.

One thing to plan around: **it is not live the moment you paste it.** The
widget needs a signed embed token, and the signing secret behind it is
provisioned by us — email
[help@branditscan.com](mailto:help@branditscan.com) (same-day turnaround,
free to set up). **Include your production domain AND your local dev hosts**
(`localhost`, `*.test`, …) in that email — the widget only renders inside
registered domains, and registered dev hosts get http/any-port treatment so
local development against the production widget just works. **This is not SSO and there are no API calls** — your
server just mints a stateless token per page render:

```php
// Any language works — this is ~10 lines of standard HMAC. PHP shown.
function branditscanWidgetToken(string $secret, int $platformId, $user): string
{
    $claims = [
        'v'         => 1,
        'pid'       => $platformId,
        'uid'       => 'plat-' . $user->id,   // your stable per-user id — NEVER change the format
        'name'      => mb_substr($user->display_name, 0, 100),
        'stagename' => $user->stagename,      // 3–50 chars: letters, digits, spaces, - _ .
        'originals' => $user->official_urls,  // ≤10 http(s) URLs of THEIR OWN pages — we safelist
                                              // these so creators can't file against themselves
        'iat'       => time(),
        'exp'       => time() + 86400,        // 24h
    ];
    $payload = rtrim(strtr(base64_encode(json_encode($claims, JSON_UNESCAPED_SLASHES)), '+/', '-_'), '=');
    return $payload . '.' . hash_hmac('sha256', $payload, $secret);
}
```

The section ships hidden; the iframe has no `src` until someone shows intent,
so it never loads (or costs anything) for visitors who don't click:

```html
<section id="takedown-widget" hidden>
  <iframe id="bs-takedowns"
          data-src="https://branditscan.com/embed/takedowns?t={{ $widgetToken }}&funnel=off"
          referrerpolicy="no-referrer" scrolling="no"
          style="width:100%;border:0;height:520px;"></iframe>
</section>

<script>
(function () {
  var section = document.getElementById('takedown-widget');
  if (!section) return;
  var frame = document.getElementById('bs-takedowns');

  function reveal(scroll) {
    section.hidden = false;
    if (frame && !frame.getAttribute('src') && frame.dataset.src) frame.src = frame.dataset.src;
    if (scroll) section.scrollIntoView({ behavior: 'auto' });  // 'smooth' is throttled in background tabs
  }

  document.addEventListener('click', function (e) {
    var trigger = e.target.closest('[data-bs-open-takedowns], a[href$="#takedown-widget"]');
    if (trigger) { e.preventDefault(); reveal(true); }
  });
  if (location.hash === '#takedown-widget') reveal(true);      // arriving with the hash: reveal
  window.addEventListener('hashchange', function () {
    if (location.hash === '#takedown-widget') reveal(true);
  });
})();
</script>
```

`funnel=off` disables the widget's own built-in leak-check upsell card — it's
redundant here because the real Leak Check report sits right below. Height is
handled by the funnel module (`bs-takedowns:height`). Optional polish: on the
`bs-takedowns:expired` message, silently fetch a fresh token from your own
endpoint and swap `frame.src` instead of letting the widget show its reload
card.

### 5.3 Leak Check embed — deferred and state-gated

The full report. The iframe ships with **no `src`** — the module decides once
per pageview from the cached standing:

```html
<section data-bs-leak-section>
  <h4>Your leak report</h4>
  <iframe id="bs-leakcheck"
          data-src-prefilled="https://branditscan.com/embed/leak-check?aff=your-affiliate-slug&border=0&stagename={{ urlencode(creator.stagename) }}"
          referrerpolicy="no-referrer" scrolling="no"
          style="width:100%;border:0;height:600px;"></iframe>
</section>
```

- `at_risk` → the report auto-runs (instantly, from our server-side cache the
  probe already warmed). Every visit refreshes your 24h cache — the module
  harvests the visible embed's messages too.
- `good` / `account` → the whole section hides. Nothing to look at, and it
  saves the scan quota for creators who actually have a report.
- no data yet → the embed loads prefilled and **doubles as that day's probe**
  — the module never runs a hidden probe on a page that has a visible embed.

The embed's own CTAs handle the BranditScan signup, attributed to your
`?aff=` slug. That's the funnel's paid end — no SSO, no API, nothing else to
wire.

---

## 6. Step 3 — the menu item

The funnel's top. It sits on **every** page, costs nothing (pure cache), and
quietly advertises the number until the creator clicks. Name it whatever
fits your product — **"Takedowns"**, **"Leaks"**, **"Remove Leaks"**,
**"Protection"** all read well; the samples use Takedowns.

A live reference — the badge between the platform's own nav items:

![Live partner implementation — the nav item with the creator's real traffic-loss badge](https://branditscan.com/front/lp/assets/images/developers/funnel/yv-menu-item.png)

```html
<a href="/takedowns" class="your-nav-link-class">
  <!-- your icon -->
  Takedowns
  <span data-bs-nav-badge hidden></span>
</a>
```

What renders, by state — the module handles all of it:

| State | Badge | Why |
|---|---|---|
| `at_risk` | red `▲ 18%` (from `traffic.pct`) or `6 leaks` (fallback) | alert wins the glance in a busy sidebar |
| `account` | green `✓`, sized like your other nav badges | reads native, no nagging |
| `good` / no data | nothing | a permanent green tick would desensitize — silence is the reward state |

**Keep it compact.** `▲ 18%` — not `▲ 18% traffic loss`. Anything longer
truncates the nav label itself on most sidebars. Copy variants that tested
well when space allows (e.g. a wider top-nav): `"▲ 18% of your traffic is
leaking"`, `"6 leaks found"`.

---

## 7. Step 4 — the banner (optional)

The highest-converting surface: the creator's own leaked images, on their own
dashboard, in your visual language. Also drops straight into a **settings
page** — same hooks, same module (one `[data-bs-banner]` per page).

A live reference — the at-risk state: personalized headline, the actual
pirate domains, a green "+16% revenue to reclaim", a member case study:

![Live partner implementation — the personalized at-risk dashboard banner](https://branditscan.com/front/lp/assets/images/developers/funnel/yv-banner.png)

Design of the pattern: the banner is **server-rendered with generic copy as
the default state** — the module only *upgrades* it when the cache has
something personal to say. No layout shift, no flash of copy being replaced,
and creators with no data still see a working pitch.

The markup + CSS below is a complete, ready-to-paste banner — it reproduces
the live sample on the [Developers page](https://branditscan.com/developers#funnel)
banner step exactly, sliding proof strip included (the module duplicates the
tiles into a `.bs-leak-track--loop`, the CSS animates it, hover pauses it,
`prefers-reduced-motion` disables it, and a strip too short to slide stays
static). Restyle freely — every class name is yours.

```html
<div class="bs-banner" data-bs-banner
     data-user-name="{{ creator.first_name }}"
     onclick="if (!event.target.closest('a')) location.href='/takedowns'">

  <span class="bs-banner__stat">
    <b data-bs-banner-stat hidden></b>          <!-- "+18%" (green — opportunity framing) -->
    <i data-bs-banner-stat-label hidden></i>    <!-- "revenue to reclaim" -->
  </span>

  <h3 data-bs-banner-headline>Protect your content</h3>

  <p data-bs-banner-sub>
    Pirate sites repost creator content within days of upload. Run a free
    scan and see what's out there with your name on it.
  </p>

  <p data-bs-banner-domains hidden></p>         <!-- "Found on: site1 · site2 · +2 more" -->

  <div data-bs-banner-thumbs hidden></div>      <!-- their leaked images: blurred sliding tiles -->

  <a href="/takedowns">Check my name &rarr;</a>

  <small>Powered by BranditScan</small>
</div>
```

```css
/* The banner, verbatim from the live sample — restyle freely. */
.bs-banner { position: relative; overflow: hidden; cursor: pointer; border-radius: 14px;
             border: 1px solid rgba(233, 162, 0, .42); background: #fffdf2;
             padding: 18px 128px 14px 20px; }
.bs-banner h3 { margin: 0 0 3px; font-size: 15.5px; }
.bs-banner p  { margin: 0; font-size: 12.5px; line-height: 1.55; color: #606662; }
.bs-banner [data-bs-banner-domains] { margin-top: 6px; font-size: 11.5px; color: #8c928e; }
.bs-banner__stat { position: absolute; top: 18px; right: 20px; text-align: right;
                   color: #1f9d5b; font-size: 22px; line-height: 1.1; font-weight: 600; }
.bs-banner__stat i { display: block; font-style: normal; font-weight: 400;
                     font-size: 10.5px; color: #7a807c; margin-top: 2px; }
.bs-banner > a { display: inline-block; margin-top: 10px; font-size: 12.5px;
                 font-weight: 600; color: #7f42fd; text-decoration: none; }
.bs-banner > small { display: block; margin-top: 10px; font-size: 10.5px; color: #969c98; }
.bs-banner--protected { border-color: rgba(31, 157, 91, .4); background: #f3fbf6; }

/* The proof strip — the module fills [data-bs-banner-thumbs] with a
   .bs-leak-track and duplicates the tiles, so -50% loops seamlessly. */
[data-bs-banner-thumbs] { margin-top: 12px; overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent); }
.bs-leak-track { display: flex; width: max-content; }
.bs-leak-track--loop { animation: bsLeakSlide 32s linear infinite; }
[data-bs-banner-thumbs]:hover .bs-leak-track { animation-play-state: paused; }
.bs-leak-tile { position: relative; flex-shrink: 0; width: 64px; height: 64px; margin-right: 8px;
                border: 2px solid #e5484d; border-radius: 10px; overflow: hidden; }
.bs-leak-tile img:first-child { width: 100%; height: 100%; object-fit: cover;
                                filter: blur(7px); transform: scale(1.15); }  /* the blur is non-negotiable */
.bs-leak-tile__favicon { position: absolute; right: 3px; bottom: 3px; width: 15px; height: 15px;
                         border-radius: 4px; background: #fff; padding: 1px; }
@keyframes bsLeakSlide { to { transform: translateX(-50%); } }
@media (max-width: 560px) {
  .bs-banner { padding-right: 20px; }
  .bs-banner__stat { position: static; display: block; margin-bottom: 6px; }
}
@media (prefers-reduced-motion: reduce) { .bs-leak-track--loop { animation: none; } }
```

What the module does to it on `at_risk`:

- headline → `"Sarah, we found your content on pirate sites."`
- sub → `"Take it down free — takedowns are covered for every creator."`
- domains line → `"Found on: fapello.com · thothub.vip · +3 more"`
- stat → green `+18%` / `revenue to reclaim` (fallback: `6` / `leaks to remove`)
- thumbs strip → up to 8 **blurred** tiles of their actual leaked images,
  each badged with the pirate site's favicon and sliding as a seamless
  marquee. Preloaded first — dead hotlinks never render (see section 8 for
  why they die).

On `account` it recolors to the protected state and swaps the copy — never
leave a "sign up" pitch in front of someone who already pays BranditScan.

Two production notes:

- The card is a `div` with a delegated click-through, not an `<a>` — it
  contains real links and links can't nest.
- If the banner lives in a measured container (carousel, masonry), dispatch
  `window.dispatchEvent(new Event('resize'))` after the module upgrades it —
  the strip changes the card's height.

**Do it your way.** The three steps above are the proven pattern, not a
rulebook — after step 1 the result is yours via the `bs-leak-data` event
(section 8): put the % on the earnings page, in the onboarding checklist, on
a settings row. More placement ideas in section 12.

---

## 8. Data reference

### 8.1 The `bs-leak-check:result` message

Sent by the widget (origin `https://branditscan.com`) when a scan completes:

| Field | Type | Meaning |
|---|---|---|
| `stagename` | string | **echoed on every result** — the exact name the scan ran for. Verify it against your logged-in creator's before caching (the module does). |
| `standing` | `'at_risk' \| 'good' \| 'account'` | `account` = the name belongs to an active BranditScan member — **no scan data is returned** for it (the message is only `type` + `standing` + `stagename`). |
| `traffic` | `{ pct, surface }` \| null | **the headline number**: % of the creator's search traffic (web click-share, `surface:'web'`) or image traffic (`'images'`) going to pirate sites. |
| `share` | number | web click-share % to pirates (0 when the leak is images-only) |
| `piratePct` | number | % of scanned results that are pirated |
| `pirates` | number | total pirate results found (web + images) |
| `domains` | string[] ≤6 | the pirate domains found |
| `imagePirates` | number | pirated image results |
| `estimatedLoss` | string \| null | formatted $ loss estimate (e.g. `"$12.4K"`) — render only when it starts with `$` |
| `thumbs` | string[] ≤50 | thumbnails of confirmed-pirated Google Images results |
| `samples` | string[] ≤50 | images pulled straight off the pirate sites — `[]` on the first message, see below |

**`samples` arrives in a second message.** The site-hosted pull finishes
after the main scan, so the result is posted twice: first with
`samples: []` (render your badge immediately — don't wait), then re-posted
identical-but-enriched when the pull lands. Keep the latest verified message
per stagename (the module does) and you get both for free. The gap is
~100–500ms when cached on our side, several seconds on a first-ever pull.

Also from the widget: `bs-leak-check:height` `{ height }` on every layout
change, and inbound `bs-leak-check:scan` `{ stagename }` to drive a scan from
your own UI.

### 8.2 Rendering the image URLs — rules that matter

- `thumbs`/`samples` point **directly at images on the pirate sites/CDNs**,
  not at copies we host. Always set `referrerPolicy = 'no-referrer'` (defeats
  most hotlink protection), always preload off-DOM and render only survivors,
  and expect attrition — which is why you store 12 and show 8.
- They are the creator's own **explicit** content. Render them **blurred**
  (`blur(7px)` + `scale(1.15)` inside `overflow:hidden`). See section 11 —
  this is not optional.
- Thumbs die over time **as takedowns succeed**. That's the product working;
  the preload + 24h refresh handle it.
- A few URLs may be plain `http` and get blocked as mixed content — the
  preload's `onerror` path covers that too.

---

## 9. Caching & limits

Four cache layers stack so the funnel is nearly free at steady state:

| Layer | Where | TTL | What it saves |
|---|---|---|---|
| Your `localStorage` cache | creator's browser (your origin) | 24h | everything — most pageviews render with zero BranditScan traffic |
| The embed's own result cache | creator's browser (inside the iframe) | 24h | repeat iframe loads render + re-post **without any network call** — even a probe on every pageview costs no scans |
| Our scan cache | BranditScan servers | ~14 days per stagename | probes that do reach us usually land here — free, instant |
| Our samples cache | BranditScan servers | ~14 days per stagename | the site-hosted image pull |

Scans run inside the iframe — i.e. from the **creator's own browser IP** — so
rate limits scope per creator automatically and your server IPs are never
involved. The limits are generous for real usage and hitting one is
invisible by design: the probe simply gets no result that day, nothing
renders, and tomorrow's pageview retries. Names that belong to an active
BranditScan member (`standing: 'account'`) don't consume scans at all.

A single creator's steady-state footprint: one fresh scan every couple of
weeks, one cheap cache-hit request per day, everything else served from
browser caches.

**Rolling out to a large base:** the first-ever scan per creator is the only
expensive event. If you're switching the funnel on for tens of thousands of
creators at once, [tell us first](mailto:help@branditscan.com) — we'll make
sure capacity is ready for your launch window, or help you enable it per
cohort.

---

## 10. Developing locally

- The `no-referrer` policy in the snippets makes localhost work out of the
  box. If you remove it, scans will 403 **silently** (widget renders, never
  posts a result) — that's the #1 integration time-sink, see section 3.
- Fresh scans (names never scanned before) are rate-limited — pick a couple
  of test names and reuse them; repeat scans of the same name serve from
  cache and are effectively free.
- To reset your client state: clear `bs-leak-check:*` keys from your
  origin's localStorage. (The embed keeps its own 24h cache inside the
  iframe — reuse the same test name and expect cached results within a day;
  that's the production behavior your users get too.)
- Automated-browser gotcha: background tabs throttle
  `scrollIntoView({ behavior:'smooth' })` — use `behavior:'auto'` in tests
  (the samples already do).

---

## 11. Copy & design rules

These came from live creator feedback on the first production deployment of
this funnel — they materially changed conversion:

1. **Opportunity beats fear.** "Your content is getting leaked" + red
   `▲ 18% traffic loss` felt intrusive. What works: state the finding plainly
   ("we found your content on pirate sites"), sell the fix in the sub-line,
   and frame the stat green and positive — **`+18% revenue to reclaim`**.
   Exception: the **nav badge stays red/alerting** — it has to win a glance;
   the banner has to convert a click. Alert → attention; reframe → action.
2. **Blur the leaked images. Always.** The strip proves the leaks are real
   without parading anyone's body on their own dashboard. The clear view
   belongs inside the full report where the creator chose to look. Creators
   asked for exactly this.
3. **One number per surface.** The sub-line never repeats the percentage the
   stat already shows — repetition reads as pressure.
4. **Never lie about account state.** The moment `standing` says `account`,
   every upsell on every surface flips to "protected" framing. Pitching a
   paying member converts nobody and burns trust.
5. **Honest free-vs-paid comparison beats pure upsell.** Say plainly that
   self-submission is free and the subscription is for volume/automation —
   with a fit line on each card telling the creator which one is for them.
6. **Don't promise "free takedowns" unless you've provisioned the Self
   Submissions widget** (section 5.2). Without it, the funnel sells the free
   *report* and the automated protection — never a service your page can't
   deliver.
7. **"Powered by BranditScan"** on the banner and page header. The
   co-branding builds the trust the signup step converts.

---

## 12. More placement ideas

All of these read from the same cache — zero extra BranditScan traffic:

| Placement | What to show | Why it works |
|---|---|---|
| **Settings page** | the compact banner (section 7 markup as a list row) | creators visit settings with account-hygiene intent |
| **Earnings / payouts page** | one line: `"≈ $12.4K/yr of your revenue is at risk — see where"` (only when `estimatedLoss` starts with `$`) | money context makes the loss number land hardest |
| **Onboarding checklist** | "Check your name for leaks" as a task, `at_risk` marks it urgent | new creators are in setup mode — highest action rate |
| **Post-upload success screen** | small note: `"6 sites are already reposting creators like you — check yours"` | the moment they think about content spreading |
| **Empty states** (no messages, no sales yet) | the generic banner | attention with nothing competing for it |
| **In-app inbox / announcements** | a pinned card linking to `/takedowns` | platforms with strong inbox habits get cheap reach |

Rule of thumb: **nav badge + one banner + the Takedowns page** is the proven
trio. Add more surfaces gradually — every surface shows the *same* number
(one story), and none of them should nag a `good` or `account` creator.

---

## 13. Gotchas

The complete field list from production integrations. Each one is already
handled in the snippets above — this is your debugging map:

1. **Localhost/blocked referrer → silent scan 403.** Widget renders,
   never posts `result` (only `height`). Fix: `no-referrer` on every frame.
2. **`samples` arrives in a second message** (`[]` first). Keep the latest
   verified message; hidden probes must linger ~15s, not exit on first result.
3. **`standing:'account'` carries no scan data.** Don't reach for `traffic`
   or `pirates` on member results.
4. **`estimatedLoss` can arrive unformatted** (`"21"`). Only render values
   starting with `$`.
5. **Hidden iframes:** `display:none` breaks the widget's boot;
   `loading="lazy"` never loads off-screen. Park at `left:-9999px`.
6. **First-ever scans can exceed 90s** (the widget retries internally and
   self-heals). Probe timeouts must be generous — 180s; it's invisible,
   waiting is free.
7. **Cache poisoning via the visible embed:** creators can type any name into
   the report page. Verify the `stagename` echo on every message — only the
   logged-in creator's own result may be cached.
8. **Hotlinked thumbnails die unpredictably.** Preload off-DOM, render
   survivors only, keep a no-strip fallback (the banner works without it).
9. **Measured containers** (carousels): re-trigger measurement (dispatch a
   window `resize`) after the module changes the banner's height.
10. **Decide once per pageview.** A late probe result must never yank a
    loaded widget out from under a reading visitor — hence the
    `dataset.decided` / `dataset.done` guards everywhere.

---

## 14. Partner tier

Everything above is self-serve. For platforms that want to go deeper, there's
a credentialed partner tier — used by our largest integrations:

- **Higher self-submission volume, custom branding and launch limits** on
  the widget you provisioned in 5.2;
- **Server-side scan data** (JSON API — render badges backend-side, no
  iframes);
- **One-click SSO** into BranditScan for your creators (signup wizard
  pre-filled, accounts linked to your platform, attributed);
- **Custom widget skins** that disappear into your product, and a bundled
  per-platform preset param.

[Book an intro call](https://branditscan.com/developers#book-a-call) or email
[help@branditscan.com](mailto:help@branditscan.com) — provisioning is
same-day.

---

## 15. QA checklist

Run on a staging creator account before shipping:

- [ ] First dashboard load: probe iframe appears off-screen, disappears
      within ~3 minutes, `localStorage['bs-leak-check:{stagename}']` is set.
- [ ] Second load: **no** probe iframe, badge/banner render instantly from
      cache, zero requests to `branditscan.com`.
- [ ] At-risk creator: nav badge shows `▲ {pct}%`; banner shows name
      headline, domains line, green stat, blurred strip; Takedowns page
      auto-runs the report and fills `data-bs-pct`.
- [ ] Clean creator (`good`): no badge, generic banner, leak section hidden
      on the Takedowns page.
- [ ] Member (`account`): green `✓` badge, protected banner, no upsell
      anywhere.
- [ ] On the Takedowns page, manually scan a **different** name in the
      report: the cached result for the logged-in creator is unchanged.
- [ ] Self Submissions widget (if provisioned): hidden on load, reveals on
      the free card's CTA and on `#takedown-widget` deep links, iframe `src`
      set only at reveal.
- [ ] Thumbs strip: every visible tile is blurred; broken images never
      render.
- [ ] Block `branditscan.com` in devtools and reload: no console errors, no
      broken UI — every surface silently shows its default state.

---

## 16. For AI coding agents

You are integrating the BranditScan protection funnel into a creator
platform. This document is self-contained — the snippets in sections 4–7 are
production-ready and the `data-bs-*` attribute names are a contract; keep
them exactly as written.

**Before writing code, ask the operator for:**

1. The template variable holding the logged-in creator's **stagename**, and
   where layout-level scripts go (the module must run on every dashboard
   page).
2. Their BranditScan **affiliate slug** (from the Earnings Hub; optional but
   recommended — it's how the platform gets paid).
3. Which file renders the **nav** and which page is the **dashboard** (for
   the banner). Whether a "Takedowns" page/route exists or should be created.
4. Whether they have a **Self Submissions signing secret**. If not, have the
   operator email [help@branditscan.com](mailto:help@branditscan.com) for one
   **now** (same-day turnaround) and build section 5.2 anyway — it is the
   preferred conversion path. Ship everything else immediately; keep the
   free-submission card hidden until the widget is live (never promise
   takedowns the page can't deliver yet).

**First, confirm the shape.** Ask the operator which integration they want:
the **full funnel** (recommended — steps 1–4), a **page only** (section 5,
no nav badge or banner), or **iframe-only / custom** (a plain embed or a
headless data feed — the Appendix). Every shape is valid; build exactly what
they choose and skip the rest without warnings.

**For the full funnel, implement in the step order:** module (section 4) →
Takedowns page (section 5: 5.3 the report, then 5.1 the cards, then 5.2 the
widget — request the secret on day one, wire it the moment it arrives) →
menu item (section 6) → banner (section 7). Verify with the QA checklist
(15) after each step. Do not remove `no-referrer`
attributes, do not swap the off-screen probe for `display:none`, do not
unblur the image strip, and do not render any surface for creators without a
stagename.

---

## Appendix — lighter shapes & the full embed reference

Everything in this guide is optional except the iframe itself. All of these
are valid integrations — pick what fits and skip the rest:

| Shape | What you build | Sections |
|---|---|---|
| **Full funnel** *(recommended)* | module + page + menu item + banner | 4 → 5 → 6 → 7 |
| **Page only** | the dedicated page with the widgets, plain nav link, no badge or banner | 5 |
| **Iframe only** | one visible Leak Check embed anywhere — no module, no JS beyond the height listener | below |
| **Headless / custom** | the module alone; render the `bs-leak-data` payload in any UI you want | 4 + 8 + 12 |
| **Self Submissions only** | just the takedowns widget on a page (signing secret required) | 5.2 |

### The minimal shape — one iframe

No module, no cache to manage, no states to handle — the widget is its own
UI. Visitors type a name (or you prefill with `?stagename=`), the report
renders in place:

```html
<!-- BranditScan Leak Check widget -->
<iframe src="https://branditscan.com/embed/leak-check?aff=your-affiliate-slug"
        title="Free Leak Check by BranditScan"
        style="display:block;width:100%;max-width:960px;margin:0 auto;height:620px;border:0;"
        referrerpolicy="no-referrer" scrolling="no"></iframe>
<script>
  /* auto-height — the only wiring an iframe-only integration needs */
  window.addEventListener("message", function (e) {
    if (e.origin !== "https://branditscan.com" || !e.data || e.data.type !== "bs-leak-check:height") return;
    document.querySelectorAll("iframe[src^='https://branditscan.com/embed/leak-check']").forEach(function (f) {
      if (f.contentWindow === e.source) f.style.height = Math.ceil(e.data.height) + "px";
    });
  });
</script>
```

### Leak Check embed — all URL parameters

| Parameter | What it does |
|---|---|
| `aff` | your affiliate slug — signups from the widget credit you |
| `stagename` | prefills the search and runs it immediately on load |
| `rail` | pins the side visual: `odometer` (money counter), `sweep` (before/after), `cases` (case studies), `loss` (average piracy loss); omit for rotation per load |
| `border` | `border=0` turns the animated shell border off |
| `blur` | `blur=0` renders result images unblurred (NSFW-blurred with click-to-reveal by default) |
| `branding` | `branding=0` hides the BranditScan endnote (white-label) |
| `w` | max-width cap in px (400–1400) for standalone previews — real embeds size via the host iframe |
| `t` | partner tier only — signed widget token; flips the CTAs to one-click SSO for linked members |

postMessage contract (`bs-leak-check:result` / `:height` out, `:scan` in):
section 8.

### Agency variant — one embed, a whole roster

Same scanner rebuilt for agencies: they add every model's stage name, the
widget scans each one and rolls it up into an agency report (models at risk,
revenue at risk, per-model drill-down). Own endpoint and postMessage
namespace:

```html
<iframe src="https://branditscan.com/embed/agency-leak-check"
        title="Agency Leak Check by BranditScan"
        style="display:block;width:100%;max-width:960px;margin:0 auto;height:620px;border:0;"
        referrerpolicy="no-referrer" scrolling="no"></iframe>
```

| Parameter | What it does |
|---|---|
| `stagenames` | comma-separated roster prefill that runs on load (capped per scan) |
| `aff` | your affiliate slug — same attribution as the single widget |
| `rail` | side visual (defaults to the money counter here) |
| `border` / `blur` / `branding` | same as the single widget |
| `w` | max-width cap in px (400–1400) for standalone previews |

```text
OUT  { type: "bs-agency-leak-check:height", height }
OUT  { type: "bs-agency-leak-check:result",
       models: [{ name, standing, piratePct, pirates }],
       atRisk, clean, protected, failed, total,
       avgPct, estimatedYearlyLoss, domains }
IN   { type: "bs-agency-leak-check:scan", stagenames: ["jane doe", "ava belle"] }
```

### Self Submissions widget — display parameters

All optional, on the `/embed/takedowns?t={token}` URL (the token itself:
section 5.2):

| Parameter | What it does |
|---|---|
| `funnel` | the built-in leak-check upsell card: `simple` (default), `pulse`, or personalized variants `teaser` / `revenue` / `loss` / `evidence` / `wall`; `funnel=off` removes it (recommended when the real report sits on the same page) |
| `branding` | `branding=0` runs white-label |
| `border` | `border=1` adds the animated shell border (off by default here) |
| `blur` | `blur=0` unblurs the funnel's image thumbnails |
| `celebrate` | `celebrate=1` adds the celebration reel of the creator's real removals |
| `logo` | URL of your logo for the widget header |

---

*Questions, launch-window capacity, signing secrets, partner tier:
[help@branditscan.com](mailto:help@branditscan.com). Custom skins and the
on-request APIs (Search, Piracy Data, SSO, Network Manager, Platform) are
documented on the [Developers page](https://branditscan.com/developers).*
