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:
- Paywalls-hosted VAI (easier): load VAI directly from
https://paywalls.net/. - 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:
- 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 thepage_viewevent. Do not callgtag('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.
- If you own the
- Ad server tags — set key-values on your ad server so line items can target (or exclude) traffic by classification
- 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__.vatandwindow.__PW_VAI__.actare present.
Common issues
- CSP blocks the script: ensure your CSP allows
https://paywalls.netinscript-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.jsreturns JavaScript that setswindow.__PW_VAI__.GET /pw/vai.jsonreturns the same data as JSON.GET /pw/jwks.jsonreturns public keys so partners can verify thejws.
Important operational requirements:
- These endpoints should be same-origin (avoid third-party script hosts for VAI).
vai.jsonshould be not cacheable (e.g.Cache-Control: private, no-store, max-age=0) because assertions are short-lived.vai.jsis 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
asyncordeferto the/pw/vai.jstag 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.jsimplementation, you can use either downstream.
Quick verification checklist
In a browser on the inventory domain:
- Open
https://YOUR_DOMAIN/pw/vai.jsonand confirm:dommatches the page hostnameexpis in the future and short-livedjwsis present
- Open the page and check in DevTools Console:
window.__PW_VAI__is definedwindow.__PW_VAI__.vatandwindow.__PW_VAI__.actlook reasonable
- Open
https://YOUR_DOMAIN/pw/jwks.jsonand confirm:- It returns a
keysarray and includes thekidreferenced by the VAI object
- It returns a
Common issues (fast fixes)
/pw/vai.jsis 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.jsonmust not be cached. dommismatch: 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.jsscript tag.