diff --git a/integrationExamples/gpt/azerionedgeRtdProvider_example.html b/integrationExamples/gpt/azerionedgeRtdProvider_example.html
new file mode 100644
index 000000000000..880fe5ed7060
--- /dev/null
+++ b/integrationExamples/gpt/azerionedgeRtdProvider_example.html
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Azerion Edge RTD
+
+
+
+
+
+ Segments:
+
+
+
diff --git a/modules/.submodules.json b/modules/.submodules.json
index 61d8c843d47b..cfa98b5ab324 100644
--- a/modules/.submodules.json
+++ b/modules/.submodules.json
@@ -63,6 +63,7 @@
"airgridRtdProvider",
"akamaiDapRtdProvider",
"arcspanRtdProvider",
+ "azerionedgeRtdProvider",
"blueconicRtdProvider",
"brandmetricsRtdProvider",
"browsiRtdProvider",
diff --git a/modules/azerionedgeRtdProvider.js b/modules/azerionedgeRtdProvider.js
new file mode 100644
index 000000000000..a162ce074aa4
--- /dev/null
+++ b/modules/azerionedgeRtdProvider.js
@@ -0,0 +1,143 @@
+/**
+ * This module adds the Azerion provider to the real time data module of prebid.
+ *
+ * The {@link module:modules/realTimeData} module is required
+ * @module modules/azerionedgeRtdProvider
+ * @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('./rtdModule/index.js').RtdSubmodule} RtdSubmodule
+ */
+
+const REAL_TIME_MODULE = 'realTimeData';
+const SUBREAL_TIME_MODULE = 'azerionedge';
+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://edge.hyth.io/js/${VERSION}/${publisherPath}azerion-edge.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: 'azerionedge',
+ 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 azerionedgeSubmodule = {
+ name: SUBREAL_TIME_MODULE,
+ init: init,
+ getBidRequestData: getBidRequestData,
+};
+
+submodule(REAL_TIME_MODULE, azerionedgeSubmodule);
diff --git a/modules/azerionedgeRtdProvider.md b/modules/azerionedgeRtdProvider.md
new file mode 100644
index 000000000000..2849bef3f637
--- /dev/null
+++ b/modules/azerionedgeRtdProvider.md
@@ -0,0 +1,112 @@
+---
+layout: page_v2
+title: azerion edge RTD Provider
+display_name: Azerion Edge RTD Provider
+description: Client-side contextual cookieless audiences.
+page_type: module
+module_type: rtd
+module_code: azerionedgeRtdProvider
+enable_download: true
+vendor_specific: true
+sidebarType: 1
+---
+
+# Azerion Edge RTD Provider
+
+Client-side contextual cookieless audiences.
+
+Azerion Edge RTD module helps publishers to capture users' interest
+audiences on their site, and attach these into the bid request.
+
+Maintainer: [azerion.com](https://www.azerion.com/)
+
+{:.no_toc}
+
+- TOC
+ {:toc}
+
+## Integration
+
+Compile the Azerion Edge RTD module (`azerionedgeRtdProvider`) into your Prebid build,
+along with the parent RTD Module (`rtdModule`):
+
+```bash
+gulp build --modules=rtdModule,azerionedgeRtdProvider
+```
+
+Set configuration via `pbjs.setConfig`.
+
+```js
+pbjs.setConfig(
+ ...
+ realTimeData: {
+ auctionDelay: 1000,
+ dataProviders: [
+ {
+ name: 'azerionedge',
+ waitForIt: true,
+ params: {
+ key: '',
+ bidders: ['improvedigital'],
+ process: {}
+ }
+ }
+ ]
+ }
+ ...
+}
+```
+
+### Parameter Description
+
+{: .table .table-bordered .table-striped }
+| Name | Type | Description | Notes |
+| :--- | :------- | :------------------ | :--------------- |
+| name | `String` | RTD sub module name | Always "azerionedge" |
+| 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 Azerion Edge script. | Optional. Defaults to `{}`. |
+
+## Context
+
+As all data collection is on behalf of the publisher and based on the consent the publisher has
+received from the user, this module does not require a TCF vendor configuration. Consent is
+provided to the module when the user gives the relevant permissions on the publisher website.
+
+As Prebid.js utilizes TCF vendor consent for the RTD module to load, the module needs to be labeled
+within the Vendor Exceptions.
+
+### Instructions
+
+If the Prebid GDPR enforcement is enabled, the module should be labeled
+as exception, as shown below:
+
+```js
+[
+ {
+ purpose: 'storage',
+ enforcePurpose: true,
+ enforceVendor: true,
+ vendorExceptions: ["azerionedge"]
+ },
+ ...
+]
+```
+
+## Testing
+
+To view an example:
+
+```bash
+gulp serve-fast --modules=rtdModule,azerionedgeRtdProvider
+```
+
+Access [http://localhost:9999/integrationExamples/gpt/azerionedgeRtdProvider_example.html](http://localhost:9999/integrationExamples/gpt/azerionedgeRtdProvider_example.html)
+in your browser.
+
+Run the unit tests:
+
+```bash
+npm test -- --file "test/spec/modules/azerionedgeRtdProvider_spec.js"
+```
diff --git a/src/adloader.js b/src/adloader.js
index 5309f3a3d42e..c2da26463203 100644
--- a/src/adloader.js
+++ b/src/adloader.js
@@ -20,6 +20,7 @@ const _approvedLoadExternalJSList = [
'hadron',
'medianet',
'improvedigital',
+ 'azerionedge',
'aaxBlockmeter',
'confiant',
'arcspan',
@@ -33,7 +34,7 @@ const _approvedLoadExternalJSList = [
'contxtful',
'id5',
'lucead',
-]
+];
/**
* Loads external javascript. Can only be used if external JS is approved by Prebid. See https://github.com/prebid/prebid-js-external-js-template#policy
diff --git a/test/spec/modules/azerionedgeRtdProvider_spec.js b/test/spec/modules/azerionedgeRtdProvider_spec.js
new file mode 100644
index 000000000000..f08aaebdf556
--- /dev/null
+++ b/test/spec/modules/azerionedgeRtdProvider_spec.js
@@ -0,0 +1,183 @@
+import { config } from 'src/config.js';
+import * as azerionedgeRTD from 'modules/azerionedgeRtdProvider.js';
+import { loadExternalScript } from '../../../src/adloader.js';
+
+describe('Azerion Edge 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: 'azerionedge', waitForIt: true };
+
+ let reqBidsConfigObj;
+ let storageStub;
+
+ beforeEach(function () {
+ config.resetConfig();
+ reqBidsConfigObj = { ortb2Fragments: { bidder: {} } };
+ window.azerionPublisherAudiences = sinon.spy();
+ storageStub = sinon.stub(azerionedgeRTD.storage, 'getDataFromLocalStorage');
+ });
+
+ afterEach(function () {
+ delete window.azerionPublisherAudiences;
+ storageStub.restore();
+ });
+
+ describe('initialisation', function () {
+ let returned;
+
+ beforeEach(function () {
+ returned = azerionedgeRTD.azerionedgeSubmodule.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://edge.hyth.io/js/v1/azerion-edge.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 = azerionedgeRTD.azerionedgeSubmodule.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://edge.hyth.io/js/v1/${key}/azerion-edge.min.js`;
+ expect(loadExternalScript.args[0][0]).to.deep.equal(expected);
+ });
+ });
+
+ describe('with process configuration', function () {
+ beforeEach(function () {
+ window.azerionPublisherAudiences.resetHistory();
+ loadExternalScript.resetHistory();
+ returned = azerionedgeRTD.azerionedgeSubmodule.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 () {
+ azerionedgeRTD.azerionedgeSubmodule.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));
+ azerionedgeRTD.azerionedgeSubmodule.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: 'azerionedge',
+ segment: [{ id: '1' }, { id: '2' }],
+ },
+ ],
+ },
+ };
+
+ it('for improvedigital by default', function () {
+ azerionedgeRTD.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 } };
+ azerionedgeRTD.setAudiencesToBidders(reqBidsConfigObj, config, audiences);
+ expect(reqBidsConfigObj.ortb2Fragments.bidder[bidder]).to.deep.equal(
+ expected
+ );
+ });
+ });
+ });
+});