diff --git a/integrationExamples/gpt/permutiveRtdProvider_example.html b/integrationExamples/gpt/permutiveRtdProvider_example.html
new file mode 100644
index 00000000000..0814dcece5b
--- /dev/null
+++ b/integrationExamples/gpt/permutiveRtdProvider_example.html
@@ -0,0 +1,223 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Basic Prebid.js Example
+ Div-1
+
+
+
+
+
+
+ Div-2
+
+
+
+
+
+
+
diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js
new file mode 100644
index 00000000000..8ec215d3cca
--- /dev/null
+++ b/modules/permutiveRtdProvider.js
@@ -0,0 +1,171 @@
+/**
+ * This module adds permutive provider to the real time data module
+ * The {@link module:modules/realTimeData} module is required
+ * The module will add custom segment targeting to ad units of specific bidders
+ * @module modules/permutiveRtdProvider
+ * @requires module:modules/realTimeData
+ */
+import { getGlobal } from '../src/prebidGlobal.js'
+import { submodule } from '../src/hook.js'
+import { getStorageManager } from '../src/storageManager.js'
+import { deepSetValue, deepAccess, isFn, mergeDeep } from '../src/utils.js'
+import includes from 'core-js-pure/features/array/includes.js'
+
+export const storage = getStorageManager()
+
+function init (config, userConsent) {
+ return true
+}
+
+/**
+* Set segment targeting from cache and then try to wait for Permutive
+* to initialise to get realtime segment targeting
+*/
+export function initSegments (reqBidsConfigObj, callback, customConfig) {
+ const permutiveOnPage = isPermutiveOnPage()
+ const config = mergeDeep({
+ waitForIt: false,
+ params: {
+ maxSegs: 500,
+ acBidders: [],
+ overwrites: {}
+ }
+ }, customConfig)
+
+ setSegments(reqBidsConfigObj, config)
+
+ if (config.waitForIt && permutiveOnPage) {
+ window.permutive.ready(function () {
+ setSegments(reqBidsConfigObj, config)
+ callback()
+ }, 'realtime')
+ } else {
+ callback()
+ }
+}
+
+function setSegments (reqBidsConfigObj, config) {
+ const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits
+ const data = getSegments(config.params.maxSegs)
+ const utils = { deepSetValue, deepAccess, isFn, mergeDeep }
+
+ adUnits.forEach(adUnit => {
+ adUnit.bids.forEach(bid => {
+ const { bidder } = bid
+ const acEnabled = isAcEnabled(config, bidder)
+ const customFn = getCustomBidderFn(config, bidder)
+ const defaultFn = getDefaultBidderFn(bidder)
+
+ if (customFn) {
+ customFn(bid, data, acEnabled, utils, defaultFn)
+ } else if (defaultFn) {
+ defaultFn(bid, data, acEnabled)
+ } else {
+
+ }
+ })
+ })
+}
+
+function getCustomBidderFn (config, bidder) {
+ const overwriteFn = deepAccess(config, `params.overwrites.${bidder}`)
+
+ if (overwriteFn && isFn(overwriteFn)) {
+ return overwriteFn
+ } else {
+ return null
+ }
+}
+
+/**
+* Returns a function that receives a `bid` object, a `data` object and a `acEnabled` boolean
+* and which will set the right segment targeting keys for `bid` based on `data` and `acEnabled`
+* @param {string} bidder
+* @param {object} data
+*/
+function getDefaultBidderFn (bidder) {
+ const bidderMapper = {
+ appnexus: function (bid, data, acEnabled) {
+ if (acEnabled && data.ac && data.ac.length) {
+ deepSetValue(bid, 'params.keywords.p_standard', data.ac)
+ }
+ if (data.appnexus && data.appnexus.length) {
+ deepSetValue(bid, 'params.keywords.permutive', data.appnexus)
+ }
+
+ return bid
+ },
+ rubicon: function (bid, data, acEnabled) {
+ if (acEnabled && data.ac && data.ac.length) {
+ deepSetValue(bid, 'params.visitor.p_standard', data.ac)
+ }
+ if (data.rubicon && data.rubicon.length) {
+ deepSetValue(bid, 'params.visitor.permutive', data.rubicon)
+ }
+
+ return bid
+ },
+ ozone: function (bid, data, acEnabled) {
+ if (acEnabled && data.ac && data.ac.length) {
+ deepSetValue(bid, 'params.customData.0.targeting.p_standard', data.ac)
+ }
+
+ return bid
+ }
+ }
+
+ return bidderMapper[bidder]
+}
+
+export function isAcEnabled (config, bidder) {
+ const acBidders = deepAccess(config, 'params.acBidders') || []
+ return includes(acBidders, bidder)
+}
+
+export function isPermutiveOnPage () {
+ return typeof window.permutive !== 'undefined' && typeof window.permutive.ready === 'function'
+}
+
+/**
+* Returns all relevant segment IDs in an object
+*/
+export function getSegments (maxSegs) {
+ const legacySegs = readSegments('_psegs').map(Number).filter(seg => seg >= 1000000).map(String)
+ const _ppam = readSegments('_ppam')
+ const _pcrprs = readSegments('_pcrprs')
+
+ const segments = {
+ ac: [..._pcrprs, ..._ppam, ...legacySegs],
+ rubicon: readSegments('_prubicons'),
+ appnexus: readSegments('_papns'),
+ gam: readSegments('_pdfps')
+ }
+
+ for (const type in segments) {
+ segments[type] = segments[type].slice(0, maxSegs)
+ }
+
+ return segments
+}
+
+/**
+ * Gets an array of segment IDs from LocalStorage
+ * or returns an empty array
+ * @param {string} key
+ */
+function readSegments (key) {
+ try {
+ return JSON.parse(storage.getDataFromLocalStorage(key) || '[]')
+ } catch (e) {
+ return []
+ }
+}
+
+/** @type {RtdSubmodule} */
+export const permutiveSubmodule = {
+ name: 'permutive',
+ getBidRequestData: initSegments,
+ init: init
+}
+
+submodule('realTimeData', permutiveSubmodule)
diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md
new file mode 100644
index 00000000000..55bdf6420cf
--- /dev/null
+++ b/modules/permutiveRtdProvider.md
@@ -0,0 +1,99 @@
+# Permutive Real-time Data Submodule
+This submodule reads segments from Permutive and attaches them as targeting keys to bid requests. Using this module will deliver best targeting results, leveraging Permutive's real-time segmentation and modelling capabilities.
+
+## Usage
+Compile the Permutive RTD module into your Prebid build:
+```
+gulp build --modules=permutiveRtdProvider
+```
+You then need to enable the Permutive RTD in your Prebid configuration, using the below format:
+
+```javascript
+pbjs.setConfig({
+ ...,
+ realTimeData: {
+ auctionDelay: 50, // optional auction delay
+ dataProviders: [{
+ name: 'permutive',
+ waitForIt: true, // should be true if there's an `auctionDelay`
+ params: {
+ acBidders: ['appnexus', 'rubicon', 'ozone']
+ }
+ }]
+ },
+ ...
+})
+```
+
+## Supported Bidders
+The below bidders are currently support by the Permutive RTD module. Please reach out to your Permutive Account Manager to request support for any additional bidders.
+
+| Bidder | ID | First-party segments | Audience Connector |
+| ----------- | ---------- | -------------------- | ------------------ |
+| Xandr | `appnexus` | Yes | Yes |
+| Magnite | `rubicon` | Yes | Yes |
+| Ozone | `ozone` | No | Yes |
+
+* **First-party segments:** When enabling the respective Activation for a segment in Permutive, this module will automatically attach that segment to the bid request. There is no need to enable individual bidders in the module configuration, it will automatically reflect which SSP integrations you have enabled in Permutive. Permutive segments will be sent in the `permutive` key-value.
+
+* **Audience Connector:** You'll need to define which bidder should receive Audience Connector segments. You need to include the `ID` of any bidder in the `acBidders` array. Audience Connector segments will be sent in the `p_standard` key-value.
+
+
+## Parameters
+| Name | Type | Description | Default |
+| ----------------- | -------------------- | ------------------ | ------------------ |
+| name | String | This should always be `permutive` | - |
+| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` |
+| params | Object | | - |
+| params.acBidders | String[] | An array of bidders which should receive AC segments. Pleasee see `Supported Bidders` for bidder support and possible values. | `[]` |
+| params.maxSegs | Integer | Maximum number of segments to be included in either the `permutive` or `p_standard` key-value. | `500` |
+| params.overwrites | Object | See `Custom Bidder Setup` for details on how to define custom bidder functions. | `{}` |
+
+
+## Custom Bidder Setup
+You can overwrite the default bidder function, for example to include a different set of segments or to support additional bidders. The below example modifies what first-party segments Magnite receives (segments from `gam` instead of `rubicon`). As best practise we recommend to first call `defaultFn` and then only overwrite specific key-values. The below example only overwrites `permutive` while `p_standard` are still set by `defaultFn` (if `rubicon` is an enabled `acBidder`).
+
+```javascript
+pbjs.setConfig({
+ ...,
+ realTimeData: {
+ auctionDelay: 50,
+ dataProviders: [{
+ name: 'permutive',
+ waitForIt: true,
+ params: {
+ acBidders: ['appnexus', 'rubicon'],
+ maxSegs: 450,
+ overwrites: {
+ rubicon: function (bid, data, acEnabled, utils, defaultFn) {
+ if (defaultFn){
+ bid = defaultFn(bid, data, acEnabled)
+ }
+ if (data.gam && data.gam.length) {
+ utils.deepSetValue(bid, 'params.visitor.permutive', data.gam)
+ }
+ }
+ }
+ }
+ }]
+ },
+ ...
+})
+```
+Any custom bidder function will receive the following parameters:
+
+| Name | Type | Description |
+| ------------- |-------------- | --------------------------------------- |
+| bid | Object | The bidder specific bidder object. You will mutate this object to set the appropriate targeting keys. |
+| data | Object | An object containing Permutive segments |
+| data.appnexus | string[] | Segments exposed by the Xandr SSP integration |
+| data.rubicon | string[] | Segments exposed by the Magnite SSP integration |
+| data.gam | string[] | Segments exposed by the Google Ad Manager integration |
+| data.ac | string[] | Segments exposed by the Audience Connector |
+| acEnabled | Boolean | `true` if the current bidder in included in `params.acBidders` |
+| utils | {} | An object containing references to various util functions used by `permutiveRtdProvider.js`. Please make sure not to overwrite any of these. |
+| defaultFn | Function | The default function for this bidder. Please note that this can be `undefined` if there is no default function for this bidder (see `Supported Bidders`). The function expect the following parameters: `bid`, `data`, `acEnabled` and will return `bid`. |
+
+**Warning**
+
+The custom bidder function will mutate the `bid` object. Please be aware that this could break your bid request if you accidentally overwrite any fields other than the `permutive` or `p_standard` key-values or if you change the structure of the `bid` object in any way.
diff --git a/test/spec/modules/permutiveRtdProvider_spec.js b/test/spec/modules/permutiveRtdProvider_spec.js
new file mode 100644
index 00000000000..d55bbc58056
--- /dev/null
+++ b/test/spec/modules/permutiveRtdProvider_spec.js
@@ -0,0 +1,282 @@
+import { permutiveSubmodule, storage, getSegments, initSegments, isAcEnabled, isPermutiveOnPage } from 'modules/permutiveRtdProvider.js'
+import { deepAccess } from '../../../src/utils.js'
+
+describe('permutiveRtdProvider', function () {
+ before(function () {
+ const data = getTargetingData()
+ setLocalStorage(data)
+ })
+
+ after(function () {
+ const data = getTargetingData()
+ removeLocalStorage(data)
+ })
+
+ describe('permutiveSubmodule', function () {
+ it('should initalise and return true', function () {
+ expect(permutiveSubmodule.init()).to.equal(true)
+ })
+ })
+
+ describe('Getting segments', function () {
+ it('should retrieve segments in the expected structure', function () {
+ const data = transformedTargeting()
+ expect(getSegments(250)).to.deep.equal(data)
+ })
+ it('should enforce max segments', function () {
+ const max = 1
+ const segments = getSegments(max)
+
+ for (const key in segments) {
+ expect(segments[key]).to.have.length(max)
+ }
+ })
+ })
+
+ describe('Default segment targeting', function () {
+ it('sets segment targeting for Xandr', function () {
+ const data = transformedTargeting()
+ const adUnits = getAdUnits()
+ const config = getConfig()
+
+ initSegments({ adUnits }, callback, config)
+
+ function callback () {
+ adUnits.forEach(adUnit => {
+ adUnit.bids.forEach(bid => {
+ const { bidder, params } = bid
+
+ if (bidder === 'appnexus') {
+ expect(deepAccess(params, 'keywords.permutive')).to.eql(data.appnexus)
+ expect(deepAccess(params, 'keywords.p_standard')).to.eql(data.ac)
+ }
+ })
+ })
+ }
+ })
+ it('sets segment targeting for Rubicon', function () {
+ const data = transformedTargeting()
+ const adUnits = getAdUnits()
+ const config = getConfig()
+
+ initSegments({ adUnits }, callback, config)
+
+ function callback () {
+ adUnits.forEach(adUnit => {
+ adUnit.bids.forEach(bid => {
+ const { bidder, params } = bid
+
+ if (bidder === 'rubicon') {
+ expect(deepAccess(params, 'visitor.permutive')).to.eql(data.rubicon)
+ expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac)
+ }
+ })
+ })
+ }
+ })
+ it('sets segment targeting for Ozone', function () {
+ const data = transformedTargeting()
+ const adUnits = getAdUnits()
+ const config = getConfig()
+
+ initSegments({ adUnits }, callback, config)
+
+ function callback () {
+ adUnits.forEach(adUnit => {
+ adUnit.bids.forEach(bid => {
+ const { bidder, params } = bid
+
+ if (bidder === 'ozone') {
+ expect(deepAccess(params, 'customData.0.targeting.p_standard')).to.eql(data.ac)
+ }
+ })
+ })
+ }
+ })
+ })
+
+ describe('Custom segment targeting', function () {
+ it('sets custom segment targeting for Rubicon', function () {
+ const data = transformedTargeting()
+ const adUnits = getAdUnits()
+ const config = getConfig()
+
+ config.params.overwrites = {
+ rubicon: function (bid, data, acEnabled, utils, defaultFn) {
+ if (defaultFn) {
+ bid = defaultFn(bid, data, acEnabled)
+ }
+ if (data.gam && data.gam.length) {
+ utils.deepSetValue(bid, 'params.visitor.permutive', data.gam)
+ }
+ }
+ }
+
+ initSegments({ adUnits }, callback, config)
+
+ function callback () {
+ adUnits.forEach(adUnit => {
+ adUnit.bids.forEach(bid => {
+ const { bidder, params } = bid
+
+ if (bidder === 'rubicon') {
+ expect(deepAccess(params, 'visitor.permutive')).to.eql(data.gam)
+ expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac)
+ }
+ })
+ })
+ }
+ })
+ })
+
+ describe('Permutive on page', function () {
+ it('checks if Permutive is on page', function () {
+ expect(isPermutiveOnPage()).to.equal(false)
+ })
+ })
+
+ describe('AC is enabled', function () {
+ it('checks if AC is enabled for Xandr', function () {
+ expect(isAcEnabled({ params: { acBidders: ['appnexus'] } }, 'appnexus')).to.equal(true)
+ expect(isAcEnabled({ params: { acBidders: ['kjdbfkvb'] } }, 'appnexus')).to.equal(false)
+ })
+ it('checks if AC is enabled for Magnite', function () {
+ expect(isAcEnabled({ params: { acBidders: ['rubicon'] } }, 'rubicon')).to.equal(true)
+ expect(isAcEnabled({ params: { acBidders: ['kjdbfkb'] } }, 'rubicon')).to.equal(false)
+ })
+ it('checks if AC is enabled for Ozone', function () {
+ expect(isAcEnabled({ params: { acBidders: ['ozone'] } }, 'ozone')).to.equal(true)
+ expect(isAcEnabled({ params: { acBidders: ['kjdvb'] } }, 'ozone')).to.equal(false)
+ })
+ })
+})
+
+function setLocalStorage (data) {
+ for (const key in data) {
+ storage.setDataInLocalStorage(key, JSON.stringify(data[key]))
+ }
+}
+
+function removeLocalStorage (data) {
+ for (const key in data) {
+ storage.removeDataFromLocalStorage(key)
+ }
+}
+
+function getConfig () {
+ return {
+ name: 'permutive',
+ waitForIt: true,
+ params: {
+ acBidders: ['appnexus', 'rubicon', 'ozone'],
+ maxSegs: 500
+ }
+ }
+}
+
+function transformedTargeting () {
+ const data = getTargetingData()
+
+ return {
+ ac: [...data._pcrprs, ...data._ppam, ...data._psegs.filter(seg => seg >= 1000000)],
+ appnexus: data._papns,
+ rubicon: data._prubicons,
+ gam: data._pdfps
+ }
+}
+
+function getTargetingData () {
+ return {
+ _pdfps: ['gam1', 'gam2'],
+ _prubicons: ['rubicon1', 'rubicon2'],
+ _papns: ['appnexus1', 'appnexus2'],
+ _psegs: ['1234', '1000001', '1000002'],
+ _ppam: ['ppam1', 'ppam2'],
+ _pcrprs: ['pcrprs1', 'pcrprs2']
+ }
+}
+
+function getAdUnits () {
+ return [
+ {
+ code: '/19968336/header-bid-tag-0',
+ mediaTypes: {
+ banner: {
+ sizes: [
+ [300, 250],
+ [300, 600]
+ ]
+ }
+ },
+ bids: [
+ {
+ bidder: 'appnexus',
+ params: {
+ placementId: 13144370,
+ keywords: {
+ inline_kvs: ['1']
+ }
+ }
+ },
+ {
+ bidder: 'rubicon',
+ params: {
+ accountId: '9840',
+ siteId: '123564',
+ zoneId: '583584',
+ inventory: {
+ area: ['home']
+ },
+ visitor: {
+ inline_kvs: ['1']
+ }
+ }
+ },
+ {
+ bidder: 'ozone',
+ params: {
+ publisherId: 'OZONEGMG0001',
+ siteId: '4204204209',
+ placementId: '0420420500',
+ customData: [
+ {
+ settings: {},
+ targeting: {
+ inline_kvs: ['1', '2', '3', '4']
+ }
+ }
+ ],
+ ozoneData: {}
+ }
+ }
+ ]
+ },
+ {
+ code: '/19968336/header-bid-tag-1',
+ mediaTypes: {
+ banner: {
+ sizes: [
+ [728, 90],
+ [970, 250]
+ ]
+ }
+ },
+ bids: [
+ {
+ bidder: 'appnexus',
+ params: {
+ placementId: 13144370
+ }
+ },
+ {
+ bidder: 'ozone',
+ params: {
+ publisherId: 'OZONEGMG0001',
+ siteId: '4204204209',
+ placementId: '0420420500'
+ }
+ }
+ ]
+ }
+ ]
+}