diff --git a/integrationExamples/gpt/azerionRtdProvider_example.html b/integrationExamples/gpt/azerionRtdProvider_example.html
new file mode 100644
index 00000000000..978e9c8646e
--- /dev/null
+++ b/integrationExamples/gpt/azerionRtdProvider_example.html
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Azerion RTD
+
+
+
+
+
+ Segments:
+
+
+
diff --git a/modules/.submodules.json b/modules/.submodules.json
index 61d8c843d47..dcd3699d339 100644
--- a/modules/.submodules.json
+++ b/modules/.submodules.json
@@ -63,6 +63,7 @@
"airgridRtdProvider",
"akamaiDapRtdProvider",
"arcspanRtdProvider",
+ "azerionRtdProvider",
"blueconicRtdProvider",
"brandmetricsRtdProvider",
"browsiRtdProvider",
diff --git a/modules/azerionRtdProvider.js b/modules/azerionRtdProvider.js
new file mode 100644
index 00000000000..8dda0aa8fb8
--- /dev/null
+++ b/modules/azerionRtdProvider.js
@@ -0,0 +1,129 @@
+/**
+ * This module adds the Azerion provider to the real time data module of prebid.
+ *
+ * The {@link module:modules/realTimeData} module is required
+ * @module modules/azerionRtdProvider
+ * @requires module:modules/realTimeData
+ */
+import { submodule } from '../src/hook.js';
+import { mergeDeep } from '../src/utils.js';
+import { getStorageManager } from '../src/storageManager.js';
+import { loadExternalScript } from '../src/adloader.js';
+import { MODULE_TYPE_RTD } from '../src/activities/modules.js';
+
+/**
+ * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule
+ */
+
+const REAL_TIME_MODULE = 'realTimeData';
+const SUBREAL_TIME_MODULE = 'azerion';
+export const STORAGE_KEY = 'ht-pa-v1-a';
+
+export const storage = getStorageManager({
+ moduleType: MODULE_TYPE_RTD, moduleName: SUBREAL_TIME_MODULE
+});
+
+/**
+ * Get script url to load
+ *
+ * @param {Object} config
+ *
+ * @return {String}
+ */
+function getScriptURL(config) {
+ const VERSION = 'v1';
+ const key = config.params?.key;
+ const publisherPath = key ? `${key}/` : '';
+ return `https://pa.hyth.io/js/${VERSION}/${publisherPath}htpa.min.js`;
+}
+
+/**
+ * Attach script tag to DOM
+ *
+ * @param {Object} config
+ *
+ * @return {void}
+ */
+export function attachScript(config) {
+ const script = getScriptURL(config);
+ loadExternalScript(script, SUBREAL_TIME_MODULE, () => {
+ if (typeof window.azerionPublisherAudiences === 'function') {
+ window.azerionPublisherAudiences(config.params?.process || {});
+ }
+ });
+}
+
+/**
+ * Fetch audiences info from localStorage.
+ *
+ * @return {Array} Audience ids.
+ */
+export function getAudiences() {
+ try {
+ const data = storage.getDataFromLocalStorage(STORAGE_KEY);
+ return JSON.parse(data).map(({id}) => id);
+ } catch (_) {
+ return [];
+ }
+}
+
+/**
+ * Pass audience data to configured bidders, using ORTB2
+ *
+ * @param {Object} reqBidsConfigObj
+ * @param {Object} config
+ * @param {Array} audiences
+ *
+ * @return {void}
+ */
+export function setAudiencesToBidders(reqBidsConfigObj, config, audiences) {
+ const defaultBidders = ['improvedigital'];
+ const bidders = config.params?.bidders || defaultBidders;
+ bidders.forEach((bidderCode) => mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, {[bidderCode]: {
+ user: {
+ data: [
+ { name: 'azerion', ext: { segtax: 4 }, segment: audiences.map(id => ({id})) }
+ ]
+ }
+ }}));
+}
+
+/**
+ * Module initialisation.
+ *
+ * @param {Object} config
+ * @param {Object} userConsent
+ *
+ * @return {boolean}
+ */
+function init(config, userConsent) {
+ attachScript(config);
+ return true;
+}
+
+/**
+ * Real-time user audiences retrieval
+ *
+ * @param {Object} reqBidsConfigObj
+ * @param {function} callback
+ * @param {Object} config
+ * @param {Object} userConsent
+ *
+ * @return {void}
+ */
+export function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) {
+ const audiences = getAudiences();
+ if (audiences.length > 0) {
+ setAudiencesToBidders(reqBidsConfigObj, config, audiences);
+ }
+ callback();
+}
+
+/** @type {RtdSubmodule} */
+export const azerionSubmodule = {
+ name: SUBREAL_TIME_MODULE,
+ init: init,
+ getBidRequestData: getBidRequestData,
+};
+
+submodule(REAL_TIME_MODULE, azerionSubmodule);
diff --git a/modules/azerionRtdProvider.md b/modules/azerionRtdProvider.md
new file mode 100644
index 00000000000..5d074f6a96a
--- /dev/null
+++ b/modules/azerionRtdProvider.md
@@ -0,0 +1,94 @@
+---
+layout: page_v2
+title: azerion RTD Provider
+display_name: Azerion RTD Provider
+description: Real Time audience generator.
+page_type: module
+module_type: rtd
+module_code: azerionRtdProvider
+enable_download: true
+vendor_specific: true
+sidebarType: 1
+---
+
+# Azerion RTD Provider
+
+Azerion RTD is designed to help publishers find its users interests
+while providing these interests to buyers in the bid request. All this without
+exposing data to thir-party services.
+
+{:.no_toc}
+
+- TOC
+ {:toc}
+
+## Integration
+
+Compile the Azerion RTD module (`azerionRtdProvider`) into your Prebid build,
+along with the parent RTD Module (`rtdModule`):
+
+```bash
+gulp build --modules=rtdModule,azerionRtdProvider,appnexusBidAdapter,improvedigitalBidAdapter
+```
+
+Set configuration via `pbjs.setConfig`.
+
+```js
+pbjs.setConfig(
+ ...
+ realTimeData: {
+ auctionDelay: 1000,
+ dataProviders: [
+ {
+ name: 'azerion',
+ waitForIt: true,
+ params: {
+ publisherId: 'publisherId',
+ bidders: ['appnexus', 'improvedigital'],
+ process: {}
+ }
+ }
+ ]
+ }
+ ...
+}
+```
+
+### Parameter Description
+
+{: .table .table-bordered .table-striped }
+| Name | Type | Description | Notes |
+| :--- | :------- | :------------------ | :--------------- |
+| name | `String` | RTD sub module name | Always "azerion" |
+| waitForIt | `Boolean` | Required to ensure that the auction is delayed for the module to respond. | Optional. Defaults to false but recommended to true. |
+| params.key | `String` | Publisher partner specific key | Optional |
+| params.bidders | `Array` | Bidders with which to share segment information | Optional. Defaults to "improvedigital". |
+| params.process | `Object` | Configuration for the publisher audiences script. | Optional. Defaults to `{}`. |
+
+### Configuration `process` Description
+
+{: .table .table-bordered .table-striped }
+| Name | Type | Description | Notes |
+| :------------- | :-------- | :----------------------------------------------- | :-------------------------- |
+| process.optout | `Boolean` | Disables the process of audiences for the users. | Optional. Defaults to false |
+
+## Testing
+
+To view an example:
+
+```bash
+gulp serve-fast --modules=rtdModule,azerionRtdProvider,appnexusBidAdapter,improvedigitalBidAdapter
+```
+
+Access [http://localhost:9999/integrationExamples/gpt/azerionRtdProvider_example.html](http://localhost:9999/integrationExamples/gpt/azerionRtdProvider_example.html)
+in your browser.
+
+Run the unit tests:
+
+```bash
+npm test -- --file "test/spec/modules/azerionRtdProvider_spec.js"
+```
+
+## Support
+
+If you require further assistance please contact [support@azerion.com](mailto:support@azerion.com).
diff --git a/src/adloader.js b/src/adloader.js
index 5309f3a3d42..b756a230362 100644
--- a/src/adloader.js
+++ b/src/adloader.js
@@ -20,6 +20,7 @@ const _approvedLoadExternalJSList = [
'hadron',
'medianet',
'improvedigital',
+ 'azerion',
'aaxBlockmeter',
'confiant',
'arcspan',
diff --git a/test/spec/modules/azerionRtdProvider_spec.js b/test/spec/modules/azerionRtdProvider_spec.js
new file mode 100644
index 00000000000..b5d94569913
--- /dev/null
+++ b/test/spec/modules/azerionRtdProvider_spec.js
@@ -0,0 +1,152 @@
+import {config} from 'src/config.js';
+import * as azerionRTD from 'modules/azerionRtdProvider.js';
+import {loadExternalScript} from '../../../src/adloader.js';
+
+describe('Azerion RTD submodule', function () {
+ const STORAGE_KEY = 'ht-pa-v1-a';
+ const USER_AUDIENCES = [{id: '1', visits: 123}, {id: '2', visits: 456}];
+
+ const key = 'publisher123';
+ const bidders = ['appnexus', 'improvedigital'];
+ const process = {key: 'value'};
+ const dataProvider = { name: 'azerion', waitForIt: true };
+
+ let reqBidsConfigObj;
+ let storageStub;
+
+ beforeEach(function () {
+ config.resetConfig();
+ reqBidsConfigObj = {ortb2Fragments: {bidder: {}}};
+ window.azerionPublisherAudiences = sinon.spy();
+ storageStub = sinon.stub(azerionRTD.storage, 'getDataFromLocalStorage');
+ });
+
+ afterEach(function () {
+ delete window.azerionPublisherAudiences;
+ storageStub.restore();
+ });
+
+ describe('initialisation', function () {
+ let returned;
+
+ beforeEach(function() {
+ returned = azerionRTD.azerionSubmodule.init(dataProvider);
+ });
+
+ it('should return true', function () {
+ expect(returned).to.equal(true);
+ });
+
+ it('should load external script', function() {
+ expect(loadExternalScript.called).to.be.true;
+ });
+
+ it('should load external script with default versioned url', function() {
+ const expected = 'https://pa.hyth.io/js/v1/htpa.min.js';
+ expect(loadExternalScript.args[0][0]).to.deep.equal(expected);
+ });
+
+ it('should call azerionPublisherAudiencesStub with empty configuration', function() {
+ expect(window.azerionPublisherAudiences.args[0][0]).to.deep.equal({});
+ });
+
+ describe('with key', function() {
+ beforeEach(function() {
+ window.azerionPublisherAudiences.resetHistory();
+ loadExternalScript.resetHistory();
+ returned = azerionRTD.azerionSubmodule.init({...dataProvider, params: {key}});
+ })
+
+ it('should return true', function () {
+ expect(returned).to.equal(true);
+ });
+
+ it('should load external script with publisher id url', function() {
+ const expected = `https://pa.hyth.io/js/v1/${key}/htpa.min.js`;
+ expect(loadExternalScript.args[0][0]).to.deep.equal(expected);
+ });
+ });
+
+ describe('with process configuration', function() {
+ beforeEach(function() {
+ window.azerionPublisherAudiences.resetHistory();
+ loadExternalScript.resetHistory();
+ returned = azerionRTD.azerionSubmodule.init({...dataProvider, params: {process}});
+ })
+
+ it('should return true', function () {
+ expect(returned).to.equal(true);
+ });
+
+ it('should call azerionPublisherAudiencesStub with process configuration', function() {
+ expect(window.azerionPublisherAudiences.args[0][0]).to.deep.equal(process);
+ });
+ });
+ });
+
+ describe('gets audiences', function () {
+ let callbackStub;
+
+ beforeEach(function() {
+ callbackStub = sinon.mock();
+ })
+
+ describe('with empty storage', function() {
+ beforeEach(function() {
+ azerionRTD.azerionSubmodule.getBidRequestData(reqBidsConfigObj, callbackStub, dataProvider);
+ });
+
+ it('does not run apply audiences to bidders', function() {
+ expect(reqBidsConfigObj.ortb2Fragments.bidder).to.deep.equal({});
+ });
+
+ it('calls callback anyway', function() {
+ expect(callbackStub.called).to.be.true;
+ });
+ });
+
+ describe('with populate storage', function() {
+ beforeEach(function() {
+ storageStub.withArgs(STORAGE_KEY).returns(JSON.stringify(USER_AUDIENCES));
+ azerionRTD.azerionSubmodule.getBidRequestData(reqBidsConfigObj, callbackStub, dataProvider);
+ });
+
+ it('does apply audiences to bidder', function() {
+ const segments = reqBidsConfigObj.ortb2Fragments.bidder['improvedigital'].user.data[0].segment;
+ expect(segments).to.deep.equal([{id: '1'}, {id: '2'}]);
+ });
+
+ it('calls callback always', function() {
+ expect(callbackStub.called).to.be.true;
+ });
+ })
+ });
+
+ describe('sets audiences in bidder', function () {
+ const audiences = USER_AUDIENCES.map(({id}) => id);
+ const expected = {
+ 'user': {
+ 'data': [
+ {
+ 'ext': { 'segtax': 4 },
+ 'name': 'azerion',
+ 'segment': [ { 'id': '1' }, { 'id': '2' } ],
+ },
+ ],
+ },
+ };
+
+ it('for improvedigital by default', function () {
+ azerionRTD.setAudiencesToBidders(reqBidsConfigObj, dataProvider, audiences);
+ expect(reqBidsConfigObj.ortb2Fragments.bidder['improvedigital']).to.deep.equal(expected);
+ });
+
+ bidders.forEach((bidder) => {
+ it(`for ${bidder}`, function () {
+ const config = {...dataProvider, params: { bidders }};
+ azerionRTD.setAudiencesToBidders(reqBidsConfigObj, config, audiences);
+ expect(reqBidsConfigObj.ortb2Fragments.bidder[bidder]).to.deep.equal(expected);
+ });
+ });
+ });
+});