VAI Busy Ops Guide

This page is the fastest way to put VAI on a page using a script tag.

Publishers can integrate VAI in two modes:

  1. Paywalls-hosted VAI (easier): load VAI directly from https://paywalls.net/.
  2. Publisher-hosted VAI (additional integration): serve VAI from the same origin as your inventory domain (typically via CDN integration - see CDN integration). This is also the approach used for cryptographically signed domain provenance that buyers can verify in RTB.

In either mode, your wrapper / ad stack reads VAI and passes it into:

  1. Analytics — add VAI classification as custom dimensions to your analytics. Two approaches depending on your setup (see examples below):
    • If you own the gtag('config') call: pass VAI dims directly into it — they'll appear on the page_view event. Do not call gtag('config') a second time or you'll get duplicate pageviews.
    • If gtag is already configured (e.g. by a tag manager or another script): use gtag('event', 'vai_classification', ...) to send a dedicated event with the VAI dims.
  2. Ad server tags — set key-values on your ad server so line items can target (or exclude) traffic by classification
  3. RTB — pass VAI into the bid stream via OpenRTB (e.g. Prebid ORTB2) so buyers can see validated actor data

If you need the full details (claims, signing, JWKS, and verification), see the VAI technical spec.


Mode 1: Paywalls-hosted VAI (easiest)

Script URL

Use the Paywalls-hosted script base:

<script src="https://paywalls.net/pw/vai.js"></script>

Option A: simplest script tag (blocking, no hook)

<!-- 1) Load VAI first (blocking, so it's available immediately) -->
<script src="https://paywalls.net/pw/vai.js"></script>

<!-- 2) Analytics: pick ONE of the two approaches below -->

<!-- Approach 1: You own the gtag config call.
     Pass VAI dims into config so they appear on the page_view event.
     ⚠️ Do NOT use this if gtag('config') is already called elsewhere
     — a second config fires a duplicate page_view. -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  var vaiConfig = {};
  var vai = window.__PW_VAI__;
  if (vai && vai.vat && vai.act) {
    vaiConfig.vai_vat = vai.vat;
    vaiConfig.vai_act = vai.act;
  }
  gtag('config', 'G-XXXXXXXXXX', vaiConfig);
</script>

<!-- Approach 2: gtag is already configured by another script.
     Send a custom event instead — no duplicate page_view. -->
<script>
  (function () {
    var vai = window.__PW_VAI__;
    if (!vai || !window.gtag) return;
    gtag('event', 'vai_classification', {
      vai_vat: vai.vat,
      vai_act: vai.act
    });
  })();
</script>

<!-- 3) Pass VAI into ad server and bid stream -->
<script>
  (function () {
    var vai = window.__PW_VAI__;
    if (!vai) return;

    // Ad server key-values (e.g. GPT)
    if (window.googletag && googletag.pubads) {
      googletag.pubads().setTargeting('vai_vat', vai.vat);
      googletag.pubads().setTargeting('vai_act', vai.act);
    }

    // Prebid ORTB2
    if (window.pbjs && pbjs.setConfig) {
      pbjs.setConfig({
        ortb2: {
          site: {
            ext: {
              data: {
                vai: { iss: vai.iss, dom: vai.dom }
              }
            }
          },
          user: {
            ext: {
              data: {
                vai: { iss: vai.iss, vat: vai.vat, act: vai.act, mstk: vai.mstk, jws: vai.jws }
              }
            }
          }
        }
      });
    }
  })();
</script>

Option B: hook-based setup (recommended for tag managers)

With the hook pattern, VAI may arrive after gtag('config') has already fired. The hook sends a vai_classification event to capture the data without risking a duplicate page_view.

<script>
  // Define the hook first
  window.__PW_VAI_HOOK__ = function (vai) {
    // Send a custom event (config already fired; don't call it again)
    if (window.gtag) {
      gtag('event', 'vai_classification', {
        vai_vat: vai.vat,
        vai_act: vai.act
      });
    }

    // Ad server key-values (e.g. GPT)
    if (window.googletag && googletag.pubads) {
      googletag.pubads().setTargeting('vai_vat', vai.vat);
      googletag.pubads().setTargeting('vai_act', vai.act);
    }

    // Prebid ORTB2
    if (window.pbjs && pbjs.setConfig) {
      pbjs.setConfig({
        ortb2: {
          site: {
            ext: {
              data: {
                vai: { iss: vai.iss, dom: vai.dom }
              }
            }
          },
          user: {
            ext: {
              data: {
                vai: { iss: vai.iss, vat: vai.vat, act: vai.act, mstk: vai.mstk, jws: vai.jws }
              }
            }
          }
        }
      });
    }

    // Optional: expose it for debugging
    window.__PW_VAI__ = vai;
  };
</script>

<script src="https://paywalls.net/pw/vai.js"></script>

Quick verification

  • In DevTools Console, confirm window.__PW_VAI__ is defined.
  • Confirm window.__PW_VAI__.vat and window.__PW_VAI__.act are present.

Common issues

  • CSP blocks the script: ensure your CSP allows https://paywalls.net in script-src.
  • Ordering issues (tag manager): use the hook approach and define window.__PW_VAI_HOOK__ before loading the script.

Mode 2: Publisher-hosted VAI (same-origin + domain provenance)

Use this mode when you want VAI to be served from your inventory domain (same-origin), and when you want buyers to be able to verify publisher-signed domain provenance via the jws + your jwks.json.

This mode typically requires CDN integration so VAI endpoints are present on your public surface. See CDN integration for setup details.

Prerequisites (must already be live)

Before you touch page code, confirm these endpoints work on your inventory domain:

  • GET /pw/vai.js returns JavaScript that sets window.__PW_VAI__.
  • GET /pw/vai.json returns the same data as JSON.
  • GET /pw/jwks.json returns public keys so partners can verify the jws.

Important operational requirements:

  • These endpoints should be same-origin (avoid third-party script hosts for VAI).
  • vai.json should be not cacheable (e.g. Cache-Control: private, no-store, max-age=0) because assertions are short-lived.
  • vai.js is a static loader and MAY be cached (e.g. Cache-Control: public, max-age=300) — it contains no per-request data.
  • Don’t vary HTML by VAI (keep VAI cache-friendly; VAI is metadata, not page personalization).

If you don’t have these endpoints yet, start with CDN integration and then follow the VAI technical spec.


Option A: simplest script tag (blocking, no hook)

Use this when you control the HTML and can load VAI before your wrapper reads it.

<!-- 1) Load VAI first (blocking, so it's available before gtag config) -->
<script src="/pw/vai.js"></script>

<!-- 2) Google tag — VAI dims go into config so page_view is decorated -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  var vaiConfig = {};
  var vai = window.__PW_VAI__;
  if (vai && vai.vat && vai.act) {
    vaiConfig.vai_vat = vai.vat;
    vaiConfig.vai_act = vai.act;
  }
  gtag('config', 'G-XXXXXXXXXX', vaiConfig);
</script>

<!-- 3) Pass VAI into ad server and bid stream -->
<script>
  (function () {
    var vai = window.__PW_VAI__;
    if (!vai) return;

    // Ad server key-values (e.g. GPT)
    if (window.googletag && googletag.pubads) {
      googletag.pubads().setTargeting('vai_vat', vai.vat);
      googletag.pubads().setTargeting('vai_act', vai.act);
    }

    // Prebid ORTB2
    if (window.pbjs && pbjs.setConfig) {
      pbjs.setConfig({
        ortb2: {
          site: {
            ext: {
              data: {
                vai: { iss: vai.iss, dom: vai.dom }
              }
            }
          },
          user: {
            ext: {
              data: {
                vai: { iss: vai.iss, vat: vai.vat, act: vai.act, mstk: vai.mstk, jws: vai.jws }
              }
            }
          }
        }
      });
    }
  })();
</script>

Notes:

  • Don’t add async or defer to the /pw/vai.js tag if your next script assumes VAI is already present.
  • If your environment can’t guarantee ordering (common with tag managers), use the hook approach below.

Option B: hook-based setup (recommended for tag managers)

If you need strict ordering without relying on script execution timing, define a hook before loading /pw/vai.js.

With the hook pattern, VAI arrives asynchronously — gtag('config') has almost certainly already fired. The hook sends a vai_classification event to capture the data without risking a duplicate page_view.

<script>
  // 1) Define the hook first
  window.__PW_VAI_HOOK__ = function (vai) {
    // Send a custom event (config already fired; don't call it again)
    if (window.gtag) {
      gtag('event', 'vai_classification', {
        vai_vat: vai.vat,
        vai_act: vai.act
      });
    }

    // Ad server key-values (e.g. GPT)
    if (window.googletag && googletag.pubads) {
      googletag.pubads().setTargeting('vai_vat', vai.vat);
      googletag.pubads().setTargeting('vai_act', vai.act);
    }

    // Prebid ORTB2
    if (window.pbjs && pbjs.setConfig) {
      pbjs.setConfig({
        ortb2: {
          site: {
            ext: {
              data: {
                vai: { iss: vai.iss, dom: vai.dom }
              }
            }
          },
          user: {
            ext: {
              data: {
                vai: { iss: vai.iss, vat: vai.vat, act: vai.act, mstk: vai.mstk, jws: vai.jws }
              }
            }
          }
        }
      });
    }

    // Optional: expose it for debugging
    window.__PW_VAI__ = vai;
  };
</script>

<!-- 2) Load VAI (same-origin) -->
<script src="/pw/vai.js"></script>

Notes:

  • The hook should be a simple function (no dependencies) and should be defined as early as possible.
  • If both the hook and window.__PW_VAI__ are supported by your /pw/vai.js implementation, you can use either downstream.

Quick verification checklist

In a browser on the inventory domain:

  1. Open https://YOUR_DOMAIN/pw/vai.json and confirm:
    • dom matches the page hostname
    • exp is in the future and short-lived
    • jws is present
  2. Open the page and check in DevTools Console:
    • window.__PW_VAI__ is defined
    • window.__PW_VAI__.vat and window.__PW_VAI__.act look reasonable
  3. Open https://YOUR_DOMAIN/pw/jwks.json and confirm:
    • It returns a keys array and includes the kid referenced by the VAI object

Common issues (fast fixes)

  • /pw/vai.js is 404: the endpoint isn’t deployed on the public surface. Fix routing/edge deployment.
  • CSP blocks the script: ensure your CSP allows same-origin scripts (typically script-src 'self' ...).
  • VAI loads but doesn't update: check caching headers; vai.json must not be cached.
  • dom mismatch: VAI must be bound to the inventory domain; ensure you’re testing on the real host.
  • Hook not called: make sure window.__PW_VAI_HOOK__ is defined before the /pw/vai.js script tag.