diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index f15985dc6a4..a5f5cdf5aaf 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,31 +1,34 @@
 ### ๐Ÿ”ง Type of changes
 - [ ] new bid adapter
-- [ ] update bid adapter
+- [ ] bid adapter update
 - [ ] new feature
 - [ ] new analytics adapter
 - [ ] new module
+- [ ] module update
 - [ ] bugfix
 - [ ] documentation
 - [ ] configuration
+- [ ] dependency update
 - [ ] tech debt (test coverage, refactorings, etc.)
 
 ### โœจ What's the context?
-
-What's the context for the changes? Are there any
-
+What's the context for the changes?
 
 ### ๐Ÿง  Rationale behind the change
-
 Why did you choose to make these changes? Were there any trade-offs you had to consider?
 
+### ๐Ÿ”Ž New Bid Adapter Checklist
+- [ ] verify email contact works
+- [ ] NO fully dynamic hostnames
+- [ ] geographic host parameters are NOT required
+- [ ] direct use of HTTP is prohibited - *implement an existing Bidder interface that will do all the job*
+- [ ] if the ORTB is just forwarded to the endpoint, use the generic adapter - *define the new adapter as the alias of the generic adapter*
+- [ ] cover an adapter configuration with an integration test
 
 ### ๐Ÿงช Test plan
-
 How do you know the changes are safe to ship to production?
 
-
 ### ๐ŸŽ Quality check
-
 - [ ] Are your changes following [our code style guidelines](https://github.com/prebid/prebid-server-java/blob/master/docs/developers/code-style.md)?
 - [ ] Are there any breaking changes in your code?
 - [ ] Does your test coverage exceed 90%?
diff --git a/.github/workflows/code-path-changes.yml b/.github/workflows/code-path-changes.yml
new file mode 100644
index 00000000000..8d180480538
--- /dev/null
+++ b/.github/workflows/code-path-changes.yml
@@ -0,0 +1,34 @@
+name: Notify Code Path Changes
+
+on:
+  pull_request:
+    types: [opened, synchronize]
+    paths:
+      - '**'
+
+env:
+  OAUTH2_CLIENT_ID: ${{ secrets.OAUTH2_CLIENT_ID }}
+  OAUTH2_CLIENT_SECRET: ${{ secrets.OAUTH2_CLIENT_SECRET }}
+  OAUTH2_REFRESH_TOKEN: ${{ secrets.OAUTH2_REFRESH_TOKEN }}
+  GITHUB_REPOSITORY: ${{ github.repository }}
+  GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
+  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+jobs:
+  notify:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout Code
+        uses: actions/checkout@v3
+
+      - name: Set up Node.js
+        uses: actions/setup-node@v3
+        with:
+          node-version: '18'
+
+      - name: Install dependencies
+        run: npm install axios nodemailer
+
+      - name: Run Notification Script
+        run: |
+          node .github/workflows/scripts/send-notification-on-change.js
diff --git a/.github/workflows/pr-java-ci.yml b/.github/workflows/pr-java-ci.yml
index 3ead6423d9f..ee5780984e5 100644
--- a/.github/workflows/pr-java-ci.yml
+++ b/.github/workflows/pr-java-ci.yml
@@ -27,6 +27,7 @@ jobs:
         with:
           distribution: 'temurin'
           cache: 'maven'
+          cache-dependency-path: extra/pom.xml
           java-version: ${{ matrix.java }}
 
       - name: Build with Maven
diff --git a/.github/workflows/scripts/codepath-notification b/.github/workflows/scripts/codepath-notification
new file mode 100644
index 00000000000..7665b800901
--- /dev/null
+++ b/.github/workflows/scripts/codepath-notification
@@ -0,0 +1,19 @@
+# when a changed file paths matches the regex, send an alert email
+# structure of the file is:
+#
+# javascriptRegex : email address
+#
+# For example, in PBS Java, there are many paths that can belong to bid adapter:
+#
+# /src/main/java/org/prebid/server/bidder/BIDDER
+# /src/main/resources/static/bidder-params/BIDDER.json
+# /src/main/resources/bidder-config/BIDDER.yaml
+# /src//main/java/org/prebid/server/proto/openrtb/ext/request/BIDDER
+# /src/test/resources/org/prebid/server/it/openrtb2/BIDDER
+# /src/test/java/org/prebid/server/it/BIDDERTest.java
+# /src/test/java/org/prebid/server/bidder/BIDDER
+# /src/main/java/org/prebid/server/spring/config/bidder/BIDDERConfiguration.java
+#
+# The aim is to find a minimal set of regex patterns that matches any file in these paths
+
+/ix|Ix|ix.json|ix.yaml: pdu-supply-prebid@indexexchange.com
diff --git a/.github/workflows/scripts/send-notification-on-change.js b/.github/workflows/scripts/send-notification-on-change.js
new file mode 100644
index 00000000000..f4e4fdcd3ca
--- /dev/null
+++ b/.github/workflows/scripts/send-notification-on-change.js
@@ -0,0 +1,139 @@
+// send-notification-on-change.js
+//
+// called by the code-path-changes.yml workflow, this script queries github for
+// the changes in the current PR, checkes the config file for whether any of those
+// file paths are set to alert an email address, and sends email to multiple
+// parties if needed
+
+const fs = require('fs');
+const path = require('path');
+const axios = require('axios');
+const nodemailer = require('nodemailer');
+
+async function getAccessToken(clientId, clientSecret, refreshToken) {
+  try {
+    const response = await axios.post('https://oauth2.googleapis.com/token', {
+      client_id: clientId,
+      client_secret: clientSecret,
+      refresh_token: refreshToken,
+      grant_type: 'refresh_token',
+    });
+    return response.data.access_token;
+  } catch (error) {
+    console.error('Failed to fetch access token:', error.response?.data || error.message);
+    process.exit(1);
+  }
+}
+
+(async () => {
+  const configFilePath = path.join(__dirname, 'codepath-notification');
+  const repo = process.env.GITHUB_REPOSITORY;
+  const prNumber = process.env.GITHUB_PR_NUMBER;
+  const token = process.env.GITHUB_TOKEN;
+
+  // Generate OAuth2 access token
+  const clientId = process.env.OAUTH2_CLIENT_ID;
+  const clientSecret = process.env.OAUTH2_CLIENT_SECRET;
+  const refreshToken = process.env.OAUTH2_REFRESH_TOKEN;
+
+  // validate params
+  if (!repo || !prNumber || !token || !clientId || !clientSecret || !refreshToken) {
+    console.error('Missing required environment variables.');
+    process.exit(1);
+  }
+
+  // the whole process is in a big try/catch. e.g. if the config file doesn't exist, github is down, etc.
+  try {
+    // Read and process the configuration file
+    const configFileContent = fs.readFileSync(configFilePath, 'utf-8');
+    const configRules = configFileContent
+      .split('\n')
+      .filter(line => line.trim() !== '' && !line.trim().startsWith('#')) // Ignore empty lines and comments
+      .map(line => {
+        const [regex, email] = line.split(':').map(part => part.trim());
+        return { regex: new RegExp(regex), email };
+      });
+
+    // Fetch changed files from github
+    const [owner, repoName] = repo.split('/');
+    const apiUrl = `https://api.github.com/repos/${owner}/${repoName}/pulls/${prNumber}/files`;
+    const response = await axios.get(apiUrl, {
+      headers: {
+        Authorization: `Bearer ${token}`,
+        Accept: 'application/vnd.github.v3+json',
+      },
+    });
+
+    const changedFiles = response.data.map(file => file.filename);
+    console.log('Changed files:', changedFiles);
+
+    // match file pathnames that are in the config and group them by email address
+    const matchesByEmail = {};
+    changedFiles.forEach(file => {
+      configRules.forEach(rule => {
+        if (rule.regex.test(file)) {
+          if (!matchesByEmail[rule.email]) {
+            matchesByEmail[rule.email] = [];
+          }
+          matchesByEmail[rule.email].push(file);
+        }
+      });
+    });
+
+    // Exit successfully if no matches were found
+    if (Object.keys(matchesByEmail).length === 0) {
+      console.log('No matches found. Exiting successfully.');
+      process.exit(0);
+    }
+
+    console.log('Grouped matches by email:', matchesByEmail);
+
+    // get ready to email the changes
+    const accessToken = await getAccessToken(clientId, clientSecret, refreshToken);
+
+    // Configure Nodemailer with OAuth2
+    //  service: 'Gmail',
+    const transporter = nodemailer.createTransport({
+      host: "smtp.gmail.com",
+      port: 465,
+      secure: true,
+      auth: {
+        type: 'OAuth2',
+        user: 'info@prebid.org',
+        clientId: clientId,
+        clientSecret: clientSecret,
+        refreshToken: refreshToken,
+        accessToken: accessToken
+      },
+    });
+
+    // Send one email per recipient
+    for (const [email, files] of Object.entries(matchesByEmail)) {
+      const emailBody = `
+        ${email},
+        <p>
+        Files owned by you have been changed in open source ${repo}. The <a href="https://github.com/${repo}/pull/${prNumber}">pull request is #${prNumber}</a>. These are the files you own that have been modified:
+        <ul>
+          ${files.map(file => `<li>${file}</li>`).join('')}
+        </ul>
+      `;
+
+      try {
+        await transporter.sendMail({
+          from: `"Prebid Info" <info@prebid.org>`,
+          to: email,
+          subject: `Files have been changed in open source ${repo}`,
+          html: emailBody,
+        });
+
+        console.log(`Email sent successfully to ${email}`);
+        console.log(`${emailBody}`);
+      } catch (error) {
+        console.error(`Failed to send email to ${email}:`, error.message);
+      }
+    }
+  } catch (error) {
+    console.error('Error:', error.message);
+    process.exit(1);
+  }
+})();
diff --git a/.github/workflows/slack-stale-pr.yml b/.github/workflows/slack-stale-pr.yml
new file mode 100644
index 00000000000..a610c3e7de9
--- /dev/null
+++ b/.github/workflows/slack-stale-pr.yml
@@ -0,0 +1,27 @@
+name: Post Stale PRs To Slack
+
+on:
+  # run Monday 9am and on-demand
+  workflow_dispatch:
+  schedule:
+    - cron:  '0 9 * * 1'
+
+jobs:
+  fetch-PRs:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Fetch pull requests
+        id: local
+        uses: paritytech/stale-pr-finder@v0.3.0
+        with:
+          GITHUB_TOKEN: ${{ github.token }}
+          days-stale: 14
+          ignoredLabels: "blocked"
+      - name: Post to a Slack channel
+        id: slack
+        uses: slackapi/slack-github-action@v1.27.1
+        with:
+          channel-id: ${{ secrets.SLACK_CHANNEL_ID }}
+          slack-message: "${{ steps.local.outputs.message }}"
+        env:
+          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
diff --git a/docs/admin-endpoints.md b/docs/admin-endpoints.md
new file mode 100644
index 00000000000..b3176a4379c
--- /dev/null
+++ b/docs/admin-endpoints.md
@@ -0,0 +1,209 @@
+# Admin enpoints
+
+Prebid Server Java offers a set of admin endpoints for managing and monitoring the server's health, configurations, and
+metrics. Below is a detailed description of each endpoint, including HTTP methods, paths, parameters, and responses.
+
+## General settings
+
+Each endpoint can be either enabled or disabled by changing `admin-endpoints.<endoint name>.enabled` toggle. Defaults to
+`false`.
+
+Each endpoint can be configured to serve either on application port (configured via `server.http.port` setting) or
+admin port (configured via `admin.port` setting) by changing `admin-endpoints.<endpoint name>.on-application-port`
+setting.
+By default, all admin endpoints reside on admin port.
+
+Each endpoint can be configured to serve on a certain path by setting `admin-endpoints.<endpoint name>.path`.
+
+Each endpoint can be configured to either require basic authorization or not by changing
+`admin-endpoints.<endpoint name>.protected` setting,
+defaults to `true`. Allowed credentials are globally configured for all admin endpoints with
+`admin-endpoints.credentials.<username>`
+setting.
+
+## Endpoints
+
+1. Version info
+
+- Name: version
+- Endpoint: Configured via `admin-endpoints.version.path` setting
+- Methods:
+    - `GET`:
+        - Description: Returns the version information for the Prebid Server Java instance.
+        - Parameters: None
+        - Responses:
+            - 200 OK: JSON containing version details
+            ```json
+            {
+                "version": "x.x.x",
+                "revision": "commit-hash"
+            }
+           ```
+
+2. Currency rates
+
+- Name: currency-rates
+- Methods:
+    - `GET`:
+        - Description: Returns the latest information about currency rates used by server instance.
+        - Parameters: None
+        - Responses:
+            - 200 OK: JSON containing version details
+            ```json
+            {
+                "active": "true",
+                "source": "http://currency-source"
+                "fetchingIntervalNs": 200,
+                "lastUpdated": "02/01/2018 - 13:45:30 UTC"
+                ... Rates ...
+            }
+           ```
+
+3. Cache notification endpoint
+
+- Name: storedrequest
+- Methods:
+    - `POST`:
+        - Description: Updates stored requests/imps data stored in server instance cache.
+        - Parameters:
+            - body:
+              ```json
+              {
+                  "requests": {
+                      "<request-name-1>": "<body-1>",
+                      ... Requests data ...
+                  },
+                  "imps": {
+                      "<imp-name-1>": "<body-1>",
+                      ... Imps data ...
+                  }
+              }
+              ``` 
+        - Responses:
+            - 200 OK
+            - 400 BAD REQUEST
+            - 405 METHOD NOT ALLOWED
+    - `DELETE`:
+        - Description: Invalidates stored requests/imps data stored in server instance cache.
+        - Parameters:
+            - body:
+              ```json
+              {
+                  "requests": ["<request-name-1>", ... Request names ...],
+                  "imps": ["<imp-name-1>", ... Imp names ...]
+              }
+              ``` 
+        - Responses:
+            - 200 OK
+            - 400 BAD REQUEST
+            - 405 METHOD NOT ALLOWED
+
+4. Amp cache notification endpoint
+
+- Name: storedrequest-amp
+- Methods:
+    - `POST`:
+        - Description: Updates stored requests/imps data for amp, stored in server instance cache.
+        - Parameters:
+            - body:
+              ```json
+              {
+                  "requests": {
+                      "<request-name-1>": "<body-1>",
+                      ... Requests data ...
+                  },
+                  "imps": {
+                      "<imp-name-1>": "<body-1>",
+                      ... Imps data ...
+                  }
+              }
+              ``` 
+        - Responses:
+            - 200 OK
+            - 400 BAD REQUEST
+            - 405 METHOD NOT ALLOWED
+    - `DELETE`:
+        - Description: Invalidates stored requests/imps data for amp, stored in server instance cache.
+        - Parameters:
+            - body:
+              ```json
+              {
+                  "requests": ["<request-name-1>", ... Request names ...],
+                  "imps": ["<imp-name-1>", ... Imp names ...]
+              }
+              ``` 
+        - Responses:
+            - 200 OK
+            - 400 BAD REQUEST
+            - 405 METHOD NOT ALLOWED
+
+5. Account cache notification endpoint
+
+- Name: cache-invalidation
+- Methods:
+    - any:
+        - Description: Invalidates cached data for a provided account in server instance cache.
+        - Parameters:
+            - `account`: Account id.
+        - Responses:
+            - 200 OK
+            - 400 BAD REQUEST
+
+
+6. Http interaction logging endpoint
+
+- Name: logging-httpinteraction
+- Methods:
+    - any:
+        - Description: Changes request logging specification in server instance.
+        - Parameters:
+            - `endpoint`: Endpoint. Should be either: `auction` or `amp`.
+            - `statusCode`: Status code for logging spec.
+            - `account`: Account id.
+            - `bidder`: Bidder code.
+            - `limit`: Limit of requests for specification to be valid.
+        - Responses:
+            - 200 OK
+            - 400 BAD REQUEST
+- Additional settings:
+    - `logging.http-interaction.max-limit` - max limit for logging specification limit.
+
+7. Logging level control endpoint
+
+- Name: logging-changelevel
+- Methods:
+    - any:
+        - Description: Changes request logging level for specified amount of time in server instance.
+        - Parameters:
+            - `level`: Logging level. Should be one of: `all`, `trace`, `debug`, `info`, `warn`, `error`, `off`.
+            - `duration`: Duration of logging level (in millis) before reset to original one.
+        - Responses:
+            - 200 OK
+            - 400 BAD REQUEST
+- Additional settings:
+    - `logging.change-level.max-duration-ms` - max duration of changed logger level.
+
+8. Tracer log endpoint
+
+- Name: tracelog
+- Methods:
+    - any:
+        - Description: Adds trace logging specification for specified amount of time in server instance.
+        - Parameters:
+            - `account`: Account id.
+            - `bidderCode`: Bidder code.
+            - `level`: Log level. Should be one of: `info`, `warn`, `trace`, `error`, `fatal`, `debug`.
+            - `duration`: Duration of logging specification (in seconds).
+        - Responses:
+            - 200 OK
+            - 400 BAD REQUEST
+
+9. Collected metrics endpoint
+
+- Name: collected-metrics
+- Methods:
+    - any:
+        - Description: Adds trace logging specification for specified amount of time in server instance.
+        - Parameters: None
+        - Responses:
+            - 200 OK: JSON containing metrics data.
diff --git a/docs/application-settings.md b/docs/application-settings.md
index bf0dc61bfd0..2a20e0d8342 100644
--- a/docs/application-settings.md
+++ b/docs/application-settings.md
@@ -19,8 +19,13 @@ There are two ways to configure application settings: database and file. This do
       operational warning.
     - "enforce": if a bidder returns a creative that's larger in height or width than any of the allowed sizes, reject
       the bid and log an operational warning.
+- `auction.bidadjustments` - configuration JSON for default bid adjustments
+- `auction.bidadjustments.mediatype.{banner, video-instream, video-outstream, audio, native, *}.{<BIDDER>, *}.{<DEAL_ID>, *}[]` - array of bid adjustment to be applied to any bid of the provided mediatype, <BIDDER> and <DEAL_ID> (`*` means ANY)
+- `auction.bidadjustments.mediatype.*.*.*[].adjtype` - type of the bid adjustment (cpm, multiplier, static)
+- `auction.bidadjustments.mediatype.*.*.*[].value` - value of the bid adjustment
+- `auction.bidadjustments.mediatype.*.*.*[].currency` - currency of the bid adjustment
 - `auction.events.enabled` - enables events for account if true
-- `auction.price-floors.enabeled` - enables price floors for account if true. Defaults to true.
+- `auction.price-floors.enabled` - enables price floors for account if true. Defaults to true.
 - `auction.price-floors.fetch.enabled`- enables data fetch for price floors for account if true. Defaults to false.
 - `auction.price-floors.fetch.url` - url to fetch price floors data from.
 - `auction.price-floors.fetch.timeout-ms` - timeout for fetching price floors data. Defaults to 5000.
@@ -96,6 +101,7 @@ Keep in mind following restrictions:
 - `cookie-sync.pri` - a list of prioritized bidder codes
 - `cookie-sync.coop-sync.default` - if the "coopSync" value isn't specified in the `/cookie_sync` request, use this
 - `hooks` - configuration for Prebid Server Modules. For further details, see: https://docs.prebid.org/prebid-server/pbs-modules/index.html#2-define-an-execution-plan
+- `hooks.admin.module-execution` - a key-value map, where a key is a module name and a value is a boolean, that defines whether modules hooks should/should not be always executed; if the module is not specified it is executed by default when it's present in the execution plan
 - `settings.geo-lookup` - enables geo lookup for account if true. Defaults to false.
 
 Here are the definitions of the "purposes" that can be defined in the GDPR setting configurations:
diff --git a/docs/build.md b/docs/build.md
index 67b0b8af26e..ed2c18b7e0a 100644
--- a/docs/build.md
+++ b/docs/build.md
@@ -1,9 +1,15 @@
 # Build project
 
 To build the project, you will need at least
-[Java 11](https://download.java.net/java/GA/jdk11/9/GPL/openjdk-11.0.2_linux-x64_bin.tar.gz)
+[Java 21](https://whichjdk.com/)
 and [Maven](https://maven.apache.org/) installed.
 
+If for whatever reason this Java reference will be stale, 
+you can always get the current project Java version from `pom.xml` property
+```xml
+<java.version>...</java.version>
+```
+
 To verify the installed Java run in console:
 
 ```bash
@@ -13,9 +19,9 @@ java -version
 which should show something like (yours may be different):
 
 ```
-openjdk version "11.0.2" 2019-01-15
-OpenJDK Runtime Environment 18.9 (build 11.0.2+9)
-OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode)
+openjdk version "21.0.5" 2024-10-15 LTS
+OpenJDK Runtime Environment Corretto-21.0.5.11.1 (build 21.0.5+11-LTS)
+OpenJDK 64-Bit Server VM Corretto-21.0.5.11.1 (build 21.0.5+11-LTS, mixed mode, sharing)
 ```
 
 Follow next steps to create JAR which can be deployed locally.
diff --git a/docs/config-app.md b/docs/config-app.md
index 40a2c42784e..c037a74c482 100644
--- a/docs/config-app.md
+++ b/docs/config-app.md
@@ -20,12 +20,17 @@ This parameter exists to allow to change the location of the directory Vert.x wi
 - `server.ssl` - enable SSL/TLS support.
 - `server.jks-path` - path to the java keystore (if ssl is enabled).
 - `server.jks-password` - password for the keystore (if ssl is enabled).
+- `server.cpu-load-monitoring.measurement-interval-ms` - the CPU load monitoring interval (milliseconds)
 
 ## HTTP Server
 - `server.max-headers-size` - set the maximum length of all headers, deprecated(use server.max-headers-size instead).
 - `server.ssl` - enable SSL/TLS support, deprecated(use server.ssl instead).
 - `server.jks-path` - path to the java keystore (if ssl is enabled), deprecated(use server.jks-path instead).
 - `server.jks-password` - password for the keystore (if ssl is enabled), deprecated(use server.jks-password instead).
+- `server.max-initial-line-length` - set the maximum length of the initial line
+- `server.idle-timeout` - set the maximum time idle connections could exist before being reaped
+- `server.enable-quickack` - enables the TCP_QUICKACK option - only with linux native transport.
+- `server.enable-reuseport` - set the value of reuse port
 - `server.http.server-instances` - how many http server instances should be created.
   This parameter affects how many CPU cores will be utilized by the application. Rough assumption - one http server instance will keep 1 CPU core busy.
 - `server.http.enabled` - if set to `true` enables http server
@@ -107,11 +112,11 @@ Removes and downloads file again if depending service cant process probably corr
 - `auction.timeout-notification.log-sampling-rate` - instructs apply sampling when logging bidder timeout notification results
 
 ## Video
-- `auction.video.stored-required` - flag forces to merge with stored request
-- `auction.blocklisted-accounts` - comma separated list of blocklisted account IDs.
+- `video.stored-request-required` - flag forces to merge with stored request
 - `video.stored-requests-timeout-ms` - timeout for stored requests fetching.
-- `auction.ad-server-currency` - default currency for video auction, if its value was not specified in request. Important note: PBS uses ISO-4217 codes for the representation of currencies.
+- `auction.blocklisted-accounts` - comma separated list of blocklisted account IDs.
 - `auction.video.escape-log-cache-regex` - regex to remove from cache debug log xml.
+- `auction.ad-server-currency` - default currency for video auction, if its value was not specified in request. Important note: PBS uses ISO-4217 codes for the representation of currencies.
 
 ## Setuid
 - `setuid.default-timeout-ms` - default operation timeout for requests to `/setuid` endpoint.
@@ -126,6 +131,7 @@ Removes and downloads file again if depending service cant process probably corr
 ## Vtrack
 - `vtrack.allow-unknown-bidder` - flag that allows servicing requests with bidders who were not configured in Prebid Server.
 - `vtrack.modify-vast-for-unknown-bidder` - flag that allows modifying the VAST value and adding the impression tag to it, for bidders who were not configured in Prebid Server.
+- `vtrack.default-timeout-ms` - a default timeout in ms for the vtrack request
 
 ## Adapters
 - `adapters.*` - the section for bidder specific configuration options.
@@ -147,6 +153,7 @@ There are several typical keys:
 - `adapters.<BIDDER_NAME>.usersync.type` - usersync type (i.e. redirect, iframe).
 - `adapters.<BIDDER_NAME>.usersync.support-cors` - flag signals if CORS supported by usersync.
 - `adapters.<BIDDER_NAME>.debug.allow` - enables debug output in the auction response for the given bidder. Default `true`.
+- `adapters.<BIDDER_NAME>.tmax-deduction-ms` - adjusts the tmax sent to the bidder by deducting the provided value (ms). Default `0 ms` - no deduction.
 
 In addition, each bidder could have arbitrary aliases configured that will look and act very much the same as the bidder itself.
 Aliases are configured by adding child configuration object at `adapters.<BIDDER_NAME>.aliases.<BIDDER_ALIAS>.`, aliases 
@@ -160,9 +167,8 @@ Also, each bidder could have its own bidder-specific options.
 
 ## Logging
 - `logging.http-interaction.max-limit` - maximum value for the number of interactions to log in one take.
-
-## Logging
 - `logging.change-level.max-duration-ms` - maximum duration (in milliseconds) for which logging level could be changed.
+- `logging.sampling-rate` - a percentage of messages that are logged
 
 ## Currency Converter
 - `currency-converter.external-rates.enabled` - if equals to `true` the currency conversion service will be enabled to fetch updated rates and convert bid currencies from external source. Also enables `/currency-rates` endpoint on admin port.
@@ -213,6 +219,11 @@ Also, each bidder could have its own bidder-specific options.
 - `admin-endpoints.collected-metrics.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`.
 - `admin-endpoints.collected-metrics.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials`
 
+- `admin-endpoints.logging-changelevel.enabled` - if equals to `true` the endpoint will be available.
+- `admin-endpoints.logging-changelevel.path` - the server context path where the endpoint will be accessible
+- `admin-endpoints.logging-changelevel.on-application-port` - when equals to `false` endpoint will be bound to `admin.port`.
+- `admin-endpoints.logging-changelevel.protected` - when equals to `true` endpoint will be protected by basic authentication configured in `admin-endpoints.credentials`
+
 - `admin-endpoints.credentials` - user and password for access to admin endpoints if `admin-endpoints.[NAME].protected` is true`.
 
 ## Metrics
@@ -264,6 +275,9 @@ See [metrics documentation](metrics.md) for complete list of metrics submitted a
 - `metrics.accounts.basic-verbosity` - a list of accounts for which only basic metrics will be submitted.
 - `metrics.accounts.detailed-verbosity` - a list of accounts for which all metrics will be submitted. 
 
+For `JVM` metrics
+- `metrics.jmx.enabled` - if equals to `true` then `jvm.gc` and `jvm.memory` metrics will be submitted
+
 ## Cache
 - `cache.scheme` - set the external Cache Service protocol: `http`, `https`, etc.
 - `cache.host` - set the external Cache Service destination in format `host:port`.
@@ -278,6 +292,7 @@ See [metrics documentation](metrics.md) for complete list of metrics submitted a
 for particular publisher account. Overrides `cache.banner-ttl-seconds` property.
 - `cache.account.<ACCOUNT>.video-ttl-seconds` - how long (in seconds) video creative will be available in Cache Service 
 for particular publisher account. Overrides `cache.video-ttl-seconds` property.
+- `cache.default-ttl-seconds.{banner, video, audio, native}` - a default value how long (in seconds) a creative of the specific type will be available in Cache Service
 
 ## Application settings (account configuration, stored ad unit configurations, stored requests)
 Preconfigured application settings can be obtained from multiple data sources consequently: 
@@ -366,6 +381,19 @@ contain 'WHERE last_updated > ?' for MySQL and 'WHERE last_updated > $1' for Pos
 - `settings.in-memory-cache.database-update.refresh-rate` - refresh period in ms for stored request updates.
 - `settings.in-memory-cache.database-update.timeout` - timeout for obtaining stored request updates.
 
+For S3 storage configuration
+- `settings.in-memory-cache.s3-update.refresh-rate` - refresh period in ms for stored request updates in S3
+- `settings.s3.access-key-id` - an access key
+- `settings.s3.secret-access-key` - a secret access key
+- `settings.s3.region` - a region, AWS_GLOBAL by default
+- `settings.s3.endpoint` - an endpoint
+- `settings.s3.bucket` - a bucket name
+- `settings.s3.force-path-style` - forces the S3 client to use path-style addressing for buckets.
+- `settings.s3.accounts-dir` - a directory with stored accounts
+- `settings.s3.stored-imps-dir` - a directory with stored imps
+- `settings.s3.stored-requests-dir` - a directory with stored requests
+- `settings.s3.stored-responses-dir` - a directory with stored responses
+
 For targeting available next options:
 - `settings.targeting.truncate-attr-chars` - set the max length for names of targeting keywords (0 means no truncation).
 
@@ -398,6 +426,7 @@ If not defined in config all other Health Checkers would be disabled and endpoin
 - `gdpr.eea-countries` - comma separated list of countries in European Economic Area (EEA).
 - `gdpr.default-value` - determines GDPR in scope default value (if no information in request and no geolocation data).
 - `gdpr.host-vendor-id` - the organization running a cluster of Prebid Servers.
+- `datacenter-region` - the datacenter region of a cluster of Prebid Servers
 - `gdpr.enabled` - gdpr feature switch. Default `true`.
 - `gdpr.purposes.pN.enforce-purpose` - define type of enforcement confirmation: `no`/`basic`/`full`. Default `full`
 - `gdpr.purposes.pN.enforce-vendors` - if equals to `true`, user must give consent to use vendors. Purposes will be omitted. Default `true`
@@ -427,9 +456,30 @@ If not defined in config all other Health Checkers would be disabled and endpoin
 - `geolocation.type` - set the geo location service provider, can be `maxmind` or custom provided by hosting company.
 - `geolocation.maxmind` - section for [MaxMind](https://www.maxmind.com) configuration as geo location service provider.
 - `geolocation.maxmind.remote-file-syncer` - use RemoteFileSyncer component for downloading/updating MaxMind database file. See [RemoteFileSyncer](#remote-file-syncer) section for its configuration.
+- `geolocation.configurations[]` - a list of geo-lookup configurations for the `configuration` `geolocation.type`
+- `geolocation.configurations[].address-pattern` - an address pattern for matching an IP to look up
+- `geolocation.configurations[].geo-info.continent` - a continent to return on the `configuration` geo-lookup
+- `geolocation.configurations[].geo-info.country` - a country to return on the `configuration` geo-lookup
+- `geolocation.configurations[].geo-info.region` - a region to return on the `configuration` geo-lookup
+- `geolocation.configurations[].geo-info.region-code` - a region code to return on the `configuration` geo-lookup
+- `geolocation.configurations[].geo-info.city` - a city to return on the `configuration` geo-lookup
+- `geolocation.configurations[].geo-info.metro-google` - a metro Google to return on the `configuration` geo-lookup
+- `geolocation.configurations[].geo-info.metro-nielsen` - a metro Nielsen to return on the `configuration` geo-lookup
+- `geolocation.configurations[].geo-info.zip` - a zip to return on the `configuration` geo-lookup
+- `geolocation.configurations[].geo-info.connection-speed` - a connection-speed to return on the `configuration` geo-lookup
+- `geolocation.configurations[].geo-info.lat` - a lat to return on the `configuration` geo-lookup
+- `geolocation.configurations[].geo-info.lon` - a lon to return on the `configuration` geo-lookup
+- `geolocation.configurations[].geo-info.time-zone` - a time zone to return on the `configuration` geo-lookup
+
+## IPv6
+- `ipv6.always-mask-right` - a bit mask for masking an IPv6 address of the device
+- `ipv6.anon-left-mask-bits` - a bit mask for anonymizing an IPv6 address of the device
+- `ipv6.private-networks` - a list of known private/local networks to skip masking of an IP address of the device
 
 ## Analytics
 - `analytics.global.adapters` - Names of analytics adapters that will work for each request, except those disabled at the account level.
+
+For the `pubstack` analytics adapter
 - `analytics.pubstack.enabled` - if equals to `true` the Pubstack analytics module will be enabled. Default value is `false`. 
 - `analytics.pubstack.endpoint` - url for reporting events and fetching configuration. 
 - `analytics.pubstack.scopeid` - defined the scope provided by the Pubstack Support Team.
@@ -439,9 +489,50 @@ If not defined in config all other Health Checkers would be disabled and endpoin
 - `analytics.pubstack.buffers.count` - threshold in events count for buffer to send events
 - `analytics.pubstack.buffers.report-ttl-ms` - max period between two reports.
 
+For the `greenbids` analytics adapter
+- `analytics.greenbids.enabled` - if equals to `true` the Greenbids analytics module will be enabled. Default value is `false`.
+- `analytics.greenbids.analytics-server-version` - a server version to add to the event
+- `analytics.greenbids.analytics-server` - url for reporting events
+- `analytics.greenbids.timeout-ms` - timeout in milliseconds for report requests.
+- `analytics.greenbids.exploratory-sampling-split` - a sampling rate for report requests
+- `analytics.greenbids.default-sampling-rate` - a default sampling rate for report requests
+
+For the `agma` analytics adapter
+- `analytics.agma.enabled` - if equals to `true` the Agma analytics module will be enabled. Default value is `false`.
+- `analytics.agma.endpoint.url` - url for reporting events
+- `analytics.agma.endpoint.timeout-ms` - timeout in milliseconds for report requests.
+- `analytics.agma.endpoint.gzip` - if equals to `true` the Agma analytics module enables gzip encoding. Default value is `false`.
+- `analytics.agma.buffers.size-bytes` - threshold in bytes for buffer to send events.
+- `analytics.agma.buffers.count` - threshold in events count for buffer to send events.
+- `analytics.agma.buffers.timeout-ms` - max period between two reports.
+- `analytics.agma.accounts[].code` - an account code to send with an event
+- `analytics.agma.accounts[].publisher-id` - a publisher id to match an event to send
+- `analytics.agma.accounts[].site-app-id` - a site or app id to match an event to send
+
+## Modules
+- `hooks.admin.module-execution` - a key-value map, where a key is a module name and a value is a boolean, that defines whether modules hooks should/should not be always executed; if the module is not specified it is executed by default when it's present in the execution plan
+- `settings.modules.require-config-to-invoke` - when enabled it requires a runtime config to exist for a module.
+
 ## Debugging
 - `debug.override-token` - special string token for overriding Prebid Server account and/or adapter debug information presence in the auction response.
 
 To override (force enable) account and/or bidder adapter debug setting, a client must include `x-pbs-debug-override`
 HTTP header in the auction call containing same token as in the `debug.override-token` property. This will make Prebid
 Server ignore account `auction.debug-allow` and/or `adapters.<BIDDER_NAME>.debug.allow` properties.
+
+## Privacy Sandbox
+- `auction.privacysandbox.topicsdomain` - the list of Sec-Browsing-Topics for the Privacy Sandbox
+
+## AMP
+- `amp.custom-targeting` - a list of bidders that support custom targeting
+
+## Hooks
+- `hooks.host-execution-plan` - a host execution plan for modules
+- `hooks.default-account-execution-plan` - a default account execution plan
+
+## Price Floors Debug
+- `price-floors.enabled` - enables price floors for account if true. Defaults to true.
+- `price-floors.min-max-age-sec` - a price floors fetch data time to live in cache.
+- `price-floors.min-period-sec` - a refresh period for fetching price floors data.
+- `price-floors.min-timeout-ms` - a min timeout in ms for fetching price floors data.
+- `price-floors.max-timeout-ms` - a max timeout in ms for fetching price floors data.
diff --git a/docs/developers/code-reviews.md b/docs/developers/code-reviews.md
index cc8ed667849..ba7fb0ee526 100644
--- a/docs/developers/code-reviews.md
+++ b/docs/developers/code-reviews.md
@@ -3,33 +3,21 @@
 ## Standards
 Anyone is free to review and comment on any [open pull requests](https://github.com/prebid/prebid-server-java/pulls).
 
-All pull requests must be reviewed and approved by at least one [core member](https://github.com/orgs/prebid/teams/core/members) before merge.
-
-Very small pull requests may be merged with just one review if they:
-
-1. Do not change the public API.
-2. Have low risk of bugs, in the opinion of the reviewer.
-3. Introduce no new features, or impact the code architecture.
-
-Larger pull requests must meet at least one of the following two additional requirements.
-
-1. Have a second approval from a core member
-2. Be open for 5 business days with no new changes requested.
+1. PRs that touch only adapters and modules can be approved by one reviewer before merge.
+2. PRs that touch PBS-core must be reviewed and approved by at least two 'core' reviewers before merge.
 
 ## Process
 
-New pull requests should be [assigned](https://help.github.com/articles/assigning-issues-and-pull-requests-to-other-github-users/) 
-to a core member for review within 3 business days of being opened.
-That person should either approve the changes or request changes within 4 business days of being assigned.
-If they're too busy, they should assign it to someone else who can review it within that timeframe.
+New pull requests must be [assigned](https://help.github.com/articles/assigning-issues-and-pull-requests-to-other-github-users/) 
+to a reviewer within 5 business days of being opened. That person must either approve the changes or request changes within 5 business days of being assigned.
+
+If a reviewer is too busy, they should re-assign it to someone else as soon as possible so that person has enough time to take over the review and still meet the 5-day goal. Please tag the new reviewer in the PR. If you don't know who to assign it to, use the #prebid-server-java-dev Slack channel to ask for help in re-assigning.
 
-If the changes are small, that member can merge the PR once the changes are complete. Otherwise, they should
-assign the pull request to another member for a second review.
+If a reviewer is going to be unavailable for more than a few days, they should update the notes column of the duty spreadsheet or drop a note about their availability into the Slack channel.
 
-The pull request can then be merged whenever the second reviewer approves, or if 5 business days pass with no farther
-changes requested by anybody, whichever comes first.
+After the review, if the PR touches PBS-core, it must be assigned to a second reviewer.
 
-## Priorities
+## Review Priorities
 
 Code reviews should focus on things which cannot be validated by machines.
 
@@ -43,4 +31,10 @@ explaining it. Are there better ways to achieve those goals?
 - Does the code use any global, mutable state? [Inject dependencies](https://en.wikipedia.org/wiki/Dependency_injection) instead!
 - Can the code be organized into smaller, more modular pieces?
 - Is there dead code which can be deleted? Or TODO comments which should be resolved?
-- Look for code used by other adapters. Encourage adapter submitter to utilize common code. 
+- Look for code used by other adapters. Encourage adapter submitter to utilize common code.
+- Specific bid adapter rules:
+    - The email contact must work and be a group, not an individual.
+    - Host endpoints cannot be fully dynamic. i.e. they can utilize "https://REGION.example.com", but not "https://HOST".
+    - They cannot _require_ a "region" parameter. Region may be an optional parameter, but must have a default.
+    - No direct use of HTTP is prohibited - *implement an existing Bidder interface that will do all the job*
+    - If the ORTB is just forwarded to the endpoint, use the generic adapter - *define the new adapter as the alias of the generic adapter*
diff --git a/docs/developers/code-style.md b/docs/developers/code-style.md
index 14704d20799..de42811030f 100644
--- a/docs/developers/code-style.md
+++ b/docs/developers/code-style.md
@@ -28,7 +28,7 @@ in `pom.xml` directly.
 
 It is recommended to define version of library to separate property in `pom.xml`:
 
-```
+```xml
 <project>
     <properties>
         <caffeine.version>2.6.2</caffeine.version>
@@ -48,7 +48,7 @@ It is recommended to define version of library to separate property in `pom.xml`
 
 Do not use wildcard in imports because they hide what exactly is required by the class.
 
-```
+```java
 // bad
 import java.util.*;
 
@@ -61,7 +61,7 @@ import java.util.Map;
 
 Prefer to use `camelCase` naming convention for variables and methods.
 
-```
+```java
 // bad
 String account_id = "id";
 
@@ -71,7 +71,7 @@ String accountId = "id";
 
 Name of variable should be self-explanatory:
 
-```
+```java
 // bad
 String s = resolveParamA();
 
@@ -83,7 +83,7 @@ This helps other developers flesh your code out better without additional questi
 
 For `Map`s it is recommended to use `To` between key and value designation:
 
-```
+```java
 // bad
 Map<Imp, ExtImp> map = getData();
 
@@ -97,7 +97,7 @@ Make data transfer object(DTO) classes immutable with static constructor.
 This can be achieved by using Lombok and `@Value(staticConstructor="of")`. When constructor uses multiple(more than 4) arguments, use builder instead(`@Builder`).
 If dto must be modified somewhere, use builders annotation `toBuilder=true` parameter and rebuild instance by calling `toBuilder()` method.
 
-```
+```java
 // bad
 public class MyDto {
 
@@ -138,7 +138,7 @@ final MyDto updatedDto = myDto.toBuilder().value("newValue").build();
 Although Java supports the `var` keyword at the time of writing this documentation, the maintainers have chosen not to utilize it within the PBS codebase.
 Instead, write full variable type.
 
-```
+```java
 // bad
 final var result = getResult();
 
@@ -150,7 +150,7 @@ final Data result = getResult();
 
 Enclosing parenthesis should be placed on expression end.
 
-```
+```java
 // bad
 methodCall(
     long list of arguments
@@ -163,7 +163,7 @@ methodCall(
 
 This also applies for nested expressions.
 
-```
+```java
 // bad 
 methodCall(
     nestedCall(
@@ -181,7 +181,7 @@ methodCall(
 
 Please, place methods inside a class in call order.
 
-```
+```java
 // bad
 public interface Test {
 
@@ -249,7 +249,7 @@ Define interface first method, then all methods that it is calling, then second
 Not strict, but methods with long parameters list, that cannot be placed on single line,
 should add empty line before body definition.
 
-```
+```java
 // bad
 public static void method(
     parameters definitions) {
@@ -266,7 +266,7 @@ public static void method(
 
 Use collection literals where it is possible to define and initialize collections.
 
-```
+```java
 // bad 
 final List<String> foo = new ArrayList();
 foo.add("foo");
@@ -278,7 +278,7 @@ final List<String> foo = List.of("foo", "bar");
 
 Also, use special methods of Collections class for empty or single-value one-line collection creation. This makes developer intention clear and code less error-prone.
 
-```
+```java
 // bad 
 return List.of();
 
@@ -296,7 +296,7 @@ return Collections.singletonList("foo");
 
 It is recommended to declare variable as `final`- not strict but rather project convention to keep the code safe.
 
-```
+```java
 // bad
 String value = "value";
 
@@ -308,7 +308,7 @@ final String value = "value";
 
 Results of long ternary operators should be on separate lines:
 
-```
+```java
 // bad
 boolean result = someVeryVeryLongConditionThatForcesLineWrap ? firstResult
     : secondResult;
@@ -321,7 +321,7 @@ boolean result = someVeryVeryLongConditionThatForcesLineWrap
 
 Not so strict, but short ternary operations should be on one line:
 
-```
+```java
 // bad
 boolean result = someShortCondition
     ? firstResult
@@ -335,7 +335,7 @@ boolean result = someShortCondition ? firstResult : secondResult;
 
 Do not rely on operator precedence in boolean logic, use parenthesis instead. This will make code simpler and less error-prone.
 
-```
+```java
 // bad
 final boolean result = a && b || c;
 
@@ -347,7 +347,7 @@ final boolean result = (a && b) || c;
 
 Try to avoid hard-readable multiple nested method calls:
 
-```
+```java
 // bad
 int resolvedValue = resolveValue(fetchExternalJson(url, httpClient), populateAdditionalKeys(mainKeys, keyResolver));
 
@@ -361,7 +361,7 @@ int resolvedValue = resolveValue(externalJson, additionalKeys);
 
 Try not to retrieve same data more than once:
 
-```
+```java
 // bad
 if (getData() != null) {
     final Data resolvedData = resolveData(getData());
@@ -380,7 +380,7 @@ if (data != null) {
 
 If you're dealing with incoming data, please be sure to check if the nested object is not null before chaining.
 
-```
+```java
 // bad
 final ExtRequestTargeting targeting = bidRequest.getExt().getPrebid().getTargeting();
 
@@ -400,7 +400,7 @@ We are trying to get rid of long chains of null checks, which are described in s
 
 Don't leave commented code (don't think about the future).
 
-```
+```java
 // bad
 // String iWillUseThisLater = "never";
 ```
@@ -426,7 +426,7 @@ The code should be covered over 90%.
 
 The common way for writing tests has to comply with `given-when-then` style.
 
-```
+```java
 // given
 final BidRequest bidRequest = BidRequest.builder().id("").build();
 
@@ -451,7 +451,7 @@ The team decided to use name `target` for class instance under test.
 
 Unit tests should be as granular as possible. Try to split unit tests into smaller ones until this is impossible to do.
 
-```
+```java
 // bad
 @Test
 public void testFooBar() {
@@ -487,7 +487,7 @@ public void testBar() {
 This also applies to cases where same method is tested with different arguments inside single unit test.
 Note: This represents the replacement we have selected for parameterized testing.
 
-```
+```java
 // bad
 @Test
 public void testFooFirstSecond() {
@@ -527,7 +527,7 @@ It is also recommended to structure test method names with this scheme:
 name of method that is being tested, word `should`, what a method should return.
 If a method should return something based on a certain condition, add word `when` and description of a condition.
 
-```
+```java
 // bad
 @Test
 public void doSomethingTest() {
@@ -547,7 +547,7 @@ public void processDataShouldReturnResultWhenInputIsData() {
 
 Place data used in test as close as possible to test code. This will make tests easier to read, review and understand.
 
-```
+```java
 // bad
 @Test
 public void testFoo() {
@@ -576,7 +576,7 @@ This point also implies the next one.
 Since we are trying to improve test simplicity and readability and place test data close to tests, we decided to avoid usage of top level constants where it is possible.
 Instead, just inline constant values.
 
-```
+```java
 // bad
 public class TestClass {
 
@@ -609,7 +609,7 @@ public class TestClass {
 
 Don't use real information in tests, like existing endpoint URLs, account IDs, etc.
 
-```
+```java
 // bad
 String ENDPOINT_URL = "https://prebid.org";
 
diff --git a/docs/metrics.md b/docs/metrics.md
index 11f2165978c..85df92bc269 100644
--- a/docs/metrics.md
+++ b/docs/metrics.md
@@ -37,6 +37,7 @@ where `[DATASOURCE]` is a data source name, `DEFAULT_DS` by defaul.
 
 ## General auction metrics
 - `app_requests` - number of requests received from applications
+- `debug_requests` - number of requests received (when debug mode is enabled)
 - `no_cookie_requests` - number of requests without `uids` cookie or with one that didn't contain at least one live UID
 - `request_time` - timer tracking how long did it take for Prebid Server to serve a request
 - `imps_requested` - number if impressions requested
@@ -89,6 +90,7 @@ Following metrics are collected and submitted if account is configured with `bas
 
 Following metrics are collected and submitted if account is configured with `detailed` verbosity:
 - `account.<account-id>.requests.type.(openrtb2-web,openrtb-app,amp,legacy)` - number of requests received from account with `<account-id>` broken down by type of incoming request
+- `account.<account-id>.debug_requests` - number of requests received from account with `<account-id>` broken down by type of incoming request (when debug mode is enabled)
 - `account.<account-id>.requests.rejected` - number of rejected requests caused by incorrect `accountId`
 - `account.<account-id>.adapter.<bidder-name>.request_time` - timer tracking how long did it take to make a request to `<bidder-name>` when incoming request was from `<account-id>` 
 - `account.<account-id>.adapter.<bidder-name>.bids_received` - number of bids received from `<bidder-name>` when incoming request was from `<account-id>`
@@ -133,3 +135,15 @@ Following metrics are collected and submitted if account is configured with `det
 - `analytics.<reporter-name>.(auction|amp|video|cookie_sync|event|setuid).timeout` - number of event requests, failed with timeout cause
 - `analytics.<reporter-name>.(auction|amp|video|cookie_sync|event|setuid).err` - number of event requests, failed with errors
 - `analytics.<reporter-name>.(auction|amp|video|cookie_sync|event|setuid).badinput` - number of event requests, rejected with bad input cause
+
+## Modules metrics
+- `modules.module.<module>.stage.<stage>.hook.<hook>.call` - number of times the hook is called
+- `modules.module.<module>.stage.<stage>.hook.<hook>.duration` - timer tracking the called hook execution time
+- `modules.module.<module>.stage.<stage>.hook.<hook>.success.(noop|update|reject|no-invocation)` - number of times the hook is called successfully with the action applied
+- `modules.module.<module>.stage.<stage>.hook.<hook>.(failure|timeout|execution-error)` - number of times the hook execution is failed
+
+## Modules per-account metrics
+- `account.<account-id>.modules.module.<module>.call` - number of times the module is called
+- `account.<account-id>.modules.module.<module>.duration` - timer tracking the called module execution time
+- `account.<account-id>.modules.module.<module>.success.(noop|update|reject|no-invocation)` - number of times the module is called successfully with the action applied
+- `account.<account-id>.modules.module.<module>.failure` - number of times the module execution is failed
diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml
index 5f17d237be8..9eb4d2aaf33 100644
--- a/extra/bundle/pom.xml
+++ b/extra/bundle/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.prebid</groupId>
         <artifactId>prebid-server-aggregator</artifactId>
-        <version>3.15.0-SNAPSHOT</version>
+        <version>3.19.0-SNAPSHOT</version>
         <relativePath>../../extra/pom.xml</relativePath>
     </parent>
 
@@ -45,6 +45,16 @@
             <artifactId>pb-response-correction</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.prebid.server.hooks.modules</groupId>
+            <artifactId>greenbids-real-time-data</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.prebid.server.hooks.modules</groupId>
+            <artifactId>pb-request-correction</artifactId>
+            <version>${project.version}</version>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/extra/modules/confiant-ad-quality/pom.xml b/extra/modules/confiant-ad-quality/pom.xml
index a4b77048c76..0a90c077a58 100644
--- a/extra/modules/confiant-ad-quality/pom.xml
+++ b/extra/modules/confiant-ad-quality/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.prebid.server.hooks.modules</groupId>
         <artifactId>all-modules</artifactId>
-        <version>3.15.0-SNAPSHOT</version>
+        <version>3.19.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>confiant-ad-quality</artifactId>
diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java
index 57eac3d3620..0a7e9c7c2ea 100644
--- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java
+++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java
@@ -3,10 +3,10 @@
 import com.iab.openrtb.response.Bid;
 import org.prebid.server.auction.model.BidderResponse;
 import org.prebid.server.bidder.model.BidderBid;
-import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl;
-import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.AppliedToImpl;
-import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ResultImpl;
-import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.TagsImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
 import org.prebid.server.hooks.v1.analytics.AppliedTo;
 import org.prebid.server.hooks.v1.analytics.Result;
 import org.prebid.server.hooks.v1.analytics.Tags;
diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java
index 7db1446bcce..8a65e74db63 100644
--- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java
+++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java
@@ -18,9 +18,9 @@
 import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanResult;
 import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanner;
 import org.prebid.server.hooks.modules.com.confiant.adquality.model.GroupByIssues;
-import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.InvocationResultImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
 import org.prebid.server.hooks.v1.InvocationStatus;
 import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
 import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesHook;
diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java
deleted file mode 100644
index 76fa5759644..00000000000
--- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/InvocationResultImpl.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model;
-
-import lombok.Builder;
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.v1.InvocationAction;
-import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationStatus;
-import org.prebid.server.hooks.v1.PayloadUpdate;
-import org.prebid.server.hooks.v1.analytics.Tags;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Builder
-@Value
-public class InvocationResultImpl<PAYLOAD> implements InvocationResult<PAYLOAD> {
-
-    InvocationStatus status;
-
-    String message;
-
-    InvocationAction action;
-
-    PayloadUpdate<PAYLOAD> payloadUpdate;
-
-    List<String> errors;
-
-    List<String> warnings;
-
-    List<String> debugMessages;
-
-    Object moduleContext;
-
-    Tags analyticsTags;
-}
diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java
deleted file mode 100644
index 4453cb34e12..00000000000
--- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics;
-
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.v1.analytics.Activity;
-import org.prebid.server.hooks.v1.analytics.Result;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Value(staticConstructor = "of")
-public class ActivityImpl implements Activity {
-
-    String name;
-
-    String status;
-
-    List<Result> results;
-}
diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java
deleted file mode 100644
index 34beae0b73b..00000000000
--- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics;
-
-import lombok.Builder;
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.v1.analytics.AppliedTo;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Value
-@Builder
-public class AppliedToImpl implements AppliedTo {
-
-    List<String> impIds;
-
-    List<String> bidders;
-
-    boolean request;
-
-    boolean response;
-
-    List<String> bidIds;
-}
diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java
deleted file mode 100644
index 439552f562f..00000000000
--- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics;
-
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.v1.analytics.AppliedTo;
-import org.prebid.server.hooks.v1.analytics.Result;
-
-@Accessors(fluent = true)
-@Value(staticConstructor = "of")
-public class ResultImpl implements Result {
-
-    String status;
-
-    ObjectNode values;
-
-    AppliedTo appliedTo;
-}
diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java
deleted file mode 100644
index 1c01790d6b8..00000000000
--- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics;
-
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.v1.analytics.Activity;
-import org.prebid.server.hooks.v1.analytics.Tags;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Value(staticConstructor = "of")
-public class TagsImpl implements Tags {
-
-    List<Activity> activities;
-}
diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java
index 9ec01a7cfed..f3ea0d4764e 100644
--- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java
+++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java
@@ -2,10 +2,10 @@
 
 import org.junit.jupiter.api.Test;
 import org.prebid.server.auction.model.BidderResponse;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
 import org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils;
-import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl;
-import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.AppliedToImpl;
-import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ResultImpl;
 import org.prebid.server.hooks.v1.analytics.Tags;
 
 import java.util.List;
diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java
index d4b39a8214e..926865781d3 100644
--- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java
+++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java
@@ -16,15 +16,15 @@
 import org.prebid.server.auction.model.BidderResponse;
 import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
 import org.prebid.server.bidder.model.BidderSeatBid;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
 import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl;
 import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsMapper;
 import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanResult;
 import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanner;
 import org.prebid.server.hooks.modules.com.confiant.adquality.core.RedisParser;
 import org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils;
-import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl;
-import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.AppliedToImpl;
-import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ResultImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationResult;
 import org.prebid.server.hooks.v1.InvocationStatus;
diff --git a/extra/modules/fiftyone-devicedetection/pom.xml b/extra/modules/fiftyone-devicedetection/pom.xml
index 963b239763e..79af3b4471e 100644
--- a/extra/modules/fiftyone-devicedetection/pom.xml
+++ b/extra/modules/fiftyone-devicedetection/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.prebid.server.hooks.modules</groupId>
         <artifactId>all-modules</artifactId>
-        <version>3.15.0-SNAPSHOT</version>
+        <version>3.19.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>fiftyone-devicedetection</artifactId>
@@ -15,7 +15,6 @@
 
     <properties>
         <fiftyone-device-detection.version>4.4.94</fiftyone-device-detection.version>
-        <logback.version>1.2.13</logback.version>
     </properties>
 
     <dependencies>
@@ -32,18 +31,5 @@
             <artifactId>device-detection</artifactId>
             <version>${fiftyone-device-detection.version}</version>
         </dependency>
-
-        <dependency>
-            <groupId>ch.qos.logback</groupId>
-            <artifactId>logback-classic</artifactId>
-            <version>${logback.version}</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>ch.qos.logback</groupId>
-            <artifactId>logback-core</artifactId>
-            <version>${logback.version}</version>
-            <scope>test</scope>
-        </dependency>
     </dependencies>
 </project>
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java
index 9df4e2a0237..6a652ccf109 100644
--- a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java
@@ -2,10 +2,10 @@
 
 import org.prebid.server.hooks.modules.fiftyone.devicedetection.model.boundary.CollectedEvidence;
 import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.ModuleContext;
-import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.InvocationResultImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationContext;
 import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
 import org.prebid.server.hooks.v1.InvocationStatus;
 import org.prebid.server.hooks.v1.entrypoint.EntrypointHook;
 import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload;
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java
index 081177e8ca1..5c4b268cf68 100644
--- a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java
+++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java
@@ -13,9 +13,9 @@
 import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core.EnrichmentResult;
 import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.core.SecureHeadersRetriever;
 import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.ModuleContext;
-import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.InvocationResultImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
 import org.prebid.server.hooks.v1.InvocationStatus;
 import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
 import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java
deleted file mode 100644
index ead75085974..00000000000
--- a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/model/InvocationResultImpl.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model;
-
-import lombok.Builder;
-import org.prebid.server.hooks.v1.InvocationAction;
-import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationStatus;
-import org.prebid.server.hooks.v1.PayloadUpdate;
-import org.prebid.server.hooks.v1.analytics.Tags;
-
-import java.util.List;
-
-@Builder
-public record InvocationResultImpl<PAYLOAD>(
-        InvocationStatus status,
-        String message,
-        InvocationAction action,
-        PayloadUpdate<PAYLOAD> payloadUpdate,
-        List<String> errors,
-        List<String> warnings,
-        List<String> debugMessages,
-        Object moduleContext,
-        Tags analyticsTags
-) implements InvocationResult<PAYLOAD> {
-}
diff --git a/extra/modules/greenbids-real-time-data/pom.xml b/extra/modules/greenbids-real-time-data/pom.xml
new file mode 100644
index 00000000000..16e1389cb7b
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/pom.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.prebid.server.hooks.modules</groupId>
+        <artifactId>all-modules</artifactId>
+        <version>3.19.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>greenbids-real-time-data</artifactId>
+
+    <name>greenbids-real-time-data</name>
+    <description>Greenbids Real Time Data</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.github.ua-parser</groupId>
+            <artifactId>uap-java</artifactId>
+            <version>1.6.1</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.microsoft.onnxruntime</groupId>
+            <artifactId>onnxruntime</artifactId>
+            <version>1.16.1</version> <!-- Use the latest available version -->
+        </dependency>
+
+        <dependency>
+            <groupId>com.google.cloud</groupId>
+            <artifactId>google-cloud-storage</artifactId>
+            <version>2.41.0</version>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/extra/modules/greenbids-real-time-data/src/lombok.config b/extra/modules/greenbids-real-time-data/src/lombok.config
new file mode 100644
index 00000000000..efd92714219
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/lombok.config
@@ -0,0 +1 @@
+lombok.anyConstructor.addConstructorProperties = true
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java
new file mode 100644
index 00000000000..a40c98ebb25
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/DatabaseReaderFactory.java
@@ -0,0 +1,55 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.config;
+
+import io.vertx.core.Promise;
+import io.vertx.core.Vertx;
+import com.maxmind.geoip2.DatabaseReader;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.vertx.Initializable;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.concurrent.atomic.AtomicReference;
+
+public class DatabaseReaderFactory implements Initializable {
+
+    private final String geoLiteCountryUrl;
+
+    private final Vertx vertx;
+
+    private final AtomicReference<DatabaseReader> databaseReaderRef = new AtomicReference<>();
+
+    public DatabaseReaderFactory(String geoLitCountryUrl, Vertx vertx) {
+        this.geoLiteCountryUrl = geoLitCountryUrl;
+        this.vertx = vertx;
+    }
+
+    @Override
+    public void initialize(Promise<Void> initializePromise) {
+
+        vertx.executeBlocking(() -> {
+            try {
+                final URL url = new URL(geoLiteCountryUrl);
+                final Path databasePath = Files.createTempFile("GeoLite2-Country", ".mmdb");
+
+                try (InputStream inputStream = url.openStream();
+                     FileOutputStream outputStream = new FileOutputStream(databasePath.toFile())) {
+                    inputStream.transferTo(outputStream);
+                }
+
+                databaseReaderRef.set(new DatabaseReader.Builder(databasePath.toFile()).build());
+            } catch (IOException e) {
+                throw new PreBidException("Failed to initialize DatabaseReader from URL", e);
+            }
+            return null;
+        }).<Void>mapEmpty()
+        .onComplete(initializePromise);
+    }
+
+    public DatabaseReader getDatabaseReader() {
+        return databaseReaderRef.get();
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java
new file mode 100644
index 00000000000..959352d1908
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataConfiguration.java
@@ -0,0 +1,134 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.config;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+import io.vertx.core.Vertx;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThrottlingThresholdsFactory;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ModelCache;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerFactory;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThresholdCache;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.GreenbidsRealTimeDataProcessedAuctionRequestHook;
+import org.prebid.server.json.ObjectMapperProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@ConditionalOnProperty(prefix = "hooks." + GreenbidsRealTimeDataModule.CODE, name = "enabled", havingValue = "true")
+@Configuration
+@EnableConfigurationProperties(GreenbidsRealTimeDataProperties.class)
+public class GreenbidsRealTimeDataConfiguration {
+
+    @Bean
+    DatabaseReaderFactory databaseReaderFactory(GreenbidsRealTimeDataProperties properties, Vertx vertx) {
+        return new DatabaseReaderFactory(properties.getGeoLiteCountryPath(), vertx);
+    }
+
+    @Bean
+    GreenbidsInferenceDataService greenbidsInferenceDataService(DatabaseReaderFactory databaseReaderFactory) {
+        return new GreenbidsInferenceDataService(
+                databaseReaderFactory, ObjectMapperProvider.mapper());
+    }
+
+    @Bean
+    GreenbidsRealTimeDataModule greenbidsRealTimeDataModule(
+            FilterService filterService,
+            OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds,
+            GreenbidsInferenceDataService greenbidsInferenceDataService,
+            GreenbidsInvocationService greenbidsInvocationService) {
+
+        return new GreenbidsRealTimeDataModule(List.of(
+                new GreenbidsRealTimeDataProcessedAuctionRequestHook(
+                        ObjectMapperProvider.mapper(),
+                        filterService,
+                        onnxModelRunnerWithThresholds,
+                        greenbidsInferenceDataService,
+                        greenbidsInvocationService)));
+    }
+
+    @Bean
+    FilterService filterService() {
+        return new FilterService();
+    }
+
+    @Bean
+    Storage storage(GreenbidsRealTimeDataProperties properties) {
+        return StorageOptions.newBuilder()
+                .setProjectId(properties.getGoogleCloudGreenbidsProject()).build().getService();
+    }
+
+    @Bean
+    OnnxModelRunnerFactory onnxModelRunnerFactory() {
+        return new OnnxModelRunnerFactory();
+    }
+
+    @Bean
+    ThrottlingThresholdsFactory throttlingThresholdsFactory() {
+        return new ThrottlingThresholdsFactory();
+    }
+
+    @Bean
+    ModelCache modelCache(
+            GreenbidsRealTimeDataProperties properties,
+            Vertx vertx,
+            Storage storage,
+            OnnxModelRunnerFactory onnxModelRunnerFactory) {
+
+        final Cache<String, OnnxModelRunner> modelCacheWithExpiration = Caffeine.newBuilder()
+                .expireAfterWrite(properties.getCacheExpirationMinutes(), TimeUnit.MINUTES)
+                .build();
+
+        return new ModelCache(
+                storage,
+                properties.getGcsBucketName(),
+                modelCacheWithExpiration,
+                properties.getOnnxModelCacheKeyPrefix(),
+                vertx,
+                onnxModelRunnerFactory);
+    }
+
+    @Bean
+    ThresholdCache thresholdCache(
+            GreenbidsRealTimeDataProperties properties,
+            Vertx vertx,
+            Storage storage,
+            ThrottlingThresholdsFactory throttlingThresholdsFactory) {
+
+        final Cache<String, ThrottlingThresholds> thresholdsCacheWithExpiration = Caffeine.newBuilder()
+                .expireAfterWrite(properties.getCacheExpirationMinutes(), TimeUnit.MINUTES)
+                .build();
+
+        return new ThresholdCache(
+                storage,
+                properties.getGcsBucketName(),
+                ObjectMapperProvider.mapper(),
+                thresholdsCacheWithExpiration,
+                properties.getThresholdsCacheKeyPrefix(),
+                vertx,
+                throttlingThresholdsFactory);
+    }
+
+    @Bean
+    OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds(
+            ModelCache modelCache,
+            ThresholdCache thresholdCache) {
+
+        return new OnnxModelRunnerWithThresholds(modelCache, thresholdCache);
+    }
+
+    @Bean
+    GreenbidsInvocationService greenbidsInvocationService() {
+        return new GreenbidsInvocationService();
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java
new file mode 100644
index 00000000000..b2e5bdcfeb8
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataModule.java
@@ -0,0 +1,29 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.config;
+
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.InvocationContext;
+import org.prebid.server.hooks.v1.Module;
+
+import java.util.Collection;
+import java.util.List;
+
+public class GreenbidsRealTimeDataModule implements Module {
+
+    public static final String CODE = "greenbids-real-time-data";
+
+    private final List<? extends Hook<?, ? extends InvocationContext>> hooks;
+
+    public GreenbidsRealTimeDataModule(List<? extends Hook<?, ? extends InvocationContext>> hooks) {
+        this.hooks = hooks;
+    }
+
+    @Override
+    public String code() {
+        return CODE;
+    }
+
+    @Override
+    public Collection<? extends Hook<?, ? extends InvocationContext>> hooks() {
+        return hooks;
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java
new file mode 100644
index 00000000000..86736a6011f
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/config/GreenbidsRealTimeDataProperties.java
@@ -0,0 +1,21 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.config;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@ConfigurationProperties(prefix = "hooks.modules." + GreenbidsRealTimeDataModule.CODE)
+@Data
+public class GreenbidsRealTimeDataProperties {
+
+    String googleCloudGreenbidsProject;
+
+    String geoLiteCountryPath;
+
+    String gcsBucketName;
+
+    Integer cacheExpirationMinutes;
+
+    String onnxModelCacheKeyPrefix;
+
+    String thresholdsCacheKeyPrefix;
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java
new file mode 100644
index 00000000000..094c2d18df1
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterService.java
@@ -0,0 +1,123 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import ai.onnxruntime.OnnxTensor;
+import ai.onnxruntime.OnnxValue;
+import ai.onnxruntime.OrtException;
+import ai.onnxruntime.OrtSession;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage;
+import org.springframework.util.CollectionUtils;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+public class FilterService {
+
+    public Map<String, Map<String, Boolean>> filterBidders(
+            OnnxModelRunner onnxModelRunner,
+            List<ThrottlingMessage> throttlingMessages,
+            Double threshold) {
+
+        final OrtSession.Result results;
+        try {
+            final String[][] throttlingInferenceRows = convertToArray(throttlingMessages);
+            results = onnxModelRunner.runModel(throttlingInferenceRows);
+            return processModelResults(results, throttlingMessages, threshold);
+        } catch (OrtException e) {
+            throw new PreBidException("Exception during model inference: ", e);
+        }
+    }
+
+    private static String[][] convertToArray(List<ThrottlingMessage> messages) {
+        return messages.stream()
+                .map(message -> new String[]{
+                        message.getBrowser(),
+                        message.getBidder(),
+                        message.getAdUnitCode(),
+                        message.getCountry(),
+                        message.getHostname(),
+                        message.getDevice(),
+                        message.getHourBucket(),
+                        message.getMinuteQuadrant()})
+                .toArray(String[][]::new);
+    }
+
+    private Map<String, Map<String, Boolean>> processModelResults(
+            OrtSession.Result results,
+            List<ThrottlingMessage> throttlingMessages,
+            Double threshold) {
+
+        validateThrottlingMessages(throttlingMessages);
+
+        return StreamSupport.stream(results.spliterator(), false)
+                .peek(FilterService::validateOnnxTensor)
+                .filter(onnxItem -> Objects.equals(onnxItem.getKey(), "probabilities"))
+                .map(Map.Entry::getValue)
+                .map(OnnxTensor.class::cast)
+                .peek(tensor -> validateTensorSize(tensor, throttlingMessages.size()))
+                .map(tensor -> extractAndProcessProbabilities(tensor, throttlingMessages, threshold))
+                .map(Map::entrySet)
+                .flatMap(Collection::stream)
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+    }
+
+    private static void validateThrottlingMessages(List<ThrottlingMessage> throttlingMessages) {
+        if (throttlingMessages == null || CollectionUtils.isEmpty(throttlingMessages)) {
+            throw new PreBidException("throttlingMessages cannot be null or empty");
+        }
+    }
+
+    private static void validateOnnxTensor(Map.Entry<String, OnnxValue> onnxItem) {
+        if (!(onnxItem.getValue() instanceof OnnxTensor)) {
+            throw new PreBidException("Expected OnnxTensor for 'probabilities', but found: "
+                    + onnxItem.getValue().getClass().getName());
+        }
+    }
+
+    private static void validateTensorSize(OnnxTensor tensor, int expectedSize) {
+        final long[] tensorShape = tensor.getInfo().getShape();
+        if (tensorShape.length == 0 || tensorShape[0] != expectedSize) {
+            throw new PreBidException("Mismatch between tensor size and throttlingMessages size");
+        }
+    }
+
+    private Map<String, Map<String, Boolean>> extractAndProcessProbabilities(
+            OnnxTensor tensor,
+            List<ThrottlingMessage> throttlingMessages,
+            Double threshold) {
+
+        try {
+            final float[][] probabilities = extractProbabilitiesValues(tensor);
+            return processProbabilities(probabilities, throttlingMessages, threshold);
+        } catch (OrtException e) {
+            throw new PreBidException("Exception when extracting proba from OnnxTensor: ", e);
+        }
+    }
+
+    private float[][] extractProbabilitiesValues(OnnxTensor tensor) throws OrtException {
+        return (float[][]) tensor.getValue();
+    }
+
+    private Map<String, Map<String, Boolean>> processProbabilities(
+            float[][] probabilities,
+            List<ThrottlingMessage> throttlingMessages,
+            Double threshold) {
+
+        final Map<String, Map<String, Boolean>> result = new HashMap<>();
+
+        for (int i = 0; i < probabilities.length; i++) {
+            final ThrottlingMessage message = throttlingMessages.get(i);
+            final String impId = message.getAdUnitCode();
+            final String bidder = message.getBidder();
+            final boolean isKeptInAuction = probabilities[i][1] > threshold;
+            result.computeIfAbsent(impId, k -> new HashMap<>()).put(bidder, isKeptInAuction);
+        }
+
+        return result;
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java
new file mode 100644
index 00000000000..3bd3e37b859
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataService.java
@@ -0,0 +1,184 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Imp;
+import com.maxmind.geoip2.DatabaseReader;
+import com.maxmind.geoip2.exception.GeoIp2Exception;
+import com.maxmind.geoip2.model.CountryResponse;
+import com.maxmind.geoip2.record.Country;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.config.DatabaseReaderFactory;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage;
+import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+public class GreenbidsInferenceDataService {
+
+    private final DatabaseReaderFactory databaseReaderFactory;
+
+    private final ObjectMapper mapper;
+
+    public GreenbidsInferenceDataService(DatabaseReaderFactory dbReaderFactory, ObjectMapper mapper) {
+        this.databaseReaderFactory = Objects.requireNonNull(dbReaderFactory);
+        this.mapper = Objects.requireNonNull(mapper);
+    }
+
+    public List<ThrottlingMessage> extractThrottlingMessagesFromBidRequest(BidRequest bidRequest) {
+        final GreenbidsUserAgent userAgent = Optional.ofNullable(bidRequest.getDevice())
+                .map(Device::getUa)
+                .map(GreenbidsUserAgent::new)
+                .orElse(null);
+
+        return extractThrottlingMessages(bidRequest, userAgent);
+    }
+
+    private List<ThrottlingMessage> extractThrottlingMessages(
+            BidRequest bidRequest,
+            GreenbidsUserAgent greenbidsUserAgent) {
+
+        final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC"));
+        final Integer hourBucket = timestamp.getHour();
+        final Integer minuteQuadrant = (timestamp.getMinute() / 15) + 1;
+
+        final String hostname = bidRequest.getSite().getDomain();
+        final List<Imp> imps = bidRequest.getImp();
+
+        return imps.stream()
+                .map(imp -> extractMessagesForImp(
+                        imp,
+                        bidRequest,
+                        greenbidsUserAgent,
+                        hostname,
+                        hourBucket,
+                        minuteQuadrant))
+                .flatMap(Collection::stream)
+                .collect(Collectors.toList());
+    }
+
+    private List<ThrottlingMessage> extractMessagesForImp(
+            Imp imp,
+            BidRequest bidRequest,
+            GreenbidsUserAgent greenbidsUserAgent,
+            String hostname,
+            Integer hourBucket,
+            Integer minuteQuadrant) {
+
+        final String impId = imp.getId();
+        final ObjectNode impExt = imp.getExt();
+        final JsonNode bidderNode = extImpPrebid(impExt.get("prebid")).getBidder();
+        final String ip = Optional.ofNullable(bidRequest.getDevice())
+                .map(Device::getIp)
+                .orElse(null);
+        final String countryFromIp = getCountry(ip);
+        return createThrottlingMessages(
+                bidderNode,
+                impId,
+                greenbidsUserAgent,
+                countryFromIp,
+                hostname,
+                hourBucket,
+                minuteQuadrant);
+    }
+
+    private String getCountry(String ip) {
+        if (ip == null) {
+            return null;
+        }
+
+        final DatabaseReader databaseReader = databaseReaderFactory.getDatabaseReader();
+        try {
+            final InetAddress inetAddress = InetAddress.getByName(ip);
+            final CountryResponse response = databaseReader.country(inetAddress);
+            final Country country = response.getCountry();
+            return country.getName();
+        } catch (IOException | GeoIp2Exception e) {
+            throw new PreBidException("Failed to fetch country from geoLite DB", e);
+        }
+    }
+
+    private List<ThrottlingMessage> createThrottlingMessages(
+            JsonNode bidderNode,
+            String impId,
+            GreenbidsUserAgent greenbidsUserAgent,
+            String countryFromIp,
+            String hostname,
+            Integer hourBucket,
+            Integer minuteQuadrant) {
+
+        final List<ThrottlingMessage> throttlingImpMessages = new ArrayList<>();
+
+        if (!bidderNode.isObject()) {
+            return throttlingImpMessages;
+        }
+
+        final ObjectNode bidders = (ObjectNode) bidderNode;
+        final Iterator<String> fieldNames = bidders.fieldNames();
+        while (fieldNames.hasNext()) {
+            final String bidderName = fieldNames.next();
+            throttlingImpMessages.add(buildThrottlingMessage(
+                    bidderName,
+                    impId,
+                    greenbidsUserAgent,
+                    countryFromIp,
+                    hostname,
+                    hourBucket,
+                    minuteQuadrant));
+        }
+
+        return throttlingImpMessages;
+    }
+
+    private ThrottlingMessage buildThrottlingMessage(
+            String bidderName,
+            String impId,
+            GreenbidsUserAgent greenbidsUserAgent,
+            String countryFromIp,
+            String hostname,
+            Integer hourBucket,
+            Integer minuteQuadrant) {
+
+        final String browser = Optional.ofNullable(greenbidsUserAgent)
+                .map(GreenbidsUserAgent::getBrowser)
+                .orElse(StringUtils.EMPTY);
+
+        final String device = Optional.ofNullable(greenbidsUserAgent)
+                .map(GreenbidsUserAgent::getDevice)
+                .orElse(StringUtils.EMPTY);
+
+        return ThrottlingMessage.builder()
+                .browser(browser)
+                .bidder(StringUtils.defaultString(bidderName))
+                .adUnitCode(StringUtils.defaultString(impId))
+                .country(StringUtils.defaultString(countryFromIp))
+                .hostname(StringUtils.defaultString(hostname))
+                .device(device)
+                .hourBucket(StringUtils.defaultString(hourBucket.toString()))
+                .minuteQuadrant(StringUtils.defaultString(minuteQuadrant.toString()))
+                .build();
+    }
+
+    private ExtImpPrebid extImpPrebid(JsonNode extImpPrebid) {
+        try {
+            return mapper.treeToValue(extImpPrebid, ExtImpPrebid.class);
+        } catch (JsonProcessingException e) {
+            throw new PreBidException("Error decoding imp.ext.prebid: " + e.getMessage(), e);
+        }
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationService.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationService.java
new file mode 100644
index 00000000000..67d42d47bc2
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationService.java
@@ -0,0 +1,120 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult;
+import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult;
+import org.prebid.server.hooks.v1.InvocationAction;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+public class GreenbidsInvocationService {
+
+    private static final int RANGE_16_BIT_INTEGER_DIVISION_BASIS = 0x10000;
+
+    public GreenbidsInvocationResult createGreenbidsInvocationResult(
+            Partner partner,
+            BidRequest bidRequest,
+            Map<String, Map<String, Boolean>> impsBiddersFilterMap) {
+
+        final String greenbidsId = UUID.randomUUID().toString();
+        final boolean isExploration = isExploration(partner, greenbidsId);
+
+        final BidRequest updatedBidRequest = isExploration
+                ? bidRequest
+                : bidRequest.toBuilder()
+                .imp(updateImps(bidRequest, impsBiddersFilterMap))
+                .build();
+        final InvocationAction invocationAction = isExploration
+                ? InvocationAction.no_action
+                : InvocationAction.update;
+        final Map<String, Map<String, Boolean>> impsBiddersFilterMapToAnalyticsTag = isExploration
+                ? keepAllBiddersForAnalyticsResult(impsBiddersFilterMap)
+                : impsBiddersFilterMap;
+        final Map<String, Ortb2ImpExtResult> ort2ImpExtResultMap = createOrtb2ImpExtForImps(
+                bidRequest, impsBiddersFilterMapToAnalyticsTag, greenbidsId, isExploration);
+        final AnalyticsResult analyticsResult = AnalyticsResult.of(
+                "success", ort2ImpExtResultMap, null, null);
+
+        return GreenbidsInvocationResult.of(updatedBidRequest, invocationAction, analyticsResult);
+    }
+
+    private Boolean isExploration(Partner partner, String greenbidsId) {
+        final int hashInt = Integer.parseInt(
+                greenbidsId.substring(greenbidsId.length() - 4), 16);
+        return hashInt < partner.getExplorationRate() * RANGE_16_BIT_INTEGER_DIVISION_BASIS;
+    }
+
+    private List<Imp> updateImps(BidRequest bidRequest, Map<String, Map<String, Boolean>> impsBiddersFilterMap) {
+        return bidRequest.getImp().stream()
+                .map(imp -> updateImp(imp, impsBiddersFilterMap.get(imp.getId())))
+                .toList();
+    }
+
+    private Imp updateImp(Imp imp, Map<String, Boolean> bidderFilterMap) {
+        return imp.toBuilder()
+                .ext(updateImpExt(imp.getExt(), bidderFilterMap))
+                .build();
+    }
+
+    private ObjectNode updateImpExt(ObjectNode impExt, Map<String, Boolean> bidderFilterMap) {
+        final ObjectNode updatedExt = impExt.deepCopy();
+        Optional.ofNullable((ObjectNode) updatedExt.get("prebid"))
+                .map(prebidNode -> (ObjectNode) prebidNode.get("bidder"))
+                .ifPresent(bidderNode ->
+                        bidderFilterMap.entrySet().stream()
+                                .filter(entry -> !entry.getValue())
+                                .map(Map.Entry::getKey)
+                                .forEach(bidderNode::remove));
+        return updatedExt;
+    }
+
+    private Map<String, Map<String, Boolean>> keepAllBiddersForAnalyticsResult(
+            Map<String, Map<String, Boolean>> impsBiddersFilterMap) {
+
+        return impsBiddersFilterMap.entrySet().stream()
+                        .collect(Collectors.toMap(
+                                Map.Entry::getKey,
+                                entry -> entry.getValue().entrySet().stream()
+                                        .collect(Collectors.toMap(Map.Entry::getKey, e -> true))));
+    }
+
+    private Map<String, Ortb2ImpExtResult> createOrtb2ImpExtForImps(
+            BidRequest bidRequest,
+            Map<String, Map<String, Boolean>> impsBiddersFilterMap,
+            String greenbidsId,
+            Boolean isExploration) {
+
+        return bidRequest.getImp().stream()
+                .collect(Collectors.toMap(
+                        Imp::getId,
+                        imp -> createOrtb2ImpExt(imp, impsBiddersFilterMap, greenbidsId, isExploration)));
+    }
+
+    private Ortb2ImpExtResult createOrtb2ImpExt(
+            Imp imp,
+            Map<String, Map<String, Boolean>> impsBiddersFilterMap,
+            String greenbidsId,
+            Boolean isExploration) {
+
+        final String tid = Optional.ofNullable(imp)
+                .map(Imp::getExt)
+                .map(impExt -> impExt.get("tid"))
+                .map(JsonNode::asText)
+                .orElse(StringUtils.EMPTY);
+        final Map<String, Boolean> impBiddersFilterMap = impsBiddersFilterMap.get(imp.getId());
+        final ExplorationResult explorationResult = ExplorationResult.of(
+                greenbidsId, impBiddersFilterMap, isExploration);
+        return Ortb2ImpExtResult.of(explorationResult, tid);
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java
new file mode 100644
index 00000000000..b7450d71560
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgent.java
@@ -0,0 +1,67 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import org.apache.commons.lang3.StringUtils;
+import ua_parser.Client;
+import ua_parser.Device;
+import ua_parser.OS;
+import ua_parser.Parser;
+import ua_parser.UserAgent;
+
+import java.util.Optional;
+import java.util.Set;
+
+public class GreenbidsUserAgent {
+
+    public static final Set<String> PC_OS_FAMILIES = Set.of(
+            "Windows 95", "Windows 98", "Solaris");
+
+    private static final Parser UA_PARSER = new Parser();
+
+    private final String userAgentString;
+
+    private final UserAgent userAgent;
+
+    private final Device device;
+
+    private final OS os;
+
+    public GreenbidsUserAgent(String userAgentString) {
+        this.userAgentString = userAgentString;
+        final Client client = UA_PARSER.parse(userAgentString);
+        this.userAgent = client.userAgent;
+        this.device = client.device;
+        this.os = client.os;
+    }
+
+    public String getDevice() {
+        return Optional.ofNullable(device)
+                .map(device -> isPC() ? "PC" : device.family)
+                .orElse(StringUtils.EMPTY);
+    }
+
+    public String getBrowser() {
+        return Optional.ofNullable(userAgent)
+                .filter(userAgent -> !"Other".equals(userAgent.family) && StringUtils.isNoneBlank(userAgent.family))
+                .map(ua -> "%s %s".formatted(ua.family, StringUtils.defaultString(userAgent.major)).trim())
+                .orElse(StringUtils.EMPTY);
+    }
+
+    private boolean isPC() {
+        final String osFamily = osFamily();
+        return Optional.ofNullable(userAgentString)
+                .map(userAgent -> userAgent.contains("Windows NT")
+                        || PC_OS_FAMILIES.contains(osFamily)
+                        || ("Windows".equals(osFamily) && "ME".equals(osMajor()))
+                        || ("Mac OS X".equals(osFamily) && !userAgent.contains("Silk"))
+                        || (userAgent.contains("Linux") && userAgent.contains("X11")))
+                .orElse(false);
+    }
+
+    private String osFamily() {
+        return Optional.ofNullable(os).map(os -> os.family).orElse(StringUtils.EMPTY);
+    }
+
+    private String osMajor() {
+        return Optional.ofNullable(os).map(os -> os.major).orElse(StringUtils.EMPTY);
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java
new file mode 100644
index 00000000000..01087287d44
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCache.java
@@ -0,0 +1,96 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import ai.onnxruntime.OrtException;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.google.cloud.storage.Blob;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageException;
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
+
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ModelCache {
+
+    private static final Logger logger = LoggerFactory.getLogger(ModelCache.class);
+
+    private final String gcsBucketName;
+
+    private final Cache<String, OnnxModelRunner> cache;
+
+    private final Storage storage;
+
+    private final String onnxModelCacheKeyPrefix;
+
+    private final AtomicBoolean isFetching;
+
+    private final Vertx vertx;
+
+    private final OnnxModelRunnerFactory onnxModelRunnerFactory;
+
+    public ModelCache(
+            Storage storage,
+            String gcsBucketName,
+            Cache<String, OnnxModelRunner> cache,
+            String onnxModelCacheKeyPrefix,
+            Vertx vertx,
+            OnnxModelRunnerFactory onnxModelRunnerFactory) {
+        this.gcsBucketName = Objects.requireNonNull(gcsBucketName);
+        this.cache = Objects.requireNonNull(cache);
+        this.storage = Objects.requireNonNull(storage);
+        this.onnxModelCacheKeyPrefix = Objects.requireNonNull(onnxModelCacheKeyPrefix);
+        this.isFetching = new AtomicBoolean(false);
+        this.vertx = Objects.requireNonNull(vertx);
+        this.onnxModelRunnerFactory = Objects.requireNonNull(onnxModelRunnerFactory);
+    }
+
+    public Future<OnnxModelRunner> get(String onnxModelPath, String pbuid) {
+        final String cacheKey = onnxModelCacheKeyPrefix + pbuid;
+        final OnnxModelRunner cachedOnnxModelRunner = cache.getIfPresent(cacheKey);
+
+        if (cachedOnnxModelRunner != null) {
+            return Future.succeededFuture(cachedOnnxModelRunner);
+        }
+
+        if (isFetching.compareAndSet(false, true)) {
+            try {
+                return fetchAndCacheModelRunner(onnxModelPath, cacheKey);
+            } finally {
+                isFetching.set(false);
+            }
+        }
+
+        return Future.failedFuture("ModelRunner fetching in progress. Skip current request");
+    }
+
+    private Future<OnnxModelRunner> fetchAndCacheModelRunner(String onnxModelPath, String cacheKey) {
+        return vertx.executeBlocking(() -> getBlob(onnxModelPath))
+                .map(this::loadModelRunner)
+                .onSuccess(onnxModelRunner -> cache.put(cacheKey, onnxModelRunner))
+                .onFailure(error -> logger.error("Failed to fetch ONNX model"));
+    }
+
+    private Blob getBlob(String onnxModelPath) {
+        try {
+            return Optional.ofNullable(storage.get(gcsBucketName))
+                    .map(bucket -> bucket.get(onnxModelPath))
+                    .orElseThrow(() -> new PreBidException("Bucket not found: " + gcsBucketName));
+        } catch (StorageException e) {
+            throw new PreBidException("Error accessing GCS artefact for model: ", e);
+        }
+    }
+
+    private OnnxModelRunner loadModelRunner(Blob blob) {
+        try {
+            final byte[] onnxModelBytes = blob.getContent();
+            return onnxModelRunnerFactory.create(onnxModelBytes);
+        } catch (OrtException e) {
+            throw new PreBidException("Failed to convert blob to ONNX model", e);
+        }
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java
new file mode 100644
index 00000000000..d5570f30272
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunner.java
@@ -0,0 +1,24 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import ai.onnxruntime.OnnxTensor;
+import ai.onnxruntime.OrtEnvironment;
+import ai.onnxruntime.OrtException;
+import ai.onnxruntime.OrtSession;
+
+import java.util.Collections;
+
+public class OnnxModelRunner {
+
+    private static final OrtEnvironment ENVIRONMENT = OrtEnvironment.getEnvironment();
+
+    private final OrtSession session;
+
+    public OnnxModelRunner(byte[] onnxModelBytes) throws OrtException {
+        session = ENVIRONMENT.createSession(onnxModelBytes, new OrtSession.SessionOptions());
+    }
+
+    public OrtSession.Result runModel(String[][] throttlingInferenceRow) throws OrtException {
+        final OnnxTensor inputTensor = OnnxTensor.createTensor(ENVIRONMENT, throttlingInferenceRow);
+        return session.run(Collections.singletonMap("input", inputTensor));
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java
new file mode 100644
index 00000000000..b6082cf3e12
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerFactory.java
@@ -0,0 +1,10 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import ai.onnxruntime.OrtException;
+
+public class OnnxModelRunnerFactory {
+
+    public OnnxModelRunner create(byte[] bytes) throws OrtException {
+        return new OnnxModelRunner(bytes);
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java
new file mode 100644
index 00000000000..adbc1e17b2c
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerWithThresholds.java
@@ -0,0 +1,31 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import io.vertx.core.Future;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner;
+
+import java.util.Objects;
+
+public class OnnxModelRunnerWithThresholds {
+
+    private final ModelCache modelCache;
+
+    private final ThresholdCache thresholdCache;
+
+    public OnnxModelRunnerWithThresholds(
+            ModelCache modelCache,
+            ThresholdCache thresholdCache) {
+        this.modelCache = Objects.requireNonNull(modelCache);
+        this.thresholdCache = Objects.requireNonNull(thresholdCache);
+    }
+
+    public Future<OnnxModelRunner> retrieveOnnxModelRunner(Partner partner) {
+        final String onnxModelPath = "models_pbuid=" + partner.getPbuid() + ".onnx";
+        return modelCache.get(onnxModelPath, partner.getPbuid());
+    }
+
+    public Future<Double> retrieveThreshold(Partner partner) {
+        final String thresholdJsonPath = "thresholds_pbuid=" + partner.getPbuid() + ".json";
+        return thresholdCache.get(thresholdJsonPath, partner.getPbuid())
+                .map(partner::getThreshold);
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java
new file mode 100644
index 00000000000..44eb3d1403a
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCache.java
@@ -0,0 +1,102 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.google.cloud.storage.Blob;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageException;
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class ThresholdCache {
+
+    private static final Logger logger = LoggerFactory.getLogger(ThresholdCache.class);
+
+    private final String gcsBucketName;
+
+    private final Cache<String, ThrottlingThresholds> cache;
+
+    private final Storage storage;
+
+    private final ObjectMapper mapper;
+
+    private final String thresholdsCacheKeyPrefix;
+
+    private final AtomicBoolean isFetching;
+
+    private final Vertx vertx;
+
+    private final ThrottlingThresholdsFactory throttlingThresholdsFactory;
+
+    public ThresholdCache(
+            Storage storage,
+            String gcsBucketName,
+            ObjectMapper mapper,
+            Cache<String, ThrottlingThresholds> cache,
+            String thresholdsCacheKeyPrefix,
+            Vertx vertx,
+            ThrottlingThresholdsFactory throttlingThresholdsFactory) {
+        this.gcsBucketName = Objects.requireNonNull(gcsBucketName);
+        this.cache = Objects.requireNonNull(cache);
+        this.storage = Objects.requireNonNull(storage);
+        this.mapper = Objects.requireNonNull(mapper);
+        this.thresholdsCacheKeyPrefix = Objects.requireNonNull(thresholdsCacheKeyPrefix);
+        this.isFetching = new AtomicBoolean(false);
+        this.vertx = Objects.requireNonNull(vertx);
+        this.throttlingThresholdsFactory = Objects.requireNonNull(throttlingThresholdsFactory);
+    }
+
+    public Future<ThrottlingThresholds> get(String thresholdJsonPath, String pbuid) {
+        final String cacheKey = thresholdsCacheKeyPrefix + pbuid;
+        final ThrottlingThresholds cachedThrottlingThresholds = cache.getIfPresent(cacheKey);
+
+        if (cachedThrottlingThresholds != null) {
+            return Future.succeededFuture(cachedThrottlingThresholds);
+        }
+
+        if (isFetching.compareAndSet(false, true)) {
+            try {
+                return fetchAndCacheThrottlingThresholds(thresholdJsonPath, cacheKey);
+            } finally {
+                isFetching.set(false);
+            }
+        }
+
+        return Future.failedFuture("ThrottlingThresholds fetching in progress. Skip current request");
+    }
+
+    private Future<ThrottlingThresholds> fetchAndCacheThrottlingThresholds(String thresholdJsonPath, String cacheKey) {
+        return vertx.executeBlocking(() -> getBlob(thresholdJsonPath))
+                .map(this::loadThrottlingThresholds)
+                .onSuccess(thresholds -> cache.put(cacheKey, thresholds))
+                .onFailure(error -> logger.error("Failed to fetch thresholds"));
+    }
+
+    private Blob getBlob(String thresholdJsonPath) {
+        try {
+            return Optional.ofNullable(storage.get(gcsBucketName))
+                    .map(bucket -> bucket.get(thresholdJsonPath))
+                    .orElseThrow(() -> new PreBidException("Bucket not found: " + gcsBucketName));
+        } catch (StorageException e) {
+            throw new PreBidException("Error accessing GCS artefact for threshold: ", e);
+        }
+    }
+
+    private ThrottlingThresholds loadThrottlingThresholds(Blob blob) {
+        try {
+            final byte[] jsonBytes = blob.getContent();
+            return throttlingThresholdsFactory.create(jsonBytes, mapper);
+        } catch (IOException e) {
+            throw new PreBidException("Failed to load throttling thresholds json", e);
+        }
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java
new file mode 100644
index 00000000000..e7ac4a6a4a9
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThrottlingThresholdsFactory.java
@@ -0,0 +1,15 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds;
+
+import java.io.IOException;
+
+public class ThrottlingThresholdsFactory {
+
+    public ThrottlingThresholds create(byte[] bytes, ObjectMapper mapper) throws IOException {
+        final JsonNode thresholdsJsonNode = mapper.readTree(bytes);
+        return mapper.treeToValue(thresholdsJsonNode, ThrottlingThresholds.class);
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/Partner.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/Partner.java
new file mode 100644
index 00000000000..2be7c1887e8
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/Partner.java
@@ -0,0 +1,34 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.model.data;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Value;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.IntStream;
+
+@Value(staticConstructor = "of")
+public class Partner {
+
+    String pbuid;
+
+    @JsonProperty("targetTpr")
+    Double targetTpr;
+
+    @JsonProperty("explorationRate")
+    Double explorationRate;
+
+    public Double getThreshold(ThrottlingThresholds throttlingThresholds) {
+        final List<Double> truePositiveRates = throttlingThresholds.getTpr();
+        final List<Double> thresholds = throttlingThresholds.getThresholds();
+
+        final int minSize = Math.min(truePositiveRates.size(), thresholds.size());
+
+        return IntStream.range(0, minSize)
+                .filter(i -> truePositiveRates.get(i) >= targetTpr)
+                .mapToObj(thresholds::get)
+                .max(Comparator.naturalOrder())
+                .orElse(0.0);
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java
new file mode 100644
index 00000000000..8acb6718936
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/data/ThrottlingMessage.java
@@ -0,0 +1,25 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.model.data;
+
+import lombok.Builder;
+import lombok.Value;
+
+@Builder(toBuilder = true)
+@Value
+public class ThrottlingMessage {
+
+    String browser;
+
+    String bidder;
+
+    String adUnitCode;
+
+    String country;
+
+    String hostname;
+
+    String device;
+
+    String hourBucket;
+
+    String minuteQuadrant;
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java
new file mode 100644
index 00000000000..ccd6594ee38
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/filter/ThrottlingThresholds.java
@@ -0,0 +1,13 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter;
+
+import lombok.Value;
+
+import java.util.List;
+
+@Value(staticConstructor = "of")
+public class ThrottlingThresholds {
+
+    List<Double> thresholds;
+
+    List<Double> tpr;
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java
new file mode 100644
index 00000000000..9d175b5b4b3
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/AnalyticsResult.java
@@ -0,0 +1,18 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.model.result;
+
+import lombok.Value;
+import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult;
+
+import java.util.Map;
+
+@Value(staticConstructor = "of")
+public class AnalyticsResult {
+
+    String status;
+
+    Map<String, Ortb2ImpExtResult> values;
+
+    String bidder;
+
+    String impId;
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java
new file mode 100644
index 00000000000..0aff44ceaec
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/model/result/GreenbidsInvocationResult.java
@@ -0,0 +1,15 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.model.result;
+
+import com.iab.openrtb.request.BidRequest;
+import lombok.Value;
+import org.prebid.server.hooks.v1.InvocationAction;
+
+@Value(staticConstructor = "of")
+public class GreenbidsInvocationResult {
+
+    BidRequest updatedBidRequest;
+
+    InvocationAction invocationAction;
+
+    AnalyticsResult analyticsResult;
+}
diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java
new file mode 100644
index 00000000000..3b677a78a18
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java
@@ -0,0 +1,210 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.v1;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import io.vertx.core.Future;
+import org.apache.commons.collections4.CollectionUtils;
+import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult;
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.analytics.Result;
+import org.prebid.server.hooks.v1.analytics.Tags;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+public class GreenbidsRealTimeDataProcessedAuctionRequestHook implements ProcessedAuctionRequestHook {
+
+    private static final String CODE = "greenbids-real-time-data-processed-auction-request";
+    private static final String ACTIVITY = "greenbids-filter";
+    private static final String SUCCESS_STATUS = "success";
+    private static final String BID_REQUEST_ANALYTICS_EXTENSION_NAME = "greenbids-rtd";
+
+    private final ObjectMapper mapper;
+    private final FilterService filterService;
+    private final OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds;
+    private final GreenbidsInferenceDataService greenbidsInferenceDataService;
+    private final GreenbidsInvocationService greenbidsInvocationService;
+
+    public GreenbidsRealTimeDataProcessedAuctionRequestHook(
+            ObjectMapper mapper,
+            FilterService filterService,
+            OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds,
+            GreenbidsInferenceDataService greenbidsInferenceDataService,
+            GreenbidsInvocationService greenbidsInvocationService) {
+        this.mapper = Objects.requireNonNull(mapper);
+        this.filterService = Objects.requireNonNull(filterService);
+        this.onnxModelRunnerWithThresholds = Objects.requireNonNull(onnxModelRunnerWithThresholds);
+        this.greenbidsInferenceDataService = Objects.requireNonNull(greenbidsInferenceDataService);
+        this.greenbidsInvocationService = Objects.requireNonNull(greenbidsInvocationService);
+    }
+
+    @Override
+    public Future<InvocationResult<AuctionRequestPayload>> call(
+            AuctionRequestPayload auctionRequestPayload,
+            AuctionInvocationContext invocationContext) {
+
+        final AuctionContext auctionContext = invocationContext.auctionContext();
+        final BidRequest bidRequest = auctionContext.getBidRequest();
+        final Partner partner = parseBidRequestExt(bidRequest);
+
+        if (partner == null) {
+            return Future.succeededFuture(toInvocationResult(
+                    bidRequest, null, InvocationAction.no_action));
+        }
+
+        return Future.all(
+                        onnxModelRunnerWithThresholds.retrieveOnnxModelRunner(partner),
+                        onnxModelRunnerWithThresholds.retrieveThreshold(partner))
+                .compose(compositeFuture -> toInvocationResult(
+                        bidRequest,
+                        partner,
+                        compositeFuture.resultAt(0),
+                        compositeFuture.resultAt(1)))
+                .recover(throwable -> Future.succeededFuture(toInvocationResult(
+                        bidRequest, null, InvocationAction.no_action)));
+    }
+
+    private Partner parseBidRequestExt(BidRequest bidRequest) {
+        return Optional.ofNullable(bidRequest)
+                .map(BidRequest::getExt)
+                .map(ExtRequest::getPrebid)
+                .map(ExtRequestPrebid::getAnalytics)
+                .filter(this::isNotEmptyObjectNode)
+                .map(analytics -> (ObjectNode) analytics.get(BID_REQUEST_ANALYTICS_EXTENSION_NAME))
+                .map(this::toPartner)
+                .orElse(null);
+    }
+
+    private boolean isNotEmptyObjectNode(JsonNode analytics) {
+        return analytics != null && analytics.isObject() && !analytics.isEmpty();
+    }
+
+    private Partner toPartner(ObjectNode adapterNode) {
+        try {
+            return mapper.treeToValue(adapterNode, Partner.class);
+        } catch (JsonProcessingException e) {
+            return null;
+        }
+    }
+
+    private Future<InvocationResult<AuctionRequestPayload>> toInvocationResult(
+            BidRequest bidRequest,
+            Partner partner,
+            OnnxModelRunner onnxModelRunner,
+            Double threshold) {
+
+        final Map<String, Map<String, Boolean>> impsBiddersFilterMap;
+        try {
+            final List<ThrottlingMessage> throttlingMessages = greenbidsInferenceDataService
+                    .extractThrottlingMessagesFromBidRequest(bidRequest);
+
+            impsBiddersFilterMap = filterService.filterBidders(
+                    onnxModelRunner,
+                    throttlingMessages,
+                    threshold);
+        } catch (PreBidException e) {
+            return Future.succeededFuture(toInvocationResult(
+                    bidRequest, null, InvocationAction.no_action));
+        }
+
+        final GreenbidsInvocationResult greenbidsInvocationResult = greenbidsInvocationService
+                .createGreenbidsInvocationResult(partner, bidRequest, impsBiddersFilterMap);
+
+        return Future.succeededFuture(toInvocationResult(
+                greenbidsInvocationResult.getUpdatedBidRequest(),
+                greenbidsInvocationResult.getAnalyticsResult(),
+                greenbidsInvocationResult.getInvocationAction()));
+    }
+
+    private InvocationResult<AuctionRequestPayload> toInvocationResult(
+            BidRequest bidRequest,
+            AnalyticsResult analyticsResult,
+            InvocationAction action) {
+
+        final List<AnalyticsResult> analyticsResults = analyticsResult != null
+                ? Collections.singletonList(analyticsResult)
+                : Collections.emptyList();
+
+        return switch (action) {
+            case InvocationAction.update -> InvocationResultImpl
+                    .<AuctionRequestPayload>builder()
+                    .status(InvocationStatus.success)
+                    .action(action)
+                    .payloadUpdate(payload -> AuctionRequestPayloadImpl.of(bidRequest))
+                    .analyticsTags(toAnalyticsTags(analyticsResults))
+                    .build();
+            default -> InvocationResultImpl
+                    .<AuctionRequestPayload>builder()
+                    .status(InvocationStatus.success)
+                    .action(action)
+                    .analyticsTags(toAnalyticsTags(analyticsResults))
+                    .build();
+        };
+    }
+
+    private Tags toAnalyticsTags(List<AnalyticsResult> analyticsResults) {
+        if (CollectionUtils.isEmpty(analyticsResults)) {
+            return null;
+        }
+
+        return TagsImpl.of(Collections.singletonList(ActivityImpl.of(
+                ACTIVITY,
+                SUCCESS_STATUS,
+                toResults(analyticsResults))));
+    }
+
+    private List<Result> toResults(List<AnalyticsResult> analyticsResults) {
+        return analyticsResults.stream()
+                .map(this::toResult)
+                .toList();
+    }
+
+    private Result toResult(AnalyticsResult analyticsResult) {
+        return ResultImpl.of(
+                analyticsResult.getStatus(),
+                toObjectNode(analyticsResult.getValues()),
+                AppliedToImpl.builder()
+                        .bidders(Collections.singletonList(analyticsResult.getBidder()))
+                        .impIds(Collections.singletonList(analyticsResult.getImpId()))
+                        .build());
+    }
+
+    private ObjectNode toObjectNode(Map<String, Ortb2ImpExtResult> values) {
+        return values != null ? mapper.valueToTree(values) : null;
+    }
+
+    @Override
+    public String code() {
+        return CODE;
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java
new file mode 100644
index 00000000000..0a3ab82b9cd
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/FilterServiceTest.java
@@ -0,0 +1,177 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import ai.onnxruntime.OnnxTensor;
+import ai.onnxruntime.OnnxValue;
+import ai.onnxruntime.OrtException;
+import ai.onnxruntime.OrtSession;
+import ai.onnxruntime.TensorInfo;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.AbstractMap;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class FilterServiceTest {
+
+    @Mock
+    private OnnxModelRunner onnxModelRunnerMock;
+
+    @Mock
+    private OrtSession.Result results;
+
+    @Mock
+    private OnnxTensor onnxTensor;
+
+    @Mock
+    private TensorInfo tensorInfo;
+
+    @Mock
+    private OnnxValue onnxValue;
+
+    private final FilterService target = new FilterService();
+
+    @Test
+    public void filterBiddersShouldReturnFilteredBiddersWhenValidThrottlingMessagesProvided()
+            throws OrtException, IOException {
+        // given
+        final List<ThrottlingMessage> throttlingMessages = createThrottlingMessages();
+        final Double threshold = 0.5;
+        final OnnxModelRunner onnxModelRunner = givenOnnxModelRunner();
+
+        // when
+        final Map<String, Map<String, Boolean>> impsBiddersFilterMap = target.filterBidders(
+                onnxModelRunner, throttlingMessages, threshold);
+
+        // then
+        assertThat(impsBiddersFilterMap).isNotNull();
+        assertThat(impsBiddersFilterMap.get("adUnit1").get("bidder1")).isTrue();
+        assertThat(impsBiddersFilterMap.get("adUnit2").get("bidder2")).isFalse();
+        assertThat(impsBiddersFilterMap.get("adUnit3").get("bidder3")).isFalse();
+    }
+
+    @Test
+    public void validateOnnxTensorShouldThrowPreBidExceptionWhenOnnxValueIsNotTensor() throws OrtException {
+        // given
+        final List<ThrottlingMessage> throttlingMessages = createThrottlingMessages();
+        final Double threshold = 0.5;
+
+        when(onnxModelRunnerMock.runModel(any(String[][].class))).thenReturn(results);
+        when(results.spliterator()).thenReturn(Arrays.asList(createInvalidOnnxItem()).spliterator());
+
+        // when & then
+        assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold))
+                .isInstanceOf(PreBidException.class)
+                .hasMessageContaining("Expected OnnxTensor for 'probabilities', but found");
+    }
+
+    @Test
+    public void filterBiddersShouldThrowPreBidExceptionWhenOrtExceptionOccurs() throws OrtException {
+        // given
+        final List<ThrottlingMessage> throttlingMessages = createThrottlingMessages();
+        final Double threshold = 0.5;
+
+        when(onnxModelRunnerMock.runModel(any(String[][].class)))
+                .thenThrow(new OrtException("Exception during runModel"));
+
+        // when & then
+        assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold))
+                .isInstanceOf(PreBidException.class)
+                .hasMessageContaining("Exception during model inference");
+    }
+
+    @Test
+    public void filterBiddersShouldThrowPreBidExceptionWhenThrottlingMessagesIsEmpty() {
+        // given
+        final List<ThrottlingMessage> throttlingMessages = Collections.emptyList();
+        final Double threshold = 0.5;
+
+        // when & then
+        assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold))
+                .isInstanceOf(PreBidException.class)
+                .hasMessageContaining("throttlingMessages cannot be null or empty");
+    }
+
+    @Test
+    public void filterBiddersShouldThrowPreBidExceptionWhenTensorSizeMismatchOccurs() throws OrtException {
+        // given
+        final List<ThrottlingMessage> throttlingMessages = createThrottlingMessages();
+        final Double threshold = 0.5;
+
+        when(onnxModelRunnerMock.runModel(any(String[][].class))).thenReturn(results);
+        when(results.spliterator()).thenReturn(Arrays.asList(createOnnxItem()).spliterator());
+        when(onnxTensor.getInfo()).thenReturn(tensorInfo);
+        when(tensorInfo.getShape()).thenReturn(new long[]{0});
+
+        // when & then
+        assertThatThrownBy(() -> target.filterBidders(onnxModelRunnerMock, throttlingMessages, threshold))
+                .isInstanceOf(PreBidException.class)
+                .hasMessageContaining("Mismatch between tensor size and throttlingMessages size");
+    }
+
+    private OnnxModelRunner givenOnnxModelRunner() throws OrtException, IOException {
+        final byte[] onnxModelBytes = Files.readAllBytes(Paths.get(
+                "src/test/resources/models_pbuid=test-pbuid.onnx"));
+        return new OnnxModelRunner(onnxModelBytes);
+    }
+
+    private List<ThrottlingMessage> createThrottlingMessages() {
+        final ThrottlingMessage throttlingMessage1 = ThrottlingMessage.builder()
+                .browser("Chrome")
+                .bidder("bidder1")
+                .adUnitCode("adUnit1")
+                .country("US")
+                .hostname("localhost")
+                .device("PC")
+                .hourBucket("10")
+                .minuteQuadrant("1")
+                .build();
+
+        final ThrottlingMessage throttlingMessage2 = ThrottlingMessage.builder()
+                .browser("Firefox")
+                .bidder("bidder2")
+                .adUnitCode("adUnit2")
+                .country("FR")
+                .hostname("www.leparisien.fr")
+                .device("Mobile")
+                .hourBucket("11")
+                .minuteQuadrant("2")
+                .build();
+
+        final ThrottlingMessage throttlingMessage3 = ThrottlingMessage.builder()
+                .browser("Safari")
+                .bidder("bidder3")
+                .adUnitCode("adUnit3")
+                .country("FR")
+                .hostname("www.lesechos.fr")
+                .device("Tablet")
+                .hourBucket("12")
+                .minuteQuadrant("3")
+                .build();
+
+        return Arrays.asList(throttlingMessage1, throttlingMessage2, throttlingMessage3);
+    }
+
+    private Map.Entry<String, OnnxValue> createOnnxItem() {
+        return new AbstractMap.SimpleEntry<>("probabilities", onnxTensor);
+    }
+
+    private Map.Entry<String, OnnxValue> createInvalidOnnxItem() {
+        return new AbstractMap.SimpleEntry<>("probabilities", onnxValue);
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java
new file mode 100644
index 00000000000..1ac1bcc5cb1
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInferenceDataServiceTest.java
@@ -0,0 +1,165 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import com.iab.openrtb.request.Banner;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Imp;
+import com.maxmind.geoip2.DatabaseReader;
+import com.maxmind.geoip2.exception.GeoIp2Exception;
+import com.maxmind.geoip2.model.CountryResponse;
+import com.maxmind.geoip2.record.Country;
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.config.DatabaseReaderFactory;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.function.UnaryOperator;
+
+import static java.util.function.UnaryOperator.identity;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mock.Strictness.LENIENT;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBanner;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDevice;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt;
+
+@ExtendWith(MockitoExtension.class)
+public class GreenbidsInferenceDataServiceTest {
+
+    @Mock(strictness = LENIENT)
+    private DatabaseReaderFactory databaseReaderFactory;
+
+    @Mock
+    private DatabaseReader databaseReader;
+
+    @Mock
+    private Country country;
+
+    private GreenbidsInferenceDataService target;
+
+    @BeforeEach
+    public void setUp() {
+        when(databaseReaderFactory.getDatabaseReader()).thenReturn(databaseReader);
+        target = new GreenbidsInferenceDataService(databaseReaderFactory, TestBidRequestProvider.MAPPER);
+    }
+
+    @Test
+    public void extractThrottlingMessagesFromBidRequestShouldReturnValidThrottlingMessages()
+            throws IOException, GeoIp2Exception {
+        // given
+        final Banner banner = givenBanner();
+        final Imp imp = Imp.builder()
+                .id("adunitcodevalue")
+                .ext(givenImpExt())
+                .banner(banner)
+                .build();
+        final Device device = givenDevice(identity());
+        final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null);
+
+        final CountryResponse countryResponse = mock(CountryResponse.class);
+
+        final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC"));
+        final Integer expectedHourBucket = timestamp.getHour();
+        final Integer expectedMinuteQuadrant = (timestamp.getMinute() / 15) + 1;
+
+        when(databaseReader.country(any(InetAddress.class))).thenReturn(countryResponse);
+        when(countryResponse.getCountry()).thenReturn(country);
+        when(country.getName()).thenReturn("US");
+
+        // when
+        final List<ThrottlingMessage> throttlingMessages = target.extractThrottlingMessagesFromBidRequest(bidRequest);
+
+        // then
+        assertThat(throttlingMessages).isNotEmpty();
+        assertThat(throttlingMessages.getFirst().getBidder()).isEqualTo("rubicon");
+        assertThat(throttlingMessages.get(1).getBidder()).isEqualTo("appnexus");
+        assertThat(throttlingMessages.getLast().getBidder()).isEqualTo("pubmatic");
+
+        throttlingMessages.forEach(message -> {
+            assertThat(message.getAdUnitCode()).isEqualTo("adunitcodevalue");
+            assertThat(message.getCountry()).isEqualTo("US");
+            assertThat(message.getHostname()).isEqualTo("www.leparisien.fr");
+            assertThat(message.getDevice()).isEqualTo("PC");
+            assertThat(message.getHourBucket()).isEqualTo(String.valueOf(expectedHourBucket));
+            assertThat(message.getMinuteQuadrant()).isEqualTo(String.valueOf(expectedMinuteQuadrant));
+        });
+    }
+
+    @Test
+    public void extractThrottlingMessagesFromBidRequestShouldHandleMissingIp() {
+        // given
+        final Banner banner = givenBanner();
+        final Imp imp = Imp.builder()
+                .id("adunitcodevalue")
+                .ext(givenImpExt())
+                .banner(banner)
+                .build();
+        final Device device = givenDeviceWithoutIp(identity());
+        final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null);
+
+        final ZonedDateTime timestamp = ZonedDateTime.now(ZoneId.of("UTC"));
+        final Integer expectedHourBucket = timestamp.getHour();
+        final Integer expectedMinuteQuadrant = (timestamp.getMinute() / 15) + 1;
+
+        // when
+        final List<ThrottlingMessage> throttlingMessages = target.extractThrottlingMessagesFromBidRequest(bidRequest);
+
+        // then
+        assertThat(throttlingMessages).isNotEmpty();
+
+        assertThat(throttlingMessages.getFirst().getBidder()).isEqualTo("rubicon");
+        assertThat(throttlingMessages.get(1).getBidder()).isEqualTo("appnexus");
+        assertThat(throttlingMessages.getLast().getBidder()).isEqualTo("pubmatic");
+
+        throttlingMessages.forEach(message -> {
+            assertThat(message.getAdUnitCode()).isEqualTo("adunitcodevalue");
+            assertThat(message.getCountry()).isEqualTo(StringUtils.EMPTY);
+            assertThat(message.getHostname()).isEqualTo("www.leparisien.fr");
+            assertThat(message.getDevice()).isEqualTo("PC");
+            assertThat(message.getHourBucket()).isEqualTo(String.valueOf(expectedHourBucket));
+            assertThat(message.getMinuteQuadrant()).isEqualTo(String.valueOf(expectedMinuteQuadrant));
+        });
+    }
+
+    @Test
+    public void extractThrottlingMessagesFromBidRequestShouldThrowPreBidExceptionWhenGeoIpFails()
+            throws IOException, GeoIp2Exception {
+        // given
+        final Banner banner = givenBanner();
+        final Imp imp = Imp.builder()
+                .id("adunitcodevalue")
+                .ext(givenImpExt())
+                .banner(banner)
+                .build();
+        final Device device = givenDevice(identity());
+        final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null);
+
+        when(databaseReader.country(any(InetAddress.class))).thenThrow(new GeoIp2Exception("GeoIP failure"));
+
+        // when & then
+        assertThatThrownBy(() -> target.extractThrottlingMessagesFromBidRequest(bidRequest))
+                .isInstanceOf(PreBidException.class)
+                .hasMessageContaining("Failed to fetch country from geoLite DB");
+    }
+
+    private Device givenDeviceWithoutIp(UnaryOperator<Device.DeviceBuilder> deviceCustomizer) {
+        final String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36"
+                + " (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36";
+        return deviceCustomizer.apply(Device.builder().ua(userAgent)).build();
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationServiceTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationServiceTest.java
new file mode 100644
index 00000000000..1bf0f5409e4
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsInvocationServiceTest.java
@@ -0,0 +1,126 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.iab.openrtb.request.Banner;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Imp;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult;
+import org.prebid.server.hooks.v1.InvocationAction;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.function.UnaryOperator.identity;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBanner;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDevice;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt;
+
+@ExtendWith(MockitoExtension.class)
+public class GreenbidsInvocationServiceTest {
+
+    private GreenbidsInvocationService target;
+
+    @BeforeEach
+    public void setUp() {
+        target = new GreenbidsInvocationService();
+    }
+
+    @Test
+    public void createGreenbidsInvocationResultShouldReturnUpdateBidRequestWhenNotExploration() {
+        // given
+        final Banner banner = givenBanner();
+        final Imp imp = Imp.builder()
+                .id("adunitcodevalue")
+                .ext(givenImpExt())
+                .banner(banner)
+                .build();
+        final Device device = givenDevice(identity());
+        final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null);
+        final Map<String, Map<String, Boolean>> impsBiddersFilterMap = givenImpsBiddersFilterMap();
+        final Partner partner = givenPartner(0.0);
+
+        // when
+        final GreenbidsInvocationResult result = target.createGreenbidsInvocationResult(
+                partner, bidRequest, impsBiddersFilterMap);
+
+        // then
+        final JsonNode updatedBidRequestExtPrebidBidders = result.getUpdatedBidRequest().getImp().getFirst().getExt()
+                .get("prebid").get("bidder");
+        final Ortb2ImpExtResult ortb2ImpExtResult = result.getAnalyticsResult().getValues().get("adunitcodevalue");
+        final Map<String, Boolean> keptInAuction = ortb2ImpExtResult.getGreenbids().getKeptInAuction();
+
+        assertThat(result.getInvocationAction()).isEqualTo(InvocationAction.update);
+        assertThat(updatedBidRequestExtPrebidBidders.has("rubicon")).isTrue();
+        assertThat(updatedBidRequestExtPrebidBidders.has("appnexus")).isFalse();
+        assertThat(updatedBidRequestExtPrebidBidders.has("pubmatic")).isFalse();
+        assertThat(ortb2ImpExtResult).isNotNull();
+        assertThat(ortb2ImpExtResult.getGreenbids().getIsExploration()).isFalse();
+        assertThat(ortb2ImpExtResult.getGreenbids().getFingerprint()).isNotNull();
+        assertThat(keptInAuction.get("rubicon")).isTrue();
+        assertThat(keptInAuction.get("appnexus")).isFalse();
+        assertThat(keptInAuction.get("pubmatic")).isFalse();
+
+    }
+
+    @Test
+    public void createGreenbidsInvocationResultShouldReturnNoActionWhenExploration() {
+        // given
+        final Banner banner = givenBanner();
+        final Imp imp = Imp.builder()
+                .id("adunitcodevalue")
+                .ext(givenImpExt())
+                .banner(banner)
+                .build();
+        final Device device = givenDevice(identity());
+        final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null);
+        final Map<String, Map<String, Boolean>> impsBiddersFilterMap = givenImpsBiddersFilterMap();
+        final Partner partner = givenPartner(1.0);
+
+        // when
+        final GreenbidsInvocationResult result = target.createGreenbidsInvocationResult(
+                partner, bidRequest, impsBiddersFilterMap);
+
+        // then
+        final JsonNode updatedBidRequestExtPrebidBidders = result.getUpdatedBidRequest().getImp().getFirst().getExt()
+                .get("prebid").get("bidder");
+        final Ortb2ImpExtResult ortb2ImpExtResult = result.getAnalyticsResult().getValues().get("adunitcodevalue");
+        final Map<String, Boolean> keptInAuction = ortb2ImpExtResult.getGreenbids().getKeptInAuction();
+
+        assertThat(result.getInvocationAction()).isEqualTo(InvocationAction.no_action);
+        assertThat(updatedBidRequestExtPrebidBidders.has("rubicon")).isTrue();
+        assertThat(updatedBidRequestExtPrebidBidders.has("appnexus")).isTrue();
+        assertThat(updatedBidRequestExtPrebidBidders.has("pubmatic")).isTrue();
+        assertThat(ortb2ImpExtResult).isNotNull();
+        assertThat(ortb2ImpExtResult.getGreenbids().getIsExploration()).isTrue();
+        assertThat(ortb2ImpExtResult.getGreenbids().getFingerprint()).isNotNull();
+        assertThat(keptInAuction.get("rubicon")).isTrue();
+        assertThat(keptInAuction.get("appnexus")).isTrue();
+        assertThat(keptInAuction.get("pubmatic")).isTrue();
+    }
+
+    private Map<String, Map<String, Boolean>> givenImpsBiddersFilterMap() {
+        final Map<String, Boolean> biddersFitlerMap = new HashMap<>();
+        biddersFitlerMap.put("rubicon", true);
+        biddersFitlerMap.put("appnexus", false);
+        biddersFitlerMap.put("pubmatic", false);
+
+        final Map<String, Map<String, Boolean>> impsBiddersFilterMap = new HashMap<>();
+        impsBiddersFilterMap.put("adunitcodevalue", biddersFitlerMap);
+
+        return impsBiddersFilterMap;
+    }
+
+    private Partner givenPartner(Double explorationRate) {
+        return Partner.of("test-pbuid", 0.60, explorationRate);
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java
new file mode 100644
index 00000000000..b4839146a44
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/GreenbidsUserAgentTest.java
@@ -0,0 +1,59 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import org.apache.commons.lang3.StringUtils;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GreenbidsUserAgentTest {
+
+    @Test
+    public void getDeviceShouldReturnPCWhenWindowsNTInUserAgent() {
+        // given
+        final String userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)";
+
+        // when
+        final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString);
+
+        // then
+        assertThat(greenbidsUserAgent.getDevice()).isEqualTo("PC");
+    }
+
+    @Test
+    public void getDeviceShouldReturnDeviceIPhoneWhenIOSInUserAgent() {
+        // given
+        final String userAgentString = "Mozilla/5.0 (iPhone; CPU iPhone OS 14_2 like Mac OS X)";
+
+        // when
+        final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString);
+
+        // then
+        assertThat(greenbidsUserAgent.getDevice()).isEqualTo("iPhone");
+    }
+
+    @Test
+    public void getBrowserShouldReturnBrowserNameAndVersionWhenUserAgentIsPresent() {
+        // given
+        final String userAgentString =
+                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
+                        + " (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3";
+
+        // when
+        final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString);
+
+        // then
+        assertThat(greenbidsUserAgent.getBrowser()).isEqualTo("Chrome 58");
+    }
+
+    @Test
+    public void getBrowserShouldReturnEmptyStringWhenBrowserIsNull() {
+        // given
+        final String userAgentString = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)";
+
+        // when
+        final GreenbidsUserAgent greenbidsUserAgent = new GreenbidsUserAgent(userAgentString);
+
+        // then
+        assertThat(greenbidsUserAgent.getBrowser()).isEqualTo(StringUtils.EMPTY);
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java
new file mode 100644
index 00000000000..0c326f4249e
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ModelCacheTest.java
@@ -0,0 +1,191 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import ai.onnxruntime.OrtException;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageException;
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import com.google.cloud.storage.Blob;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.exception.PreBidException;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mock.Strictness.LENIENT;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class ModelCacheTest {
+
+    private static final String GCS_BUCKET_NAME = "test_bucket";
+    private static final String MODEL_CACHE_KEY_PREFIX = "onnxModelRunner_";
+    private static final String PBUUID = "test-pbuid";
+    private static final String ONNX_MODEL_PATH = "model.onnx";
+
+    @Mock
+    private Cache<String, OnnxModelRunner> cache;
+
+    @Mock(strictness = LENIENT)
+    private Storage storage;
+
+    @Mock(strictness = LENIENT)
+    private Bucket bucket;
+
+    @Mock(strictness = LENIENT)
+    private Blob blob;
+
+    @Mock
+    private OnnxModelRunner onnxModelRunner;
+
+    @Mock(strictness = LENIENT)
+    private OnnxModelRunnerFactory onnxModelRunnerFactory;
+
+    @Mock
+    private ModelCache target;
+
+    private Vertx vertx;
+
+    @BeforeEach
+    public void setUp() {
+        vertx = Vertx.vertx();
+        target = new ModelCache(
+                storage, GCS_BUCKET_NAME, cache, MODEL_CACHE_KEY_PREFIX, vertx, onnxModelRunnerFactory);
+    }
+
+    @Test
+    public void getShouldReturnModelFromCacheWhenPresent() {
+        // given
+        final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID;
+        when(cache.getIfPresent(eq(cacheKey))).thenReturn(onnxModelRunner);
+
+        // when
+        final Future<OnnxModelRunner> future = target.get(ONNX_MODEL_PATH, PBUUID);
+
+        // then
+        assertThat(future.succeeded()).isTrue();
+        assertThat(future.result()).isEqualTo(onnxModelRunner);
+        verify(cache).getIfPresent(eq(cacheKey));
+    }
+
+    @Test
+    public void getShouldSkipFetchingWhenFetchingInProgress() throws NoSuchFieldException, IllegalAccessException {
+        // given
+        final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID;
+
+        final ModelCache spyModelCache = spy(target);
+        final AtomicBoolean mockFetchingState = mock(AtomicBoolean.class);
+
+        when(cache.getIfPresent(eq(cacheKey))).thenReturn(null);
+        when(mockFetchingState.compareAndSet(false, true)).thenReturn(false);
+        final Field isFetchingField = ModelCache.class.getDeclaredField("isFetching");
+        isFetchingField.setAccessible(true);
+        isFetchingField.set(spyModelCache, mockFetchingState);
+
+        // when
+        final Future<OnnxModelRunner> result = spyModelCache.get(ONNX_MODEL_PATH, PBUUID);
+
+        // then
+        assertThat(result.failed()).isTrue();
+        assertThat(result.cause().getMessage()).isEqualTo(
+                "ModelRunner fetching in progress. Skip current request");
+    }
+
+    @Test
+    public void getShouldFetchModelWhenNotInCache() throws OrtException {
+        // given
+        final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID;
+        final byte[] bytes = new byte[]{1, 2, 3};
+
+        when(cache.getIfPresent(eq(cacheKey))).thenReturn(null);
+        when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket);
+        when(bucket.get(ONNX_MODEL_PATH)).thenReturn(blob);
+        when(blob.getContent()).thenReturn(bytes);
+        when(onnxModelRunnerFactory.create(bytes)).thenReturn(onnxModelRunner);
+
+        // when
+        final Future<OnnxModelRunner> future = target.get(ONNX_MODEL_PATH, PBUUID);
+
+        // then
+        future.onComplete(ar -> {
+            assertThat(ar.succeeded()).isTrue();
+            assertThat(ar.result()).isEqualTo(onnxModelRunner);
+            verify(cache).put(eq(cacheKey), eq(onnxModelRunner));
+        });
+    }
+
+    @Test
+    public void getShouldThrowExceptionWhenStorageFails() {
+        // given
+        final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID;
+
+        when(cache.getIfPresent(eq(cacheKey))).thenReturn(null);
+        when(storage.get(GCS_BUCKET_NAME)).thenThrow(new StorageException(500, "Storage Error"));
+
+        // when
+        final Future<OnnxModelRunner> future = target.get(ONNX_MODEL_PATH, PBUUID);
+
+        // then
+        future.onComplete(ar -> {
+            assertThat(ar.cause()).isInstanceOf(PreBidException.class);
+            assertThat(ar.cause().getMessage()).contains("Error accessing GCS artefact for model");
+        });
+    }
+
+    @Test
+    public void getShouldThrowExceptionWhenOnnxModelFails() throws OrtException {
+        // given
+        final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID;
+        final byte[] bytes = new byte[]{1, 2, 3};
+
+        when(cache.getIfPresent(eq(cacheKey))).thenReturn(null);
+        when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket);
+        when(bucket.get(ONNX_MODEL_PATH)).thenReturn(blob);
+        when(blob.getContent()).thenReturn(bytes);
+        when(onnxModelRunnerFactory.create(bytes)).thenThrow(
+                new OrtException("Failed to convert blob to ONNX model"));
+
+        // when
+        final Future<OnnxModelRunner> future = target.get(ONNX_MODEL_PATH, PBUUID);
+
+        // then
+        future.onComplete(ar -> {
+            assertThat(ar.failed()).isTrue();
+            assertThat(ar.cause()).isInstanceOf(PreBidException.class);
+            assertThat(ar.cause().getMessage()).contains("Failed to convert blob to ONNX model");
+        });
+    }
+
+    @Test
+    public void getShouldThrowExceptionWhenBucketNotFound() {
+        // given
+        final String cacheKey = MODEL_CACHE_KEY_PREFIX + PBUUID;
+
+        when(cache.getIfPresent(eq(cacheKey))).thenReturn(null);
+        when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket);
+        when(bucket.get(ONNX_MODEL_PATH)).thenReturn(blob);
+        when(blob.getContent()).thenThrow(new PreBidException("Bucket not found"));
+
+        // when
+        final Future<OnnxModelRunner> future = target.get(ONNX_MODEL_PATH, PBUUID);
+
+        // then
+        future.onComplete(ar -> {
+            assertThat(ar.failed()).isTrue();
+            assertThat(ar.cause()).isInstanceOf(PreBidException.class);
+            assertThat(ar.cause().getMessage()).contains("Bucket not found");
+        });
+    }
+
+}
diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java
new file mode 100644
index 00000000000..4a18b0a0fc0
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/OnnxModelRunnerTest.java
@@ -0,0 +1,73 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import ai.onnxruntime.OnnxTensor;
+import ai.onnxruntime.OrtException;
+import ai.onnxruntime.OrtSession;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.StreamSupport;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class OnnxModelRunnerTest {
+
+    private OnnxModelRunner target;
+
+    @BeforeEach
+    public void setUp() throws OrtException, IOException {
+        target = givenOnnxModelRunner();
+    }
+
+    @Test
+    public void runModelShouldReturnProbabilitiesWhenValidThrottlingInferenceRow() throws OrtException {
+        // given
+        final String[][] throttlingInferenceRow = {{
+                "Chrome 59", "rubicon", "adunitcodevalue", "US", "www.leparisien.fr", "PC", "10", "1"}};
+
+        // when
+        final OrtSession.Result actualResult = target.runModel(throttlingInferenceRow);
+
+        // then
+        final float[][] probabilities = StreamSupport.stream(actualResult.spliterator(), false)
+                .filter(onnxItem -> Objects.equals(onnxItem.getKey(), "probabilities"))
+                .map(Map.Entry::getValue)
+                .map(OnnxTensor.class::cast)
+                .map(tensor -> {
+                    try {
+                        return (float[][]) tensor.getValue();
+                    } catch (OrtException e) {
+                        throw new RuntimeException(e);
+                    }
+                }).findFirst().get();
+
+        assertThat(actualResult).isNotNull();
+        assertThat(actualResult).hasSize(2);
+        assertThat(probabilities[0]).isNotEmpty();
+        assertThat(probabilities[0][0]).isBetween(0.0f, 1.0f);
+        assertThat(probabilities[0][1]).isBetween(0.0f, 1.0f);
+    }
+
+    @Test
+    public void runModelShouldThrowOrtExceptionWhenNonValidThrottlingInferenceRow() {
+        // given
+        final String[][] throttlingInferenceRowWithMissingColumn = {{
+                "Chrome 59", "adunitcodevalue", "US", "www.leparisien.fr", "PC", "10", "1"}};
+
+        // when & then
+        assertThatThrownBy(() -> target.runModel(throttlingInferenceRowWithMissingColumn))
+                .isInstanceOf(OrtException.class);
+    }
+
+    private OnnxModelRunner givenOnnxModelRunner() throws OrtException, IOException {
+        final byte[] onnxModelBytes = Files.readAllBytes(Paths.get(
+                "src/test/resources/models_pbuid=test-pbuid.onnx"));
+        return new OnnxModelRunner(onnxModelBytes);
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java
new file mode 100644
index 00000000000..90a8d521f71
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/core/ThresholdCacheTest.java
@@ -0,0 +1,198 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.core;
+
+import com.github.benmanes.caffeine.cache.Cache;
+import com.google.cloud.storage.Blob;
+import com.google.cloud.storage.Bucket;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageException;
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mock.Strictness.LENIENT;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class ThresholdCacheTest {
+
+    private static final String GCS_BUCKET_NAME = "test_bucket";
+    private static final String THRESHOLD_CACHE_KEY_PREFIX = "onnxModelRunner_";
+    private static final String PBUUID = "test-pbuid";
+    private static final String THRESHOLDS_PATH = "thresholds.json";
+
+    @Mock
+    private Cache<String, ThrottlingThresholds> cache;
+
+    @Mock(strictness = LENIENT)
+    private Storage storage;
+
+    @Mock(strictness = LENIENT)
+    private Bucket bucket;
+
+    @Mock(strictness = LENIENT)
+    private Blob blob;
+
+    @Mock
+    private ThrottlingThresholds throttlingThresholds;
+
+    @Mock(strictness = LENIENT)
+    private ThrottlingThresholdsFactory throttlingThresholdsFactory;
+
+    private Vertx vertx;
+
+    private ThresholdCache target;
+
+    @BeforeEach
+    public void setUp() {
+        vertx = Vertx.vertx();
+        target = new ThresholdCache(
+                storage,
+                GCS_BUCKET_NAME,
+                TestBidRequestProvider.MAPPER,
+                cache,
+                THRESHOLD_CACHE_KEY_PREFIX,
+                vertx,
+                throttlingThresholdsFactory);
+    }
+
+    @Test
+    public void getShouldReturnThresholdsFromCacheWhenPresent() {
+        // given
+        final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID;
+        when(cache.getIfPresent(eq(cacheKey))).thenReturn(throttlingThresholds);
+
+        // when
+        final Future<ThrottlingThresholds> future = target.get(THRESHOLDS_PATH, PBUUID);
+
+        // then
+        assertThat(future.succeeded()).isTrue();
+        assertThat(future.result()).isEqualTo(throttlingThresholds);
+        verify(cache).getIfPresent(eq(cacheKey));
+    }
+
+    @Test
+    public void getShouldSkipFetchingWhenFetchingInProgress() throws NoSuchFieldException, IllegalAccessException {
+        // given
+        final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID;
+
+        final ThresholdCache spyThresholdCache = spy(target);
+        final AtomicBoolean mockFetchingState = mock(AtomicBoolean.class);
+
+        when(cache.getIfPresent(eq(cacheKey))).thenReturn(null);
+        when(mockFetchingState.compareAndSet(false, true)).thenReturn(false);
+
+        final Field isFetchingField = ThresholdCache.class.getDeclaredField("isFetching");
+        isFetchingField.setAccessible(true);
+        isFetchingField.set(spyThresholdCache, mockFetchingState);
+
+        // when
+        final Future<ThrottlingThresholds> result = spyThresholdCache.get(THRESHOLDS_PATH, PBUUID);
+
+        // then
+        assertThat(result.failed()).isTrue();
+        assertThat(result.cause().getMessage()).isEqualTo(
+                "ThrottlingThresholds fetching in progress. Skip current request");
+    }
+
+    @Test
+    public void getShouldFetchThresholdsWhenNotInCache() throws IOException {
+        // given
+        final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID;
+        final String jsonContent = "test_json_content";
+        final byte[] bytes = jsonContent.getBytes(StandardCharsets.UTF_8);
+
+        when(cache.getIfPresent(eq(cacheKey))).thenReturn(null);
+        when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket);
+        when(bucket.get(THRESHOLDS_PATH)).thenReturn(blob);
+        when(blob.getContent()).thenReturn(bytes);
+        when(throttlingThresholdsFactory.create(bytes, TestBidRequestProvider.MAPPER))
+                .thenReturn(throttlingThresholds);
+
+        // when
+        final Future<ThrottlingThresholds> future = target.get(THRESHOLDS_PATH, PBUUID);
+
+        // then
+        future.onComplete(ar -> {
+            assertThat(ar.succeeded()).isTrue();
+            assertThat(ar.result()).isEqualTo(throttlingThresholds);
+            verify(cache).put(eq(cacheKey), eq(throttlingThresholds));
+        });
+    }
+
+    @Test
+    public void getShouldThrowExceptionWhenStorageFails() {
+        // given
+        final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID;
+        when(cache.getIfPresent(eq(cacheKey))).thenReturn(null);
+        when(storage.get(GCS_BUCKET_NAME)).thenThrow(new StorageException(500, "Storage Error"));
+
+        // when
+        final Future<ThrottlingThresholds> future = target.get(THRESHOLDS_PATH, PBUUID);
+
+        // then
+        future.onComplete(ar -> {
+            assertThat(ar.cause()).isInstanceOf(PreBidException.class);
+            assertThat(ar.cause().getMessage()).contains("Error accessing GCS artefact for threshold");
+        });
+    }
+
+    @Test
+    public void getShouldThrowExceptionWhenLoadingJsonFails() throws IOException {
+        // given
+        final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID;
+        final String jsonContent = "test_json_content";
+        final byte[] bytes = jsonContent.getBytes(StandardCharsets.UTF_8);
+        when(cache.getIfPresent(eq(cacheKey))).thenReturn(null);
+        when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket);
+        when(bucket.get(THRESHOLDS_PATH)).thenReturn(blob);
+        when(blob.getContent()).thenReturn(bytes);
+        when(throttlingThresholdsFactory.create(bytes, TestBidRequestProvider.MAPPER)).thenThrow(
+                new IOException("Failed to load throttling thresholds json"));
+
+        // when
+        final Future<ThrottlingThresholds> future = target.get(THRESHOLDS_PATH, PBUUID);
+
+        // then
+        future.onComplete(ar -> {
+            assertThat(ar.cause()).isInstanceOf(PreBidException.class);
+            assertThat(ar.cause().getMessage()).contains("Failed to load throttling thresholds json");
+        });
+    }
+
+    @Test
+    public void getShouldThrowExceptionWhenBucketNotFound() {
+        // given
+        final String cacheKey = THRESHOLD_CACHE_KEY_PREFIX + PBUUID;
+        when(cache.getIfPresent(eq(cacheKey))).thenReturn(null);
+        when(storage.get(GCS_BUCKET_NAME)).thenReturn(bucket);
+        when(bucket.get(THRESHOLDS_PATH)).thenReturn(blob);
+        when(blob.getContent()).thenThrow(new PreBidException("Bucket not found"));
+
+        // when
+        final Future<ThrottlingThresholds> future = target.get(THRESHOLDS_PATH, PBUUID);
+
+        // then
+        future.onComplete(ar -> {
+            assertThat(ar.failed()).isTrue();
+            assertThat(ar.cause()).isInstanceOf(PreBidException.class);
+            assertThat(ar.cause().getMessage()).contains("Bucket not found");
+        });
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java
new file mode 100644
index 00000000000..11ca069e447
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/util/TestBidRequestProvider.java
@@ -0,0 +1,93 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.util;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.iab.openrtb.request.Banner;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Format;
+import com.iab.openrtb.request.Imp;
+import com.iab.openrtb.request.Site;
+import org.prebid.server.json.ObjectMapperProvider;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.UnaryOperator;
+
+public class TestBidRequestProvider {
+
+    public static final ObjectMapper MAPPER = ObjectMapperProvider.mapper();
+
+    private TestBidRequestProvider() { }
+
+    public static BidRequest givenBidRequest(
+            UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
+            List<Imp> imps,
+            Device device,
+            ExtRequest extRequest) {
+
+        return bidRequestCustomizer.apply(BidRequest.builder()
+                .id("request")
+                .imp(imps)
+                .site(givenSite(site -> site))
+                .device(device)
+                .ext(extRequest)).build();
+    }
+
+    public static Site givenSite(UnaryOperator<Site.SiteBuilder> siteCustomizer) {
+        return siteCustomizer.apply(Site.builder().domain("www.leparisien.fr")).build();
+    }
+
+    public static ObjectNode givenImpExt() {
+        final ObjectNode bidderNode = MAPPER.createObjectNode();
+
+        final ObjectNode rubiconNode = MAPPER.createObjectNode();
+        rubiconNode.put("accountId", 1001);
+        rubiconNode.put("siteId", 267318);
+        rubiconNode.put("zoneId", 1861698);
+        bidderNode.set("rubicon", rubiconNode);
+
+        final ObjectNode appnexusNode = MAPPER.createObjectNode();
+        appnexusNode.put("placementId", 123456);
+        bidderNode.set("appnexus", appnexusNode);
+
+        final ObjectNode pubmaticNode = MAPPER.createObjectNode();
+        pubmaticNode.put("publisherId", "156209");
+        pubmaticNode.put("adSlot", "slot1@300x250");
+        bidderNode.set("pubmatic", pubmaticNode);
+
+        final ObjectNode prebidNode = MAPPER.createObjectNode();
+        prebidNode.set("bidder", bidderNode);
+
+        final ObjectNode extNode = MAPPER.createObjectNode();
+        extNode.set("prebid", prebidNode);
+        extNode.set("tid", TextNode.valueOf("67eaab5f-27a6-4689-93f7-bd8f024576e3"));
+
+        return extNode;
+    }
+
+    public static Device givenDevice(UnaryOperator<Device.DeviceBuilder> deviceCustomizer) {
+        final String userAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36"
+                + " (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36";
+        return deviceCustomizer.apply(Device.builder().ua(userAgent).ip("151.101.194.216")).build();
+    }
+
+    public static Device givenDeviceWithoutUserAgent(UnaryOperator<Device.DeviceBuilder> deviceCustomizer) {
+        return deviceCustomizer.apply(Device.builder().ip("151.101.194.216")).build();
+    }
+
+    public static Banner givenBanner() {
+        final Format format = Format.builder()
+                .w(320)
+                .h(50)
+                .build();
+
+        return Banner.builder()
+                .format(Collections.singletonList(format))
+                .w(240)
+                .h(400)
+                .build();
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java
new file mode 100644
index 00000000000..157b70b9474
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java
@@ -0,0 +1,466 @@
+package org.prebid.server.hooks.modules.greenbids.real.time.data.v1;
+
+import ai.onnxruntime.OrtException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.github.benmanes.caffeine.cache.Cache;
+import com.google.cloud.storage.Storage;
+import com.google.cloud.storage.StorageOptions;
+import com.iab.openrtb.request.Banner;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Imp;
+import com.maxmind.geoip2.DatabaseReader;
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult;
+import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult;
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.config.DatabaseReaderFactory;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ModelCache;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerFactory;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThresholdCache;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThrottlingThresholdsFactory;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult;
+import org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.analytics.Result;
+import org.prebid.server.hooks.v1.analytics.Tags;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+import org.prebid.server.model.HttpRequestContext;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.UnaryOperator;
+
+import static java.util.function.UnaryOperator.identity;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mock.Strictness.LENIENT;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBanner;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenBidRequest;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDevice;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenDeviceWithoutUserAgent;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenImpExt;
+import static org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider.givenSite;
+
+@ExtendWith(MockitoExtension.class)
+public class GreenbidsRealTimeDataProcessedAuctionRequestHookTest {
+
+    @Mock
+    private Cache<String, OnnxModelRunner> modelCacheWithExpiration;
+
+    @Mock
+    private Cache<String, ThrottlingThresholds> thresholdsCacheWithExpiration;
+
+    @Mock(strictness = LENIENT)
+    private DatabaseReaderFactory databaseReaderFactory;
+
+    @Mock
+    private DatabaseReader dbReader;
+
+    private GreenbidsRealTimeDataProcessedAuctionRequestHook target;
+
+    @BeforeEach
+    public void setUp() throws IOException {
+        final Storage storage = StorageOptions.newBuilder()
+                .setProjectId("test_project").build().getService();
+        final FilterService filterService = new FilterService();
+        final OnnxModelRunnerFactory onnxModelRunnerFactory = new OnnxModelRunnerFactory();
+        final ThrottlingThresholdsFactory throttlingThresholdsFactory = new ThrottlingThresholdsFactory();
+        final ModelCache modelCache = new ModelCache(
+                storage,
+                "test_bucket",
+                modelCacheWithExpiration,
+                "onnxModelRunner_",
+                Vertx.vertx(),
+                onnxModelRunnerFactory);
+        final ThresholdCache thresholdCache = new ThresholdCache(
+                storage,
+                "test_bucket",
+                TestBidRequestProvider.MAPPER,
+                thresholdsCacheWithExpiration,
+                "throttlingThresholds_",
+                Vertx.vertx(),
+                throttlingThresholdsFactory);
+        final OnnxModelRunnerWithThresholds onnxModelRunnerWithThresholds = new OnnxModelRunnerWithThresholds(
+                modelCache,
+                thresholdCache);
+        when(databaseReaderFactory.getDatabaseReader()).thenReturn(dbReader);
+        final GreenbidsInferenceDataService greenbidsInferenceDataService = new GreenbidsInferenceDataService(
+                databaseReaderFactory,
+                TestBidRequestProvider.MAPPER);
+        final GreenbidsInvocationService greenbidsInvocationService = new GreenbidsInvocationService();
+        target = new GreenbidsRealTimeDataProcessedAuctionRequestHook(
+                TestBidRequestProvider.MAPPER,
+                filterService,
+                onnxModelRunnerWithThresholds,
+                greenbidsInferenceDataService,
+                greenbidsInvocationService);
+    }
+
+    @Test
+    public void callShouldExitEarlyWhenPartnerNotActivatedInBidRequest() {
+        // given
+        final Banner banner = givenBanner();
+
+        final Imp imp = Imp.builder()
+                .id("adunitcodevalue")
+                .ext(givenImpExt())
+                .banner(banner)
+                .build();
+
+        final Device device = givenDevice(identity());
+        final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, null);
+        final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context);
+        final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(auctionContext);
+        when(invocationContext.auctionContext()).thenReturn(auctionContext);
+
+        // when
+        final Future<InvocationResult<AuctionRequestPayload>> future = target
+                .call(null, invocationContext);
+        final InvocationResult<AuctionRequestPayload> result = future.result();
+
+        // then
+        assertThat(future).isNotNull();
+        assertThat(future.succeeded()).isTrue();
+        assertThat(result).isNotNull();
+        assertThat(result.status()).isEqualTo(InvocationStatus.success);
+        assertThat(result.action()).isEqualTo(InvocationAction.no_action);
+        assertThat(result.analyticsTags()).isNull();
+    }
+
+    @Disabled("Broken until dbReader is mocked")
+    @Test
+    public void callShouldNotFilterBiddersAndReturnAnalyticsTagWhenExploration() throws OrtException, IOException {
+        // given
+        final Banner banner = givenBanner();
+
+        final Imp imp = Imp.builder()
+                .id("adunitcodevalue")
+                .ext(givenImpExt())
+                .banner(banner)
+                .build();
+
+        final Double explorationRate = 1.0;
+        final Device device = givenDevice(identity());
+        final ExtRequest extRequest = givenExtRequest(explorationRate);
+        final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, extRequest);
+        final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context);
+        final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(auctionContext);
+        when(invocationContext.auctionContext()).thenReturn(auctionContext);
+        when(modelCacheWithExpiration.getIfPresent("onnxModelRunner_test-pbuid"))
+                .thenReturn(givenOnnxModelRunner());
+        when(thresholdsCacheWithExpiration.getIfPresent("throttlingThresholds_test-pbuid"))
+                .thenReturn(givenThrottlingThresholds());
+
+        final AnalyticsResult expectedAnalyticsResult = expectedAnalyticsResult(true, true);
+
+        // when
+        final Future<InvocationResult<AuctionRequestPayload>> future = target
+                .call(null, invocationContext);
+        final InvocationResult<AuctionRequestPayload> result = future.result();
+
+        // then
+        final ActivityImpl activity = (ActivityImpl) result.analyticsTags().activities().getFirst();
+        final ResultImpl resultImpl = (ResultImpl) activity.results().getFirst();
+        final String fingerprint = resultImpl.values()
+                .get("adunitcodevalue")
+                .get("greenbids")
+                .get("fingerprint").asText();
+
+        assertThat(future).isNotNull();
+        assertThat(future.succeeded()).isTrue();
+        assertThat(result).isNotNull();
+        assertThat(result.status()).isEqualTo(InvocationStatus.success);
+        assertThat(result.action()).isEqualTo(InvocationAction.no_action);
+        assertThat(result.analyticsTags()).isNotNull();
+        assertThat(result.analyticsTags()).usingRecursiveComparison()
+                .ignoringFields(
+                        "activities.results"
+                                + ".values._children"
+                                + ".adunitcodevalue._children"
+                                + ".greenbids._children.fingerprint")
+                .isEqualTo(toAnalyticsTags(List.of(expectedAnalyticsResult)));
+        assertThat(fingerprint).isNotNull();
+    }
+
+    @Disabled("Broken until dbReader is mocked")
+    @Test
+    public void callShouldFilterBiddersBasedOnModelWhenAnyFeatureNotAvailable() throws OrtException, IOException {
+        // given
+        final Banner banner = givenBanner();
+
+        final Imp imp = Imp.builder()
+                .id("adunitcodevalue")
+                .ext(givenImpExt())
+                .banner(banner)
+                .build();
+
+        final Double explorationRate = 0.0001;
+        final Device device = givenDeviceWithoutUserAgent(identity());
+        final ExtRequest extRequest = givenExtRequest(explorationRate);
+        final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, extRequest);
+        final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context);
+        final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(auctionContext);
+        when(invocationContext.auctionContext()).thenReturn(auctionContext);
+        when(modelCacheWithExpiration.getIfPresent("onnxModelRunner_test-pbuid"))
+                .thenReturn(givenOnnxModelRunner());
+        when(thresholdsCacheWithExpiration.getIfPresent("throttlingThresholds_test-pbuid"))
+                .thenReturn(givenThrottlingThresholds());
+
+        final BidRequest expectedBidRequest = expectedUpdatedBidRequest(request -> request, explorationRate, device);
+        final AnalyticsResult expectedAnalyticsResult = expectedAnalyticsResult(false, false);
+
+        // when
+        final Future<InvocationResult<AuctionRequestPayload>> future = target
+                .call(null, invocationContext);
+        final InvocationResult<AuctionRequestPayload> result = future.result();
+        final BidRequest resultBidRequest = result
+                .payloadUpdate()
+                .apply(AuctionRequestPayloadImpl.of(bidRequest))
+                .bidRequest();
+
+        // then
+        final ActivityImpl activity = (ActivityImpl) result.analyticsTags().activities().getFirst();
+        final ResultImpl resultImpl = (ResultImpl) activity.results().getFirst();
+        final String fingerprint = resultImpl.values()
+                .get("adunitcodevalue")
+                .get("greenbids")
+                .get("fingerprint").asText();
+
+        assertThat(future).isNotNull();
+        assertThat(future.succeeded()).isTrue();
+        assertThat(result).isNotNull();
+        assertThat(result.status()).isEqualTo(InvocationStatus.success);
+        assertThat(result.action()).isEqualTo(InvocationAction.update);
+        assertThat(result.analyticsTags()).isNotNull();
+        assertThat(result.analyticsTags()).usingRecursiveComparison()
+                .ignoringFields(
+                        "activities.results"
+                                + ".values._children"
+                                + ".adunitcodevalue._children"
+                                + ".greenbids._children.fingerprint")
+                .isEqualTo(toAnalyticsTags(List.of(expectedAnalyticsResult)));
+        assertThat(fingerprint).isNotNull();
+        assertThat(resultBidRequest).usingRecursiveComparison().isEqualTo(expectedBidRequest);
+    }
+
+    @Disabled("Broken until dbReader is mocked")
+    @Test
+    public void callShouldFilterBiddersBasedOnModelResults() throws OrtException, IOException {
+        // given
+        final Banner banner = givenBanner();
+
+        final Imp imp = Imp.builder()
+                .id("adunitcodevalue")
+                .ext(givenImpExt())
+                .banner(banner)
+                .build();
+
+        final Double explorationRate = 0.0001;
+        final Device device = givenDevice(identity());
+        final ExtRequest extRequest = givenExtRequest(explorationRate);
+        final BidRequest bidRequest = givenBidRequest(request -> request, List.of(imp), device, extRequest);
+        final AuctionContext auctionContext = givenAuctionContext(bidRequest, context -> context);
+        final AuctionInvocationContext invocationContext = givenAuctionInvocationContext(auctionContext);
+        when(invocationContext.auctionContext()).thenReturn(auctionContext);
+        when(modelCacheWithExpiration.getIfPresent("onnxModelRunner_test-pbuid"))
+                .thenReturn(givenOnnxModelRunner());
+        when(thresholdsCacheWithExpiration.getIfPresent("throttlingThresholds_test-pbuid"))
+                .thenReturn(givenThrottlingThresholds());
+
+        final BidRequest expectedBidRequest = expectedUpdatedBidRequest(
+                request -> request, explorationRate, device);
+        final AnalyticsResult expectedAnalyticsResult = expectedAnalyticsResult(false, false);
+
+        // when
+        final Future<InvocationResult<AuctionRequestPayload>> future = target
+                .call(null, invocationContext);
+        final InvocationResult<AuctionRequestPayload> result = future.result();
+        final BidRequest resultBidRequest = result
+                .payloadUpdate()
+                .apply(AuctionRequestPayloadImpl.of(bidRequest))
+                .bidRequest();
+
+        // then
+        final ActivityImpl activityImpl = (ActivityImpl) result.analyticsTags().activities().getFirst();
+        final ResultImpl resultImpl = (ResultImpl) activityImpl.results().getFirst();
+        final String fingerprint = resultImpl.values()
+                .get("adunitcodevalue")
+                .get("greenbids")
+                .get("fingerprint").asText();
+
+        assertThat(future).isNotNull();
+        assertThat(future.succeeded()).isTrue();
+        assertThat(result).isNotNull();
+        assertThat(result.status()).isEqualTo(InvocationStatus.success);
+        assertThat(result.action()).isEqualTo(InvocationAction.update);
+        assertThat(result.analyticsTags()).isNotNull();
+        assertThat(result.analyticsTags()).usingRecursiveComparison()
+                .ignoringFields(
+                        "activities.results"
+                                + ".values._children"
+                                + ".adunitcodevalue._children"
+                                + ".greenbids._children.fingerprint")
+                .isEqualTo(toAnalyticsTags(List.of(expectedAnalyticsResult)));
+        assertThat(fingerprint).isNotNull();
+        assertThat(resultBidRequest).usingRecursiveComparison()
+                .isEqualTo(expectedBidRequest);
+    }
+
+    static ExtRequest givenExtRequest(Double explorationRate) {
+        final ObjectNode greenbidsNode = TestBidRequestProvider.MAPPER.createObjectNode();
+        greenbidsNode.put("pbuid", "test-pbuid");
+        greenbidsNode.put("targetTpr", 0.60);
+        greenbidsNode.put("explorationRate", explorationRate);
+
+        final ObjectNode analyticsNode = TestBidRequestProvider.MAPPER.createObjectNode();
+        analyticsNode.set("greenbids-rtd", greenbidsNode);
+
+        return ExtRequest.of(ExtRequestPrebid
+                .builder()
+                .analytics(analyticsNode)
+                .build());
+    }
+
+    private AuctionContext givenAuctionContext(
+            BidRequest bidRequest,
+            UnaryOperator<AuctionContext.AuctionContextBuilder> auctionContextCustomizer) {
+
+        final AuctionContext.AuctionContextBuilder auctionContextBuilder = AuctionContext.builder()
+                .httpRequest(HttpRequestContext.builder().build())
+                .bidRequest(bidRequest);
+
+        return auctionContextCustomizer.apply(auctionContextBuilder).build();
+    }
+
+    private AuctionInvocationContext givenAuctionInvocationContext(AuctionContext auctionContext) {
+        final AuctionInvocationContext invocationContext = mock(AuctionInvocationContext.class);
+        when(invocationContext.auctionContext()).thenReturn(auctionContext);
+        return invocationContext;
+    }
+
+    private OnnxModelRunner givenOnnxModelRunner() throws OrtException, IOException {
+        final byte[] onnxModelBytes = Files.readAllBytes(Paths.get(
+                "src/test/resources/models_pbuid=test-pbuid.onnx"));
+        return new OnnxModelRunner(onnxModelBytes);
+    }
+
+    private ThrottlingThresholds givenThrottlingThresholds() throws IOException {
+        final JsonNode thresholdsJsonNode = TestBidRequestProvider.MAPPER.readTree(
+                Files.newInputStream(Paths.get(
+                        "src/test/resources/thresholds_pbuid=test-pbuid.json")));
+        return TestBidRequestProvider.MAPPER
+                .treeToValue(thresholdsJsonNode, ThrottlingThresholds.class);
+    }
+
+    private BidRequest expectedUpdatedBidRequest(
+            UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
+            Double explorationRate,
+            Device device) {
+
+        final Banner banner = givenBanner();
+
+        final ObjectNode bidderNode = TestBidRequestProvider.MAPPER.createObjectNode();
+        final ObjectNode prebidNode = TestBidRequestProvider.MAPPER.createObjectNode();
+        prebidNode.set("bidder", bidderNode);
+
+        final ObjectNode extNode = TestBidRequestProvider.MAPPER.createObjectNode();
+        extNode.set("prebid", prebidNode);
+        extNode.set("tid", TextNode.valueOf("67eaab5f-27a6-4689-93f7-bd8f024576e3"));
+
+        final Imp imp = Imp.builder()
+                .id("adunitcodevalue")
+                .ext(extNode)
+                .banner(banner)
+                .build();
+
+        return bidRequestCustomizer.apply(BidRequest.builder()
+                .id("request")
+                .imp(List.of(imp))
+                .site(givenSite(site -> site))
+                .device(device)
+                .ext(givenExtRequest(explorationRate))).build();
+    }
+
+    private AnalyticsResult expectedAnalyticsResult(Boolean isExploration, Boolean isKeptInAuction) {
+        return AnalyticsResult.of(
+                "success",
+                Map.of("adunitcodevalue", expectedOrtb2ImpExtResult(isExploration, isKeptInAuction)),
+                null,
+                null);
+    }
+
+    private Ortb2ImpExtResult expectedOrtb2ImpExtResult(Boolean isExploration, Boolean isKeptInAuction) {
+        return Ortb2ImpExtResult.of(
+                expectedExplorationResult(isExploration, isKeptInAuction), "67eaab5f-27a6-4689-93f7-bd8f024576e3");
+    }
+
+    private ExplorationResult expectedExplorationResult(Boolean isExploration, Boolean isKeptInAuction) {
+        final Map<String, Boolean> keptInAuction = Map.of(
+                "appnexus", isKeptInAuction,
+                "pubmatic", isKeptInAuction,
+                "rubicon", isKeptInAuction);
+        return ExplorationResult.of("60a7c66c-c542-48c6-a319-ea7b9f97947f", keptInAuction, isExploration);
+    }
+
+    private Tags toAnalyticsTags(List<AnalyticsResult> analyticsResults) {
+        return TagsImpl.of(Collections.singletonList(ActivityImpl.of(
+                "greenbids-filter",
+                "success",
+                toResults(analyticsResults))));
+    }
+
+    private List<Result> toResults(List<AnalyticsResult> analyticsResults) {
+        return analyticsResults.stream()
+                .map(this::toResult)
+                .toList();
+    }
+
+    private Result toResult(AnalyticsResult analyticsResult) {
+        return ResultImpl.of(
+                analyticsResult.getStatus(),
+                toObjectNode(analyticsResult.getValues()),
+                AppliedToImpl.builder()
+                        .bidders(Collections.singletonList(analyticsResult.getBidder()))
+                        .impIds(Collections.singletonList(analyticsResult.getImpId()))
+                        .build());
+    }
+
+    private ObjectNode toObjectNode(Map<String, Ortb2ImpExtResult> values) {
+        return values != null ? TestBidRequestProvider.MAPPER.valueToTree(values) : null;
+    }
+}
diff --git a/extra/modules/greenbids-real-time-data/src/test/resources/models_pbuid=test-pbuid.onnx b/extra/modules/greenbids-real-time-data/src/test/resources/models_pbuid=test-pbuid.onnx
new file mode 100644
index 00000000000..f0acc8c66fe
Binary files /dev/null and b/extra/modules/greenbids-real-time-data/src/test/resources/models_pbuid=test-pbuid.onnx differ
diff --git a/extra/modules/greenbids-real-time-data/src/test/resources/thresholds_pbuid=test-pbuid.json b/extra/modules/greenbids-real-time-data/src/test/resources/thresholds_pbuid=test-pbuid.json
new file mode 100644
index 00000000000..462a6459297
--- /dev/null
+++ b/extra/modules/greenbids-real-time-data/src/test/resources/thresholds_pbuid=test-pbuid.json
@@ -0,0 +1,14 @@
+{
+  "thresholds": [
+    0.4,
+    0.224,
+    0.018,
+    0.018
+  ],
+  "tpr": [
+    0.8,
+    0.95,
+    0.99,
+    0.9999
+  ]
+}
diff --git a/extra/modules/ortb2-blocking/pom.xml b/extra/modules/ortb2-blocking/pom.xml
index 90fe75bac96..6f52cb99006 100644
--- a/extra/modules/ortb2-blocking/pom.xml
+++ b/extra/modules/ortb2-blocking/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.prebid.server.hooks.modules</groupId>
         <artifactId>all-modules</artifactId>
-        <version>3.15.0-SNAPSHOT</version>
+        <version>3.19.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>ortb2-blocking</artifactId>
diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java
index a7ee0425135..47b3e3204c7 100644
--- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java
+++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReader.java
@@ -104,13 +104,13 @@ public Result<BlockedAttributes> blockedAttributesFor(BidRequest bidRequest) {
         final Result<List<String>> bapp =
                 blockedAttribute(BAPP_FIELD, String.class, BLOCKED_APP_FIELD, requestMediaTypes);
         final Result<Map<String, List<Integer>>> btype =
-                blockedAttributesForImps(BTYPE_FIELD, Integer.class, BLOCKED_BANNER_TYPE_FIELD, bidRequest);
+                blockedAttributesForImps(BTYPE_FIELD, Integer.class, BLOCKED_BANNER_TYPE_FIELD, BANNER_MEDIA_TYPE, bidRequest);
         final Result<Map<String, List<Integer>>> bannerBattr =
-                blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_BANNER_ATTR_FIELD, bidRequest);
+                blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_BANNER_ATTR_FIELD, BANNER_MEDIA_TYPE, bidRequest);
         final Result<Map<String, List<Integer>>> videoBattr =
-                blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_VIDEO_ATTR_FIELD, bidRequest);
+                blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_VIDEO_ATTR_FIELD, VIDEO_MEDIA_TYPE, bidRequest);
         final Result<Map<String, List<Integer>>> audioBattr =
-                blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_AUDIO_ATTR_FIELD, bidRequest);
+                blockedAttributesForImps(BATTR_FIELD, Integer.class, BLOCKED_AUDIO_ATTR_FIELD, AUDIO_MEDIA_TYPE, bidRequest);
         final Result<Map<MediaType, Map<String, List<Integer>>>> battr =
                 mergeBlockedAttributes(bannerBattr, videoBattr, audioBattr);
 
@@ -226,19 +226,23 @@ private Integer blockedCattaxComplementFromConfig() {
     private <T> Result<Map<String, List<T>>> blockedAttributesForImps(String attribute,
                                                                       Class<T> attributeType,
                                                                       String fieldName,
+                                                                      String attributeMediaType,
                                                                       BidRequest bidRequest) {
 
         final Map<String, List<T>> attributeValues = new HashMap<>();
         final List<Result<?>> results = new ArrayList<>();
 
         for (final Imp imp : bidRequest.getImp()) {
-            final Result<List<T>> attributeForImp = blockedAttribute(
-                    attribute, attributeType, fieldName, mediaTypesFrom(imp));
-
-            if (attributeForImp.hasValue()) {
-                attributeValues.put(imp.getId(), attributeForImp.getValue());
+            final Set<String> actualMediaTypes = mediaTypesFrom(imp);
+            if (actualMediaTypes.contains(attributeMediaType)) {
+                final Result<List<T>> attributeForImp = blockedAttribute(
+                        attribute, attributeType, fieldName, actualMediaTypes);
+
+                if (attributeForImp.hasValue()) {
+                    attributeValues.put(imp.getId(), attributeForImp.getValue());
+                }
+                results.add(attributeForImp);
             }
-            results.add(attributeForImp);
         }
 
         return Result.of(
diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java
index 2c9b40b6079..5435544e2cb 100644
--- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java
+++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlocker.java
@@ -105,7 +105,8 @@ public ExecutionResult<BlockedBids> block() {
             final List<String> warnings = MergeUtils.mergeMessages(blockedBidResults);
 
             if (blockedBids != null) {
-                rejectBlockedBids(blockedBidResults);
+                blockedBidIndexes.forEach(index ->
+                        rejectBlockedBid(blockedBidResults.get(index).getValue(), bids.get(index)));
             }
 
             return ExecutionResult.<BlockedBids>builder()
@@ -287,26 +288,19 @@ private String debugEntryFor(int index, BlockingResult blockingResult) {
                 blockingResult.getFailedChecks());
     }
 
-    private void rejectBlockedBids(List<Result<BlockingResult>> blockedBidResults) {
-        blockedBidResults.stream()
-                .map(Result::getValue)
-                .filter(BlockingResult::isBlocked)
-                .forEach(this::rejectBlockedBid);
-    }
-
-    private void rejectBlockedBid(BlockingResult blockingResult) {
+    private void rejectBlockedBid(BlockingResult blockingResult, BidderBid blockedBid) {
         if (blockingResult.getBattrCheckResult().isFailed()
                 || blockingResult.getBappCheckResult().isFailed()
                 || blockingResult.getBcatCheckResult().isFailed()) {
 
-            bidRejectionTracker.reject(
-                    blockingResult.getImpId(),
+            bidRejectionTracker.rejectBid(
+                    blockedBid,
                     BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
         }
 
         if (blockingResult.getBadvCheckResult().isFailed()) {
-            bidRejectionTracker.reject(
-                    blockingResult.getImpId(),
+            bidRejectionTracker.rejectBid(
+                    blockedBid,
                     BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED);
         }
     }
diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java
index ac963b94857..78744e7c07f 100644
--- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java
+++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdater.java
@@ -13,7 +13,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Optional;
 
 public class RequestUpdater {
 
@@ -99,13 +98,15 @@ private static List<Integer> extractBattr(Map<MediaType, Map<String, List<Intege
     }
 
     private static Banner updateBanner(Banner banner, List<Integer> btype, List<Integer> battr) {
-        final List<Integer> existingBtype = banner != null ? banner.getBtype() : null;
-        final List<Integer> existingBattr = banner != null ? banner.getBattr() : null;
+        if (banner == null) {
+            return null;
+        }
+
+        final List<Integer> existingBtype = banner.getBtype();
+        final List<Integer> existingBattr = banner.getBattr();
 
         return CollectionUtils.isEmpty(existingBtype) || CollectionUtils.isEmpty(existingBattr)
-                ? Optional.ofNullable(banner)
-                .map(Banner::toBuilder)
-                .orElseGet(Banner::builder)
+                ? banner.toBuilder()
                 .btype(CollectionUtils.isNotEmpty(existingBtype) ? existingBtype : btype)
                 .battr(CollectionUtils.isNotEmpty(existingBattr) ? existingBattr : battr)
                 .build()
@@ -113,22 +114,26 @@ private static Banner updateBanner(Banner banner, List<Integer> btype, List<Inte
     }
 
     private static Video updateVideo(Video video, List<Integer> battr) {
-        final List<Integer> existingBattr = video != null ? video.getBattr() : null;
+        if (video == null) {
+            return null;
+        }
+
+        final List<Integer> existingBattr = video.getBattr();
         return CollectionUtils.isEmpty(existingBattr)
-                ? Optional.ofNullable(video)
-                .map(Video::toBuilder)
-                .orElseGet(Video::builder)
+                ? video.toBuilder()
                 .battr(battr)
                 .build()
                 : video;
     }
 
     private static Audio updateAudio(Audio audio, List<Integer> battr) {
-        final List<Integer> existingBattr = audio != null ? audio.getBattr() : null;
+        if (audio == null) {
+            return null;
+        }
+
+        final List<Integer> existingBattr = audio.getBattr();
         return CollectionUtils.isEmpty(existingBattr)
-                ? Optional.ofNullable(audio)
-                .map(Audio::toBuilder)
-                .orElseGet(Audio::builder)
+                ? audio.toBuilder()
                 .battr(battr)
                 .build()
                 : audio;
diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java
index ff9e6ab6c3c..6ef69c93140 100644
--- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java
+++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java
@@ -5,13 +5,13 @@
 import org.prebid.server.auction.BidderAliases;
 import org.prebid.server.auction.versionconverter.OrtbVersion;
 import org.prebid.server.bidder.BidderCatalog;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.BlockedAttributesResolver;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.RequestUpdater;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult;
 import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderRequestPayloadImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationResult;
 import org.prebid.server.hooks.v1.InvocationStatus;
diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java
index 329e99d3bbc..720823f4513 100644
--- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java
+++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java
@@ -6,18 +6,18 @@
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.ObjectUtils;
 import org.prebid.server.auction.versionconverter.OrtbVersion;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.BidsBlocker;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.ResponseUpdater;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.model.AnalyticsResult;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedBids;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult;
 import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderResponsePayloadImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ActivityImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.AppliedToImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ResultImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.TagsImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationResult;
 import org.prebid.server.hooks.v1.InvocationStatus;
diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java
deleted file mode 100644
index bd394217b21..00000000000
--- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.prebid.server.hooks.modules.ortb2.blocking.v1.model;
-
-import com.iab.openrtb.request.BidRequest;
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.v1.bidder.BidderRequestPayload;
-
-@Accessors(fluent = true)
-@Value(staticConstructor = "of")
-public class BidderRequestPayloadImpl implements BidderRequestPayload {
-
-    BidRequest bidRequest;
-}
diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java
deleted file mode 100644
index 72d678c89a5..00000000000
--- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.prebid.server.hooks.modules.ortb2.blocking.v1.model;
-
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.bidder.model.BidderBid;
-import org.prebid.server.hooks.v1.bidder.BidderResponsePayload;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Value(staticConstructor = "of")
-public class BidderResponsePayloadImpl implements BidderResponsePayload {
-
-    List<BidderBid> bids;
-}
diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java
deleted file mode 100644
index 48be15fdf37..00000000000
--- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/InvocationResultImpl.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package org.prebid.server.hooks.modules.ortb2.blocking.v1.model;
-
-import lombok.Builder;
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext;
-import org.prebid.server.hooks.v1.InvocationAction;
-import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationStatus;
-import org.prebid.server.hooks.v1.PayloadUpdate;
-import org.prebid.server.hooks.v1.analytics.Tags;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Builder
-@Value
-public class InvocationResultImpl<PAYLOAD> implements InvocationResult<PAYLOAD> {
-
-    InvocationStatus status;
-
-    String message;
-
-    InvocationAction action;
-
-    PayloadUpdate<PAYLOAD> payloadUpdate;
-
-    List<String> errors;
-
-    List<String> warnings;
-
-    List<String> debugMessages;
-
-    ModuleContext moduleContext;
-
-    Tags analyticsTags;
-}
diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java
index f6568663807..e20bf2c3dac 100644
--- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java
+++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/AccountConfigReaderTest.java
@@ -400,7 +400,7 @@ public void blockedAttributesForShouldReturnErrorWhenBlockedBannerTypeIsNotInteg
         final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true);
 
         // when and then
-        assertThatThrownBy(() -> reader.blockedAttributesFor(emptyRequest()))
+        assertThatThrownBy(() -> reader.blockedAttributesFor(request(imp -> imp.banner(Banner.builder().build()))))
                 .isInstanceOf(InvalidAccountConfigurationException.class)
                 .hasMessage("blocked-banner-type field in account configuration has unexpected type. "
                         + "Expected class java.lang.Integer");
@@ -700,17 +700,23 @@ public void blockedAttributesForShouldReturnResultWithBtypeAndWarningsFromOverri
                 .btype(Attribute.btypeBuilder()
                         .actionOverrides(AttributeActionOverrides.blocked(asList(
                                 ArrayOverride.of(
-                                        Conditions.of(singletonList("bidder1"), singletonList("video")),
+                                        Conditions.of(singletonList("bidder1"), singletonList("banner")),
                                         singletonList(1)),
                                 ArrayOverride.of(
-                                        Conditions.of(singletonList("bidder1"), singletonList("video")),
+                                        Conditions.of(singletonList("bidder1"), singletonList("banner")),
                                         singletonList(2)),
                                 ArrayOverride.of(
-                                        Conditions.of(singletonList("bidder1"), singletonList("banner")),
+                                        Conditions.of(singletonList("bidder1"), singletonList("video")),
                                         singletonList(3)),
                                 ArrayOverride.of(
-                                        Conditions.of(singletonList("bidder1"), singletonList("banner")),
-                                        singletonList(4)))))
+                                        Conditions.of(singletonList("bidder1"), singletonList("video")),
+                                        singletonList(4)),
+                                ArrayOverride.of(
+                                        Conditions.of(singletonList("bidder1"), singletonList("audio")),
+                                        singletonList(5)),
+                                ArrayOverride.of(
+                                        Conditions.of(singletonList("bidder1"), singletonList("audio")),
+                                        singletonList(6)))))
                         .build())
                 .build()));
         final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true);
@@ -718,20 +724,16 @@ public void blockedAttributesForShouldReturnResultWithBtypeAndWarningsFromOverri
         // when and then
         final Map<String, List<Integer>> expectedBtype = new HashMap<>();
         expectedBtype.put("impId1", singletonList(1));
-        expectedBtype.put("impId2", singletonList(3));
         assertThat(reader
                 .blockedAttributesFor(BidRequest.builder()
                         .imp(asList(
-                                Imp.builder().id("impId1").video(Video.builder().build()).build(),
-                                Imp.builder().id("impId2").banner(Banner.builder().build()).build()))
+                                Imp.builder().id("impId1").banner(Banner.builder().build()).build(),
+                                Imp.builder().id("impId2").video(Video.builder().build()).build()))
                         .build()))
                 .isEqualTo(Result.of(
                         BlockedAttributes.builder().btype(expectedBtype).build(),
-                        asList(
-                                "More than one conditions matches request. Bidder: bidder1, " +
-                                        "request media types: [video]",
-                                "More than one conditions matches request. Bidder: bidder1, " +
-                                        "request media types: [banner]")));
+                        List.of("More than one conditions matches request. Bidder: bidder1, " +
+                                "request media types: [banner]")));
     }
 
     @Test
@@ -778,8 +780,8 @@ public void blockedAttributesForShouldReturnResultWithAllAttributesForBanner() {
         final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true);
 
         // when and then
-        assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1")))).isEqualTo(
-                Result.withValue(BlockedAttributes.builder()
+        assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1").banner(Banner.builder().build()))))
+                .isEqualTo(Result.withValue(BlockedAttributes.builder()
                         .badv(singletonList("domain3.com"))
                         .bcat(singletonList("cat3"))
                         .bapp(singletonList("app3"))
@@ -832,12 +834,11 @@ public void blockedAttributesForShouldReturnResultWithAllAttributesForVideo() {
         final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true);
 
         // when and then
-        assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1")))).isEqualTo(
-                Result.withValue(BlockedAttributes.builder()
+        assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1").video(Video.builder().build()))))
+                .isEqualTo(Result.withValue(BlockedAttributes.builder()
                         .badv(singletonList("domain3.com"))
                         .bcat(singletonList("cat3"))
                         .bapp(singletonList("app3"))
-                        .btype(singletonMap("impId1", singletonList(3)))
                         .battr(singletonMap(MediaType.VIDEO, singletonMap("impId1", singletonList(3))))
                         .build()));
     }
@@ -886,12 +887,11 @@ public void blockedAttributesForShouldReturnResultWithAllAttributesForAudio() {
         final AccountConfigReader reader = AccountConfigReader.create(accountConfig, "bidder1", ORTB_VERSION, true);
 
         // when and then
-        assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1")))).isEqualTo(
-                Result.withValue(BlockedAttributes.builder()
+        assertThat(reader.blockedAttributesFor(request(imp -> imp.id("impId1").audio(Audio.builder().build()))))
+                .isEqualTo(Result.withValue(BlockedAttributes.builder()
                         .badv(singletonList("domain3.com"))
                         .bcat(singletonList("cat3"))
                         .bapp(singletonList("app3"))
-                        .btype(singletonMap("impId1", singletonList(3)))
                         .battr(singletonMap(MediaType.AUDIO, singletonMap("impId1", singletonList(3))))
                         .build()));
     }
diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java
index 79b1309a2ba..b595b9c5fdb 100644
--- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java
+++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/BidsBlockerTest.java
@@ -197,13 +197,13 @@ public void shouldReturnResultWithBidWhenBidWithBlockedAdomainAndEnforceBlocksTr
                 .build()));
 
         // when
-        final List<BidderBid> bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com"))));
+        final BidderBid bid = bid(bidBuilder -> bidBuilder.adomain(singletonList("domain1.com")));
         final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com"));
-        final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, false);
+        final BidsBlocker blocker = BidsBlocker.create(singletonList(bid), "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, false);
 
         // when and then
         assertThat(blocker.block()).satisfies(result -> hasValue(result, 0));
-        verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED);
+        verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED);
     }
 
     @Test
@@ -304,9 +304,9 @@ public void shouldReturnEmptyResultWhenBidWithBlockedAdomainAndInDealsExceptions
                 .build()));
 
         // when
-        final List<BidderBid> bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com"))));
+        final BidderBid bid = bid(bidBuilder -> bidBuilder.adomain(singletonList("domain1.com")));
         final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com"));
-        final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true);
+        final BidsBlocker blocker = BidsBlocker.create(singletonList(bid), "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, true);
 
         // when and then
         assertThat(blocker.block()).satisfies(BidsBlockerTest::isEmpty);
@@ -324,13 +324,13 @@ public void shouldReturnResultWithBidWhenBidWithBlockedAdomainAndNotInDealsExcep
                 .build()));
 
         // when
-        final List<BidderBid> bids = singletonList(bid(bid -> bid.adomain(singletonList("domain1.com"))));
+        final BidderBid bid = bid(bidBuilder -> bidBuilder.adomain(singletonList("domain1.com")));
         final BlockedAttributes blockedAttributes = attributesWithBadv(singletonList("domain1.com"));
-        final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, false);
+        final BidsBlocker blocker = BidsBlocker.create(singletonList(bid), "bidder1", ORTB_VERSION, accountConfig, blockedAttributes, bidRejectionTracker, false);
 
         // when and then
         assertThat(blocker.block()).satisfies(result -> hasValue(result, 0));
-        verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED);
+        verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED);
     }
 
     @Test
@@ -344,8 +344,8 @@ public void shouldReturnResultWithBidAndDebugMessageWhenBidIsBlocked() {
                 .build()));
 
         // when
-        final List<BidderBid> bids = singletonList(bid());
-        final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true);
+        final BidderBid bid = bid();
+        final BidsBlocker blocker = BidsBlocker.create(singletonList(bid), "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, true);
 
         // when and then
         assertThat(blocker.block()).satisfies(result -> {
@@ -353,7 +353,7 @@ public void shouldReturnResultWithBidAndDebugMessageWhenBidIsBlocked() {
             assertThat(result.getDebugMessages()).containsOnly(
                     "Bid 0 from bidder bidder1 has been rejected, failed checks: [bcat]");
         });
-        verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
+        verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
     }
 
     @Test
@@ -367,12 +367,12 @@ public void shouldReturnResultWithBidWithoutDebugMessageWhenBidIsBlockedAndDebug
                 .build()));
 
         // when
-        final List<BidderBid> bids = singletonList(bid());
-        final BidsBlocker blocker = BidsBlocker.create(bids, "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, false);
+        final BidderBid bid = bid();
+        final BidsBlocker blocker = BidsBlocker.create(singletonList(bid), "bidder1", ORTB_VERSION, accountConfig, null, bidRejectionTracker, false);
 
         // when and then
         assertThat(blocker.block()).satisfies(result -> hasValue(result, 0));
-        verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
+        verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
     }
 
     @Test
@@ -393,22 +393,23 @@ public void shouldReturnResultWithAnalyticsResults() {
                         .build())
                 .build()));
 
+        final BidderBid bid1 = bid(bid -> bid
+                .impid("impId1")
+                .adomain(asList("domain2.com", "domain3.com", "domain4.com"))
+                .bundle("app2"));
+        final BidderBid bid2 = bid(bid -> bid
+                .impid("impId2")
+                .cat(asList("cat2", "cat3", "cat4"))
+                .attr(asList(2, 3, 4)));
+        final BidderBid bid3 = bid(bid -> bid
+                .impid("impId1")
+                .adomain(singletonList("domain5.com"))
+                .cat(singletonList("cat5"))
+                .bundle("app5")
+                .attr(singletonList(5)));
+
         // when
-        final List<BidderBid> bids = asList(
-                bid(bid -> bid
-                        .impid("impId1")
-                        .adomain(asList("domain2.com", "domain3.com", "domain4.com"))
-                        .bundle("app2")),
-                bid(bid -> bid
-                        .impid("impId2")
-                        .cat(asList("cat2", "cat3", "cat4"))
-                        .attr(asList(2, 3, 4))),
-                bid(bid -> bid
-                        .impid("impId1")
-                        .adomain(singletonList("domain5.com"))
-                        .cat(singletonList("cat5"))
-                        .bundle("app5")
-                        .attr(singletonList(5))));
+        final List<BidderBid> bids = asList(bid1, bid2, bid3);
         final BlockedAttributes blockedAttributes = BlockedAttributes.builder()
                 .badv(asList("domain1.com", "domain2.com", "domain3.com"))
                 .bcat(asList("cat1", "cat2", "cat3"))
@@ -436,9 +437,9 @@ public void shouldReturnResultWithAnalyticsResults() {
                     AnalyticsResult.of("success-allow", null, "bidder1", "impId1"));
         });
 
-        verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
-        verify(bidRejectionTracker).reject("impId2", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
-        verify(bidRejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED);
+        verify(bidRejectionTracker).rejectBid(bid1, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
+        verify(bidRejectionTracker).rejectBid(bid2, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
+        verify(bidRejectionTracker).rejectBid(bid1, BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED);
         verifyNoMoreInteractions(bidRejectionTracker);
     }
 
@@ -467,23 +468,30 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() {
                 .build()));
 
         // when
-        final List<BidderBid> bids = asList(
-                bid(bid -> bid.adomain(singletonList("domain1.com"))),
-                bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat1"))),
-                bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2"))),
-                bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2")).bundle("app1")),
-                bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2")).bundle("app2")),
-                bid(bid -> bid
-                        .adomain(singletonList("domain2.com"))
-                        .cat(singletonList("cat2"))
-                        .bundle("app2")
-                        .attr(singletonList(1))),
-                bid(bid -> bid
-                        .adomain(singletonList("domain2.com"))
-                        .cat(singletonList("cat2"))
-                        .bundle("app2")
-                        .attr(singletonList(2))),
-                bid());
+        final BidderBid bid1 = bid(bid -> bid.adomain(singletonList("domain1.com")));
+        final BidderBid bid2 = bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat1")));
+        final BidderBid bid3 = bid(bid -> bid.adomain(singletonList("domain2.com")).cat(singletonList("cat2")));
+        final BidderBid bid4 = bid(bid -> bid
+                .adomain(singletonList("domain2.com"))
+                .cat(singletonList("cat2"))
+                .bundle("app1"));
+        final BidderBid bid5 = bid(bid -> bid
+                .adomain(singletonList("domain2.com"))
+                .cat(singletonList("cat2"))
+                .bundle("app2"));
+        final BidderBid bid6 = bid(bid -> bid
+                .adomain(singletonList("domain2.com"))
+                .cat(singletonList("cat2"))
+                .bundle("app2")
+                .attr(singletonList(1)));
+        final BidderBid bid7 = bid(bid -> bid
+                .adomain(singletonList("domain2.com"))
+                .cat(singletonList("cat2"))
+                .bundle("app2")
+                .attr(singletonList(2)));
+        final BidderBid bid8 = bid();
+
+        final List<BidderBid> bids = asList(bid1, bid2, bid3, bid4, bid5, bid6, bid7, bid8);
         final BlockedAttributes blockedAttributes = BlockedAttributes.builder()
                 .badv(asList("domain1.com", "domain2.com"))
                 .bcat(asList("cat1", "cat2"))
@@ -503,8 +511,13 @@ public void shouldReturnResultWithoutSomeBidsWhenAllAttributesInConfig() {
                     "Bid 7 from bidder bidder1 has been rejected, failed checks: [badv, bcat]");
         });
 
-        verify(bidRejectionTracker, times(5)).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
-        verify(bidRejectionTracker, times(2)).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED);
+        verify(bidRejectionTracker).rejectBid(bid1, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
+        verify(bidRejectionTracker).rejectBid(bid1, BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED);
+        verify(bidRejectionTracker).rejectBid(bid2, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
+        verify(bidRejectionTracker).rejectBid(bid4, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
+        verify(bidRejectionTracker).rejectBid(bid6, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
+        verify(bidRejectionTracker).rejectBid(bid8, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
+        verify(bidRejectionTracker).rejectBid(bid8, BidRejectionReason.RESPONSE_REJECTED_ADVERTISER_BLOCKED);
         verifyNoMoreInteractions(bidRejectionTracker);
     }
 
diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java
index 630c09e96d1..1f03f82edb1 100644
--- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java
+++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/core/RequestUpdaterTest.java
@@ -354,7 +354,12 @@ MediaType.VIDEO, singletonMap("impId1", asList(3, 4)),
                                 MediaType.AUDIO, singletonMap("impId1", asList(5, 6))))
                         .build());
         final BidRequest request = BidRequest.builder()
-                .imp(singletonList(Imp.builder().id("impId1").build()))
+                .imp(singletonList(Imp.builder()
+                        .id("impId1")
+                        .banner(Banner.builder().build())
+                        .video(Video.builder().build())
+                        .audio(Audio.builder().build())
+                        .build()))
                 .build();
 
         // when and then
@@ -373,4 +378,115 @@ MediaType.AUDIO, singletonMap("impId1", asList(5, 6))))
                         .build()))
                 .build());
     }
+
+    @Test
+    public void shouldNotUpdateMissingBanner() {
+        // given
+        final RequestUpdater updater = RequestUpdater.create(
+                BlockedAttributes.builder()
+                        .badv(asList("domain1.com", "domain2.com"))
+                        .bcat(asList("cat1", "cat2"))
+                        .bapp(asList("app1", "app2"))
+                        .btype(singletonMap("impId1", asList(1, 2)))
+                        .battr(Map.of(
+                                MediaType.BANNER, singletonMap("impId1", asList(1, 2)),
+                                MediaType.VIDEO, singletonMap("impId1", asList(3, 4)),
+                                MediaType.AUDIO, singletonMap("impId1", asList(5, 6))))
+                        .build());
+        final BidRequest request = BidRequest.builder()
+                .imp(singletonList(Imp.builder()
+                        .id("impId1")
+                        .video(Video.builder().build())
+                        .audio(Audio.builder().build())
+                        .build()))
+                .build();
+
+        // when and then
+        assertThat(updater.update(request)).isEqualTo(BidRequest.builder()
+                .badv(asList("domain1.com", "domain2.com"))
+                .bcat(asList("cat1", "cat2"))
+                .bapp(asList("app1", "app2"))
+                .imp(singletonList(Imp.builder()
+                        .id("impId1")
+                        .video(Video.builder().battr(asList(3, 4)).build())
+                        .audio(Audio.builder().battr(asList(5, 6)).build())
+                        .build()))
+                .build());
+    }
+
+    @Test
+    public void shouldNotUpdateMissingVideo() {
+        // given
+        final RequestUpdater updater = RequestUpdater.create(
+                BlockedAttributes.builder()
+                        .badv(asList("domain1.com", "domain2.com"))
+                        .bcat(asList("cat1", "cat2"))
+                        .bapp(asList("app1", "app2"))
+                        .btype(singletonMap("impId1", asList(1, 2)))
+                        .battr(Map.of(
+                                MediaType.BANNER, singletonMap("impId1", asList(1, 2)),
+                                MediaType.VIDEO, singletonMap("impId1", asList(3, 4)),
+                                MediaType.AUDIO, singletonMap("impId1", asList(5, 6))))
+                        .build());
+        final BidRequest request = BidRequest.builder()
+                .imp(singletonList(Imp.builder()
+                        .id("impId1")
+                        .banner(Banner.builder().build())
+                        .audio(Audio.builder().build())
+                        .build()))
+                .build();
+
+        // when and then
+        assertThat(updater.update(request)).isEqualTo(BidRequest.builder()
+                .badv(asList("domain1.com", "domain2.com"))
+                .bcat(asList("cat1", "cat2"))
+                .bapp(asList("app1", "app2"))
+                .imp(singletonList(Imp.builder()
+                        .id("impId1")
+                        .banner(Banner.builder()
+                                .btype(asList(1, 2))
+                                .battr(asList(1, 2))
+                                .build())
+                        .audio(Audio.builder().battr(asList(5, 6)).build())
+                        .build()))
+                .build());
+    }
+
+    @Test
+    public void shouldNotUpdateMissingAudio() {
+        // given
+        final RequestUpdater updater = RequestUpdater.create(
+                BlockedAttributes.builder()
+                        .badv(asList("domain1.com", "domain2.com"))
+                        .bcat(asList("cat1", "cat2"))
+                        .bapp(asList("app1", "app2"))
+                        .btype(singletonMap("impId1", asList(1, 2)))
+                        .battr(Map.of(
+                                MediaType.BANNER, singletonMap("impId1", asList(1, 2)),
+                                MediaType.VIDEO, singletonMap("impId1", asList(3, 4)),
+                                MediaType.AUDIO, singletonMap("impId1", asList(5, 6))))
+                        .build());
+        final BidRequest request = BidRequest.builder()
+                .imp(singletonList(Imp.builder()
+                        .id("impId1")
+                        .banner(Banner.builder().build())
+                        .video(Video.builder().build())
+                        .build()))
+                .build();
+
+        // when and then
+        assertThat(updater.update(request)).isEqualTo(BidRequest.builder()
+                .badv(asList("domain1.com", "domain2.com"))
+                .bcat(asList("cat1", "cat2"))
+                .bapp(asList("app1", "app2"))
+                .imp(singletonList(Imp.builder()
+                        .id("impId1")
+                        .banner(Banner.builder()
+                                .btype(asList(1, 2))
+                                .battr(asList(1, 2))
+                                .build())
+                        .video(Video.builder().battr(asList(3, 4)).build())
+                        .build()))
+                .build());
+    }
 }
diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java
index fe1ea6e9614..b2182a92f6e 100644
--- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java
+++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java
@@ -18,6 +18,8 @@
 import org.prebid.server.auction.versionconverter.OrtbVersion;
 import org.prebid.server.bidder.BidderCatalog;
 import org.prebid.server.bidder.BidderInfo;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ArrayOverride;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides;
@@ -27,8 +29,6 @@
 import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes;
 import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext;
 import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderInvocationContextImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderRequestPayloadImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationResult;
 import org.prebid.server.hooks.v1.InvocationStatus;
@@ -276,7 +276,8 @@ private static BidderInfo bidderInfo(OrtbVersion ortbVersion) {
                 false,
                 false,
                 null,
-                Ortb.of(false));
+                Ortb.of(false),
+                0L);
     }
 
     private static BidRequest emptyRequest() {
diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java
index 0e0a6811835..351bfbb9d33 100644
--- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java
+++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java
@@ -13,6 +13,12 @@
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.BidRejectionTracker;
 import org.prebid.server.bidder.model.BidderBid;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides;
 import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attributes;
@@ -22,12 +28,6 @@
 import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes;
 import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext;
 import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderInvocationContextImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderResponsePayloadImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.InvocationResultImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ActivityImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.AppliedToImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ResultImpl;
-import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.TagsImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationResult;
 import org.prebid.server.hooks.v1.InvocationStatus;
diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java
index 8b68c9279df..d39d0a4ca5f 100644
--- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java
+++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderInvocationContextImpl.java
@@ -7,7 +7,7 @@
 import lombok.experimental.Accessors;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.BidRejectionTracker;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.hooks.v1.bidder.BidderInvocationContext;
 import org.prebid.server.model.Endpoint;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
diff --git a/extra/modules/pb-request-correction/pom.xml b/extra/modules/pb-request-correction/pom.xml
new file mode 100644
index 00000000000..d44752c5730
--- /dev/null
+++ b/extra/modules/pb-request-correction/pom.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.prebid.server.hooks.modules</groupId>
+        <artifactId>all-modules</artifactId>
+        <version>3.19.0-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>pb-request-correction</artifactId>
+
+    <name>pb-request-correction</name>
+    <description>Request correction module</description>
+</project>
diff --git a/extra/modules/pb-request-correction/src/lombok.config b/extra/modules/pb-request-correction/src/lombok.config
new file mode 100644
index 00000000000..efd92714219
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/lombok.config
@@ -0,0 +1 @@
+lombok.anyConstructor.addConstructorProperties = true
diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java
new file mode 100644
index 00000000000..3daa937c37c
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProvider.java
@@ -0,0 +1,25 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core;
+
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer;
+
+import java.util.List;
+import java.util.Objects;
+
+public class RequestCorrectionProvider {
+
+    private final List<CorrectionProducer> correctionProducers;
+
+    public RequestCorrectionProvider(List<CorrectionProducer> correctionProducers) {
+        this.correctionProducers = Objects.requireNonNull(correctionProducers);
+    }
+
+    public List<Correction> corrections(Config config, BidRequest bidRequest) {
+        return correctionProducers.stream()
+                .filter(correctionProducer -> correctionProducer.shouldProduce(config, bidRequest))
+                .map(correctionProducer -> correctionProducer.produce(config))
+                .toList();
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java
new file mode 100644
index 00000000000..44cac23337e
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/config/model/Config.java
@@ -0,0 +1,21 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.config.model;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Value;
+
+@Value
+@Builder
+public class Config {
+
+    boolean enabled;
+
+    @JsonAlias("pbsdkAndroidInstlRemove")
+    @JsonProperty("pbsdk-android-instl-remove")
+    boolean interstitialCorrectionEnabled;
+
+    @JsonAlias("pbsdkUaCleanup")
+    @JsonProperty("pbsdk-ua-cleanup")
+    boolean userAgentCorrectionEnabled;
+}
diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java
new file mode 100644
index 00000000000..2cfda5fce68
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/Correction.java
@@ -0,0 +1,9 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.correction;
+
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config;
+
+public interface Correction {
+
+    BidRequest apply(BidRequest bidRequest);
+}
diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java
new file mode 100644
index 00000000000..a92132656d5
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/CorrectionProducer.java
@@ -0,0 +1,11 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.correction;
+
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config;
+
+public interface CorrectionProducer {
+
+    boolean shouldProduce(Config config, BidRequest bidRequest);
+
+    Correction produce(Config config);
+}
diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java
new file mode 100644
index 00000000000..75d86c511fb
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrection.java
@@ -0,0 +1,24 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction;
+
+public class InterstitialCorrection implements Correction {
+
+    @Override
+    public BidRequest apply(BidRequest bidRequest) {
+        return bidRequest.toBuilder()
+                .imp(bidRequest.getImp().stream()
+                        .map(InterstitialCorrection::removeInterstitial)
+                        .toList())
+                .build();
+    }
+
+    private static Imp removeInterstitial(Imp imp) {
+        final Integer interstitial = imp.getInstl();
+        return interstitial != null && interstitial == 1
+                ? imp.toBuilder().instl(null).build()
+                : imp;
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java
new file mode 100644
index 00000000000..c9bd1995867
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducer.java
@@ -0,0 +1,80 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial;
+
+import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer;
+import org.prebid.server.hooks.modules.pb.request.correction.core.util.VersionUtil;
+import org.prebid.server.proto.openrtb.ext.request.ExtApp;
+import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid;
+
+import java.util.List;
+import java.util.Optional;
+
+public class InterstitialCorrectionProducer implements CorrectionProducer {
+
+    private static final InterstitialCorrection CORRECTION_INSTANCE = new InterstitialCorrection();
+
+    private static final String PREBID_MOBILE = "prebid-mobile";
+    private static final String ANDROID = "android";
+
+    private static final int MAX_VERSION_MAJOR = 2;
+    private static final int MAX_VERSION_MINOR = 2;
+    private static final int MAX_VERSION_PATCH = 3;
+
+    @Override
+    public boolean shouldProduce(Config config, BidRequest bidRequest) {
+        final App app = bidRequest.getApp();
+        return config.isInterstitialCorrectionEnabled()
+                && hasInterstitialToRemove(bidRequest.getImp())
+                && isPrebidMobile(app)
+                && isAndroid(app)
+                && isApplicableVersion(app);
+    }
+
+    private static boolean hasInterstitialToRemove(List<Imp> imps) {
+        for (Imp imp : imps) {
+            final Integer interstitial = imp.getInstl();
+            if (interstitial != null && interstitial == 1) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private static boolean isPrebidMobile(App app) {
+        final String source = Optional.ofNullable(app)
+                .map(App::getExt)
+                .map(ExtApp::getPrebid)
+                .map(ExtAppPrebid::getSource)
+                .orElse(null);
+
+        return StringUtils.equalsIgnoreCase(source, PREBID_MOBILE);
+    }
+
+    private static boolean isAndroid(App app) {
+        return StringUtils.containsIgnoreCase(app.getBundle(), ANDROID);
+    }
+
+    private static boolean isApplicableVersion(App app) {
+        return Optional.ofNullable(app)
+                .map(App::getExt)
+                .map(ExtApp::getPrebid)
+                .map(ExtAppPrebid::getVersion)
+                .map(InterstitialCorrectionProducer::checkVersion)
+                .orElse(false);
+    }
+
+    private static boolean checkVersion(String version) {
+        return VersionUtil.isVersionLessThan(version, MAX_VERSION_MAJOR, MAX_VERSION_MINOR, MAX_VERSION_PATCH);
+    }
+
+    @Override
+    public Correction produce(Config config) {
+        return CORRECTION_INSTANCE;
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java
new file mode 100644
index 00000000000..f1b6b40eacc
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrection.java
@@ -0,0 +1,25 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction;
+
+import java.util.regex.Pattern;
+
+public class UserAgentCorrection implements Correction {
+
+    private static final Pattern USER_AGENT_PATTERN = Pattern.compile("PrebidMobile/[0-9][^ ]*");
+
+    @Override
+    public BidRequest apply(BidRequest bidRequest) {
+        return bidRequest.toBuilder()
+                .device(correctDevice(bidRequest.getDevice()))
+                .build();
+    }
+
+    private static Device correctDevice(Device device) {
+        return device.toBuilder()
+                .ua(USER_AGENT_PATTERN.matcher(device.getUa()).replaceAll(""))
+                .build();
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java
new file mode 100644
index 00000000000..f4c8d4f76dd
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducer.java
@@ -0,0 +1,76 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent;
+
+import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer;
+import org.prebid.server.hooks.modules.pb.request.correction.core.util.VersionUtil;
+import org.prebid.server.proto.openrtb.ext.request.ExtApp;
+import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid;
+
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class UserAgentCorrectionProducer implements CorrectionProducer {
+
+    private static final UserAgentCorrection CORRECTION_INSTANCE = new UserAgentCorrection();
+
+    private static final String PREBID_MOBILE = "prebid-mobile";
+    private static final Pattern USER_AGENT_PATTERN = Pattern.compile(".*PrebidMobile/[0-9]+[^ ]*.*");
+
+
+    private static final int MAX_VERSION_MAJOR = 2;
+    private static final int MAX_VERSION_MINOR = 1;
+    private static final int MAX_VERSION_PATCH = 6;
+
+    @Override
+    public boolean shouldProduce(Config config, BidRequest bidRequest) {
+        final App app = bidRequest.getApp();
+        return config.isUserAgentCorrectionEnabled()
+                && isPrebidMobile(app)
+                && isApplicableVersion(app)
+                && isApplicableDevice(bidRequest.getDevice());
+    }
+
+    private static boolean isPrebidMobile(App app) {
+        final String source = Optional.ofNullable(app)
+                .map(App::getExt)
+                .map(ExtApp::getPrebid)
+                .map(ExtAppPrebid::getSource)
+                .orElse(null);
+
+        return StringUtils.equalsIgnoreCase(source, PREBID_MOBILE);
+    }
+
+    private static boolean isApplicableVersion(App app) {
+        return Optional.ofNullable(app)
+                .map(App::getExt)
+                .map(ExtApp::getPrebid)
+                .map(ExtAppPrebid::getVersion)
+                .map(UserAgentCorrectionProducer::checkVersion)
+                .orElse(false);
+    }
+
+    private static boolean checkVersion(String version) {
+        return VersionUtil.isVersionLessThan(version, MAX_VERSION_MAJOR, MAX_VERSION_MINOR, MAX_VERSION_PATCH);
+    }
+
+    private static boolean isApplicableDevice(Device device) {
+        return Optional.ofNullable(device)
+                .map(Device::getUa)
+                .filter(StringUtils::isNotEmpty)
+                .map(USER_AGENT_PATTERN::matcher)
+                .map(Matcher::matches)
+                .orElse(false);
+    }
+
+
+    @Override
+    public Correction produce(Config config) {
+        return CORRECTION_INSTANCE;
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java
new file mode 100644
index 00000000000..2e84f01183e
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtil.java
@@ -0,0 +1,35 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.util;
+
+public class VersionUtil {
+
+    public static boolean isVersionLessThan(String versionAsString, int major, int minor, int patch) {
+        return compareVersion(versionAsString, major, minor, patch) < 0;
+    }
+
+    private static int compareVersion(String versionAsString, int major, int minor, int patch) {
+        final String[] version = versionAsString.split("\\.");
+
+        final int parsedMajor = getAtAsIntOrDefault(version, 0, -1);
+        final int parsedMinor = getAtAsIntOrDefault(version, 1, 0);
+        final int parsedPatch = getAtAsIntOrDefault(version, 2, 0);
+
+        int diff = parsedMajor >= 0 ? parsedMajor - major : 1;
+        diff = diff == 0 ? parsedMinor - minor : diff;
+        diff = diff == 0 ? parsedPatch - patch : diff;
+
+        return diff;
+    }
+
+    private static int getAtAsIntOrDefault(String[] array, int index, int defaultValue) {
+        return array.length > index ? intOrDefault(array[index], defaultValue) : defaultValue;
+    }
+
+    private static int intOrDefault(String intAsString, int defaultValue) {
+        try {
+            final int parsed = Integer.parseInt(intAsString);
+            return parsed >= 0 ? parsed : defaultValue;
+        } catch (NumberFormatException e) {
+            return defaultValue;
+        }
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java
new file mode 100644
index 00000000000..ecbd725e42d
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/spring/config/RequestCorrectionModuleConfiguration.java
@@ -0,0 +1,38 @@
+package org.prebid.server.hooks.modules.pb.request.correction.spring.config;
+
+import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial.InterstitialCorrectionProducer;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent.UserAgentCorrectionProducer;
+import org.prebid.server.hooks.modules.pb.request.correction.v1.RequestCorrectionModule;
+import org.prebid.server.json.ObjectMapperProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.List;
+
+@Configuration
+@ConditionalOnProperty(prefix = "hooks." + RequestCorrectionModule.CODE, name = "enabled", havingValue = "true")
+public class RequestCorrectionModuleConfiguration {
+
+    @Bean
+    InterstitialCorrectionProducer interstitialCorrectionProducer() {
+        return new InterstitialCorrectionProducer();
+    }
+
+    @Bean
+    UserAgentCorrectionProducer userAgentCorrectionProducer() {
+        return new UserAgentCorrectionProducer();
+    }
+
+    @Bean
+    RequestCorrectionProvider requestCorrectionProvider(List<CorrectionProducer> correctionProducers) {
+        return new RequestCorrectionProvider(correctionProducers);
+    }
+
+    @Bean
+    RequestCorrectionModule requestCorrectionModule(RequestCorrectionProvider requestCorrectionProvider) {
+        return new RequestCorrectionModule(requestCorrectionProvider, ObjectMapperProvider.mapper());
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java
new file mode 100644
index 00000000000..10d20a3b823
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionModule.java
@@ -0,0 +1,32 @@
+package org.prebid.server.hooks.modules.pb.request.correction.v1;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider;
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.InvocationContext;
+import org.prebid.server.hooks.v1.Module;
+
+import java.util.Collection;
+import java.util.Collections;
+
+public class RequestCorrectionModule implements Module {
+
+    public static final String CODE = "pb-request-correction";
+
+    private final Collection<? extends Hook<?, ? extends InvocationContext>> hooks;
+
+    public RequestCorrectionModule(RequestCorrectionProvider requestCorrectionProvider, ObjectMapper mapper) {
+        this.hooks = Collections.singleton(
+                new RequestCorrectionProcessedAuctionHook(requestCorrectionProvider, mapper));
+    }
+
+    @Override
+    public String code() {
+        return CODE;
+    }
+
+    @Override
+    public Collection<? extends Hook<?, ? extends InvocationContext>> hooks() {
+        return hooks;
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java
new file mode 100644
index 00000000000..b9142c93d26
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java
@@ -0,0 +1,104 @@
+package org.prebid.server.hooks.modules.pb.request.correction.v1;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import io.vertx.core.Future;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
+import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider;
+import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook;
+
+import java.util.List;
+import java.util.Objects;
+
+public class RequestCorrectionProcessedAuctionHook implements ProcessedAuctionRequestHook {
+
+    private static final String CODE = "pb-request-correction-processed-auction-request";
+
+    private final RequestCorrectionProvider requestCorrectionProvider;
+    private final ObjectMapper mapper;
+
+    public RequestCorrectionProcessedAuctionHook(RequestCorrectionProvider requestCorrectionProvider, ObjectMapper mapper) {
+        this.requestCorrectionProvider = Objects.requireNonNull(requestCorrectionProvider);
+        this.mapper = Objects.requireNonNull(mapper);
+    }
+
+    @Override
+    public Future<InvocationResult<AuctionRequestPayload>> call(AuctionRequestPayload payload,
+                                                                AuctionInvocationContext context) {
+
+        final Config config;
+        try {
+            config = moduleConfig(context.accountConfig());
+        } catch (PreBidException e) {
+            return failure(e.getMessage());
+        }
+
+        if (config == null || !config.isEnabled()) {
+            return noAction();
+        }
+
+        final BidRequest bidRequest = payload.bidRequest();
+
+        final List<Correction> corrections = requestCorrectionProvider.corrections(config, bidRequest);
+        if (corrections.isEmpty()) {
+            return noAction();
+        }
+
+        final InvocationResult<AuctionRequestPayload> invocationResult =
+                InvocationResultImpl.<AuctionRequestPayload>builder()
+                        .status(InvocationStatus.success)
+                        .action(InvocationAction.update)
+                        .payloadUpdate(initialPayload -> AuctionRequestPayloadImpl.of(
+                                applyCorrections(initialPayload.bidRequest(), corrections)))
+                        .build();
+
+        return Future.succeededFuture(invocationResult);
+    }
+
+    private Config moduleConfig(ObjectNode accountConfig) {
+        try {
+            return mapper.treeToValue(accountConfig, Config.class);
+        } catch (JsonProcessingException e) {
+            throw new PreBidException(e.getMessage());
+        }
+    }
+
+    private static BidRequest applyCorrections(BidRequest bidRequest, List<Correction> corrections) {
+        BidRequest result = bidRequest;
+        for (Correction correction : corrections) {
+            result = correction.apply(result);
+        }
+        return result;
+    }
+
+    private Future<InvocationResult<AuctionRequestPayload>> failure(String message) {
+        return Future.succeededFuture(InvocationResultImpl.<AuctionRequestPayload>builder()
+                .status(InvocationStatus.failure)
+                .message(message)
+                .action(InvocationAction.no_action)
+                .build());
+    }
+
+    private static Future<InvocationResult<AuctionRequestPayload>> noAction() {
+        return Future.succeededFuture(InvocationResultImpl.<AuctionRequestPayload>builder()
+                .status(InvocationStatus.success)
+                .action(InvocationAction.no_action)
+                .build());
+    }
+
+    @Override
+    public String code() {
+        return CODE;
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java
new file mode 100644
index 00000000000..56856d10c16
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/RequestCorrectionProviderTest.java
@@ -0,0 +1,58 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.CorrectionProducer;
+
+import java.util.List;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+@ExtendWith(MockitoExtension.class)
+public class RequestCorrectionProviderTest {
+
+    @Mock
+    private CorrectionProducer correctionProducer;
+
+    private RequestCorrectionProvider target;
+
+    @BeforeEach
+    public void setUp() {
+        target = new RequestCorrectionProvider(singletonList(correctionProducer));
+    }
+
+    @Test
+    public void correctionsShouldReturnEmptyListIfAllCorrectionsDisabled() {
+        // given
+        given(correctionProducer.shouldProduce(any(), any())).willReturn(false);
+
+        // when
+        final List<Correction> corrections = target.corrections(null, null);
+
+        // then
+        assertThat(corrections).isEmpty();
+    }
+
+    @Test
+    public void correctionsShouldReturnProducedCorrection() {
+        // given
+        given(correctionProducer.shouldProduce(any(), any())).willReturn(true);
+
+        final Correction correction = mock(Correction.class);
+        given(correctionProducer.produce(any())).willReturn(correction);
+
+        // when
+        final List<Correction> corrections = target.corrections(null, null);
+
+        // then
+        assertThat(corrections).containsExactly(correction);
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java
new file mode 100644
index 00000000000..3a44b7158e3
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionProducerTest.java
@@ -0,0 +1,132 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial;
+
+import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config;
+import org.prebid.server.proto.openrtb.ext.request.ExtApp;
+import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class InterstitialCorrectionProducerTest {
+
+    private final InterstitialCorrectionProducer target = new InterstitialCorrectionProducer();
+
+    @Test
+    public void shouldProduceReturnsFalseIfCorrectionDisabled() {
+        // given
+        final Config config = Config.builder()
+                .interstitialCorrectionEnabled(false)
+                .build();
+        final BidRequest bidRequest = BidRequest.builder().build();
+
+        // when
+        final boolean result = target.shouldProduce(config, bidRequest);
+
+        // then
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void shouldProduceReturnsFalseIfThereIsNothingToDo() {
+        // given
+        final Config config = Config.builder()
+                .interstitialCorrectionEnabled(true)
+                .build();
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(emptyList())
+                .app(App.builder().build())
+                .build();
+
+        // when
+        final boolean result = target.shouldProduce(config, bidRequest);
+
+        // then
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void shouldProduceReturnsFalseIfSourceIsNotPrebidMobile() {
+        // given
+        final Config config = Config.builder()
+                .interstitialCorrectionEnabled(true)
+                .build();
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(Imp.builder().instl(1).build()))
+                .app(App.builder().ext(ExtApp.of(ExtAppPrebid.of("source", null), null)).build())
+                .build();
+
+        // when
+        final boolean result = target.shouldProduce(config, bidRequest);
+
+        // then
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void shouldProduceReturnsFalseIfBundleNotAnAndroid() {
+        // given
+        final Config config = Config.builder()
+                .interstitialCorrectionEnabled(true)
+                .build();
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(Imp.builder().instl(1).build()))
+                .app(App.builder()
+                        .bundle("bundle")
+                        .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", null), null))
+                        .build())
+                .build();
+
+        // when
+        final boolean result = target.shouldProduce(config, bidRequest);
+
+        // then
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void shouldProduceReturnsFalseIfVersionInvalid() {
+        // given
+        final Config config = Config.builder()
+                .interstitialCorrectionEnabled(true)
+                .build();
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(Imp.builder().instl(1).build()))
+                .app(App.builder()
+                        .bundle("bundleAndroid")
+                        .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1a.2.3"), null))
+                        .build())
+                .build();
+
+        // when
+        final boolean result = target.shouldProduce(config, bidRequest);
+
+        // then
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void shouldProduceReturnsTrueWhenAllConditionsMatch() {
+        // given
+        final Config config = Config.builder()
+                .interstitialCorrectionEnabled(true)
+                .build();
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(Imp.builder().instl(1).build()))
+                .app(App.builder()
+                        .bundle("bundleAndroid")
+                        .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null))
+                        .build())
+                .build();
+
+        // when
+        final boolean result = target.shouldProduce(config, bidRequest);
+
+        // then
+        assertThat(result).isTrue();
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java
new file mode 100644
index 00000000000..490607a7d5e
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/interstitial/InterstitialCorrectionTest.java
@@ -0,0 +1,35 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.correction.interstitial;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class InterstitialCorrectionTest {
+
+    private final InterstitialCorrection target = new InterstitialCorrection();
+
+    @Test
+    public void applyShouldCorrectInterstitial() {
+        // given
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(asList(
+                        Imp.builder().instl(0).build(),
+                        Imp.builder().build(),
+                        Imp.builder().instl(1).build()))
+                .build();
+
+        // when
+        final BidRequest result = target.apply(bidRequest);
+
+        // then
+        assertThat(result)
+                .extracting(BidRequest::getImp)
+                .asInstanceOf(InstanceOfAssertFactories.list(Imp.class))
+                .extracting(Imp::getInstl)
+                .containsExactly(0, null, null);
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java
new file mode 100644
index 00000000000..cb7e3458bef
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionProducerTest.java
@@ -0,0 +1,125 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent;
+
+import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Imp;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config;
+import org.prebid.server.proto.openrtb.ext.request.ExtApp;
+import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class UserAgentCorrectionProducerTest {
+
+    private final UserAgentCorrectionProducer target = new UserAgentCorrectionProducer();
+
+    @Test
+    public void shouldProduceReturnsFalseIfCorrectionDisabled() {
+        // given
+        final Config config = Config.builder()
+                .userAgentCorrectionEnabled(false)
+                .build();
+        final BidRequest bidRequest = BidRequest.builder().build();
+
+        // when
+        final boolean result = target.shouldProduce(config, bidRequest);
+
+        // then
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void shouldProduceReturnsFalseIfThereIsNothingToDo() {
+        // given
+        final Config config = Config.builder()
+                .userAgentCorrectionEnabled(true)
+                .build();
+        final BidRequest bidRequest = BidRequest.builder().build();
+
+        // when
+        final boolean result = target.shouldProduce(config, bidRequest);
+
+        // then
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void shouldProduceReturnsFalseIfSourceIsNotPrebidMobile() {
+        // given
+        final Config config = Config.builder()
+                .userAgentCorrectionEnabled(true)
+                .build();
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(Imp.builder().instl(1).build()))
+                .app(App.builder().ext(ExtApp.of(ExtAppPrebid.of("source", null), null)).build())
+                .build();
+
+        // when
+        final boolean result = target.shouldProduce(config, bidRequest);
+
+        // then
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void shouldProduceReturnsFalseIfVersionInvalid() {
+        // given
+        final Config config = Config.builder()
+                .userAgentCorrectionEnabled(true)
+                .build();
+        final BidRequest bidRequest = BidRequest.builder()
+                .app(App.builder()
+                        .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1a.2.3"), null))
+                        .build())
+                .build();
+
+        // when
+        final boolean result = target.shouldProduce(config, bidRequest);
+
+        // then
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void shouldProduceReturnsFalseIfDeviceUserAgentDoesNotMatch() {
+        // given
+        final Config config = Config.builder()
+                .userAgentCorrectionEnabled(true)
+                .build();
+        final BidRequest bidRequest = BidRequest.builder()
+                .device(Device.builder().ua("Blah blah").build())
+                .app(App.builder()
+                        .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null))
+                        .build())
+                .build();
+
+        // when
+        final boolean result = target.shouldProduce(config, bidRequest);
+
+        // then
+        assertThat(result).isFalse();
+    }
+
+    @Test
+    public void shouldProduceReturnsTrueWhenAllConditionsMatch() {
+        // given
+        final Config config = Config.builder()
+                .userAgentCorrectionEnabled(true)
+                .build();
+        final BidRequest bidRequest = BidRequest.builder()
+                .device(Device.builder().ua("Blah PrebidMobile/1asdf blah").build())
+                .app(App.builder()
+                        .ext(ExtApp.of(ExtAppPrebid.of("prebid-mobile", "1.2.3"), null))
+                        .build())
+                .build();
+
+        // when
+        final boolean result = target.shouldProduce(config, bidRequest);
+
+        // then
+        assertThat(result).isTrue();
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java
new file mode 100644
index 00000000000..c8ed5f6762d
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/correction/useragent/UserAgentCorrectionTest.java
@@ -0,0 +1,29 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.correction.useragent;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class UserAgentCorrectionTest {
+
+    private final UserAgentCorrection target = new UserAgentCorrection();
+
+    @Test
+    public void applyShouldCorrectUserAgent() {
+        // given
+        final BidRequest bidRequest = BidRequest.builder()
+                .device(Device.builder().ua("blah PrebidMobile/1asdf blah").build())
+                .build();
+
+        // when
+        final BidRequest result = target.apply(bidRequest);
+
+        // then
+        assertThat(result)
+                .extracting(BidRequest::getDevice)
+                .extracting(Device::getUa)
+                .isEqualTo("blah  blah");
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java
new file mode 100644
index 00000000000..8da1ec6a3c3
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/core/util/VersionUtilTest.java
@@ -0,0 +1,52 @@
+package org.prebid.server.hooks.modules.pb.request.correction.core.util;
+
+import org.junit.jupiter.api.Test;
+
+import static java.lang.Integer.MAX_VALUE;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class VersionUtilTest {
+
+    @Test
+    public void isVersionLessThanShouldReturnFalseIfVersionGreaterThanRequired() {
+        // when and then
+        assertThat(VersionUtil.isVersionLessThan("2.4.3", 2, 2, 3)).isFalse();
+    }
+
+    @Test
+    public void isVersionLessThenShouldReturnFalseIfVersionIsEqualToRequired() {
+        // when and then
+        assertThat(VersionUtil.isVersionLessThan("2.4.3", 2, 4, 3)).isFalse();
+    }
+
+    @Test
+    public void isVersionLessThenShouldReturnTrueIfVersionIsLessThanRequired() {
+        // when and then
+        assertThat(VersionUtil.isVersionLessThan("2.2.3", 2, 4, 3)).isTrue();
+    }
+
+    @Test
+    public void isVersionLessThenShouldReturnExpectedResults() {
+        // major
+        assertThat(VersionUtil.isVersionLessThan("0", 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("1", 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("2", 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("3", 2, 2, 3)).isFalse();
+
+        // minor
+        assertThat(VersionUtil.isVersionLessThan("0." + MAX_VALUE, 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("1." + MAX_VALUE, 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("2.0", 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("2.1", 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("2.2", 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("2.3", 2, 2, 3)).isFalse();
+
+        // patch
+        assertThat(VersionUtil.isVersionLessThan("0.%d.%d".formatted(MAX_VALUE, MAX_VALUE), 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("1.%d.%d".formatted(MAX_VALUE, MAX_VALUE), 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("2.1." + MAX_VALUE, 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("2.2.1", 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("2.2.2", 2, 2, 3)).isTrue();
+        assertThat(VersionUtil.isVersionLessThan("2.2.3", 2, 2, 3)).isFalse();
+    }
+}
diff --git a/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java
new file mode 100644
index 00000000000..9250e188cce
--- /dev/null
+++ b/extra/modules/pb-request-correction/src/test/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHookTest.java
@@ -0,0 +1,120 @@
+package org.prebid.server.hooks.modules.pb.request.correction.v1;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.vertx.core.Future;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider;
+import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config;
+import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+import org.prebid.server.json.ObjectMapperProvider;
+
+import java.util.Map;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+@ExtendWith(MockitoExtension.class)
+public class RequestCorrectionProcessedAuctionHookTest {
+
+    private static final ObjectMapper MAPPER = ObjectMapperProvider.mapper();
+
+    @Mock
+    private RequestCorrectionProvider requestCorrectionProvider;
+
+    private RequestCorrectionProcessedAuctionHook target;
+
+    @Mock
+    private AuctionRequestPayload payload;
+
+    @Mock
+    private AuctionInvocationContext invocationContext;
+
+    @BeforeEach
+    public void setUp() {
+        given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.builder()
+                .enabled(true)
+                .interstitialCorrectionEnabled(true)
+                .build()));
+
+        target = new RequestCorrectionProcessedAuctionHook(requestCorrectionProvider, MAPPER);
+    }
+
+    @Test
+    public void callShouldReturnFailedResultOnInvalidConfiguration() {
+        // given
+        given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Map.of("enabled", emptyList())));
+
+        // when
+        final Future<InvocationResult<AuctionRequestPayload>> result = target.call(payload, invocationContext);
+
+        //then
+        assertThat(result.result()).satisfies(invocationResult -> {
+            assertThat(invocationResult.status()).isEqualTo(InvocationStatus.failure);
+            assertThat(invocationResult.message()).startsWith("Cannot deserialize value of type");
+            assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action);
+        });
+    }
+
+    @Test
+    public void callShouldReturnNoActionOnDisabledConfig() {
+        // given
+        given(invocationContext.accountConfig()).willReturn(MAPPER.valueToTree(Config.builder()
+                .enabled(false)
+                .interstitialCorrectionEnabled(true)
+                .build()));
+
+        // when
+        final Future<InvocationResult<AuctionRequestPayload>> result = target.call(payload, invocationContext);
+
+        //then
+        assertThat(result.result()).satisfies(invocationResult -> {
+            assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success);
+            assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action);
+        });
+    }
+
+    @Test
+    public void callShouldReturnNoActionIfThereIsNoApplicableCorrections() {
+        // given
+        given(requestCorrectionProvider.corrections(any(), any())).willReturn(emptyList());
+
+        // when
+        final Future<InvocationResult<AuctionRequestPayload>> result = target.call(payload, invocationContext);
+
+        //then
+        assertThat(result.result()).satisfies(invocationResult -> {
+            assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success);
+            assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_action);
+        });
+    }
+
+    @Test
+    public void callShouldReturnUpdate() {
+        // given
+        final Correction correction = mock(Correction.class);
+        given(requestCorrectionProvider.corrections(any(), any())).willReturn(singletonList(correction));
+
+        // when
+        final Future<InvocationResult<AuctionRequestPayload>> result = target.call(payload, invocationContext);
+
+        //then
+        assertThat(result.result()).satisfies(invocationResult -> {
+            assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success);
+            assertThat(invocationResult.action()).isEqualTo(InvocationAction.update);
+            assertThat(invocationResult.payloadUpdate()).isNotNull();
+        });
+    }
+}
diff --git a/extra/modules/pb-response-correction/pom.xml b/extra/modules/pb-response-correction/pom.xml
index 802bed7fe04..7be380ae1e1 100644
--- a/extra/modules/pb-response-correction/pom.xml
+++ b/extra/modules/pb-response-correction/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.prebid.server.hooks.modules</groupId>
         <artifactId>all-modules</artifactId>
-        <version>3.15.0-SNAPSHOT</version>
+        <version>3.19.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>pb-response-correction</artifactId>
diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java
index 09e4640b064..9f9e8e75659 100644
--- a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java
+++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java
@@ -11,9 +11,9 @@
 import org.prebid.server.hooks.modules.pb.response.correction.core.ResponseCorrectionProvider;
 import org.prebid.server.hooks.modules.pb.response.correction.core.config.model.Config;
 import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction;
-import org.prebid.server.hooks.modules.pb.response.correction.v1.model.InvocationResultImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
 import org.prebid.server.hooks.v1.InvocationStatus;
 import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
 import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesHook;
@@ -57,7 +57,7 @@ public Future<InvocationResult<AllProcessedBidResponsesPayload>> call(AllProcess
             return noAction();
         }
 
-        final InvocationResult<AllProcessedBidResponsesPayload> invocationResult = InvocationResultImpl.builder()
+        final InvocationResult<AllProcessedBidResponsesPayload> invocationResult = InvocationResultImpl.<AllProcessedBidResponsesPayload>builder()
                 .status(InvocationStatus.success)
                 .action(InvocationAction.update)
                 .payloadUpdate(initialPayload -> AllProcessedBidResponsesPayloadImpl.of(
@@ -84,7 +84,7 @@ private static List<BidderResponse> applyCorrections(List<BidderResponse> bidder
     }
 
     private Future<InvocationResult<AllProcessedBidResponsesPayload>> failure(String message) {
-        return Future.succeededFuture(InvocationResultImpl.builder()
+        return Future.succeededFuture(InvocationResultImpl.<AllProcessedBidResponsesPayload>builder()
                 .status(InvocationStatus.failure)
                 .message(message)
                 .action(InvocationAction.no_action)
@@ -92,7 +92,7 @@ private Future<InvocationResult<AllProcessedBidResponsesPayload>> failure(String
     }
 
     private static Future<InvocationResult<AllProcessedBidResponsesPayload>> noAction() {
-        return Future.succeededFuture(InvocationResultImpl.builder()
+        return Future.succeededFuture(InvocationResultImpl.<AllProcessedBidResponsesPayload>builder()
                 .status(InvocationStatus.success)
                 .action(InvocationAction.no_action)
                 .build());
diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java
deleted file mode 100644
index 1a39413583c..00000000000
--- a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/model/InvocationResultImpl.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package org.prebid.server.hooks.modules.pb.response.correction.v1.model;
-
-import lombok.Builder;
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.v1.InvocationAction;
-import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationStatus;
-import org.prebid.server.hooks.v1.PayloadUpdate;
-import org.prebid.server.hooks.v1.analytics.Tags;
-import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
-import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Builder
-@Value
-public class InvocationResultImpl implements InvocationResult<AllProcessedBidResponsesPayload> {
-
-    InvocationStatus status;
-
-    String message;
-
-    InvocationAction action;
-
-    PayloadUpdate<AllProcessedBidResponsesPayload> payloadUpdate;
-
-    List<String> errors;
-
-    List<String> warnings;
-
-    List<String> debugMessages;
-
-    Object moduleContext;
-
-    Tags analyticsTags;
-}
diff --git a/extra/modules/pb-richmedia-filter/pom.xml b/extra/modules/pb-richmedia-filter/pom.xml
index fc852b520f2..304b2f06147 100644
--- a/extra/modules/pb-richmedia-filter/pom.xml
+++ b/extra/modules/pb-richmedia-filter/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.prebid.server.hooks.modules</groupId>
         <artifactId>all-modules</artifactId>
-        <version>3.15.0-SNAPSHOT</version>
+        <version>3.19.0-SNAPSHOT</version>
     </parent>
 
     <artifactId>pb-richmedia-filter</artifactId>
diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/config/PbRichmediaFilterModuleConfiguration.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/config/PbRichmediaFilterModuleConfiguration.java
index bce3668c0a5..fe3a8a37ac6 100644
--- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/config/PbRichmediaFilterModuleConfiguration.java
+++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/config/PbRichmediaFilterModuleConfiguration.java
@@ -20,8 +20,8 @@ public class PbRichmediaFilterModuleConfiguration {
 
     @Bean
     PbRichmediaFilterModule pbRichmediaFilterModule(
-            @Value("${hooks.modules.pb-richmedia-filter.filter-mraid}") Boolean filterMraid,
-            @Value("${hooks.modules.pb-richmedia-filter.mraid-script-pattern}") String mraidScriptPattern) {
+            @Value("${hooks.modules.pb-richmedia-filter.filter-mraid:false}") boolean filterMraid,
+            @Value("${hooks.modules.pb-richmedia-filter.mraid-script-pattern:#{null}}") String mraidScriptPattern) {
 
         final ObjectMapper mapper = ObjectMapperProvider.mapper();
         final PbRichMediaFilterProperties globalProperties = PbRichMediaFilterProperties.of(
diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java
index e528ce69c4e..499de57b1d8 100644
--- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java
+++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilter.java
@@ -57,7 +57,7 @@ public MraidFilterResult filterByPattern(String mraidScriptPattern,
                 analyticsResults.add(analyticsResult);
 
                 bidRejectionTrackers.get(bidder)
-                        .reject(rejectedImps, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
+                        .rejectBids(invalidBids, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
 
                 final List<BidderError> errors = new ArrayList<>(seatBid.getErrors());
                 errors.add(BidderError.of("Invalid bid", BidderError.Type.invalid_bid, new HashSet<>(rejectedImps)));
diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java
index ee92a5e2064..c63baeda58c 100644
--- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java
+++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java
@@ -6,17 +6,17 @@
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.BooleanUtils;
 import org.prebid.server.auction.model.BidderResponse;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
 import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl;
 import org.prebid.server.hooks.modules.pb.richmedia.filter.core.BidResponsesMraidFilter;
 import org.prebid.server.hooks.modules.pb.richmedia.filter.core.ModuleConfigResolver;
 import org.prebid.server.hooks.modules.pb.richmedia.filter.model.AnalyticsResult;
 import org.prebid.server.hooks.modules.pb.richmedia.filter.model.MraidFilterResult;
 import org.prebid.server.hooks.modules.pb.richmedia.filter.model.PbRichMediaFilterProperties;
-import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.InvocationResultImpl;
-import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ActivityImpl;
-import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.AppliedToImpl;
-import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ResultImpl;
-import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.TagsImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationResult;
 import org.prebid.server.hooks.v1.InvocationStatus;
diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java
deleted file mode 100644
index bb9e887ca02..00000000000
--- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics;
-
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.v1.analytics.Activity;
-import org.prebid.server.hooks.v1.analytics.Result;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Value(staticConstructor = "of")
-public class ActivityImpl implements Activity {
-
-    String name;
-
-    String status;
-
-    List<Result> results;
-}
diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java
deleted file mode 100644
index 24f793287b5..00000000000
--- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics;
-
-import lombok.Builder;
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.v1.analytics.AppliedTo;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Value
-@Builder
-public class AppliedToImpl implements AppliedTo {
-
-    List<String> impIds;
-
-    List<String> bidders;
-
-    boolean request;
-
-    boolean response;
-
-    List<String> bidIds;
-}
diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java
deleted file mode 100644
index e15359f5c14..00000000000
--- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics;
-
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.v1.analytics.AppliedTo;
-import org.prebid.server.hooks.v1.analytics.Result;
-
-@Accessors(fluent = true)
-@Value(staticConstructor = "of")
-public class ResultImpl implements Result {
-
-    String status;
-
-    ObjectNode values;
-
-    AppliedTo appliedTo;
-}
diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java
deleted file mode 100644
index b996bcb4355..00000000000
--- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics;
-
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.v1.analytics.Activity;
-import org.prebid.server.hooks.v1.analytics.Tags;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Value(staticConstructor = "of")
-public class TagsImpl implements Tags {
-
-    List<Activity> activities;
-}
diff --git a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java
index f2066474df9..0198210d1ca 100644
--- a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java
+++ b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/core/BidResponsesMraidFilterTest.java
@@ -50,15 +50,14 @@ public void filterShouldReturnOriginalBidsWhenNoBidsHaveMraidScriptInAdm() {
     @Test
     public void filterShouldReturnFilteredBidsWhenBidsWithMraidScriptIsFilteredOut() {
         // given
-        final BidderResponse responseA = givenBidderResponse("bidderA", List.of(
-                givenBid("imp_id1", "adm1"),
-                givenBid("imp_id2", "adm2")));
-        final BidderResponse responseB = givenBidderResponse("bidderB", List.of(
-                givenBid("imp_id1", "adm1"),
-                givenBid("imp_id2", "adm2_mraid.js")));
-        final BidderResponse responseC = givenBidderResponse("bidderC", List.of(
-                givenBid("imp_id1", "adm1_mraid.js"),
-                givenBid("imp_id2", "adm2_mraid.js")));
+        final BidderBid givenBid1 = givenBid("imp_id1", "adm1");
+        final BidderBid givenBid2 = givenBid("imp_id2", "adm2");
+        final BidderBid givenInvalidBid1 = givenBid("imp_id1", "adm1_mraid.js");
+        final BidderBid givenInvalidBid2 = givenBid("imp_id2", "adm2_mraid.js");
+
+        final BidderResponse responseA = givenBidderResponse("bidderA", List.of(givenBid1, givenBid2));
+        final BidderResponse responseB = givenBidderResponse("bidderB", List.of(givenBid1, givenInvalidBid2));
+        final BidderResponse responseC = givenBidderResponse("bidderC", List.of(givenInvalidBid1, givenInvalidBid2));
 
         final BidRejectionTracker bidRejectionTrackerA = mock(BidRejectionTracker.class);
         final BidRejectionTracker bidRejectionTrackerB = mock(BidRejectionTracker.class);
@@ -77,10 +76,10 @@ public void filterShouldReturnFilteredBidsWhenBidsWithMraidScriptIsFilteredOut()
         // then
         final BidderResponse expectedResponseA = givenBidderResponse(
                 "bidderA",
-                List.of(givenBid("imp_id1", "adm1"), givenBid("imp_id2", "adm2")));
+                List.of(givenBid1, givenBid2));
         final BidderResponse expectedResponseB = givenBidderResponse(
                 "bidderB",
-                List.of(givenBid("imp_id1", "adm1")),
+                List.of(givenBid1),
                 List.of(givenError("imp_id2")));
         final BidderResponse expectedResponseC = givenBidderResponse(
                 "bidderC",
@@ -106,9 +105,9 @@ public void filterShouldReturnFilteredBidsWhenBidsWithMraidScriptIsFilteredOut()
 
         verifyNoInteractions(bidRejectionTrackerA);
         verify(bidRejectionTrackerB)
-                .reject(List.of("imp_id2"), BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
+                .rejectBids(List.of(givenInvalidBid2), BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
         verify(bidRejectionTrackerC)
-                .reject(List.of("imp_id1", "imp_id2"), BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
+                .rejectBids(List.of(givenInvalidBid1, givenInvalidBid2), BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE);
         verifyNoMoreInteractions(bidRejectionTrackerB, bidRejectionTrackerC);
     }
 
diff --git a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java
index 47d5ab27253..2a87faec776 100644
--- a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java
+++ b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java
@@ -11,16 +11,16 @@
 import org.prebid.server.auction.model.BidRejectionTracker;
 import org.prebid.server.auction.model.BidderResponse;
 import org.prebid.server.bidder.model.BidderSeatBid;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
 import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl;
 import org.prebid.server.hooks.modules.pb.richmedia.filter.core.BidResponsesMraidFilter;
 import org.prebid.server.hooks.modules.pb.richmedia.filter.core.ModuleConfigResolver;
 import org.prebid.server.hooks.modules.pb.richmedia.filter.model.AnalyticsResult;
 import org.prebid.server.hooks.modules.pb.richmedia.filter.model.MraidFilterResult;
 import org.prebid.server.hooks.modules.pb.richmedia.filter.model.PbRichMediaFilterProperties;
-import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ActivityImpl;
-import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.AppliedToImpl;
-import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ResultImpl;
-import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.TagsImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationResult;
 import org.prebid.server.hooks.v1.InvocationStatus;
diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml
index 700902c8834..d6bdcf20c34 100644
--- a/extra/modules/pom.xml
+++ b/extra/modules/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.prebid</groupId>
         <artifactId>prebid-server-aggregator</artifactId>
-        <version>3.15.0-SNAPSHOT</version>
+        <version>3.19.0-SNAPSHOT</version>
         <relativePath>../../extra/pom.xml</relativePath>
     </parent>
 
@@ -22,6 +22,8 @@
         <module>pb-richmedia-filter</module>
         <module>fiftyone-devicedetection</module>
         <module>pb-response-correction</module>
+        <module>greenbids-real-time-data</module>
+        <module>pb-request-correction</module>
     </modules>
 
     <dependencyManagement>
diff --git a/extra/pom.xml b/extra/pom.xml
index 6fe748f904a..c45f2f75feb 100644
--- a/extra/pom.xml
+++ b/extra/pom.xml
@@ -4,7 +4,7 @@
 
     <groupId>org.prebid</groupId>
     <artifactId>prebid-server-aggregator</artifactId>
-    <version>3.15.0-SNAPSHOT</version>
+    <version>3.19.0-SNAPSHOT</version>
     <packaging>pom</packaging>
 
     <scm>
diff --git a/pom.xml b/pom.xml
index e674293243e..7cc9695605a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
     <parent>
         <groupId>org.prebid</groupId>
         <artifactId>prebid-server-aggregator</artifactId>
-        <version>3.15.0-SNAPSHOT</version>
+        <version>3.19.0-SNAPSHOT</version>
         <relativePath>extra/pom.xml</relativePath>
     </parent>
 
diff --git a/sample/prebid-config-with-51d-dd.yaml b/sample/configs/prebid-config-with-51d-dd.yaml
similarity index 93%
rename from sample/prebid-config-with-51d-dd.yaml
rename to sample/configs/prebid-config-with-51d-dd.yaml
index f32674538a3..d523bdbdf11 100644
--- a/sample/prebid-config-with-51d-dd.yaml
+++ b/sample/configs/prebid-config-with-51d-dd.yaml
@@ -21,10 +21,10 @@ settings:
   enforce-valid-account: false
   generate-storedrequest-bidrequest-id: true
   filesystem:
-    settings-filename: sample/sample-app-settings.yaml
-    stored-requests-dir: sample/stored
-    stored-imps-dir: sample/stored
-    stored-responses-dir: sample/stored
+    settings-filename: sample/configs/sample-app-settings.yaml
+    stored-requests-dir: sample
+    stored-imps-dir: sample
+    stored-responses-dir: sample
     categories-dir:
 gdpr:
   default-value: 1
diff --git a/sample/configs/prebid-config-with-module.yaml b/sample/configs/prebid-config-with-module.yaml
index 93a6a13d29f..06fc01fe3e2 100644
--- a/sample/configs/prebid-config-with-module.yaml
+++ b/sample/configs/prebid-config-with-module.yaml
@@ -21,10 +21,10 @@ settings:
   enforce-valid-account: false
   generate-storedrequest-bidrequest-id: true
   filesystem:
-    settings-filename: sample/sample-app-settings.yaml
-    stored-requests-dir: sample/stored
-    stored-imps-dir: sample/stored
-    stored-responses-dir: sample/stored
+    settings-filename: sample/configs/sample-app-settings.yaml
+    stored-requests-dir: sample
+    stored-imps-dir: sample
+    stored-responses-dir: sample
     categories-dir:
 gdpr:
   default-value: 1
diff --git a/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java
index 93665840a21..ed99c241ee5 100644
--- a/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java
+++ b/src/main/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporter.java
@@ -16,6 +16,7 @@
 import io.vertx.core.Vertx;
 import io.vertx.core.http.HttpHeaders;
 import io.vertx.core.http.HttpMethod;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.prebid.server.analytics.AnalyticsReporter;
 import org.prebid.server.analytics.model.AmpEvent;
@@ -115,6 +116,11 @@ public <T> Future<Void> processEvent(T event) {
         }
 
         final AuctionContext auctionContext = contextAndType.getLeft();
+        final String eventType = contextAndType.getRight();
+        if (auctionContext == null) {
+            return Future.succeededFuture();
+        }
+
         final BidRequest bidRequest = auctionContext.getBidRequest();
         final TimeoutContext timeoutContext = auctionContext.getTimeoutContext();
         final PrivacyContext privacyContext = auctionContext.getPrivacyContext();
@@ -133,7 +139,7 @@ public <T> Future<Void> processEvent(T event) {
         }
 
         final AgmaEvent agmaEvent = AgmaEvent.builder()
-                .eventType(contextAndType.getRight())
+                .eventType(eventType)
                 .accountCode(accountCode)
                 .requestId(bidRequest.getId())
                 .app(bidRequest.getApp())
@@ -146,11 +152,7 @@ public <T> Future<Void> processEvent(T event) {
 
         final String eventString = jacksonMapper.encodeToString(agmaEvent);
         buffer.put(eventString, eventString.length());
-        final List<String> toFlush = buffer.pollToFlush();
-        if (!toFlush.isEmpty()) {
-            sendEvents(toFlush);
-        }
-
+        sendEvents(buffer.pollToFlush());
         return Future.succeededFuture();
     }
 
@@ -200,10 +202,15 @@ private static String getPublisherId(BidRequest bidRequest) {
             return null;
         }
 
-        return publisherId;
+        return StringUtils.isNotBlank(appSiteId)
+                ? String.format("%s_%s", StringUtils.defaultString(publisherId), appSiteId)
+                : publisherId;
     }
 
     private void sendEvents(List<String> events) {
+        if (events.isEmpty()) {
+            return;
+        }
         final String payload = preparePayload(events);
         final Future<HttpClientResponse> responseFuture = compressToGzip
                 ? httpClient.request(HttpMethod.POST, url, headers, gzip(payload), httpTimeoutMs)
diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java
index 5b87a0d4112..65de3f02ebc 100644
--- a/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java
+++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporter.java
@@ -21,6 +21,7 @@
 import org.prebid.server.analytics.model.AmpEvent;
 import org.prebid.server.analytics.model.AuctionEvent;
 import org.prebid.server.analytics.reporter.greenbids.model.CommonMessage;
+import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult;
 import org.prebid.server.analytics.reporter.greenbids.model.ExtBanner;
 import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAdUnit;
 import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAnalyticsProperties;
@@ -29,9 +30,19 @@
 import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsSource;
 import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsUnifiedCode;
 import org.prebid.server.analytics.reporter.greenbids.model.MediaTypes;
+import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult;
+import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpResult;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.BidRejectionTracker;
 import org.prebid.server.exception.PreBidException;
+import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookExecutionContext;
+import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
+import org.prebid.server.hooks.v1.analytics.Activity;
+import org.prebid.server.hooks.v1.analytics.Result;
+import org.prebid.server.hooks.v1.analytics.Tags;
 import org.prebid.server.json.EncodeException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.log.Logger;
@@ -50,6 +61,8 @@
 import java.time.Clock;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -110,9 +123,13 @@ public <T> Future<Void> processEvent(T event) {
             return Future.succeededFuture();
         }
 
-        final String greenbidsId = UUID.randomUUID().toString();
         final String billingId = UUID.randomUUID().toString();
 
+        final Map<String, Ortb2ImpExtResult> analyticsResultFromAnalyticsTag = extractAnalyticsResultFromAnalyticsTag(
+                auctionContext);
+
+        final String greenbidsId = greenbidsId(analyticsResultFromAnalyticsTag);
+
         if (!isSampled(greenbidsBidRequestExt.getGreenbidsSampling(), greenbidsId)) {
             return Future.succeededFuture();
         }
@@ -124,7 +141,8 @@ public <T> Future<Void> processEvent(T event) {
                     bidResponse,
                     greenbidsId,
                     billingId,
-                    greenbidsBidRequestExt);
+                    greenbidsBidRequestExt,
+                    analyticsResultFromAnalyticsTag);
             commonMessageJson = jacksonMapper.encodeToString(commonMessage);
         } catch (PreBidException e) {
             return Future.failedFuture(e);
@@ -162,6 +180,10 @@ private GreenbidsPrebidExt parseBidRequestExt(BidRequest bidRequest) {
                 .orElse(null);
     }
 
+    private boolean isNotEmptyObjectNode(JsonNode analytics) {
+        return analytics != null && analytics.isObject() && !analytics.isEmpty();
+    }
+
     private GreenbidsPrebidExt toGreenbidsPrebidExt(ObjectNode adapterNode) {
         try {
             return jacksonMapper.mapper().treeToValue(adapterNode, GreenbidsPrebidExt.class);
@@ -170,8 +192,62 @@ private GreenbidsPrebidExt toGreenbidsPrebidExt(ObjectNode adapterNode) {
         }
     }
 
-    private boolean isNotEmptyObjectNode(JsonNode analytics) {
-        return analytics != null && analytics.isObject() && !analytics.isEmpty();
+    private Map<String, Ortb2ImpExtResult> extractAnalyticsResultFromAnalyticsTag(AuctionContext auctionContext) {
+        return Optional.ofNullable(auctionContext)
+                .map(AuctionContext::getHookExecutionContext)
+                .map(HookExecutionContext::getStageOutcomes)
+                .map(stages -> stages.get(Stage.processed_auction_request))
+                .stream()
+                .flatMap(Collection::stream)
+                .filter(stageExecutionOutcome -> "auction-request".equals(stageExecutionOutcome.getEntity()))
+                .map(StageExecutionOutcome::getGroups)
+                .flatMap(Collection::stream)
+                .map(GroupExecutionOutcome::getHooks)
+                .flatMap(Collection::stream)
+                .filter(hook -> "greenbids-real-time-data".equals(hook.getHookId().getModuleCode()))
+                .map(HookExecutionOutcome::getAnalyticsTags)
+                .map(Tags::activities)
+                .flatMap(Collection::stream)
+                .filter(activity -> "greenbids-filter".equals(activity.name()))
+                .map(Activity::results)
+                .map(List::getFirst)
+                .map(Result::values)
+                .map(this::parseAnalyticsResult)
+                .flatMap(map -> map.entrySet().stream())
+                .collect(Collectors.toMap(
+                        Map.Entry::getKey,
+                        Map.Entry::getValue,
+                        (existing, replacement) -> existing));
+    }
+
+    private Map<String, Ortb2ImpExtResult> parseAnalyticsResult(ObjectNode analyticsResult) {
+        try {
+            final Map<String, Ortb2ImpExtResult> parsedAnalyticsResult = new HashMap<>();
+            final Iterator<Map.Entry<String, JsonNode>> fields = analyticsResult.fields();
+
+            while (fields.hasNext()) {
+                final Map.Entry<String, JsonNode> field = fields.next();
+                final String impId = field.getKey();
+                final JsonNode explorationResultNode = field.getValue();
+                final Ortb2ImpExtResult ortb2ImpExtResult = jacksonMapper.mapper()
+                        .treeToValue(explorationResultNode, Ortb2ImpExtResult.class);
+                parsedAnalyticsResult.put(impId, ortb2ImpExtResult);
+            }
+
+            return parsedAnalyticsResult;
+        } catch (JsonProcessingException e) {
+            throw new PreBidException("Analytics result parsing error", e);
+        }
+    }
+
+    private String greenbidsId(Map<String, Ortb2ImpExtResult> analyticsResultFromAnalyticsTag) {
+        return Optional.ofNullable(analyticsResultFromAnalyticsTag)
+                .map(Map::values)
+                .map(Collection::stream)
+                .flatMap(Stream::findFirst)
+                .map(Ortb2ImpExtResult::getGreenbids)
+                .map(ExplorationResult::getFingerprint)
+                .orElseGet(() -> UUID.randomUUID().toString());
     }
 
     private Future<Void> processAnalyticServerResponse(HttpClientResponse response) {
@@ -213,7 +289,8 @@ private CommonMessage createBidMessage(
             BidResponse bidResponse,
             String greenbidsId,
             String billingId,
-            GreenbidsPrebidExt greenbidsImpExt) {
+            GreenbidsPrebidExt greenbidsImpExt,
+            Map<String, Ortb2ImpExtResult> analyticsResultFromAnalyticsTag) {
         final Optional<BidRequest> bidRequest = Optional.ofNullable(auctionContext.getBidRequest());
 
         final List<Imp> imps = bidRequest
@@ -231,8 +308,10 @@ private CommonMessage createBidMessage(
 
         final Map<String, NonBid> seatsWithNonBids = getSeatsWithNonBids(auctionContext);
 
-        final List<GreenbidsAdUnit> adUnitsWithBidResponses = imps.stream().map(imp -> createAdUnit(
-                imp, seatsWithBids, seatsWithNonBids, bidResponse.getCur())).toList();
+        final List<GreenbidsAdUnit> adUnitsWithBidResponses = imps.stream().map(imp ->
+                createAdUnit(
+                        imp, seatsWithBids, seatsWithNonBids, bidResponse.getCur(), analyticsResultFromAnalyticsTag))
+                .toList();
 
         final String auctionId = bidRequest
                 .map(BidRequest::getId)
@@ -283,7 +362,7 @@ private static Map<String, NonBid> getSeatsWithNonBids(AuctionContext auctionCon
     }
 
     private static SeatNonBid toSeatNonBid(String bidder, BidRejectionTracker bidRejectionTracker) {
-        final List<NonBid> nonBids = bidRejectionTracker.getRejectionReasons().entrySet().stream()
+        final List<NonBid> nonBids = bidRejectionTracker.getRejectedImps().entrySet().stream()
                 .map(entry -> NonBid.of(entry.getKey(), entry.getValue()))
                 .toList();
 
@@ -294,7 +373,8 @@ private GreenbidsAdUnit createAdUnit(
             Imp imp,
             Map<String, Bid> seatsWithBids,
             Map<String, NonBid> seatsWithNonBids,
-            String currency) {
+            String currency,
+            Map<String, Ortb2ImpExtResult> analyticsResultFromAnalyticsTag) {
         final ExtBanner extBanner = getExtBanner(imp.getBanner());
         final Video video = imp.getVideo();
         final Native nativeObject = imp.getXNative();
@@ -317,11 +397,17 @@ private GreenbidsAdUnit createAdUnit(
         final List<GreenbidsBid> bids = extractBidders(
                 imp.getId(), seatsWithBids, seatsWithNonBids, impExtPrebid, currency);
 
+        final Ortb2ImpResult ortb2ImpResult = Optional.ofNullable(analyticsResultFromAnalyticsTag)
+                .map(analyticsResult -> analyticsResult.get(imp.getId()))
+                .map(Ortb2ImpResult::of)
+                .orElse(null);
+
         return GreenbidsAdUnit.builder()
                 .code(adUnitCode)
                 .unifiedCode(greenbidsUnifiedCode)
                 .mediaTypes(mediaTypes)
                 .bids(bids)
+                .ortb2ImpResult(ortb2ImpResult)
                 .build();
     }
 
diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExplorationResult.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExplorationResult.java
new file mode 100644
index 00000000000..48a2a0e8038
--- /dev/null
+++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/ExplorationResult.java
@@ -0,0 +1,18 @@
+package org.prebid.server.analytics.reporter.greenbids.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Value;
+
+import java.util.Map;
+
+@Value(staticConstructor = "of")
+public class ExplorationResult {
+
+    String fingerprint;
+
+    @JsonProperty("keptInAuction")
+    Map<String, Boolean> keptInAuction;
+
+    @JsonProperty("isExploration")
+    Boolean isExploration;
+}
diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java
index ce3ae13231a..52f0ebab684 100644
--- a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java
+++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/GreenbidsAdUnit.java
@@ -19,4 +19,7 @@ public class GreenbidsAdUnit {
     MediaTypes mediaTypes;
 
     List<GreenbidsBid> bids;
+
+    @JsonProperty("ortb2Imp")
+    Ortb2ImpResult ortb2ImpResult;
 }
diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpExtResult.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpExtResult.java
new file mode 100644
index 00000000000..c6cc8350bd8
--- /dev/null
+++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpExtResult.java
@@ -0,0 +1,11 @@
+package org.prebid.server.analytics.reporter.greenbids.model;
+
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class Ortb2ImpExtResult {
+
+    ExplorationResult greenbids;
+
+    String tid;
+}
diff --git a/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java
new file mode 100644
index 00000000000..377bd3c677a
--- /dev/null
+++ b/src/main/java/org/prebid/server/analytics/reporter/greenbids/model/Ortb2ImpResult.java
@@ -0,0 +1,9 @@
+package org.prebid.server.analytics.reporter.greenbids.model;
+
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class Ortb2ImpResult {
+
+    Ortb2ImpExtResult ext;
+}
diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java
index 1c0b837bb4e..86fe28fdcbe 100644
--- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java
+++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java
@@ -52,7 +52,7 @@
 import org.prebid.server.events.EventsService;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.hooks.execution.HookStageExecutor;
 import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
 import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesPayload;
@@ -95,6 +95,7 @@
 import org.prebid.server.settings.model.AccountEventsConfig;
 import org.prebid.server.settings.model.AccountTargetingConfig;
 import org.prebid.server.settings.model.VideoStoredDataResult;
+import org.prebid.server.spring.config.model.CacheDefaultTtlProperties;
 import org.prebid.server.util.StreamUtil;
 import org.prebid.server.vast.VastModifier;
 
@@ -139,6 +140,7 @@ public class BidResponseCreator {
     private final Clock clock;
     private final JacksonMapper mapper;
     private final CacheTtl mediaTypeCacheTtl;
+    private final CacheDefaultTtlProperties cacheDefaultProperties;
 
     private final String cacheHost;
     private final String cachePath;
@@ -156,7 +158,8 @@ public BidResponseCreator(CoreCacheService coreCacheService,
                               int truncateAttrChars,
                               Clock clock,
                               JacksonMapper mapper,
-                              CacheTtl mediaTypeCacheTtl) {
+                              CacheTtl mediaTypeCacheTtl,
+                              CacheDefaultTtlProperties cacheDefaultProperties) {
 
         this.coreCacheService = Objects.requireNonNull(coreCacheService);
         this.bidderCatalog = Objects.requireNonNull(bidderCatalog);
@@ -171,6 +174,7 @@ public BidResponseCreator(CoreCacheService coreCacheService,
         this.clock = Objects.requireNonNull(clock);
         this.mapper = Objects.requireNonNull(mapper);
         this.mediaTypeCacheTtl = Objects.requireNonNull(mediaTypeCacheTtl);
+        this.cacheDefaultProperties = Objects.requireNonNull(cacheDefaultProperties);
 
         cacheHost = Objects.requireNonNull(coreCacheService.getEndpointHost());
         cachePath = Objects.requireNonNull(coreCacheService.getEndpointPath());
@@ -436,8 +440,8 @@ private BidInfo toBidInfo(Bid bid,
                 .bidType(type)
                 .bidder(bidder)
                 .correspondingImp(correspondingImp)
-                .ttl(resolveBannerTtl(bid, correspondingImp, cacheInfo, account))
-                .videoTtl(type == BidType.video ? resolveVideoTtl(bid, correspondingImp, cacheInfo, account) : null)
+                .ttl(resolveTtl(bid, type, correspondingImp, cacheInfo, account))
+                .vastTtl(type == BidType.video ? resolveVastTtl(bid, correspondingImp, cacheInfo, account) : null)
                 .category(categoryMappingResult.getCategory(bid))
                 .satisfiedPriority(categoryMappingResult.isBidSatisfiesPriority(bid))
                 .build();
@@ -457,31 +461,43 @@ private static Optional<Imp> correspondingImp(String impId, List<Imp> imps) {
                 .findFirst();
     }
 
-    private Integer resolveBannerTtl(Bid bid, Imp imp, BidRequestCacheInfo cacheInfo, Account account) {
-        final AccountAuctionConfig accountAuctionConfig = account.getAuction();
+    private Integer resolveTtl(Bid bid, BidType type, Imp imp, BidRequestCacheInfo cacheInfo, Account account) {
         final Integer bidTtl = bid.getExp();
         final Integer impTtl = imp != null ? imp.getExp() : null;
+        final Integer requestTtl = cacheInfo.getCacheBidsTtl();
 
-        return ObjectUtils.firstNonNull(
-                bidTtl,
-                impTtl,
-                cacheInfo.getCacheBidsTtl(),
-                accountAuctionConfig != null ? accountAuctionConfig.getBannerCacheTtl() : null,
-                mediaTypeCacheTtl.getBannerCacheTtl());
+        final AccountAuctionConfig accountAuctionConfig = account.getAuction();
+        final Integer accountTtl = accountAuctionConfig != null ? switch (type) {
+            case banner -> accountAuctionConfig.getBannerCacheTtl();
+            case video -> accountAuctionConfig.getVideoCacheTtl();
+            case audio, xNative -> null;
+        } : null;
+
+        final Integer mediaTypeTtl = switch (type) {
+            case banner -> mediaTypeCacheTtl.getBannerCacheTtl();
+            case video -> mediaTypeCacheTtl.getVideoCacheTtl();
+            case audio, xNative -> null;
+        };
 
+        final Integer defaultTtl = switch (type) {
+            case banner -> cacheDefaultProperties.getBannerTtl();
+            case video -> cacheDefaultProperties.getVideoTtl();
+            case audio -> cacheDefaultProperties.getAudioTtl();
+            case xNative -> cacheDefaultProperties.getNativeTtl();
+        };
+
+        return ObjectUtils.firstNonNull(bidTtl, impTtl, requestTtl, accountTtl, mediaTypeTtl, defaultTtl);
     }
 
-    private Integer resolveVideoTtl(Bid bid, Imp imp, BidRequestCacheInfo cacheInfo, Account account) {
+    private Integer resolveVastTtl(Bid bid, Imp imp, BidRequestCacheInfo cacheInfo, Account account) {
         final AccountAuctionConfig accountAuctionConfig = account.getAuction();
-        final Integer bidTtl = bid.getExp();
-        final Integer impTtl = imp != null ? imp.getExp() : null;
-
         return ObjectUtils.firstNonNull(
-                bidTtl,
-                impTtl,
+                bid.getExp(),
+                imp != null ? imp.getExp() : null,
                 cacheInfo.getCacheVideoBidsTtl(),
                 accountAuctionConfig != null ? accountAuctionConfig.getVideoCacheTtl() : null,
-                mediaTypeCacheTtl.getVideoCacheTtl());
+                mediaTypeCacheTtl.getVideoCacheTtl(),
+                cacheDefaultProperties.getVideoTtl());
     }
 
     private Future<List<BidderResponse>> invokeProcessedBidderResponseHooks(List<BidderResponse> bidderResponses,
@@ -1369,7 +1385,7 @@ private Bid toBid(BidInfo bidInfo,
 
         final Integer ttl = Optional.ofNullable(cacheInfo)
                 .map(info -> ObjectUtils.max(cacheInfo.getTtl(), cacheInfo.getVideoTtl()))
-                .orElseGet(() -> ObjectUtils.max(bidInfo.getTtl(), bidInfo.getVideoTtl()));
+                .orElseGet(() -> ObjectUtils.max(bidInfo.getTtl(), bidInfo.getVastTtl()));
 
         return bid.toBuilder()
                 .ext(updatedBidExt)
@@ -1742,7 +1758,7 @@ private static BidResponse populateSeatNonBid(AuctionContext auctionContext, Bid
     }
 
     private static SeatNonBid toSeatNonBid(String bidder, BidRejectionTracker bidRejectionTracker) {
-        final List<NonBid> nonBid = bidRejectionTracker.getRejectionReasons().entrySet().stream()
+        final List<NonBid> nonBid = bidRejectionTracker.getRejectedImps().entrySet().stream()
                 .map(entry -> NonBid.of(entry.getKey(), entry.getValue()))
                 .toList();
 
diff --git a/src/main/java/org/prebid/server/auction/BidderAliases.java b/src/main/java/org/prebid/server/auction/BidderAliases.java
index b9108f589c1..87ad01efe3b 100644
--- a/src/main/java/org/prebid/server/auction/BidderAliases.java
+++ b/src/main/java/org/prebid/server/auction/BidderAliases.java
@@ -39,7 +39,9 @@ public boolean isAliasDefined(String alias) {
     }
 
     public String resolveBidder(String aliasOrBidder) {
-        return aliasToBidder.getOrDefault(aliasOrBidder, aliasOrBidder);
+        return bidderCatalog.isValidName(aliasOrBidder)
+                ? aliasOrBidder
+                : aliasToBidder.getOrDefault(aliasOrBidder, aliasOrBidder);
     }
 
     public boolean isSame(String bidder1, String bidder2) {
@@ -47,9 +49,8 @@ public boolean isSame(String bidder1, String bidder2) {
     }
 
     public Integer resolveAliasVendorId(String alias) {
-        return aliasToVendorId.containsKey(alias)
-                ? aliasToVendorId.get(alias)
-                : resolveAliasVendorIdViaCatalog(alias);
+        final Integer vendorId = resolveAliasVendorIdViaCatalog(alias);
+        return vendorId == null ? aliasToVendorId.get(alias) : vendorId;
     }
 
     private Integer resolveAliasVendorIdViaCatalog(String alias) {
diff --git a/src/main/java/org/prebid/server/auction/BidsAdjuster.java b/src/main/java/org/prebid/server/auction/BidsAdjuster.java
index 8134662fcd9..4ae7a6e3e3e 100644
--- a/src/main/java/org/prebid/server/auction/BidsAdjuster.java
+++ b/src/main/java/org/prebid/server/auction/BidsAdjuster.java
@@ -1,32 +1,19 @@
 package org.prebid.server.auction;
 
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.DecimalNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.fasterxml.jackson.databind.node.TextNode;
 import com.iab.openrtb.request.BidRequest;
 import com.iab.openrtb.response.Bid;
-import org.apache.commons.lang3.StringUtils;
-import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.AuctionParticipation;
 import org.prebid.server.auction.model.BidderResponse;
+import org.prebid.server.bidadjustments.BidAdjustmentsProcessor;
 import org.prebid.server.bidder.model.BidderBid;
 import org.prebid.server.bidder.model.BidderError;
 import org.prebid.server.bidder.model.BidderSeatBid;
-import org.prebid.server.currency.CurrencyConversionService;
-import org.prebid.server.exception.PreBidException;
 import org.prebid.server.floors.PriceFloorEnforcer;
-import org.prebid.server.json.JacksonMapper;
-import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors;
-import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
-import org.prebid.server.proto.openrtb.ext.request.ImpMediaType;
 import org.prebid.server.util.ObjectUtil;
-import org.prebid.server.util.PbsUtil;
 import org.prebid.server.validation.ResponseBidValidator;
 import org.prebid.server.validation.model.ValidationResult;
 
-import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -35,29 +22,20 @@
 
 public class BidsAdjuster {
 
-    private static final String ORIGINAL_BID_CPM = "origbidcpm";
-    private static final String ORIGINAL_BID_CURRENCY = "origbidcur";
-
     private final ResponseBidValidator responseBidValidator;
-    private final CurrencyConversionService currencyService;
-    private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver;
     private final PriceFloorEnforcer priceFloorEnforcer;
+    private final BidAdjustmentsProcessor bidAdjustmentsProcessor;
     private final DsaEnforcer dsaEnforcer;
-    private final JacksonMapper mapper;
 
     public BidsAdjuster(ResponseBidValidator responseBidValidator,
-                        CurrencyConversionService currencyService,
-                        BidAdjustmentFactorResolver bidAdjustmentFactorResolver,
                         PriceFloorEnforcer priceFloorEnforcer,
-                        DsaEnforcer dsaEnforcer,
-                        JacksonMapper mapper) {
+                        BidAdjustmentsProcessor bidAdjustmentsProcessor,
+                        DsaEnforcer dsaEnforcer) {
 
         this.responseBidValidator = Objects.requireNonNull(responseBidValidator);
-        this.currencyService = Objects.requireNonNull(currencyService);
-        this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver);
         this.priceFloorEnforcer = Objects.requireNonNull(priceFloorEnforcer);
+        this.bidAdjustmentsProcessor = Objects.requireNonNull(bidAdjustmentsProcessor);
         this.dsaEnforcer = Objects.requireNonNull(dsaEnforcer);
-        this.mapper = Objects.requireNonNull(mapper);
     }
 
     public List<AuctionParticipation> validateAndAdjustBids(List<AuctionParticipation> auctionParticipations,
@@ -66,12 +44,18 @@ public List<AuctionParticipation> validateAndAdjustBids(List<AuctionParticipatio
 
         return auctionParticipations.stream()
                 .map(auctionParticipation -> validBidderResponse(auctionParticipation, auctionContext, aliases))
-                .map(auctionParticipation -> applyBidPriceChanges(auctionParticipation, auctionContext.getBidRequest()))
+
+                .map(auctionParticipation -> bidAdjustmentsProcessor.enrichWithAdjustedBids(
+                        auctionParticipation,
+                        auctionContext.getBidRequest(),
+                        auctionContext.getBidAdjustments()))
+
                 .map(auctionParticipation -> priceFloorEnforcer.enforce(
                         auctionContext.getBidRequest(),
                         auctionParticipation,
                         auctionContext.getAccount(),
                         auctionContext.getBidRejectionTrackers().get(auctionParticipation.getBidder())))
+
                 .map(auctionParticipation -> dsaEnforcer.enforce(
                         auctionContext.getBidRequest(),
                         auctionParticipation,
@@ -137,104 +121,4 @@ private BidderError makeValidationBidderError(Bid bid, ValidationResult validati
         final String bidId = ObjectUtil.getIfNotNullOrDefault(bid, Bid::getId, () -> "unknown");
         return BidderError.invalidBid("BidId `" + bidId + "` validation messages: " + validationErrors);
     }
-
-    private AuctionParticipation applyBidPriceChanges(AuctionParticipation auctionParticipation,
-                                                      BidRequest bidRequest) {
-        if (auctionParticipation.isRequestBlocked()) {
-            return auctionParticipation;
-        }
-
-        final BidderResponse bidderResponse = auctionParticipation.getBidderResponse();
-        final BidderSeatBid seatBid = bidderResponse.getSeatBid();
-
-        final List<BidderBid> bidderBids = seatBid.getBids();
-        if (bidderBids.isEmpty()) {
-            return auctionParticipation;
-        }
-
-        final List<BidderBid> updatedBidderBids = new ArrayList<>(bidderBids.size());
-        final List<BidderError> errors = new ArrayList<>(seatBid.getErrors());
-        final String adServerCurrency = bidRequest.getCur().getFirst();
-
-        for (final BidderBid bidderBid : bidderBids) {
-            try {
-                final BidderBid updatedBidderBid =
-                        updateBidderBidWithBidPriceChanges(bidderBid, bidderResponse, bidRequest, adServerCurrency);
-                updatedBidderBids.add(updatedBidderBid);
-            } catch (PreBidException e) {
-                errors.add(BidderError.generic(e.getMessage()));
-            }
-        }
-
-        final BidderResponse resultBidderResponse = bidderResponse.with(seatBid.toBuilder()
-                .bids(updatedBidderBids)
-                .errors(errors)
-                .build());
-        return auctionParticipation.with(resultBidderResponse);
-    }
-
-    private BidderBid updateBidderBidWithBidPriceChanges(BidderBid bidderBid,
-                                                         BidderResponse bidderResponse,
-                                                         BidRequest bidRequest,
-                                                         String adServerCurrency) {
-        final Bid bid = bidderBid.getBid();
-        final String bidCurrency = bidderBid.getBidCurrency();
-        final BigDecimal price = bid.getPrice();
-
-        final BigDecimal priceInAdServerCurrency = currencyService.convertCurrency(
-                price, bidRequest, StringUtils.stripToNull(bidCurrency), adServerCurrency);
-
-        final BigDecimal priceAdjustmentFactor =
-                bidAdjustmentForBidder(bidderResponse.getBidder(), bidRequest, bidderBid);
-        final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, priceInAdServerCurrency);
-
-        final ObjectNode bidExt = bid.getExt();
-        final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode();
-
-        updateExtWithOrigPriceValues(updatedBidExt, price, bidCurrency);
-
-        final Bid.BidBuilder bidBuilder = bid.toBuilder();
-        if (adjustedPrice.compareTo(price) != 0) {
-            bidBuilder.price(adjustedPrice);
-        }
-
-        if (!updatedBidExt.isEmpty()) {
-            bidBuilder.ext(updatedBidExt);
-        }
-
-        return bidderBid.toBuilder().bid(bidBuilder.build()).build();
-    }
-
-    private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, BidderBid bidderBid) {
-        final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest);
-        if (adjustmentFactors == null) {
-            return null;
-        }
-        final ImpMediaType mediaType = ImpMediaTypeResolver.resolve(
-                bidderBid.getBid().getImpid(), bidRequest.getImp(), bidderBid.getType());
-
-        return bidAdjustmentFactorResolver.resolve(mediaType, adjustmentFactors, bidder);
-    }
-
-    private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) {
-        final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest);
-        return prebid != null ? prebid.getBidadjustmentfactors() : null;
-    }
-
-    private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) {
-        return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0
-                ? price.multiply(priceAdjustmentFactor)
-                : price;
-    }
-
-    private static void updateExtWithOrigPriceValues(ObjectNode updatedBidExt, BigDecimal price, String bidCurrency) {
-        addPropertyToNode(updatedBidExt, ORIGINAL_BID_CPM, new DecimalNode(price));
-        if (StringUtils.isNotBlank(bidCurrency)) {
-            addPropertyToNode(updatedBidExt, ORIGINAL_BID_CURRENCY, new TextNode(bidCurrency));
-        }
-    }
-
-    private static void addPropertyToNode(ObjectNode node, String propertyName, JsonNode propertyValue) {
-        node.set(propertyName, propertyValue);
-    }
 }
diff --git a/src/main/java/org/prebid/server/auction/DsaEnforcer.java b/src/main/java/org/prebid/server/auction/DsaEnforcer.java
index 6719829cc56..369842b36c6 100644
--- a/src/main/java/org/prebid/server/auction/DsaEnforcer.java
+++ b/src/main/java/org/prebid/server/auction/DsaEnforcer.java
@@ -72,7 +72,7 @@ public AuctionParticipation enforce(BidRequest bidRequest,
                 }
             } catch (PreBidException e) {
                 warnings.add(BidderError.invalidBid("Bid \"%s\": %s".formatted(bid.getId(), e.getMessage())));
-                rejectionTracker.reject(bid.getImpid(), BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
+                rejectionTracker.rejectBid(bidderBid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
                 updatedBidderBids.remove(bidderBid);
             }
         }
diff --git a/src/main/java/org/prebid/server/auction/ExchangeService.java b/src/main/java/org/prebid/server/auction/ExchangeService.java
index 66b34a8df46..40c8ff86bde 100644
--- a/src/main/java/org/prebid/server/auction/ExchangeService.java
+++ b/src/main/java/org/prebid/server/auction/ExchangeService.java
@@ -57,19 +57,12 @@
 import org.prebid.server.cookie.UidsCookie;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.floors.PriceFloorAdjuster;
 import org.prebid.server.floors.PriceFloorProcessor;
 import org.prebid.server.hooks.execution.HookStageExecutor;
-import org.prebid.server.hooks.execution.model.ExecutionAction;
-import org.prebid.server.hooks.execution.model.ExecutionStatus;
-import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
-import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
-import org.prebid.server.hooks.execution.model.HookId;
 import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
-import org.prebid.server.hooks.execution.model.Stage;
-import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
 import org.prebid.server.hooks.v1.bidder.BidderRequestPayload;
 import org.prebid.server.hooks.v1.bidder.BidderResponsePayload;
 import org.prebid.server.json.JacksonMapper;
@@ -110,7 +103,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.EnumMap;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -221,8 +213,7 @@ public Future<AuctionContext> holdAuction(AuctionContext context) {
         return processAuctionRequest(context)
                 .compose(this::invokeResponseHooks)
                 .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags)
-                .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo)
-                .map(this::updateHooksMetrics);
+                .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo);
     }
 
     private Future<AuctionContext> processAuctionRequest(AuctionContext context) {
@@ -249,6 +240,10 @@ private Future<AuctionContext> runAuction(AuctionContext receivedContext) {
         final Map<String, MultiBidConfig> bidderToMultiBid = bidderToMultiBids(bidRequest, debugWarnings);
         receivedContext.getBidRejectionTrackers().putAll(makeBidRejectionTrackers(bidRequest, aliases));
 
+        final boolean debugEnabled = receivedContext.getDebugContext().isDebugEnabled();
+        metrics.updateDebugRequestMetrics(debugEnabled);
+        metrics.updateAccountDebugRequestMetrics(account, debugEnabled);
+
         return storedResponseProcessor.getStoredResponseResult(bidRequest.getImp(), timeout)
                 .map(storedResponseResult -> populateStoredResponse(storedResponseResult, storedAuctionResponses))
                 .compose(storedResponseResult ->
@@ -273,7 +268,8 @@ private Future<AuctionContext> runAuction(AuctionContext receivedContext) {
                                 storedAuctionResponses,
                                 bidRequest.getImp(),
                                 context.getBidRejectionTrackers()))
-                        .map(auctionParticipations -> dropZeroNonDealBids(auctionParticipations, debugWarnings))
+                        .map(auctionParticipations -> dropZeroNonDealBids(
+                                auctionParticipations, debugWarnings, debugEnabled))
                         .map(auctionParticipations ->
                                 bidsAdjuster.validateAndAdjustBids(auctionParticipations, context, aliases))
                         .map(auctionParticipations -> updateResponsesMetrics(auctionParticipations, account, aliases))
@@ -284,7 +280,7 @@ private Future<AuctionContext> runAuction(AuctionContext receivedContext) {
                                 logger,
                                 bidResponse,
                                 context.getBidRequest(),
-                                context.getDebugContext().isDebugEnabled()))
+                                debugEnabled))
                         .compose(bidResponse -> bidResponsePostProcessor.postProcess(
                                 context.getHttpRequest(), uidsCookie, bidRequest, bidResponse, account))
                         .map(context::with));
@@ -705,7 +701,7 @@ private AuctionParticipation createAuctionParticipation(
         if (blockedRequestByTcf) {
             context.getBidRejectionTrackers()
                     .get(bidder)
-                    .rejectAll(BidRejectionReason.REQUEST_BLOCKED_PRIVACY);
+                    .rejectAllImps(BidRejectionReason.REQUEST_BLOCKED_PRIVACY);
 
             return AuctionParticipation.builder()
                     .bidder(bidder)
@@ -1158,7 +1154,7 @@ private static Future<BidderResponse> processReject(AuctionContext auctionContex
 
         auctionContext.getBidRejectionTrackers()
                 .get(bidderName)
-                .rejectAll(bidRejectionReason);
+                .rejectAllImps(bidRejectionReason);
         final BidderSeatBid bidderSeatBid = BidderSeatBid.builder()
                 .warnings(warnings)
                 .build();
@@ -1197,7 +1193,7 @@ private Future<BidderResponse> requestBidsOrRejectBidder(
         if (hookStageResult.isShouldReject()) {
             auctionContext.getBidRejectionTrackers()
                     .get(bidderRequest.getBidder())
-                    .rejectAll(BidRejectionReason.REQUEST_BLOCKED_GENERAL);
+                    .rejectAllImps(BidRejectionReason.REQUEST_BLOCKED_GENERAL);
 
             return Future.succeededFuture(BidderResponse.of(bidderRequest.getBidder(), BidderSeatBid.empty(), 0));
         }
@@ -1221,6 +1217,7 @@ private Future<BidderResponse> requestBids(BidderRequest bidderRequest,
         final String bidderName = bidderRequest.getBidder();
         final String resolvedBidderName = aliases.resolveBidder(bidderName);
         final Bidder<?> bidder = bidderCatalog.bidderByName(resolvedBidderName);
+        final long bidderTmaxDeductionMs = bidderCatalog.bidderInfoByName(resolvedBidderName).getTmaxDeductionMs();
         final BidRejectionTracker bidRejectionTracker = auctionContext.getBidRejectionTrackers().get(bidderName);
 
         final TimeoutContext timeoutContext = auctionContext.getTimeoutContext();
@@ -1229,7 +1226,8 @@ private Future<BidderResponse> requestBids(BidderRequest bidderRequest,
         final long bidderRequestStartTime = clock.millis();
 
         return Future.succeededFuture(bidderRequest.getBidRequest())
-                .map(bidRequest -> adjustTmax(bidRequest, auctionStartTime, adjustmentFactor, bidderRequestStartTime))
+                .map(bidRequest -> adjustTmax(
+                        bidRequest, auctionStartTime, adjustmentFactor, bidderRequestStartTime, bidderTmaxDeductionMs))
                 .map(bidRequest -> ortbVersionConversionManager.convertFromAuctionSupportedVersion(
                         bidRequest, bidderRequest.getOrtbVersion()))
                 .map(bidderRequest::with)
@@ -1244,9 +1242,16 @@ private Future<BidderResponse> requestBids(BidderRequest bidderRequest,
                 .map(seatBid -> BidderResponse.of(bidderName, seatBid, responseTime(bidderRequestStartTime)));
     }
 
-    private BidRequest adjustTmax(BidRequest bidRequest, long startTime, int adjustmentFactor, long currentTime) {
+    private BidRequest adjustTmax(BidRequest bidRequest,
+                                  long startTime,
+                                  int adjustmentFactor,
+                                  long currentTime,
+                                  long bidderTmaxDeductionMs) {
+
         final long tmax = timeoutResolver.limitToMax(bidRequest.getTmax());
-        final long adjustedTmax = timeoutResolver.adjustForBidder(tmax, adjustmentFactor, currentTime - startTime);
+        final long adjustedTmax = timeoutResolver.adjustForBidder(
+                tmax, adjustmentFactor, currentTime - startTime, bidderTmaxDeductionMs);
+
         return tmax != adjustedTmax
                 ? bidRequest.toBuilder().tmax(adjustedTmax).build()
                 : bidRequest;
@@ -1269,15 +1274,18 @@ private BidderResponse rejectBidderResponseOrProceed(HookStageExecutionResult<Bi
     }
 
     private List<AuctionParticipation> dropZeroNonDealBids(List<AuctionParticipation> auctionParticipations,
-                                                           List<String> debugWarnings) {
+                                                           List<String> debugWarnings,
+                                                           boolean isDebugEnabled) {
 
         return auctionParticipations.stream()
-                .map(auctionParticipation -> dropZeroNonDealBids(auctionParticipation, debugWarnings))
+                .map(auctionParticipation -> dropZeroNonDealBids(auctionParticipation, debugWarnings, isDebugEnabled))
                 .toList();
     }
 
     private AuctionParticipation dropZeroNonDealBids(AuctionParticipation auctionParticipation,
-                                                     List<String> debugWarnings) {
+                                                     List<String> debugWarnings,
+                                                     boolean isDebugEnabled) {
+
         final BidderResponse bidderResponse = auctionParticipation.getBidderResponse();
         final BidderSeatBid seatBid = bidderResponse.getSeatBid();
         final List<BidderBid> bidderBids = seatBid.getBids();
@@ -1287,8 +1295,11 @@ private AuctionParticipation dropZeroNonDealBids(AuctionParticipation auctionPar
             final Bid bid = bidderBid.getBid();
             if (isZeroNonDealBids(bid.getPrice(), bid.getDealid())) {
                 metrics.updateAdapterRequestErrorMetric(bidderResponse.getBidder(), MetricName.unknown_error);
-                debugWarnings.add("Dropped bid '%s'. Does not contain a positive (or zero if there is a deal) 'price'"
-                        .formatted(bid.getId()));
+                if (isDebugEnabled) {
+                    debugWarnings.add(
+                            "Dropped bid '%s'. Does not contain a positive (or zero if there is a deal) 'price'"
+                            .formatted(bid.getId()));
+                }
             } else {
                 validBids.add(bidderBid);
             }
@@ -1367,57 +1378,4 @@ private static MetricName bidderErrorTypeToMetric(BidderError.Type errorType) {
             case rejected_ipf, generic -> MetricName.unknown_error;
         };
     }
-
-    private AuctionContext updateHooksMetrics(AuctionContext context) {
-        final EnumMap<Stage, List<StageExecutionOutcome>> stageOutcomes =
-                context.getHookExecutionContext().getStageOutcomes();
-
-        final Account account = context.getAccount();
-
-        stageOutcomes.forEach((stage, outcomes) -> updateHooksStageMetrics(account, stage, outcomes));
-
-        // account might be null if request is rejected by the entrypoint hook
-        if (account != null) {
-            stageOutcomes.values().stream()
-                    .flatMap(Collection::stream)
-                    .map(StageExecutionOutcome::getGroups)
-                    .flatMap(Collection::stream)
-                    .map(GroupExecutionOutcome::getHooks)
-                    .flatMap(Collection::stream)
-                    .collect(Collectors.groupingBy(
-                            outcome -> outcome.getHookId().getModuleCode(),
-                            Collectors.summingLong(HookExecutionOutcome::getExecutionTime)))
-                    .forEach((moduleCode, executionTime) ->
-                            metrics.updateAccountModuleDurationMetric(account, moduleCode, executionTime));
-        }
-
-        return context;
-    }
-
-    private void updateHooksStageMetrics(Account account, Stage stage, List<StageExecutionOutcome> stageOutcomes) {
-        stageOutcomes.stream()
-                .flatMap(stageOutcome -> stageOutcome.getGroups().stream())
-                .flatMap(groupOutcome -> groupOutcome.getHooks().stream())
-                .forEach(hookOutcome -> updateHookInvocationMetrics(account, stage, hookOutcome));
-    }
-
-    private void updateHookInvocationMetrics(Account account, Stage stage, HookExecutionOutcome hookOutcome) {
-        final HookId hookId = hookOutcome.getHookId();
-        final ExecutionStatus status = hookOutcome.getStatus();
-        final ExecutionAction action = hookOutcome.getAction();
-        final String moduleCode = hookId.getModuleCode();
-
-        metrics.updateHooksMetrics(
-                moduleCode,
-                stage,
-                hookId.getHookImplCode(),
-                status,
-                hookOutcome.getExecutionTime(),
-                action);
-
-        // account might be null if request is rejected by the entrypoint hook
-        if (account != null) {
-            metrics.updateAccountHooksMetrics(account, moduleCode, status, action);
-        }
-    }
 }
diff --git a/src/main/java/org/prebid/server/auction/FpdResolver.java b/src/main/java/org/prebid/server/auction/FpdResolver.java
index e1e4fa1e436..f0ade099ece 100644
--- a/src/main/java/org/prebid/server/auction/FpdResolver.java
+++ b/src/main/java/org/prebid/server/auction/FpdResolver.java
@@ -1,29 +1,20 @@
 package org.prebid.server.auction;
 
-import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.NullNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.iab.openrtb.request.App;
-import com.iab.openrtb.request.Data;
 import com.iab.openrtb.request.Dooh;
 import com.iab.openrtb.request.Site;
 import com.iab.openrtb.request.User;
-import org.apache.commons.lang3.ObjectUtils;
+import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.json.JsonMerger;
-import org.prebid.server.proto.openrtb.ext.request.ExtApp;
-import org.prebid.server.proto.openrtb.ext.request.ExtDooh;
-import org.prebid.server.proto.openrtb.ext.request.ExtSite;
-import org.prebid.server.proto.openrtb.ext.request.ExtUser;
 
-import java.util.Collections;
-import java.util.List;
 import java.util.Objects;
 import java.util.Set;
-import java.util.function.Function;
-import java.util.function.Predicate;
-import java.util.stream.StreamSupport;
 
 public class FpdResolver {
 
@@ -33,17 +24,8 @@ public class FpdResolver {
     private static final String APP = "app";
     private static final String DOOH = "dooh";
     private static final Set<String> KNOWN_FPD_ATTRIBUTES = Set.of(USER, SITE, APP, DOOH, BIDDERS);
-    private static final String EXT = "ext";
     private static final String CONTEXT = "context";
     private static final String DATA = "data";
-    private static final Set<String> USER_DATA_ATTR = Collections.singleton("geo");
-    private static final Set<String> APP_DATA_ATTR = Set.of("id", "content", "publisher", "privacypolicy");
-    private static final Set<String> SITE_DATA_ATTR = Set.of("id", "content", "publisher", "privacypolicy", "mobile");
-    private static final Set<String> DOOH_DATA_ATTR = Set.of("id", "content", "publisher", "privacypolicy");
-
-    private static final TypeReference<List<Data>> USER_DATA_TYPE_REFERENCE =
-            new TypeReference<>() {
-            };
 
     private final JacksonMapper jacksonMapper;
     private final JsonMerger jsonMerger;
@@ -54,146 +36,37 @@ public FpdResolver(JacksonMapper jacksonMapper, JsonMerger jsonMerger) {
     }
 
     public User resolveUser(User originUser, ObjectNode fpdUser) {
-        if (fpdUser == null) {
-            return originUser;
-        }
-        final User resultUser = originUser == null ? User.builder().build() : originUser;
-        final ExtUser resolvedExtUser = resolveUserExt(fpdUser, resultUser);
-        return resultUser.toBuilder()
-                .keywords(ObjectUtils.defaultIfNull(getString(fpdUser, "keywords"), resultUser.getKeywords()))
-                .gender(ObjectUtils.defaultIfNull(getString(fpdUser, "gender"), resultUser.getGender()))
-                .yob(ObjectUtils.defaultIfNull(getInteger(fpdUser, "yob"), resultUser.getYob()))
-                .data(ObjectUtils.defaultIfNull(getFpdUserData(fpdUser), resultUser.getData()))
-                .ext(resolvedExtUser)
-                .build();
-    }
-
-    private ExtUser resolveUserExt(ObjectNode fpdUser, User originUser) {
-        final ExtUser originExtUser = originUser.getExt();
-        final ObjectNode resolvedData =
-                mergeExtData(fpdUser.path(EXT).path(DATA), originExtUser != null ? originExtUser.getData() : null);
-
-        return updateUserExtDataWithFpdAttr(fpdUser, originExtUser, resolvedData);
-    }
-
-    private ExtUser updateUserExtDataWithFpdAttr(ObjectNode fpdUser, ExtUser originExtUser, ObjectNode extData) {
-        final ObjectNode resultData = extData != null ? extData : jacksonMapper.mapper().createObjectNode();
-        USER_DATA_ATTR.forEach(attribute -> setAttr(fpdUser, resultData, attribute));
-        return originExtUser != null
-                ? originExtUser.toBuilder().data(resultData.isEmpty() ? null : resultData).build()
-                : resultData.isEmpty() ? null : ExtUser.builder().data(resultData).build();
-    }
-
-    private List<Data> getFpdUserData(ObjectNode fpdUser) {
-        final ArrayNode fpdUserDataNode = getValueFromJsonNode(
-                fpdUser, DATA, node -> (ArrayNode) node, JsonNode::isArray);
-
-        return toList(fpdUserDataNode, USER_DATA_TYPE_REFERENCE);
+        return mergeFpd(originUser, fpdUser, User.class);
     }
 
     public App resolveApp(App originApp, ObjectNode fpdApp) {
-        if (fpdApp == null) {
-            return originApp;
-        }
-        final App resultApp = originApp == null ? App.builder().build() : originApp;
-        final ExtApp resolvedExtApp = resolveAppExt(fpdApp, resultApp);
-        return resultApp.toBuilder()
-                .name(ObjectUtils.defaultIfNull(getString(fpdApp, "name"), resultApp.getName()))
-                .bundle(ObjectUtils.defaultIfNull(getString(fpdApp, "bundle"), resultApp.getBundle()))
-                .storeurl(ObjectUtils.defaultIfNull(getString(fpdApp, "storeurl"), resultApp.getStoreurl()))
-                .domain(ObjectUtils.defaultIfNull(getString(fpdApp, "domain"), resultApp.getDomain()))
-                .cat(ObjectUtils.defaultIfNull(getStrings(fpdApp, "cat"), resultApp.getCat()))
-                .sectioncat(ObjectUtils.defaultIfNull(getStrings(fpdApp, "sectioncat"), resultApp.getSectioncat()))
-                .pagecat(ObjectUtils.defaultIfNull(getStrings(fpdApp, "pagecat"), resultApp.getPagecat()))
-                .keywords(ObjectUtils.defaultIfNull(getString(fpdApp, "keywords"), resultApp.getKeywords()))
-                .ext(resolvedExtApp)
-                .build();
-    }
-
-    private ExtApp resolveAppExt(ObjectNode fpdApp, App originApp) {
-        final ExtApp originExtApp = originApp.getExt();
-        final ObjectNode resolvedData =
-                mergeExtData(fpdApp.path(EXT).path(DATA), originExtApp != null ? originExtApp.getData() : null);
-
-        return updateAppExtDataWithFpdAttr(fpdApp, originExtApp, resolvedData);
-    }
-
-    private ExtApp updateAppExtDataWithFpdAttr(ObjectNode fpdApp, ExtApp originExtApp, ObjectNode extData) {
-        final ObjectNode resultData = extData != null ? extData : jacksonMapper.mapper().createObjectNode();
-        APP_DATA_ATTR.forEach(attribute -> setAttr(fpdApp, resultData, attribute));
-        return originExtApp != null
-                ? ExtApp.of(originExtApp.getPrebid(), resultData.isEmpty() ? null : resultData)
-                : resultData.isEmpty() ? null : ExtApp.of(null, resultData);
+        return mergeFpd(originApp, fpdApp, App.class);
     }
 
     public Site resolveSite(Site originSite, ObjectNode fpdSite) {
-        if (fpdSite == null) {
-            return originSite;
-        }
-        final Site resultSite = originSite == null ? Site.builder().build() : originSite;
-        final ExtSite resolvedExtSite = resolveSiteExt(fpdSite, resultSite);
-        return resultSite.toBuilder()
-                .name(ObjectUtils.defaultIfNull(getString(fpdSite, "name"), resultSite.getName()))
-                .domain(ObjectUtils.defaultIfNull(getString(fpdSite, "domain"), resultSite.getDomain()))
-                .cat(ObjectUtils.defaultIfNull(getStrings(fpdSite, "cat"), resultSite.getCat()))
-                .sectioncat(ObjectUtils.defaultIfNull(getStrings(fpdSite, "sectioncat"), resultSite.getSectioncat()))
-                .pagecat(ObjectUtils.defaultIfNull(getStrings(fpdSite, "pagecat"), resultSite.getPagecat()))
-                .page(ObjectUtils.defaultIfNull(getString(fpdSite, "page"), resultSite.getPage()))
-                .keywords(ObjectUtils.defaultIfNull(getString(fpdSite, "keywords"), resultSite.getKeywords()))
-                .ref(ObjectUtils.defaultIfNull(getString(fpdSite, "ref"), resultSite.getRef()))
-                .search(ObjectUtils.defaultIfNull(getString(fpdSite, "search"), resultSite.getSearch()))
-                .ext(resolvedExtSite)
-                .build();
-    }
-
-    private ExtSite resolveSiteExt(ObjectNode fpdSite, Site originSite) {
-        final ExtSite originExtSite = originSite.getExt();
-        final ObjectNode resolvedData =
-                mergeExtData(fpdSite.path(EXT).path(DATA), originExtSite != null ? originExtSite.getData() : null);
-
-        return updateSiteExtDataWithFpdAttr(fpdSite, originExtSite, resolvedData);
-    }
-
-    private ExtSite updateSiteExtDataWithFpdAttr(ObjectNode fpdSite, ExtSite originExtSite, ObjectNode extData) {
-        final ObjectNode resultData = extData != null ? extData : jacksonMapper.mapper().createObjectNode();
-        SITE_DATA_ATTR.forEach(attribute -> setAttr(fpdSite, resultData, attribute));
-        return originExtSite != null
-                ? ExtSite.of(originExtSite.getAmp(), resultData.isEmpty() ? null : resultData)
-                : resultData.isEmpty() ? null : ExtSite.of(null, resultData);
+        return mergeFpd(originSite, fpdSite, Site.class);
     }
 
     public Dooh resolveDooh(Dooh originDooh, ObjectNode fpdDooh) {
-        if (fpdDooh == null) {
-            return originDooh;
-        }
-        final Dooh resultDooh = originDooh == null ? Dooh.builder().build() : originDooh;
-        final ExtDooh resolvedExtDooh = resolveDoohExt(fpdDooh, resultDooh);
-        return resultDooh.toBuilder()
-                .name(ObjectUtils.defaultIfNull(getString(fpdDooh, "name"), resultDooh.getName()))
-                .venuetype(ObjectUtils.defaultIfNull(getStrings(fpdDooh, "venuetype"), resultDooh.getVenuetype()))
-                .venuetypetax(ObjectUtils.defaultIfNull(
-                        getInteger(fpdDooh, "venuetypetax"),
-                        resultDooh.getVenuetypetax()))
-                .domain(ObjectUtils.defaultIfNull(getString(fpdDooh, "domain"), resultDooh.getDomain()))
-                .keywords(ObjectUtils.defaultIfNull(getString(fpdDooh, "keywords"), resultDooh.getKeywords()))
-                .ext(resolvedExtDooh)
-                .build();
+        return mergeFpd(originDooh, fpdDooh, Dooh.class);
     }
 
-    private ExtDooh resolveDoohExt(ObjectNode fpdDooh, Dooh originDooh) {
-        final ExtDooh originExtDooh = originDooh.getExt();
-        final ObjectNode resolvedData =
-                mergeExtData(fpdDooh.path(EXT).path(DATA), originExtDooh != null ? originExtDooh.getData() : null);
+    private <T> T mergeFpd(T original, ObjectNode fpd, Class<T> tClass) {
+        if (fpd == null || fpd.isNull() || fpd.isMissingNode()) {
+            return original;
+        }
 
-        return updateDoohExtDataWithFpdAttr(fpdDooh, originExtDooh, resolvedData);
-    }
+        final ObjectMapper mapper = jacksonMapper.mapper();
 
-    private ExtDooh updateDoohExtDataWithFpdAttr(ObjectNode fpdDooh, ExtDooh originExtDooh, ObjectNode extData) {
-        final ObjectNode resultData = extData != null ? extData : jacksonMapper.mapper().createObjectNode();
-        DOOH_DATA_ATTR.forEach(attribute -> setAttr(fpdDooh, resultData, attribute));
-        return originExtDooh != null
-                ? ExtDooh.of(resultData.isEmpty() ? null : resultData)
-                : resultData.isEmpty() ? null : ExtDooh.of(resultData);
+        final JsonNode originalAsJsonNode = original != null
+                ? mapper.valueToTree(original)
+                : NullNode.getInstance();
+        final JsonNode merged = jsonMerger.merge(fpd, originalAsJsonNode);
+        try {
+            return mapper.treeToValue(merged, tClass);
+        } catch (JsonProcessingException e) {
+            throw new InvalidRequestException("Can't convert merging result class " + tClass.getName());
+        }
     }
 
     public ObjectNode resolveImpExt(ObjectNode impExt, ObjectNode targeting) {
@@ -277,62 +150,4 @@ private void removeOrReplace(ObjectNode impExt, String field, JsonNode jsonNode)
             impExt.set(field, jsonNode);
         }
     }
-
-    private ObjectNode mergeExtData(JsonNode fpdData, JsonNode originData) {
-        if (fpdData.isMissingNode() || !fpdData.isObject()) {
-            return originData != null && originData.isObject() ? ((ObjectNode) originData).deepCopy() : null;
-        }
-
-        if (originData != null && originData.isObject()) {
-            return (ObjectNode) jsonMerger.merge(fpdData, originData);
-        }
-        return fpdData.isObject() ? (ObjectNode) fpdData : null;
-    }
-
-    private static void setAttr(ObjectNode source, ObjectNode dest, String fieldName) {
-        final JsonNode field = source.get(fieldName);
-        if (field != null) {
-            dest.set(fieldName, field);
-        }
-    }
-
-    private static List<String> getStrings(JsonNode firstItem, String fieldName) {
-        final JsonNode valueNode = firstItem.get(fieldName);
-        final ArrayNode arrayNode = valueNode != null && valueNode.isArray() ? (ArrayNode) valueNode : null;
-        return arrayNode != null && isTextualArray(arrayNode)
-                ? StreamSupport.stream(arrayNode.spliterator(), false)
-                .map(JsonNode::asText)
-                .toList()
-                : null;
-    }
-
-    private static boolean isTextualArray(ArrayNode arrayNode) {
-        return StreamSupport.stream(arrayNode.spliterator(), false).allMatch(JsonNode::isTextual);
-    }
-
-    private static String getString(ObjectNode firstItem, String fieldName) {
-        return getValueFromJsonNode(firstItem, fieldName, JsonNode::asText, JsonNode::isTextual);
-    }
-
-    private static Integer getInteger(ObjectNode firstItem, String fieldName) {
-        return getValueFromJsonNode(firstItem, fieldName, JsonNode::asInt, JsonNode::isInt);
-    }
-
-    private <T> List<T> toList(JsonNode node, TypeReference<List<T>> listTypeReference) {
-        try {
-            return jacksonMapper.mapper().convertValue(node, listTypeReference);
-        } catch (IllegalArgumentException e) {
-            return null;
-        }
-    }
-
-    private static <T> T getValueFromJsonNode(ObjectNode firstItem, String fieldName,
-                                              Function<JsonNode, T> nodeConverter,
-                                              Predicate<JsonNode> isCorrectType) {
-        final JsonNode valueNode = firstItem.get(fieldName);
-        return valueNode != null && isCorrectType.test(valueNode)
-                ? nodeConverter.apply(valueNode)
-                : null;
-    }
-
 }
diff --git a/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java b/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java
index 4c1a3b0b8cf..609e7481b81 100644
--- a/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java
+++ b/src/main/java/org/prebid/server/auction/GeoLocationServiceWrapper.java
@@ -8,7 +8,7 @@
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.IpAddress;
 import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.geolocation.GeoLocationService;
 import org.prebid.server.geolocation.model.GeoInfo;
 import org.prebid.server.log.Logger;
diff --git a/src/main/java/org/prebid/server/auction/HooksMetricsService.java b/src/main/java/org/prebid/server/auction/HooksMetricsService.java
new file mode 100644
index 00000000000..0b31d28444f
--- /dev/null
+++ b/src/main/java/org/prebid/server/auction/HooksMetricsService.java
@@ -0,0 +1,81 @@
+package org.prebid.server.auction;
+
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.hooks.execution.model.ExecutionAction;
+import org.prebid.server.hooks.execution.model.ExecutionStatus;
+import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
+import org.prebid.server.metric.Metrics;
+import org.prebid.server.settings.model.Account;
+
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class HooksMetricsService {
+
+    private final Metrics metrics;
+
+    public HooksMetricsService(Metrics metrics) {
+        this.metrics = Objects.requireNonNull(metrics);
+    }
+
+    public AuctionContext updateHooksMetrics(AuctionContext context) {
+        final EnumMap<Stage, List<StageExecutionOutcome>> stageOutcomes =
+                context.getHookExecutionContext().getStageOutcomes();
+
+        final Account account = context.getAccount();
+
+        stageOutcomes.forEach((stage, outcomes) -> updateHooksStageMetrics(account, stage, outcomes));
+
+        // account might be null if request is rejected by the entrypoint hook
+        if (account != null) {
+            stageOutcomes.values().stream()
+                    .flatMap(Collection::stream)
+                    .map(StageExecutionOutcome::getGroups)
+                    .flatMap(Collection::stream)
+                    .map(GroupExecutionOutcome::getHooks)
+                    .flatMap(Collection::stream)
+                    .filter(hookOutcome -> hookOutcome.getAction() != ExecutionAction.no_invocation)
+                    .collect(Collectors.groupingBy(
+                            outcome -> outcome.getHookId().getModuleCode(),
+                            Collectors.summingLong(HookExecutionOutcome::getExecutionTime)))
+                    .forEach((moduleCode, executionTime) ->
+                            metrics.updateAccountModuleDurationMetric(account, moduleCode, executionTime));
+        }
+
+        return context;
+    }
+
+    private void updateHooksStageMetrics(Account account, Stage stage, List<StageExecutionOutcome> stageOutcomes) {
+        stageOutcomes.stream()
+                .flatMap(stageOutcome -> stageOutcome.getGroups().stream())
+                .flatMap(groupOutcome -> groupOutcome.getHooks().stream())
+                .forEach(hookOutcome -> updateHookInvocationMetrics(account, stage, hookOutcome));
+    }
+
+    private void updateHookInvocationMetrics(Account account, Stage stage, HookExecutionOutcome hookOutcome) {
+        final HookId hookId = hookOutcome.getHookId();
+        final ExecutionStatus status = hookOutcome.getStatus();
+        final ExecutionAction action = hookOutcome.getAction();
+        final String moduleCode = hookId.getModuleCode();
+
+        metrics.updateHooksMetrics(
+                moduleCode,
+                stage,
+                hookId.getHookImplCode(),
+                status,
+                hookOutcome.getExecutionTime(),
+                action);
+
+        // account might be null if request is rejected by the entrypoint hook
+        if (account != null) {
+            metrics.updateAccountHooksMetrics(account, moduleCode, status, action);
+        }
+    }
+}
diff --git a/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java b/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java
index 3256ed360e1..964b89b8b3e 100644
--- a/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java
+++ b/src/main/java/org/prebid/server/auction/ImpMediaTypeResolver.java
@@ -31,12 +31,14 @@ private static ImpMediaType resolveBidAdjustmentVideoMediaType(String bidImpId,
                 .orElse(null);
 
         if (bidImpVideo == null) {
-            return null;
+            return ImpMediaType.video_outstream;
         }
 
         final Integer placement = bidImpVideo.getPlacement();
-        return placement == null || Objects.equals(placement, 1)
-                ? ImpMediaType.video
+        final Integer plcmt = bidImpVideo.getPlcmt();
+
+        return Objects.equals(placement, 1) || Objects.equals(plcmt, 1)
+                ? ImpMediaType.video_instream
                 : ImpMediaType.video_outstream;
     }
 }
diff --git a/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java b/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java
index 35668bbf0f4..07ac2ce7015 100644
--- a/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java
+++ b/src/main/java/org/prebid/server/auction/OrtbTypesResolver.java
@@ -1,5 +1,6 @@
 package org.prebid.server.auction;
 
+import com.fasterxml.jackson.core.JsonPointer;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.node.ArrayNode;
@@ -14,19 +15,18 @@
 import org.prebid.server.log.ConditionalLogger;
 import org.prebid.server.log.Logger;
 import org.prebid.server.log.LoggerFactory;
+import org.prebid.server.util.StreamUtil;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
-import java.util.function.Supplier;
+import java.util.function.BiFunction;
+import java.util.function.Function;
 import java.util.stream.Collectors;
-import java.util.stream.StreamSupport;
 
 /**
  * Service resolves types inconsistency and cast them if possible to ortb2 protocol.
@@ -40,36 +40,32 @@ public class OrtbTypesResolver {
     private static final String USER = "user";
     private static final String APP = "app";
     private static final String SITE = "site";
+    private static final String EXT = "ext";
+    private static final String DATA = "data";
+    private static final String CONFIG = "config";
+    private static final String FPD = "fpd";
+    private static final String ORTB2 = "ortb2";
     private static final String CONTEXT = "context";
-    private static final String BIDREQUEST = "bidrequest";
-    private static final String TARGETING = "targeting";
     private static final String UNKNOWN_REFERER = "unknown referer";
-    private static final String DATA = "data";
-    private static final String EXT = "ext";
 
-    private static final Map<String, Set<String>> FIRST_ARRAY_ELEMENT_STANDARD_FIELDS;
-    private static final Map<String, Set<String>> FIRST_ARRAY_ELEMENT_REQUEST_FIELDS;
+    private static final JsonPointer EXT_PREBID_BIDDER_CONFIG = JsonPointer.valueOf("/ext/prebid/bidderconfig");
+    private static final JsonPointer CONFIG_ORTB2 = JsonPointer.valueOf("/config/ortb2");
+    private static final JsonPointer APP_BUNDLE = JsonPointer.valueOf("/app/bundle");
+    private static final JsonPointer SITE_PAGE = JsonPointer.valueOf("/site/page");
+
+    private static final Map<String, Set<String>> FIRST_ARRAY_ELEMENT_FIELDS;
     private static final Map<String, Set<String>> COMMA_SEPARATED_ELEMENT_FIELDS;
 
     static {
-        FIRST_ARRAY_ELEMENT_REQUEST_FIELDS = new HashMap<>();
-        FIRST_ARRAY_ELEMENT_REQUEST_FIELDS.put(USER, new HashSet<>(Collections.singleton("gender")));
-        FIRST_ARRAY_ELEMENT_REQUEST_FIELDS.put(APP, new HashSet<>(Arrays.asList("id", "name", "bundle", "storeurl",
-                "domain")));
-        FIRST_ARRAY_ELEMENT_REQUEST_FIELDS.put(SITE, new HashSet<>(Arrays.asList("id", "name", "domain", "page",
-                "ref", "search")));
-
-        FIRST_ARRAY_ELEMENT_STANDARD_FIELDS = new HashMap<>();
-        FIRST_ARRAY_ELEMENT_STANDARD_FIELDS.put(USER, new HashSet<>(Collections.singleton("gender")));
-        FIRST_ARRAY_ELEMENT_STANDARD_FIELDS.put(APP, new HashSet<>(Arrays.asList("name", "bundle", "storeurl",
-                "domain")));
-        FIRST_ARRAY_ELEMENT_STANDARD_FIELDS.put(SITE, new HashSet<>(Arrays.asList("name", "domain", "page", "ref",
-                "search")));
-
-        COMMA_SEPARATED_ELEMENT_FIELDS = new HashMap<>();
-        COMMA_SEPARATED_ELEMENT_FIELDS.put(USER, Collections.singleton("keywords"));
-        COMMA_SEPARATED_ELEMENT_FIELDS.put(APP, Collections.singleton("keywords"));
-        COMMA_SEPARATED_ELEMENT_FIELDS.put(SITE, Collections.singleton("keywords"));
+        FIRST_ARRAY_ELEMENT_FIELDS = Map.of(
+                USER, Collections.singleton("gender"),
+                APP, Set.of("id", "name", "bundle", "storeurl", "domain"),
+                SITE, Set.of("id", "name", "domain", "page", "ref", "search"));
+
+        COMMA_SEPARATED_ELEMENT_FIELDS = Map.of(
+                USER, Collections.singleton("keywords"),
+                APP, Collections.singleton("keywords"),
+                SITE, Collections.singleton("keywords"));
     }
 
     private final double logSamplingRate;
@@ -83,291 +79,306 @@ public OrtbTypesResolver(double logSamplingRate, JacksonMapper jacksonMapper, Js
         this.jsonMerger = Objects.requireNonNull(jsonMerger);
     }
 
-    /**
-     * Resolves fields types inconsistency to ortb2 protocol for {@param bidRequest} for bidRequest level parameters
-     * and bidderconfig.
-     * Mutates both parameters, {@param fpdContainerNode} and {@param warnings}.
-     */
     public void normalizeBidRequest(JsonNode bidRequest, List<String> warnings, String referer) {
         final List<String> resolverWarnings = new ArrayList<>();
-        final String rowOriginBidRequest = getOriginalRowContainerNode(bidRequest);
-        normalizeRequestFpdFields(bidRequest, resolverWarnings);
-        final JsonNode bidderConfigs = bidRequest.path("ext").path("prebid").path("bidderconfig");
+
+        normalizeFpdFields(bidRequest, "bidrequest.", resolverWarnings);
+
+        final String source = source(bidRequest);
+        final JsonNode bidderConfigs = bidRequest.at(EXT_PREBID_BIDDER_CONFIG);
         if (!bidderConfigs.isMissingNode() && bidderConfigs.isArray()) {
             for (JsonNode bidderConfig : bidderConfigs) {
+                mergeFpdFieldsToOrtb2(bidderConfig, source);
 
-                mergeFpdFieldsToOrtb2(bidderConfig);
-
-                final JsonNode ortb2Config = bidderConfig.path("config").path("ortb2");
+                final JsonNode ortb2Config = bidderConfig.at(CONFIG_ORTB2);
                 if (!ortb2Config.isMissingNode()) {
-                    normalizeStandardFpdFields(ortb2Config, resolverWarnings, "bidrequest.ext.prebid.bidderconfig");
+                    normalizeFpdFields(ortb2Config, "bidrequest.ext.prebid.bidderconfig.", resolverWarnings);
                 }
             }
         }
-        processWarnings(resolverWarnings, warnings, rowOriginBidRequest, referer, BIDREQUEST);
+
+        processWarnings(resolverWarnings, warnings, referer, "bidrequest", getOriginalRowContainerNode(bidRequest));
     }
 
-    private String getOriginalRowContainerNode(JsonNode bidRequest) {
-        try {
-            return jacksonMapper.mapper().writeValueAsString(bidRequest);
-        } catch (JsonProcessingException e) {
-            // should never happen
-            throw new InvalidRequestException("Failed to decode container node to string");
+    private void normalizeFpdFields(JsonNode fpdContainerNode, String prefix, List<String> warnings) {
+        if (fpdContainerNode != null && fpdContainerNode.isObject()) {
+            final ObjectNode fpdContainerObjectNode = (ObjectNode) fpdContainerNode;
+            updateFpdWithNormalizedNode(fpdContainerObjectNode, USER, warnings, prefix);
+            updateFpdWithNormalizedNode(fpdContainerObjectNode, APP, warnings, prefix);
+            updateFpdWithNormalizedNode(fpdContainerObjectNode, SITE, warnings, prefix);
         }
     }
 
-    /**
-     * Merges fpd fields into ortb2:
-     * config.fpd.context -> config.ortb2.site
-     * config.fpd.user -> config.ortb2.user
-     */
-    private void mergeFpdFieldsToOrtb2(JsonNode bidderConfig) {
-        final JsonNode config = bidderConfig.path("config");
-        final JsonNode configFpd = config.path("fpd");
-
-        if (configFpd.isMissingNode()) {
-            return;
-        }
+    private static String source(JsonNode bidRequest) {
+        return Optional.ofNullable(stringAt(bidRequest, APP_BUNDLE))
+                .orElseGet(() -> stringAt(bidRequest, SITE_PAGE));
+    }
 
-        final JsonNode configOrtb = config.path("ortb2");
+    private static String stringAt(JsonNode node, JsonPointer path) {
+        final JsonNode at = node.at(path);
+        return at.isMissingNode() || at.isNull() || !at.isTextual()
+                ? null
+                : at.textValue();
+    }
 
-        final JsonNode fpdContext = configFpd.get(CONTEXT);
-        final JsonNode ortbSite = configOrtb.get(SITE);
-        final JsonNode updatedOrtbSite = ortbSite == null
-                ? fpdContext
-                : fpdContext != null ? jsonMerger.merge(fpdContext, ortbSite) : null;
+    private void updateFpdWithNormalizedNode(ObjectNode containerNode,
+                                             String nodeNameToNormalize,
+                                             List<String> warnings,
+                                             String nodePrefix) {
+
+        updateWithNormalizedNode(
+                containerNode,
+                nodeNameToNormalize,
+                normalizeNode(
+                        containerNode.get(nodeNameToNormalize),
+                        nodeNameToNormalize,
+                        warnings,
+                        nodePrefix));
+    }
 
-        final JsonNode fpdUser = configFpd.get(USER);
-        final JsonNode ortbUser = configOrtb.get(USER);
-        final JsonNode updatedOrtbUser = ortbUser == null
-                ? fpdUser
-                : fpdUser != null ? jsonMerger.merge(fpdUser, ortbUser) : null;
+    private static void updateWithNormalizedNode(ObjectNode containerNode,
+                                                 String fieldName,
+                                                 JsonNode normalizedNode) {
 
-        if (updatedOrtbUser == null && updatedOrtbSite == null) {
-            return;
+        if (normalizedNode == null) {
+            containerNode.remove(fieldName);
+        } else {
+            containerNode.set(fieldName, normalizedNode);
         }
+    }
 
-        final ObjectNode ortbObjectNode = configOrtb.isMissingNode()
-                ? jacksonMapper.mapper().createObjectNode()
-                : (ObjectNode) configOrtb;
-
-        if (updatedOrtbSite != null) {
-            ortbObjectNode.set(SITE, updatedOrtbSite);
+    private JsonNode normalizeNode(JsonNode containerNode, String nodeName, List<String> warnings, String nodePrefix) {
+        if (containerNode == null) {
+            return null;
         }
+        if (!containerNode.isObject()) {
+            warnings.add("%s%s field ignored. Expected type is object, but was `%s`."
+                    .formatted(nodePrefix, nodeName, containerNode.getNodeType().name()));
 
-        if (updatedOrtbUser != null) {
-            ortbObjectNode.set(USER, updatedOrtbUser);
+            return null;
         }
 
-        ((ObjectNode) config).set("ortb2", ortbObjectNode);
-    }
+        final ObjectNode containerObjectNode = (ObjectNode) containerNode;
 
-    /**
-     * Resolves fields types inconsistency to ortb2 protocol for {@param targeting}.
-     * Mutates both parameters, {@param targeting} and {@param warnings}.
-     */
-    public void normalizeTargeting(JsonNode targeting, List<String> warnings, String referer) {
-        final List<String> resolverWarnings = new ArrayList<>();
-        final String rowOriginTargeting = getOriginalRowContainerNode(targeting);
-        normalizeStandardFpdFields(targeting, resolverWarnings, TARGETING);
-        processWarnings(resolverWarnings, warnings, rowOriginTargeting, referer, TARGETING);
-    }
+        normalizeFields(
+                FIRST_ARRAY_ELEMENT_FIELDS,
+                nodeName,
+                containerObjectNode,
+                (name, node) -> toFirstElementTextNode(name, node, warnings, nodePrefix, nodeName));
+        normalizeFields(
+                COMMA_SEPARATED_ELEMENT_FIELDS,
+                nodeName,
+                containerObjectNode,
+                (name, node) -> toCommaSeparatedTextNode(name, node, warnings, nodePrefix, nodeName));
 
-    /**
-     * Resolves fields types inconsistency to ortb2 protocol for {@param fpdContainerNode}.
-     * Mutates both parameters, {@param fpdContainerNode} and {@param warnings}.
-     */
-    private void normalizeStandardFpdFields(JsonNode fpdContainerNode, List<String> warnings, String nodePrefix) {
-        final String normalizedNodePrefix = nodePrefix.endsWith(".") ? nodePrefix : nodePrefix.concat(".");
-        if (fpdContainerNode != null && fpdContainerNode.isObject()) {
-            final ObjectNode fpdContainerObjectNode = (ObjectNode) fpdContainerNode;
-            updateWithNormalizedNode(fpdContainerObjectNode, USER, FIRST_ARRAY_ELEMENT_STANDARD_FIELDS,
-                    COMMA_SEPARATED_ELEMENT_FIELDS, normalizedNodePrefix, warnings);
-            updateWithNormalizedNode(fpdContainerObjectNode, APP, FIRST_ARRAY_ELEMENT_STANDARD_FIELDS,
-                    COMMA_SEPARATED_ELEMENT_FIELDS, normalizedNodePrefix, warnings);
-            updateWithNormalizedNode(fpdContainerObjectNode, SITE, FIRST_ARRAY_ELEMENT_STANDARD_FIELDS,
-                    COMMA_SEPARATED_ELEMENT_FIELDS, normalizedNodePrefix, warnings);
-        }
-    }
+        normalizeDataExtension(containerObjectNode, warnings, nodePrefix, nodeName);
 
-    private void normalizeRequestFpdFields(JsonNode fpdContainerNode, List<String> warnings) {
-        if (fpdContainerNode != null && fpdContainerNode.isObject()) {
-            final ObjectNode fpdContainerObjectNode = (ObjectNode) fpdContainerNode;
-            final String bidRequestPrefix = BIDREQUEST + ".";
-            updateWithNormalizedNode(fpdContainerObjectNode, USER, FIRST_ARRAY_ELEMENT_REQUEST_FIELDS,
-                    COMMA_SEPARATED_ELEMENT_FIELDS, bidRequestPrefix, warnings);
-            updateWithNormalizedNode(fpdContainerObjectNode, APP, FIRST_ARRAY_ELEMENT_REQUEST_FIELDS,
-                    COMMA_SEPARATED_ELEMENT_FIELDS, bidRequestPrefix, warnings);
-            updateWithNormalizedNode(fpdContainerObjectNode, SITE, FIRST_ARRAY_ELEMENT_REQUEST_FIELDS,
-                    COMMA_SEPARATED_ELEMENT_FIELDS, bidRequestPrefix, warnings);
-        }
+        return containerNode;
     }
 
-    private void updateWithNormalizedNode(ObjectNode containerNode, String nodeNameToNormalize,
-                                          Map<String, Set<String>> firstArrayElementsFields,
-                                          Map<String, Set<String>> commaSeparatedElementFields,
-                                          String nodePrefix, List<String> warnings) {
-        final JsonNode normalizedNode = normalizeNode(containerNode.get(nodeNameToNormalize), nodeNameToNormalize,
-                firstArrayElementsFields, commaSeparatedElementFields, nodePrefix, warnings);
-        if (normalizedNode != null) {
-            containerNode.set(nodeNameToNormalize, normalizedNode);
-        } else {
-            containerNode.remove(nodeNameToNormalize);
-        }
-    }
+    private static void normalizeFields(Map<String, Set<String>> nodeNameToFields,
+                                        String nodeName,
+                                        ObjectNode containerObjectNode,
+                                        BiFunction<String, JsonNode, JsonNode> fieldNormalizer) {
 
-    private JsonNode normalizeNode(JsonNode containerNode, String nodeName,
-                                   Map<String, Set<String>> firstArrayElementsFields,
-                                   Map<String, Set<String>> commaSeparatedElementFields,
-                                   String nodePrefix, List<String> warnings) {
-        if (containerNode != null) {
-            if (containerNode.isObject()) {
-                final ObjectNode containerObjectNode = (ObjectNode) containerNode;
-
-                CollectionUtils.emptyIfNull(firstArrayElementsFields.get(nodeName))
-                        .forEach(fieldName -> updateWithNormalizedField(containerObjectNode, fieldName,
-                                () -> toFirstElementTextNode(containerObjectNode, fieldName, nodeName, nodePrefix,
-                                        warnings)));
-
-                CollectionUtils.emptyIfNull(commaSeparatedElementFields.get(nodeName))
-                        .forEach(fieldName -> updateWithNormalizedField(containerObjectNode, fieldName,
-                                () -> toCommaSeparatedTextNode(containerObjectNode, fieldName, nodeName, nodePrefix,
-                                        warnings)));
-
-                normalizeDataExtension(containerObjectNode, nodeName, nodePrefix, warnings);
-            } else {
-                warnings.add("%s%s field ignored. Expected type is object, but was `%s`."
-                        .formatted(nodePrefix, nodeName, containerNode.getNodeType().name()));
-                return null;
-            }
-        }
-        return containerNode;
+        nodeNameToFields.get(nodeName)
+                .forEach(fieldName -> updateWithNormalizedNode(
+                        containerObjectNode,
+                        fieldName,
+                        fieldNormalizer.apply(fieldName, containerObjectNode.get(fieldName))));
     }
 
-    private void updateWithNormalizedField(ObjectNode containerNode, String fieldName,
-                                           Supplier<JsonNode> normalizationSupplier) {
-        final JsonNode normalizedField = normalizationSupplier.get();
-        if (normalizedField == null) {
-            containerNode.remove(fieldName);
-        } else {
-            containerNode.set(fieldName, normalizedField);
-        }
+    private static TextNode toFirstElementTextNode(String fieldName,
+                                                   JsonNode fieldNode,
+                                                   List<String> warnings,
+                                                   String nodePrefix,
+                                                   String containerName) {
+
+        return toTextNode(
+                fieldName,
+                fieldNode,
+                arrayNode -> arrayNode.get(0).asText(),
+                warnings,
+                nodePrefix,
+                containerName,
+                "Converted to string by taking first element of array.");
     }
 
-    private JsonNode toFirstElementTextNode(ObjectNode containerNode,
-                                            String fieldName,
-                                            String containerName,
-                                            String nodePrefix,
-                                            List<String> warnings) {
+    private static TextNode toTextNode(String fieldName,
+                                       JsonNode fieldNode,
+                                       Function<ArrayNode, String> mapper,
+                                       List<String> warnings,
+                                       String nodePrefix,
+                                       String containerName,
+                                       String action) {
 
-        final JsonNode node = containerNode.get(fieldName);
-        if (node == null || node.isNull() || node.isTextual()) {
-            return node;
+        if (fieldNode == null || fieldNode.isNull()) {
+            return null;
         }
 
-        final boolean isArray = node.isArray();
-        final ArrayNode arrayNode = isArray ? (ArrayNode) node : null;
-        final boolean isTextualArray = arrayNode != null && isTextualArray(arrayNode) && !arrayNode.isEmpty();
+        if (fieldNode.isTextual()) {
+            return (TextNode) fieldNode;
+        }
+
+        final ArrayNode arrayNode = fieldNode.isArray() ? (ArrayNode) fieldNode : null;
+        final boolean isTextualArray = arrayNode != null && !arrayNode.isEmpty() && isTextualArray(arrayNode);
 
-        if (isTextualArray && !arrayNode.isEmpty()) {
+        if (isTextualArray) {
             warnings.add("""
                     Incorrect type for first party data field %s%s.%s, expected is string, \
-                    but was an array of strings. Converted to string by taking first element of array."""
-                    .formatted(nodePrefix, containerName, fieldName));
-            return new TextNode(arrayNode.get(0).asText());
+                    but was an array of strings. %s"""
+                    .formatted(nodePrefix, containerName, fieldName, action));
+
+            return new TextNode(mapper.apply(arrayNode));
         } else {
-            warnForExpectedStringArrayType(fieldName, containerName, warnings, nodePrefix, node.getNodeType());
+            warnForExpectedStringArrayType(warnings, nodePrefix, containerName, fieldName, fieldNode.getNodeType());
             return null;
         }
     }
 
-    private JsonNode toCommaSeparatedTextNode(ObjectNode containerNode,
-                                              String fieldName,
-                                              String containerName,
-                                              String nodePrefix,
-                                              List<String> warnings) {
+    private static boolean isTextualArray(ArrayNode arrayNode) {
+        return StreamUtil.asStream(arrayNode.iterator()).allMatch(JsonNode::isTextual);
+    }
 
-        final JsonNode node = containerNode.get(fieldName);
-        if (node == null || node.isNull() || node.isTextual()) {
-            return node;
-        }
+    private static void warnForExpectedStringArrayType(List<String> warnings,
+                                                       String nodePrefix,
+                                                       String containerName,
+                                                       String fieldName,
+                                                       JsonNodeType nodeType) {
 
-        final boolean isArray = node.isArray();
-        final ArrayNode arrayNode = isArray ? (ArrayNode) node : null;
-        final boolean isTextualArray = arrayNode != null && isTextualArray(arrayNode) && !arrayNode.isEmpty();
+        warnings.add("""
+                Incorrect type for first party data field %s%s.%s, expected strings, \
+                but was `%s`. Failed to convert to correct type.""".formatted(
+                nodePrefix,
+                containerName,
+                fieldName,
+                nodeType == JsonNodeType.ARRAY ? "ARRAY of different types" : nodeType.name()));
+    }
 
-        if (isTextualArray) {
-            warnings.add("""
-                    Incorrect type for first party data field %s%s.%s, expected is string, \
-                    but was an array of strings. Converted to string by separating values with comma."""
-                    .formatted(nodePrefix, containerName, fieldName));
+    private static TextNode toCommaSeparatedTextNode(String fieldName,
+                                                     JsonNode fieldNode,
+                                                     List<String> warnings,
+                                                     String nodePrefix,
+                                                     String containerName) {
 
-            return new TextNode(StreamSupport.stream(arrayNode.spliterator(), false)
-                    .map(jsonNode -> (TextNode) jsonNode)
-                    .map(TextNode::textValue)
-                    .collect(Collectors.joining(",")));
-        } else {
-            warnForExpectedStringArrayType(fieldName, containerName, warnings, nodePrefix, node.getNodeType());
-            return null;
-        }
+        return toTextNode(
+                fieldName,
+                fieldNode,
+                arrayNode -> StreamUtil.asStream(arrayNode.spliterator())
+                        .map(TextNode.class::cast)
+                        .map(TextNode::textValue)
+                        .collect(Collectors.joining(",")),
+                warnings,
+                nodePrefix,
+                containerName,
+                "Converted to string by separating values with comma.");
     }
 
-    private void normalizeDataExtension(ObjectNode containerNode, String containerName, String nodePrefix,
-                                        List<String> warnings) {
+    private void normalizeDataExtension(ObjectNode containerNode,
+                                        List<String> warnings,
+                                        String nodePrefix,
+                                        String containerName) {
+
         final JsonNode data = containerNode.get(DATA);
         if (data == null || !data.isObject()) {
             return;
         }
+
         final JsonNode extData = containerNode.path(EXT).path(DATA);
         final JsonNode ext = containerNode.get(EXT);
         if (!extData.isNull() && !extData.isMissingNode()) {
             final JsonNode resolvedExtData = jsonMerger.merge(data, extData);
             ((ObjectNode) ext).set(DATA, resolvedExtData);
         } else {
-            copyDataToExtData(containerNode, containerName, nodePrefix, warnings, data);
+            copyDataToExtData(containerNode, data, warnings, nodePrefix, containerName);
         }
+
         containerNode.remove(DATA);
     }
 
-    private void copyDataToExtData(ObjectNode containerNode, String containerName, String nodePrefix,
-                                   List<String> warnings, JsonNode data) {
+    private void copyDataToExtData(ObjectNode containerNode,
+                                   JsonNode data,
+                                   List<String> warnings,
+                                   String nodePrefix,
+                                   String containerName) {
+
         final JsonNode ext = containerNode.get(EXT);
-        if (ext != null && ext.isObject()) {
-            ((ObjectNode) ext).set(DATA, data);
-        } else if (ext != null && !ext.isObject()) {
+        if (ext == null) {
+            createExtAndCopyData(containerNode, data);
+        } else if (!ext.isObject()) {
             warnings.add("""
                     Incorrect type for first party data field %s%s.%s, \
                     expected is object, but was %s. Replaced with object"""
                     .formatted(nodePrefix, containerName, EXT, ext.getNodeType()));
-            containerNode.set(EXT, jacksonMapper.mapper().createObjectNode().set(DATA, data));
+            createExtAndCopyData(containerNode, data);
         } else {
-            containerNode.set(EXT, jacksonMapper.mapper().createObjectNode().set(DATA, data));
+            ((ObjectNode) ext).set(DATA, data);
         }
     }
 
-    private void warnForExpectedStringArrayType(String fieldName, String containerName, List<String> warnings,
-                                                String nodePrefix, JsonNodeType nodeType) {
-        warnings.add("""
-                Incorrect type for first party data field %s%s.%s, expected strings, \
-                but was `%s`. Failed to convert to correct type.""".formatted(
-                nodePrefix,
-                containerName,
-                fieldName,
-                nodeType == JsonNodeType.ARRAY ? "ARRAY of different types" : nodeType.name()));
+    private void createExtAndCopyData(ObjectNode containerNode, JsonNode data) {
+        containerNode.set(EXT, jacksonMapper.mapper().createObjectNode().set(DATA, data));
     }
 
-    private static boolean isTextualArray(ArrayNode arrayNode) {
-        return StreamSupport.stream(arrayNode.spliterator(), false).allMatch(JsonNode::isTextual);
+    private void mergeFpdFieldsToOrtb2(JsonNode bidderConfig, String source) {
+        final JsonNode config = bidderConfig.path(CONFIG);
+        final JsonNode configFpd = config.path(FPD);
+
+        if (configFpd.isMissingNode()) {
+            return;
+        }
+
+        logDeprecatedFpdConfig(source);
+
+        final JsonNode configOrtb = config.path(ORTB2);
+        final JsonNode updatedOrtbSite = updatedOrtb2Node(configFpd, CONTEXT, configOrtb, SITE);
+        final JsonNode updatedOrtbUser = updatedOrtb2Node(configFpd, USER, configOrtb, USER);
+
+        if (updatedOrtbUser == null && updatedOrtbSite == null) {
+            return;
+        }
+
+        final ObjectNode ortbObjectNode = configOrtb.isMissingNode()
+                ? jacksonMapper.mapper().createObjectNode()
+                : (ObjectNode) configOrtb;
+
+        setIfNotNull(ortbObjectNode, SITE, updatedOrtbSite);
+        setIfNotNull(ortbObjectNode, USER, updatedOrtbUser);
+
+        ((ObjectNode) config).set(ORTB2, ortbObjectNode);
+    }
+
+    private void logDeprecatedFpdConfig(String source) {
+        final String messagePart = source != null ? " on " + source : StringUtils.EMPTY;
+        ORTB_TYPES_RESOLVING_LOGGER.warn("Usage of deprecated FPD config path" + messagePart, logSamplingRate);
+    }
+
+    private JsonNode updatedOrtb2Node(JsonNode configFpd, String fpdField, JsonNode configOrtb, String ortbField) {
+        final JsonNode fpdNode = configFpd.get(fpdField);
+        final JsonNode ortbNode = configOrtb.get(ortbField);
+        return ortbNode == null
+                ? fpdNode
+                : fpdNode != null ? jsonMerger.merge(ortbNode, fpdNode) : null;
+    }
+
+    private static void setIfNotNull(ObjectNode destination, String fieldName, JsonNode data) {
+        if (data != null) {
+            destination.set(fieldName, data);
+        }
     }
 
-    private void processWarnings(List<String> resolverWarning, List<String> warnings, String containerValue,
-                                 String referer, String containerName) {
-        if (CollectionUtils.isNotEmpty(resolverWarning)) {
-            warnings.addAll(updateWithWarningPrefix(resolverWarning));
-            // log only 1% of cases
+    private void processWarnings(List<String> resolverWarnings,
+                                 List<String> warnings,
+                                 String referer,
+                                 String containerName,
+                                 String containerValue) {
+
+        if (CollectionUtils.isNotEmpty(resolverWarnings)) {
+            warnings.addAll(updateWithWarningPrefix(resolverWarnings));
+
             ORTB_TYPES_RESOLVING_LOGGER.warn(
                     "WARNINGS: %s. \n Referer = %s and %s = %s".formatted(
-                            String.join("\n", resolverWarning),
+                            String.join("\n", resolverWarnings),
                             StringUtils.isNotBlank(referer) ? referer : UNKNOWN_REFERER,
                             containerName,
                             containerValue),
@@ -375,7 +386,22 @@ private void processWarnings(List<String> resolverWarning, List<String> warnings
         }
     }
 
-    private List<String> updateWithWarningPrefix(List<String> resolverWarning) {
+    private static List<String> updateWithWarningPrefix(List<String> resolverWarning) {
         return resolverWarning.stream().map(warning -> "WARNING: " + warning).toList();
     }
+
+    private String getOriginalRowContainerNode(JsonNode bidRequest) {
+        try {
+            return jacksonMapper.mapper().writeValueAsString(bidRequest);
+        } catch (JsonProcessingException e) {
+            // should never happen
+            throw new InvalidRequestException("Failed to decode container node to string");
+        }
+    }
+
+    public void normalizeTargeting(JsonNode targeting, List<String> warnings, String referer) {
+        final List<String> resolverWarnings = new ArrayList<>();
+        normalizeFpdFields(targeting, "targeting.", resolverWarnings);
+        processWarnings(resolverWarnings, warnings, referer, "targeting", getOriginalRowContainerNode(targeting));
+    }
 }
diff --git a/src/main/java/org/prebid/server/auction/PriceGranularity.java b/src/main/java/org/prebid/server/auction/PriceGranularity.java
index 81cec03fd62..75620e4d953 100644
--- a/src/main/java/org/prebid/server/auction/PriceGranularity.java
+++ b/src/main/java/org/prebid/server/auction/PriceGranularity.java
@@ -72,6 +72,12 @@ public static PriceGranularity createFromString(String stringPriceGranularity) {
         }
     }
 
+    public static PriceGranularity createFromStringOrDefault(String stringPriceGranularity) {
+        return isValidStringPriceGranularityType(stringPriceGranularity)
+                ? STRING_TO_CUSTOM_PRICE_GRANULARITY.get(PriceGranularityType.valueOf(stringPriceGranularity))
+                : PriceGranularity.DEFAULT;
+    }
+
     /**
      * Returns list of {@link ExtGranularityRange}s.
      */
diff --git a/src/main/java/org/prebid/server/auction/SkippedAuctionService.java b/src/main/java/org/prebid/server/auction/SkippedAuctionService.java
index dd8c95c0f50..e833b317cc2 100644
--- a/src/main/java/org/prebid/server/auction/SkippedAuctionService.java
+++ b/src/main/java/org/prebid/server/auction/SkippedAuctionService.java
@@ -8,7 +8,7 @@
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.StoredResponseResult;
 import org.prebid.server.exception.InvalidRequestException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
 import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse;
diff --git a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java
index f982870c049..3729c5da661 100644
--- a/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java
+++ b/src/main/java/org/prebid/server/auction/StoredRequestProcessor.java
@@ -12,8 +12,8 @@
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.InvalidStoredImpException;
 import org.prebid.server.exception.InvalidStoredRequestException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.identity.IdGenerator;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.json.JsonMerger;
diff --git a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java
index b769d2974b1..1f5b0d83258 100644
--- a/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java
+++ b/src/main/java/org/prebid/server/auction/StoredResponseProcessor.java
@@ -21,7 +21,7 @@
 import org.prebid.server.bidder.model.BidderSeatBid;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.proto.openrtb.ext.request.ExtImp;
 import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
diff --git a/src/main/java/org/prebid/server/auction/TimeoutResolver.java b/src/main/java/org/prebid/server/auction/TimeoutResolver.java
index 4b9d2411eb1..44ae2984aa1 100644
--- a/src/main/java/org/prebid/server/auction/TimeoutResolver.java
+++ b/src/main/java/org/prebid/server/auction/TimeoutResolver.java
@@ -32,16 +32,16 @@ public long limitToMax(Long timeout) {
                 : Math.min(timeout, maxTimeout);
     }
 
-    public long adjustForBidder(long timeout, int adjustFactor, long spentTime) {
-        return adjustWithFactor(timeout, adjustFactor / 100.0, spentTime);
+    public long adjustForBidder(long timeout, int adjustFactor, long spentTime, long bidderTmaxDeductionMs) {
+        return adjustWithFactor(timeout, adjustFactor / 100.0, spentTime, bidderTmaxDeductionMs);
     }
 
     public long adjustForRequest(long timeout, long spentTime) {
-        return adjustWithFactor(timeout, 1.0, spentTime);
+        return adjustWithFactor(timeout, 1.0, spentTime, 0L);
     }
 
-    private long adjustWithFactor(long timeout, double adjustFactor, long spentTime) {
-        return limitToMin((long) (timeout * adjustFactor) - spentTime - upstreamResponseTime);
+    private long adjustWithFactor(long timeout, double adjustFactor, long spentTime, long deductionTime) {
+        return limitToMin((long) (timeout * adjustFactor) - spentTime - deductionTime - upstreamResponseTime);
     }
 
     private long limitToMin(long timeout) {
diff --git a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java
index 84ebacc7416..f6bcb5599af 100644
--- a/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java
+++ b/src/main/java/org/prebid/server/auction/VideoStoredRequestProcessor.java
@@ -24,7 +24,7 @@
 import org.prebid.server.auction.model.Tuple2;
 import org.prebid.server.auction.model.WithPodErrors;
 import org.prebid.server.exception.InvalidRequestException;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.json.JsonMerger;
 import org.prebid.server.log.Logger;
diff --git a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java
index b13cf522b49..e9a7b7818a1 100644
--- a/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java
+++ b/src/main/java/org/prebid/server/auction/categorymapping/BasicCategoryMappingService.java
@@ -28,7 +28,7 @@
 import org.prebid.server.bidder.model.BidderSeatBid;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory;
 import org.prebid.server.proto.openrtb.ext.request.ExtDealTier;
diff --git a/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java
index 088a9604b9d..2c3b3f369b0 100644
--- a/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java
+++ b/src/main/java/org/prebid/server/auction/categorymapping/CategoryMappingService.java
@@ -4,7 +4,7 @@
 import io.vertx.core.Future;
 import org.prebid.server.auction.model.BidderResponse;
 import org.prebid.server.auction.model.CategoryMappingResult;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 
 import java.util.List;
 
diff --git a/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java b/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java
index f6161fa6f90..88ac988c521 100644
--- a/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java
+++ b/src/main/java/org/prebid/server/auction/categorymapping/NoOpCategoryMappingService.java
@@ -4,7 +4,7 @@
 import io.vertx.core.Future;
 import org.prebid.server.auction.model.BidderResponse;
 import org.prebid.server.auction.model.CategoryMappingResult;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 
 import java.util.List;
 
diff --git a/src/main/java/org/prebid/server/auction/model/AuctionContext.java b/src/main/java/org/prebid/server/auction/model/AuctionContext.java
index 3ee60aab4fa..5dbe83c3ff2 100644
--- a/src/main/java/org/prebid/server/auction/model/AuctionContext.java
+++ b/src/main/java/org/prebid/server/auction/model/AuctionContext.java
@@ -8,6 +8,7 @@
 import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
 import org.prebid.server.auction.gpp.model.GppContext;
 import org.prebid.server.auction.model.debug.DebugContext;
+import org.prebid.server.bidadjustments.model.BidAdjustments;
 import org.prebid.server.cache.model.DebugHttpCall;
 import org.prebid.server.cookie.UidsCookie;
 import org.prebid.server.geolocation.model.GeoInfo;
@@ -17,6 +18,7 @@
 import org.prebid.server.privacy.model.PrivacyContext;
 import org.prebid.server.settings.model.Account;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
@@ -71,6 +73,10 @@ public class AuctionContext {
 
     CachedDebugLog cachedDebugLog;
 
+    @JsonIgnore
+    @Builder.Default
+    BidAdjustments bidAdjustments = BidAdjustments.of(Collections.emptyMap());
+
     public AuctionContext with(Account account) {
         return this.toBuilder().account(account).build();
     }
@@ -124,6 +130,12 @@ public AuctionContext with(GeoInfo geoInfo) {
                 .build();
     }
 
+    public AuctionContext with(BidAdjustments bidAdjustments) {
+        return this.toBuilder()
+                .bidAdjustments(bidAdjustments)
+                .build();
+    }
+
     public AuctionContext withRequestRejected() {
         return this.toBuilder()
                 .requestRejected(true)
diff --git a/src/main/java/org/prebid/server/auction/model/BidInfo.java b/src/main/java/org/prebid/server/auction/model/BidInfo.java
index 1cb95bcf681..aa3be49fd48 100644
--- a/src/main/java/org/prebid/server/auction/model/BidInfo.java
+++ b/src/main/java/org/prebid/server/auction/model/BidInfo.java
@@ -33,7 +33,7 @@ public class BidInfo {
 
     Integer ttl;
 
-    Integer videoTtl;
+    Integer vastTtl;
 
     public String getBidId() {
         final ObjectNode extNode = bid != null ? bid.getExt() : null;
diff --git a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java
index e916ee0b3a5..fc3ee36bd2a 100644
--- a/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java
+++ b/src/main/java/org/prebid/server/auction/model/BidRejectionReason.java
@@ -74,6 +74,11 @@ public enum BidRejectionReason {
      */
     RESPONSE_REJECTED_BELOW_FLOOR(301),
 
+    /**
+     * The bidder returns a bid that doesn't meet the price deal floor.
+     */
+    RESPONSE_REJECTED_BELOW_DEAL_FLOOR(304),
+
     /**
      * Rejected by the DSA validations
      */
@@ -101,14 +106,14 @@ public enum BidRejectionReason {
      */
     RESPONSE_REJECTED_ADVERTISER_BLOCKED(356);
 
-    public final int code;
+    private final int code;
 
     BidRejectionReason(int code) {
         this.code = code;
     }
 
     @JsonValue
-    private int getValue() {
+    public int getValue() {
         return code;
     }
 
diff --git a/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java b/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java
index b0504ec13a3..9810606ada3 100644
--- a/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java
+++ b/src/main/java/org/prebid/server/auction/model/BidRejectionTracker.java
@@ -1,90 +1,162 @@
 package org.prebid.server.auction.model;
 
 import com.iab.openrtb.response.Bid;
+import org.apache.commons.lang3.tuple.Pair;
 import org.prebid.server.bidder.model.BidderBid;
 import org.prebid.server.log.ConditionalLogger;
 import org.prebid.server.log.Logger;
 import org.prebid.server.log.LoggerFactory;
 import org.prebid.server.util.MapUtil;
 
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
 public class BidRejectionTracker {
 
     private static final Logger logger = LoggerFactory.getLogger(BidRejectionTracker.class);
 
-    private static final ConditionalLogger MULTIPLE_BID_REJECTIONS_LOGGER =
+    private static final ConditionalLogger BID_REJECTIONS_LOGGER =
             new ConditionalLogger("multiple-bid-rejections", logger);
 
-    private static final String WARNING_TEMPLATE =
-            "Bid with imp id: %s for bidder: %s rejected due to: %s, but has been already rejected";
+    private static final String MULTIPLE_REJECTIONS_WARNING_TEMPLATE =
+            "Warning: bidder %s on imp %s responded with multiple nonbid reasons.";
+
+    private static final String INCONSISTENT_RESPONSES_WARNING_TEMPLATE =
+            "Warning: Inconsistent responses from bidder %s on imp %s: both bids and nonbids.";
 
     private final double logSamplingRate;
     private final String bidder;
     private final Set<String> involvedImpIds;
-    private final Set<String> succeededImpIds;
-    private final Map<String, BidRejectionReason> rejectedImpIds;
+    private final Map<String, Set<String>> succeededBidsIds;
+    private final Map<String, List<Pair<BidderBid, BidRejectionReason>>> rejectedBids;
 
     public BidRejectionTracker(String bidder, Set<String> involvedImpIds, double logSamplingRate) {
         this.bidder = bidder;
         this.involvedImpIds = new HashSet<>(involvedImpIds);
         this.logSamplingRate = logSamplingRate;
 
-        succeededImpIds = new HashSet<>();
-        rejectedImpIds = new HashMap<>();
-    }
-
-    public void succeed(String impId) {
-        if (involvedImpIds.contains(impId)) {
-            succeededImpIds.add(impId);
-            rejectedImpIds.remove(impId);
-        }
+        succeededBidsIds = new HashMap<>();
+        rejectedBids = new HashMap<>();
     }
 
+    /**
+     * Restores ONLY imps from rejection, rejected bids are preserved for analytics.
+     * A bid can be rejected only once.
+     */
     public void succeed(Collection<BidderBid> bids) {
         bids.stream()
                 .map(BidderBid::getBid)
                 .filter(Objects::nonNull)
-                .map(Bid::getImpid)
-                .filter(Objects::nonNull)
                 .forEach(this::succeed);
     }
 
+    private void succeed(Bid bid) {
+        final String bidId = bid.getId();
+        final String impId = bid.getImpid();
+        if (involvedImpIds.contains(impId)) {
+            succeededBidsIds.computeIfAbsent(impId, key -> new HashSet<>()).add(bidId);
+            if (rejectedBids.containsKey(impId)) {
+                BID_REJECTIONS_LOGGER.warn(
+                        INCONSISTENT_RESPONSES_WARNING_TEMPLATE.formatted(bidder, impId),
+                        logSamplingRate);
+            }
+        }
+    }
+
     public void restoreFromRejection(Collection<BidderBid> bids) {
         succeed(bids);
     }
 
-    public void reject(String impId, BidRejectionReason reason) {
-        if (involvedImpIds.contains(impId) && !rejectedImpIds.containsKey(impId)) {
-            rejectedImpIds.put(impId, reason);
-            succeededImpIds.remove(impId);
-        } else if (rejectedImpIds.containsKey(impId)) {
-            MULTIPLE_BID_REJECTIONS_LOGGER.warn(
-                    WARNING_TEMPLATE.formatted(impId, bidder, reason), logSamplingRate);
+    public void rejectBids(Collection<BidderBid> bidderBids, BidRejectionReason reason) {
+        bidderBids.forEach(bidderBid -> rejectBid(bidderBid, reason));
+    }
+
+    public void rejectBid(BidderBid bidderBid, BidRejectionReason reason) {
+        final Bid bid = bidderBid.getBid();
+        final String impId = bid.getImpid();
+
+        reject(impId, bidderBid, reason);
+    }
+
+    private void reject(String impId, BidderBid bid, BidRejectionReason reason) {
+        if (involvedImpIds.contains(impId)) {
+            if (rejectedBids.containsKey(impId)) {
+                BID_REJECTIONS_LOGGER.warn(
+                        MULTIPLE_REJECTIONS_WARNING_TEMPLATE.formatted(bidder, impId), logSamplingRate);
+            }
+
+            rejectedBids.computeIfAbsent(impId, key -> new ArrayList<>()).add(Pair.of(bid, reason));
+
+            if (succeededBidsIds.containsKey(impId)) {
+                final String bidId = Optional.ofNullable(bid).map(BidderBid::getBid).map(Bid::getId).orElse(null);
+                final Set<String> succeededBids = succeededBidsIds.get(impId);
+                final boolean removed = bidId == null || succeededBids.remove(bidId);
+                if (removed && !succeededBids.isEmpty()) {
+                    BID_REJECTIONS_LOGGER.warn(
+                            INCONSISTENT_RESPONSES_WARNING_TEMPLATE.formatted(bidder, impId),
+                            logSamplingRate);
+                }
+            }
+        }
+    }
+
+    public void rejectImps(Collection<String> impIds, BidRejectionReason reason) {
+        impIds.forEach(impId -> rejectImp(impId, reason));
+    }
+
+    public void rejectImp(String impId, BidRejectionReason reason) {
+        if (reason.getValue() >= 300) {
+            throw new IllegalArgumentException("The non-bid code 300 and higher assumes "
+                    + "that there is a rejected bid that shouldn't be lost");
         }
+        reject(impId, null, reason);
     }
 
-    public void reject(Collection<String> impIds, BidRejectionReason reason) {
-        impIds.forEach(impId -> reject(impId, reason));
+    public void rejectAllImps(BidRejectionReason reason) {
+        involvedImpIds.forEach(impId -> rejectImp(impId, reason));
     }
 
-    public void rejectAll(BidRejectionReason reason) {
-        involvedImpIds.forEach(impId -> reject(impId, reason));
+    /**
+     * If an impression has at least one valid bid, it's not considered rejected.
+     * If no valid bids are returned for the impression, only the first one rejected reason will be returned
+     */
+    public Map<String, BidRejectionReason> getRejectedImps() {
+        final Map<String, BidRejectionReason> rejectedImpIds = new HashMap<>();
+        for (String impId : involvedImpIds) {
+            final Set<String> succeededBids = succeededBidsIds.getOrDefault(impId, Collections.emptySet());
+            if (succeededBids.isEmpty()) {
+                if (rejectedBids.containsKey(impId)) {
+                    rejectedImpIds.put(impId, rejectedBids.get(impId).getFirst().getRight());
+                } else {
+                    rejectedImpIds.put(impId, BidRejectionReason.NO_BID);
+                }
+            }
+        }
+
+        return rejectedImpIds;
     }
 
-    public Map<String, BidRejectionReason> getRejectionReasons() {
-        final Map<String, BidRejectionReason> missingImpIds = new HashMap<>();
+    /**
+     * Bid is absent for the non-bid code from 0 to 299
+     */
+    public Map<String, List<Pair<BidderBid, BidRejectionReason>>> getRejectedBids() {
+        final Map<String, List<Pair<BidderBid, BidRejectionReason>>> missingImpIds = new HashMap<>();
         for (String impId : involvedImpIds) {
-            if (!succeededImpIds.contains(impId) && !rejectedImpIds.containsKey(impId)) {
-                missingImpIds.put(impId, BidRejectionReason.NO_BID);
+            final Set<String> succeededBids = succeededBidsIds.getOrDefault(impId, Collections.emptySet());
+            if (succeededBids.isEmpty() && !rejectedBids.containsKey(impId)) {
+                missingImpIds.computeIfAbsent(impId, key -> new ArrayList<>())
+                        .add(Pair.of(null, BidRejectionReason.NO_BID));
             }
         }
 
-        return MapUtil.merge(missingImpIds, rejectedImpIds);
+        return MapUtil.merge(missingImpIds, rejectedBids);
     }
 }
diff --git a/src/main/java/org/prebid/server/auction/model/SetuidContext.java b/src/main/java/org/prebid/server/auction/model/SetuidContext.java
index d045b0ceadc..05c451bf863 100644
--- a/src/main/java/org/prebid/server/auction/model/SetuidContext.java
+++ b/src/main/java/org/prebid/server/auction/model/SetuidContext.java
@@ -8,7 +8,7 @@
 import org.prebid.server.auction.gpp.model.GppContext;
 import org.prebid.server.bidder.UsersyncMethodType;
 import org.prebid.server.cookie.UidsCookie;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.privacy.model.PrivacyContext;
 import org.prebid.server.settings.model.Account;
 
diff --git a/src/main/java/org/prebid/server/auction/model/TimeoutContext.java b/src/main/java/org/prebid/server/auction/model/TimeoutContext.java
index b8379056afa..87c390e6b2e 100644
--- a/src/main/java/org/prebid/server/auction/model/TimeoutContext.java
+++ b/src/main/java/org/prebid/server/auction/model/TimeoutContext.java
@@ -1,7 +1,7 @@
 package org.prebid.server.auction.model;
 
 import lombok.Value;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 
 @Value(staticConstructor = "of")
 public class TimeoutContext {
diff --git a/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java b/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java
index 55e32fd1372..0e1e6cbab13 100644
--- a/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java
+++ b/src/main/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactory.java
@@ -6,7 +6,7 @@
 import org.prebid.server.auction.ImplicitParametersExtractor;
 import org.prebid.server.auction.IpAddressHelper;
 import org.prebid.server.auction.model.IpAddress;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.metric.MetricName;
 import org.prebid.server.privacy.PrivacyExtractor;
 import org.prebid.server.privacy.gdpr.TcfDefinerService;
diff --git a/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java b/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java
index 3e81b3fcf4f..b637e656290 100644
--- a/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java
+++ b/src/main/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactory.java
@@ -6,7 +6,7 @@
 import org.prebid.server.auction.ImplicitParametersExtractor;
 import org.prebid.server.auction.IpAddressHelper;
 import org.prebid.server.auction.model.IpAddress;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.metric.MetricName;
 import org.prebid.server.privacy.PrivacyExtractor;
 import org.prebid.server.privacy.gdpr.TcfDefinerService;
diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcement.java
index df7603048ee..395dff73802 100644
--- a/src/main/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcement.java
+++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcement.java
@@ -12,6 +12,7 @@
 import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload;
 import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl;
 import org.prebid.server.activity.infrastructure.payload.impl.PrivacyEnforcementServiceActivityInvocationPayload;
+import org.prebid.server.auction.BidderAliases;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.BidderPrivacyResult;
 import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
@@ -21,7 +22,7 @@
 import java.util.Objects;
 import java.util.Optional;
 
-public class ActivityEnforcement {
+public class ActivityEnforcement implements PrivacyEnforcement {
 
     private final UserFpdActivityMask userFpdActivityMask;
 
@@ -29,17 +30,19 @@ public ActivityEnforcement(UserFpdActivityMask userFpdActivityMask) {
         this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask);
     }
 
-    public Future<List<BidderPrivacyResult>> enforce(List<BidderPrivacyResult> bidderPrivacyResults,
-                                                     AuctionContext auctionContext) {
+    @Override
+    public Future<List<BidderPrivacyResult>> enforce(AuctionContext auctionContext,
+                                                     BidderAliases aliases,
+                                                     List<BidderPrivacyResult> results) {
 
-        final List<BidderPrivacyResult> results = bidderPrivacyResults.stream()
+        final List<BidderPrivacyResult> enforcedResults = results.stream()
                 .map(bidderPrivacyResult -> applyActivityRestrictions(
                         bidderPrivacyResult,
                         auctionContext.getActivityInfrastructure(),
                         auctionContext.getBidRequest()))
                 .toList();
 
-        return Future.succeededFuture(results);
+        return Future.succeededFuture(enforcedResults);
     }
 
     private BidderPrivacyResult applyActivityRestrictions(BidderPrivacyResult bidderPrivacyResult,
diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcement.java
index 10fe183801d..51a8d8f4622 100644
--- a/src/main/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcement.java
+++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcement.java
@@ -1,10 +1,7 @@
 package org.prebid.server.auction.privacy.enforcement;
 
 import com.iab.openrtb.request.BidRequest;
-import com.iab.openrtb.request.Device;
-import com.iab.openrtb.request.User;
 import io.vertx.core.Future;
-import org.apache.commons.lang3.ObjectUtils;
 import org.prebid.server.auction.BidderAliases;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.BidderPrivacyResult;
@@ -22,12 +19,12 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
 
-public class CcpaEnforcement {
+public class CcpaEnforcement implements PrivacyEnforcement {
 
     private static final String CATCH_ALL_BIDDERS = "*";
 
@@ -47,9 +44,10 @@ public CcpaEnforcement(UserFpdCcpaMask userFpdCcpaMask,
         this.ccpaEnforce = ccpaEnforce;
     }
 
+    @Override
     public Future<List<BidderPrivacyResult>> enforce(AuctionContext auctionContext,
-                                                     Map<String, User> bidderToUser,
-                                                     BidderAliases aliases) {
+                                                     BidderAliases aliases,
+                                                     List<BidderPrivacyResult> results) {
 
         final Ccpa ccpa = auctionContext.getPrivacyContext().getPrivacy().getCcpa();
         final BidRequest bidRequest = auctionContext.getBidRequest();
@@ -58,7 +56,7 @@ public Future<List<BidderPrivacyResult>> enforce(AuctionContext auctionContext,
         final boolean isCcpaEnabled = isCcpaEnabled(auctionContext.getAccount(), auctionContext.getRequestTypeMetric());
 
         final Set<String> enforcedBidders = isCcpaEnabled && isCcpaEnforced
-                ? extractCcpaEnforcedBidders(bidderToUser.keySet(), bidRequest, aliases)
+                ? extractCcpaEnforcedBidders(results, bidRequest, aliases)
                 : Collections.emptySet();
 
         metrics.updatePrivacyCcpaMetrics(
@@ -68,7 +66,11 @@ public Future<List<BidderPrivacyResult>> enforce(AuctionContext auctionContext,
                 isCcpaEnabled,
                 enforcedBidders);
 
-        return Future.succeededFuture(maskCcpa(bidderToUser, enforcedBidders, bidRequest.getDevice()));
+        final List<BidderPrivacyResult> enforcedResults = results.stream()
+                .map(result -> enforcedBidders.contains(result.getRequestBidder()) ? maskCcpa(result) : result)
+                .toList();
+
+        return Future.succeededFuture(enforcedResults);
     }
 
     public boolean isCcpaEnforced(Ccpa ccpa, Account account) {
@@ -79,19 +81,21 @@ private boolean isCcpaEnabled(Account account, MetricName requestType) {
         final Optional<AccountCcpaConfig> accountCcpaConfig = Optional.ofNullable(account.getPrivacy())
                 .map(AccountPrivacyConfig::getCcpa);
 
-        return ObjectUtils.firstNonNull(
-                accountCcpaConfig
-                        .map(AccountCcpaConfig::getEnabledForRequestType)
-                        .map(enabledForRequestType -> enabledForRequestType.isEnabledFor(requestType))
-                        .orElse(null),
-                accountCcpaConfig
-                        .map(AccountCcpaConfig::getEnabled)
-                        .orElse(null),
-                ccpaEnforce);
+        return accountCcpaConfig
+                .map(AccountCcpaConfig::getEnabledForRequestType)
+                .map(enabledForRequestType -> enabledForRequestType.isEnabledFor(requestType))
+                .or(() -> accountCcpaConfig.map(AccountCcpaConfig::getEnabled))
+                .orElse(ccpaEnforce);
     }
 
-    private Set<String> extractCcpaEnforcedBidders(Set<String> bidders, BidRequest bidRequest, BidderAliases aliases) {
-        final Set<String> ccpaEnforcedBidders = new HashSet<>(bidders);
+    private Set<String> extractCcpaEnforcedBidders(List<BidderPrivacyResult> results,
+                                                   BidRequest bidRequest,
+                                                   BidderAliases aliases) {
+
+        final Set<String> ccpaEnforcedBidders = results.stream()
+                .map(BidderPrivacyResult::getRequestBidder)
+                .collect(Collectors.toCollection(HashSet::new));
+
         final List<String> nosaleBidders = Optional.ofNullable(bidRequest.getExt())
                 .map(ExtRequest::getPrebid)
                 .map(ExtRequestPrebid::getNosale)
@@ -109,14 +113,11 @@ private Set<String> extractCcpaEnforcedBidders(Set<String> bidders, BidRequest b
         return ccpaEnforcedBidders;
     }
 
-    private List<BidderPrivacyResult> maskCcpa(Map<String, User> bidderToUser, Set<String> bidders, Device device) {
-        final Device maskedDevice = userFpdCcpaMask.maskDevice(device);
-        return bidders.stream()
-                .map(bidder -> BidderPrivacyResult.builder()
-                        .requestBidder(bidder)
-                        .user(userFpdCcpaMask.maskUser(bidderToUser.get(bidder)))
-                        .device(maskedDevice)
-                        .build())
-                .toList();
+    private BidderPrivacyResult maskCcpa(BidderPrivacyResult result) {
+        return BidderPrivacyResult.builder()
+                .requestBidder(result.getRequestBidder())
+                .user(userFpdCcpaMask.maskUser(result.getUser()))
+                .device(userFpdCcpaMask.maskDevice(result.getDevice()))
+                .build();
     }
 }
diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcement.java
index 92471e85ec2..9ebe0c8d044 100644
--- a/src/main/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcement.java
+++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcement.java
@@ -1,18 +1,18 @@
 package org.prebid.server.auction.privacy.enforcement;
 
-import com.iab.openrtb.request.Device;
-import com.iab.openrtb.request.User;
 import io.vertx.core.Future;
+import org.prebid.server.auction.BidderAliases;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.BidderPrivacyResult;
 import org.prebid.server.auction.privacy.enforcement.mask.UserFpdCoppaMask;
 import org.prebid.server.metric.Metrics;
 
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
 
-public class CoppaEnforcement {
+public class CoppaEnforcement implements PrivacyEnforcement {
 
     private final UserFpdCoppaMask userFpdCoppaMask;
     private final Metrics metrics;
@@ -22,23 +22,34 @@ public CoppaEnforcement(UserFpdCoppaMask userFpdCoppaMask, Metrics metrics) {
         this.metrics = Objects.requireNonNull(metrics);
     }
 
-    public boolean isApplicable(AuctionContext auctionContext) {
-        return auctionContext.getPrivacyContext().getPrivacy().getCoppa() == 1;
-    }
+    @Override
+    public Future<List<BidderPrivacyResult>> enforce(AuctionContext auctionContext,
+                                                     BidderAliases aliases,
+                                                     List<BidderPrivacyResult> results) {
+
+        if (!isApplicable(auctionContext)) {
+            return Future.succeededFuture(results);
+        }
+
+        final Set<String> bidders = results.stream()
+                .map(BidderPrivacyResult::getRequestBidder)
+                .collect(Collectors.toSet());
 
-    public Future<List<BidderPrivacyResult>> enforce(AuctionContext auctionContext, Map<String, User> bidderToUser) {
-        metrics.updatePrivacyCoppaMetric(auctionContext.getActivityInfrastructure(), bidderToUser.keySet());
-        return Future.succeededFuture(results(bidderToUser, auctionContext.getBidRequest().getDevice()));
+        metrics.updatePrivacyCoppaMetric(auctionContext.getActivityInfrastructure(), bidders);
+        return Future.succeededFuture(enforce(results));
     }
 
-    private List<BidderPrivacyResult> results(Map<String, User> bidderToUser, Device device) {
-        final Device maskedDevice = userFpdCoppaMask.maskDevice(device);
-        return bidderToUser.entrySet().stream()
-                .map(bidderAndUser -> BidderPrivacyResult.builder()
-                        .requestBidder(bidderAndUser.getKey())
-                        .user(userFpdCoppaMask.maskUser(bidderAndUser.getValue()))
-                        .device(maskedDevice)
+    private List<BidderPrivacyResult> enforce(List<BidderPrivacyResult> results) {
+        return results.stream()
+                .map(result -> BidderPrivacyResult.builder()
+                        .requestBidder(result.getRequestBidder())
+                        .user(userFpdCoppaMask.maskUser(result.getUser()))
+                        .device(userFpdCoppaMask.maskDevice(result.getDevice()))
                         .build())
                 .toList();
     }
+
+    private static boolean isApplicable(AuctionContext auctionContext) {
+        return auctionContext.getPrivacyContext().getPrivacy().getCoppa() == 1;
+    }
 }
diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcement.java
new file mode 100644
index 00000000000..d12e290fb2e
--- /dev/null
+++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcement.java
@@ -0,0 +1,15 @@
+package org.prebid.server.auction.privacy.enforcement;
+
+import io.vertx.core.Future;
+import org.prebid.server.auction.BidderAliases;
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.auction.model.BidderPrivacyResult;
+
+import java.util.List;
+
+public interface PrivacyEnforcement {
+
+    Future<List<BidderPrivacyResult>> enforce(AuctionContext auctionContext,
+                                              BidderAliases aliases,
+                                              List<BidderPrivacyResult> results);
+}
diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java
index 3f4e4055dca..f50a7c637f6 100644
--- a/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java
+++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementService.java
@@ -5,58 +5,41 @@
 import org.prebid.server.auction.BidderAliases;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.BidderPrivacyResult;
-import org.prebid.server.util.ListUtil;
 
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
 
 /**
  * Service provides masking for OpenRTB client sensitive information.
  */
 public class PrivacyEnforcementService {
 
-    private final CoppaEnforcement coppaEnforcement;
-    private final CcpaEnforcement ccpaEnforcement;
-    private final TcfEnforcement tcfEnforcement;
-    private final ActivityEnforcement activityEnforcement;
+    private final List<PrivacyEnforcement> enforcements;
 
-    public PrivacyEnforcementService(CoppaEnforcement coppaEnforcement,
-                                     CcpaEnforcement ccpaEnforcement,
-                                     TcfEnforcement tcfEnforcement,
-                                     ActivityEnforcement activityEnforcement) {
-
-        this.coppaEnforcement = Objects.requireNonNull(coppaEnforcement);
-        this.ccpaEnforcement = Objects.requireNonNull(ccpaEnforcement);
-        this.tcfEnforcement = Objects.requireNonNull(tcfEnforcement);
-        this.activityEnforcement = Objects.requireNonNull(activityEnforcement);
+    public PrivacyEnforcementService(final List<PrivacyEnforcement> enforcements) {
+        this.enforcements = Objects.requireNonNull(enforcements);
     }
 
     public Future<List<BidderPrivacyResult>> mask(AuctionContext auctionContext,
                                                   Map<String, User> bidderToUser,
                                                   BidderAliases aliases) {
 
-        // For now, COPPA masking all values, so we can omit TCF masking.
-        return coppaEnforcement.isApplicable(auctionContext)
-                ? coppaEnforcement.enforce(auctionContext, bidderToUser)
-                : ccpaEnforcement.enforce(auctionContext, bidderToUser, aliases)
-                .compose(ccpaResult -> tcfEnforcement.enforce(
-                                auctionContext,
-                                bidderToUser,
-                                biddersToApplyTcf(bidderToUser.keySet(), ccpaResult),
-                                aliases)
-                        .map(tcfResult -> ListUtil.union(ccpaResult, tcfResult)))
-                .compose(bidderPrivacyResults -> activityEnforcement.enforce(bidderPrivacyResults, auctionContext));
-    }
+        final List<BidderPrivacyResult> initialResults = bidderToUser.entrySet().stream()
+                .map(entry -> BidderPrivacyResult.builder()
+                        .requestBidder(entry.getKey())
+                        .user(entry.getValue())
+                        .device(auctionContext.getBidRequest().getDevice())
+                        .build())
+                .toList();
+
+        Future<List<BidderPrivacyResult>> composedResult = Future.succeededFuture(initialResults);
 
-    private static Set<String> biddersToApplyTcf(Set<String> bidders, List<BidderPrivacyResult> ccpaResult) {
-        final Set<String> biddersToApplyTcf = new HashSet<>(bidders);
-        ccpaResult.stream()
-                .map(BidderPrivacyResult::getRequestBidder)
-                .forEach(biddersToApplyTcf::remove);
+        for (PrivacyEnforcement enforcement : enforcements) {
+            composedResult = composedResult.compose(
+                    results -> enforcement.enforce(auctionContext, aliases, results));
+        }
 
-        return biddersToApplyTcf;
+        return composedResult;
     }
 }
diff --git a/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java b/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java
index 48e098f63a6..933ba724f0f 100644
--- a/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java
+++ b/src/main/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcement.java
@@ -30,8 +30,9 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 
-public class TcfEnforcement {
+public class TcfEnforcement implements PrivacyEnforcement {
 
     private static final Logger logger = LoggerFactory.getLogger(TcfEnforcement.class);
 
@@ -59,30 +60,25 @@ public Future<Map<Integer, PrivacyEnforcementAction>> enforce(Set<Integer> vendo
                 .map(TcfResponse::getActions);
     }
 
+    @Override
     public Future<List<BidderPrivacyResult>> enforce(AuctionContext auctionContext,
-                                                     Map<String, User> bidderToUser,
-                                                     Set<String> bidders,
-                                                     BidderAliases aliases) {
+                                                     BidderAliases aliases,
+                                                     List<BidderPrivacyResult> results) {
 
-        final Device device = auctionContext.getBidRequest().getDevice();
-        final AccountGdprConfig accountGdprConfig = accountGdprConfig(auctionContext.getAccount());
         final MetricName requestType = auctionContext.getRequestTypeMetric();
         final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure();
+        final Set<String> bidders = results.stream()
+                .map(BidderPrivacyResult::getRequestBidder)
+                .collect(Collectors.toSet());
 
         return tcfDefinerService.resultForBidderNames(
                         bidders,
-                        VendorIdResolver.of(aliases, bidderCatalog),
+                        VendorIdResolver.of(aliases),
                         auctionContext.getPrivacyContext().getTcfContext(),
-                        accountGdprConfig)
+                        accountGdprConfig(auctionContext.getAccount()))
                 .map(TcfResponse::getActions)
-                .map(enforcements -> updateMetrics(
-                        activityInfrastructure,
-                        enforcements,
-                        aliases,
-                        requestType,
-                        bidderToUser,
-                        device))
-                .map(enforcements -> bidderToPrivacyResult(enforcements, bidders, bidderToUser, device));
+                .map(enforcements -> updateMetrics(activityInfrastructure, enforcements, aliases, requestType, results))
+                .map(enforcements -> applyEnforcements(enforcements, results));
     }
 
     private static AccountGdprConfig accountGdprConfig(Account account) {
@@ -94,22 +90,21 @@ private Map<String, PrivacyEnforcementAction> updateMetrics(ActivityInfrastructu
                                                                 Map<String, PrivacyEnforcementAction> enforcements,
                                                                 BidderAliases aliases,
                                                                 MetricName requestType,
-                                                                Map<String, User> bidderToUser,
-                                                                Device device) {
-
-        final boolean isLmtEnforcedAndEnabled = isLmtEnforcedAndEnabled(device);
+                                                                List<BidderPrivacyResult> results) {
 
         // Metrics should represent real picture of the bidding process, so if bidder request is blocked
         // by privacy then no reason to increment another metrics, like geo masked, etc.
-        for (final Map.Entry<String, PrivacyEnforcementAction> bidderEnforcement : enforcements.entrySet()) {
-            final String bidder = bidderEnforcement.getKey();
-            final PrivacyEnforcementAction enforcement = bidderEnforcement.getValue();
-            final User user = bidderToUser.get(bidder);
+        for (BidderPrivacyResult result : results) {
+            final String bidder = result.getRequestBidder();
+            final User user = result.getUser();
+            final Device device = result.getDevice();
+            final PrivacyEnforcementAction enforcement = enforcements.get(bidder);
 
             final boolean requestBlocked = enforcement.isBlockBidderRequest();
             final boolean ufpdRemoved = !requestBlocked
                     && ((enforcement.isRemoveUserFpd() && shouldRemoveUserData(user))
                     || (enforcement.isMaskDeviceInfo() && shouldRemoveDeviceData(device)));
+            final boolean isLmtEnforcedAndEnabled = isLmtEnforcedAndEnabled(device);
             final boolean uidsRemoved = !requestBlocked && enforcement.isRemoveUserIds() && shouldRemoveUids(user);
             final boolean geoMasked = !requestBlocked && enforcement.isMaskGeo() && shouldMaskGeo(user, device);
             final boolean analyticsBlocked = !requestBlocked && enforcement.isBlockAnalyticsReport();
@@ -165,32 +160,19 @@ private boolean isLmtEnforcedAndEnabled(Device device) {
         return lmtEnforce && device != null && Objects.equals(device.getLmt(), 1);
     }
 
-    private List<BidderPrivacyResult> bidderToPrivacyResult(Map<String, PrivacyEnforcementAction> bidderToEnforcement,
-                                                            Set<String> bidders,
-                                                            Map<String, User> bidderToUser,
-                                                            Device device) {
-
-        final boolean isLmtEnabled = isLmtEnforcedAndEnabled(device);
+    private List<BidderPrivacyResult> applyEnforcements(Map<String, PrivacyEnforcementAction> enforcements,
+                                                        List<BidderPrivacyResult> results) {
 
-        return bidders.stream()
-                .map(bidder -> createBidderPrivacyResult(
-                        bidder,
-                        bidderToUser.get(bidder),
-                        device,
-                        bidderToEnforcement,
-                        isLmtEnabled))
+        return results.stream()
+                .map(result -> applyEnforcement(enforcements.get(result.getRequestBidder()), result))
                 .toList();
     }
 
-    private BidderPrivacyResult createBidderPrivacyResult(String bidder,
-                                                          User user,
-                                                          Device device,
-                                                          Map<String, PrivacyEnforcementAction> bidderToEnforcement,
-                                                          boolean isLmtEnabled) {
+    private BidderPrivacyResult applyEnforcement(PrivacyEnforcementAction enforcement, BidderPrivacyResult result) {
+        final String bidder = result.getRequestBidder();
 
-        final PrivacyEnforcementAction privacyEnforcementAction = bidderToEnforcement.get(bidder);
-        final boolean blockBidderRequest = privacyEnforcementAction.isBlockBidderRequest();
-        final boolean blockAnalyticsReport = privacyEnforcementAction.isBlockAnalyticsReport();
+        final boolean blockBidderRequest = enforcement.isBlockBidderRequest();
+        final boolean blockAnalyticsReport = enforcement.isBlockAnalyticsReport();
 
         if (blockBidderRequest) {
             return BidderPrivacyResult.builder()
@@ -200,14 +182,18 @@ private BidderPrivacyResult createBidderPrivacyResult(String bidder,
                     .build();
         }
 
-        final boolean maskUserFpd = privacyEnforcementAction.isRemoveUserFpd() || isLmtEnabled;
-        final boolean maskUserIds = privacyEnforcementAction.isRemoveUserIds() || isLmtEnabled;
-        final boolean maskGeo = privacyEnforcementAction.isMaskGeo() || isLmtEnabled;
-        final Set<String> eidExceptions = privacyEnforcementAction.getEidExceptions();
+        final User user = result.getUser();
+        final Device device = result.getDevice();
+
+        final boolean isLmtEnabled = isLmtEnforcedAndEnabled(device);
+        final boolean maskUserFpd = enforcement.isRemoveUserFpd() || isLmtEnabled;
+        final boolean maskUserIds = enforcement.isRemoveUserIds() || isLmtEnabled;
+        final boolean maskGeo = enforcement.isMaskGeo() || isLmtEnabled;
+        final Set<String> eidExceptions = enforcement.getEidExceptions();
         final User maskedUser = userFpdTcfMask.maskUser(user, maskUserFpd, maskUserIds, eidExceptions);
 
-        final boolean maskIp = privacyEnforcementAction.isMaskDeviceIp() || isLmtEnabled;
-        final boolean maskDeviceInfo = privacyEnforcementAction.isMaskDeviceInfo() || isLmtEnabled;
+        final boolean maskIp = enforcement.isMaskDeviceIp() || isLmtEnabled;
+        final boolean maskDeviceInfo = enforcement.isMaskDeviceInfo() || isLmtEnabled;
         final Device maskedDevice = userFpdTcfMask.maskDevice(device, maskIp, maskGeo, maskDeviceInfo);
 
         return BidderPrivacyResult.builder()
diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java
index 0084afc7aca..fb8187ed231 100644
--- a/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java
+++ b/src/main/java/org/prebid/server/auction/requestfactory/AmpRequestFactory.java
@@ -52,6 +52,7 @@
 import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest;
 import org.prebid.server.proto.openrtb.ext.request.ExtUser;
 import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.AccountAuctionConfig;
 import org.prebid.server.util.HttpUtil;
 
 import java.util.ArrayList;
@@ -60,6 +61,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
 public class AmpRequestFactory {
@@ -272,6 +274,7 @@ private static User createUser(ConsentParam consentParam, String addtlConsent) {
 
         final ExtUser extUser = consentedProvidersSettings != null
                 ? ExtUser.builder()
+                .deprecatedConsentedProvidersSettings(consentedProvidersSettings)
                 .consentedProvidersSettings(consentedProvidersSettings)
                 .build()
                 : null;
@@ -407,7 +410,7 @@ private Future<BidRequest> updateBidRequest(AuctionContext auctionContext) {
                 .map(ortbVersionConversionManager::convertToAuctionSupportedVersion)
                 .map(bidRequest -> gppService.updateBidRequest(bidRequest, auctionContext))
                 .map(bidRequest -> validateStoredBidRequest(storedRequestId, bidRequest))
-                .map(this::fillExplicitParameters)
+                .map(bidRequest -> fillExplicitParameters(bidRequest, account))
                 .map(bidRequest -> overrideParameters(bidRequest, httpRequest, auctionContext.getPrebidErrors()))
                 .map(bidRequest -> paramsResolver.resolve(bidRequest, auctionContext, ENDPOINT, true))
                 .map(bidRequest -> ortb2RequestFactory.removeEmptyEids(bidRequest, auctionContext.getDebugWarnings()))
@@ -459,7 +462,7 @@ private static BidRequest validateStoredBidRequest(String tagId, BidRequest bidR
      * - Sets {@link BidRequest}.test = 1 if it was passed in {@link RoutingContext}
      * - Updates {@link BidRequest}.ext.prebid.amp.data with all query parameters
      */
-    private BidRequest fillExplicitParameters(BidRequest bidRequest) {
+    private BidRequest fillExplicitParameters(BidRequest bidRequest, Account account) {
         final List<Imp> imps = bidRequest.getImp();
         // Force HTTPS as AMP requires it, but pubs can forget to set it.
         final Imp imp = imps.getFirst();
@@ -496,6 +499,7 @@ private BidRequest fillExplicitParameters(BidRequest bidRequest) {
                     .imp(setSecure ? Collections.singletonList(imps.getFirst().toBuilder().secure(1).build()) : imps)
                     .ext(extRequest(
                             bidRequest,
+                            account,
                             setDefaultTargeting,
                             setDefaultCache))
                     .build();
@@ -692,6 +696,7 @@ private static List<Format> parseMultiSizeParam(String ms) {
      * Creates updated bidrequest.ext {@link ObjectNode}.
      */
     private ExtRequest extRequest(BidRequest bidRequest,
+                                  Account account,
                                   boolean setDefaultTargeting,
                                   boolean setDefaultCache) {
 
@@ -704,7 +709,7 @@ private ExtRequest extRequest(BidRequest bidRequest,
                     : ExtRequestPrebid.builder();
 
             if (setDefaultTargeting) {
-                prebidBuilder.targeting(createTargetingWithDefaults(prebid));
+                prebidBuilder.targeting(createTargetingWithDefaults(prebid, account));
             }
             if (setDefaultCache) {
                 prebidBuilder.cache(ExtRequestPrebidCache.of(ExtRequestPrebidCacheBids.of(null, null),
@@ -727,15 +732,14 @@ private ExtRequest extRequest(BidRequest bidRequest,
      * Creates updated with default values bidrequest.ext.targeting {@link ExtRequestTargeting} if at least one of it's
      * child properties is missed or entire targeting does not exist.
      */
-    private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid) {
+    private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid, Account account) {
         final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null;
         final boolean isTargetingNull = targeting == null;
 
         final JsonNode priceGranularityNode = isTargetingNull ? null : targeting.getPricegranularity();
         final boolean isPriceGranularityNull = priceGranularityNode == null || priceGranularityNode.isNull();
-        final JsonNode outgoingPriceGranularityNode
-                = isPriceGranularityNull
-                ? mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT))
+        final JsonNode outgoingPriceGranularityNode = isPriceGranularityNull
+                ? mapper.mapper().valueToTree(ExtPriceGranularity.from(getDefaultPriceGranularity(account)))
                 : priceGranularityNode;
 
         final ExtMediaTypePriceGranularity mediaTypePriceGranularity = isTargetingNull
@@ -759,6 +763,14 @@ private ExtRequestTargeting createTargetingWithDefaults(ExtRequestPrebid prebid)
                 .build();
     }
 
+    private static PriceGranularity getDefaultPriceGranularity(Account account) {
+        return Optional.ofNullable(account)
+                .map(Account::getAuction)
+                .map(AccountAuctionConfig::getPriceGranularity)
+                .map(PriceGranularity::createFromStringOrDefault)
+                .orElse(PriceGranularity.DEFAULT);
+    }
+
     @Value(staticConstructor = "of")
     private static class GppSidExtraction {
 
diff --git a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java
index bd720a25f2b..34140a26228 100644
--- a/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java
+++ b/src/main/java/org/prebid/server/auction/requestfactory/AuctionRequestFactory.java
@@ -17,6 +17,7 @@
 import org.prebid.server.auction.model.AuctionStoredResult;
 import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory;
 import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager;
+import org.prebid.server.bidadjustments.BidAdjustmentsRetriever;
 import org.prebid.server.cookie.CookieDeprecationService;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.json.JacksonMapper;
@@ -50,6 +51,7 @@ public class AuctionRequestFactory {
     private final JacksonMapper mapper;
     private final OrtbTypesResolver ortbTypesResolver;
     private final GeoLocationServiceWrapper geoLocationServiceWrapper;
+    private final BidAdjustmentsRetriever bidAdjustmentsRetriever;
 
     private static final String ENDPOINT = Endpoint.openrtb2_auction.value();
 
@@ -66,7 +68,8 @@ public AuctionRequestFactory(long maxRequestSize,
                                  AuctionPrivacyContextFactory auctionPrivacyContextFactory,
                                  DebugResolver debugResolver,
                                  JacksonMapper mapper,
-                                 GeoLocationServiceWrapper geoLocationServiceWrapper) {
+                                 GeoLocationServiceWrapper geoLocationServiceWrapper,
+                                 BidAdjustmentsRetriever bidAdjustmentsRetriever) {
 
         this.maxRequestSize = maxRequestSize;
         this.ortb2RequestFactory = Objects.requireNonNull(ortb2RequestFactory);
@@ -82,6 +85,7 @@ public AuctionRequestFactory(long maxRequestSize,
         this.debugResolver = Objects.requireNonNull(debugResolver);
         this.mapper = Objects.requireNonNull(mapper);
         this.geoLocationServiceWrapper = Objects.requireNonNull(geoLocationServiceWrapper);
+        this.bidAdjustmentsRetriever = Objects.requireNonNull(bidAdjustmentsRetriever);
     }
 
     /**
@@ -142,6 +146,8 @@ public Future<AuctionContext> enrichAuctionContext(AuctionContext initialContext
                 .compose(auctionContext -> ortb2RequestFactory.enrichBidRequestWithAccountAndPrivacyData(auctionContext)
                         .map(auctionContext::with))
 
+                .map(auctionContext -> auctionContext.with(bidAdjustmentsRetriever.retrieve(auctionContext)))
+
                 .compose(auctionContext -> ortb2RequestFactory.executeProcessedAuctionRequestHooks(auctionContext)
                         .map(auctionContext::with))
 
diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java
index 8b377c08bf9..5bcabe413db 100644
--- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java
+++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolver.java
@@ -56,6 +56,8 @@
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
 import org.prebid.server.proto.openrtb.ext.request.ExtSite;
 import org.prebid.server.proto.openrtb.ext.response.BidType;
+import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.AccountAuctionConfig;
 import org.prebid.server.util.HttpUtil;
 import org.prebid.server.util.ObjectUtil;
 import org.prebid.server.util.StreamUtil;
@@ -187,7 +189,11 @@ public BidRequest resolve(BidRequest bidRequest,
         final ExtRequest ext = bidRequest.getExt();
         final List<Imp> imps = bidRequest.getImp();
         final ExtRequest populatedExt = populateRequestExt(
-                ext, bidRequest, ObjectUtils.defaultIfNull(populatedImps, imps), endpoint);
+                ext,
+                bidRequest,
+                ObjectUtils.defaultIfNull(populatedImps, imps),
+                endpoint,
+                auctionContext.getAccount());
 
         final Source source = bidRequest.getSource();
         final Source populatedSource = populateSource(source, populatedExt, hasStoredBidRequest);
@@ -713,10 +719,15 @@ private static boolean isUniqueIds(List<Imp> imps) {
         return impIdsSet.size() == impIdsList.size();
     }
 
-    private ExtRequest populateRequestExt(ExtRequest ext, BidRequest bidRequest, List<Imp> imps, String endpoint) {
+    private ExtRequest populateRequestExt(ExtRequest ext,
+                                          BidRequest bidRequest,
+                                          List<Imp> imps,
+                                          String endpoint,
+                                          Account account) {
+
         final ExtRequestPrebid prebid = ObjectUtil.getIfNotNull(ext, ExtRequest::getPrebid);
 
-        final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, imps);
+        final ExtRequestTargeting updatedTargeting = targetingOrNull(prebid, imps, account);
         final ExtRequestPrebidCache updatedCache = cacheOrNull(prebid);
         final ExtRequestPrebidChannel updatedChannel = channelOrNull(prebid, bidRequest, endpoint);
 
@@ -783,7 +794,7 @@ private static void resolveImpMediaTypes(Imp imp, Set<BidType> impsMediaTypes) {
     /**
      * Returns populated {@link ExtRequestTargeting} or null if no changes were applied.
      */
-    private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List<Imp> imps) {
+    private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List<Imp> imps, Account account) {
         final ExtRequestTargeting targeting = prebid != null ? prebid.getTargeting() : null;
 
         final boolean isTargetingNotNull = targeting != null;
@@ -796,8 +807,12 @@ private ExtRequestTargeting targetingOrNull(ExtRequestPrebid prebid, List<Imp> i
 
         if (isPriceGranularityNull || isPriceGranularityTextual || isIncludeWinnersNull || isIncludeBidderKeysNull) {
             return targeting.toBuilder()
-                    .pricegranularity(resolvePriceGranularity(targeting, isPriceGranularityNull,
-                            isPriceGranularityTextual, imps))
+                    .pricegranularity(resolvePriceGranularity(
+                            targeting,
+                            isPriceGranularityNull,
+                            isPriceGranularityTextual,
+                            imps,
+                            account))
                     .includewinners(isIncludeWinnersNull || targeting.getIncludewinners())
                     .includebidderkeys(isIncludeBidderKeysNull
                             ? !isWinningOnly(prebid.getCache())
@@ -822,14 +837,22 @@ private boolean isWinningOnly(ExtRequestPrebidCache cache) {
      * In case of valid string price granularity replaced it with appropriate custom view.
      * In case of invalid string value throws {@link InvalidRequestException}.
      */
-    private JsonNode resolvePriceGranularity(ExtRequestTargeting targeting, boolean isPriceGranularityNull,
-                                             boolean isPriceGranularityTextual, List<Imp> imps) {
+    private JsonNode resolvePriceGranularity(ExtRequestTargeting targeting,
+                                             boolean isPriceGranularityNull,
+                                             boolean isPriceGranularityTextual,
+                                             List<Imp> imps,
+                                             Account account) {
 
         final boolean hasAllMediaTypes = checkExistingMediaTypes(targeting.getMediatypepricegranularity())
                 .containsAll(getImpMediaTypes(imps));
 
         if (isPriceGranularityNull && !hasAllMediaTypes) {
-            return mapper.mapper().valueToTree(ExtPriceGranularity.from(PriceGranularity.DEFAULT));
+            final PriceGranularity defaultPriceGranularity = Optional.ofNullable(account)
+                    .map(Account::getAuction)
+                    .map(AccountAuctionConfig::getPriceGranularity)
+                    .map(PriceGranularity::createFromStringOrDefault)
+                    .orElse(PriceGranularity.DEFAULT);
+            return mapper.mapper().valueToTree(ExtPriceGranularity.from(defaultPriceGranularity));
         }
 
         final JsonNode priceGranularityNode = targeting.getPricegranularity();
diff --git a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java
index 336f2b5f8f1..01c4c8a43dc 100644
--- a/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java
+++ b/src/main/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactory.java
@@ -33,8 +33,8 @@
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.PreBidException;
 import org.prebid.server.exception.UnauthorizedAccountException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.geolocation.CountryCodeMapper;
 import org.prebid.server.geolocation.model.GeoInfo;
 import org.prebid.server.hooks.execution.HookStageExecutor;
@@ -385,6 +385,7 @@ private static HttpRequestContext toHttpRequest(HookStageExecutionResult<Entrypo
         }
 
         return HttpRequestContext.builder()
+                .httpMethod(routingContext.request().method())
                 .absoluteUri(routingContext.request().absoluteURI())
                 .queryParams(stageResult.getPayload().queryParams())
                 .headers(stageResult.getPayload().headers())
diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java
new file mode 100644
index 00000000000..9ddeefb6e2e
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidator.java
@@ -0,0 +1,100 @@
+package org.prebid.server.bidadjustments;
+
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.bidadjustments.model.BidAdjustmentType;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule;
+import org.prebid.server.proto.openrtb.ext.request.ImpMediaType;
+import org.prebid.server.validation.ValidationException;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class BidAdjustmentRulesValidator {
+
+    public static final Set<String> SUPPORTED_MEDIA_TYPES = Set.of(
+            BidAdjustmentsResolver.WILDCARD,
+            ImpMediaType.banner.toString(),
+            ImpMediaType.audio.toString(),
+            ImpMediaType.video_instream.toString(),
+            ImpMediaType.video_outstream.toString(),
+            ImpMediaType.xNative.toString());
+
+    private BidAdjustmentRulesValidator() {
+
+    }
+
+    public static void validate(ExtRequestBidAdjustments bidAdjustments) throws ValidationException {
+        if (bidAdjustments == null) {
+            return;
+        }
+
+        final Map<String, Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>>> mediatypes =
+                bidAdjustments.getMediatype();
+
+        if (MapUtils.isEmpty(mediatypes)) {
+            return;
+        }
+
+        for (String mediatype : mediatypes.keySet()) {
+            if (SUPPORTED_MEDIA_TYPES.contains(mediatype)) {
+                final Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>> bidders = mediatypes.get(mediatype);
+                if (MapUtils.isEmpty(bidders)) {
+                    throw new ValidationException("no bidders found in %s".formatted(mediatype));
+                }
+                for (String bidder : bidders.keySet()) {
+                    final Map<String, List<ExtRequestBidAdjustmentsRule>> deals = bidders.get(bidder);
+
+                    if (MapUtils.isEmpty(deals)) {
+                        throw new ValidationException("no deals found in %s.%s".formatted(mediatype, bidder));
+                    }
+
+                    for (String dealId : deals.keySet()) {
+                        final String path = "%s.%s.%s".formatted(mediatype, bidder, dealId);
+                        validateRules(deals.get(dealId), path);
+                    }
+                }
+            }
+        }
+    }
+
+    private static void validateRules(List<ExtRequestBidAdjustmentsRule> rules,
+                                      String path) throws ValidationException {
+
+        if (rules == null) {
+            throw new ValidationException("no bid adjustment rules found in %s".formatted(path));
+        }
+
+        for (ExtRequestBidAdjustmentsRule rule : rules) {
+            final BidAdjustmentType type = rule.getAdjType();
+            final String currency = rule.getCurrency();
+            final BigDecimal value = rule.getValue();
+
+            final boolean isNotSpecifiedCurrency = StringUtils.isBlank(currency);
+
+            final boolean unknownType = type == null || type == BidAdjustmentType.UNKNOWN;
+
+            final boolean invalidCpm = type == BidAdjustmentType.CPM
+                    && (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE));
+
+            final boolean invalidMultiplier = type == BidAdjustmentType.MULTIPLIER
+                    && isValueNotInRange(value, 0, 100);
+
+            final boolean invalidStatic = type == BidAdjustmentType.STATIC
+                    && (isNotSpecifiedCurrency || isValueNotInRange(value, 0, Integer.MAX_VALUE));
+
+            if (unknownType || invalidCpm || invalidMultiplier || invalidStatic) {
+                throw new ValidationException("the found rule %s in %s is invalid".formatted(rule, path));
+            }
+        }
+    }
+
+    private static boolean isValueNotInRange(BigDecimal value, int minValue, int maxValue) {
+        return value == null
+                || value.compareTo(BigDecimal.valueOf(minValue)) < 0
+                || value.compareTo(BigDecimal.valueOf(maxValue)) >= 0;
+    }
+}
diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java
new file mode 100644
index 00000000000..1136876c7f6
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessor.java
@@ -0,0 +1,205 @@
+package org.prebid.server.bidadjustments;
+
+import com.fasterxml.jackson.databind.node.DecimalNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.response.Bid;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.auction.ImpMediaTypeResolver;
+import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver;
+import org.prebid.server.auction.model.AuctionParticipation;
+import org.prebid.server.auction.model.BidderResponse;
+import org.prebid.server.bidadjustments.model.BidAdjustments;
+import org.prebid.server.bidder.model.BidderBid;
+import org.prebid.server.bidder.model.BidderError;
+import org.prebid.server.bidder.model.BidderSeatBid;
+import org.prebid.server.bidder.model.Price;
+import org.prebid.server.currency.CurrencyConversionService;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
+import org.prebid.server.proto.openrtb.ext.request.ImpMediaType;
+import org.prebid.server.util.PbsUtil;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class BidAdjustmentsProcessor {
+
+    private static final String ORIGINAL_BID_CPM = "origbidcpm";
+    private static final String ORIGINAL_BID_CURRENCY = "origbidcur";
+
+    private final CurrencyConversionService currencyService;
+    private final BidAdjustmentFactorResolver bidAdjustmentFactorResolver;
+    private final BidAdjustmentsResolver bidAdjustmentsResolver;
+    private final JacksonMapper mapper;
+
+    public BidAdjustmentsProcessor(CurrencyConversionService currencyService,
+                                   BidAdjustmentFactorResolver bidAdjustmentFactorResolver,
+                                   BidAdjustmentsResolver bidAdjustmentsResolver,
+                                   JacksonMapper mapper) {
+
+        this.currencyService = Objects.requireNonNull(currencyService);
+        this.bidAdjustmentFactorResolver = Objects.requireNonNull(bidAdjustmentFactorResolver);
+        this.bidAdjustmentsResolver = Objects.requireNonNull(bidAdjustmentsResolver);
+        this.mapper = Objects.requireNonNull(mapper);
+    }
+
+    public AuctionParticipation enrichWithAdjustedBids(AuctionParticipation auctionParticipation,
+                                                       BidRequest bidRequest,
+                                                       BidAdjustments bidAdjustments) {
+
+        if (auctionParticipation.isRequestBlocked()) {
+            return auctionParticipation;
+        }
+
+        final BidderResponse bidderResponse = auctionParticipation.getBidderResponse();
+        final BidderSeatBid seatBid = bidderResponse.getSeatBid();
+
+        final List<BidderBid> bidderBids = seatBid.getBids();
+        if (bidderBids.isEmpty()) {
+            return auctionParticipation;
+        }
+
+        final List<BidderError> errors = new ArrayList<>(seatBid.getErrors());
+        final String bidder = auctionParticipation.getBidder();
+
+        final List<BidderBid> updatedBidderBids = bidderBids.stream()
+                .map(bidderBid -> applyBidAdjustments(bidderBid, bidRequest, bidder, bidAdjustments, errors))
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+
+        final BidderResponse updatedBidderResponse = bidderResponse.with(seatBid.toBuilder()
+                .bids(updatedBidderBids)
+                .errors(errors)
+                .build());
+
+        return auctionParticipation.with(updatedBidderResponse);
+    }
+
+    private BidderBid applyBidAdjustments(BidderBid bidderBid,
+                                          BidRequest bidRequest,
+                                          String bidder,
+                                          BidAdjustments bidAdjustments,
+                                          List<BidderError> errors) {
+        try {
+            final Price originalPrice = getOriginalPrice(bidderBid);
+
+            final ImpMediaType mediaType = ImpMediaTypeResolver.resolve(
+                    bidderBid.getBid().getImpid(),
+                    bidRequest.getImp(),
+                    bidderBid.getType());
+
+            final Price priceWithFactorsApplied = applyBidAdjustmentFactors(
+                    originalPrice,
+                    bidder,
+                    bidRequest,
+                    mediaType);
+
+            final Price priceWithAdjustmentsApplied = applyBidAdjustmentRules(
+                    priceWithFactorsApplied,
+                    bidder,
+                    bidRequest,
+                    bidAdjustments,
+                    mediaType,
+                    bidderBid.getBid().getDealid());
+
+            return updateBid(originalPrice, priceWithAdjustmentsApplied, bidderBid, bidRequest);
+        } catch (PreBidException e) {
+            errors.add(BidderError.generic(e.getMessage()));
+            return null;
+        }
+    }
+
+    private BidderBid updateBid(Price originalPrice, Price adjustedPrice, BidderBid bidderBid, BidRequest bidRequest) {
+        final Bid bid = bidderBid.getBid();
+        final ObjectNode bidExt = bid.getExt();
+        final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode();
+
+        final BigDecimal originalBidPrice = originalPrice.getValue();
+        final String originalBidCurrency = originalPrice.getCurrency();
+        updatedBidExt.set(ORIGINAL_BID_CPM, new DecimalNode(originalBidPrice));
+        if (StringUtils.isNotBlank(originalBidCurrency)) {
+            updatedBidExt.set(ORIGINAL_BID_CURRENCY, new TextNode(originalBidCurrency));
+        }
+
+        final String requestCurrency = bidRequest.getCur().getFirst();
+        final BigDecimal requestCurrencyPrice = currencyService.convertCurrency(
+                adjustedPrice.getValue(),
+                bidRequest,
+                adjustedPrice.getCurrency(),
+                requestCurrency);
+
+        return bidderBid.toBuilder()
+                .bidCurrency(requestCurrency)
+                .bid(bid.toBuilder()
+                        .ext(updatedBidExt)
+                        .price(requestCurrencyPrice)
+                        .build())
+                .build();
+    }
+
+    private Price getOriginalPrice(BidderBid bidderBid) {
+        final Bid bid = bidderBid.getBid();
+        final String bidCurrency = bidderBid.getBidCurrency();
+        final BigDecimal price = bid.getPrice();
+
+        return Price.of(StringUtils.stripToNull(bidCurrency), price);
+    }
+
+    private Price applyBidAdjustmentFactors(Price bidPrice,
+                                            String bidder,
+                                            BidRequest bidRequest,
+                                            ImpMediaType mediaType) {
+
+        final String bidCurrency = bidPrice.getCurrency();
+        final BigDecimal price = bidPrice.getValue();
+
+        final BigDecimal priceAdjustmentFactor = bidAdjustmentForBidder(bidder, bidRequest, mediaType);
+        final BigDecimal adjustedPrice = adjustPrice(priceAdjustmentFactor, price);
+
+        return Price.of(bidCurrency, adjustedPrice.compareTo(price) != 0 ? adjustedPrice : price);
+    }
+
+    private BigDecimal bidAdjustmentForBidder(String bidder, BidRequest bidRequest, ImpMediaType mediaType) {
+        final ExtRequestBidAdjustmentFactors adjustmentFactors = extBidAdjustmentFactors(bidRequest);
+        if (adjustmentFactors == null) {
+            return null;
+        }
+
+        final ImpMediaType targetMediaType = mediaType == ImpMediaType.video_instream ? ImpMediaType.video : mediaType;
+        return bidAdjustmentFactorResolver.resolve(targetMediaType, adjustmentFactors, bidder);
+    }
+
+    private static ExtRequestBidAdjustmentFactors extBidAdjustmentFactors(BidRequest bidRequest) {
+        final ExtRequestPrebid prebid = PbsUtil.extRequestPrebid(bidRequest);
+        return prebid != null ? prebid.getBidadjustmentfactors() : null;
+    }
+
+    private static BigDecimal adjustPrice(BigDecimal priceAdjustmentFactor, BigDecimal price) {
+        return priceAdjustmentFactor != null && priceAdjustmentFactor.compareTo(BigDecimal.ONE) != 0
+                ? price.multiply(priceAdjustmentFactor)
+                : price;
+    }
+
+    private Price applyBidAdjustmentRules(Price bidPrice,
+                                          String bidder,
+                                          BidRequest bidRequest,
+                                          BidAdjustments bidAdjustments,
+                                          ImpMediaType mediaType,
+                                          String dealId) {
+
+        return bidAdjustmentsResolver.resolve(
+                bidPrice,
+                bidRequest,
+                bidAdjustments,
+                mediaType,
+                bidder,
+                dealId);
+    }
+}
diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java
new file mode 100644
index 00000000000..ffac1cbc51a
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsResolver.java
@@ -0,0 +1,106 @@
+package org.prebid.server.bidadjustments;
+
+import com.iab.openrtb.request.BidRequest;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.bidadjustments.model.BidAdjustmentType;
+import org.prebid.server.bidadjustments.model.BidAdjustments;
+import org.prebid.server.bidder.model.Price;
+import org.prebid.server.currency.CurrencyConversionService;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule;
+import org.prebid.server.proto.openrtb.ext.request.ImpMediaType;
+import org.prebid.server.util.BidderUtil;
+import org.prebid.server.util.dsl.config.PrebidConfigMatchingStrategy;
+import org.prebid.server.util.dsl.config.PrebidConfigParameter;
+import org.prebid.server.util.dsl.config.PrebidConfigParameters;
+import org.prebid.server.util.dsl.config.PrebidConfigSource;
+import org.prebid.server.util.dsl.config.impl.MostAccurateCombinationStrategy;
+import org.prebid.server.util.dsl.config.impl.SimpleDirectParameter;
+import org.prebid.server.util.dsl.config.impl.SimpleParameters;
+import org.prebid.server.util.dsl.config.impl.SimpleSource;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+public class BidAdjustmentsResolver {
+
+    public static final String WILDCARD = "*";
+    public static final String DELIMITER = "|";
+
+    private final PrebidConfigMatchingStrategy matchingStrategy;
+    private final CurrencyConversionService currencyService;
+
+    public BidAdjustmentsResolver(CurrencyConversionService currencyService) {
+        this.currencyService = Objects.requireNonNull(currencyService);
+        this.matchingStrategy = new MostAccurateCombinationStrategy();
+    }
+
+    public Price resolve(Price initialPrice,
+                         BidRequest bidRequest,
+                         BidAdjustments bidAdjustments,
+                         ImpMediaType targetMediaType,
+                         String targetBidder,
+                         String targetDealId) {
+
+        final List<ExtRequestBidAdjustmentsRule> adjustmentsRules = findRules(
+                bidAdjustments,
+                targetMediaType,
+                targetBidder,
+                targetDealId);
+
+        return adjustPrice(initialPrice, adjustmentsRules, bidRequest);
+    }
+
+    private List<ExtRequestBidAdjustmentsRule> findRules(BidAdjustments bidAdjustments,
+                                                         ImpMediaType targetMediaType,
+                                                         String targetBidder,
+                                                         String targetDealId) {
+
+        final Map<String, List<ExtRequestBidAdjustmentsRule>> rules = bidAdjustments.getRules();
+        final PrebidConfigSource source = SimpleSource.of(WILDCARD, DELIMITER, rules.keySet());
+        final PrebidConfigParameters parameters = createParameters(targetMediaType, targetBidder, targetDealId);
+
+        final String rule = matchingStrategy.match(source, parameters);
+        return rule == null ? Collections.emptyList() : rules.get(rule);
+    }
+
+    private PrebidConfigParameters createParameters(ImpMediaType mediaType, String bidder, String dealId) {
+        final List<PrebidConfigParameter> conditionsMatchers = List.of(
+                SimpleDirectParameter.of(mediaType.toString()),
+                SimpleDirectParameter.of(bidder),
+                StringUtils.isNotBlank(dealId) ? SimpleDirectParameter.of(dealId) : PrebidConfigParameter.wildcard());
+
+        return SimpleParameters.of(conditionsMatchers);
+    }
+
+    private Price adjustPrice(Price price,
+                              List<ExtRequestBidAdjustmentsRule> bidAdjustmentRules,
+                              BidRequest bidRequest) {
+
+        String resolvedCurrency = price.getCurrency();
+        BigDecimal resolvedPrice = price.getValue();
+
+        for (ExtRequestBidAdjustmentsRule rule : bidAdjustmentRules) {
+            final BidAdjustmentType adjustmentType = rule.getAdjType();
+            final BigDecimal adjustmentValue = rule.getValue();
+            final String adjustmentCurrency = rule.getCurrency();
+
+            switch (adjustmentType) {
+                case MULTIPLIER -> resolvedPrice = BidderUtil.roundFloor(resolvedPrice.multiply(adjustmentValue));
+                case CPM -> {
+                    final BigDecimal convertedAdjustmentValue = currencyService.convertCurrency(
+                            adjustmentValue, bidRequest, adjustmentCurrency, resolvedCurrency);
+                    resolvedPrice = BidderUtil.roundFloor(resolvedPrice.subtract(convertedAdjustmentValue));
+                }
+                case STATIC -> {
+                    resolvedPrice = adjustmentValue;
+                    resolvedCurrency = adjustmentCurrency;
+                }
+            }
+        }
+
+        return Price.of(resolvedCurrency, resolvedPrice);
+    }
+}
diff --git a/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java
new file mode 100644
index 00000000000..6a151754bb2
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidadjustments/BidAdjustmentsRetriever.java
@@ -0,0 +1,86 @@
+package org.prebid.server.bidadjustments;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.iab.openrtb.request.BidRequest;
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.bidadjustments.model.BidAdjustments;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.json.JsonMerger;
+import org.prebid.server.log.ConditionalLogger;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
+import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.AccountAuctionConfig;
+import org.prebid.server.validation.ValidationException;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+
+public class BidAdjustmentsRetriever {
+
+    private static final Logger logger = LoggerFactory.getLogger(BidAdjustmentsRetriever.class);
+    private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger);
+
+    private final ObjectMapper mapper;
+    private final JsonMerger jsonMerger;
+    private final double samplingRate;
+
+    public BidAdjustmentsRetriever(JacksonMapper mapper,
+                                   JsonMerger jsonMerger,
+                                   double samplingRate) {
+        this.mapper = Objects.requireNonNull(mapper).mapper();
+        this.jsonMerger = Objects.requireNonNull(jsonMerger);
+        this.samplingRate = samplingRate;
+    }
+
+    public BidAdjustments retrieve(AuctionContext auctionContext) {
+        final List<String> debugWarnings = auctionContext.getDebugWarnings();
+        final boolean debugEnabled = auctionContext.getDebugContext().isDebugEnabled();
+
+        final JsonNode requestBidAdjustmentsNode = Optional.ofNullable(auctionContext.getBidRequest())
+                .map(BidRequest::getExt)
+                .map(ExtRequest::getPrebid)
+                .map(ExtRequestPrebid::getBidadjustments)
+                .orElseGet(mapper::createObjectNode);
+
+        final JsonNode accountBidAdjustmentsNode = Optional.ofNullable(auctionContext.getAccount())
+                .map(Account::getAuction)
+                .map(AccountAuctionConfig::getBidAdjustments)
+                .orElseGet(mapper::createObjectNode);
+
+        final JsonNode mergedBidAdjustmentsNode = jsonMerger.merge(
+                requestBidAdjustmentsNode,
+                accountBidAdjustmentsNode);
+
+        final List<String> resolvedWarnings = debugEnabled ? debugWarnings : null;
+        return convertAndValidate(mergedBidAdjustmentsNode, resolvedWarnings, "request")
+                .or(() -> convertAndValidate(accountBidAdjustmentsNode, resolvedWarnings, "account"))
+                .orElse(BidAdjustments.of(Collections.emptyMap()));
+    }
+
+    private Optional<BidAdjustments> convertAndValidate(JsonNode bidAdjustmentsNode,
+                                                        List<String> debugWarnings,
+                                                        String errorLocation) {
+        try {
+            final ExtRequestBidAdjustments accountBidAdjustments = mapper.convertValue(
+                    bidAdjustmentsNode,
+                    ExtRequestBidAdjustments.class);
+
+            BidAdjustmentRulesValidator.validate(accountBidAdjustments);
+            return Optional.of(BidAdjustments.of(accountBidAdjustments));
+        } catch (IllegalArgumentException | ValidationException e) {
+            final String message = "bid adjustment from " + errorLocation + " was invalid: " + e.getMessage();
+            if (debugWarnings != null) {
+                debugWarnings.add(message);
+            }
+            conditionalLogger.error(message, samplingRate);
+            return Optional.empty();
+        }
+    }
+}
diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java
new file mode 100644
index 00000000000..e9b790e5eab
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustmentType.java
@@ -0,0 +1,19 @@
+package org.prebid.server.bidadjustments.model;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+
+public enum BidAdjustmentType {
+
+    CPM, MULTIPLIER, STATIC, UNKNOWN;
+
+    @SuppressWarnings("unused")
+    @JsonCreator
+    public static BidAdjustmentType of(String name) {
+        try {
+            return BidAdjustmentType.valueOf(name.toUpperCase());
+        } catch (IllegalArgumentException e) {
+            return UNKNOWN;
+        }
+    }
+
+}
diff --git a/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java
new file mode 100644
index 00000000000..385a7644811
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidadjustments/model/BidAdjustments.java
@@ -0,0 +1,52 @@
+package org.prebid.server.bidadjustments.model;
+
+import lombok.Value;
+import org.apache.commons.collections4.MapUtils;
+import org.prebid.server.bidadjustments.BidAdjustmentRulesValidator;
+import org.prebid.server.bidadjustments.BidAdjustmentsResolver;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Value(staticConstructor = "of")
+public class BidAdjustments {
+
+    private static final String RULE_SCHEME =
+            "%s" + BidAdjustmentsResolver.DELIMITER + "%s" + BidAdjustmentsResolver.DELIMITER + "%s";
+
+    Map<String, List<ExtRequestBidAdjustmentsRule>> rules;
+
+    public static BidAdjustments of(ExtRequestBidAdjustments bidAdjustments) {
+        if (bidAdjustments == null) {
+            return BidAdjustments.of(Collections.emptyMap());
+        }
+
+        final Map<String, List<ExtRequestBidAdjustmentsRule>> rules = new HashMap<>();
+
+        final Map<String, Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>>> mediatypes =
+                bidAdjustments.getMediatype();
+
+        if (MapUtils.isEmpty(mediatypes)) {
+            return BidAdjustments.of(Collections.emptyMap());
+        }
+
+        for (String mediatype : mediatypes.keySet()) {
+            if (BidAdjustmentRulesValidator.SUPPORTED_MEDIA_TYPES.contains(mediatype)) {
+                final Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>> bidders = mediatypes.get(mediatype);
+                for (String bidder : bidders.keySet()) {
+                    final Map<String, List<ExtRequestBidAdjustmentsRule>> deals = bidders.get(bidder);
+                    for (String dealId : deals.keySet()) {
+                        rules.put(RULE_SCHEME.formatted(mediatype, bidder, dealId), deals.get(dealId));
+                    }
+                }
+            }
+        }
+
+        return BidAdjustments.of(MapUtils.unmodifiableMap(rules));
+    }
+
+}
diff --git a/src/main/java/org/prebid/server/bidder/BidderInfo.java b/src/main/java/org/prebid/server/bidder/BidderInfo.java
index c9659135eb7..1ff8f323701 100644
--- a/src/main/java/org/prebid/server/bidder/BidderInfo.java
+++ b/src/main/java/org/prebid/server/bidder/BidderInfo.java
@@ -40,6 +40,8 @@ public class BidderInfo {
 
     Ortb ortb;
 
+    long tmaxDeductionMs;
+
     public static BidderInfo create(boolean enabled,
                                     OrtbVersion ortbVersion,
                                     boolean debugAllowed,
@@ -55,7 +57,8 @@ public static BidderInfo create(boolean enabled,
                                     boolean ccpaEnforced,
                                     boolean modifyingVastXmlAllowed,
                                     CompressionType compressionType,
-                                    org.prebid.server.spring.config.bidder.model.Ortb ortb) {
+                                    org.prebid.server.spring.config.bidder.model.Ortb ortb,
+                                    long tmaxDeductionMs) {
 
         return of(
                 enabled,
@@ -74,7 +77,8 @@ public static BidderInfo create(boolean enabled,
                 ccpaEnforced,
                 modifyingVastXmlAllowed,
                 compressionType,
-                Ortb.of(ortb.getMultiFormatSupported()));
+                Ortb.of(ortb.getMultiFormatSupported()),
+                tmaxDeductionMs);
     }
 
     private static PlatformInfo platformInfo(List<MediaType> mediaTypes) {
diff --git a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java
index 2a2fde46430..0345aa4c29e 100644
--- a/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java
+++ b/src/main/java/org/prebid/server/bidder/HttpBidderRequester.java
@@ -24,8 +24,9 @@
 import org.prebid.server.bidder.model.HttpResponse;
 import org.prebid.server.bidder.model.Result;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.log.ConditionalLogger;
 import org.prebid.server.log.Logger;
 import org.prebid.server.log.LoggerFactory;
 import org.prebid.server.model.CaseInsensitiveMultiMap;
@@ -62,24 +63,28 @@
 public class HttpBidderRequester {
 
     private static final Logger logger = LoggerFactory.getLogger(HttpBidderRequester.class);
+    private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger);
 
     private final HttpClient httpClient;
     private final BidderRequestCompletionTrackerFactory completionTrackerFactory;
     private final BidderErrorNotifier bidderErrorNotifier;
     private final HttpBidderRequestEnricher requestEnricher;
     private final JacksonMapper mapper;
+    private final double logSamplingRate;
 
     public HttpBidderRequester(HttpClient httpClient,
                                BidderRequestCompletionTrackerFactory completionTrackerFactory,
                                BidderErrorNotifier bidderErrorNotifier,
                                HttpBidderRequestEnricher requestEnricher,
-                               JacksonMapper mapper) {
+                               JacksonMapper mapper,
+                               double logSamplingRate) {
 
         this.httpClient = Objects.requireNonNull(httpClient);
         this.completionTrackerFactory = completionTrackerFactoryOrFallback(completionTrackerFactory);
         this.bidderErrorNotifier = Objects.requireNonNull(bidderErrorNotifier);
         this.requestEnricher = Objects.requireNonNull(requestEnricher);
         this.mapper = Objects.requireNonNull(mapper);
+        this.logSamplingRate = logSamplingRate;
     }
 
     /**
@@ -151,7 +156,7 @@ private static void rejectErrors(BidRejectionTracker bidRejectionTracker,
 
         bidderErrors.stream()
                 .filter(error -> CollectionUtils.isNotEmpty(error.getImpIds()))
-                .forEach(error -> bidRejectionTracker.reject(error.getImpIds(), reason));
+                .forEach(error -> bidRejectionTracker.rejectImps(error.getImpIds(), reason));
     }
 
     private <T> boolean isStoredResponse(List<HttpRequest<T>> httpRequests, String storedResponse, String bidder) {
@@ -241,9 +246,9 @@ private static byte[] gzip(byte[] value) {
     /**
      * Produces {@link Future} with {@link BidderCall} containing request and error description.
      */
-    private static <T> Future<BidderCall<T>> failResponse(Throwable exception, HttpRequest<T> httpRequest) {
-        logger.warn("Error occurred while sending HTTP request to a bidder url: {} with message: {}",
-                httpRequest.getUri(), exception.getMessage());
+    private <T> Future<BidderCall<T>> failResponse(Throwable exception, HttpRequest<T> httpRequest) {
+        conditionalLogger.warn("Error occurred while sending HTTP request to a bidder url: %s with message: %s"
+                        .formatted(httpRequest.getUri(), exception.getMessage()), logSamplingRate);
         logger.debug("Error occurred while sending HTTP request to a bidder url: {}",
                 exception, httpRequest.getUri());
 
@@ -392,7 +397,7 @@ private void handleBidderCallError(BidderCall<T> bidderCall) {
                     .orElse(null);
 
             if (statusCode != null && statusCode == HttpResponseStatus.SERVICE_UNAVAILABLE.code()) {
-                bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.ERROR_BIDDER_UNREACHABLE);
+                bidRejectionTracker.rejectImps(requestedImpIds, BidRejectionReason.ERROR_BIDDER_UNREACHABLE);
                 return;
             }
 
@@ -400,7 +405,7 @@ private void handleBidderCallError(BidderCall<T> bidderCall) {
                     && (statusCode < HttpResponseStatus.OK.code()
                     || statusCode >= HttpResponseStatus.BAD_REQUEST.code())) {
 
-                bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.ERROR_INVALID_BID_RESPONSE);
+                bidRejectionTracker.rejectImps(requestedImpIds, BidRejectionReason.ERROR_INVALID_BID_RESPONSE);
                 return;
             }
 
@@ -412,9 +417,9 @@ private void handleBidderCallError(BidderCall<T> bidderCall) {
             }
 
             if (callErrorType == BidderError.Type.timeout) {
-                bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.ERROR_TIMED_OUT);
+                bidRejectionTracker.rejectImps(requestedImpIds, BidRejectionReason.ERROR_TIMED_OUT);
             } else {
-                bidRejectionTracker.reject(requestedImpIds, BidRejectionReason.ERROR_GENERAL);
+                bidRejectionTracker.rejectImps(requestedImpIds, BidRejectionReason.ERROR_GENERAL);
             }
         }
 
diff --git a/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java b/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java
index b0207293e4d..fe0e8ad6a03 100644
--- a/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java
+++ b/src/main/java/org/prebid/server/bidder/displayio/DisplayioBidder.java
@@ -102,11 +102,8 @@ private BigDecimal resolveBidFloor(BidRequest bidRequest, Imp imp) {
         final BigDecimal bidFloor = imp.getBidfloor();
         final String bidFloorCurrency = imp.getBidfloorcur();
 
-        if (!BidderUtil.isValidPrice(bidFloor)) {
-            throw new PreBidException("BidFloor should be defined");
-        }
-
-        if (StringUtils.isNotBlank(bidFloorCurrency)
+        if (BidderUtil.isValidPrice(bidFloor)
+                && StringUtils.isNotBlank(bidFloorCurrency)
                 && !StringUtils.equalsIgnoreCase(bidFloorCurrency, BIDDER_CURRENCY)) {
             return currencyConversionService.convertCurrency(bidFloor, bidRequest, bidFloorCurrency, BIDDER_CURRENCY);
         }
diff --git a/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java b/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java
index 25578dafb75..77ec518e216 100644
--- a/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java
+++ b/src/main/java/org/prebid/server/bidder/epsilon/EpsilonBidder.java
@@ -262,7 +262,13 @@ private Bid updateBidWithId(Bid bid) {
     private static BidType getType(String impId, List<Imp> imps) {
         for (Imp imp : imps) {
             if (imp.getId().equals(impId)) {
-                return imp.getVideo() != null ? BidType.video : BidType.banner;
+                if (imp.getVideo() != null) {
+                    return BidType.video;
+                } else if (imp.getAudio() != null) {
+                    return BidType.audio;
+                } else {
+                    return BidType.banner;
+                }
             }
         }
         return BidType.banner;
diff --git a/src/main/java/org/prebid/server/bidder/flipp/FlippBidder.java b/src/main/java/org/prebid/server/bidder/flipp/FlippBidder.java
index 7fc42804347..dedd8399f1b 100644
--- a/src/main/java/org/prebid/server/bidder/flipp/FlippBidder.java
+++ b/src/main/java/org/prebid/server/bidder/flipp/FlippBidder.java
@@ -70,6 +70,8 @@ public class FlippBidder implements Bidder<CampaignRequestBody> {
     private static final Set<Integer> AD_TYPES = Set.of(4309, 641);
     private static final Set<Integer> DTX_TYPES = Set.of(5061);
     private static final String EXT_REQUEST_TRANSMIT_EIDS = "transmitEids";
+    private static final int DEFAULT_STANDARD_HEIGHT = 2400;
+    private static final int DEFAULT_COMPACT_HEIGHT = 600;
 
     private final String endpointUrl;
     private final JacksonMapper mapper;
@@ -144,7 +146,7 @@ private static PrebidRequest createPrebidRequest(Imp imp, ExtImpFlipp extImp) {
         final Format format = Optional.ofNullable(imp.getBanner())
                 .map(Banner::getFormat)
                 .filter(CollectionUtils::isNotEmpty)
-                .map(formats -> formats.getFirst())
+                .map(List::getFirst)
                 .orElse(null);
 
         return PrebidRequest.builder()
@@ -272,32 +274,38 @@ public final Result<List<BidderBid>> makeBids(BidderCall<CampaignRequestBody> ht
         }
     }
 
-    private static List<BidderBid> extractBids(CampaignResponseBody campaignResponseBody, BidRequest bidRequest) {
+    private List<BidderBid> extractBids(CampaignResponseBody campaignResponseBody, BidRequest bidRequest) {
         return Optional.ofNullable(campaignResponseBody)
                 .map(CampaignResponseBody::getDecisions)
                 .map(Decisions::getInline)
                 .stream()
                 .flatMap(Collection::stream)
-                .filter(inline -> isInlineValid(bidRequest, inline))
-                .map(inline -> BidderBid.of(constructBid(inline), BidType.banner, "USD"))
+                .map(inline -> makeBid(inline, getCorrespondingImp(bidRequest, inline)))
+                .filter(Objects::nonNull)
                 .toList();
     }
 
-    private static boolean isInlineValid(BidRequest bidRequest, Inline inline) {
+    private static Imp getCorrespondingImp(BidRequest bidRequest, Inline inline) {
         final String requestId = Optional.ofNullable(inline)
                 .map(Inline::getPrebid)
                 .map(Prebid::getRequestId)
                 .orElse(null);
 
-        return requestId != null && bidRequest.getImp().stream()
-                .map(Imp::getId)
-                .anyMatch(impId -> impId.equals(requestId));
+        return requestId != null
+                ? bidRequest.getImp().stream().filter(imp -> imp.getId().equals(requestId)).findFirst().orElse(null)
+                : null;
     }
 
-    private static Bid constructBid(Inline inline) {
+    private BidderBid makeBid(Inline inline, Imp imp) {
+        return imp == null
+                ? null
+                : BidderBid.of(constructBid(inline, parseImpExt(imp)), BidType.banner, "USD");
+    }
+
+    private static Bid constructBid(Inline inline, ExtImpFlipp extImp) {
         final Prebid prebid = inline.getPrebid();
         final Data data = Optional.ofNullable(inline.getContents())
-                .map(content -> content.getFirst())
+                .map(List::getFirst)
                 .map(Content::getData)
                 .orElse(null);
 
@@ -308,7 +316,21 @@ private static Bid constructBid(Inline inline) {
                 .id(Integer.toString(inline.getAdId()))
                 .impid(prebid.getRequestId())
                 .w(data != null ? data.getWidth() : null)
-                .h(data != null ? 0 : null)
+                .h(resolveHeight(data, extImp))
                 .build();
     }
+
+    private static Integer resolveHeight(Data data, ExtImpFlipp extImp) {
+        final boolean startCompact = Optional.ofNullable(extImp)
+                .map(ExtImpFlipp::getOptions)
+                .map(ExtImpFlippOptions::getStartCompact)
+                .orElse(false);
+
+        return Optional.ofNullable(data)
+                .map(Data::getCustomData)
+                .map(customData -> customData.get(startCompact ? "compactHeight" : "standardHeight"))
+                .filter(JsonNode::isNumber)
+                .map(JsonNode::asInt)
+                .orElse(startCompact ? DEFAULT_COMPACT_HEIGHT : DEFAULT_STANDARD_HEIGHT);
+    }
 }
diff --git a/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java b/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java
index e444038f329..62b0ad34155 100644
--- a/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java
+++ b/src/main/java/org/prebid/server/bidder/gotthamads/GothamAdsBidder.java
@@ -34,7 +34,7 @@ public class GothamAdsBidder implements Bidder<BidRequest> {
 
     private static final TypeReference<ExtPrebid<?, GothamAdsImpExt>> TYPE_REFERENCE = new TypeReference<>() {
     };
-    private static final String ACCOUNT_ID_MACRO = "{{AccountId}}";
+    private static final String ACCOUNT_ID_MACRO = "{{AccountID}}";
     private static final String X_OPENRTB_VERSION = "2.5";
 
     private final String endpointUrl;
diff --git a/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java b/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java
index a2cba1d3c36..e86a6182eb9 100644
--- a/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java
+++ b/src/main/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidder.java
@@ -4,11 +4,9 @@
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.iab.openrtb.request.BidRequest;
 import com.iab.openrtb.request.Imp;
-import com.iab.openrtb.request.User;
 import com.iab.openrtb.response.Bid;
 import com.iab.openrtb.response.BidResponse;
 import com.iab.openrtb.response.SeatBid;
@@ -26,13 +24,10 @@
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.proto.openrtb.ext.ExtPrebid;
-import org.prebid.server.proto.openrtb.ext.request.ConsentedProvidersSettings;
-import org.prebid.server.proto.openrtb.ext.request.ExtUser;
 import org.prebid.server.proto.openrtb.ext.request.improvedigital.ExtImpImprovedigital;
 import org.prebid.server.proto.openrtb.ext.response.BidType;
 import org.prebid.server.util.BidderUtil;
 import org.prebid.server.util.HttpUtil;
-import org.prebid.server.util.ObjectUtil;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -50,9 +45,6 @@ public class ImprovedigitalBidder implements Bidder<BidRequest> {
     private static final TypeReference<ExtPrebid<?, ExtImpImprovedigital>> IMPROVEDIGITAL_EXT_TYPE_REFERENCE =
             new TypeReference<>() {
             };
-    private static final String CONSENT_PROVIDERS_SETTINGS_OUT_KEY = "consented_providers_settings";
-    private static final String CONSENTED_PROVIDERS_KEY = "consented_providers";
-    private static final String REGEX_SPLIT_STRING_BY_DOT = "\\.";
 
     private static final String IS_REWARDED_INVENTORY_FIELD = "is_rewarded_inventory";
     private static final JsonPointer IS_REWARDED_INVENTORY_POINTER
@@ -89,46 +81,6 @@ public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request
         return Result.withValues(httpRequests);
     }
 
-    private ExtUser getAdditionalConsentProvidersUserExt(ExtUser extUser) {
-        final String consentedProviders = ObjectUtil.getIfNotNull(
-                ObjectUtil.getIfNotNull(extUser, ExtUser::getConsentedProvidersSettings),
-                ConsentedProvidersSettings::getConsentedProviders);
-
-        if (StringUtils.isBlank(consentedProviders)) {
-            return extUser;
-        }
-
-        final String[] consentedProvidersParts = StringUtils.split(consentedProviders, "~");
-        final String consentedProvidersPart = consentedProvidersParts.length > 1 ? consentedProvidersParts[1] : null;
-        if (StringUtils.isBlank(consentedProvidersPart)) {
-            return extUser;
-        }
-
-        return fillExtUser(extUser, consentedProvidersPart.split(REGEX_SPLIT_STRING_BY_DOT));
-    }
-
-    private ExtUser fillExtUser(ExtUser extUser, String[] arrayOfSplitString) {
-        final JsonNode consentProviderSettingJsonNode;
-        try {
-            consentProviderSettingJsonNode = customJsonNode(arrayOfSplitString);
-        } catch (IllegalArgumentException e) {
-            throw new PreBidException(e.getMessage());
-        }
-
-        return mapper.fillExtension(extUser, consentProviderSettingJsonNode);
-    }
-
-    private JsonNode customJsonNode(String[] arrayOfSplitString) {
-        final Integer[] integers = mapper.mapper().convertValue(arrayOfSplitString, Integer[].class);
-        final ArrayNode arrayNode = mapper.mapper().createArrayNode();
-        for (Integer integer : integers) {
-            arrayNode.add(integer);
-        }
-
-        return mapper.mapper().createObjectNode().set(CONSENT_PROVIDERS_SETTINGS_OUT_KEY,
-                mapper.mapper().createObjectNode().set(CONSENTED_PROVIDERS_KEY, arrayNode));
-    }
-
     private ExtImpImprovedigital parseImpExt(Imp imp) {
         try {
             return mapper.mapper().convertValue(imp.getExt(), IMPROVEDIGITAL_EXT_TYPE_REFERENCE).getBidder();
@@ -149,12 +101,8 @@ private static Imp updateImp(Imp imp) {
     }
 
     private HttpRequest<BidRequest> resolveRequest(BidRequest bidRequest, Imp imp, Integer publisherId) {
-        final User user = bidRequest.getUser();
         final BidRequest modifiedRequest = bidRequest.toBuilder()
                 .imp(Collections.singletonList(updateImp(imp)))
-                .user(user != null
-                        ? user.toBuilder().ext(getAdditionalConsentProvidersUserExt(user.getExt())).build()
-                        : null)
                 .build();
 
         final String pathPrefix = publisherId != null && publisherId > 0
diff --git a/src/main/java/org/prebid/server/bidder/insticator/InsticatorBidder.java b/src/main/java/org/prebid/server/bidder/insticator/InsticatorBidder.java
new file mode 100644
index 00000000000..562dd763473
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidder/insticator/InsticatorBidder.java
@@ -0,0 +1,266 @@
+package org.prebid.server.bidder.insticator;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Imp;
+import com.iab.openrtb.request.Publisher;
+import com.iab.openrtb.request.Site;
+import com.iab.openrtb.request.Video;
+import com.iab.openrtb.response.Bid;
+import com.iab.openrtb.response.BidResponse;
+import com.iab.openrtb.response.SeatBid;
+import io.vertx.core.MultiMap;
+import org.apache.commons.collections4.CollectionUtils;
+import org.prebid.server.bidder.Bidder;
+import org.prebid.server.bidder.model.BidderBid;
+import org.prebid.server.bidder.model.BidderCall;
+import org.prebid.server.bidder.model.BidderError;
+import org.prebid.server.bidder.model.HttpRequest;
+import org.prebid.server.bidder.model.Price;
+import org.prebid.server.bidder.model.Result;
+import org.prebid.server.currency.CurrencyConversionService;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.json.DecodeException;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.proto.openrtb.ext.ExtPrebid;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.insticator.ExtImpInsticator;
+import org.prebid.server.proto.openrtb.ext.response.BidType;
+import org.prebid.server.util.BidderUtil;
+import org.prebid.server.util.HttpUtil;
+import org.prebid.server.util.ObjectUtil;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+
+public class InsticatorBidder implements Bidder<BidRequest> {
+
+    private static final TypeReference<ExtPrebid<?, ExtImpInsticator>> TYPE_REFERENCE = new TypeReference<>() {
+    };
+
+    private static final String DEFAULT_BIDDER_CURRENCY = "USD";
+    private static final String INSTICATOR_FIELD = "insticator";
+    private static final InsticatorExtRequestCaller DEFAULT_INSTICATOR_CALLER =
+            InsticatorExtRequestCaller.of("Prebid-Server", "n/a");
+
+    private final CurrencyConversionService currencyConversionService;
+    private final String endpointUrl;
+    private final JacksonMapper mapper;
+
+    public InsticatorBidder(CurrencyConversionService currencyConversionService,
+                            String endpointUrl,
+                            JacksonMapper mapper) {
+
+        this.currencyConversionService = Objects.requireNonNull(currencyConversionService);
+        this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
+        this.mapper = Objects.requireNonNull(mapper);
+    }
+
+    @Override
+    public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
+        final Map<String, List<Imp>> groupedImps = new HashMap<>();
+        final List<BidderError> errors = new ArrayList<>();
+
+        String publisherId = null;
+
+        for (Imp imp : request.getImp()) {
+            try {
+                validateImp(imp);
+                final ExtImpInsticator extImp = parseImpExt(imp);
+
+                if (publisherId == null) {
+                    publisherId = extImp.getPublisherId();
+                }
+
+                final Imp modifiedImp = modifyImp(request, imp, extImp);
+                groupedImps.computeIfAbsent(extImp.getAdUnitId(), key -> new ArrayList<>()).add(modifiedImp);
+            } catch (PreBidException e) {
+                errors.add(BidderError.badInput(e.getMessage()));
+            }
+        }
+
+        final BidRequest modifiedRequest = modifyRequest(request, publisherId, errors);
+        final List<HttpRequest<BidRequest>> requests = groupedImps.values().stream()
+                .map(imps -> modifiedRequest.toBuilder().imp(imps).build())
+                .map(finalRequest -> BidderUtil.defaultRequest(
+                        finalRequest,
+                        makeHeaders(finalRequest.getDevice()),
+                        endpointUrl,
+                        mapper))
+                .toList();
+
+        return Result.of(requests, errors);
+    }
+
+    private void validateImp(Imp imp) {
+        final Video video = imp.getVideo();
+        if (video == null) {
+            return;
+        }
+
+        if (isInvalidDimension(video.getH())
+                || isInvalidDimension(video.getW())
+                || CollectionUtils.isNotEmpty(video.getMimes())) {
+
+            throw new PreBidException("One or more invalid or missing video field(s) w, h, mimes");
+        }
+    }
+
+    private static boolean isInvalidDimension(Integer dimension) {
+        return dimension == null || dimension == 0;
+    }
+
+    private ExtImpInsticator parseImpExt(Imp imp) {
+        try {
+            return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder();
+        } catch (IllegalArgumentException e) {
+            throw new PreBidException(e.getMessage());
+        }
+    }
+
+    private Imp modifyImp(BidRequest request, Imp imp, ExtImpInsticator extImp) {
+        final Price bidFloorPrice = resolveBidFloor(request, imp);
+        return imp.toBuilder()
+                .ext(mapper.mapper().createObjectNode().set(INSTICATOR_FIELD, mapper.mapper().valueToTree(extImp)))
+                .bidfloorcur(bidFloorPrice.getCurrency())
+                .bidfloor(bidFloorPrice.getValue())
+                .build();
+    }
+
+    private Price resolveBidFloor(BidRequest bidRequest, Imp imp) {
+        final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor());
+        return BidderUtil.isValidPrice(initialBidFloorPrice)
+                ? convertBidFloor(initialBidFloorPrice, bidRequest)
+                : initialBidFloorPrice;
+    }
+
+    private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) {
+        final BigDecimal convertedPrice = currencyConversionService.convertCurrency(
+                bidFloorPrice.getValue(),
+                bidRequest,
+                bidFloorPrice.getCurrency(),
+                DEFAULT_BIDDER_CURRENCY);
+
+        return Price.of(DEFAULT_BIDDER_CURRENCY, BidderUtil.roundFloor(convertedPrice));
+    }
+
+    private BidRequest modifyRequest(BidRequest request, String publisherId, List<BidderError> errors) {
+        return request.toBuilder()
+                .site(modifySite(request.getSite(), publisherId))
+                .app(modifyApp(request.getApp(), publisherId))
+                .ext(modifyExtRequest(request.getExt(), errors))
+                .build();
+    }
+
+    private static Site modifySite(Site site, String id) {
+        return Optional.ofNullable(site)
+                .map(Site::toBuilder)
+                .map(builder -> builder.publisher(modifyPublisher(site.getPublisher(), id)))
+                .map(Site.SiteBuilder::build)
+                .orElse(null);
+    }
+
+    private static App modifyApp(App app, String id) {
+        return Optional.ofNullable(app)
+                .map(App::toBuilder)
+                .map(builder -> builder.publisher(modifyPublisher(app.getPublisher(), id)))
+                .map(App.AppBuilder::build)
+                .orElse(null);
+    }
+
+    private static Publisher modifyPublisher(Publisher publisher, String id) {
+        return Optional.ofNullable(publisher)
+                .map(Publisher::toBuilder)
+                .orElseGet(Publisher::builder)
+                .id(id)
+                .build();
+    }
+
+    private ExtRequest modifyExtRequest(ExtRequest extRequest, List<BidderError> errors) {
+        final ExtRequest modifiedExtRequest = extRequest == null ? ExtRequest.empty() : extRequest;
+        final InsticatorExtRequest existingInsticator = getInsticatorExtRequest(modifiedExtRequest, errors);
+
+        modifiedExtRequest.addProperty(
+                INSTICATOR_FIELD,
+                mapper.mapper().valueToTree(buildInsticator(existingInsticator)));
+
+        return modifiedExtRequest;
+    }
+
+    private InsticatorExtRequest getInsticatorExtRequest(ExtRequest modifiedExtRequest, List<BidderError> errors) {
+        try {
+            return mapper.mapper().convertValue(
+                    modifiedExtRequest.getProperty(INSTICATOR_FIELD),
+                    InsticatorExtRequest.class);
+        } catch (IllegalArgumentException e) {
+            errors.add(BidderError.badInput(e.getMessage()));
+            return null;
+        }
+    }
+
+    private static InsticatorExtRequest buildInsticator(InsticatorExtRequest existingInsticator) {
+        if (existingInsticator == null || CollectionUtils.isEmpty(existingInsticator.getCaller())) {
+            return InsticatorExtRequest.of(Collections.singletonList(DEFAULT_INSTICATOR_CALLER));
+        }
+
+        final List<InsticatorExtRequestCaller> callers = new ArrayList<>(existingInsticator.getCaller());
+        callers.add(DEFAULT_INSTICATOR_CALLER);
+        return InsticatorExtRequest.of(Collections.unmodifiableList(callers));
+    }
+
+    private static MultiMap makeHeaders(Device device) {
+        final MultiMap headers = HttpUtil.headers();
+        HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER,
+                ObjectUtil.getIfNotNull(device, Device::getUa));
+        HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER,
+                ObjectUtil.getIfNotNull(device, Device::getIp));
+        HttpUtil.addHeaderIfValueIsNotEmpty(headers, "IP",
+                ObjectUtil.getIfNotNull(device, Device::getIp));
+        HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER,
+                ObjectUtil.getIfNotNull(device, Device::getIpv6));
+
+        return headers;
+    }
+
+    @Override
+    public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
+        try {
+            final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
+            return Result.withValues(extractBids(bidResponse));
+        } catch (DecodeException | PreBidException e) {
+            return Result.withError(BidderError.badServerResponse(e.getMessage()));
+        }
+    }
+
+    private static List<BidderBid> extractBids(BidResponse bidResponse) {
+        if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
+            return Collections.emptyList();
+        }
+
+        return bidResponse.getSeatbid().stream()
+                .filter(Objects::nonNull)
+                .map(SeatBid::getBid)
+                .filter(Objects::nonNull)
+                .flatMap(Collection::stream)
+                .filter(Objects::nonNull)
+                .map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur()))
+                .filter(Objects::nonNull)
+                .toList();
+    }
+
+    private static BidType getBidType(Bid bid) {
+        return switch (bid.getMtype()) {
+            case 2 -> BidType.video;
+            case null, default -> BidType.banner;
+        };
+    }
+}
diff --git a/src/main/java/org/prebid/server/bidder/insticator/InsticatorExtRequest.java b/src/main/java/org/prebid/server/bidder/insticator/InsticatorExtRequest.java
new file mode 100644
index 00000000000..079c7270ea5
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidder/insticator/InsticatorExtRequest.java
@@ -0,0 +1,12 @@
+package org.prebid.server.bidder.insticator;
+
+import lombok.Value;
+
+import java.util.List;
+
+@Value(staticConstructor = "of")
+public class InsticatorExtRequest {
+
+    List<InsticatorExtRequestCaller> caller;
+
+}
diff --git a/src/main/java/org/prebid/server/bidder/insticator/InsticatorExtRequestCaller.java b/src/main/java/org/prebid/server/bidder/insticator/InsticatorExtRequestCaller.java
new file mode 100644
index 00000000000..9b7ec994558
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidder/insticator/InsticatorExtRequestCaller.java
@@ -0,0 +1,12 @@
+package org.prebid.server.bidder.insticator;
+
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class InsticatorExtRequestCaller {
+
+    String name;
+
+    String version;
+
+}
diff --git a/src/main/java/org/prebid/server/bidder/ix/IxBidder.java b/src/main/java/org/prebid/server/bidder/ix/IxBidder.java
index 5c7c468af8f..5fb26e698fd 100644
--- a/src/main/java/org/prebid/server/bidder/ix/IxBidder.java
+++ b/src/main/java/org/prebid/server/bidder/ix/IxBidder.java
@@ -409,11 +409,14 @@ private static ExtBidPrebidVideo videoInfo(ExtBidPrebidVideo extBidPrebidVideo)
     private List<FledgeAuctionConfig> extractFledge(IxBidResponse bidResponse) {
         return Optional.ofNullable(bidResponse)
                 .map(IxBidResponse::getExt)
-                .map(IxExtBidResponse::getFledgeAuctionConfigs)
-                .orElse(Collections.emptyMap())
-                .entrySet()
+                .map(IxExtBidResponse::getProtectedAudienceAuctionConfigs)
+                .orElse(Collections.emptyList())
                 .stream()
-                .map(e -> FledgeAuctionConfig.builder().impId(e.getKey()).config(e.getValue()).build())
+                .filter(Objects::nonNull)
+                .map(ixAuctionConfig -> FledgeAuctionConfig.builder()
+                        .impId(ixAuctionConfig.getBidId())
+                        .config(ixAuctionConfig.getConfig())
+                        .build())
                 .toList();
     }
 }
diff --git a/src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java b/src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java
new file mode 100644
index 00000000000..709fab87429
--- /dev/null
+++ b/src/main/java/org/prebid/server/bidder/ix/model/response/AuctionConfigExtBidResponse.java
@@ -0,0 +1,14 @@
+package org.prebid.server.bidder.ix.model.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class AuctionConfigExtBidResponse {
+
+    @JsonProperty("bidId")
+    String bidId;
+
+    ObjectNode config;
+}
diff --git a/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java b/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java
index c292317d22c..c586817df2c 100644
--- a/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java
+++ b/src/main/java/org/prebid/server/bidder/ix/model/response/IxExtBidResponse.java
@@ -1,15 +1,14 @@
 package org.prebid.server.bidder.ix.model.response;
 
 import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.databind.node.ObjectNode;
 import lombok.Value;
 
-import java.util.Map;
+import java.util.List;
 
 @Value(staticConstructor = "of")
 public class IxExtBidResponse {
 
-    @JsonProperty("fledge_auction_configs")
-    Map<String, ObjectNode> fledgeAuctionConfigs;
+    @JsonProperty("protectedAudienceAuctionConfigs")
+    List<AuctionConfigExtBidResponse> protectedAudienceAuctionConfigs;
 
 }
diff --git a/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java b/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java
index 2ef79d3bfd4..6cdae97b5cb 100644
--- a/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java
+++ b/src/main/java/org/prebid/server/bidder/openx/OpenxBidder.java
@@ -44,6 +44,7 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 public class OpenxBidder implements Bidder<BidRequest> {
 
@@ -72,9 +73,12 @@ public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest bidRequ
                 .collect(Collectors.groupingBy(OpenxBidder::resolveImpType));
 
         final List<BidderError> processingErrors = new ArrayList<>();
-        final List<BidRequest> outgoingRequests = makeRequests(bidRequest,
+        final List<BidRequest> outgoingRequests = makeRequests(
+                bidRequest,
                 differentiatedImps.get(OpenxImpType.banner),
-                differentiatedImps.get(OpenxImpType.video), processingErrors);
+                differentiatedImps.get(OpenxImpType.video),
+                differentiatedImps.get(OpenxImpType.xNative),
+                processingErrors);
 
         final List<BidderError> errors = errors(differentiatedImps.get(OpenxImpType.other), processingErrors);
 
@@ -101,13 +105,21 @@ public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequ
         return Result.withError(BidderError.generic("Deprecated adapter method invoked"));
     }
 
-    private List<BidRequest> makeRequests(BidRequest bidRequest, List<Imp> bannerImps, List<Imp> videoImps,
-                                          List<BidderError> errors) {
+    private List<BidRequest> makeRequests(
+            BidRequest bidRequest,
+            List<Imp> bannerImps,
+            List<Imp> videoImps,
+            List<Imp> nativeImps,
+            List<BidderError> errors) {
         final List<BidRequest> bidRequests = new ArrayList<>();
-        // single request for all banner imps
-        final BidRequest bannerRequest = createSingleRequest(bannerImps, bidRequest, errors);
-        if (bannerRequest != null) {
-            bidRequests.add(bannerRequest);
+        // single request for all banner and native imps
+        final List<Imp> bannerAndNativeImps = Stream.of(bannerImps, nativeImps)
+                .filter(Objects::nonNull)
+                .flatMap(Collection::stream)
+                .toList();
+        final BidRequest bannerAndNativeImpsRequest = createSingleRequest(bannerAndNativeImps, bidRequest, errors);
+        if (bannerAndNativeImpsRequest != null) {
+            bidRequests.add(bannerAndNativeImpsRequest);
         }
 
         if (CollectionUtils.isNotEmpty(videoImps)) {
@@ -128,16 +140,33 @@ private static OpenxImpType resolveImpType(Imp imp) {
         if (imp.getVideo() != null) {
             return OpenxImpType.video;
         }
+        if (imp.getXNative() != null) {
+            return OpenxImpType.xNative;
+        }
         return OpenxImpType.other;
     }
 
+    private static BidType resolveBidType(Imp imp) {
+        if (imp.getBanner() != null) {
+            return BidType.banner;
+        }
+        if (imp.getVideo() != null) {
+            return BidType.video;
+        }
+        if (imp.getXNative() != null) {
+            return BidType.xNative;
+        }
+        return BidType.banner;
+    }
+
     private List<BidderError> errors(List<Imp> notSupportedImps, List<BidderError> processingErrors) {
         final List<BidderError> errors = new ArrayList<>();
         // add errors for imps with unsupported media types
         if (CollectionUtils.isNotEmpty(notSupportedImps)) {
             errors.addAll(
                     notSupportedImps.stream()
-                            .map(imp -> "OpenX only supports banner and video imps. Ignoring imp id=" + imp.getId())
+                            .map(imp ->
+                                    "OpenX only supports banner, video and native imps. Ignoring imp id=" + imp.getId())
                             .map(BidderError::badInput)
                             .toList());
         }
@@ -276,7 +305,7 @@ private static ExtBidPrebidVideo getVideoInfo(Bid bid) {
 
     private static Map<String, BidType> impIdToBidType(BidRequest bidRequest) {
         return bidRequest.getImp().stream()
-                .collect(Collectors.toMap(Imp::getId, imp -> imp.getBanner() != null ? BidType.banner : BidType.video));
+                .collect(Collectors.toMap(Imp::getId, OpenxBidder::resolveBidType));
     }
 
     private static BidType getBidType(Bid bid, Map<String, BidType> impIdToBidType) {
diff --git a/src/main/java/org/prebid/server/bidder/openx/model/OpenxImpType.java b/src/main/java/org/prebid/server/bidder/openx/model/OpenxImpType.java
index 7d9dfb4e5d5..c872e7f97e6 100644
--- a/src/main/java/org/prebid/server/bidder/openx/model/OpenxImpType.java
+++ b/src/main/java/org/prebid/server/bidder/openx/model/OpenxImpType.java
@@ -3,7 +3,7 @@
 public enum OpenxImpType {
 
     // supported
-    banner, video,
+    banner, video, xNative,
     // not supported
     other
 }
diff --git a/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java b/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java
index ce0a5161120..a73438b3ce3 100644
--- a/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java
+++ b/src/main/java/org/prebid/server/bidder/pgamssp/PgamSspBidder.java
@@ -14,15 +14,19 @@
 import org.prebid.server.bidder.model.BidderCall;
 import org.prebid.server.bidder.model.BidderError;
 import org.prebid.server.bidder.model.HttpRequest;
+import org.prebid.server.bidder.model.Price;
 import org.prebid.server.bidder.model.Result;
+import org.prebid.server.currency.CurrencyConversionService;
 import org.prebid.server.exception.PreBidException;
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.proto.openrtb.ext.ExtPrebid;
 import org.prebid.server.proto.openrtb.ext.request.pgamssp.PgamSspImpExt;
 import org.prebid.server.proto.openrtb.ext.response.BidType;
+import org.prebid.server.util.BidderUtil;
 import org.prebid.server.util.HttpUtil;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -35,12 +39,18 @@ public class PgamSspBidder implements Bidder<BidRequest> {
     };
     private static final String PUBLISHER_IMP_EXT_TYPE = "publisher";
     private static final String NETWORK_IMP_EXT_TYPE = "network";
+    private static final String DEFAULT_BID_CURRENCY = "USD";
 
     private final String endpointUrl;
+    private final CurrencyConversionService currencyConversionService;
     private final JacksonMapper mapper;
 
-    public PgamSspBidder(String endpointUrl, JacksonMapper mapper) {
+    public PgamSspBidder(String endpointUrl,
+                         CurrencyConversionService currencyConversionService,
+                         JacksonMapper mapper) {
+
         this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
+        this.currencyConversionService = Objects.requireNonNull(currencyConversionService);
         this.mapper = Objects.requireNonNull(mapper);
     }
 
@@ -51,7 +61,7 @@ public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request
         for (Imp imp : request.getImp()) {
             try {
                 final PgamSspImpExt impExt = parseImpExt(imp);
-                final BidRequest modifiedBidRequest = makeRequest(request, imp, impExt);
+                final BidRequest modifiedBidRequest = makeRequest(request, modifyImp(imp, request), impExt);
                 httpRequests.add(makeHttpRequest(modifiedBidRequest, imp.getId()));
             } catch (PreBidException e) {
                 return Result.withError(BidderError.badInput(e.getMessage()));
@@ -61,6 +71,31 @@ public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request
         return Result.withValues(httpRequests);
     }
 
+    private Imp modifyImp(Imp imp, BidRequest bidRequest) {
+        final Price resolvedBidFloor = resolveBidFloor(imp, bidRequest);
+        return imp.toBuilder()
+                .bidfloor(resolvedBidFloor.getValue())
+                .bidfloorcur(resolvedBidFloor.getCurrency())
+                .build();
+    }
+
+    private Price resolveBidFloor(Imp imp, BidRequest bidRequest) {
+        final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor());
+        return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, DEFAULT_BID_CURRENCY)
+                ? convertBidFloor(initialBidFloorPrice, bidRequest)
+                : initialBidFloorPrice;
+    }
+
+    private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) {
+        final BigDecimal convertedPrice = currencyConversionService.convertCurrency(
+                bidFloorPrice.getValue(),
+                bidRequest,
+                bidFloorPrice.getCurrency(),
+                DEFAULT_BID_CURRENCY);
+
+        return Price.of(DEFAULT_BID_CURRENCY, convertedPrice);
+    }
+
     private PgamSspImpExt parseImpExt(Imp imp) throws PreBidException {
         try {
             return mapper.mapper().convertValue(imp.getExt(), PGAMSSP_EXT_TYPE_REFERENCE).getBidder();
diff --git a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java
index a60a6ab2efa..d84465416f8 100644
--- a/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java
+++ b/src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java
@@ -15,6 +15,7 @@
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
 import org.prebid.server.bidder.Bidder;
 import org.prebid.server.bidder.model.BidderBid;
 import org.prebid.server.bidder.model.BidderCall;
@@ -32,6 +33,9 @@
 import org.prebid.server.exception.PreBidException;
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.proto.openrtb.ext.FlexibleExtension;
+import org.prebid.server.proto.openrtb.ext.request.ExtApp;
+import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
 import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmatic;
@@ -90,9 +94,12 @@ public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request
         String publisherId = null;
         PubmaticWrapper wrapper;
         final List<String> acat;
+        final Pair<String, String> displayManagerFields;
+
         try {
             acat = extractAcat(request);
             wrapper = extractWrapper(request);
+            displayManagerFields = extractDisplayManagerFields(request.getApp());
         } catch (IllegalArgumentException e) {
             return Result.withError(BidderError.badInput(e.getMessage()));
         }
@@ -110,7 +117,7 @@ public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request
 
                 wrapper = merge(wrapper, extImpPubmatic.getWrapper());
 
-                validImps.add(modifyImp(imp, impExt));
+                validImps.add(modifyImp(imp, impExt, displayManagerFields.getLeft(), displayManagerFields.getRight()));
             } catch (PreBidException e) {
                 errors.add(BidderError.badInput(e.getMessage()));
             }
@@ -151,6 +158,34 @@ private static JsonNode getExtRequestPrebidBidderparams(BidRequest request) {
         return bidderParams != null ? bidderParams.get(BIDDER_NAME) : null;
     }
 
+    private Pair<String, String> extractDisplayManagerFields(App app) {
+        String source;
+        String version;
+
+        final ExtApp extApp = app != null ? app.getExt() : null;
+        final ExtAppPrebid extAppPrebid = extApp != null ? extApp.getPrebid() : null;
+
+        source = extAppPrebid != null ? extAppPrebid.getSource() : null;
+        version = extAppPrebid != null ? extAppPrebid.getVersion() : null;
+        if (StringUtils.isNoneBlank(source, version)) {
+            return Pair.of(source, version);
+        }
+
+        source = getPropertyValue(extApp, "source");
+        version = getPropertyValue(extApp, "version");
+        return StringUtils.isNoneBlank(source, version)
+                ? Pair.of(source, version)
+                : Pair.of(null, null);
+    }
+
+    private static String getPropertyValue(FlexibleExtension flexibleExtension, String propertyName) {
+        return Optional.ofNullable(flexibleExtension)
+                .map(ext -> ext.getProperty(propertyName))
+                .filter(JsonNode::isValueNode)
+                .map(JsonNode::asText)
+                .orElse(null);
+    }
+
     private static void validateMediaType(Imp imp) {
         if (imp.getBanner() == null && imp.getVideo() == null && imp.getXNative() == null) {
             throw new PreBidException(
@@ -191,7 +226,7 @@ private static Integer stripToNull(Integer value) {
         return value == null || value == 0 ? null : value;
     }
 
-    private Imp modifyImp(Imp imp, PubmaticBidderImpExt impExt) {
+    private Imp modifyImp(Imp imp, PubmaticBidderImpExt impExt, String displayManager, String displayManagerVersion) {
         final Banner banner = imp.getBanner();
         final ExtImpPubmatic impExtBidder = impExt.getBidder();
 
@@ -201,6 +236,8 @@ private Imp modifyImp(Imp imp, PubmaticBidderImpExt impExt) {
                 .banner(banner != null ? assignSizesIfMissing(banner) : null)
                 .audio(null)
                 .bidfloor(resolveBidFloor(impExtBidder.getKadfloor(), imp.getBidfloor()))
+                .displaymanager(StringUtils.firstNonBlank(imp.getDisplaymanager(), displayManager))
+                .displaymanagerver(StringUtils.firstNonBlank(imp.getDisplaymanagerver(), displayManagerVersion))
                 .ext(!newExt.isEmpty() ? newExt : null);
 
         enrichWithAdSlotParameters(impBuilder, impExtBidder.getAdSlot(), banner);
@@ -400,7 +437,7 @@ private BidRequest modifyBidRequest(BidRequest request,
                 .imp(imps)
                 .site(modifySite(request.getSite(), publisherId))
                 .app(modifyApp(request.getApp(), publisherId))
-                .ext(modifyExtRequest(request.getExt(), wrapper, acat))
+                .ext(modifyExtRequest(wrapper, acat))
                 .build();
     }
 
@@ -426,7 +463,7 @@ private static Publisher modifyPublisher(Publisher publisher, String publisherId
                 : Publisher.builder().id(publisherId).build();
     }
 
-    private ExtRequest modifyExtRequest(ExtRequest extRequest, PubmaticWrapper wrapper, List<String> acat) {
+    private ExtRequest modifyExtRequest(PubmaticWrapper wrapper, List<String> acat) {
         final ObjectNode extNode = mapper.mapper().createObjectNode();
 
         if (wrapper != null) {
@@ -437,9 +474,10 @@ private ExtRequest modifyExtRequest(ExtRequest extRequest, PubmaticWrapper wrapp
             extNode.putPOJO(ACAT_EXT_REQUEST, acat);
         }
 
+        final ExtRequest newExtRequest = ExtRequest.empty();
         return extNode.isEmpty()
-                ? extRequest
-                : mapper.fillExtension(extRequest == null ? ExtRequest.empty() : extRequest, extNode);
+                ? newExtRequest
+                : mapper.fillExtension(newExtRequest, extNode);
     }
 
     private HttpRequest<BidRequest> makeHttpRequest(BidRequest request) {
diff --git a/src/main/java/org/prebid/server/bidder/rise/RiseBidder.java b/src/main/java/org/prebid/server/bidder/rise/RiseBidder.java
index 05a5dafa212..11967f78e85 100644
--- a/src/main/java/org/prebid/server/bidder/rise/RiseBidder.java
+++ b/src/main/java/org/prebid/server/bidder/rise/RiseBidder.java
@@ -132,6 +132,7 @@ private static BidType resolveBidType(Bid bid) throws PreBidException {
         return switch (markupType) {
             case 1 -> BidType.banner;
             case 2 -> BidType.video;
+            case 4 -> BidType.xNative;
             default ->
                     throw new PreBidException("Unsupported MType: %s, for bid: %s".formatted(markupType, bid.getId()));
         };
diff --git a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java
index 031f7590475..ebc7a7dccf4 100644
--- a/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java
+++ b/src/main/java/org/prebid/server/bidder/rubicon/RubiconBidder.java
@@ -154,6 +154,7 @@ public class RubiconBidder implements Bidder<BidRequest> {
     private static final String FPD_KEYWORDS_FIELD = "keywords";
     private static final String DFP_ADUNIT_CODE_FIELD = "dfp_ad_unit_code";
     private static final String STYPE_FIELD = "stype";
+    private static final String TID_FIELD = "tid";
     private static final String PREBID_EXT = "prebid";
     private static final String PBS_LOGIN = "pbs_login";
     private static final String PBS_VERSION = "pbs_version";
@@ -700,6 +701,7 @@ private RubiconImpExt makeImpExt(Imp imp,
                 .maxbids(getMaxBids(extRequest))
                 .gpid(getGpid(imp.getExt()))
                 .skadn(getSkadn(imp.getExt()))
+                .tid(getTid(imp.getExt()))
                 .prebid(rubiconImpExtPrebid)
                 .build();
     }
@@ -947,6 +949,11 @@ private ObjectNode getSkadn(ObjectNode impExt) {
         return skadnNode != null && skadnNode.isObject() ? (ObjectNode) skadnNode : null;
     }
 
+    private String getTid(ObjectNode impExt) {
+        final JsonNode tidNode = impExt.get(TID_FIELD);
+        return tidNode != null && tidNode.isTextual() ? tidNode.asText() : null;
+    }
+
     private String getAdSlot(Imp imp) {
         final ObjectNode dataNode = toObjectNode(imp.getExt().get(FPD_DATA_FIELD));
 
@@ -1617,19 +1624,27 @@ private List<BidderBid> bidsFromResponse(BidRequest prebidRequest,
     }
 
     private RubiconSeatBid updateSeatBids(RubiconSeatBid seatBid, List<BidderError> errors) {
-        final String buyer = seatBid.getBuyer();
-        final int networkId = NumberUtils.toInt(buyer, 0);
-        if (networkId <= 0) {
+        final Integer networkId = resolveNetworkId(seatBid);
+        final String seat = seatBid.getSeat();
+
+        if (networkId == null && seat == null) {
             return seatBid;
         }
+
         final List<RubiconBid> updatedBids = seatBid.getBid().stream()
-                .map(bid -> insertNetworkIdToMeta(bid, networkId, errors))
+                .map(bid -> prepareBidMeta(bid, seat, networkId, errors))
                 .filter(Objects::nonNull)
                 .toList();
         return seatBid.toBuilder().bid(updatedBids).build();
     }
 
-    private RubiconBid insertNetworkIdToMeta(RubiconBid bid, int networkId, List<BidderError> errors) {
+    private static Integer resolveNetworkId(RubiconSeatBid seatBid) {
+        final String buyer = seatBid.getBuyer();
+        final int networkId = NumberUtils.toInt(buyer, 0);
+        return networkId <= 0 ? null : networkId;
+    }
+
+    private RubiconBid prepareBidMeta(RubiconBid bid, String seat, Integer networkId, List<BidderError> errors) {
         final ObjectNode bidExt = bid.getExt();
         final ExtPrebid<ExtBidPrebid, ObjectNode> extPrebid;
         try {
@@ -1640,9 +1655,13 @@ private RubiconBid insertNetworkIdToMeta(RubiconBid bid, int networkId, List<Bid
         }
         final ExtBidPrebid extBidPrebid = extPrebid != null ? extPrebid.getPrebid() : null;
         final ExtBidPrebidMeta meta = extBidPrebid != null ? extBidPrebid.getMeta() : null;
-        final ExtBidPrebidMeta updatedMeta = meta != null
-                ? meta.toBuilder().networkId(networkId).build()
-                : ExtBidPrebidMeta.builder().networkId(networkId).build();
+
+        final ExtBidPrebidMeta updatedMeta = Optional.ofNullable(meta)
+                .map(ExtBidPrebidMeta::toBuilder)
+                .orElseGet(ExtBidPrebidMeta::builder)
+                .networkId(networkId)
+                .seat(seat)
+                .build();
 
         final ExtBidPrebid modifiedExtBidPrebid = extBidPrebid != null
                 ? extBidPrebid.toBuilder().meta(updatedMeta).build()
diff --git a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExt.java b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExt.java
index 1c39f9d9345..d40333b891a 100644
--- a/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExt.java
+++ b/src/main/java/org/prebid/server/bidder/rubicon/proto/request/RubiconImpExt.java
@@ -20,5 +20,7 @@ public class RubiconImpExt {
 
     ObjectNode skadn;
 
+    String tid;
+
     RubiconImpExtPrebid prebid;
 }
diff --git a/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java b/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java
index 414dc9844c3..7387617488a 100644
--- a/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java
+++ b/src/main/java/org/prebid/server/bidder/smaato/SmaatoBidder.java
@@ -64,14 +64,13 @@ public class SmaatoBidder implements Bidder<BidRequest> {
     private static final TypeReference<ExtPrebid<?, ExtImpSmaato>> SMAATO_EXT_TYPE_REFERENCE =
             new TypeReference<>() {
             };
-    private static final String CLIENT_VERSION = "prebid_server_1.1";
+    private static final String CLIENT_VERSION = "prebid_server_1.2";
     private static final String SMT_ADTYPE_HEADER = "X-Smt-Adtype";
     private static final String SMT_EXPIRES_HEADER = "X-Smt-Expires";
     private static final String SMT_AD_TYPE_IMG = "Img";
     private static final String SMT_ADTYPE_RICHMEDIA = "Richmedia";
     private static final String SMT_ADTYPE_VIDEO = "Video";
     private static final String SMT_ADTYPE_NATIVE = "Native";
-    private static final String IMP_EXT_SKADN_FIELD = "skadn";
 
     private static final int DEFAULT_TTL = 300;
 
@@ -204,7 +203,7 @@ private BidRequest preparePodRequest(BidRequest bidRequest, List<Imp> imps, List
             final String adBreakId = getIfNotNullOrThrow(extImpSmaato, ExtImpSmaato::getAdbreakId, "adbreakId");
 
             return modifyBidRequest(bidRequest, publisherId, () ->
-                    modifyImpsForAdBreak(imps, adBreakId, resolveImpExtSkadn(impExt)));
+                    modifyImpsForAdBreak(imps, adBreakId, removeBidderNodeFromImpExt(impExt)));
         } catch (PreBidException | IllegalArgumentException e) {
             errors.add(BidderError.badInput(e.getMessage()));
             return null;
@@ -231,14 +230,14 @@ private BidRequest modifyBidRequest(BidRequest bidRequest, String publisherId, S
         return bidRequestBuilder.imp(impSupplier.get()).build();
     }
 
-    private List<Imp> modifyImpsForAdBreak(List<Imp> imps, String adBreakId, ObjectNode impExtSkadn) {
+    private List<Imp> modifyImpsForAdBreak(List<Imp> imps, String adBreakId, ObjectNode impExt) {
         return IntStream.range(0, imps.size())
                 .mapToObj(idx ->
-                        modifyImpForAdBreak(imps.get(idx), idx + 1, adBreakId, idx == 0 ? impExtSkadn : null))
+                        modifyImpForAdBreak(imps.get(idx), idx + 1, adBreakId, idx == 0 ? impExt : null))
                 .toList();
     }
 
-    private Imp modifyImpForAdBreak(Imp imp, Integer sequence, String adBreakId, ObjectNode impExtSkadn) {
+    private Imp modifyImpForAdBreak(Imp imp, Integer sequence, String adBreakId, ObjectNode impExt) {
         final Video modifiedVideo = imp.getVideo().toBuilder()
                 .sequence(sequence)
                 .ext(mapper.mapper().createObjectNode().set("context", TextNode.valueOf("adpod")))
@@ -246,7 +245,7 @@ private Imp modifyImpForAdBreak(Imp imp, Integer sequence, String adBreakId, Obj
         return imp.toBuilder()
                 .tagid(adBreakId)
                 .video(modifiedVideo)
-                .ext(impExtSkadn)
+                .ext(impExt)
                 .build();
     }
 
@@ -293,27 +292,28 @@ private BidRequest prepareIndividualRequest(BidRequest bidRequest, Imp imp, List
             final String adSpaceId = getIfNotNullOrThrow(extImpSmaato, ExtImpSmaato::getAdspaceId, "adspaceId");
 
             return modifyBidRequest(bidRequest, publisherId, () ->
-                    modifyImpForAdSpace(imp, adSpaceId, resolveImpExtSkadn(impExt)));
+                    modifyImpForAdSpace(imp, adSpaceId, removeBidderNodeFromImpExt(impExt)));
         } catch (PreBidException | IllegalArgumentException e) {
             errors.add(BidderError.badInput(e.getMessage()));
             return null;
         }
     }
 
-    private ObjectNode resolveImpExtSkadn(ObjectNode impExt) {
-        if (!impExt.has(IMP_EXT_SKADN_FIELD)) {
+    private ObjectNode removeBidderNodeFromImpExt(ObjectNode impExt) {
+        if (impExt == null) {
             return null;
-        } else if (impExt.get(IMP_EXT_SKADN_FIELD).isEmpty() || !impExt.get(IMP_EXT_SKADN_FIELD).isObject()) {
-            throw new PreBidException("Invalid imp.ext.skadn");
-        } else {
-            return mapper.mapper().createObjectNode().set(IMP_EXT_SKADN_FIELD, impExt.get(IMP_EXT_SKADN_FIELD));
         }
+
+        final ObjectNode impExtCopy = impExt.deepCopy();
+
+        impExtCopy.remove("bidder");
+        return impExtCopy.isEmpty() ? null : impExtCopy;
     }
 
-    private List<Imp> modifyImpForAdSpace(Imp imp, String adSpaceId, ObjectNode impExtSkadn) {
+    private List<Imp> modifyImpForAdSpace(Imp imp, String adSpaceId, ObjectNode impExt) {
         final Imp modifiedImp = imp.toBuilder()
                 .tagid(adSpaceId)
-                .ext(impExtSkadn)
+                .ext(impExt)
                 .build();
 
         return Collections.singletonList(modifiedImp);
diff --git a/src/main/java/org/prebid/server/cache/CoreCacheService.java b/src/main/java/org/prebid/server/cache/CoreCacheService.java
index bcf839cd383..e86ac4f5d9b 100644
--- a/src/main/java/org/prebid/server/cache/CoreCacheService.java
+++ b/src/main/java/org/prebid/server/cache/CoreCacheService.java
@@ -5,6 +5,7 @@
 import com.fasterxml.jackson.databind.node.TextNode;
 import com.iab.openrtb.response.Bid;
 import io.vertx.core.Future;
+import io.vertx.core.MultiMap;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.auction.model.AuctionContext;
@@ -26,7 +27,7 @@
 import org.prebid.server.events.EventsContext;
 import org.prebid.server.events.EventsService;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.identity.UUIDIdGenerator;
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
@@ -61,8 +62,6 @@ public class CoreCacheService {
 
     private static final Logger logger = LoggerFactory.getLogger(CoreCacheService.class);
 
-    private static final Map<String, List<String>> DEBUG_HEADERS =
-            HttpUtil.toDebugHeaders(CacheServiceUtil.CACHE_HEADERS);
     private static final String BID_WURL_ATTRIBUTE = "wurl";
 
     private final HttpClient httpClient;
@@ -76,11 +75,16 @@ public class CoreCacheService {
     private final UUIDIdGenerator idGenerator;
     private final JacksonMapper mapper;
 
+    private final MultiMap cacheHeaders;
+    private final Map<String, List<String>> debugHeaders;
+
     public CoreCacheService(
             HttpClient httpClient,
             URL endpointUrl,
             String cachedAssetUrlTemplate,
             long expectedCacheTimeMs,
+            String apiKey,
+            boolean isApiKeySecured,
             VastModifier vastModifier,
             EventsService eventsService,
             Metrics metrics,
@@ -98,6 +102,11 @@ public CoreCacheService(
         this.clock = Objects.requireNonNull(clock);
         this.idGenerator = Objects.requireNonNull(idGenerator);
         this.mapper = Objects.requireNonNull(mapper);
+
+        cacheHeaders = isApiKeySecured
+                ? HttpUtil.headers().add(HttpUtil.X_PBC_API_KEY_HEADER, Objects.requireNonNull(apiKey))
+                : HttpUtil.headers();
+        debugHeaders = HttpUtil.toDebugHeaders(cacheHeaders);
     }
 
     public String getEndpointHost() {
@@ -121,7 +130,10 @@ public String cacheVideoDebugLog(CachedDebugLog cachedDebugLog, Integer videoCac
         final List<CachedCreative> cachedCreatives = Collections.singletonList(
                 makeDebugCacheCreative(cachedDebugLog, cacheKey, videoCacheTtl));
         final BidCacheRequest bidCacheRequest = toBidCacheRequest(cachedCreatives);
-        httpClient.post(endpointUrl.toString(), HttpUtil.headers(), mapper.encodeToString(bidCacheRequest),
+        httpClient.post(
+                endpointUrl.toString(),
+                cacheHeaders,
+                mapper.encodeToString(bidCacheRequest),
                 expectedCacheTimeMs);
         return cacheKey;
     }
@@ -155,7 +167,7 @@ private Future<BidCacheResponse> makeRequest(BidCacheRequest bidCacheRequest,
         final long startTime = clock.millis();
         return httpClient.post(
                         endpointUrl.toString(),
-                        CacheServiceUtil.CACHE_HEADERS,
+                        cacheHeaders,
                         mapper.encodeToString(bidCacheRequest),
                         remainingTimeout)
                 .map(response -> toBidCacheResponse(
@@ -238,7 +250,7 @@ private List<CacheBid> getCacheBids(List<BidInfo> bidInfos) {
     private List<CacheBid> getVideoCacheBids(List<BidInfo> bidInfos) {
         return bidInfos.stream()
                 .filter(bidInfo -> Objects.equals(bidInfo.getBidType(), BidType.video))
-                .map(bidInfo -> CacheBid.of(bidInfo, bidInfo.getVideoTtl()))
+                .map(bidInfo -> CacheBid.of(bidInfo, bidInfo.getVastTtl()))
                 .toList();
     }
 
@@ -286,7 +298,7 @@ private Future<CacheServiceResult> doCacheOpenrtb(List<CacheBid> bids,
         final CacheHttpRequest httpRequest = CacheHttpRequest.of(url, body);
 
         final long startTime = clock.millis();
-        return httpClient.post(url, CacheServiceUtil.CACHE_HEADERS, body, remainingTimeout)
+        return httpClient.post(url, cacheHeaders, body, remainingTimeout)
                 .map(response -> processResponseOpenrtb(response,
                         httpRequest,
                         cachedCreatives.size(),
@@ -348,7 +360,7 @@ private DebugHttpCall makeDebugHttpCall(String endpoint,
                 .responseStatus(httpResponse != null ? httpResponse.getStatusCode() : null)
                 .responseBody(httpResponse != null ? httpResponse.getBody() : null)
                 .responseTimeMillis(responseTime(startTime))
-                .requestHeaders(DEBUG_HEADERS)
+                .requestHeaders(debugHeaders)
                 .build();
     }
 
diff --git a/src/main/java/org/prebid/server/cookie/CookieSyncService.java b/src/main/java/org/prebid/server/cookie/CookieSyncService.java
index 2d381bfa665..fb15b5478d9 100644
--- a/src/main/java/org/prebid/server/cookie/CookieSyncService.java
+++ b/src/main/java/org/prebid/server/cookie/CookieSyncService.java
@@ -43,6 +43,7 @@
 import org.prebid.server.spring.config.bidder.model.usersync.CookieFamilySource;
 import org.prebid.server.util.HttpUtil;
 import org.prebid.server.util.ObjectUtil;
+import org.prebid.server.util.StreamUtil;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -54,7 +55,6 @@
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
-import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
@@ -385,16 +385,11 @@ private static Set<String> allowedBiddersByPriority(CookieSyncContext cookieSync
 
     private List<BidderUsersyncStatus> validStatuses(Set<String> biddersToSync, CookieSyncContext cookieSyncContext) {
         return biddersToSync.stream()
-                .filter(distinctBy(bidder -> bidderCatalog.cookieFamilyName(bidder).orElseThrow()))
+                .filter(StreamUtil.distinctBy(bidder -> bidderCatalog.cookieFamilyName(bidder).orElseThrow()))
                 .map(bidder -> validStatus(bidder, cookieSyncContext))
                 .toList();
     }
 
-    private static <T> Predicate<T> distinctBy(Function<? super T, ?> keyExtractor) {
-        final Set<Object> seen = new HashSet<>();
-        return value -> seen.add(keyExtractor.apply(value));
-    }
-
     private BidderUsersyncStatus validStatus(String bidder, CookieSyncContext cookieSyncContext) {
         final BiddersContext biddersContext = cookieSyncContext.getBiddersContext();
         final RoutingContext routingContext = cookieSyncContext.getRoutingContext();
diff --git a/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java b/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java
index 886c245122c..281313d25f5 100644
--- a/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java
+++ b/src/main/java/org/prebid/server/cookie/model/CookieSyncContext.java
@@ -8,7 +8,7 @@
 import org.prebid.server.auction.gpp.model.GppContext;
 import org.prebid.server.bidder.UsersyncMethodChooser;
 import org.prebid.server.cookie.UidsCookie;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.privacy.model.PrivacyContext;
 import org.prebid.server.proto.request.CookieSyncRequest;
 import org.prebid.server.settings.model.Account;
diff --git a/src/main/java/org/prebid/server/execution/RemoteFileProcessor.java b/src/main/java/org/prebid/server/execution/RemoteFileProcessor.java
deleted file mode 100644
index 8621e00dbce..00000000000
--- a/src/main/java/org/prebid/server/execution/RemoteFileProcessor.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.prebid.server.execution;
-
-import io.vertx.core.Future;
-
-/**
- * Contract fro services which use external files.
- */
-public interface RemoteFileProcessor {
-
-    Future<?> setDataPath(String dataFilePath);
-}
-
diff --git a/src/main/java/org/prebid/server/execution/file/FileProcessor.java b/src/main/java/org/prebid/server/execution/file/FileProcessor.java
new file mode 100644
index 00000000000..f17ab4758ee
--- /dev/null
+++ b/src/main/java/org/prebid/server/execution/file/FileProcessor.java
@@ -0,0 +1,8 @@
+package org.prebid.server.execution.file;
+
+import io.vertx.core.Future;
+
+public interface FileProcessor {
+
+    Future<?> setDataPath(String dataFilePath);
+}
diff --git a/src/main/java/org/prebid/server/execution/file/FileUtil.java b/src/main/java/org/prebid/server/execution/file/FileUtil.java
new file mode 100644
index 00000000000..3de28c1992f
--- /dev/null
+++ b/src/main/java/org/prebid/server/execution/file/FileUtil.java
@@ -0,0 +1,106 @@
+package org.prebid.server.execution.file;
+
+import io.vertx.core.Vertx;
+import io.vertx.core.file.FileProps;
+import io.vertx.core.file.FileSystem;
+import io.vertx.core.file.FileSystemException;
+import io.vertx.core.http.HttpClientOptions;
+import org.apache.commons.lang3.ObjectUtils;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.execution.file.syncer.FileSyncer;
+import org.prebid.server.execution.file.syncer.LocalFileSyncer;
+import org.prebid.server.execution.file.syncer.RemoteFileSyncerV2;
+import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy;
+import org.prebid.server.execution.retry.FixedIntervalRetryPolicy;
+import org.prebid.server.execution.retry.RetryPolicy;
+import org.prebid.server.spring.config.model.ExponentialBackoffProperties;
+import org.prebid.server.spring.config.model.FileSyncerProperties;
+import org.prebid.server.spring.config.model.HttpClientProperties;
+
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class FileUtil {
+
+    private FileUtil() {
+    }
+
+    public static void createAndCheckWritePermissionsFor(FileSystem fileSystem, String filePath) {
+        try {
+            final Path dirPath = Paths.get(filePath).getParent();
+            final String dirPathString = dirPath.toString();
+            final FileProps props = fileSystem.existsBlocking(dirPathString)
+                    ? fileSystem.propsBlocking(dirPathString)
+                    : null;
+
+            if (props == null || !props.isDirectory()) {
+                fileSystem.mkdirsBlocking(dirPathString);
+            } else if (!Files.isWritable(dirPath)) {
+                throw new PreBidException("No write permissions for directory: " + dirPath);
+            }
+        } catch (FileSystemException | InvalidPathException e) {
+            throw new PreBidException("Cannot create directory for file: " + filePath, e);
+        }
+    }
+
+    public static FileSyncer fileSyncerFor(FileProcessor fileProcessor,
+                                           FileSyncerProperties properties,
+                                           Vertx vertx) {
+
+        return switch (properties.getType()) {
+            case LOCAL -> new LocalFileSyncer(
+                    fileProcessor,
+                    properties.getSaveFilepath(),
+                    properties.getUpdateIntervalMs(),
+                    toRetryPolicy(properties),
+                    vertx);
+            case REMOTE -> remoteFileSyncer(fileProcessor, properties, vertx);
+        };
+    }
+
+    private static RemoteFileSyncerV2 remoteFileSyncer(FileProcessor fileProcessor,
+                                                       FileSyncerProperties properties,
+                                                       Vertx vertx) {
+
+        final HttpClientProperties httpClientProperties = properties.getHttpClient();
+        final HttpClientOptions httpClientOptions = new HttpClientOptions()
+                .setConnectTimeout(httpClientProperties.getConnectTimeoutMs())
+                .setMaxRedirects(httpClientProperties.getMaxRedirects());
+
+        return new RemoteFileSyncerV2(
+                fileProcessor,
+                properties.getDownloadUrl(),
+                properties.getSaveFilepath(),
+                properties.getTmpFilepath(),
+                vertx.createHttpClient(httpClientOptions),
+                properties.getTimeoutMs(),
+                properties.isCheckSize(),
+                properties.getUpdateIntervalMs(),
+                toRetryPolicy(properties),
+                vertx);
+    }
+
+    // TODO: remove after transition period
+    private static RetryPolicy toRetryPolicy(FileSyncerProperties properties) {
+        final Long retryIntervalMs = properties.getRetryIntervalMs();
+        final Integer retryCount = properties.getRetryCount();
+        final boolean fixedRetryPolicyDefined = ObjectUtils.anyNotNull(retryIntervalMs, retryCount);
+        final boolean fixedRetryPolicyValid = ObjectUtils.allNotNull(retryIntervalMs, retryCount)
+                || !fixedRetryPolicyDefined;
+
+        if (!fixedRetryPolicyValid) {
+            throw new IllegalArgumentException("fixed interval retry policy is invalid");
+        }
+
+        final ExponentialBackoffProperties exponentialBackoffProperties = properties.getRetry();
+        return fixedRetryPolicyDefined
+                ? FixedIntervalRetryPolicy.limited(retryIntervalMs, retryCount)
+                : ExponentialBackoffRetryPolicy.of(
+                exponentialBackoffProperties.getDelayMillis(),
+                exponentialBackoffProperties.getMaxDelayMillis(),
+                exponentialBackoffProperties.getFactor(),
+                exponentialBackoffProperties.getJitter());
+    }
+}
diff --git a/src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java b/src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java
new file mode 100644
index 00000000000..55517caa9a7
--- /dev/null
+++ b/src/main/java/org/prebid/server/execution/file/supplier/LocalFileSupplier.java
@@ -0,0 +1,47 @@
+package org.prebid.server.execution.file.supplier;
+
+import io.vertx.core.Future;
+import io.vertx.core.file.FileProps;
+import io.vertx.core.file.FileSystem;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Supplier;
+
+public class LocalFileSupplier implements Supplier<Future<String>> {
+
+    private final String filePath;
+    private final FileSystem fileSystem;
+    private final AtomicLong lastSupplyTime;
+
+    public LocalFileSupplier(String filePath, FileSystem fileSystem) {
+        this.filePath = Objects.requireNonNull(filePath);
+        this.fileSystem = Objects.requireNonNull(fileSystem);
+        lastSupplyTime = new AtomicLong(Long.MIN_VALUE);
+    }
+
+    @Override
+    public Future<String> get() {
+        return fileSystem.exists(filePath)
+                .compose(exists -> exists
+                        ? fileSystem.props(filePath)
+                        : Future.failedFuture("File %s not found.".formatted(filePath)))
+                .map(this::getFileIfModified);
+    }
+
+    private String getFileIfModified(FileProps fileProps) {
+        final long lastModifiedTime = lasModifiedTime(fileProps);
+        final long lastSupplyTime = this.lastSupplyTime.get();
+
+        if (lastSupplyTime < lastModifiedTime) {
+            this.lastSupplyTime.compareAndSet(lastSupplyTime, lastModifiedTime);
+            return filePath;
+        }
+
+        return null;
+    }
+
+    private static long lasModifiedTime(FileProps fileProps) {
+        return Math.max(fileProps.creationTime(), fileProps.lastModifiedTime());
+    }
+}
diff --git a/src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java b/src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java
new file mode 100644
index 00000000000..e8b8f313c54
--- /dev/null
+++ b/src/main/java/org/prebid/server/execution/file/supplier/RemoteFileSupplier.java
@@ -0,0 +1,160 @@
+package org.prebid.server.execution.file.supplier;
+
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.Future;
+import io.vertx.core.file.CopyOptions;
+import io.vertx.core.file.FileProps;
+import io.vertx.core.file.FileSystem;
+import io.vertx.core.file.OpenOptions;
+import io.vertx.core.http.HttpClient;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.http.HttpClientResponse;
+import io.vertx.core.http.HttpHeaders;
+import io.vertx.core.http.HttpMethod;
+import io.vertx.core.http.RequestOptions;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.execution.file.FileUtil;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
+import org.prebid.server.util.HttpUtil;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+public class RemoteFileSupplier implements Supplier<Future<String>> {
+
+    private static final Logger logger = LoggerFactory.getLogger(RemoteFileSupplier.class);
+
+    private final String savePath;
+    private final String backupPath;
+    private final String tmpPath;
+    private final HttpClient httpClient;
+    private final FileSystem fileSystem;
+
+    private final RequestOptions getRequestOptions;
+    private final RequestOptions headRequestOptions;
+
+    public RemoteFileSupplier(String downloadUrl,
+                              String savePath,
+                              String tmpPath,
+                              HttpClient httpClient,
+                              long timeout,
+                              boolean checkRemoteFileSize,
+                              FileSystem fileSystem) {
+
+        this.savePath = Objects.requireNonNull(savePath);
+        this.backupPath = savePath + ".old";
+        this.tmpPath = Objects.requireNonNull(tmpPath);
+        this.httpClient = Objects.requireNonNull(httpClient);
+        this.fileSystem = Objects.requireNonNull(fileSystem);
+
+        HttpUtil.validateUrl(downloadUrl);
+        FileUtil.createAndCheckWritePermissionsFor(fileSystem, savePath);
+        FileUtil.createAndCheckWritePermissionsFor(fileSystem, backupPath);
+        FileUtil.createAndCheckWritePermissionsFor(fileSystem, tmpPath);
+
+        getRequestOptions = new RequestOptions()
+                .setMethod(HttpMethod.GET)
+                .setTimeout(timeout)
+                .setAbsoluteURI(downloadUrl)
+                .setFollowRedirects(true);
+        headRequestOptions = checkRemoteFileSize
+                ? new RequestOptions()
+                .setMethod(HttpMethod.HEAD)
+                .setTimeout(timeout)
+                .setAbsoluteURI(downloadUrl)
+                .setFollowRedirects(true)
+                : null;
+    }
+
+    @Override
+    public Future<String> get() {
+        return isDownloadRequired().compose(isDownloadRequired -> isDownloadRequired
+                ? Future.all(downloadFile(), createBackup())
+                .compose(ignored -> tmpToSave())
+                .map(savePath)
+                : Future.succeededFuture());
+    }
+
+    private Future<Boolean> isDownloadRequired() {
+        return headRequestOptions != null
+                ? fileSystem.exists(savePath)
+                .compose(exists -> exists ? isSizeChanged() : Future.succeededFuture(true))
+                : Future.succeededFuture(true);
+    }
+
+    private Future<Boolean> isSizeChanged() {
+        final Future<Long> localFileSize = fileSystem.props(savePath).map(FileProps::size);
+        final Future<Long> remoteFileSize = sendHttpRequest(headRequestOptions)
+                .map(response -> response.getHeader(HttpHeaders.CONTENT_LENGTH))
+                .map(Long::parseLong);
+
+        return Future.all(localFileSize, remoteFileSize)
+                .map(compositeResult -> !Objects.equals(compositeResult.resultAt(0), compositeResult.resultAt(1)));
+    }
+
+    private Future<Void> downloadFile() {
+        return fileSystem.open(tmpPath, new OpenOptions())
+                .compose(tmpFile -> sendHttpRequest(getRequestOptions)
+                        .compose(response -> response.pipeTo(tmpFile))
+                        .onComplete(result -> tmpFile.close()));
+    }
+
+    private Future<HttpClientResponse> sendHttpRequest(RequestOptions requestOptions) {
+        return httpClient.request(requestOptions)
+                .compose(HttpClientRequest::send)
+                .map(this::validateResponse);
+    }
+
+    private HttpClientResponse validateResponse(HttpClientResponse response) {
+        final int statusCode = response.statusCode();
+        if (statusCode != HttpResponseStatus.OK.code()) {
+            throw new PreBidException("Got unexpected response from server with status code %s and message %s"
+                    .formatted(statusCode, response.statusMessage()));
+        }
+
+        return response;
+    }
+
+    private Future<Void> tmpToSave() {
+        return copyFile(tmpPath, savePath);
+    }
+
+    public void clearTmp() {
+        fileSystem.exists(tmpPath).onSuccess(exists -> {
+            if (exists) {
+                deleteFile(tmpPath);
+            }
+        });
+    }
+
+    private Future<Void> createBackup() {
+        return fileSystem.exists(savePath)
+                .compose(exists -> exists ? copyFile(savePath, backupPath) : Future.succeededFuture());
+    }
+
+    public void deleteBackup() {
+        fileSystem.exists(backupPath).onSuccess(exists -> {
+            if (exists) {
+                deleteFile(backupPath);
+            }
+        });
+    }
+
+    public Future<Void> restoreFromBackup() {
+        return fileSystem.exists(backupPath)
+                .compose(exists -> exists
+                        ? copyFile(backupPath, savePath)
+                        .onSuccess(ignored -> deleteFile(backupPath))
+                        : Future.succeededFuture());
+    }
+
+    private Future<Void> copyFile(String from, String to) {
+        return fileSystem.move(from, to, new CopyOptions().setReplaceExisting(true));
+    }
+
+    private void deleteFile(String filePath) {
+        fileSystem.delete(filePath)
+                .onFailure(error -> logger.error("Can't delete file: " + filePath));
+    }
+}
diff --git a/src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java b/src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java
new file mode 100644
index 00000000000..fd850e126c4
--- /dev/null
+++ b/src/main/java/org/prebid/server/execution/file/syncer/FileSyncer.java
@@ -0,0 +1,84 @@
+package org.prebid.server.execution.file.syncer;
+
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import org.prebid.server.execution.file.FileProcessor;
+import org.prebid.server.execution.retry.RetryPolicy;
+import org.prebid.server.execution.retry.Retryable;
+import org.prebid.server.log.Logger;
+import org.prebid.server.log.LoggerFactory;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+public abstract class FileSyncer {
+
+    private static final Logger logger = LoggerFactory.getLogger(FileSyncer.class);
+
+    private final FileProcessor fileProcessor;
+    private final long updatePeriod;
+    private final RetryPolicy retryPolicy;
+    private final Vertx vertx;
+
+    protected FileSyncer(FileProcessor fileProcessor,
+                         long updatePeriod,
+                         RetryPolicy retryPolicy,
+                         Vertx vertx) {
+
+        this.fileProcessor = Objects.requireNonNull(fileProcessor);
+        this.updatePeriod = updatePeriod;
+        this.retryPolicy = Objects.requireNonNull(retryPolicy);
+        this.vertx = Objects.requireNonNull(vertx);
+    }
+
+    public void sync() {
+        sync(retryPolicy);
+    }
+
+    private void sync(RetryPolicy currentRetryPolicy) {
+        getFile()
+                .compose(this::processFile)
+                .onSuccess(ignored -> onSuccess())
+                .onFailure(failure -> onFailure(currentRetryPolicy, failure));
+    }
+
+    protected abstract Future<String> getFile();
+
+    private Future<?> processFile(String filePath) {
+        return filePath != null
+                ? vertx.executeBlocking(() -> fileProcessor.setDataPath(filePath))
+                .compose(Function.identity())
+                .onFailure(error -> logger.error("Can't process saved file: " + filePath))
+                : Future.succeededFuture();
+    }
+
+    private void onSuccess() {
+        doOnSuccess().onComplete(ignored -> setUpDeferredUpdate());
+    }
+
+    protected abstract Future<Void> doOnSuccess();
+
+    private void setUpDeferredUpdate() {
+        if (updatePeriod > 0) {
+            vertx.setTimer(updatePeriod, ignored -> sync());
+        }
+    }
+
+    private void onFailure(RetryPolicy currentRetryPolicy, Throwable failure) {
+        doOnFailure(failure).onComplete(ignored -> retrySync(currentRetryPolicy));
+    }
+
+    protected abstract Future<Void> doOnFailure(Throwable throwable);
+
+    private void retrySync(RetryPolicy currentRetryPolicy) {
+        if (currentRetryPolicy instanceof Retryable policy) {
+            logger.info(
+                    "Retrying file sync for {} with policy: {}",
+                    fileProcessor.getClass().getSimpleName(),
+                    policy);
+            vertx.setTimer(policy.delay(), timerId -> sync(policy.next()));
+        } else {
+            setUpDeferredUpdate();
+        }
+    }
+}
diff --git a/src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java b/src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java
new file mode 100644
index 00000000000..6ea109185b5
--- /dev/null
+++ b/src/main/java/org/prebid/server/execution/file/syncer/LocalFileSyncer.java
@@ -0,0 +1,38 @@
+package org.prebid.server.execution.file.syncer;
+
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import org.prebid.server.execution.file.FileProcessor;
+import org.prebid.server.execution.file.supplier.LocalFileSupplier;
+import org.prebid.server.execution.retry.RetryPolicy;
+
+public class LocalFileSyncer extends FileSyncer {
+
+    private final LocalFileSupplier localFileSupplier;
+
+    public LocalFileSyncer(FileProcessor fileProcessor,
+                           String localFile,
+                           long updatePeriod,
+                           RetryPolicy retryPolicy,
+                           Vertx vertx) {
+
+        super(fileProcessor, updatePeriod, retryPolicy, vertx);
+
+        localFileSupplier = new LocalFileSupplier(localFile, vertx.fileSystem());
+    }
+
+    @Override
+    protected Future<String> getFile() {
+        return localFileSupplier.get();
+    }
+
+    @Override
+    protected Future<Void> doOnSuccess() {
+        return Future.succeededFuture();
+    }
+
+    @Override
+    protected Future<Void> doOnFailure(Throwable throwable) {
+        return Future.succeededFuture();
+    }
+}
diff --git a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncer.java
similarity index 84%
rename from src/main/java/org/prebid/server/execution/RemoteFileSyncer.java
rename to src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncer.java
index b841bf8a136..8deb838646f 100644
--- a/src/main/java/org/prebid/server/execution/RemoteFileSyncer.java
+++ b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncer.java
@@ -1,13 +1,11 @@
-package org.prebid.server.execution;
+package org.prebid.server.execution.file.syncer;
 
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.vertx.core.Future;
 import io.vertx.core.Promise;
 import io.vertx.core.Vertx;
 import io.vertx.core.file.CopyOptions;
-import io.vertx.core.file.FileProps;
 import io.vertx.core.file.FileSystem;
-import io.vertx.core.file.FileSystemException;
 import io.vertx.core.file.OpenOptions;
 import io.vertx.core.http.HttpClient;
 import io.vertx.core.http.HttpClientRequest;
@@ -17,22 +15,23 @@
 import io.vertx.core.http.RequestOptions;
 import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.exception.PreBidException;
+import org.prebid.server.execution.file.FileProcessor;
+import org.prebid.server.execution.file.FileUtil;
 import org.prebid.server.execution.retry.RetryPolicy;
 import org.prebid.server.execution.retry.Retryable;
 import org.prebid.server.log.Logger;
 import org.prebid.server.log.LoggerFactory;
 import org.prebid.server.util.HttpUtil;
 
-import java.nio.file.Files;
-import java.nio.file.InvalidPathException;
-import java.nio.file.Paths;
 import java.util.Objects;
+import java.util.function.Function;
 
+@Deprecated
 public class RemoteFileSyncer {
 
     private static final Logger logger = LoggerFactory.getLogger(RemoteFileSyncer.class);
 
-    private final RemoteFileProcessor processor;
+    private final FileProcessor processor;
     private final String downloadUrl;
     private final String saveFilePath;
     private final String tmpFilePath;
@@ -44,7 +43,7 @@ public class RemoteFileSyncer {
     private final RequestOptions getFileRequestOptions;
     private final RequestOptions isUpdateRequiredRequestOptions;
 
-    public RemoteFileSyncer(RemoteFileProcessor processor,
+    public RemoteFileSyncer(FileProcessor processor,
                             String downloadUrl,
                             String saveFilePath,
                             String tmpFilePath,
@@ -64,8 +63,8 @@ public RemoteFileSyncer(RemoteFileProcessor processor,
         this.vertx = Objects.requireNonNull(vertx);
         this.fileSystem = vertx.fileSystem();
 
-        createAndCheckWritePermissionsFor(fileSystem, saveFilePath);
-        createAndCheckWritePermissionsFor(fileSystem, tmpFilePath);
+        FileUtil.createAndCheckWritePermissionsFor(fileSystem, saveFilePath);
+        FileUtil.createAndCheckWritePermissionsFor(fileSystem, tmpFilePath);
 
         getFileRequestOptions = new RequestOptions()
                 .setMethod(HttpMethod.GET)
@@ -80,20 +79,6 @@ public RemoteFileSyncer(RemoteFileProcessor processor,
                 .setFollowRedirects(true);
     }
 
-    private static void createAndCheckWritePermissionsFor(FileSystem fileSystem, String filePath) {
-        try {
-            final String dirPath = Paths.get(filePath).getParent().toString();
-            final FileProps props = fileSystem.existsBlocking(dirPath) ? fileSystem.propsBlocking(dirPath) : null;
-            if (props == null || !props.isDirectory()) {
-                fileSystem.mkdirsBlocking(dirPath);
-            } else if (!Files.isWritable(Paths.get(dirPath))) {
-                throw new PreBidException("No write permissions for directory: " + dirPath);
-            }
-        } catch (FileSystemException | InvalidPathException e) {
-            throw new PreBidException("Cannot create directory for file: " + filePath, e);
-        }
-    }
-
     public void sync() {
         fileSystem.exists(saveFilePath)
                 .compose(exists -> exists ? processSavedFile() : syncRemoteFile(retryPolicy))
@@ -101,7 +86,8 @@ public void sync() {
     }
 
     private Future<Void> processSavedFile() {
-        return processor.setDataPath(saveFilePath)
+        return vertx.executeBlocking(() -> processor.setDataPath(saveFilePath))
+                .compose(Function.identity())
                 .onFailure(error -> logger.error("Can't process saved file: " + saveFilePath))
                 .recover(ignored -> deleteFile(saveFilePath).mapEmpty())
                 .mapEmpty();
diff --git a/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerV2.java b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerV2.java
new file mode 100644
index 00000000000..54755dccc19
--- /dev/null
+++ b/src/main/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerV2.java
@@ -0,0 +1,69 @@
+package org.prebid.server.execution.file.syncer;
+
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import io.vertx.core.file.FileSystem;
+import io.vertx.core.http.HttpClient;
+import org.prebid.server.execution.file.FileProcessor;
+import org.prebid.server.execution.file.supplier.LocalFileSupplier;
+import org.prebid.server.execution.file.supplier.RemoteFileSupplier;
+import org.prebid.server.execution.retry.RetryPolicy;
+
+public class RemoteFileSyncerV2 extends FileSyncer {
+
+    private final LocalFileSupplier localFileSupplier;
+    private final RemoteFileSupplier remoteFileSupplier;
+
+    public RemoteFileSyncerV2(FileProcessor fileProcessor,
+                              String downloadUrl,
+                              String saveFilePath,
+                              String tmpFilePath,
+                              HttpClient httpClient,
+                              long timeout,
+                              boolean checkSize,
+                              long updatePeriod,
+                              RetryPolicy retryPolicy,
+                              Vertx vertx) {
+
+        super(fileProcessor, updatePeriod, retryPolicy, vertx);
+
+        final FileSystem fileSystem = vertx.fileSystem();
+        localFileSupplier = new LocalFileSupplier(saveFilePath, fileSystem);
+        remoteFileSupplier = new RemoteFileSupplier(
+                downloadUrl,
+                saveFilePath,
+                tmpFilePath,
+                httpClient,
+                timeout,
+                checkSize,
+                fileSystem);
+    }
+
+    @Override
+    protected Future<String> getFile() {
+        return localFileSupplier.get()
+                .otherwiseEmpty()
+                .compose(localFile -> localFile != null
+                        ? Future.succeededFuture(localFile)
+                        : remoteFileSupplier.get());
+    }
+
+    @Override
+    protected Future<Void> doOnSuccess() {
+        remoteFileSupplier.clearTmp();
+        remoteFileSupplier.deleteBackup();
+        forceLastSupplyTimeUpdate();
+        return Future.succeededFuture();
+    }
+
+    @Override
+    protected Future<Void> doOnFailure(Throwable throwable) {
+        remoteFileSupplier.clearTmp();
+        return remoteFileSupplier.restoreFromBackup()
+                .onSuccess(ignore -> forceLastSupplyTimeUpdate());
+    }
+
+    private void forceLastSupplyTimeUpdate() {
+        localFileSupplier.get();
+    }
+}
diff --git a/src/main/java/org/prebid/server/execution/Timeout.java b/src/main/java/org/prebid/server/execution/timeout/Timeout.java
similarity index 95%
rename from src/main/java/org/prebid/server/execution/Timeout.java
rename to src/main/java/org/prebid/server/execution/timeout/Timeout.java
index f5abf239c87..b0f37e439fc 100644
--- a/src/main/java/org/prebid/server/execution/Timeout.java
+++ b/src/main/java/org/prebid/server/execution/timeout/Timeout.java
@@ -1,4 +1,4 @@
-package org.prebid.server.execution;
+package org.prebid.server.execution.timeout;
 
 import lombok.Getter;
 
diff --git a/src/main/java/org/prebid/server/execution/TimeoutFactory.java b/src/main/java/org/prebid/server/execution/timeout/TimeoutFactory.java
similarity index 95%
rename from src/main/java/org/prebid/server/execution/TimeoutFactory.java
rename to src/main/java/org/prebid/server/execution/timeout/TimeoutFactory.java
index cbe2768af1a..ae2624c8585 100644
--- a/src/main/java/org/prebid/server/execution/TimeoutFactory.java
+++ b/src/main/java/org/prebid/server/execution/timeout/TimeoutFactory.java
@@ -1,4 +1,4 @@
-package org.prebid.server.execution;
+package org.prebid.server.execution.timeout;
 
 import java.time.Clock;
 
diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java
index ee77a6bbcb5..132bf86e782 100644
--- a/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java
+++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorEnforcer.java
@@ -179,7 +179,7 @@ private AuctionParticipation applyEnforcement(BidRequest bidRequest,
                         "Bid with id '%s' was rejected by floor enforcement: price %s is below the floor %s"
                                 .formatted(bid.getId(), price, floor), impId));
 
-                rejectionTracker.reject(impId, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR);
+                rejectionTracker.rejectBid(bidderBid, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR);
                 updatedBidderBids.remove(bidderBid);
             }
         }
diff --git a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java
index c8163131c6e..91559480537 100644
--- a/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java
+++ b/src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java
@@ -36,6 +36,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.ThreadLocalRandom;
 
 public class BasicPriceFloorProcessor implements PriceFloorProcessor {
@@ -45,6 +46,7 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor {
 
     private static final int SKIP_RATE_MIN = 0;
     private static final int SKIP_RATE_MAX = 100;
+    private static final int USE_FETCH_DATA_RATE_MAX = 100;
     private static final int MODEL_WEIGHT_MAX_VALUE = 100;
     private static final int MODEL_WEIGHT_MIN_VALUE = 1;
 
@@ -126,19 +128,32 @@ private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, Li
         final FetchResult fetchResult = floorFetcher.fetch(account);
         final FetchStatus fetchStatus = ObjectUtil.getIfNotNull(fetchResult, FetchResult::getFetchStatus);
 
-        if (shouldUseDynamicData(account) && fetchResult != null && fetchStatus == FetchStatus.success) {
+        if (fetchResult != null && fetchStatus == FetchStatus.success && shouldUseDynamicData(account, fetchResult)) {
             final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData());
             return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch);
         }
 
         if (requestFloors != null) {
             try {
-                PriceFloorRulesValidator.validateRules(requestFloors, Integer.MAX_VALUE);
+                final Optional<AccountPriceFloorsConfig> priceFloorsConfig = Optional.ofNullable(account)
+                        .map(Account::getAuction)
+                        .map(AccountAuctionConfig::getPriceFloors);
+
+                final Long maxRules = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxRules)
+                        .orElse(null);
+                final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDims)
+                        .orElse(null);
+
+                PriceFloorRulesValidator.validateRules(
+                        requestFloors,
+                        PriceFloorsConfigResolver.resolveMaxValue(maxRules),
+                        PriceFloorsConfigResolver.resolveMaxValue(maxDimensions));
+
                 return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request);
             } catch (PreBidException e) {
-                errors.add("Failed to parse price floors from request, with a reason : %s ".formatted(e.getMessage()));
+                errors.add("Failed to parse price floors from request, with a reason: %s".formatted(e.getMessage()));
                 conditionalLogger.error(
-                        "Failed to parse price floors from request with id: '%s', with a reason : %s "
+                        "Failed to parse price floors from request with id: '%s', with a reason: %s"
                                 .formatted(bidRequest.getId(), e.getMessage()),
                         0.01d);
             }
@@ -147,13 +162,20 @@ private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, Li
         return createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData);
     }
 
-    private static boolean shouldUseDynamicData(Account account) {
-        final AccountAuctionConfig auctionConfig = ObjectUtil.getIfNotNull(account, Account::getAuction);
-        final AccountPriceFloorsConfig floorsConfig =
-                ObjectUtil.getIfNotNull(auctionConfig, AccountAuctionConfig::getPriceFloors);
+    private static boolean shouldUseDynamicData(Account account, FetchResult fetchResult) {
+        final boolean isUsingDynamicDataAllowed = Optional.ofNullable(account)
+                .map(Account::getAuction)
+                .map(AccountAuctionConfig::getPriceFloors)
+                .map(AccountPriceFloorsConfig::getUseDynamicData)
+                .map(BooleanUtils::isNotFalse)
+                .orElse(true);
+
+        final boolean shouldUseDynamicData = Optional.ofNullable(fetchResult.getRulesData())
+                .map(PriceFloorData::getUseFetchDataRate)
+                .map(rate -> ThreadLocalRandom.current().nextInt(USE_FETCH_DATA_RATE_MAX) < rate)
+                .orElse(true);
 
-        return BooleanUtils.isNotFalse(
-                ObjectUtil.getIfNotNull(floorsConfig, AccountPriceFloorsConfig::getUseDynamicData));
+        return isUsingDynamicDataAllowed && shouldUseDynamicData;
     }
 
     private PriceFloorRules mergeFloors(PriceFloorRules requestFloors,
diff --git a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java
index 0692dce090a..b7d4ac4185f 100644
--- a/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java
+++ b/src/main/java/org/prebid/server/floors/PriceFloorFetcher.java
@@ -13,7 +13,7 @@
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.http.HttpStatus;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.floors.model.PriceFloorData;
 import org.prebid.server.floors.model.PriceFloorDebugProperties;
 import org.prebid.server.floors.proto.FetchResult;
@@ -176,7 +176,10 @@ private ResponseCacheInfo parseFloorResponse(HttpClientResponse httpClientRespon
         }
 
         final PriceFloorData priceFloorData = parsePriceFloorData(body, accountId);
-        PriceFloorRulesValidator.validateRulesData(priceFloorData, resolveMaxRules(fetchConfig.getMaxRules()));
+        PriceFloorRulesValidator.validateRulesData(
+                priceFloorData,
+                PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxRules()),
+                PriceFloorsConfigResolver.resolveMaxValue(fetchConfig.getMaxSchemaDims()));
 
         return ResponseCacheInfo.of(priceFloorData,
                 FetchStatus.success,
@@ -194,12 +197,6 @@ private PriceFloorData parsePriceFloorData(String body, String accountId) {
         return priceFloorData;
     }
 
-    private static int resolveMaxRules(Long accountMaxRules) {
-        return accountMaxRules != null && !accountMaxRules.equals(0L)
-                ? Math.toIntExact(accountMaxRules)
-                : Integer.MAX_VALUE;
-    }
-
     private Long cacheTtlFromResponse(HttpClientResponse httpClientResponse, String fetchUrl) {
         final String cacheControlValue = httpClientResponse.getHeaders().get(HttpHeaders.CACHE_CONTROL);
         final Matcher cacheHeaderMatcher = StringUtils.isNotBlank(cacheControlValue)
diff --git a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java
index b976ea69c97..028686f82d4 100644
--- a/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java
+++ b/src/main/java/org/prebid/server/floors/PriceFloorRulesValidator.java
@@ -4,12 +4,16 @@
 import org.apache.commons.collections4.MapUtils;
 import org.prebid.server.exception.PreBidException;
 import org.prebid.server.floors.model.PriceFloorData;
+import org.prebid.server.floors.model.PriceFloorField;
 import org.prebid.server.floors.model.PriceFloorModelGroup;
 import org.prebid.server.floors.model.PriceFloorRules;
+import org.prebid.server.floors.model.PriceFloorSchema;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 
 public class PriceFloorRulesValidator {
 
@@ -17,11 +21,13 @@ public class PriceFloorRulesValidator {
     private static final int MODEL_WEIGHT_MIN_VALUE = 1;
     private static final int SKIP_RATE_MIN = 0;
     private static final int SKIP_RATE_MAX = 100;
+    private static final int USE_FETCH_DATA_RATE_MIN = 0;
+    private static final int USE_FETCH_DATA_RATE_MAX = 100;
 
     private PriceFloorRulesValidator() {
     }
 
-    public static void validateRules(PriceFloorRules priceFloorRules, Integer maxRules) {
+    public static void validateRules(PriceFloorRules priceFloorRules, Integer maxRules, Integer maxDimensions) {
 
         final Integer rootSkipRate = priceFloorRules.getSkipRate();
         if (rootSkipRate != null && (rootSkipRate < SKIP_RATE_MIN || rootSkipRate > SKIP_RATE_MAX)) {
@@ -34,10 +40,10 @@ public static void validateRules(PriceFloorRules priceFloorRules, Integer maxRul
             throw new PreBidException("Price floor floorMin must be positive float, but was " + floorMin);
         }
 
-        validateRulesData(priceFloorRules.getData(), maxRules);
+        validateRulesData(priceFloorRules.getData(), maxRules, maxDimensions);
     }
 
-    public static void validateRulesData(PriceFloorData priceFloorData, Integer maxRules) {
+    public static void validateRulesData(PriceFloorData priceFloorData, Integer maxRules, Integer maxDimensions) {
         if (priceFloorData == null) {
             throw new PreBidException("Price floor rules data must be present");
         }
@@ -48,16 +54,24 @@ public static void validateRulesData(PriceFloorData priceFloorData, Integer maxR
                     "Price floor data skipRate must be in range(0-100), but was " + dataSkipRate);
         }
 
+        final Integer useFetchDataRate = priceFloorData.getUseFetchDataRate();
+        if (useFetchDataRate != null
+                && (useFetchDataRate < USE_FETCH_DATA_RATE_MIN || useFetchDataRate > USE_FETCH_DATA_RATE_MAX)) {
+
+            throw new PreBidException(
+                    "Price floor data useFetchDataRate must be in range(0-100), but was " + useFetchDataRate);
+        }
+
         if (CollectionUtils.isEmpty(priceFloorData.getModelGroups())) {
             throw new PreBidException("Price floor rules should contain at least one model group");
         }
 
         priceFloorData.getModelGroups().stream()
                 .filter(Objects::nonNull)
-                .forEach(modelGroup -> validateModelGroup(modelGroup, maxRules));
+                .forEach(modelGroup -> validateModelGroup(modelGroup, maxRules, maxDimensions));
     }
 
-    private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer maxRules) {
+    private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer maxRules, Integer maxDimensions) {
         final Integer modelWeight = modelGroup.getModelWeight();
         if (modelWeight != null
                 && (modelWeight < MODEL_WEIGHT_MIN_VALUE || modelWeight > MODEL_WEIGHT_MAX_VALUE)) {
@@ -85,8 +99,21 @@ private static void validateModelGroup(PriceFloorModelGroup modelGroup, Integer
         }
 
         if (maxRules != null && values.size() > maxRules) {
-            throw new PreBidException(
-                    "Price floor rules number %s exceeded its maximum number %s".formatted(values.size(), maxRules));
+            throw new PreBidException("Price floor rules number %s exceeded its maximum number %s"
+                    .formatted(values.size(), maxRules));
+        }
+
+        final List<PriceFloorField> fields = Optional.ofNullable(modelGroup.getSchema())
+                .map(PriceFloorSchema::getFields)
+                .orElse(null);
+
+        if (CollectionUtils.isEmpty(fields)) {
+            throw new PreBidException("Price floor dimensions can't be null or empty, but were " + fields);
+        }
+
+        if (maxDimensions != null && fields.size() > maxDimensions) {
+            throw new PreBidException("Price floor schema dimensions %s exceeded its maximum number %s"
+                    .formatted(fields.size(), maxDimensions));
         }
     }
 }
diff --git a/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java b/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java
index 73eb62cfe84..14834e24b7c 100644
--- a/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java
+++ b/src/main/java/org/prebid/server/floors/PriceFloorsConfigResolver.java
@@ -23,13 +23,15 @@ public class PriceFloorsConfigResolver {
     private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger);
 
     private static final int MIN_MAX_AGE_SEC_VALUE = 600;
+    private static final int MAX_AGE_SEC_VALUE = Integer.MAX_VALUE;
     private static final int MIN_PERIODIC_SEC_VALUE = 300;
     private static final int MIN_TIMEOUT_MS_VALUE = 10;
     private static final int MAX_TIMEOUT_MS_VALUE = 10_000;
     private static final int MIN_RULES_VALUE = 0;
-    private static final int MIN_FILE_SIZE_VALUE = 0;
-    private static final int MAX_AGE_SEC_VALUE = Integer.MAX_VALUE;
     private static final int MAX_RULES_VALUE = Integer.MAX_VALUE;
+    private static final int MIN_DIMENSIONS_VALUE = 0;
+    private static final int MAX_DIMENSIONS_VALUE = 19;
+    private static final int MIN_FILE_SIZE_VALUE = 0;
     private static final int MAX_FILE_SIZE_VALUE = Integer.MAX_VALUE;
     private static final int MIN_ENFORCE_RATE_VALUE = 0;
     private static final int MAX_ENFORCE_RATE_VALUE = 100;
@@ -71,6 +73,16 @@ private static void validatePriceFloorConfig(Account account) {
             throw new PreBidException(invalidPriceFloorsPropertyMessage("enforce-floors-rate", enforceRate));
         }
 
+        final Long maxRules = floorsConfig.getMaxRules();
+        if (maxRules != null && isNotInRange(maxRules, MIN_RULES_VALUE, MAX_RULES_VALUE)) {
+            throw new PreBidException(invalidPriceFloorsPropertyMessage("max-rules", maxRules));
+        }
+
+        final Long maxDimensions = floorsConfig.getMaxSchemaDims();
+        if (maxDimensions != null && isNotInRange(maxDimensions, MIN_DIMENSIONS_VALUE, MAX_DIMENSIONS_VALUE)) {
+            throw new PreBidException(invalidPriceFloorsPropertyMessage("max-schema-dimensions", maxDimensions));
+        }
+
         final AccountPriceFloorsFetchConfig fetchConfig =
                 ObjectUtil.getIfNotNull(floorsConfig, AccountPriceFloorsConfig::getFetch);
 
@@ -108,6 +120,11 @@ private static void validatePriceFloorsFetchConfig(AccountPriceFloorsFetchConfig
             throw new PreBidException(invalidPriceFloorsPropertyMessage("max-rules", maxRules));
         }
 
+        final Long maxDimensions = fetchConfig.getMaxSchemaDims();
+        if (maxDimensions != null && isNotInRange(maxDimensions, MIN_DIMENSIONS_VALUE, MAX_DIMENSIONS_VALUE)) {
+            throw new PreBidException(invalidPriceFloorsPropertyMessage("max-schema-dimensions", maxDimensions));
+        }
+
         final Long maxFileSize = fetchConfig.getMaxFileSizeKb();
         if (maxFileSize != null && isNotInRange(maxFileSize, MIN_FILE_SIZE_VALUE, MAX_FILE_SIZE_VALUE)) {
             throw new PreBidException(invalidPriceFloorsPropertyMessage("max-file-size-kb", maxFileSize));
@@ -121,4 +138,8 @@ private static boolean isNotInRange(long number, long min, long max) {
     private static String invalidPriceFloorsPropertyMessage(String property, Object value) {
         return "Invalid price-floors property '%s', value passed: %s".formatted(property, value);
     }
+
+    public static int resolveMaxValue(Long value) {
+        return value != null && !value.equals(0L) ? Math.toIntExact(value) : Integer.MAX_VALUE;
+    }
 }
diff --git a/src/main/java/org/prebid/server/floors/model/PriceFloorData.java b/src/main/java/org/prebid/server/floors/model/PriceFloorData.java
index bdca465af36..4604ed91892 100644
--- a/src/main/java/org/prebid/server/floors/model/PriceFloorData.java
+++ b/src/main/java/org/prebid/server/floors/model/PriceFloorData.java
@@ -18,6 +18,9 @@ public class PriceFloorData {
     @JsonProperty("skipRate")
     Integer skipRate;
 
+    @JsonProperty("useFetchDataRate")
+    Integer useFetchDataRate;
+
     @JsonProperty("floorsSchemaVersion")
     String floorsSchemaVersion;
 
diff --git a/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java
index 791a1da06c1..268de61b246 100755
--- a/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java
+++ b/src/main/java/org/prebid/server/geolocation/CircuitBreakerSecuredGeoLocationService.java
@@ -2,7 +2,7 @@
 
 import io.vertx.core.Future;
 import io.vertx.core.Vertx;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.geolocation.model.GeoInfo;
 import org.prebid.server.log.ConditionalLogger;
 import org.prebid.server.log.Logger;
diff --git a/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java
index 72ec6feb2b2..30d78ea27c0 100644
--- a/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java
+++ b/src/main/java/org/prebid/server/geolocation/ConfigurationGeoLocationService.java
@@ -1,7 +1,7 @@
 package org.prebid.server.geolocation;
 
 import io.vertx.core.Future;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.geolocation.model.GeoInfo;
 import org.prebid.server.geolocation.model.GeoInfoConfiguration;
 
diff --git a/src/main/java/org/prebid/server/geolocation/GeoLocationService.java b/src/main/java/org/prebid/server/geolocation/GeoLocationService.java
index 7604a25c71a..3d4c582db38 100644
--- a/src/main/java/org/prebid/server/geolocation/GeoLocationService.java
+++ b/src/main/java/org/prebid/server/geolocation/GeoLocationService.java
@@ -1,7 +1,7 @@
 package org.prebid.server.geolocation;
 
 import io.vertx.core.Future;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.geolocation.model.GeoInfo;
 
 /**
diff --git a/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java b/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java
index 2cea7119714..5afa9311cba 100644
--- a/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java
+++ b/src/main/java/org/prebid/server/geolocation/MaxMindGeoLocationService.java
@@ -14,8 +14,8 @@
 import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
 import org.apache.commons.lang3.StringUtils;
-import org.prebid.server.execution.RemoteFileProcessor;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.file.FileProcessor;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.geolocation.model.GeoInfo;
 
 import java.io.FileInputStream;
@@ -28,7 +28,7 @@
  * Implementation of the {@link GeoLocationService}
  * backed by <a href="https://dev.maxmind.com/geoip/geoip2/geolite2/">MaxMind free database</a>
  */
-public class MaxMindGeoLocationService implements GeoLocationService, RemoteFileProcessor {
+public class MaxMindGeoLocationService implements GeoLocationService, FileProcessor {
 
     private static final String VENDOR = "maxmind";
 
diff --git a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java
index 746dffb5fc7..3ac0f44069c 100644
--- a/src/main/java/org/prebid/server/handler/CookieSyncHandler.java
+++ b/src/main/java/org/prebid/server/handler/CookieSyncHandler.java
@@ -24,8 +24,8 @@
 import org.prebid.server.cookie.model.CookieSyncContext;
 import org.prebid.server.cookie.model.PartitionedCookie;
 import org.prebid.server.exception.InvalidAccountConfigException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.log.ConditionalLogger;
diff --git a/src/main/java/org/prebid/server/handler/NotificationEventHandler.java b/src/main/java/org/prebid/server/handler/NotificationEventHandler.java
index 971cb82204f..60e11195c26 100644
--- a/src/main/java/org/prebid/server/handler/NotificationEventHandler.java
+++ b/src/main/java/org/prebid/server/handler/NotificationEventHandler.java
@@ -18,7 +18,7 @@
 import org.prebid.server.events.EventRequest;
 import org.prebid.server.events.EventUtil;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.log.Logger;
 import org.prebid.server.log.LoggerFactory;
 import org.prebid.server.model.Endpoint;
diff --git a/src/main/java/org/prebid/server/handler/SetuidHandler.java b/src/main/java/org/prebid/server/handler/SetuidHandler.java
index c036bb310cd..728285fb0f6 100644
--- a/src/main/java/org/prebid/server/handler/SetuidHandler.java
+++ b/src/main/java/org/prebid/server/handler/SetuidHandler.java
@@ -2,6 +2,7 @@
 
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.vertx.core.AsyncResult;
+import io.vertx.core.CompositeFuture;
 import io.vertx.core.Future;
 import io.vertx.core.http.Cookie;
 import io.vertx.core.http.HttpHeaders;
@@ -9,9 +10,10 @@
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.http.HttpServerResponse;
 import io.vertx.ext.web.RoutingContext;
-import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
 import org.prebid.server.activity.Activity;
 import org.prebid.server.activity.ComponentType;
 import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
@@ -26,7 +28,6 @@
 import org.prebid.server.auction.privacy.contextfactory.SetuidPrivacyContextFactory;
 import org.prebid.server.bidder.BidderCatalog;
 import org.prebid.server.bidder.UsersyncFormat;
-import org.prebid.server.bidder.UsersyncMethod;
 import org.prebid.server.bidder.UsersyncMethodType;
 import org.prebid.server.bidder.UsersyncUtil;
 import org.prebid.server.bidder.Usersyncer;
@@ -37,8 +38,8 @@
 import org.prebid.server.cookie.model.UidsCookieUpdateResult;
 import org.prebid.server.exception.InvalidAccountConfigException;
 import org.prebid.server.exception.InvalidRequestException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.log.Logger;
 import org.prebid.server.log.LoggerFactory;
 import org.prebid.server.metric.Metrics;
@@ -50,20 +51,23 @@
 import org.prebid.server.privacy.gdpr.model.TcfResponse;
 import org.prebid.server.settings.ApplicationSettings;
 import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.AccountGdprConfig;
+import org.prebid.server.settings.model.AccountPrivacyConfig;
 import org.prebid.server.util.HttpUtil;
+import org.prebid.server.util.StreamUtil;
 import org.prebid.server.vertx.verticles.server.HttpEndpoint;
 import org.prebid.server.vertx.verticles.server.application.ApplicationResource;
 
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Consumer;
 import java.util.function.Function;
-import java.util.function.Supplier;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
-import java.util.stream.Stream;
 
 public class SetuidHandler implements ApplicationResource {
 
@@ -85,7 +89,7 @@ public class SetuidHandler implements ApplicationResource {
     private final AnalyticsReporterDelegator analyticsDelegator;
     private final Metrics metrics;
     private final TimeoutFactory timeoutFactory;
-    private final Map<String, UsersyncMethodType> cookieNameToSyncType;
+    private final Map<String, Pair<String, UsersyncMethodType>> cookieNameToBidderAndSyncType;
 
     public SetuidHandler(long defaultTimeout,
                          UidsCookieService uidsCookieService,
@@ -109,52 +113,57 @@ public SetuidHandler(long defaultTimeout,
         this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator);
         this.metrics = Objects.requireNonNull(metrics);
         this.timeoutFactory = Objects.requireNonNull(timeoutFactory);
-        this.cookieNameToSyncType = collectMap(bidderCatalog);
+        this.cookieNameToBidderAndSyncType = collectUsersyncers(bidderCatalog);
     }
 
-    private static Map<String, UsersyncMethodType> collectMap(BidderCatalog bidderCatalog) {
+    private static Map<String, Pair<String, UsersyncMethodType>> collectUsersyncers(BidderCatalog bidderCatalog) {
+        validateUsersyncersDuplicates(bidderCatalog);
+
+        return bidderCatalog.usersyncReadyBidders().stream()
+                .sorted(Comparator.comparing(bidderName -> BooleanUtils.toInteger(bidderCatalog.isAlias(bidderName))))
+                .filter(StreamUtil.distinctBy(bidderCatalog::cookieFamilyName))
+                .map(bidderName -> bidderCatalog.usersyncerByName(bidderName)
+                        .map(usersyncer -> Pair.of(bidderName, usersyncer)))
+                .flatMap(Optional::stream)
+                .collect(Collectors.toMap(
+                        pair -> pair.getRight().getCookieFamilyName(),
+                        pair -> Pair.of(pair.getLeft(), preferredUserSyncType(pair.getRight()))));
+    }
 
-        final Supplier<Stream<Usersyncer>> usersyncers = () -> bidderCatalog.names()
-                .stream()
-                .filter(bidderCatalog::isActive)
+    private static void validateUsersyncersDuplicates(BidderCatalog bidderCatalog) {
+        final List<String> duplicatedCookieFamilyNames = bidderCatalog.usersyncReadyBidders().stream()
+                .filter(bidderName -> !isAliasWithRootCookieFamilyName(bidderCatalog, bidderName))
                 .map(bidderCatalog::usersyncerByName)
-                .filter(Optional::isPresent)
-                .map(Optional::get)
-                .distinct();
-
-        validateUsersyncers(usersyncers.get());
+                .flatMap(Optional::stream)
+                .map(Usersyncer::getCookieFamilyName)
+                .filter(Predicate.not(StreamUtil.distinctBy(Function.identity())))
+                .distinct()
+                .toList();
 
-        return usersyncers.get()
-                .collect(Collectors.toMap(Usersyncer::getCookieFamilyName, SetuidHandler::preferredUserSyncType));
+        if (!duplicatedCookieFamilyNames.isEmpty()) {
+            throw new IllegalArgumentException(
+                    "Duplicated \"cookie-family-name\" found, values: "
+                            + String.join(", ", duplicatedCookieFamilyNames));
+        }
     }
 
-    @Override
-    public List<HttpEndpoint> endpoints() {
-        return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.setuid.value()));
+    private static boolean isAliasWithRootCookieFamilyName(BidderCatalog bidderCatalog, String bidder) {
+        final String bidderCookieFamilyName = bidderCatalog.cookieFamilyName(bidder).orElse(StringUtils.EMPTY);
+        final String parentCookieFamilyName =
+                bidderCatalog.cookieFamilyName(bidderCatalog.resolveBaseBidder(bidder)).orElse(null);
+
+        return bidderCatalog.isAlias(bidder)
+                && parentCookieFamilyName != null
+                && parentCookieFamilyName.equals(bidderCookieFamilyName);
     }
 
     private static UsersyncMethodType preferredUserSyncType(Usersyncer usersyncer) {
-        return Stream.of(usersyncer.getIframe(), usersyncer.getRedirect())
-                .filter(Objects::nonNull)
-                .findFirst()
-                .map(UsersyncMethod::getType)
-                .get(); // when usersyncer is present, it will contain at least one method
+        return ObjectUtils.firstNonNull(usersyncer.getIframe(), usersyncer.getRedirect()).getType();
     }
 
-    private static void validateUsersyncers(Stream<Usersyncer> usersyncers) {
-        final List<String> cookieFamilyNameDuplicates = usersyncers.map(Usersyncer::getCookieFamilyName)
-                .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
-                .entrySet()
-                .stream()
-                .filter(name -> name.getValue() > 1)
-                .map(Map.Entry::getKey)
-                .distinct()
-                .toList();
-        if (CollectionUtils.isNotEmpty(cookieFamilyNameDuplicates)) {
-            throw new IllegalArgumentException(
-                    "Duplicated \"cookie-family-name\" found, values: "
-                            + String.join(", ", cookieFamilyNameDuplicates));
-        }
+    @Override
+    public List<HttpEndpoint> endpoints() {
+        return Collections.singletonList(HttpEndpoint.of(HttpMethod.GET, Endpoint.setuid.value()));
     }
 
     @Override
@@ -170,6 +179,11 @@ private Future<SetuidContext> toSetuidContext(RoutingContext routingContext) {
         final String requestAccount = httpRequest.getParam(ACCOUNT_PARAM);
         final Timeout timeout = timeoutFactory.create(defaultTimeout);
 
+        final UsersyncMethodType syncType = Optional.ofNullable(cookieName)
+                .map(cookieNameToBidderAndSyncType::get)
+                .map(Pair::getRight)
+                .orElse(null);
+
         return accountById(requestAccount, timeout)
                 .compose(account -> setuidPrivacyContextFactory.contextFrom(httpRequest, account, timeout)
                         .map(privacyContext -> SetuidContext.builder()
@@ -178,7 +192,7 @@ private Future<SetuidContext> toSetuidContext(RoutingContext routingContext) {
                                 .timeout(timeout)
                                 .account(account)
                                 .cookieName(cookieName)
-                                .syncType(cookieNameToSyncType.get(cookieName))
+                                .syncType(syncType)
                                 .privacyContext(privacyContext)
                                 .build()))
 
@@ -208,35 +222,46 @@ private void handleSetuidContextResult(AsyncResult<SetuidContext> setuidContextR
 
         if (setuidContextResult.succeeded()) {
             final SetuidContext setuidContext = setuidContextResult.result();
-            final String bidder = setuidContext.getCookieName();
+            final String bidderCookieFamily = setuidContext.getCookieName();
             final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext();
 
             try {
-                validateSetuidContext(setuidContext, bidder);
+                validateSetuidContext(setuidContext, bidderCookieFamily);
             } catch (InvalidRequestException | UnauthorizedUidsException | UnavailableForLegalReasonsException e) {
                 handleErrors(e, routingContext, tcfContext);
                 return;
             }
 
-            isAllowedForHostVendorId(tcfContext)
-                    .onComplete(hostTcfResponseResult -> respondByTcfResponse(hostTcfResponseResult, setuidContext));
+            final AccountPrivacyConfig privacyConfig = setuidContext.getAccount().getPrivacy();
+            final AccountGdprConfig accountGdprConfig = privacyConfig != null ? privacyConfig.getGdpr() : null;
+
+            final String bidderName = cookieNameToBidderAndSyncType.get(bidderCookieFamily).getLeft();
+
+            Future.all(
+                            tcfDefinerService.isAllowedForHostVendorId(tcfContext),
+                            tcfDefinerService.resultForBidderNames(
+                                    Collections.singleton(bidderName), tcfContext, accountGdprConfig))
+                    .onComplete(hostTcfResponseResult -> respondByTcfResponse(
+                            hostTcfResponseResult,
+                            bidderName,
+                            setuidContext));
         } else {
             final Throwable error = setuidContextResult.cause();
             handleErrors(error, routingContext, null);
         }
     }
 
-    private void validateSetuidContext(SetuidContext setuidContext, String bidder) {
+    private void validateSetuidContext(SetuidContext setuidContext, String bidderCookieFamily) {
         final String cookieName = setuidContext.getCookieName();
         final boolean isCookieNameBlank = StringUtils.isBlank(cookieName);
-        if (isCookieNameBlank || !cookieNameToSyncType.containsKey(cookieName)) {
+        if (isCookieNameBlank || !cookieNameToBidderAndSyncType.containsKey(cookieName)) {
             final String cookieNameError = isCookieNameBlank ? "required" : "invalid";
             throw new InvalidRequestException("\"bidder\" query param is " + cookieNameError);
         }
 
         final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext();
         if (tcfContext.isInGdprScope() && !tcfContext.isConsentValid()) {
-            metrics.updateUserSyncTcfInvalidMetric(bidder);
+            metrics.updateUserSyncTcfInvalidMetric(bidderCookieFamily);
             throw new InvalidRequestException("Consent string is invalid");
         }
 
@@ -247,7 +272,7 @@ private void validateSetuidContext(SetuidContext setuidContext, String bidder) {
 
         final ActivityInfrastructure activityInfrastructure = setuidContext.getActivityInfrastructure();
         final ActivityInvocationPayload activityInvocationPayload = TcfContextActivityInvocationPayload.of(
-                ActivityInvocationPayloadImpl.of(ComponentType.BIDDER, bidder),
+                ActivityInvocationPayloadImpl.of(ComponentType.BIDDER, bidderCookieFamily),
                 tcfContext);
 
         if (!activityInfrastructure.isAllowed(Activity.SYNC_USER, activityInvocationPayload)) {
@@ -255,47 +280,30 @@ private void validateSetuidContext(SetuidContext setuidContext, String bidder) {
         }
     }
 
-    /**
-     * If host vendor id is null, host allowed to setuid.
-     */
-    private Future<HostVendorTcfResponse> isAllowedForHostVendorId(TcfContext tcfContext) {
-        final Integer gdprHostVendorId = tcfDefinerService.getGdprHostVendorId();
-        return gdprHostVendorId == null
-                ? Future.succeededFuture(HostVendorTcfResponse.allowedVendor())
-                : tcfDefinerService.resultForVendorIds(Collections.singleton(gdprHostVendorId), tcfContext)
-                .map(this::toHostVendorTcfResponse);
-    }
-
-    private HostVendorTcfResponse toHostVendorTcfResponse(TcfResponse<Integer> tcfResponse) {
-        return HostVendorTcfResponse.of(tcfResponse.getUserInGdprScope(), tcfResponse.getCountry(),
-                isSetuidAllowed(tcfResponse));
-    }
-
-    private boolean isSetuidAllowed(TcfResponse<Integer> hostTcfResponseToSetuidContext) {
-        // allow cookie only if user is not in GDPR scope or vendor passed GDPR check
-        final boolean notInGdprScope = BooleanUtils.isFalse(hostTcfResponseToSetuidContext.getUserInGdprScope());
-
-        final Map<Integer, PrivacyEnforcementAction> vendorIdToAction = hostTcfResponseToSetuidContext.getActions();
-        final PrivacyEnforcementAction hostPrivacyAction = vendorIdToAction != null
-                ? vendorIdToAction.get(tcfDefinerService.getGdprHostVendorId())
-                : null;
-        final boolean blockPixelSync = hostPrivacyAction == null || hostPrivacyAction.isBlockPixelSync();
-
-        return notInGdprScope || !blockPixelSync;
-    }
-
-    private void respondByTcfResponse(AsyncResult<HostVendorTcfResponse> hostTcfResponseResult,
+    private void respondByTcfResponse(AsyncResult<CompositeFuture> hostTcfResponseResult,
+                                      String bidderName,
                                       SetuidContext setuidContext) {
-        final String bidderCookieName = setuidContext.getCookieName();
+
         final TcfContext tcfContext = setuidContext.getPrivacyContext().getTcfContext();
         final RoutingContext routingContext = setuidContext.getRoutingContext();
 
         if (hostTcfResponseResult.succeeded()) {
-            final HostVendorTcfResponse hostTcfResponse = hostTcfResponseResult.result();
-            if (hostTcfResponse.isVendorAllowed()) {
+            final CompositeFuture compositeFuture = hostTcfResponseResult.result();
+            final HostVendorTcfResponse hostVendorTcfResponse = compositeFuture.resultAt(0);
+            final TcfResponse<String> bidderTcfResponse = compositeFuture.resultAt(1);
+
+            final Map<String, PrivacyEnforcementAction> vendorIdToAction = bidderTcfResponse.getActions();
+            final PrivacyEnforcementAction action = vendorIdToAction != null
+                    ? vendorIdToAction.get(bidderName)
+                    : null;
+
+            final boolean notInGdprScope = BooleanUtils.isFalse(bidderTcfResponse.getUserInGdprScope());
+            final boolean isBidderVendorAllowed = notInGdprScope || action == null || !action.isBlockPixelSync();
+
+            if (hostVendorTcfResponse.isVendorAllowed() && isBidderVendorAllowed) {
                 respondWithCookie(setuidContext);
             } else {
-                metrics.updateUserSyncTcfBlockedMetric(bidderCookieName);
+                metrics.updateUserSyncTcfBlockedMetric(setuidContext.getCookieName());
 
                 final HttpResponseStatus status = new HttpResponseStatus(UNAVAILABLE_FOR_LEGAL_REASONS,
                         "Unavailable for legal reasons");
@@ -308,10 +316,9 @@ private void respondByTcfResponse(AsyncResult<HostVendorTcfResponse> hostTcfResp
 
                 analyticsDelegator.processEvent(SetuidEvent.error(status.code()), tcfContext);
             }
-
         } else {
             final Throwable error = hostTcfResponseResult.cause();
-            metrics.updateUserSyncTcfBlockedMetric(bidderCookieName);
+            metrics.updateUserSyncTcfBlockedMetric(setuidContext.getCookieName());
             handleErrors(error, routingContext, tcfContext);
         }
     }
diff --git a/src/main/java/org/prebid/server/handler/VtrackHandler.java b/src/main/java/org/prebid/server/handler/VtrackHandler.java
index 6539881eaa4..3d1243264d9 100644
--- a/src/main/java/org/prebid/server/handler/VtrackHandler.java
+++ b/src/main/java/org/prebid/server/handler/VtrackHandler.java
@@ -17,8 +17,8 @@
 import org.prebid.server.cache.proto.response.bid.BidCacheResponse;
 import org.prebid.server.events.EventUtil;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.EncodeException;
 import org.prebid.server.json.JacksonMapper;
diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java
index c1d9c58dca6..a7b39dce659 100644
--- a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java
+++ b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java
@@ -22,7 +22,10 @@
 import org.prebid.server.analytics.model.AmpEvent;
 import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator;
 import org.prebid.server.auction.AmpResponsePostProcessor;
+import org.prebid.server.auction.AnalyticsTagsEnricher;
 import org.prebid.server.auction.ExchangeService;
+import org.prebid.server.auction.HookDebugInfoEnricher;
+import org.prebid.server.auction.HooksMetricsService;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.Tuple2;
 import org.prebid.server.auction.requestfactory.AmpRequestFactory;
@@ -34,6 +37,8 @@
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.PreBidException;
 import org.prebid.server.exception.UnauthorizedAccountException;
+import org.prebid.server.hooks.execution.HookStageExecutor;
+import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.log.ConditionalLogger;
 import org.prebid.server.log.HttpInteractionLogger;
@@ -81,12 +86,14 @@ public class AmpHandler implements ApplicationResource {
     private final ExchangeService exchangeService;
     private final AnalyticsReporterDelegator analyticsDelegator;
     private final Metrics metrics;
+    private final HooksMetricsService hooksMetricsService;
     private final Clock clock;
     private final BidderCatalog bidderCatalog;
     private final Set<String> biddersSupportingCustomTargeting;
     private final AmpResponsePostProcessor ampResponsePostProcessor;
     private final HttpInteractionLogger httpInteractionLogger;
     private final PrebidVersionProvider prebidVersionProvider;
+    private final HookStageExecutor hookStageExecutor;
     private final JacksonMapper mapper;
     private final double logSamplingRate;
 
@@ -94,12 +101,14 @@ public AmpHandler(AmpRequestFactory ampRequestFactory,
                       ExchangeService exchangeService,
                       AnalyticsReporterDelegator analyticsDelegator,
                       Metrics metrics,
+                      HooksMetricsService hooksMetricsService,
                       Clock clock,
                       BidderCatalog bidderCatalog,
                       Set<String> biddersSupportingCustomTargeting,
                       AmpResponsePostProcessor ampResponsePostProcessor,
                       HttpInteractionLogger httpInteractionLogger,
                       PrebidVersionProvider prebidVersionProvider,
+                      HookStageExecutor hookStageExecutor,
                       JacksonMapper mapper,
                       double logSamplingRate) {
 
@@ -107,12 +116,14 @@ public AmpHandler(AmpRequestFactory ampRequestFactory,
         this.exchangeService = Objects.requireNonNull(exchangeService);
         this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator);
         this.metrics = Objects.requireNonNull(metrics);
+        this.hooksMetricsService = Objects.requireNonNull(hooksMetricsService);
         this.clock = Objects.requireNonNull(clock);
         this.bidderCatalog = Objects.requireNonNull(bidderCatalog);
         this.biddersSupportingCustomTargeting = Objects.requireNonNull(biddersSupportingCustomTargeting);
         this.ampResponsePostProcessor = Objects.requireNonNull(ampResponsePostProcessor);
         this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger);
         this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider);
+        this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor);
         this.mapper = Objects.requireNonNull(mapper);
         this.logSamplingRate = logSamplingRate;
     }
@@ -134,18 +145,25 @@ public void handle(RoutingContext routingContext) {
                 .httpContext(HttpRequestContext.from(routingContext));
 
         ampRequestFactory.fromRequest(routingContext, startTime)
-
                 .map(context -> addToEvent(context, ampEventBuilder::auctionContext, context))
                 .map(this::updateAppAndNoCookieAndImpsMetrics)
-
                 .compose(exchangeService::holdAuction)
-                .map(context -> addToEvent(context, ampEventBuilder::auctionContext, context))
-                .map(context -> addToEvent(context.getBidResponse(), ampEventBuilder::bidResponse, context))
-                .compose(context -> prepareAmpResponse(context, routingContext))
-                .map(result -> addToEvent(result.getLeft().getTargeting(), ampEventBuilder::targeting, result))
+                .map(context -> addContextAndBidResponseToEvent(context, ampEventBuilder, context))
+                .compose(context -> prepareSuccessfulResponse(context, routingContext, ampEventBuilder))
+                .compose(this::invokeExitpointHooks)
+                .map(context -> addContextAndBidResponseToEvent(context.getAuctionContext(), ampEventBuilder, context))
                 .onComplete(responseResult -> handleResult(responseResult, ampEventBuilder, routingContext, startTime));
     }
 
+    private static <R> R addContextAndBidResponseToEvent(AuctionContext context,
+                                                         AmpEvent.AmpEventBuilder ampEventBuilder,
+                                                         R result) {
+
+        ampEventBuilder.auctionContext(context);
+        ampEventBuilder.bidResponse(context.getBidResponse());
+        return result;
+    }
+
     private static <T, R> R addToEvent(T field, Consumer<T> consumer, R result) {
         consumer.accept(field);
         return result;
@@ -166,8 +184,44 @@ private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context
         return context;
     }
 
+    private Future<RawResponseContext> prepareSuccessfulResponse(AuctionContext auctionContext,
+                                                                 RoutingContext routingContext,
+                                                                 AmpEvent.AmpEventBuilder ampEventBuilder) {
+
+        final String origin = originFrom(routingContext);
+        final MultiMap responseHeaders = getCommonResponseHeaders(routingContext, origin)
+                .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON);
+
+        return prepareAmpResponse(auctionContext, routingContext)
+                .map(result -> addToEvent(result.getLeft().getTargeting(), ampEventBuilder::targeting, result))
+                .map(result -> RawResponseContext.builder()
+                        .responseBody(mapper.encodeToString(result.getLeft()))
+                        .responseHeaders(responseHeaders)
+                        .auctionContext(auctionContext)
+                        .build());
+    }
+
+    private Future<RawResponseContext> invokeExitpointHooks(RawResponseContext rawResponseContext) {
+        final AuctionContext auctionContext = rawResponseContext.getAuctionContext();
+        return hookStageExecutor.executeExitpointStage(
+                        rawResponseContext.getResponseHeaders(),
+                        rawResponseContext.getResponseBody(),
+                        auctionContext)
+                .map(HookStageExecutionResult::getPayload)
+                .compose(payload -> Future.succeededFuture(auctionContext)
+                        .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags)
+                        .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo)
+                        .map(hooksMetricsService::updateHooksMetrics)
+                        .map(context -> RawResponseContext.builder()
+                                .auctionContext(context)
+                                .responseHeaders(payload.responseHeaders())
+                                .responseBody(payload.responseBody())
+                                .build()));
+    }
+
     private Future<Tuple2<AmpResponse, AuctionContext>> prepareAmpResponse(AuctionContext context,
                                                                            RoutingContext routingContext) {
+
         final BidRequest bidRequest = context.getBidRequest();
         final BidResponse bidResponse = context.getBidResponse();
         final AmpResponse ampResponse = toAmpResponse(bidResponse);
@@ -271,12 +325,13 @@ private static ExtAmpVideoResponse extResponseFrom(BidResponse bidResponse) {
                 : null;
     }
 
-    private void handleResult(AsyncResult<Tuple2<AmpResponse, AuctionContext>> responseResult,
+    private void handleResult(AsyncResult<RawResponseContext> responseResult,
                               AmpEvent.AmpEventBuilder ampEventBuilder,
                               RoutingContext routingContext,
                               long startTime) {
 
         final boolean responseSucceeded = responseResult.succeeded();
+        final RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null;
 
         final MetricName metricRequestStatus;
         final List<String> errorMessages;
@@ -287,16 +342,22 @@ private void handleResult(AsyncResult<Tuple2<AmpResponse, AuctionContext>> respo
         ampEventBuilder.origin(origin);
 
         final HttpServerResponse response = routingContext.response();
-        enrichResponseWithCommonHeaders(routingContext, origin);
+        final MultiMap responseHeaders = response.headers();
 
         if (responseSucceeded) {
             metricRequestStatus = MetricName.ok;
             errorMessages = Collections.emptyList();
-
             status = HttpResponseStatus.OK;
-            enrichWithSuccessfulHeaders(response);
-            body = mapper.encodeToString(responseResult.result().getLeft());
+
+            rawResponseContext.getResponseHeaders()
+                    .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty(
+                            responseHeaders, header.getKey(), header.getValue()));
+            body = rawResponseContext.getResponseBody();
         } else {
+            getCommonResponseHeaders(routingContext, origin)
+                    .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty(
+                            responseHeaders, header.getKey(), header.getValue()));
+
             final Throwable exception = responseResult.cause();
             if (exception instanceof InvalidRequestException invalidRequestException) {
                 metricRequestStatus = MetricName.badinput;
@@ -355,8 +416,7 @@ private void handleResult(AsyncResult<Tuple2<AmpResponse, AuctionContext>> respo
 
         final int statusCode = status.code();
         final AmpEvent ampEvent = ampEventBuilder.status(statusCode).errors(errorMessages).build();
-
-        final AuctionContext auctionContext = responseSucceeded ? responseResult.result().getRight() : null;
+        final AuctionContext auctionContext = ampEvent.getAuctionContext();
 
         final PrivacyContext privacyContext = auctionContext != null ? auctionContext.getPrivacyContext() : null;
         final TcfContext tcfContext = privacyContext != null ? privacyContext.getTcfContext() : TcfContext.empty();
@@ -406,8 +466,8 @@ private void handleResponseException(Throwable exception) {
         metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr);
     }
 
-    private void enrichResponseWithCommonHeaders(RoutingContext routingContext, String origin) {
-        final MultiMap responseHeaders = routingContext.response().headers();
+    private MultiMap getCommonResponseHeaders(RoutingContext routingContext, String origin) {
+        final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap();
         HttpUtil.addHeaderIfValueIsNotEmpty(
                 responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord());
 
@@ -419,10 +479,7 @@ private void enrichResponseWithCommonHeaders(RoutingContext routingContext, Stri
         // Add AMP headers
         responseHeaders.add("AMP-Access-Control-Allow-Source-Origin", origin)
                 .add("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin");
-    }
 
-    private void enrichWithSuccessfulHeaders(HttpServerResponse response) {
-        final MultiMap headers = response.headers();
-        headers.add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON);
+        return responseHeaders;
     }
 }
diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java
index b8664bc75fd..e0dbe2ea4e1 100644
--- a/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java
+++ b/src/main/java/org/prebid/server/handler/openrtb2/AuctionHandler.java
@@ -12,7 +12,10 @@
 import io.vertx.ext.web.RoutingContext;
 import org.prebid.server.analytics.model.AuctionEvent;
 import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator;
+import org.prebid.server.auction.AnalyticsTagsEnricher;
 import org.prebid.server.auction.ExchangeService;
+import org.prebid.server.auction.HookDebugInfoEnricher;
+import org.prebid.server.auction.HooksMetricsService;
 import org.prebid.server.auction.SkippedAuctionService;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.requestfactory.AuctionRequestFactory;
@@ -22,6 +25,8 @@
 import org.prebid.server.exception.InvalidAccountConfigException;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.UnauthorizedAccountException;
+import org.prebid.server.hooks.execution.HookStageExecutor;
+import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.log.ConditionalLogger;
 import org.prebid.server.log.HttpInteractionLogger;
@@ -55,9 +60,11 @@ public class AuctionHandler implements ApplicationResource {
     private final SkippedAuctionService skippedAuctionService;
     private final AnalyticsReporterDelegator analyticsDelegator;
     private final Metrics metrics;
+    private final HooksMetricsService hooksMetricsService;
     private final Clock clock;
     private final HttpInteractionLogger httpInteractionLogger;
     private final PrebidVersionProvider prebidVersionProvider;
+    private final HookStageExecutor hookStageExecutor;
     private final JacksonMapper mapper;
 
     public AuctionHandler(double logSamplingRate,
@@ -66,9 +73,11 @@ public AuctionHandler(double logSamplingRate,
                           SkippedAuctionService skippedAuctionService,
                           AnalyticsReporterDelegator analyticsDelegator,
                           Metrics metrics,
+                          HooksMetricsService hooksMetricsService,
                           Clock clock,
                           HttpInteractionLogger httpInteractionLogger,
                           PrebidVersionProvider prebidVersionProvider,
+                          HookStageExecutor hookStageExecutor,
                           JacksonMapper mapper) {
 
         this.logSamplingRate = logSamplingRate;
@@ -77,9 +86,11 @@ public AuctionHandler(double logSamplingRate,
         this.skippedAuctionService = Objects.requireNonNull(skippedAuctionService);
         this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator);
         this.metrics = Objects.requireNonNull(metrics);
+        this.hooksMetricsService = Objects.requireNonNull(hooksMetricsService);
         this.clock = Objects.requireNonNull(clock);
         this.httpInteractionLogger = Objects.requireNonNull(httpInteractionLogger);
         this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider);
+        this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor);
         this.mapper = Objects.requireNonNull(mapper);
     }
 
@@ -102,7 +113,21 @@ public void handle(RoutingContext routingContext) {
         auctionRequestFactory.parseRequest(routingContext, startTime)
                 .compose(auctionContext -> skippedAuctionService.skipAuction(auctionContext)
                         .recover(throwable -> holdAuction(auctionEventBuilder, auctionContext)))
-                .onComplete(context -> handleResult(context, auctionEventBuilder, routingContext, startTime));
+                .map(context -> addContextAndBidResponseToEvent(context, auctionEventBuilder, context))
+                .map(context -> prepareSuccessfulResponse(context, routingContext))
+                .compose(this::invokeExitpointHooks)
+                .map(context -> addContextAndBidResponseToEvent(
+                        context.getAuctionContext(), auctionEventBuilder, context))
+                .onComplete(result -> handleResult(result, auctionEventBuilder, routingContext, startTime));
+    }
+
+    private static <R> R addContextAndBidResponseToEvent(AuctionContext context,
+                                                         AuctionEvent.AuctionEventBuilder auctionEventBuilder,
+                                                         R result) {
+
+        auctionEventBuilder.auctionContext(context);
+        auctionEventBuilder.bidResponse(context.getBidResponse());
+        return result;
     }
 
     private Future<AuctionContext> holdAuction(AuctionEvent.AuctionEventBuilder auctionEventBuilder,
@@ -110,14 +135,9 @@ private Future<AuctionContext> holdAuction(AuctionEvent.AuctionEventBuilder auct
 
         return auctionRequestFactory.enrichAuctionContext(auctionContext)
                 .map(this::updateAppAndNoCookieAndImpsMetrics)
-
                 // In case of holdAuction Exception and auctionContext is not present below
                 .map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context))
-
-                .compose(exchangeService::holdAuction)
-                // populate event with updated context
-                .map(context -> addToEvent(context, auctionEventBuilder::auctionContext, context))
-                .map(context -> addToEvent(context.getBidResponse(), auctionEventBuilder::bidResponse, context));
+                .compose(exchangeService::holdAuction);
     }
 
     private static <T, R> R addToEvent(T field, Consumer<T> consumer, R result) {
@@ -142,14 +162,53 @@ private AuctionContext updateAppAndNoCookieAndImpsMetrics(AuctionContext context
         return context;
     }
 
-    private void handleResult(AsyncResult<AuctionContext> responseResult,
+    private RawResponseContext prepareSuccessfulResponse(AuctionContext auctionContext, RoutingContext routingContext) {
+        final MultiMap responseHeaders = getCommonResponseHeaders(routingContext)
+                .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON);
+
+        return RawResponseContext.builder()
+                .responseBody(mapper.encodeToString(auctionContext.getBidResponse()))
+                .responseHeaders(responseHeaders)
+                .auctionContext(auctionContext)
+                .build();
+    }
+
+    private Future<RawResponseContext> invokeExitpointHooks(RawResponseContext rawResponseContext) {
+        final AuctionContext auctionContext = rawResponseContext.getAuctionContext();
+
+        if (auctionContext.isAuctionSkipped()) {
+            return Future.succeededFuture(auctionContext)
+                    .map(hooksMetricsService::updateHooksMetrics)
+                    .map(rawResponseContext);
+        }
+
+        return hookStageExecutor.executeExitpointStage(
+                        rawResponseContext.getResponseHeaders(),
+                        rawResponseContext.getResponseBody(),
+                        auctionContext)
+                .map(HookStageExecutionResult::getPayload)
+                .compose(payload -> Future.succeededFuture(auctionContext)
+                        .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags)
+                        .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo)
+                        .map(hooksMetricsService::updateHooksMetrics)
+                        .map(context -> RawResponseContext.builder()
+                                .auctionContext(context)
+                                .responseHeaders(payload.responseHeaders())
+                                .responseBody(payload.responseBody())
+                                .build()));
+    }
+
+    private void handleResult(AsyncResult<RawResponseContext> responseResult,
                               AuctionEvent.AuctionEventBuilder auctionEventBuilder,
                               RoutingContext routingContext,
                               long startTime) {
 
         final boolean responseSucceeded = responseResult.succeeded();
 
-        final AuctionContext auctionContext = responseSucceeded ? responseResult.result() : null;
+        final RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null;
+        final AuctionContext auctionContext = rawResponseContext != null
+                ? rawResponseContext.getAuctionContext()
+                : null;
         final boolean isAuctionSkipped = responseSucceeded && auctionContext.isAuctionSkipped();
         final MetricName requestType = responseSucceeded
                 ? auctionContext.getRequestTypeMetric()
@@ -161,16 +220,22 @@ private void handleResult(AsyncResult<AuctionContext> responseResult,
         final String body;
 
         final HttpServerResponse response = routingContext.response();
-        enrichResponseWithCommonHeaders(routingContext);
+        final MultiMap responseHeaders = response.headers();
 
         if (responseSucceeded) {
             metricRequestStatus = MetricName.ok;
             errorMessages = Collections.emptyList();
-
             status = HttpResponseStatus.OK;
-            enrichWithSuccessfulHeaders(response);
-            body = mapper.encodeToString(responseResult.result().getBidResponse());
+
+            rawResponseContext.getResponseHeaders()
+                    .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty(
+                            responseHeaders, header.getKey(), header.getValue()));
+            body = rawResponseContext.getResponseBody();
         } else {
+            getCommonResponseHeaders(routingContext)
+                    .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty(
+                            responseHeaders, header.getKey(), header.getValue()));
+
             final Throwable exception = responseResult.cause();
             if (exception instanceof InvalidRequestException invalidRequestException) {
                 metricRequestStatus = MetricName.badinput;
@@ -263,8 +328,8 @@ private void handleResponseException(Throwable throwable, MetricName requestType
         metrics.updateRequestTypeMetric(requestType, MetricName.networkerr);
     }
 
-    private void enrichResponseWithCommonHeaders(RoutingContext routingContext) {
-        final MultiMap responseHeaders = routingContext.response().headers();
+    private MultiMap getCommonResponseHeaders(RoutingContext routingContext) {
+        final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap();
         HttpUtil.addHeaderIfValueIsNotEmpty(
                 responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord());
 
@@ -272,10 +337,7 @@ private void enrichResponseWithCommonHeaders(RoutingContext routingContext) {
         if (requestHeaders.contains(HttpUtil.SEC_BROWSING_TOPICS_HEADER)) {
             responseHeaders.add(HttpUtil.OBSERVE_BROWSING_TOPICS_HEADER, "?1");
         }
-    }
 
-    private void enrichWithSuccessfulHeaders(HttpServerResponse response) {
-        response.headers()
-                .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON);
+        return responseHeaders;
     }
 }
diff --git a/src/main/java/org/prebid/server/handler/openrtb2/RawResponseContext.java b/src/main/java/org/prebid/server/handler/openrtb2/RawResponseContext.java
new file mode 100644
index 00000000000..5fe80a55c1d
--- /dev/null
+++ b/src/main/java/org/prebid/server/handler/openrtb2/RawResponseContext.java
@@ -0,0 +1,18 @@
+package org.prebid.server.handler.openrtb2;
+
+import io.vertx.core.MultiMap;
+import lombok.Builder;
+import lombok.Value;
+import org.prebid.server.auction.model.AuctionContext;
+
+@Value(staticConstructor = "of")
+@Builder(toBuilder = true)
+public class RawResponseContext {
+
+    AuctionContext auctionContext;
+
+    String responseBody;
+
+    MultiMap responseHeaders;
+
+}
diff --git a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java
index d5957c15aa7..0bb31bab72b 100644
--- a/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java
+++ b/src/main/java/org/prebid/server/handler/openrtb2/VideoHandler.java
@@ -1,15 +1,20 @@
 package org.prebid.server.handler.openrtb2;
 
+import com.iab.openrtb.request.video.PodError;
 import io.netty.handler.codec.http.HttpHeaderValues;
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
 import io.vertx.core.MultiMap;
 import io.vertx.core.http.HttpMethod;
 import io.vertx.core.http.HttpServerResponse;
 import io.vertx.ext.web.RoutingContext;
 import org.prebid.server.analytics.model.VideoEvent;
 import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator;
+import org.prebid.server.auction.AnalyticsTagsEnricher;
 import org.prebid.server.auction.ExchangeService;
+import org.prebid.server.auction.HookDebugInfoEnricher;
+import org.prebid.server.auction.HooksMetricsService;
 import org.prebid.server.auction.VideoResponseFactory;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.CachedDebugLog;
@@ -18,6 +23,8 @@
 import org.prebid.server.cache.CoreCacheService;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.UnauthorizedAccountException;
+import org.prebid.server.hooks.execution.HookStageExecutor;
+import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.log.Logger;
 import org.prebid.server.log.LoggerFactory;
@@ -56,17 +63,22 @@ public class VideoHandler implements ApplicationResource {
     private final CoreCacheService coreCacheService;
     private final AnalyticsReporterDelegator analyticsDelegator;
     private final Metrics metrics;
+    private final HooksMetricsService hooksMetricsService;
     private final Clock clock;
     private final PrebidVersionProvider prebidVersionProvider;
+    private final HookStageExecutor hookStageExecutor;
     private final JacksonMapper mapper;
 
     public VideoHandler(VideoRequestFactory videoRequestFactory,
                         VideoResponseFactory videoResponseFactory,
                         ExchangeService exchangeService,
-                        CoreCacheService coreCacheService, AnalyticsReporterDelegator analyticsDelegator,
+                        CoreCacheService coreCacheService,
+                        AnalyticsReporterDelegator analyticsDelegator,
                         Metrics metrics,
+                        HooksMetricsService hooksMetricsService,
                         Clock clock,
                         PrebidVersionProvider prebidVersionProvider,
+                        HookStageExecutor hookStageExecutor,
                         JacksonMapper mapper) {
 
         this.videoRequestFactory = Objects.requireNonNull(videoRequestFactory);
@@ -75,8 +87,10 @@ public VideoHandler(VideoRequestFactory videoRequestFactory,
         this.coreCacheService = Objects.requireNonNull(coreCacheService);
         this.analyticsDelegator = Objects.requireNonNull(analyticsDelegator);
         this.metrics = Objects.requireNonNull(metrics);
+        this.hooksMetricsService = Objects.requireNonNull(hooksMetricsService);
         this.clock = Objects.requireNonNull(clock);
         this.prebidVersionProvider = Objects.requireNonNull(prebidVersionProvider);
+        this.hookStageExecutor = Objects.requireNonNull(hookStageExecutor);
         this.mapper = Objects.requireNonNull(mapper);
     }
 
@@ -106,13 +120,55 @@ public void handle(RoutingContext routingContext) {
                 .map(contextToErrors ->
                         addToEvent(contextToErrors.getData(), videoEventBuilder::auctionContext, contextToErrors))
 
-                .map(result -> videoResponseFactory.toVideoResponse(
-                        result.getData(), result.getData().getBidResponse(),
-                        result.getPodErrors()))
+                .compose(contextToErrors ->
+                        prepareSuccessfulResponse(contextToErrors, routingContext, videoEventBuilder)
+                        .compose(this::invokeExitpointHooks)
+                        .compose(context -> toVideoResponse(context.getAuctionContext(), contextToErrors.getPodErrors())
+                                .map(videoResponse ->
+                                        addToEvent(videoResponse, videoEventBuilder::bidResponse, context)))
+                        .map(context ->
+                                addToEvent(context.getAuctionContext(), videoEventBuilder::auctionContext, context)))
+                .onComplete(result -> handleResult(result, videoEventBuilder, routingContext, startTime));
+    }
+
+    private Future<RawResponseContext> prepareSuccessfulResponse(WithPodErrors<AuctionContext> context,
+                                                                 RoutingContext routingContext,
+                                                                 VideoEvent.VideoEventBuilder videoEventBuilder) {
+
+        final AuctionContext auctionContext = context.getData();
+        final MultiMap responseHeaders = getCommonResponseHeaders(routingContext)
+                .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON);
 
+        return toVideoResponse(auctionContext, context.getPodErrors())
                 .map(videoResponse -> addToEvent(videoResponse, videoEventBuilder::bidResponse, videoResponse))
-                .onComplete(responseResult -> handleResult(responseResult, videoEventBuilder, routingContext,
-                        startTime));
+                .map(videoResponse -> RawResponseContext.builder()
+                        .responseBody(mapper.encodeToString(videoResponse))
+                        .responseHeaders(responseHeaders)
+                        .auctionContext(auctionContext)
+                        .build());
+    }
+
+    private Future<VideoResponse> toVideoResponse(AuctionContext auctionContext, List<PodError> podErrors) {
+        return Future.succeededFuture(
+                videoResponseFactory.toVideoResponse(auctionContext, auctionContext.getBidResponse(), podErrors));
+    }
+
+    private Future<RawResponseContext> invokeExitpointHooks(RawResponseContext rawResponseContext) {
+        final AuctionContext auctionContext = rawResponseContext.getAuctionContext();
+        return hookStageExecutor.executeExitpointStage(
+                        rawResponseContext.getResponseHeaders(),
+                        rawResponseContext.getResponseBody(),
+                        auctionContext)
+                .map(HookStageExecutionResult::getPayload)
+                .compose(payload -> Future.succeededFuture(auctionContext)
+                        .map(AnalyticsTagsEnricher::enrichWithAnalyticsTags)
+                        .map(HookDebugInfoEnricher::enrichWithHooksDebugInfo)
+                        .map(hooksMetricsService::updateHooksMetrics)
+                        .map(context -> RawResponseContext.builder()
+                                .auctionContext(context)
+                                .responseHeaders(payload.responseHeaders())
+                                .responseBody(payload.responseBody())
+                                .build()));
     }
 
     private static <T, R> R addToEvent(T field, Consumer<T> consumer, R result) {
@@ -120,7 +176,7 @@ private static <T, R> R addToEvent(T field, Consumer<T> consumer, R result) {
         return result;
     }
 
-    private void handleResult(AsyncResult<VideoResponse> responseResult,
+    private void handleResult(AsyncResult<RawResponseContext> responseResult,
                               VideoEvent.VideoEventBuilder videoEventBuilder,
                               RoutingContext routingContext,
                               long startTime) {
@@ -130,19 +186,25 @@ private void handleResult(AsyncResult<VideoResponse> responseResult,
         final List<String> errorMessages;
         final HttpResponseStatus status;
         final String body;
-        final VideoResponse videoResponse = responseSucceeded ? responseResult.result() : null;
+        final RawResponseContext rawResponseContext = responseSucceeded ? responseResult.result() : null;
 
         final HttpServerResponse response = routingContext.response();
-        enrichResponseWithCommonHeaders(routingContext);
+        final MultiMap responseHeaders = response.headers();
 
         if (responseSucceeded) {
             metricRequestStatus = MetricName.ok;
             errorMessages = Collections.emptyList();
 
             status = HttpResponseStatus.OK;
-            enrichWithSuccessfulHeaders(response);
-            body = mapper.encodeToString(videoResponse);
+            rawResponseContext.getResponseHeaders()
+                    .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty(
+                            responseHeaders, header.getKey(), header.getValue()));
+            body = rawResponseContext.getResponseBody();
         } else {
+            getCommonResponseHeaders(routingContext)
+                    .forEach(header -> HttpUtil.addHeaderIfValueIsNotEmpty(
+                            responseHeaders, header.getKey(), header.getValue()));
+
             final Throwable exception = responseResult.cause();
             if (exception instanceof InvalidRequestException) {
                 metricRequestStatus = MetricName.badinput;
@@ -240,8 +302,8 @@ private void handleResponseException(Throwable throwable) {
         metrics.updateRequestTypeMetric(REQUEST_TYPE_METRIC, MetricName.networkerr);
     }
 
-    private void enrichResponseWithCommonHeaders(RoutingContext routingContext) {
-        final MultiMap responseHeaders = routingContext.response().headers();
+    private MultiMap getCommonResponseHeaders(RoutingContext routingContext) {
+        final MultiMap responseHeaders = MultiMap.caseInsensitiveMultiMap();
         HttpUtil.addHeaderIfValueIsNotEmpty(
                 responseHeaders, HttpUtil.X_PREBID_HEADER, prebidVersionProvider.getNameVersionRecord());
 
@@ -249,10 +311,7 @@ private void enrichResponseWithCommonHeaders(RoutingContext routingContext) {
         if (requestHeaders.contains(HttpUtil.SEC_BROWSING_TOPICS_HEADER)) {
             responseHeaders.add(HttpUtil.OBSERVE_BROWSING_TOPICS_HEADER, "?1");
         }
-    }
 
-    private void enrichWithSuccessfulHeaders(HttpServerResponse response) {
-        response.headers()
-                .add(HttpUtil.CONTENT_TYPE_HEADER, HttpHeaderValues.APPLICATION_JSON);
+        return responseHeaders;
     }
 }
diff --git a/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java b/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java
index 6243f9ed8c7..97bd43c4abf 100644
--- a/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java
+++ b/src/main/java/org/prebid/server/health/GeoLocationHealthChecker.java
@@ -1,7 +1,7 @@
 package org.prebid.server.health;
 
 import io.vertx.core.Vertx;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.geolocation.GeoLocationService;
 import org.prebid.server.health.model.Status;
 import org.prebid.server.health.model.StatusResponse;
diff --git a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java
index 2525651f872..18d52b64c99 100644
--- a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java
+++ b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java
@@ -7,42 +7,41 @@
 import org.prebid.server.hooks.execution.model.ExecutionGroup;
 import org.prebid.server.hooks.execution.model.HookExecutionContext;
 import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.execution.provider.HookProvider;
 import org.prebid.server.hooks.v1.Hook;
 import org.prebid.server.hooks.v1.InvocationContext;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.log.ConditionalLogger;
-import org.prebid.server.log.LoggerFactory;
 
 import java.time.Clock;
+import java.util.Map;
 import java.util.concurrent.TimeoutException;
-import java.util.function.Function;
 import java.util.function.Supplier;
 
 class GroupExecutor<PAYLOAD, CONTEXT extends InvocationContext> {
 
-    private static final ConditionalLogger conditionalLogger =
-            new ConditionalLogger(LoggerFactory.getLogger(GroupExecutor.class));
-
     private final Vertx vertx;
     private final Clock clock;
+    private final Map<String, Boolean> modulesExecution;
 
     private ExecutionGroup group;
     private PAYLOAD initialPayload;
-    private Function<HookId, Hook<PAYLOAD, CONTEXT>> hookProvider;
+    private HookProvider<PAYLOAD, CONTEXT> hookProvider;
     private InvocationContextProvider<CONTEXT> invocationContextProvider;
     private HookExecutionContext hookExecutionContext;
     private boolean rejectAllowed;
 
-    private GroupExecutor(Vertx vertx, Clock clock) {
+    private GroupExecutor(Vertx vertx, Clock clock, Map<String, Boolean> modulesExecution) {
         this.vertx = vertx;
         this.clock = clock;
+        this.modulesExecution = modulesExecution;
     }
 
     public static <PAYLOAD, CONTEXT extends InvocationContext> GroupExecutor<PAYLOAD, CONTEXT> create(
             Vertx vertx,
-            Clock clock) {
+            Clock clock,
+            Map<String, Boolean> modulesExecution) {
 
-        return new GroupExecutor<>(vertx, clock);
+        return new GroupExecutor<>(vertx, clock, modulesExecution);
     }
 
     public GroupExecutor<PAYLOAD, CONTEXT> withGroup(ExecutionGroup group) {
@@ -55,7 +54,7 @@ public GroupExecutor<PAYLOAD, CONTEXT> withInitialPayload(PAYLOAD initialPayload
         return this;
     }
 
-    public GroupExecutor<PAYLOAD, CONTEXT> withHookProvider(Function<HookId, Hook<PAYLOAD, CONTEXT>> hookProvider) {
+    public GroupExecutor<PAYLOAD, CONTEXT> withHookProvider(HookProvider<PAYLOAD, CONTEXT> hookProvider) {
         this.hookProvider = hookProvider;
         return this;
     }
@@ -82,11 +81,15 @@ public Future<GroupResult<PAYLOAD>> execute() {
         Future<GroupResult<PAYLOAD>> groupFuture = Future.succeededFuture(initialGroupResult);
 
         for (final HookId hookId : group.getHookSequence()) {
-            final Hook<PAYLOAD, CONTEXT> hook = hookProvider.apply(hookId);
+            if (!modulesExecution.get(hookId.getModuleCode())) {
+                continue;
+            }
+
+            final Future<Hook<PAYLOAD, CONTEXT>> hookFuture = hook(hookId);
 
             final long startTime = clock.millis();
-            final Future<InvocationResult<PAYLOAD>> invocationResult =
-                    executeHook(hook, group.getTimeout(), initialGroupResult, hookId);
+            final Future<InvocationResult<PAYLOAD>> invocationResult = hookFuture
+                    .compose(hook -> executeHook(hook, group.getTimeout(), initialGroupResult, hookId));
 
             groupFuture = groupFuture.compose(groupResult ->
                     applyInvocationResult(invocationResult, hookId, startTime, groupResult));
@@ -95,23 +98,21 @@ public Future<GroupResult<PAYLOAD>> execute() {
         return groupFuture.recover(GroupExecutor::restoreResultFromRejection);
     }
 
-    private Future<InvocationResult<PAYLOAD>> executeHook(
-            Hook<PAYLOAD, CONTEXT> hook,
-            Long timeout,
-            GroupResult<PAYLOAD> groupResult,
-            HookId hookId) {
-
-        if (hook == null) {
-            conditionalLogger.error("Hook implementation %s does not exist or disabled".formatted(hookId), 0.01d);
-
-            return Future.failedFuture(new FailedException("Hook implementation does not exist or disabled"));
+    private Future<Hook<PAYLOAD, CONTEXT>> hook(HookId hookId) {
+        try {
+            return Future.succeededFuture(hookProvider.apply(hookId));
+        } catch (Exception e) {
+            return Future.failedFuture(new FailedException(e.getMessage()));
         }
+    }
+
+    private Future<InvocationResult<PAYLOAD>> executeHook(Hook<PAYLOAD, CONTEXT> hook,
+                                                          Long timeout,
+                                                          GroupResult<PAYLOAD> groupResult,
+                                                          HookId hookId) {
 
-        return executeWithTimeout(
-                () -> hook.call(
-                        groupResult.payload(),
-                        invocationContextProvider.apply(timeout, hookId, moduleContextFor(hookId))),
-                timeout);
+        final CONTEXT invocationContext = invocationContextProvider.apply(timeout, hookId, moduleContextFor(hookId));
+        return executeWithTimeout(() -> hook.call(groupResult.payload(), invocationContext), timeout);
     }
 
     private <T> Future<T> executeWithTimeout(Supplier<Future<T>> action, Long timeout) {
diff --git a/src/main/java/org/prebid/server/hooks/execution/GroupResult.java b/src/main/java/org/prebid/server/hooks/execution/GroupResult.java
index a4487e3a60b..8bc7c5c0723 100644
--- a/src/main/java/org/prebid/server/hooks/execution/GroupResult.java
+++ b/src/main/java/org/prebid/server/hooks/execution/GroupResult.java
@@ -173,6 +173,7 @@ private static ExecutionAction toExecutionAction(InvocationAction action) {
             case reject -> ExecutionAction.reject;
             case update -> ExecutionAction.update;
             case no_action -> ExecutionAction.no_action;
+            case no_invocation -> ExecutionAction.no_invocation;
         };
     }
 
diff --git a/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java b/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java
index 754e3925b11..f58b7f136c7 100644
--- a/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java
+++ b/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java
@@ -1,38 +1,46 @@
 package org.prebid.server.hooks.execution;
 
+import org.prebid.server.hooks.execution.model.HookId;
 import org.prebid.server.hooks.execution.model.StageWithHookType;
 import org.prebid.server.hooks.v1.Hook;
 import org.prebid.server.hooks.v1.InvocationContext;
 import org.prebid.server.hooks.v1.Module;
+import org.prebid.server.log.ConditionalLogger;
+import org.prebid.server.log.LoggerFactory;
 
 import java.util.Collection;
 import java.util.Objects;
 
-/**
- * Provides simple access to all {@link Hook}s registered in application.
- */
 public class HookCatalog {
 
+    private static final ConditionalLogger conditionalLogger =
+            new ConditionalLogger(LoggerFactory.getLogger(HookCatalog.class));
+
     private final Collection<Module> modules;
 
     public HookCatalog(Collection<Module> modules) {
         this.modules = Objects.requireNonNull(modules);
     }
 
-    public <HOOK extends Hook<?, ? extends InvocationContext>> HOOK hookById(
-            String moduleCode,
-            String hookImplCode,
-            StageWithHookType<HOOK> stage) {
+    public <HOOK extends Hook<?, ? extends InvocationContext>> HOOK hookById(HookId hookId,
+                                                                             StageWithHookType<HOOK> stage) {
 
         final Class<HOOK> clazz = stage.hookType();
         return modules.stream()
-                .filter(module -> Objects.equals(module.code(), moduleCode))
+                .filter(module -> Objects.equals(module.code(), hookId.getModuleCode()))
                 .map(Module::hooks)
                 .flatMap(Collection::stream)
-                .filter(hook -> Objects.equals(hook.code(), hookImplCode))
+                .filter(hook -> Objects.equals(hook.code(), hookId.getHookImplCode()))
                 .filter(clazz::isInstance)
                 .map(clazz::cast)
                 .findFirst()
-                .orElse(null);
+                .orElseThrow(() -> {
+                    logAbsentHook(hookId);
+                    return new IllegalArgumentException("Hook implementation does not exist or disabled");
+                });
+    }
+
+    private static void logAbsentHook(HookId hookId) {
+        conditionalLogger.error("Hook implementation %s does not exist or disabled".formatted(hookId), 0.01d);
     }
 }
diff --git a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java
index 81f44e3a528..cdb946f8d37 100644
--- a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java
+++ b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java
@@ -1,18 +1,24 @@
 package org.prebid.server.hooks.execution;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.iab.openrtb.request.BidRequest;
 import com.iab.openrtb.response.BidResponse;
 import io.vertx.core.Future;
+import io.vertx.core.MultiMap;
 import io.vertx.core.Vertx;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.ListUtils;
+import org.apache.commons.collections4.map.DefaultedMap;
 import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.BidderRequest;
 import org.prebid.server.auction.model.BidderResponse;
 import org.prebid.server.bidder.model.BidderBid;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
+import org.prebid.server.hooks.execution.model.ABTest;
 import org.prebid.server.hooks.execution.model.EndpointExecutionPlan;
 import org.prebid.server.hooks.execution.model.ExecutionGroup;
 import org.prebid.server.hooks.execution.model.ExecutionPlan;
@@ -22,6 +28,8 @@
 import org.prebid.server.hooks.execution.model.Stage;
 import org.prebid.server.hooks.execution.model.StageExecutionPlan;
 import org.prebid.server.hooks.execution.model.StageWithHookType;
+import org.prebid.server.hooks.execution.provider.HookProvider;
+import org.prebid.server.hooks.execution.provider.abtest.ABTestHookProvider;
 import org.prebid.server.hooks.execution.v1.InvocationContextImpl;
 import org.prebid.server.hooks.execution.v1.auction.AuctionInvocationContextImpl;
 import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
@@ -31,6 +39,7 @@
 import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl;
 import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl;
 import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl;
+import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl;
 import org.prebid.server.hooks.v1.Hook;
 import org.prebid.server.hooks.v1.InvocationContext;
 import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
@@ -41,24 +50,30 @@
 import org.prebid.server.hooks.v1.bidder.BidderRequestPayload;
 import org.prebid.server.hooks.v1.bidder.BidderResponsePayload;
 import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload;
+import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload;
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.model.CaseInsensitiveMultiMap;
 import org.prebid.server.model.Endpoint;
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.AccountHooksConfiguration;
+import org.prebid.server.settings.model.HooksAdminConfig;
 
 import java.time.Clock;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
 import java.util.stream.Stream;
 
 public class HookStageExecutor {
 
     private static final String ENTITY_HTTP_REQUEST = "http-request";
+    private static final String ENTITY_HTTP_RESPONSE = "http-response";
     private static final String ENTITY_AUCTION_REQUEST = "auction-request";
     private static final String ENTITY_AUCTION_RESPONSE = "auction-response";
     private static final String ENTITY_ALL_PROCESSED_BID_RESPONSES = "all-processed-bid-responses";
@@ -66,17 +81,23 @@ public class HookStageExecutor {
 
     private final ExecutionPlan hostExecutionPlan;
     private final ExecutionPlan defaultAccountExecutionPlan;
+    private final Map<String, Boolean> hostModuleExecution;
     private final HookCatalog hookCatalog;
     private final TimeoutFactory timeoutFactory;
     private final Vertx vertx;
     private final Clock clock;
+    private final ObjectMapper mapper;
+    private final boolean isConfigToInvokeRequired;
 
     private HookStageExecutor(ExecutionPlan hostExecutionPlan,
                               ExecutionPlan defaultAccountExecutionPlan,
+                              Map<String, Boolean> hostModuleExecution,
                               HookCatalog hookCatalog,
                               TimeoutFactory timeoutFactory,
                               Vertx vertx,
-                              Clock clock) {
+                              Clock clock,
+                              ObjectMapper mapper,
+                              boolean isConfigToInvokeRequired) {
 
         this.hostExecutionPlan = hostExecutionPlan;
         this.defaultAccountExecutionPlan = defaultAccountExecutionPlan;
@@ -84,26 +105,76 @@ private HookStageExecutor(ExecutionPlan hostExecutionPlan,
         this.timeoutFactory = timeoutFactory;
         this.vertx = vertx;
         this.clock = clock;
+        this.mapper = mapper;
+        this.isConfigToInvokeRequired = isConfigToInvokeRequired;
+        this.hostModuleExecution = hostModuleExecution;
     }
 
     public static HookStageExecutor create(String hostExecutionPlan,
                                            String defaultAccountExecutionPlan,
+                                           Map<String, Boolean> hostModuleExecution,
                                            HookCatalog hookCatalog,
                                            TimeoutFactory timeoutFactory,
                                            Vertx vertx,
                                            Clock clock,
-                                           JacksonMapper mapper) {
+                                           JacksonMapper mapper,
+                                           boolean isConfigToInvokeRequired) {
+
+        Objects.requireNonNull(hookCatalog);
+        Objects.requireNonNull(mapper);
 
         return new HookStageExecutor(
-                parseAndValidateExecutionPlan(
-                        hostExecutionPlan,
-                        Objects.requireNonNull(mapper),
-                        Objects.requireNonNull(hookCatalog)),
+                parseAndValidateExecutionPlan(hostExecutionPlan, mapper, hookCatalog),
                 parseAndValidateExecutionPlan(defaultAccountExecutionPlan, mapper, hookCatalog),
+                hostModuleExecution,
                 hookCatalog,
                 Objects.requireNonNull(timeoutFactory),
                 Objects.requireNonNull(vertx),
-                Objects.requireNonNull(clock));
+                Objects.requireNonNull(clock),
+                mapper.mapper(),
+                isConfigToInvokeRequired);
+    }
+
+    private static ExecutionPlan parseAndValidateExecutionPlan(String executionPlan,
+                                                               JacksonMapper mapper,
+                                                               HookCatalog hookCatalog) {
+
+        return validateExecutionPlan(parseExecutionPlan(executionPlan, mapper), hookCatalog);
+    }
+
+    private static ExecutionPlan parseExecutionPlan(String executionPlan, JacksonMapper mapper) {
+        if (StringUtils.isBlank(executionPlan)) {
+            return ExecutionPlan.empty();
+        }
+
+        try {
+            return mapper.decodeValue(executionPlan, ExecutionPlan.class);
+        } catch (DecodeException e) {
+            throw new IllegalArgumentException("Hooks execution plan could not be parsed", e);
+        }
+    }
+
+    private static ExecutionPlan validateExecutionPlan(ExecutionPlan plan, HookCatalog hookCatalog) {
+        plan.getEndpoints().values().stream()
+                .map(EndpointExecutionPlan::getStages)
+                .map(Map::entrySet)
+                .flatMap(Collection::stream)
+                .forEach(stageToPlan -> stageToPlan.getValue().getGroups().stream()
+                        .map(ExecutionGroup::getHookSequence)
+                        .flatMap(Collection::stream)
+                        .forEach(hookId -> validateHookId(stageToPlan.getKey(), hookId, hookCatalog)));
+
+        return plan;
+    }
+
+    private static void validateHookId(Stage stage, HookId hookId, HookCatalog hookCatalog) {
+        try {
+            hookCatalog.hookById(hookId, StageWithHookType.forStage(stage));
+        } catch (Throwable e) {
+            throw new IllegalArgumentException(
+                    "Hooks execution plan contains unknown or disabled hook: stage=%s, hookId=%s"
+                            .formatted(stage, hookId));
+        }
     }
 
     public Future<HookStageExecutionResult<EntrypointPayload>> executeEntrypointStage(
@@ -116,8 +187,10 @@ public Future<HookStageExecutionResult<EntrypointPayload>> executeEntrypointStag
 
         return stageExecutor(StageWithHookType.ENTRYPOINT, ENTITY_HTTP_REQUEST, context)
                 .withExecutionPlan(planForEntrypointStage(endpoint))
+                .withHookProvider(hookProviderForEntrypointStage(context))
                 .withInitialPayload(EntrypointPayloadImpl.of(queryParams, headers, body))
                 .withInvocationContextProvider(invocationContextProvider(endpoint))
+                .withModulesExecution(DefaultedMap.defaultedMap(hostModuleExecution, true))
                 .withRejectAllowed(true)
                 .execute();
     }
@@ -249,12 +322,28 @@ public Future<HookStageExecutionResult<AuctionResponsePayload>> executeAuctionRe
                 .execute();
     }
 
+    public Future<HookStageExecutionResult<ExitpointPayload>> executeExitpointStage(MultiMap responseHeaders,
+                                                                                    String responseBody,
+                                                                                    AuctionContext auctionContext) {
+
+        final Account account = ObjectUtils.defaultIfNull(auctionContext.getAccount(), EMPTY_ACCOUNT);
+        final HookExecutionContext context = auctionContext.getHookExecutionContext();
+
+        final Endpoint endpoint = context.getEndpoint();
+
+        return stageExecutor(StageWithHookType.EXITPOINT, ENTITY_HTTP_RESPONSE, context, account, endpoint)
+                .withInitialPayload(ExitpointPayloadImpl.of(responseHeaders, responseBody))
+                .withInvocationContextProvider(auctionInvocationContextProvider(endpoint, auctionContext))
+                .withRejectAllowed(false)
+                .execute();
+    }
+
     private <PAYLOAD, CONTEXT extends InvocationContext> StageExecutor<PAYLOAD, CONTEXT> stageExecutor(
             StageWithHookType<? extends Hook<PAYLOAD, CONTEXT>> stage,
             String entity,
             HookExecutionContext context) {
 
-        return StageExecutor.<PAYLOAD, CONTEXT>create(hookCatalog, vertx, clock)
+        return StageExecutor.<PAYLOAD, CONTEXT>create(vertx, clock)
                 .withStage(stage)
                 .withEntity(entity)
                 .withHookExecutionContext(context);
@@ -268,53 +357,30 @@ private <PAYLOAD, CONTEXT extends InvocationContext> StageExecutor<PAYLOAD, CONT
             Endpoint endpoint) {
 
         return stageExecutor(stage, entity, context)
-                .withExecutionPlan(planForStage(account, endpoint, stage.stage()));
-    }
-
-    private static ExecutionPlan parseAndValidateExecutionPlan(
-            String executionPlan,
-            JacksonMapper mapper,
-            HookCatalog hookCatalog) {
-
-        return validateExecutionPlan(parseExecutionPlan(executionPlan, mapper), hookCatalog);
-    }
-
-    private static ExecutionPlan validateExecutionPlan(ExecutionPlan plan, HookCatalog hookCatalog) {
-        plan.getEndpoints().values().stream()
-                .map(EndpointExecutionPlan::getStages)
-                .map(Map::entrySet)
-                .flatMap(Collection::stream)
-                .forEach(stageToPlan -> stageToPlan.getValue().getGroups().stream()
-                        .map(ExecutionGroup::getHookSequence)
-                        .flatMap(Collection::stream)
-                        .forEach(hookId -> validateHookId(stageToPlan.getKey(), hookId, hookCatalog)));
-
-        return plan;
+                .withModulesExecution(modulesExecutionForAccount(account))
+                .withExecutionPlan(planForStage(account, endpoint, stage.stage()))
+                .withHookProvider(hookProvider(stage, account, context));
     }
 
-    private static void validateHookId(Stage stage, HookId hookId, HookCatalog hookCatalog) {
-        final Hook<?, ? extends InvocationContext> hook = hookCatalog.hookById(
-                hookId.getModuleCode(),
-                hookId.getHookImplCode(),
-                StageWithHookType.forStage(stage));
-
-        if (hook == null) {
-            throw new IllegalArgumentException(
-                    "Hooks execution plan contains unknown or disabled hook: stage=%s, hookId=%s"
-                            .formatted(stage, hookId));
+    private Map<String, Boolean> modulesExecutionForAccount(Account account) {
+        final Map<String, Boolean> accountModulesExecution = Optional.ofNullable(account.getHooks())
+                .map(AccountHooksConfiguration::getAdmin)
+                .map(HooksAdminConfig::getModuleExecution)
+                .orElse(Collections.emptyMap());
+
+        final Map<String, Boolean> resultModulesExecution = new HashMap<>(accountModulesExecution);
+
+        if (isConfigToInvokeRequired) {
+            Optional.ofNullable(account.getHooks())
+                    .map(AccountHooksConfiguration::getModules)
+                    .map(Map::keySet)
+                    .stream()
+                    .flatMap(Collection::stream)
+                    .forEach(module -> resultModulesExecution.computeIfAbsent(module, key -> true));
         }
-    }
 
-    private static ExecutionPlan parseExecutionPlan(String executionPlan, JacksonMapper mapper) {
-        if (StringUtils.isBlank(executionPlan)) {
-            return ExecutionPlan.empty();
-        }
-
-        try {
-            return mapper.decodeValue(executionPlan, ExecutionPlan.class);
-        } catch (DecodeException e) {
-            throw new IllegalArgumentException("Hooks execution plan could not be parsed", e);
-        }
+        resultModulesExecution.putAll(hostModuleExecution);
+        return DefaultedMap.defaultedMap(resultModulesExecution, !isConfigToInvokeRequired);
     }
 
     private StageExecutionPlan planForEntrypointStage(Endpoint endpoint) {
@@ -361,6 +427,34 @@ private ExecutionPlan effectiveExecutionPlanFor(Account account) {
         return accountExecutionPlan != null ? accountExecutionPlan : defaultAccountExecutionPlan;
     }
 
+    private HookProvider<EntrypointPayload, InvocationContext> hookProviderForEntrypointStage(
+            HookExecutionContext context) {
+
+        return new ABTestHookProvider<>(
+                defaultHookProvider(StageWithHookType.ENTRYPOINT),
+                abTestsForEntrypointStage(),
+                context,
+                mapper);
+    }
+
+    private <PAYLOAD, CONTEXT extends InvocationContext> HookProvider<PAYLOAD, CONTEXT> hookProvider(
+            StageWithHookType<? extends Hook<PAYLOAD, CONTEXT>> stage,
+            Account account,
+            HookExecutionContext context) {
+
+        return new ABTestHookProvider<>(
+                defaultHookProvider(stage),
+                abTests(account),
+                context,
+                mapper);
+    }
+
+    private <PAYLOAD, CONTEXT extends InvocationContext> HookProvider<PAYLOAD, CONTEXT> defaultHookProvider(
+            StageWithHookType<? extends Hook<PAYLOAD, CONTEXT>> stage) {
+
+        return hookId -> hookCatalog.hookById(hookId, stage);
+    }
+
     private InvocationContextProvider<InvocationContext> invocationContextProvider(Endpoint endpoint) {
         return (timeout, hookId, moduleContext) -> invocationContext(endpoint, timeout);
     }
@@ -406,10 +500,47 @@ private Timeout createTimeout(Long timeout) {
     }
 
     private static ObjectNode accountConfigFor(Account account, HookId hookId) {
-        final AccountHooksConfiguration accountHooksConfiguration = account.getHooks();
+        final AccountHooksConfiguration accountHooksConfiguration = account != null ? account.getHooks() : null;
         final Map<String, ObjectNode> modulesConfiguration =
                 accountHooksConfiguration != null ? accountHooksConfiguration.getModules() : Collections.emptyMap();
 
         return modulesConfiguration != null ? modulesConfiguration.get(hookId.getModuleCode()) : null;
     }
+
+    protected List<ABTest> abTestsForEntrypointStage() {
+        return ListUtils.emptyIfNull(hostExecutionPlan.getAbTests()).stream()
+                .filter(HookStageExecutor::isABTestEnabled)
+                .toList();
+    }
+
+    private static boolean isABTestEnabled(ABTest abTest) {
+        return abTest != null && abTest.isEnabled();
+    }
+
+    protected List<ABTest> abTests(Account account) {
+        return abTestsFromAccount(account)
+                .or(() -> abTestsFromHostConfig(account.getId()))
+                .orElse(Collections.emptyList());
+    }
+
+    private Optional<List<ABTest>> abTestsFromAccount(Account account) {
+        return Optional.of(effectiveExecutionPlanFor(account))
+                .map(ExecutionPlan::getAbTests)
+                .map(abTests -> abTests.stream()
+                        .filter(HookStageExecutor::isABTestEnabled)
+                        .toList());
+    }
+
+    private Optional<List<ABTest>> abTestsFromHostConfig(String accountId) {
+        return Optional.ofNullable(hostExecutionPlan.getAbTests())
+                .map(abTests -> abTests.stream()
+                        .filter(HookStageExecutor::isABTestEnabled)
+                        .filter(abTest -> isABTestApplicable(abTest, accountId))
+                        .toList());
+    }
+
+    private static boolean isABTestApplicable(ABTest abTest, String account) {
+        final Set<String> accounts = abTest.getAccounts();
+        return CollectionUtils.isEmpty(accounts) || accounts.contains(account);
+    }
 }
diff --git a/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java
index f4f4a8176de..8dfa03e9a7f 100644
--- a/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java
+++ b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java
@@ -7,38 +7,39 @@
 import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
 import org.prebid.server.hooks.execution.model.StageExecutionPlan;
 import org.prebid.server.hooks.execution.model.StageWithHookType;
+import org.prebid.server.hooks.execution.provider.HookProvider;
 import org.prebid.server.hooks.v1.Hook;
 import org.prebid.server.hooks.v1.InvocationContext;
 
 import java.time.Clock;
 import java.util.ArrayList;
+import java.util.Map;
 
 class StageExecutor<PAYLOAD, CONTEXT extends InvocationContext> {
 
-    private final HookCatalog hookCatalog;
     private final Vertx vertx;
     private final Clock clock;
 
     private StageWithHookType<? extends Hook<PAYLOAD, CONTEXT>> stage;
     private String entity;
     private StageExecutionPlan executionPlan;
+    private HookProvider<PAYLOAD, CONTEXT> hookProvider;
     private PAYLOAD initialPayload;
     private InvocationContextProvider<CONTEXT> invocationContextProvider;
     private HookExecutionContext hookExecutionContext;
     private boolean rejectAllowed;
+    private Map<String, Boolean> modulesExecution;
 
-    private StageExecutor(HookCatalog hookCatalog, Vertx vertx, Clock clock) {
-        this.hookCatalog = hookCatalog;
+    private StageExecutor(Vertx vertx, Clock clock) {
         this.vertx = vertx;
         this.clock = clock;
     }
 
     public static <PAYLOAD, CONTEXT extends InvocationContext> StageExecutor<PAYLOAD, CONTEXT> create(
-            HookCatalog hookCatalog,
             Vertx vertx,
             Clock clock) {
 
-        return new StageExecutor<>(hookCatalog, vertx, clock);
+        return new StageExecutor<>(vertx, clock);
     }
 
     public StageExecutor<PAYLOAD, CONTEXT> withStage(StageWithHookType<? extends Hook<PAYLOAD, CONTEXT>> stage) {
@@ -56,6 +57,11 @@ public StageExecutor<PAYLOAD, CONTEXT> withExecutionPlan(StageExecutionPlan exec
         return this;
     }
 
+    public StageExecutor<PAYLOAD, CONTEXT> withHookProvider(HookProvider<PAYLOAD, CONTEXT> hookProvider) {
+        this.hookProvider = hookProvider;
+        return this;
+    }
+
     public StageExecutor<PAYLOAD, CONTEXT> withInitialPayload(PAYLOAD initialPayload) {
         this.initialPayload = initialPayload;
         return this;
@@ -78,6 +84,11 @@ public StageExecutor<PAYLOAD, CONTEXT> withRejectAllowed(boolean rejectAllowed)
         return this;
     }
 
+    public StageExecutor<PAYLOAD, CONTEXT> withModulesExecution(Map<String, Boolean> modulesExecution) {
+        this.modulesExecution = modulesExecution;
+        return this;
+    }
+
     public Future<HookStageExecutionResult<PAYLOAD>> execute() {
         Future<StageResult<PAYLOAD>> stageFuture = Future.succeededFuture(StageResult.of(initialPayload, entity));
 
@@ -94,11 +105,10 @@ public Future<HookStageExecutionResult<PAYLOAD>> execute() {
     }
 
     private Future<GroupResult<PAYLOAD>> executeGroup(ExecutionGroup group, PAYLOAD initialPayload) {
-        return GroupExecutor.<PAYLOAD, CONTEXT>create(vertx, clock)
+        return GroupExecutor.<PAYLOAD, CONTEXT>create(vertx, clock, modulesExecution)
                 .withGroup(group)
                 .withInitialPayload(initialPayload)
-                .withHookProvider(
-                        hookId -> hookCatalog.hookById(hookId.getModuleCode(), hookId.getHookImplCode(), stage))
+                .withHookProvider(hookProvider)
                 .withInvocationContextProvider(invocationContextProvider)
                 .withHookExecutionContext(hookExecutionContext)
                 .withRejectAllowed(rejectAllowed)
diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ABTest.java b/src/main/java/org/prebid/server/hooks/execution/model/ABTest.java
new file mode 100644
index 00000000000..67d64190101
--- /dev/null
+++ b/src/main/java/org/prebid/server/hooks/execution/model/ABTest.java
@@ -0,0 +1,29 @@
+package org.prebid.server.hooks.execution.model;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Value;
+
+import java.util.Set;
+
+@Builder
+@Value
+public class ABTest {
+
+    boolean enabled;
+
+    @JsonProperty("module-code")
+    @JsonAlias("module_code")
+    String moduleCode;
+
+    Set<String> accounts;
+
+    @JsonProperty("percent-active")
+    @JsonAlias("percent_active")
+    Integer percentActive;
+
+    @JsonProperty("log-analytics-tag")
+    @JsonAlias("log_analytics_tag")
+    Boolean logAnalyticsTag;
+}
diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java
index 5e13aa3f14c..886cec114e8 100644
--- a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java
+++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionAction.java
@@ -2,5 +2,5 @@
 
 public enum ExecutionAction {
 
-    no_action, update, reject
+    no_action, update, reject, no_invocation
 }
diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java
index 5d0af8b8e23..8d865c26a90 100644
--- a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java
+++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java
@@ -1,15 +1,20 @@
 package org.prebid.server.hooks.execution.model;
 
+import com.fasterxml.jackson.annotation.JsonProperty;
 import lombok.Value;
 import org.prebid.server.model.Endpoint;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 @Value(staticConstructor = "of")
 public class ExecutionPlan {
 
-    private static final ExecutionPlan EMPTY = of(Collections.emptyMap());
+    private static final ExecutionPlan EMPTY = of(null, Collections.emptyMap());
+
+    @JsonProperty("abtests")
+    List<ABTest> abTests;
 
     Map<Endpoint, EndpointExecutionPlan> endpoints;
 
diff --git a/src/main/java/org/prebid/server/hooks/execution/model/Stage.java b/src/main/java/org/prebid/server/hooks/execution/model/Stage.java
index 47896d8c9ab..bb7c151ed6f 100644
--- a/src/main/java/org/prebid/server/hooks/execution/model/Stage.java
+++ b/src/main/java/org/prebid/server/hooks/execution/model/Stage.java
@@ -33,5 +33,7 @@ public enum Stage {
 
     @JsonProperty("auction-response")
     @JsonAlias("auction_response")
-    auction_response
+    auction_response,
+
+    exitpoint
 }
diff --git a/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java
index f8738d2c2db..961450a3c3f 100644
--- a/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java
+++ b/src/main/java/org/prebid/server/hooks/execution/model/StageWithHookType.java
@@ -10,6 +10,7 @@
 import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook;
 import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook;
 import org.prebid.server.hooks.v1.entrypoint.EntrypointHook;
+import org.prebid.server.hooks.v1.exitpoint.ExitpointHook;
 
 public interface StageWithHookType<TYPE extends Hook<?, ? extends InvocationContext>> {
 
@@ -29,6 +30,8 @@ public interface StageWithHookType<TYPE extends Hook<?, ? extends InvocationCont
             new StageWithHookTypeImpl<>(Stage.all_processed_bid_responses, AllProcessedBidResponsesHook.class);
     StageWithHookType<AuctionResponseHook> AUCTION_RESPONSE =
             new StageWithHookTypeImpl<>(Stage.auction_response, AuctionResponseHook.class);
+    StageWithHookType<ExitpointHook> EXITPOINT =
+            new StageWithHookTypeImpl<>(Stage.exitpoint, ExitpointHook.class);
 
     Stage stage();
 
@@ -44,6 +47,7 @@ public interface StageWithHookType<TYPE extends Hook<?, ? extends InvocationCont
             case all_processed_bid_responses -> ALL_PROCESSED_BID_RESPONSES;
             case processed_bidder_response -> PROCESSED_BIDDER_RESPONSE;
             case auction_response -> AUCTION_RESPONSE;
+            case exitpoint -> EXITPOINT;
         };
     }
 }
diff --git a/src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java b/src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java
new file mode 100644
index 00000000000..83297c26396
--- /dev/null
+++ b/src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java
@@ -0,0 +1,11 @@
+package org.prebid.server.hooks.execution.provider;
+
+import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.InvocationContext;
+
+import java.util.function.Function;
+
+public interface HookProvider<PAYLOAD, CONTEXT extends InvocationContext>
+        extends Function<HookId, Hook<PAYLOAD, CONTEXT>> {
+}
diff --git a/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java
new file mode 100644
index 00000000000..e7b82803f9a
--- /dev/null
+++ b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java
@@ -0,0 +1,148 @@
+package org.prebid.server.hooks.execution.provider.abtest;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import io.vertx.core.Future;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationContext;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.PayloadUpdate;
+import org.prebid.server.hooks.v1.analytics.Activity;
+import org.prebid.server.hooks.v1.analytics.Tags;
+import org.prebid.server.util.ListUtil;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+public class ABTestHook<PAYLOAD, CONTEXT extends InvocationContext> implements Hook<PAYLOAD, CONTEXT> {
+
+    private static final String ANALYTICS_ACTIVITY_NAME = "core-module-abtests";
+
+    private final String moduleName;
+    private final Hook<PAYLOAD, CONTEXT> hook;
+    private final boolean shouldInvokeHook;
+    private final boolean logABTestAnalyticsTag;
+    private final ObjectMapper mapper;
+
+    public ABTestHook(String moduleName,
+                      Hook<PAYLOAD, CONTEXT> hook,
+                      boolean shouldInvokeHook,
+                      boolean logABTestAnalyticsTag,
+                      ObjectMapper mapper) {
+
+        this.moduleName = Objects.requireNonNull(moduleName);
+        this.hook = Objects.requireNonNull(hook);
+        this.shouldInvokeHook = shouldInvokeHook;
+        this.logABTestAnalyticsTag = logABTestAnalyticsTag;
+        this.mapper = Objects.requireNonNull(mapper);
+    }
+
+    @Override
+    public String code() {
+        return hook.code();
+    }
+
+    @Override
+    public Future<InvocationResult<PAYLOAD>> call(PAYLOAD payload, CONTEXT invocationContext) {
+        if (!shouldInvokeHook) {
+            return skippedResult();
+        }
+
+        final Future<InvocationResult<PAYLOAD>> invocationResultFuture = hook.call(payload, invocationContext);
+        return logABTestAnalyticsTag
+                ? invocationResultFuture.map(this::enrichWithABTestAnalyticsTag)
+                : invocationResultFuture;
+    }
+
+    private Future<InvocationResult<PAYLOAD>> skippedResult() {
+        return Future.succeededFuture(InvocationResultImpl.<PAYLOAD>builder()
+                .status(InvocationStatus.success)
+                .action(InvocationAction.no_invocation)
+                .analyticsTags(logABTestAnalyticsTag ? tags("skipped") : null)
+                .build());
+    }
+
+    private Tags tags(String status) {
+        return TagsImpl.of(Collections.singletonList(ActivityImpl.of(
+                ANALYTICS_ACTIVITY_NAME,
+                "success",
+                Collections.singletonList(ResultImpl.of(status, analyticsValues(), null)))));
+    }
+
+    private ObjectNode analyticsValues() {
+        final ObjectNode values = mapper.createObjectNode();
+        values.put("module", moduleName);
+        return values;
+    }
+
+    private InvocationResult<PAYLOAD> enrichWithABTestAnalyticsTag(InvocationResult<PAYLOAD> invocationResult) {
+        return new InvocationResultWithAdditionalTags<>(invocationResult, tags("run"));
+    }
+
+    private record InvocationResultWithAdditionalTags<PAYLOAD>(InvocationResult<PAYLOAD> invocationResult,
+                                                               Tags additionalTags)
+            implements InvocationResult<PAYLOAD> {
+
+        @Override
+        public InvocationStatus status() {
+            return invocationResult.status();
+        }
+
+        @Override
+        public String message() {
+            return invocationResult.message();
+        }
+
+        @Override
+        public InvocationAction action() {
+            return invocationResult.action();
+        }
+
+        @Override
+        public PayloadUpdate<PAYLOAD> payloadUpdate() {
+            return invocationResult.payloadUpdate();
+        }
+
+        @Override
+        public List<String> errors() {
+            return invocationResult.errors();
+        }
+
+        @Override
+        public List<String> warnings() {
+            return invocationResult.warnings();
+        }
+
+        @Override
+        public List<String> debugMessages() {
+            return invocationResult.debugMessages();
+        }
+
+        @Override
+        public Object moduleContext() {
+            return invocationResult.moduleContext();
+        }
+
+        @Override
+        public Tags analyticsTags() {
+            return new TagsUnion(invocationResult.analyticsTags(), additionalTags);
+        }
+    }
+
+    private record TagsUnion(Tags left, Tags right) implements Tags {
+
+        @Override
+        public List<Activity> activities() {
+            return left != null
+                    ? ListUtil.union(left.activities(), right.activities())
+                    : right.activities();
+        }
+    }
+}
diff --git a/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java
new file mode 100644
index 00000000000..6a833ab2833
--- /dev/null
+++ b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java
@@ -0,0 +1,87 @@
+package org.prebid.server.hooks.execution.provider.abtest;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.ObjectUtils;
+import org.prebid.server.hooks.execution.model.ABTest;
+import org.prebid.server.hooks.execution.model.ExecutionAction;
+import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookExecutionContext;
+import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
+import org.prebid.server.hooks.execution.provider.HookProvider;
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.InvocationContext;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class ABTestHookProvider<PAYLOAD, CONTEXT extends InvocationContext> implements HookProvider<PAYLOAD, CONTEXT> {
+
+    private final HookProvider<PAYLOAD, CONTEXT> innerHookProvider;
+    private final List<ABTest> abTests;
+    private final HookExecutionContext context;
+    private final ObjectMapper mapper;
+
+    public ABTestHookProvider(HookProvider<PAYLOAD, CONTEXT> innerHookProvider,
+                              List<ABTest> abTests,
+                              HookExecutionContext context,
+                              ObjectMapper mapper) {
+
+        this.innerHookProvider = Objects.requireNonNull(innerHookProvider);
+        this.abTests = Objects.requireNonNull(abTests);
+        this.context = Objects.requireNonNull(context);
+        this.mapper = Objects.requireNonNull(mapper);
+    }
+
+    @Override
+    public Hook<PAYLOAD, CONTEXT> apply(HookId hookId) {
+        final Hook<PAYLOAD, CONTEXT> hook = innerHookProvider.apply(hookId);
+
+        final String moduleCode = hookId.getModuleCode();
+        final ABTest abTest = searchForABTest(moduleCode);
+        if (abTest == null) {
+            return hook;
+        }
+
+        return new ABTestHook<>(
+                moduleCode,
+                hook,
+                shouldInvokeHook(moduleCode, abTest),
+                BooleanUtils.isNotFalse(abTest.getLogAnalyticsTag()),
+                mapper);
+    }
+
+    private ABTest searchForABTest(String moduleCode) {
+        return abTests.stream()
+                .filter(abTest -> moduleCode.equals(abTest.getModuleCode()))
+                .findFirst()
+                .orElse(null);
+    }
+
+    protected boolean shouldInvokeHook(String moduleCode, ABTest abTest) {
+        final HookExecutionOutcome hookExecutionOutcome = searchForPreviousExecution(moduleCode);
+        if (hookExecutionOutcome != null) {
+            return hookExecutionOutcome.getAction() != ExecutionAction.no_invocation;
+        }
+
+        final int percent = ObjectUtils.defaultIfNull(abTest.getPercentActive(), 100);
+        return ThreadLocalRandom.current().nextInt(100) < percent;
+    }
+
+    private HookExecutionOutcome searchForPreviousExecution(String moduleCode) {
+        return context.getStageOutcomes().values().stream()
+                .filter(Objects::nonNull)
+                .flatMap(Collection::stream)
+                .map(StageExecutionOutcome::getGroups)
+                .flatMap(Collection::stream)
+                .map(GroupExecutionOutcome::getHooks)
+                .flatMap(Collection::stream)
+                .filter(hookExecutionOutcome -> hookExecutionOutcome.getHookId().getModuleCode().equals(moduleCode))
+                .findFirst()
+                .orElse(null);
+    }
+}
diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java
index 6ed23ef8980..99399d5ba6b 100644
--- a/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java
+++ b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationContextImpl.java
@@ -2,7 +2,7 @@
 
 import lombok.Value;
 import lombok.experimental.Accessors;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.hooks.v1.InvocationContext;
 import org.prebid.server.model.Endpoint;
 
diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/InvocationResultImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationResultImpl.java
similarity index 91%
rename from extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/InvocationResultImpl.java
rename to src/main/java/org/prebid/server/hooks/execution/v1/InvocationResultImpl.java
index b77fc98a68b..761aef951ec 100644
--- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/InvocationResultImpl.java
+++ b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationResultImpl.java
@@ -1,4 +1,4 @@
-package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model;
+package org.prebid.server.hooks.execution.v1;
 
 import lombok.Builder;
 import lombok.Value;
diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ActivityImpl.java
similarity index 82%
rename from extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java
rename to src/main/java/org/prebid/server/hooks/execution/v1/analytics/ActivityImpl.java
index 484489a5e6f..4c9747e16bc 100644
--- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java
+++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ActivityImpl.java
@@ -1,4 +1,4 @@
-package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics;
+package org.prebid.server.hooks.execution.v1.analytics;
 
 import lombok.Value;
 import lombok.experimental.Accessors;
diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/AppliedToImpl.java
similarity index 83%
rename from extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java
rename to src/main/java/org/prebid/server/hooks/execution/v1/analytics/AppliedToImpl.java
index 2971cc40d6e..884603b4717 100644
--- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java
+++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/AppliedToImpl.java
@@ -1,4 +1,4 @@
-package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics;
+package org.prebid.server.hooks.execution.v1.analytics;
 
 import lombok.Builder;
 import lombok.Value;
@@ -8,8 +8,8 @@
 import java.util.List;
 
 @Accessors(fluent = true)
-@Value
 @Builder
+@Value
 public class AppliedToImpl implements AppliedTo {
 
     List<String> impIds;
diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ResultImpl.java
similarity index 84%
rename from extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java
rename to src/main/java/org/prebid/server/hooks/execution/v1/analytics/ResultImpl.java
index 5405799e25f..c16397e894c 100644
--- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java
+++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ResultImpl.java
@@ -1,4 +1,4 @@
-package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics;
+package org.prebid.server.hooks.execution.v1.analytics;
 
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import lombok.Value;
diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/TagsImpl.java
similarity index 81%
rename from extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java
rename to src/main/java/org/prebid/server/hooks/execution/v1/analytics/TagsImpl.java
index 9f0432b9e2f..f068f28dcef 100644
--- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java
+++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/TagsImpl.java
@@ -1,4 +1,4 @@
-package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics;
+package org.prebid.server.hooks.execution.v1.analytics;
 
 import lombok.Value;
 import lombok.experimental.Accessors;
diff --git a/src/main/java/org/prebid/server/hooks/execution/v1/exitpoint/ExitpointPayloadImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/exitpoint/ExitpointPayloadImpl.java
new file mode 100644
index 00000000000..d57080f6b90
--- /dev/null
+++ b/src/main/java/org/prebid/server/hooks/execution/v1/exitpoint/ExitpointPayloadImpl.java
@@ -0,0 +1,15 @@
+package org.prebid.server.hooks.execution.v1.exitpoint;
+
+import io.vertx.core.MultiMap;
+import lombok.Value;
+import lombok.experimental.Accessors;
+import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload;
+
+@Accessors(fluent = true)
+@Value(staticConstructor = "of")
+public class ExitpointPayloadImpl implements ExitpointPayload {
+
+    MultiMap responseHeaders;
+
+    String responseBody;
+}
diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java b/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java
index 29b22bf1b3d..821b21a730b 100644
--- a/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java
+++ b/src/main/java/org/prebid/server/hooks/v1/InvocationAction.java
@@ -2,5 +2,5 @@
 
 public enum InvocationAction {
 
-    no_action, update, reject
+    no_action, update, reject, no_invocation
 }
diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java b/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java
index 7c3b6c922d3..22493ea8a07 100644
--- a/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java
+++ b/src/main/java/org/prebid/server/hooks/v1/InvocationContext.java
@@ -1,6 +1,6 @@
 package org.prebid.server.hooks.v1;
 
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.model.Endpoint;
 
 public interface InvocationContext {
diff --git a/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointHook.java b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointHook.java
new file mode 100644
index 00000000000..02e36af17a5
--- /dev/null
+++ b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointHook.java
@@ -0,0 +1,7 @@
+package org.prebid.server.hooks.v1.exitpoint;
+
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+
+public interface ExitpointHook extends Hook<ExitpointPayload, AuctionInvocationContext> {
+}
diff --git a/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointPayload.java b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointPayload.java
new file mode 100644
index 00000000000..ae596949fa0
--- /dev/null
+++ b/src/main/java/org/prebid/server/hooks/v1/exitpoint/ExitpointPayload.java
@@ -0,0 +1,10 @@
+package org.prebid.server.hooks.v1.exitpoint;
+
+import io.vertx.core.MultiMap;
+
+public interface ExitpointPayload {
+
+    MultiMap responseHeaders();
+
+    String responseBody();
+}
diff --git a/src/main/java/org/prebid/server/metric/MetricName.java b/src/main/java/org/prebid/server/metric/MetricName.java
index fc9d3c251c3..9ea3d15f0fc 100644
--- a/src/main/java/org/prebid/server/metric/MetricName.java
+++ b/src/main/java/org/prebid/server/metric/MetricName.java
@@ -25,6 +25,7 @@ public enum MetricName {
 
     // auction
     requests,
+    debug_requests,
     app_requests,
     no_cookie_requests,
     request_time,
@@ -138,6 +139,7 @@ public enum MetricName {
     call,
     success,
     noop,
+    no_invocation("no-invocation"),
     reject,
     unknown,
     failure,
diff --git a/src/main/java/org/prebid/server/metric/Metrics.java b/src/main/java/org/prebid/server/metric/Metrics.java
index ed11d511f5f..52964a9f8b0 100644
--- a/src/main/java/org/prebid/server/metric/Metrics.java
+++ b/src/main/java/org/prebid/server/metric/Metrics.java
@@ -167,6 +167,12 @@ HooksMetrics hooks() {
         return hooksMetrics;
     }
 
+    public void updateDebugRequestMetrics(boolean debugEnabled) {
+        if (debugEnabled) {
+            incCounter(MetricName.debug_requests);
+        }
+    }
+
     public void updateAppAndNoCookieAndImpsRequestedMetrics(boolean isApp, boolean liveUidsPresent, int numImps) {
         if (isApp) {
             incCounter(MetricName.app_requests);
@@ -235,12 +241,20 @@ public void updateAccountRequestMetrics(Account account, MetricName requestType)
             final AccountMetrics accountMetrics = forAccount(account.getId());
 
             accountMetrics.incCounter(MetricName.requests);
+
             if (verbosityLevel.isAtLeast(AccountMetricsVerbosityLevel.detailed)) {
                 accountMetrics.requestType(requestType).incCounter(MetricName.requests);
             }
         }
     }
 
+    public void updateAccountDebugRequestMetrics(Account account, boolean debugEnabled) {
+        final AccountMetricsVerbosityLevel verbosityLevel = accountMetricsVerbosityResolver.forAccount(account);
+        if (verbosityLevel.isAtLeast(AccountMetricsVerbosityLevel.detailed) && debugEnabled) {
+            forAccount(account.getId()).incCounter(MetricName.debug_requests);
+        }
+    }
+
     public void updateAccountRequestRejectedByInvalidAccountMetrics(String accountId) {
         updateAccountRequestsMetrics(accountId, MetricName.rejected_by_invalid_account);
     }
@@ -614,13 +628,20 @@ public void updateHooksMetrics(
 
         final HookImplMetrics hookImplMetrics = hooks().module(moduleCode).stage(stage).hookImpl(hookImplCode);
 
-        hookImplMetrics.incCounter(MetricName.call);
+        if (action != ExecutionAction.no_invocation) {
+            hookImplMetrics.incCounter(MetricName.call);
+        }
+
         if (status == ExecutionStatus.success) {
             hookImplMetrics.success().incCounter(HookMetricMapper.fromAction(action));
         } else {
             hookImplMetrics.incCounter(HookMetricMapper.fromStatus(status));
         }
-        hookImplMetrics.updateTimer(MetricName.duration, executionTime);
+
+        if (action != ExecutionAction.no_invocation) {
+            hookImplMetrics.updateTimer(MetricName.duration, executionTime);
+        }
+
     }
 
     public void updateAccountHooksMetrics(
@@ -632,7 +653,10 @@ public void updateAccountHooksMetrics(
         if (accountMetricsVerbosityResolver.forAccount(account).isAtLeast(AccountMetricsVerbosityLevel.detailed)) {
             final ModuleMetrics accountModuleMetrics = forAccount(account.getId()).hooks().module(moduleCode);
 
-            accountModuleMetrics.incCounter(MetricName.call);
+            if (action != ExecutionAction.no_invocation) {
+                accountModuleMetrics.incCounter(MetricName.call);
+            }
+
             if (status == ExecutionStatus.success) {
                 accountModuleMetrics.success().incCounter(HookMetricMapper.fromAction(action));
             } else {
@@ -663,6 +687,7 @@ private static class HookMetricMapper {
             ACTION_TO_METRIC.put(ExecutionAction.no_action, MetricName.noop);
             ACTION_TO_METRIC.put(ExecutionAction.update, MetricName.update);
             ACTION_TO_METRIC.put(ExecutionAction.reject, MetricName.reject);
+            ACTION_TO_METRIC.put(ExecutionAction.no_invocation, MetricName.no_invocation);
         }
 
         static MetricName fromStatus(ExecutionStatus status) {
diff --git a/src/main/java/org/prebid/server/metric/StageMetrics.java b/src/main/java/org/prebid/server/metric/StageMetrics.java
index 1cc8f3adfb3..025a47368bd 100644
--- a/src/main/java/org/prebid/server/metric/StageMetrics.java
+++ b/src/main/java/org/prebid/server/metric/StageMetrics.java
@@ -21,6 +21,8 @@ class StageMetrics extends UpdatableMetrics {
         STAGE_TO_METRIC.put(Stage.raw_bidder_response, "rawbidresponse");
         STAGE_TO_METRIC.put(Stage.processed_bidder_response, "procbidresponse");
         STAGE_TO_METRIC.put(Stage.auction_response, "auctionresponse");
+        STAGE_TO_METRIC.put(Stage.all_processed_bid_responses, "allprocbidresponses");
+        STAGE_TO_METRIC.put(Stage.exitpoint, "exitpoint");
     }
 
     private static final String UNKNOWN_STAGE = "unknown";
diff --git a/src/main/java/org/prebid/server/model/HttpRequestContext.java b/src/main/java/org/prebid/server/model/HttpRequestContext.java
index efa07ed621e..9237e9b1803 100644
--- a/src/main/java/org/prebid/server/model/HttpRequestContext.java
+++ b/src/main/java/org/prebid/server/model/HttpRequestContext.java
@@ -2,6 +2,7 @@
 
 import io.vertx.core.MultiMap;
 import io.vertx.core.http.HttpHeaders;
+import io.vertx.core.http.HttpMethod;
 import io.vertx.ext.web.RoutingContext;
 import lombok.Builder;
 import lombok.Value;
@@ -16,6 +17,8 @@
 @Value
 public class HttpRequestContext {
 
+    HttpMethod httpMethod;
+
     String absoluteUri;
 
     CaseInsensitiveMultiMap queryParams;
@@ -30,6 +33,7 @@ public class HttpRequestContext {
 
     public static HttpRequestContext from(RoutingContext context) {
         return HttpRequestContext.builder()
+                .httpMethod(context.request().method())
                 .absoluteUri(context.request().uri())
                 .queryParams(CaseInsensitiveMultiMap.builder().addAll(toMap(context.request().params())).build())
                 .headers(headers(context))
diff --git a/src/main/java/org/prebid/server/privacy/HostVendorTcfDefinerService.java b/src/main/java/org/prebid/server/privacy/HostVendorTcfDefinerService.java
index 471eb5715e3..1588f1827af 100644
--- a/src/main/java/org/prebid/server/privacy/HostVendorTcfDefinerService.java
+++ b/src/main/java/org/prebid/server/privacy/HostVendorTcfDefinerService.java
@@ -51,10 +51,10 @@ private HostVendorTcfResponse toHostVendorTcfResponse(TcfResponse<Integer> tcfRe
         return HostVendorTcfResponse.of(
                 tcfResponse.getUserInGdprScope(),
                 tcfResponse.getCountry(),
-                isCookieSyncAllowed(tcfResponse));
+                isVendorAllowed(tcfResponse));
     }
 
-    private boolean isCookieSyncAllowed(TcfResponse<Integer> hostTcfResponse) {
+    private boolean isVendorAllowed(TcfResponse<Integer> hostTcfResponse) {
         return Optional.ofNullable(hostTcfResponse.getActions())
                 .map(vendorIdToAction -> vendorIdToAction.get(gdprHostVendorId))
                 .map(hostActions -> !hostActions.isBlockPixelSync())
diff --git a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java
index b02698bbce3..5c994ec590f 100644
--- a/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java
+++ b/src/main/java/org/prebid/server/privacy/gdpr/TcfDefinerService.java
@@ -10,7 +10,7 @@
 import org.prebid.server.auction.IpAddressHelper;
 import org.prebid.server.auction.model.IpAddress;
 import org.prebid.server.bidder.BidderCatalog;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.geolocation.model.GeoInfo;
 import org.prebid.server.log.ConditionalLogger;
 import org.prebid.server.log.Logger;
diff --git a/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java b/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java
index 3076d653030..5c646d3b925 100644
--- a/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java
+++ b/src/main/java/org/prebid/server/privacy/gdpr/VendorIdResolver.java
@@ -6,30 +6,20 @@
 public class VendorIdResolver {
 
     private final BidderAliases aliases;
-    private final BidderCatalog bidderCatalog;
 
-    private VendorIdResolver(BidderAliases aliases, BidderCatalog bidderCatalog) {
+    private VendorIdResolver(BidderAliases aliases) {
         this.aliases = aliases;
-        this.bidderCatalog = bidderCatalog;
     }
 
-    public static VendorIdResolver of(BidderAliases aliases, BidderCatalog bidderCatalog) {
-        return new VendorIdResolver(aliases, bidderCatalog);
+    public static VendorIdResolver of(BidderAliases aliases) {
+        return new VendorIdResolver(aliases);
     }
 
     public static VendorIdResolver of(BidderCatalog bidderCatalog) {
-        return of(null, bidderCatalog);
+        return of(BidderAliases.of(null, null, bidderCatalog));
     }
 
     public Integer resolve(String aliasOrBidder) {
-        final Integer requestAliasVendorId = aliases != null ? aliases.resolveAliasVendorId(aliasOrBidder) : null;
-
-        return requestAliasVendorId != null ? requestAliasVendorId : resolveViaCatalog(aliasOrBidder);
-    }
-
-    private Integer resolveViaCatalog(String aliasOrBidder) {
-        final String bidderName = aliases != null ? aliases.resolveBidder(aliasOrBidder) : aliasOrBidder;
-
-        return bidderCatalog.isActive(bidderName) ? bidderCatalog.vendorIdByName(bidderName) : null;
+        return aliases != null ? aliases.resolveAliasVendorId(aliasOrBidder) : null;
     }
 }
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfig.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfig.java
index 3a66ac2374e..49c68685625 100644
--- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfig.java
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfig.java
@@ -1,17 +1,10 @@
 package org.prebid.server.proto.openrtb.ext.request;
 
-import lombok.AllArgsConstructor;
 import lombok.Value;
 
-@AllArgsConstructor(staticName = "of")
-@Value
+@Value(staticConstructor = "of")
 public class ExtBidderConfig {
 
-    /**
-     * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.fpd
-     */
-    ExtBidderConfigFpd fpd;
-
     /**
      * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.ortb2
      */
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigFpd.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigFpd.java
deleted file mode 100644
index 27748982011..00000000000
--- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtBidderConfigFpd.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.prebid.server.proto.openrtb.ext.request;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import lombok.AllArgsConstructor;
-import lombok.Value;
-
-@AllArgsConstructor(staticName = "of")
-@Value
-public class ExtBidderConfigFpd {
-
-    /**
-     * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.fpd.context
-     */
-    JsonNode context;
-
-    /**
-     * Defines the contract for bidrequest.ext.prebid.bidderconfig.config.fpd.user
-     */
-    JsonNode user;
-}
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java
new file mode 100644
index 00000000000..ab0565ce44e
--- /dev/null
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustments.java
@@ -0,0 +1,15 @@
+package org.prebid.server.proto.openrtb.ext.request;
+
+import lombok.Builder;
+import lombok.Value;
+
+import java.util.List;
+import java.util.Map;
+
+@Builder(toBuilder = true)
+@Value
+public class ExtRequestBidAdjustments {
+
+    Map<String, Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>>> mediatype;
+
+}
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java
new file mode 100644
index 00000000000..a857575a85f
--- /dev/null
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestBidAdjustmentsRule.java
@@ -0,0 +1,24 @@
+package org.prebid.server.proto.openrtb.ext.request;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+import lombok.Value;
+import org.prebid.server.bidadjustments.model.BidAdjustmentType;
+
+import java.math.BigDecimal;
+
+@Builder(toBuilder = true)
+@Value
+public class ExtRequestBidAdjustmentsRule {
+
+    @JsonProperty("adjtype")
+    BidAdjustmentType adjType;
+
+    BigDecimal value;
+
+    String currency;
+
+    public String toString() {
+        return "[adjtype=%s, value=%s, currency=%s]".formatted(adjType, value, currency);
+    }
+}
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java
index 6dee1b7ba38..cb325bd088a 100644
--- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebid.java
@@ -50,6 +50,11 @@ public class ExtRequestPrebid {
      */
     ExtRequestBidAdjustmentFactors bidadjustmentfactors;
 
+    /**
+     * Defines the contract for bidrequest.ext.prebid.bidadjustments
+     */
+    ObjectNode bidadjustments;
+
     /**
      * Defines the contract for bidrequest.ext.prebid.currency
      */
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java
index 900a355a9fb..c570e221362 100644
--- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtUser.java
@@ -55,8 +55,13 @@ public class ExtUser extends FlexibleExtension {
 
     /**
      * Defines the contract for bidrequest.user.ext.ConsentedProvidersSettings
+     * <p>
+     * TODO: Remove after PBS 4.0
      */
+    @Deprecated(forRemoval = true)
     @JsonProperty("ConsentedProvidersSettings")
+    ConsentedProvidersSettings deprecatedConsentedProvidersSettings;
+
     ConsentedProvidersSettings consentedProvidersSettings;
 
     @JsonIgnore
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java
index 732ddca6236..d619ed27e80 100644
--- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ImpMediaType.java
@@ -9,6 +9,8 @@ public enum ImpMediaType {
     @JsonProperty("native")
     xNative,
     video,
+    @JsonProperty("video-instream")
+    video_instream,
     @JsonProperty("video-outstream")
     video_outstream;
 
@@ -16,6 +18,7 @@ public enum ImpMediaType {
     public String toString() {
         return this == xNative ? "native"
                 : this == video_outstream ? "video-outstream"
+                : this == video_instream ? "video-instream"
                 : super.toString();
     }
 }
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/insticator/ExtImpInsticator.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/insticator/ExtImpInsticator.java
new file mode 100644
index 00000000000..067680eb0df
--- /dev/null
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/insticator/ExtImpInsticator.java
@@ -0,0 +1,14 @@
+package org.prebid.server.proto.openrtb.ext.request.insticator;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class ExtImpInsticator {
+
+    @JsonProperty("adUnitId")
+    String adUnitId;
+
+    @JsonProperty("publisherId")
+    String publisherId;
+}
diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidMeta.java b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidMeta.java
index b7295616a82..eaba36abca0 100644
--- a/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidMeta.java
+++ b/src/main/java/org/prebid/server/proto/openrtb/ext/response/ExtBidPrebidMeta.java
@@ -67,4 +67,6 @@ public class ExtBidPrebidMeta {
     @JsonProperty("secondaryCatIds")
     List<String> secondaryCategoryIdList;
 
+    String seat;
+
 }
diff --git a/src/main/java/org/prebid/server/settings/ApplicationSettings.java b/src/main/java/org/prebid/server/settings/ApplicationSettings.java
index da414bef279..7a6582ccd42 100644
--- a/src/main/java/org/prebid/server/settings/ApplicationSettings.java
+++ b/src/main/java/org/prebid/server/settings/ApplicationSettings.java
@@ -1,7 +1,7 @@
 package org.prebid.server.settings;
 
 import io.vertx.core.Future;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.StoredDataResult;
 import org.prebid.server.settings.model.StoredResponseDataResult;
diff --git a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java
index 97348e7bbd8..9f8fcea9ff2 100644
--- a/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java
+++ b/src/main/java/org/prebid/server/settings/CachingApplicationSettings.java
@@ -3,7 +3,7 @@
 import io.vertx.core.Future;
 import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.log.Logger;
 import org.prebid.server.log.LoggerFactory;
 import org.prebid.server.metric.MetricName;
diff --git a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java
index 2edd16b7345..32d47d6abad 100644
--- a/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java
+++ b/src/main/java/org/prebid/server/settings/CompositeApplicationSettings.java
@@ -1,7 +1,7 @@
 package org.prebid.server.settings;
 
 import io.vertx.core.Future;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.settings.helper.StoredDataFetcher;
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.StoredDataResult;
diff --git a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java
index 9dad7f6a28b..c346e4824f4 100644
--- a/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java
+++ b/src/main/java/org/prebid/server/settings/DatabaseApplicationSettings.java
@@ -7,7 +7,7 @@
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.settings.helper.DatabaseStoredDataResultMapper;
diff --git a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java
index 11a0d2cb3af..bfde0fc2e81 100644
--- a/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java
+++ b/src/main/java/org/prebid/server/settings/EnrichingApplicationSettings.java
@@ -4,7 +4,7 @@
 import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.activity.ActivitiesConfigResolver;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.floors.PriceFloorsConfigResolver;
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
diff --git a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java
index 33a1ea36390..1a2f42e86c4 100644
--- a/src/main/java/org/prebid/server/settings/FileApplicationSettings.java
+++ b/src/main/java/org/prebid/server/settings/FileApplicationSettings.java
@@ -8,7 +8,7 @@
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.settings.model.Account;
diff --git a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java
index 1c78e4693c5..98517003baf 100644
--- a/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java
+++ b/src/main/java/org/prebid/server/settings/HttpApplicationSettings.java
@@ -9,7 +9,7 @@
 import org.apache.commons.collections4.MapUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.log.Logger;
diff --git a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java
index f6198a5ad94..f1c8b107c5f 100644
--- a/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java
+++ b/src/main/java/org/prebid/server/settings/S3ApplicationSettings.java
@@ -8,7 +8,7 @@
 import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.auction.model.Tuple2;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.json.DecodeException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.settings.model.Account;
diff --git a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java
index 9708c23bb1c..9943535aa7e 100644
--- a/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java
+++ b/src/main/java/org/prebid/server/settings/model/AccountAuctionConfig.java
@@ -2,6 +2,7 @@
 
 import com.fasterxml.jackson.annotation.JsonAlias;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import lombok.Builder;
 import lombok.Value;
 import org.prebid.server.spring.config.bidder.model.MediaType;
@@ -33,6 +34,9 @@ public class AccountAuctionConfig {
     @JsonAlias("bid-validations")
     AccountBidValidationConfig bidValidations;
 
+    @JsonProperty("bidadjustments")
+    ObjectNode bidAdjustments;
+
     AccountEventsConfig events;
 
     @JsonAlias("price-floors")
diff --git a/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java b/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java
index 75d3b03b6e5..f45af371385 100644
--- a/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java
+++ b/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java
@@ -14,4 +14,6 @@ public class AccountHooksConfiguration {
     ExecutionPlan executionPlan;
 
     Map<String, ObjectNode> modules;
+
+    HooksAdminConfig admin;
 }
diff --git a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java
index 76be21ea70e..9acebb1c427 100644
--- a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java
+++ b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsConfig.java
@@ -23,4 +23,10 @@ public class AccountPriceFloorsConfig {
 
     @JsonAlias("use-dynamic-data")
     Boolean useDynamicData;
+
+    @JsonAlias("max-rules")
+    Long maxRules;
+
+    @JsonAlias("max-schema-dims")
+    Long maxSchemaDims;
 }
diff --git a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java
index 2cb3854371a..42824b410e2 100644
--- a/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java
+++ b/src/main/java/org/prebid/server/settings/model/AccountPriceFloorsFetchConfig.java
@@ -21,6 +21,9 @@ public class AccountPriceFloorsFetchConfig {
     @JsonAlias("max-rules")
     Long maxRules;
 
+    @JsonAlias("max-schema-dims")
+    Long maxSchemaDims;
+
     @JsonAlias("max-age-sec")
     Long maxAgeSec;
 
diff --git a/src/main/java/org/prebid/server/settings/model/HooksAdminConfig.java b/src/main/java/org/prebid/server/settings/model/HooksAdminConfig.java
new file mode 100644
index 00000000000..36b12a401f0
--- /dev/null
+++ b/src/main/java/org/prebid/server/settings/model/HooksAdminConfig.java
@@ -0,0 +1,16 @@
+package org.prebid.server.settings.model;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import lombok.Builder;
+import lombok.Value;
+
+import java.util.Map;
+
+@Builder
+@Value
+public class HooksAdminConfig {
+
+    @JsonAlias("module-execution")
+    Map<String, Boolean> moduleExecution;
+
+}
diff --git a/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java b/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java
index cc4b80ad870..bdd9e8258e0 100644
--- a/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java
+++ b/src/main/java/org/prebid/server/settings/service/DatabasePeriodicRefreshService.java
@@ -4,8 +4,8 @@
 import io.vertx.core.Promise;
 import io.vertx.core.Vertx;
 import org.apache.commons.lang3.StringUtils;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.log.Logger;
 import org.prebid.server.log.LoggerFactory;
 import org.prebid.server.metric.MetricName;
diff --git a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java
index 01153008824..d618c36fa36 100644
--- a/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/AnalyticsConfiguration.java
@@ -5,6 +5,7 @@
 import lombok.NoArgsConstructor;
 import org.apache.commons.collections4.ListUtils;
 import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.analytics.AnalyticsReporter;
 import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator;
 import org.prebid.server.analytics.reporter.agma.AgmaAnalyticsReporter;
@@ -111,8 +112,9 @@ private static class AgmaAnalyticsConfigurationProperties {
             public AgmaAnalyticsProperties toComponentProperties() {
                 final Map<String, String> accountsByPublisherId = accounts.stream()
                         .collect(Collectors.toMap(
-                                AgmaAnalyticsAccountProperties::getPublisherId,
-                                AgmaAnalyticsAccountProperties::getCode));
+                                this::buildPublisherSiteAppIdKey,
+                                AgmaAnalyticsAccountProperties::getCode
+                        ));
 
                 return AgmaAnalyticsProperties.builder()
                         .url(endpoint.getUrl())
@@ -125,6 +127,14 @@ public AgmaAnalyticsProperties toComponentProperties() {
                         .build();
             }
 
+            private String buildPublisherSiteAppIdKey(AgmaAnalyticsAccountProperties account) {
+                final String publisherId = account.getPublisherId();
+                final String siteAppId = account.getSiteAppId();
+                return StringUtils.isNotBlank(siteAppId)
+                        ? String.format("%s_%s", publisherId, siteAppId)
+                        : publisherId;
+            }
+
             @Validated
             @NoArgsConstructor
             @Data
diff --git a/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java b/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java
index 7edc69d6176..bfb56b8c0c1 100644
--- a/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/GeoLocationConfiguration.java
@@ -1,16 +1,12 @@
 package org.prebid.server.spring.config;
 
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpClientOptions;
 import lombok.Data;
-import org.apache.commons.lang3.ObjectUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.prebid.server.auction.GeoLocationServiceWrapper;
 import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver;
-import org.prebid.server.execution.RemoteFileSyncer;
-import org.prebid.server.execution.retry.ExponentialBackoffRetryPolicy;
-import org.prebid.server.execution.retry.FixedIntervalRetryPolicy;
-import org.prebid.server.execution.retry.RetryPolicy;
+import org.prebid.server.execution.file.FileUtil;
+import org.prebid.server.execution.file.syncer.FileSyncer;
 import org.prebid.server.geolocation.CircuitBreakerSecuredGeoLocationService;
 import org.prebid.server.geolocation.ConfigurationGeoLocationService;
 import org.prebid.server.geolocation.CountryCodeMapper;
@@ -18,9 +14,7 @@
 import org.prebid.server.geolocation.MaxMindGeoLocationService;
 import org.prebid.server.metric.Metrics;
 import org.prebid.server.spring.config.model.CircuitBreakerProperties;
-import org.prebid.server.spring.config.model.ExponentialBackoffProperties;
-import org.prebid.server.spring.config.model.HttpClientProperties;
-import org.prebid.server.spring.config.model.RemoteFileSyncerProperties;
+import org.prebid.server.spring.config.model.FileSyncerProperties;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
@@ -57,14 +51,14 @@ CircuitBreakerProperties maxMindCircuitBreakerProperties() {
 
         @Bean
         @ConfigurationProperties(prefix = "geolocation.maxmind.remote-file-syncer")
-        RemoteFileSyncerProperties maxMindRemoteFileSyncerProperties() {
-            return new RemoteFileSyncerProperties();
+        FileSyncerProperties maxMindRemoteFileSyncerProperties() {
+            return new FileSyncerProperties();
         }
 
         @Bean
         @ConditionalOnProperty(prefix = "geolocation.circuit-breaker", name = "enabled", havingValue = "false",
                 matchIfMissing = true)
-        GeoLocationService basicGeoLocationService(RemoteFileSyncerProperties fileSyncerProperties,
+        GeoLocationService basicGeoLocationService(FileSyncerProperties fileSyncerProperties,
                                                    Vertx vertx) {
 
             return createGeoLocationService(fileSyncerProperties, vertx);
@@ -75,7 +69,7 @@ GeoLocationService basicGeoLocationService(RemoteFileSyncerProperties fileSyncer
         CircuitBreakerSecuredGeoLocationService circuitBreakerSecuredGeoLocationService(
                 Vertx vertx,
                 Metrics metrics,
-                RemoteFileSyncerProperties fileSyncerProperties,
+                FileSyncerProperties fileSyncerProperties,
                 @Qualifier("maxMindCircuitBreakerProperties") CircuitBreakerProperties circuitBreakerProperties,
                 Clock clock) {
 
@@ -85,49 +79,12 @@ CircuitBreakerSecuredGeoLocationService circuitBreakerSecuredGeoLocationService(
                     circuitBreakerProperties.getClosingIntervalMs(), clock);
         }
 
-        private GeoLocationService createGeoLocationService(RemoteFileSyncerProperties properties, Vertx vertx) {
+        private GeoLocationService createGeoLocationService(FileSyncerProperties properties, Vertx vertx) {
             final MaxMindGeoLocationService maxMindGeoLocationService = new MaxMindGeoLocationService();
-            final HttpClientProperties httpClientProperties = properties.getHttpClient();
-            final HttpClientOptions httpClientOptions = new HttpClientOptions()
-                    .setConnectTimeout(httpClientProperties.getConnectTimeoutMs())
-                    .setMaxRedirects(httpClientProperties.getMaxRedirects());
-
-            final RemoteFileSyncer remoteFileSyncer = new RemoteFileSyncer(
-                    maxMindGeoLocationService,
-                    properties.getDownloadUrl(),
-                    properties.getSaveFilepath(),
-                    properties.getTmpFilepath(),
-                    toRetryPolicy(properties),
-                    properties.getTimeoutMs(),
-                    properties.getUpdateIntervalMs(),
-                    vertx.createHttpClient(httpClientOptions),
-                    vertx);
-
-            remoteFileSyncer.sync();
+            final FileSyncer fileSyncer = FileUtil.fileSyncerFor(maxMindGeoLocationService, properties, vertx);
+            fileSyncer.sync();
             return maxMindGeoLocationService;
         }
-
-        // TODO: remove after transition period
-        private static RetryPolicy toRetryPolicy(RemoteFileSyncerProperties properties) {
-            final Long retryIntervalMs = properties.getRetryIntervalMs();
-            final Integer retryCount = properties.getRetryCount();
-            final boolean fixedRetryPolicyDefined = ObjectUtils.anyNotNull(retryIntervalMs, retryCount);
-            final boolean fixedRetryPolicyValid = ObjectUtils.allNotNull(retryIntervalMs, retryCount)
-                    || !fixedRetryPolicyDefined;
-
-            if (!fixedRetryPolicyValid) {
-                throw new IllegalArgumentException("fixed interval retry policy is invalid");
-            }
-
-            final ExponentialBackoffProperties exponentialBackoffProperties = properties.getRetry();
-            return fixedRetryPolicyDefined
-                    ? FixedIntervalRetryPolicy.limited(retryIntervalMs, retryCount)
-                    : ExponentialBackoffRetryPolicy.of(
-                    exponentialBackoffProperties.getDelayMillis(),
-                    exponentialBackoffProperties.getMaxDelayMillis(),
-                    exponentialBackoffProperties.getFactor(),
-                    exponentialBackoffProperties.getJitter());
-        }
     }
 
     @Configuration
diff --git a/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java b/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java
index 93eaeb48030..836b45ca285 100644
--- a/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/HealthCheckerConfiguration.java
@@ -2,7 +2,7 @@
 
 import io.vertx.core.Vertx;
 import io.vertx.sqlclient.Pool;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.geolocation.GeoLocationService;
 import org.prebid.server.health.ApplicationChecker;
 import org.prebid.server.health.DatabaseHealthChecker;
diff --git a/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java b/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java
index bffc5ee32f0..5a05ccb8c8e 100644
--- a/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/HooksConfiguration.java
@@ -3,11 +3,13 @@
 import io.vertx.core.Vertx;
 import lombok.Data;
 import lombok.NoArgsConstructor;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.hooks.execution.HookCatalog;
 import org.prebid.server.hooks.execution.HookStageExecutor;
 import org.prebid.server.hooks.v1.Module;
 import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.settings.model.HooksAdminConfig;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -15,6 +17,8 @@
 
 import java.time.Clock;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
 
 @Configuration
 public class HooksConfiguration {
@@ -30,16 +34,22 @@ HookStageExecutor hookStageExecutor(HooksConfigurationProperties hooksConfigurat
                                         TimeoutFactory timeoutFactory,
                                         Vertx vertx,
                                         Clock clock,
-                                        JacksonMapper mapper) {
+                                        JacksonMapper mapper,
+                                        @Value("${settings.modules.require-config-to-invoke:false}")
+                                        boolean isConfigToInvokeRequired) {
 
         return HookStageExecutor.create(
                 hooksConfiguration.getHostExecutionPlan(),
                 hooksConfiguration.getDefaultAccountExecutionPlan(),
+                Optional.ofNullable(hooksConfiguration.getAdmin())
+                        .map(HooksAdminConfig::getModuleExecution)
+                        .orElseGet(Collections::emptyMap),
                 hookCatalog,
                 timeoutFactory,
                 vertx,
                 clock,
-                mapper);
+                mapper,
+                isConfigToInvokeRequired);
     }
 
     @Bean
@@ -56,5 +66,7 @@ private static class HooksConfigurationProperties {
         String hostExecutionPlan;
 
         String defaultAccountExecutionPlan;
+
+        HooksAdminConfig admin;
     }
 }
diff --git a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java
index 79a62ffbe87..8a483e92a4d 100644
--- a/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/PriceFloorsConfiguration.java
@@ -3,7 +3,7 @@
 import io.vertx.core.Vertx;
 import org.prebid.server.auction.adjustment.FloorAdjustmentFactorResolver;
 import org.prebid.server.currency.CurrencyConversionService;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.floors.BasicPriceFloorAdjuster;
 import org.prebid.server.floors.BasicPriceFloorEnforcer;
 import org.prebid.server.floors.BasicPriceFloorProcessor;
diff --git a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java
index 5676d7fd43e..deaa768320c 100644
--- a/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/ServiceConfiguration.java
@@ -54,11 +54,9 @@
 import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory;
 import org.prebid.server.auction.privacy.contextfactory.CookieSyncPrivacyContextFactory;
 import org.prebid.server.auction.privacy.contextfactory.SetuidPrivacyContextFactory;
-import org.prebid.server.auction.privacy.enforcement.ActivityEnforcement;
 import org.prebid.server.auction.privacy.enforcement.CcpaEnforcement;
-import org.prebid.server.auction.privacy.enforcement.CoppaEnforcement;
+import org.prebid.server.auction.privacy.enforcement.PrivacyEnforcement;
 import org.prebid.server.auction.privacy.enforcement.PrivacyEnforcementService;
-import org.prebid.server.auction.privacy.enforcement.TcfEnforcement;
 import org.prebid.server.auction.requestfactory.AmpRequestFactory;
 import org.prebid.server.auction.requestfactory.AuctionRequestFactory;
 import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver;
@@ -66,6 +64,9 @@
 import org.prebid.server.auction.requestfactory.VideoRequestFactory;
 import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager;
 import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConverterFactory;
+import org.prebid.server.bidadjustments.BidAdjustmentsProcessor;
+import org.prebid.server.bidadjustments.BidAdjustmentsResolver;
+import org.prebid.server.bidadjustments.BidAdjustmentsRetriever;
 import org.prebid.server.bidder.BidderCatalog;
 import org.prebid.server.bidder.BidderDeps;
 import org.prebid.server.bidder.BidderErrorNotifier;
@@ -84,7 +85,7 @@
 import org.prebid.server.cookie.UidsCookieService;
 import org.prebid.server.currency.CurrencyConversionService;
 import org.prebid.server.events.EventsService;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.floors.PriceFloorAdjuster;
 import org.prebid.server.floors.PriceFloorEnforcer;
 import org.prebid.server.floors.PriceFloorProcessor;
@@ -107,6 +108,7 @@
 import org.prebid.server.privacy.gdpr.TcfDefinerService;
 import org.prebid.server.settings.ApplicationSettings;
 import org.prebid.server.settings.model.BidValidationEnforcement;
+import org.prebid.server.spring.config.model.CacheDefaultTtlProperties;
 import org.prebid.server.spring.config.model.ExternalConversionProperties;
 import org.prebid.server.spring.config.model.HttpClientCircuitBreakerProperties;
 import org.prebid.server.spring.config.model.HttpClientProperties;
@@ -160,6 +162,8 @@ CoreCacheService cacheService(
             @Value("${cache.path}") String path,
             @Value("${cache.query}") String query,
             @Value("${auction.cache.expected-request-time-ms}") long expectedCacheTimeMs,
+            @Value("${pbc.api.key:#{null}}") String apiKey,
+            @Value("${cache.api-key-secured:false}") boolean apiKeySecured,
             VastModifier vastModifier,
             EventsService eventsService,
             HttpClient httpClient,
@@ -172,6 +176,8 @@ CoreCacheService cacheService(
                 CacheServiceUtil.getCacheEndpointUrl(scheme, host, path),
                 CacheServiceUtil.getCachedAssetUrlTemplate(scheme, host, path, query),
                 expectedCacheTimeMs,
+                apiKey,
+                apiKeySecured,
                 vastModifier,
                 eventsService,
                 metrics,
@@ -420,7 +426,8 @@ AuctionRequestFactory auctionRequestFactory(
             AuctionPrivacyContextFactory auctionPrivacyContextFactory,
             DebugResolver debugResolver,
             JacksonMapper mapper,
-            GeoLocationServiceWrapper geoLocationServiceWrapper) {
+            GeoLocationServiceWrapper geoLocationServiceWrapper,
+            BidAdjustmentsRetriever bidAdjustmentsRetriever) {
 
         return new AuctionRequestFactory(
                 maxRequestSize,
@@ -436,7 +443,8 @@ AuctionRequestFactory auctionRequestFactory(
                 auctionPrivacyContextFactory,
                 debugResolver,
                 mapper,
-                geoLocationServiceWrapper);
+                geoLocationServiceWrapper,
+                bidAdjustmentsRetriever);
     }
 
     @Bean
@@ -748,11 +756,13 @@ HttpBidderRequester httpBidderRequester(
             HttpBidderRequestEnricher requestEnricher,
             JacksonMapper mapper) {
 
-        return new HttpBidderRequester(httpClient,
+        return new HttpBidderRequester(
+                httpClient,
                 bidderRequestCompletionTrackerFactory,
                 bidderErrorNotifier,
                 requestEnricher,
-                mapper);
+                mapper,
+                logSamplingRate);
     }
 
     @Bean
@@ -785,6 +795,16 @@ BidderErrorNotifier bidderErrorNotifier(
                 metrics);
     }
 
+    @Bean
+    CacheDefaultTtlProperties cacheDefaultTtlProperties(
+            @Value("${cache.default-ttl-seconds.banner:300}") Integer bannerTtl,
+            @Value("${cache.default-ttl-seconds.video:1500}") Integer videoTtl,
+            @Value("${cache.default-ttl-seconds.audio:1500}") Integer audioTtl,
+            @Value("${cache.default-ttl-seconds.native:300}") Integer nativeTtl) {
+
+        return CacheDefaultTtlProperties.of(bannerTtl, videoTtl, audioTtl, nativeTtl);
+    }
+
     @Bean
     BidResponseCreator bidResponseCreator(
             CoreCacheService coreCacheService,
@@ -800,7 +820,8 @@ BidResponseCreator bidResponseCreator(
             Clock clock,
             JacksonMapper mapper,
             @Value("${cache.banner-ttl-seconds:#{null}}") Integer bannerCacheTtl,
-            @Value("${cache.video-ttl-seconds:#{null}}") Integer videoCacheTtl) {
+            @Value("${cache.video-ttl-seconds:#{null}}") Integer videoCacheTtl,
+            CacheDefaultTtlProperties cacheDefaultTtlProperties) {
 
         return new BidResponseCreator(
                 coreCacheService,
@@ -815,7 +836,8 @@ BidResponseCreator bidResponseCreator(
                 truncateAttrChars,
                 clock,
                 mapper,
-                CacheTtl.of(bannerCacheTtl, videoCacheTtl));
+                CacheTtl.of(bannerCacheTtl, videoCacheTtl),
+                cacheDefaultTtlProperties);
     }
 
     @Bean
@@ -877,19 +899,11 @@ ExchangeService exchangeService(
 
     @Bean
     BidsAdjuster bidsAdjuster(ResponseBidValidator responseBidValidator,
-                              CurrencyConversionService currencyConversionService,
                               PriceFloorEnforcer priceFloorEnforcer,
                               DsaEnforcer dsaEnforcer,
-                              BidAdjustmentFactorResolver bidAdjustmentFactorResolver,
-                              JacksonMapper mapper) {
+                              BidAdjustmentsProcessor bidAdjustmentsProcessor) {
 
-        return new BidsAdjuster(
-                responseBidValidator,
-                currencyConversionService,
-                bidAdjustmentFactorResolver,
-                priceFloorEnforcer,
-                dsaEnforcer,
-                mapper);
+        return new BidsAdjuster(responseBidValidator, priceFloorEnforcer, bidAdjustmentsProcessor, dsaEnforcer);
     }
 
     @Bean
@@ -930,16 +944,8 @@ StoredResponseProcessor storedResponseProcessor(ApplicationSettings applicationS
     }
 
     @Bean
-    PrivacyEnforcementService privacyEnforcementService(CoppaEnforcement coppaEnforcement,
-                                                        CcpaEnforcement ccpaEnforcement,
-                                                        TcfEnforcement tcfEnforcement,
-                                                        ActivityEnforcement activityEnforcement) {
-
-        return new PrivacyEnforcementService(
-                coppaEnforcement,
-                ccpaEnforcement,
-                tcfEnforcement,
-                activityEnforcement);
+    PrivacyEnforcementService privacyEnforcementService(List<PrivacyEnforcement> enforcements) {
+        return new PrivacyEnforcementService(enforcements);
     }
 
     @Bean
@@ -1168,6 +1174,29 @@ SkippedAuctionService skipAuctionService(StoredResponseProcessor storedResponseP
         return new SkippedAuctionService(storedResponseProcessor, bidResponseCreator);
     }
 
+    @Bean
+    BidAdjustmentsRetriever bidAdjustmentsRetriever(JacksonMapper mapper, JsonMerger jsonMerger) {
+        return new BidAdjustmentsRetriever(mapper, jsonMerger, logSamplingRate);
+    }
+
+    @Bean
+    BidAdjustmentsResolver bidAdjustmentsResolver(CurrencyConversionService currencyService) {
+        return new BidAdjustmentsResolver(currencyService);
+    }
+
+    @Bean
+    BidAdjustmentsProcessor bidAdjustmentsProcessor(CurrencyConversionService currencyService,
+                                                    BidAdjustmentFactorResolver bidAdjustmentFactorResolver,
+                                                    BidAdjustmentsResolver bidAdjustmentsResolver,
+                                                    JacksonMapper mapper) {
+
+        return new BidAdjustmentsProcessor(
+                currencyService,
+                bidAdjustmentFactorResolver,
+                bidAdjustmentsResolver,
+                mapper);
+    }
+
     private static List<String> splitToList(String listAsString) {
         return splitToCollection(listAsString, ArrayList::new);
     }
diff --git a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java
index f7aaa9bb4ba..f101495eb66 100644
--- a/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/SettingsConfiguration.java
@@ -7,7 +7,7 @@
 import lombok.experimental.UtilityClass;
 import org.apache.commons.lang3.ObjectUtils;
 import org.prebid.server.activity.ActivitiesConfigResolver;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.floors.PriceFloorsConfigResolver;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.json.JsonMerger;
diff --git a/src/main/java/org/prebid/server/spring/config/bidder/InsticatorConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/InsticatorConfiguration.java
new file mode 100644
index 00000000000..7ae0e1a3432
--- /dev/null
+++ b/src/main/java/org/prebid/server/spring/config/bidder/InsticatorConfiguration.java
@@ -0,0 +1,43 @@
+package org.prebid.server.spring.config.bidder;
+
+import org.prebid.server.bidder.BidderDeps;
+import org.prebid.server.bidder.insticator.InsticatorBidder;
+import org.prebid.server.currency.CurrencyConversionService;
+import org.prebid.server.json.JacksonMapper;
+import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
+import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
+import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
+import org.prebid.server.spring.env.YamlPropertySourceFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+import jakarta.validation.constraints.NotBlank;
+
+@Configuration
+@PropertySource(value = "classpath:/bidder-config/insticator.yaml", factory = YamlPropertySourceFactory.class)
+public class InsticatorConfiguration {
+
+    private static final String BIDDER_NAME = "insticator";
+
+    @Bean("insticatorConfigurationProperties")
+    @ConfigurationProperties("adapters.insticator")
+    BidderConfigurationProperties configurationProperties() {
+        return new BidderConfigurationProperties();
+    }
+
+    @Bean
+    BidderDeps insticatorBidderDeps(BidderConfigurationProperties insticatorConfigurationProperties,
+                                    @NotBlank @Value("${external-url}") String externalUrl,
+                                    CurrencyConversionService currencyConversionService,
+                                    JacksonMapper mapper) {
+
+        return BidderDepsAssembler.forBidder(BIDDER_NAME)
+                .withConfig(insticatorConfigurationProperties)
+                .usersyncerCreator(UsersyncerCreator.create(externalUrl))
+                .bidderCreator(config -> new InsticatorBidder(currencyConversionService, config.getEndpoint(), mapper))
+                .assemble();
+    }
+}
diff --git a/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java b/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java
index 1e449c950d6..7296f81625b 100644
--- a/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/bidder/PgamSspConfiguration.java
@@ -2,6 +2,7 @@
 
 import org.prebid.server.bidder.BidderDeps;
 import org.prebid.server.bidder.pgamssp.PgamSspBidder;
+import org.prebid.server.currency.CurrencyConversionService;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
 import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
@@ -29,13 +30,14 @@ BidderConfigurationProperties configurationProperties() {
 
     @Bean
     BidderDeps pgamsspBidderDeps(BidderConfigurationProperties pgamsspConfigurationProperties,
+                                 CurrencyConversionService currencyConversionService,
                                  @NotBlank @Value("${external-url}") String externalUrl,
                                  JacksonMapper mapper) {
 
         return BidderDepsAssembler.forBidder(BIDDER_NAME)
                 .withConfig(pgamsspConfigurationProperties)
                 .usersyncerCreator(UsersyncerCreator.create(externalUrl))
-                .bidderCreator(config -> new PgamSspBidder(config.getEndpoint(), mapper))
+                .bidderCreator(config -> new PgamSspBidder(config.getEndpoint(), currencyConversionService, mapper))
                 .assemble();
     }
 }
diff --git a/src/main/java/org/prebid/server/spring/config/bidder/model/BidderConfigurationProperties.java b/src/main/java/org/prebid/server/spring/config/bidder/model/BidderConfigurationProperties.java
index 0f5fde1d480..a13e1aef3ec 100644
--- a/src/main/java/org/prebid/server/spring/config/bidder/model/BidderConfigurationProperties.java
+++ b/src/main/java/org/prebid/server/spring/config/bidder/model/BidderConfigurationProperties.java
@@ -51,6 +51,8 @@ public class BidderConfigurationProperties {
 
     private Ortb ortb;
 
+    private long tmaxDeductionMs;
+
     private final Class<? extends BidderConfigurationProperties> selfClass;
 
     public BidderConfigurationProperties() {
diff --git a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java
index cd7553bb34a..8780225ff9f 100644
--- a/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java
+++ b/src/main/java/org/prebid/server/spring/config/bidder/util/BidderInfoCreator.java
@@ -32,6 +32,7 @@ public static BidderInfo create(BidderConfigurationProperties configurationPrope
                 configurationProperties.getPbsEnforcesCcpa(),
                 configurationProperties.getModifyingVastXmlAllowed(),
                 configurationProperties.getEndpointCompression(),
-                configurationProperties.getOrtb());
+                configurationProperties.getOrtb(),
+                configurationProperties.getTmaxDeductionMs());
     }
 }
diff --git a/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java b/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java
index 21d145bf826..daba3fda594 100644
--- a/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/metrics/MetricsConfiguration.java
@@ -1,5 +1,6 @@
 package org.prebid.server.spring.config.metrics;
 
+import org.prebid.server.auction.HooksMetricsService;
 import org.slf4j.LoggerFactory;
 import com.codahale.metrics.Slf4jReporter;
 import com.codahale.metrics.ConsoleReporter;
@@ -134,6 +135,11 @@ AccountMetricsVerbosityResolver accountMetricsVerbosity(AccountsProperties accou
                 accountsProperties.getDetailedVerbosity());
     }
 
+    @Bean
+    HooksMetricsService hooksMetricsService(Metrics metrics) {
+        return new HooksMetricsService(metrics);
+    }
+
     @Component
     @ConfigurationProperties(prefix = "metrics.graphite")
     @ConditionalOnProperty(prefix = "metrics.graphite", name = "enabled", havingValue = "true")
diff --git a/src/main/java/org/prebid/server/spring/config/model/CacheDefaultTtlProperties.java b/src/main/java/org/prebid/server/spring/config/model/CacheDefaultTtlProperties.java
new file mode 100644
index 00000000000..2a3e36b6ef1
--- /dev/null
+++ b/src/main/java/org/prebid/server/spring/config/model/CacheDefaultTtlProperties.java
@@ -0,0 +1,15 @@
+package org.prebid.server.spring.config.model;
+
+import lombok.Value;
+
+@Value(staticConstructor = "of")
+public class CacheDefaultTtlProperties {
+
+    Integer bannerTtl;
+
+    Integer videoTtl;
+
+    Integer audioTtl;
+
+    Integer nativeTtl;
+}
diff --git a/src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java b/src/main/java/org/prebid/server/spring/config/model/FileSyncerProperties.java
similarity index 83%
rename from src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java
rename to src/main/java/org/prebid/server/spring/config/model/FileSyncerProperties.java
index 09e56ac59c6..54dbd81a5a9 100644
--- a/src/main/java/org/prebid/server/spring/config/model/RemoteFileSyncerProperties.java
+++ b/src/main/java/org/prebid/server/spring/config/model/FileSyncerProperties.java
@@ -11,7 +11,9 @@
 @Validated
 @Data
 @NoArgsConstructor
-public class RemoteFileSyncerProperties {
+public class FileSyncerProperties {
+
+    private Type type = Type.REMOTE;
 
     @NotBlank
     private String downloadUrl;
@@ -37,6 +39,13 @@ public class RemoteFileSyncerProperties {
     @NotNull
     private Long updateIntervalMs;
 
+    private boolean checkSize;
+
     @NotNull
     private HttpClientProperties httpClient;
+
+    public enum Type {
+
+        LOCAL, REMOTE
+    }
 }
diff --git a/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java
index 82794d58ffb..b7c9eb405da 100644
--- a/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java
+++ b/src/main/java/org/prebid/server/spring/config/server/application/ApplicationServerConfiguration.java
@@ -15,6 +15,7 @@
 import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator;
 import org.prebid.server.auction.AmpResponsePostProcessor;
 import org.prebid.server.auction.ExchangeService;
+import org.prebid.server.auction.HooksMetricsService;
 import org.prebid.server.auction.SkippedAuctionService;
 import org.prebid.server.auction.VideoResponseFactory;
 import org.prebid.server.auction.gpp.CookieSyncGppService;
@@ -29,7 +30,7 @@
 import org.prebid.server.cookie.CookieDeprecationService;
 import org.prebid.server.cookie.CookieSyncService;
 import org.prebid.server.cookie.UidsCookieService;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.handler.BidderParamHandler;
 import org.prebid.server.handler.CookieSyncHandler;
 import org.prebid.server.handler.ExceptionHandler;
@@ -49,6 +50,7 @@
 import org.prebid.server.handler.openrtb2.VideoHandler;
 import org.prebid.server.health.HealthChecker;
 import org.prebid.server.health.PeriodicHealthChecker;
+import org.prebid.server.hooks.execution.HookStageExecutor;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.log.HttpInteractionLogger;
 import org.prebid.server.metric.Metrics;
@@ -210,9 +212,11 @@ org.prebid.server.handler.openrtb2.AuctionHandler openrtbAuctionHandler(
             AuctionRequestFactory auctionRequestFactory,
             AnalyticsReporterDelegator analyticsReporter,
             Metrics metrics,
+            HooksMetricsService hooksMetricsService,
             Clock clock,
             HttpInteractionLogger httpInteractionLogger,
             PrebidVersionProvider prebidVersionProvider,
+            HookStageExecutor hookStageExecutor,
             JacksonMapper mapper) {
 
         return new org.prebid.server.handler.openrtb2.AuctionHandler(
@@ -222,9 +226,11 @@ org.prebid.server.handler.openrtb2.AuctionHandler openrtbAuctionHandler(
                 skippedAuctionService,
                 analyticsReporter,
                 metrics,
+                hooksMetricsService,
                 clock,
                 httpInteractionLogger,
                 prebidVersionProvider,
+                hookStageExecutor,
                 mapper);
     }
 
@@ -234,12 +240,14 @@ AmpHandler openrtbAmpHandler(
             ExchangeService exchangeService,
             AnalyticsReporterDelegator analyticsReporter,
             Metrics metrics,
+            HooksMetricsService hooksMetricsService,
             Clock clock,
             BidderCatalog bidderCatalog,
             AmpProperties ampProperties,
             AmpResponsePostProcessor ampResponsePostProcessor,
             HttpInteractionLogger httpInteractionLogger,
             PrebidVersionProvider prebidVersionProvider,
+            HookStageExecutor hookStageExecutor,
             JacksonMapper mapper) {
 
         return new AmpHandler(
@@ -247,12 +255,14 @@ AmpHandler openrtbAmpHandler(
                 exchangeService,
                 analyticsReporter,
                 metrics,
+                hooksMetricsService,
                 clock,
                 bidderCatalog,
                 ampProperties.getCustomTargetingSet(),
                 ampResponsePostProcessor,
                 httpInteractionLogger,
                 prebidVersionProvider,
+                hookStageExecutor,
                 mapper,
                 logSamplingRate);
     }
@@ -265,8 +275,10 @@ VideoHandler openrtbVideoHandler(
             CoreCacheService coreCacheService,
             AnalyticsReporterDelegator analyticsReporter,
             Metrics metrics,
+            HooksMetricsService hooksMetricsService,
             Clock clock,
             PrebidVersionProvider prebidVersionProvider,
+            HookStageExecutor hookStageExecutor,
             JacksonMapper mapper) {
 
         return new VideoHandler(
@@ -275,8 +287,10 @@ VideoHandler openrtbVideoHandler(
                 exchangeService,
                 coreCacheService, analyticsReporter,
                 metrics,
+                hooksMetricsService,
                 clock,
                 prebidVersionProvider,
+                hookStageExecutor,
                 mapper);
     }
 
diff --git a/src/main/java/org/prebid/server/util/StreamUtil.java b/src/main/java/org/prebid/server/util/StreamUtil.java
index 0a48bede9e7..998e6f669b6 100644
--- a/src/main/java/org/prebid/server/util/StreamUtil.java
+++ b/src/main/java/org/prebid/server/util/StreamUtil.java
@@ -1,7 +1,11 @@
 package org.prebid.server.util;
 
+import java.util.HashSet;
 import java.util.Iterator;
+import java.util.Set;
 import java.util.Spliterator;
+import java.util.function.Function;
+import java.util.function.Predicate;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
@@ -17,4 +21,9 @@ public static <T> Stream<T> asStream(Spliterator<T> spliterator) {
     public static <T> Stream<T> asStream(Iterator<T> iterator) {
         return StreamSupport.stream(IterableUtil.iterable(iterator).spliterator(), false);
     }
+
+    public static <T> Predicate<T> distinctBy(Function<? super T, ?> keyExtractor) {
+        final Set<Object> seen = new HashSet<>();
+        return value -> seen.add(keyExtractor.apply(value));
+    }
 }
diff --git a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java
index 10b939ae5dd..4e2f062218b 100644
--- a/src/main/java/org/prebid/server/validation/ResponseBidValidator.java
+++ b/src/main/java/org/prebid/server/validation/ResponseBidValidator.java
@@ -85,7 +85,7 @@ public ValidationResult validate(BidderBid bidderBid,
             final Imp correspondingImp = findCorrespondingImp(bid, bidRequest);
             if (bidderBid.getType() == BidType.banner) {
                 warnings.addAll(validateBannerFields(
-                        bid,
+                        bidderBid,
                         bidder,
                         bidRequest,
                         account,
@@ -95,7 +95,7 @@ public ValidationResult validate(BidderBid bidderBid,
             }
 
             warnings.addAll(validateSecureMarkup(
-                    bid,
+                    bidderBid,
                     bidder,
                     bidRequest,
                     account,
@@ -161,7 +161,7 @@ private ValidationException exceptionAndLogOnePercent(String message) {
         return new ValidationException(message);
     }
 
-    private List<String> validateBannerFields(Bid bid,
+    private List<String> validateBannerFields(BidderBid bidderBid,
                                               String bidder,
                                               BidRequest bidRequest,
                                               Account account,
@@ -172,7 +172,7 @@ private List<String> validateBannerFields(Bid bid,
         final BidValidationEnforcement bannerMaxSizeEnforcement = effectiveBannerMaxSizeEnforcement(account);
         if (bannerMaxSizeEnforcement != BidValidationEnforcement.skip) {
             final Format maxSize = maxSizeForBanner(correspondingImp);
-
+            final Bid bid = bidderBid.getBid();
             if (bannerSizeIsNotValid(bid, maxSize)) {
                 final String accountId = account.getId();
                 final String message = """
@@ -189,15 +189,15 @@ private List<String> validateBannerFields(Bid bid,
                         bid.getW(),
                         bid.getH());
 
-                bidRejectionTracker.reject(
-                        correspondingImp.getId(),
-                        BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED);
-
                 return singleWarningOrValidationException(
                         bannerMaxSizeEnforcement,
                         metricName -> metrics.updateSizeValidationMetrics(
                                 aliases.resolveBidder(bidder), accountId, metricName),
-                        CREATIVE_SIZE_LOGGER, message);
+                        CREATIVE_SIZE_LOGGER,
+                        message,
+                        bidRejectionTracker,
+                        bidderBid,
+                        BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED);
             }
         }
         return Collections.emptyList();
@@ -236,7 +236,7 @@ private static boolean bannerSizeIsNotValid(Bid bid, Format maxSize) {
                 || bidH == null || bidH > maxSize.getH();
     }
 
-    private List<String> validateSecureMarkup(Bid bid,
+    private List<String> validateSecureMarkup(BidderBid bidderBid,
                                               String bidder,
                                               BidRequest bidRequest,
                                               Account account,
@@ -250,6 +250,7 @@ private List<String> validateSecureMarkup(Bid bid,
 
         final String accountId = account.getId();
         final String referer = getReferer(bidRequest);
+        final Bid bid = bidderBid.getBid();
         final String adm = bid.getAdm();
 
         if (isImpSecure(correspondingImp) && markupIsNotSecure(adm)) {
@@ -258,15 +259,15 @@ private List<String> validateSecureMarkup(Bid bid,
                     creative validation for bid %s, account=%s, referrer=%s, adm=%s"""
                     .formatted(secureMarkupEnforcement, bidder, bid.getId(), accountId, referer, adm);
 
-            bidRejectionTracker.reject(
-                    correspondingImp.getId(),
-                    BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE);
-
             return singleWarningOrValidationException(
                     secureMarkupEnforcement,
                     metricName -> metrics.updateSecureValidationMetrics(
                             aliases.resolveBidder(bidder), accountId, metricName),
-                    SECURE_CREATIVE_LOGGER, message);
+                    SECURE_CREATIVE_LOGGER,
+                    message,
+                    bidRejectionTracker,
+                    bidderBid,
+                    BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE);
         }
 
         return Collections.emptyList();
@@ -281,12 +282,18 @@ private static boolean markupIsNotSecure(String adm) {
                 || !StringUtils.containsAny(adm, SECURE_MARKUP_MARKERS);
     }
 
-    private List<String> singleWarningOrValidationException(BidValidationEnforcement enforcement,
-                                                            Consumer<MetricName> metricsRecorder,
-                                                            ConditionalLogger conditionalLogger,
-                                                            String message) throws ValidationException {
+    private List<String> singleWarningOrValidationException(
+            BidValidationEnforcement enforcement,
+            Consumer<MetricName> metricsRecorder,
+            ConditionalLogger conditionalLogger,
+            String message,
+            BidRejectionTracker bidRejectionTracker,
+            BidderBid bidderBid,
+            BidRejectionReason bidRejectionReason) throws ValidationException {
+
         return switch (enforcement) {
             case enforce -> {
+                bidRejectionTracker.rejectBid(bidderBid, bidRejectionReason);
                 metricsRecorder.accept(MetricName.err);
                 conditionalLogger.warn(message, logSamplingRate);
                 throw new ValidationException(message);
diff --git a/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java b/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java
index e5aa90aabb2..7158bd4ba07 100644
--- a/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java
+++ b/src/main/java/org/prebid/server/vertx/database/BasicDatabaseClient.java
@@ -6,7 +6,7 @@
 import io.vertx.sqlclient.RowSet;
 import io.vertx.sqlclient.SqlConnection;
 import io.vertx.sqlclient.Tuple;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.log.Logger;
 import org.prebid.server.log.LoggerFactory;
 import org.prebid.server.metric.Metrics;
diff --git a/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java b/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java
index bb4fa7d09c1..ea59c9f9670 100644
--- a/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java
+++ b/src/main/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClient.java
@@ -4,7 +4,7 @@
 import io.vertx.core.Vertx;
 import io.vertx.sqlclient.Row;
 import io.vertx.sqlclient.RowSet;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.log.ConditionalLogger;
 import org.prebid.server.log.Logger;
 import org.prebid.server.log.LoggerFactory;
diff --git a/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java b/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java
index 87c9ada84c6..78a6a34ac7e 100644
--- a/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java
+++ b/src/main/java/org/prebid/server/vertx/database/DatabaseClient.java
@@ -3,7 +3,7 @@
 import io.vertx.core.Future;
 import io.vertx.sqlclient.Row;
 import io.vertx.sqlclient.RowSet;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 
 import java.util.List;
 import java.util.function.Function;
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index eb73cc16478..31efd30851b 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -174,6 +174,7 @@ settings:
             "enabled": false,
             "timeout-ms": 5000,
             "max-rules": 0,
+            "max-schema-dims": 5,
             "max-file-size-kb": 200,
             "max-age-sec": 86400,
             "period-sec": 3600
@@ -181,7 +182,9 @@ settings:
           "enforce-floors-rate": 100,
           "adjust-for-bid-adjustment": true,
           "enforce-deal-floors": true,
-          "use-dynamic-data": true
+          "use-dynamic-data": true,
+          "max-rules": 100,
+          "max-schema-dims": 3
         }
       }
     }
diff --git a/src/main/resources/bidder-config/adkernel.yaml b/src/main/resources/bidder-config/adkernel.yaml
index 8fe685f93e2..de3e735d982 100644
--- a/src/main/resources/bidder-config/adkernel.yaml
+++ b/src/main/resources/bidder-config/adkernel.yaml
@@ -2,6 +2,8 @@ adapters:
   adkernel:
     endpoint: http://pbs.adksrv.com/hb?zone=%s
     endpoint-compression: gzip
+    aliases:
+      rxnetwork: ~
     meta-info:
       maintainer-email: prebid-dev@adkernel.com
       app-media-types:
diff --git a/src/main/resources/bidder-config/algorix.yaml b/src/main/resources/bidder-config/algorix.yaml
index f76db6420ce..14d31df58d6 100644
--- a/src/main/resources/bidder-config/algorix.yaml
+++ b/src/main/resources/bidder-config/algorix.yaml
@@ -9,4 +9,4 @@ adapters:
         - native
       site-media-types:
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 1176
diff --git a/src/main/resources/bidder-config/driftpixel.yaml b/src/main/resources/bidder-config/driftpixel.yaml
index 384d75db00c..5bba4d9257f 100644
--- a/src/main/resources/bidder-config/driftpixel.yaml
+++ b/src/main/resources/bidder-config/driftpixel.yaml
@@ -13,3 +13,9 @@ adapters:
         - native
       supported-vendors:
       vendor-id: 0
+    usersync:
+      cookie-family-name: driftpixel
+      redirect:
+        url: "https://sync.driftpixel.live/psync?t=s&e=0&gdpr={{gdpr}}&consent={{gdpr_consent}}&us_privacy={{us_privacy}}&cb={{redirect_url}}"
+        support-cors: false
+        uid-macro: "%USER_ID%"
diff --git a/src/main/resources/bidder-config/epsilon.yaml b/src/main/resources/bidder-config/epsilon.yaml
index 27d1ed14343..ba7472331d7 100644
--- a/src/main/resources/bidder-config/epsilon.yaml
+++ b/src/main/resources/bidder-config/epsilon.yaml
@@ -11,9 +11,11 @@ adapters:
       app-media-types:
         - banner
         - video
+        - audio
       site-media-types:
         - banner
         - video
+        - audio
       supported-vendors:
       vendor-id: 24
     usersync:
diff --git a/src/main/resources/bidder-config/gumgum.yaml b/src/main/resources/bidder-config/gumgum.yaml
index e6bd1d4e98e..b3ee922dce4 100644
--- a/src/main/resources/bidder-config/gumgum.yaml
+++ b/src/main/resources/bidder-config/gumgum.yaml
@@ -1,6 +1,7 @@
 adapters:
   gumgum:
     endpoint: https://g2.gumgum.com/providers/prbds2s/bid
+    ortb-version: "2.6"
     meta-info:
       maintainer-email: prebid@gumgum.com
       app-media-types:
diff --git a/src/main/resources/bidder-config/insticator.yaml b/src/main/resources/bidder-config/insticator.yaml
new file mode 100644
index 00000000000..7fcf60b877c
--- /dev/null
+++ b/src/main/resources/bidder-config/insticator.yaml
@@ -0,0 +1,19 @@
+adapters:
+  insticator:
+    endpoint: https://ex.ingage.tech/v1/prebidserver
+    meta-info:
+      maintainer-email: prebid@insticator.com
+      app-media-types:
+        - banner
+        - video
+      site-media-types:
+        - banner
+        - video
+      supported-vendors:
+      vendor-id: 910
+    usersync:
+      cookie-family-name: insticator
+      iframe:
+        url: https://usync.ingage.tech?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect={{redirect_url}}
+        support-cors: false
+        uid-macro: '$UID'
diff --git a/src/main/resources/bidder-config/iqzone.yaml b/src/main/resources/bidder-config/iqzone.yaml
index ccd2f126f3f..a8292c5033a 100644
--- a/src/main/resources/bidder-config/iqzone.yaml
+++ b/src/main/resources/bidder-config/iqzone.yaml
@@ -13,3 +13,13 @@ adapters:
         - native
       supported-vendors:
       vendor-id: 0
+    usersync:
+      cookie-family-name: iqzone
+      redirect:
+        url: https://cs.iqzone.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}}
+        support-cors: false
+        uid-macro: '[UID]'
+      iframe:
+        url: https://cs.iqzone.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}}
+        support-cors: false
+        uid-macro: '[UID]'
diff --git a/src/main/resources/bidder-config/krushmedia.yaml b/src/main/resources/bidder-config/krushmedia.yaml
index 46b2d2aa47e..0f2a42e7785 100644
--- a/src/main/resources/bidder-config/krushmedia.yaml
+++ b/src/main/resources/bidder-config/krushmedia.yaml
@@ -16,6 +16,10 @@ adapters:
     usersync:
       cookie-family-name: krushmedia
       redirect:
-        url: https://cs.krushmedia.com/4e4abdd5ecc661643458a730b1aa927d.gif?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redir={{redirect_url}}
+        url: https://cs.krushmedia.com/pbserver?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redir={{redirect_url}}
         support-cors: false
-        uid-macro: '[uid]'
+        uid-macro: '[UID]'
+      iframe:
+        url: https://cs.krushmedia.com/pbserverIframe?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&ccpa={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&pbserverUrl={{redirect_url}}
+        support-cors: false
+        uid-macro: '[UID]'
diff --git a/src/main/resources/bidder-config/medianet.yaml b/src/main/resources/bidder-config/medianet.yaml
index 63aecb5f2b0..cf4119e4147 100644
--- a/src/main/resources/bidder-config/medianet.yaml
+++ b/src/main/resources/bidder-config/medianet.yaml
@@ -17,6 +17,10 @@ adapters:
       vendor-id: 142
     usersync:
       cookie-family-name: medianet
+      iframe:
+        url: https://hbx.media.net/checksync.php?cid=8CUEHS6F9&cs=87&type=mpbc&cv=37&vsSync=1&uspstring={{us_privacy}}&gdpr={{gdpr}}&gdprstring={{gdpr_consent}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}}
+        support-cors: false
+        uid-macro: '<vsid>'
       redirect:
         url: https://hbx.media.net/cksync.php?cs=1&type=pbs&ovsid=setstatuscode&bidder=medianet&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&gpp={{gpp}}&gpp_sid={{gpp_sid}}&redirect={{redirect_url}}
         support-cors: false
diff --git a/src/main/resources/bidder-config/openx.yaml b/src/main/resources/bidder-config/openx.yaml
index 9e8454131d4..a504fcdf8fd 100644
--- a/src/main/resources/bidder-config/openx.yaml
+++ b/src/main/resources/bidder-config/openx.yaml
@@ -8,9 +8,11 @@ adapters:
       app-media-types:
         - banner
         - video
+        - native
       site-media-types:
         - banner
         - video
+        - native
       supported-vendors:
       vendor-id: 69
     usersync:
diff --git a/src/main/resources/bidder-config/pgamssp.yaml b/src/main/resources/bidder-config/pgamssp.yaml
index e2088e63e0e..70105cab774 100644
--- a/src/main/resources/bidder-config/pgamssp.yaml
+++ b/src/main/resources/bidder-config/pgamssp.yaml
@@ -12,7 +12,7 @@ adapters:
         - video
         - native
       supported-vendors:
-      vendor-id: 0
+      vendor-id: 1353
     usersync:
       cookie-family-name: pgamssp
       redirect:
diff --git a/src/main/resources/bidder-config/pubmatic.yaml b/src/main/resources/bidder-config/pubmatic.yaml
index 8f59f1912f1..5ca8ad94339 100644
--- a/src/main/resources/bidder-config/pubmatic.yaml
+++ b/src/main/resources/bidder-config/pubmatic.yaml
@@ -1,6 +1,7 @@
 adapters:
   pubmatic:
     endpoint: https://hbopenbid.pubmatic.com/translator?source=prebid-server
+    ortb-version: "2.6"
     meta-info:
       maintainer-email: header-bidding@pubmatic.com
       app-media-types:
diff --git a/src/main/resources/bidder-config/richaudience.yaml b/src/main/resources/bidder-config/richaudience.yaml
index ebe84aec464..b691d330734 100644
--- a/src/main/resources/bidder-config/richaudience.yaml
+++ b/src/main/resources/bidder-config/richaudience.yaml
@@ -17,3 +17,7 @@ adapters:
         url: https://sync.richaudience.com/74889303289e27f327ad0c6de7be7264/?consentString={{gdpr_consent}}&r={{redirect_url}}
         support-cors: false
         uid-macro: '[PDID]'
+      redirect:
+        url: https://sync.richaudience.com/f7872c90c5d3791e2b51f7edce1a0a5d/?p=pbs&consentString={{gdpr_consent}}&r={{redirect_url}}
+        support-cors: false
+        uid-macro: '[PDID]'
diff --git a/src/main/resources/bidder-config/rise.yaml b/src/main/resources/bidder-config/rise.yaml
index 12fc11eb92e..f8244da16f8 100644
--- a/src/main/resources/bidder-config/rise.yaml
+++ b/src/main/resources/bidder-config/rise.yaml
@@ -2,14 +2,17 @@ adapters:
   rise:
     endpoint: https://pbs.yellowblue.io/pbs
     modifying-vast-xml-allowed: true
+    endpoint-compression: gzip
     meta-info:
       maintainer-email: rise-prog-dev@risecodes.com
       app-media-types:
         - banner
         - video
+        - native
       site-media-types:
         - banner
         - video
+        - native
       supported-vendors:
       vendor-id: 1043
     usersync:
diff --git a/src/main/resources/bidder-config/sharethrough.yaml b/src/main/resources/bidder-config/sharethrough.yaml
index 36cc0f0127f..77bcce9d31f 100644
--- a/src/main/resources/bidder-config/sharethrough.yaml
+++ b/src/main/resources/bidder-config/sharethrough.yaml
@@ -1,6 +1,7 @@
 adapters:
   sharethrough:
     endpoint: https://btlr.sharethrough.com/universal/v1?supply_id=FGMrCMMc
+    ortb-version: '2.6'
     meta-info:
       maintainer-email: pubgrowth.engineering@sharethrough.com
       app-media-types:
diff --git a/src/main/resources/bidder-config/triplelift.yaml b/src/main/resources/bidder-config/triplelift.yaml
index e8c35e3eb2c..446825e33dc 100644
--- a/src/main/resources/bidder-config/triplelift.yaml
+++ b/src/main/resources/bidder-config/triplelift.yaml
@@ -1,6 +1,7 @@
 adapters:
   triplelift:
     endpoint: https://tlx.3lift.com/s2s/auction?sra=1&supplier_id=20
+    ortb-version: "2.6"
     endpoint-compression: gzip
     meta-info:
       maintainer-email: prebid@triplelift.com
diff --git a/src/main/resources/bidder-config/tripleliftnative.yaml b/src/main/resources/bidder-config/tripleliftnative.yaml
index b090925ff82..e6c1f106f62 100644
--- a/src/main/resources/bidder-config/tripleliftnative.yaml
+++ b/src/main/resources/bidder-config/tripleliftnative.yaml
@@ -1,6 +1,7 @@
 adapters:
   triplelift_native:
     endpoint: https://tlx.3lift.com/s2sn/auction?supplier_id=20
+    ortb-version: "2.6"
     meta-info:
       maintainer-email: prebid@triplelift.com
       app-media-types:
diff --git a/src/main/resources/bidder-config/unruly.yaml b/src/main/resources/bidder-config/unruly.yaml
index 050c61c62d9..0b239b0a3f7 100644
--- a/src/main/resources/bidder-config/unruly.yaml
+++ b/src/main/resources/bidder-config/unruly.yaml
@@ -1,6 +1,7 @@
 adapters:
   unruly:
     endpoint: https://targeting.unrulymedia.com/unruly_prebid_server
+    ortb-version: "2.6"
     meta-info:
       maintainer-email: prebidsupport@unrulygroup.com
       app-media-types:
diff --git a/src/main/resources/static/bidder-params/improvedigital.json b/src/main/resources/static/bidder-params/improvedigital.json
index 5681d896e92..ecd60a98b1d 100644
--- a/src/main/resources/static/bidder-params/improvedigital.json
+++ b/src/main/resources/static/bidder-params/improvedigital.json
@@ -35,5 +35,7 @@
       "description": "Placement size"
     }
   },
-  "required": ["placementId"]
+  "required": [
+    "placementId"
+  ]
 }
diff --git a/src/main/resources/static/bidder-params/insticator.json b/src/main/resources/static/bidder-params/insticator.json
new file mode 100644
index 00000000000..645ca8e0ebe
--- /dev/null
+++ b/src/main/resources/static/bidder-params/insticator.json
@@ -0,0 +1,22 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Insticator Adapter Params",
+  "description": "A schema which validates params accepted by Insticator",
+  "type": "object",
+  "properties": {
+    "adUnitId": {
+      "type": "string",
+      "description": "Ad Unit Id",
+      "minLength": 1
+    },
+    "publisherId": {
+      "type": "string",
+      "description": "Publisher Id",
+      "minLength": 1
+    }
+  },
+  "required": [
+    "adUnitId",
+    "publisherId"
+  ]
+}
diff --git a/src/main/resources/static/bidder-params/loopme.json b/src/main/resources/static/bidder-params/loopme.json
index 89d95d8c011..5ea22ec7ba5 100644
--- a/src/main/resources/static/bidder-params/loopme.json
+++ b/src/main/resources/static/bidder-params/loopme.json
@@ -20,5 +20,5 @@
       "minLength": 1
     }
   },
-  "required": ["publisherId", "bundleId", "placementId"]
+  "required": ["publisherId"]
 }
diff --git a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy
index 5efcdf40709..2bc06ab7144 100644
--- a/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/ModuleName.groovy
@@ -6,7 +6,8 @@ enum ModuleName {
 
     PB_RICHMEDIA_FILTER("pb-richmedia-filter"),
     PB_RESPONSE_CORRECTION ("pb-response-correction"),
-    ORTB2_BLOCKING("ortb2-blocking")
+    ORTB2_BLOCKING("ortb2-blocking"),
+    PB_REQUEST_CORRECTION('pb-request-correction'),
 
     @JsonValue
     final String code
diff --git a/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy b/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy
index f91f209395c..352080844ab 100644
--- a/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/bidder/BidderName.groovy
@@ -13,6 +13,7 @@ enum BidderName {
     ALIAS_CAMEL_CASE("AlIaS"),
     GENERIC_CAMEL_CASE("GeNerIc"),
     GENERIC("generic"),
+    GENER_X("gener_x"),
     RUBICON("rubicon"),
     APPNEXUS("appnexus"),
     RUBICON_ALIAS("rubiconAlias"),
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AbTest.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AbTest.groovy
new file mode 100644
index 00000000000..baa19a80db4
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/model/config/AbTest.groovy
@@ -0,0 +1,31 @@
+package org.prebid.server.functional.model.config
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.PropertyNamingStrategies
+import com.fasterxml.jackson.databind.annotation.JsonNaming
+import groovy.transform.ToString
+
+@ToString(includeNames = true, ignoreNulls = true)
+@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy)
+class AbTest {
+
+    Boolean enabled
+    String moduleCode
+    @JsonProperty("module_code")
+    String moduleCodeSnakeCase
+    Set<Integer> accounts
+    Integer percentActive
+    @JsonProperty("percent_active")
+    Integer percentActiveSnakeCase
+    Boolean logAnalyticsTag
+    @JsonProperty("log_analytics_tag")
+    Boolean logAnalyticsTagSnakeCase
+
+    static AbTest getDefault(String moduleCode, List<Integer> accounts = null) {
+        new AbTest(enabled: true,
+                moduleCode: moduleCode,
+                accounts: accounts,
+                percentActive: 0,
+                logAnalyticsTag: true)
+    }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy
index 63d27073805..4e423c03312 100644
--- a/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountAuctionConfig.groovy
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies
 import com.fasterxml.jackson.databind.annotation.JsonNaming
 import groovy.transform.ToString
 import org.prebid.server.functional.model.bidder.BidderName
+import org.prebid.server.functional.model.request.auction.BidAdjustment
 import org.prebid.server.functional.model.request.auction.Targeting
 import org.prebid.server.functional.model.response.auction.MediaType
 
@@ -12,7 +13,7 @@ import org.prebid.server.functional.model.response.auction.MediaType
 @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy)
 class AccountAuctionConfig {
 
-    String priceGranularity
+    PriceGranularityType priceGranularity
     Integer bannerCacheTtl
     Integer videoCacheTtl
     Integer truncateTargetAttr
@@ -26,9 +27,11 @@ class AccountAuctionConfig {
     Map<BidderName, MediaType> preferredMediaType
     @JsonProperty("privacysandbox")
     PrivacySandbox privacySandbox
+    @JsonProperty("bidadjustments")
+    BidAdjustment bidAdjustments
 
     @JsonProperty("price_granularity")
-    String priceGranularitySnakeCase
+    PriceGranularityType priceGranularitySnakeCase
     @JsonProperty("banner_cache_ttl")
     Integer bannerCacheTtlSnakeCase
     @JsonProperty("video_cache_ttl")
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy
index 24f3ab97d77..bab4ec983a3 100644
--- a/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy
@@ -13,4 +13,5 @@ class AccountHooksConfiguration {
     @JsonProperty("execution_plan")
     ExecutionPlan executionPlanSnakeCase
     PbsModulesConfig modules
+    AdminConfig admin
 }
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountPriceFloorsConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountPriceFloorsConfig.groovy
index 28f908aba44..c83c69280fa 100644
--- a/src/test/groovy/org/prebid/server/functional/model/config/AccountPriceFloorsConfig.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountPriceFloorsConfig.groovy
@@ -15,6 +15,8 @@ class AccountPriceFloorsConfig {
     Boolean adjustForBidAdjustment
     Boolean enforceDealFloors
     Boolean useDynamicData
+    Long maxRules
+    Long maxSchemaDims
 
     @JsonProperty("enforce_floors_rate")
     Integer enforceFloorsRateSnakeCase
@@ -24,4 +26,8 @@ class AccountPriceFloorsConfig {
     Boolean enforceDealFloorsSnakeCase
     @JsonProperty("use_dynamic_data")
     Boolean useDynamicDataSnakeCase
+    @JsonProperty("max_rules")
+    Long maxRulesSnakeCase
+    @JsonProperty("max_schema_dims")
+    Long maxSchemaDimsSnakeCase
 }
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AdminConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AdminConfig.groovy
new file mode 100644
index 00000000000..755a47bcbaa
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/model/config/AdminConfig.groovy
@@ -0,0 +1,13 @@
+package org.prebid.server.functional.model.config
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies
+import com.fasterxml.jackson.databind.annotation.JsonNaming
+import groovy.transform.ToString
+import org.prebid.server.functional.model.ModuleName
+
+@ToString(includeNames = true, ignoreNulls = true)
+@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy)
+class AdminConfig {
+
+    Map<ModuleName, Boolean> moduleExecution
+}
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy b/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy
index b73f4fcaeb3..9ded40849d0 100644
--- a/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/config/EndpointExecutionPlan.groovy
@@ -12,4 +12,16 @@ class EndpointExecutionPlan {
         new EndpointExecutionPlan(stages:  stages.collectEntries {
             it -> [(it): StageExecutionPlan.getModuleStageExecutionPlan(name, it)] } as Map<Stage, StageExecutionPlan>)
     }
+
+    static EndpointExecutionPlan getModulesEndpointExecutionPlan(Map<Stage, List<ModuleName>> modulesStages) {
+        new EndpointExecutionPlan(
+                stages: modulesStages.collectEntries { stage, moduleNames ->
+                    [(stage): new StageExecutionPlan(
+                            groups: moduleNames.collect { moduleName ->
+                                ExecutionGroup.getModuleExecutionGroup(moduleName, stage)
+                            }
+                    )]
+                } as Map<Stage, StageExecutionPlan>
+        )
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy
index 766139bc5c5..653f8c8cbea 100644
--- a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy
@@ -1,14 +1,22 @@
 package org.prebid.server.functional.model.config
 
+import com.fasterxml.jackson.databind.PropertyNamingStrategies
+import com.fasterxml.jackson.databind.annotation.JsonNaming
 import groovy.transform.ToString
 import org.prebid.server.functional.model.ModuleName
 
 @ToString(includeNames = true, ignoreNulls = true)
+@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy)
 class ExecutionPlan {
 
+    List<AbTest> abTests
     Map<Endpoint, EndpointExecutionPlan> endpoints
 
     static ExecutionPlan getSingleEndpointExecutionPlan(Endpoint endpoint, ModuleName moduleName, List<Stage> stage) {
         new ExecutionPlan(endpoints: [(endpoint): EndpointExecutionPlan.getModuleEndpointExecutionPlan(moduleName, stage)])
     }
+
+    static ExecutionPlan getSingleEndpointExecutionPlan(Endpoint endpoint, Map<Stage, List<ModuleName>> modulesStages) {
+        new ExecutionPlan(endpoints: [(endpoint): EndpointExecutionPlan.getModulesEndpointExecutionPlan(modulesStages)])
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy
index b5c57122a3f..247bdea4353 100644
--- a/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/config/ModuleHookImplementation.groovy
@@ -9,7 +9,8 @@ enum ModuleHookImplementation {
     PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES("pb-richmedia-filter-all-processed-bid-responses-hook"),
     RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES("pb-response-correction-all-processed-bid-responses"),
     ORTB2_BLOCKING_BIDDER_REQUEST("ortb2-blocking-bidder-request"),
-    ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response")
+    ORTB2_BLOCKING_RAW_BIDDER_RESPONSE("ortb2-blocking-raw-bidder-response"),
+    PB_REQUEST_CORRECTION_PROCESSED_AUCTION_REQUEST("pb-request-correction-processed-auction-request"),
 
     @JsonValue
     final String code
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbRequestCorrectionConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbRequestCorrectionConfig.groovy
new file mode 100644
index 00000000000..5d7a980115b
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/model/config/PbRequestCorrectionConfig.groovy
@@ -0,0 +1,29 @@
+package org.prebid.server.functional.model.config
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import groovy.transform.ToString
+
+@ToString(includeNames = true, ignoreNulls = true)
+class PbRequestCorrectionConfig {
+
+    @JsonProperty("pbsdkAndroidInstlRemove")
+    Boolean interstitialCorrectionEnabled
+    @JsonProperty("pbsdkUaCleanup")
+    Boolean userAgentCorrectionEnabled
+    @JsonProperty("pbsdk-android-instl-remove")
+    Boolean interstitialCorrectionEnabledKebabCase
+    @JsonProperty("pbsdk-ua-cleanup")
+    Boolean userAgentCorrectionEnabledKebabCase
+
+    Boolean enabled
+
+    static PbRequestCorrectionConfig getDefaultConfigWithInterstitial(Boolean interstitialCorrectionEnabled = true,
+                                                                      Boolean enabled = true) {
+        new PbRequestCorrectionConfig(enabled: enabled, interstitialCorrectionEnabled: interstitialCorrectionEnabled)
+    }
+
+    static PbRequestCorrectionConfig getDefaultConfigWithUserAgentCorrection(Boolean userAgentCorrectionEnabled = true,
+                                                                             Boolean enabled = true) {
+        new PbRequestCorrectionConfig(enabled: enabled, userAgentCorrectionEnabled: userAgentCorrectionEnabled)
+    }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy
index f9121ae0b3a..59f640f966c 100644
--- a/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/config/PbsModulesConfig.groovy
@@ -12,4 +12,5 @@ class PbsModulesConfig {
     RichmediaFilter pbRichmediaFilter
     Ortb2BlockingConfig ortb2Blocking
     PbResponseCorrection pbResponseCorrection
+    PbRequestCorrectionConfig pbRequestCorrection
 }
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PriceFloorsFetch.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PriceFloorsFetch.groovy
index 1501f2e1366..89f32a951a4 100644
--- a/src/test/groovy/org/prebid/server/functional/model/config/PriceFloorsFetch.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/config/PriceFloorsFetch.groovy
@@ -26,4 +26,7 @@ class PriceFloorsFetch {
     Integer periodSec
     @JsonProperty("period_sec")
     Integer periodSecSnakeCase
+    Integer maxSchemaDims
+    @JsonProperty("max_schema_dims")
+    Integer maxSchemaDimsSnakeCase
 }
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy b/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy
new file mode 100644
index 00000000000..957a2d880bf
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/model/config/PriceGranularityType.groovy
@@ -0,0 +1,28 @@
+package org.prebid.server.functional.model.config
+
+import com.fasterxml.jackson.annotation.JsonValue
+import org.prebid.server.functional.model.request.auction.Range
+
+enum PriceGranularityType {
+
+    LOW(2, [Range.getDefault(5, 0.5)]),
+    MEDIUM(2, [Range.getDefault(20, 0.1)]),
+    MED(2, [Range.getDefault(20, 0.1)]),
+    HIGH(2, [Range.getDefault(20, 0.01)]),
+    AUTO(2, [Range.getDefault(5, 0.05), Range.getDefault(10, 0.1), Range.getDefault(20, 0.5)]),
+    DENSE(2, [Range.getDefault(3, 0.01), Range.getDefault(8, 0.05), Range.getDefault(20, 0.5)]),
+    UNKNOWN(null, [])
+
+    final Integer precision
+    final List<Range> ranges
+
+    PriceGranularityType(Integer precision, List<Range> ranges) {
+        this.precision = precision
+        this.ranges = ranges
+    }
+
+    @JsonValue
+    String toLowerCase() {
+        return name().toLowerCase()
+    }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy b/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy
index c77fd9ebcda..178f22552ae 100644
--- a/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/config/Stage.groovy
@@ -6,20 +6,22 @@ import groovy.transform.ToString
 @ToString
 enum Stage {
 
-    ENTRYPOINT("entrypoint"),
-    RAW_AUCTION_REQUEST("raw-auction-request"),
-    PROCESSED_AUCTION_REQUEST("processed-auction-request"),
-    BIDDER_REQUEST("bidder-request"),
-    RAW_BIDDER_RESPONSE("raw-bidder-response"),
-    PROCESSED_BIDDER_RESPONSE("processed-bidder-response"),
-    ALL_PROCESSED_BID_RESPONSES("all-processed-bid-responses"),
-    AUCTION_RESPONSE("auction-response")
+    ENTRYPOINT("entrypoint", "entrypoint"),
+    RAW_AUCTION_REQUEST("raw-auction-request", "rawauction"),
+    PROCESSED_AUCTION_REQUEST("processed-auction-request", "procauction"),
+    BIDDER_REQUEST("bidder-request", "bidrequest"),
+    RAW_BIDDER_RESPONSE("raw-bidder-response", "rawbidresponse"),
+    PROCESSED_BIDDER_RESPONSE("processed-bidder-response", "procbidresponse"),
+    ALL_PROCESSED_BID_RESPONSES("all-processed-bid-responses", "allprocbidresponses"),
+    AUCTION_RESPONSE("auction-response", "auctionresponse")
 
     @JsonValue
     final String value
+    final String metricValue
 
-    Stage(String value) {
+    Stage(String value, String metricValue) {
         this.value = value
+        this.metricValue = metricValue
     }
 
     @Override
diff --git a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy
index 431890c371d..a9d913eccd1 100644
--- a/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/pricefloors/PriceFloorData.groovy
@@ -16,6 +16,7 @@ class PriceFloorData implements ResponseModel {
     String floorProvider
     Currency currency
     Integer skipRate
+    Integer useFetchDataRate
     String floorsSchemaVersion
     Integer modelTimestamp
     List<ModelGroup> modelGroups
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentRule.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentRule.groovy
new file mode 100644
index 00000000000..953f66fd988
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentRule.groovy
@@ -0,0 +1,17 @@
+package org.prebid.server.functional.model.request.auction
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.PropertyNamingStrategies
+import com.fasterxml.jackson.databind.annotation.JsonNaming
+import groovy.transform.ToString
+import org.prebid.server.functional.model.Currency
+
+@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy)
+@ToString(includeNames = true, ignoreNulls = true)
+class AdjustmentRule {
+
+    @JsonProperty('adjtype')
+    AdjustmentType adjustmentType
+    BigDecimal value
+    Currency currency
+}
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy
new file mode 100644
index 00000000000..20574d525a1
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AdjustmentType.groovy
@@ -0,0 +1,13 @@
+package org.prebid.server.functional.model.request.auction
+
+import com.fasterxml.jackson.annotation.JsonValue
+
+enum AdjustmentType {
+
+    MULTIPLIER, CPM, STATIC, UNKNOWN
+
+    @JsonValue
+    String getValue() {
+        name().toLowerCase()
+    }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy
index b31926c14b5..ee3c1c9a8f0 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppExt.groovy
@@ -6,4 +6,5 @@ import groovy.transform.ToString
 class AppExt {
 
     AppExtData data
+    AppPrebid prebid
 }
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy
new file mode 100644
index 00000000000..edb365d4d6f
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/AppPrebid.groovy
@@ -0,0 +1,10 @@
+package org.prebid.server.functional.model.request.auction
+
+import groovy.transform.ToString
+
+@ToString(includeNames = true, ignoreNulls = true)
+class AppPrebid {
+
+    String source
+    String version
+}
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy
new file mode 100644
index 00000000000..7f7250a6a75
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustment.groovy
@@ -0,0 +1,20 @@
+package org.prebid.server.functional.model.request.auction
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies
+import com.fasterxml.jackson.databind.annotation.JsonNaming
+import groovy.transform.ToString
+import org.prebid.server.functional.util.PBSUtils
+
+@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy)
+@ToString(includeNames = true, ignoreNulls = true)
+class BidAdjustment {
+
+    Map<BidAdjustmentMediaType, BidAdjustmentRule> mediaType
+    Integer version
+
+    static getDefaultWithSingleMediaTypeRule(BidAdjustmentMediaType type,
+                                     BidAdjustmentRule rule,
+                                     Integer version = PBSUtils.randomNumber) {
+        new BidAdjustment(mediaType: [(type): rule], version: version)
+    }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy
index a005d407241..9cb90edb27b 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentFactors.groovy
@@ -15,7 +15,6 @@ class BidAdjustmentFactors {
     Map<BidderName, BigDecimal> adjustments
     Map<BidAdjustmentMediaType, Map<BidderName, BigDecimal>> mediaTypes
 
-
     @JsonAnyGetter
     Map<BidderName, BigDecimal> getAdjustments() {
         adjustments
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy
index a959f5b800c..26a58655215 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentMediaType.groovy
@@ -8,7 +8,10 @@ enum BidAdjustmentMediaType {
     AUDIO("audio"),
     NATIVE("native"),
     VIDEO("video"),
-    VIDEO_OUTSTREAM("video-outstream")
+    VIDEO_IN_STREAM("video-instream"),
+    VIDEO_OUT_STREAM("video-outstream"),
+    ANY('*'),
+    UNKNOWN('unknown')
 
     @JsonValue
     String value
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy
new file mode 100644
index 00000000000..4fcfc1125e1
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidAdjustmentRule.groovy
@@ -0,0 +1,16 @@
+package org.prebid.server.functional.model.request.auction
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.PropertyNamingStrategies
+import com.fasterxml.jackson.databind.annotation.JsonNaming
+import groovy.transform.ToString
+
+@JsonNaming(PropertyNamingStrategies.LowerCaseStrategy)
+@ToString(includeNames = true, ignoreNulls = true)
+class BidAdjustmentRule {
+
+    @JsonProperty('*')
+    Map<String, List<AdjustmentRule>> wildcardBidder
+    Map<String, List<AdjustmentRule>> generic
+    Map<String, List<AdjustmentRule>> alias
+}
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy
index 1157580209a..aa9da45a4b6 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/BidRequest.groovy
@@ -5,10 +5,12 @@ import groovy.transform.EqualsAndHashCode
 import groovy.transform.ToString
 import org.prebid.server.functional.model.Currency
 
+import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED
 import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP
 import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH
 import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE
 import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO
+import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE
 import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO
 
 @EqualsAndHashCode
@@ -22,7 +24,7 @@ class BidRequest {
     Dooh dooh
     Device device
     User user
-    Integer test
+    DebugCondition test
     Integer at
     Long tmax
     List<String> wseat
@@ -47,6 +49,10 @@ class BidRequest {
         getDefaultRequest(channel, Imp.getDefaultImpression(VIDEO))
     }
 
+    static BidRequest getDefaultNativeRequest(DistributionChannel channel = SITE) {
+        getDefaultRequest(channel, Imp.getDefaultImpression(NATIVE))
+    }
+
     static BidRequest getDefaultAudioRequest(DistributionChannel channel = SITE) {
         getDefaultRequest(channel, Imp.getDefaultImpression(AUDIO))
     }
@@ -63,7 +69,7 @@ class BidRequest {
             regs = Regs.defaultRegs
             id = UUID.randomUUID()
             tmax = 2500
-            ext = new BidRequestExt(prebid: new Prebid(debug: 1))
+            ext = new BidRequestExt(prebid: new Prebid(debug: ENABLED))
             if (channel == SITE) {
                 site = Site.defaultSite
             }
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy
index a1078731f44..605f286c803 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Bidder.groovy
@@ -13,6 +13,8 @@ class Bidder {
 
     Generic alias
     Generic generic
+    @JsonProperty("gener_x")
+    Generic generX
     @JsonProperty("GeNerIc")
     Generic genericCamelCase
     Rubicon rubicon
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ConsentedProvidersSettings.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ConsentedProvidersSettings.groovy
new file mode 100644
index 00000000000..aa7bd511cb2
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ConsentedProvidersSettings.groovy
@@ -0,0 +1,12 @@
+package org.prebid.server.functional.model.request.auction
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategies
+import com.fasterxml.jackson.databind.annotation.JsonNaming
+import groovy.transform.ToString
+
+@ToString(includeNames = true, ignoreNulls = true)
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy)
+class ConsentedProvidersSettings {
+
+    String consentedProviders
+}
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/DebugCondition.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/DebugCondition.groovy
new file mode 100644
index 00000000000..066080c56da
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/DebugCondition.groovy
@@ -0,0 +1,15 @@
+package org.prebid.server.functional.model.request.auction
+
+import com.fasterxml.jackson.annotation.JsonValue
+
+enum DebugCondition {
+
+    DISABLED(0), ENABLED(1)
+
+    @JsonValue
+    final int value
+
+    private DebugCondition(int value) {
+        this.value = value
+    }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidFloors.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidFloors.groovy
index cb5abf3ff87..c0c80038f5c 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidFloors.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/ExtPrebidFloors.groovy
@@ -20,6 +20,7 @@ class ExtPrebidFloors {
     ExtPrebidPriceFloorEnforcement enforcement
     Integer skipRate
     PriceFloorData data
+    Long maxSchemaDims
 
     static ExtPrebidFloors getExtPrebidFloors() {
         new ExtPrebidFloors(floorMin: FLOOR_MIN,
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy
index c669d61f5a0..6eec49cf39d 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy
@@ -6,7 +6,7 @@ import groovy.transform.ToString
 @ToString
 enum FetchStatus {
 
-    NONE, SUCCESS, TIMEOUT, INPROGRESS, ERROR, SUCCESS_ALLOW, SUCCESS_BLOCK
+    NONE, SUCCESS, TIMEOUT, INPROGRESS, ERROR, SUCCESS_ALLOW, SUCCESS_BLOCK, SKIPPED, RUN
 
     @JsonValue
     String getValue() {
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy
index dbea9b32624..13c97a36ba4 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Imp.groovy
@@ -30,7 +30,7 @@ class Imp {
     Pmp pmp
     String displayManager
     String displayManagerVer
-    Integer instl
+    OperationState instl
     String tagId
     BigDecimal bidFloor
     Currency bidFloorCur
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy
index 7240fd91719..ba89f5680fa 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Prebid.groovy
@@ -10,11 +10,12 @@ import org.prebid.server.functional.model.bidder.BidderName
 @ToString(includeNames = true, ignoreNulls = true)
 class Prebid {
 
-    Integer debug
+    DebugCondition debug
     Boolean returnAllBidStatus
     Map<String, BidderName> aliases
     Map<String, Integer> aliasgvlids
     BidAdjustmentFactors bidAdjustmentFactors
+    BidAdjustment bidAdjustments
     PrebidCurrency currency
     Targeting targeting
     TraceLevel trace
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy
index 29f4472cab2..873c686a578 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PriceGranularity.groovy
@@ -1,10 +1,21 @@
 package org.prebid.server.functional.model.request.auction
 
+import groovy.transform.EqualsAndHashCode
 import groovy.transform.ToString
+import org.prebid.server.functional.model.config.PriceGranularityType
 
 @ToString(includeNames = true, ignoreNulls = true)
+@EqualsAndHashCode
 class PriceGranularity {
 
     Integer precision
     List<Range> ranges
+
+    static PriceGranularity getDefault(PriceGranularityType granularity) {
+        new PriceGranularity(precision: granularity.precision, ranges: granularity.ranges)
+    }
+
+    static PriceGranularity getDefault() {
+        getDefault(PriceGranularityType.MED)
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy
index 59fb0b34c25..9bdb94f007d 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/PublicCountryIp.groovy
@@ -4,7 +4,8 @@ enum PublicCountryIp {
 
     USA_IP("209.232.44.21", "d646:2414:17b2:f371:9b62:f176:b4c0:51cd"),
     UKR_IP("193.238.111.14", "3080:f30f:e4bc:0f56:41be:6aab:9d0a:58e2"),
-    CAN_IP("70.71.245.39", "f9b2:c742:1922:7d4b:7122:c7fc:8b75:98c8")
+    CAN_IP("70.71.245.39", "f9b2:c742:1922:7d4b:7122:c7fc:8b75:98c8"),
+    BGR_IP("31.211.128.0", "2002:1fd3:8000:0000:0000:0000:0000:0000")
 
     final String v4
     final String v6
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy
index c5fa8cb2220..1b106b67faa 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/Range.groovy
@@ -1,10 +1,16 @@
 package org.prebid.server.functional.model.request.auction
 
+import groovy.transform.EqualsAndHashCode
 import groovy.transform.ToString
 
 @ToString(includeNames = true, ignoreNulls = true)
+@EqualsAndHashCode
 class Range {
 
     BigDecimal max
     BigDecimal increment
+
+    static Range getDefault(Integer max, BigDecimal increment) {
+        new Range(max: max, increment: increment)
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy
index 9a9c62fde81..d7e9cb2242e 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/RegsExt.groovy
@@ -10,6 +10,7 @@ class RegsExt {
 
     @Deprecated(since = "enabling support of ortb 2.6")
     Integer gdpr
+    Integer coppa
     @Deprecated(since = "enabling support of ortb 2.6")
     String usPrivacy
     String gpc
diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy
index e547d8f37ed..af07e197c28 100644
--- a/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/UserExt.groovy
@@ -1,8 +1,12 @@
 package org.prebid.server.functional.model.request.auction
 
+import com.fasterxml.jackson.annotation.JsonProperty
+import com.fasterxml.jackson.databind.PropertyNamingStrategies
+import com.fasterxml.jackson.databind.annotation.JsonNaming
 import groovy.transform.ToString
 
 @ToString(includeNames = true, ignoreNulls = true)
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy)
 class UserExt {
 
     String consent
@@ -11,6 +15,9 @@ class UserExt {
     UserTime time
     UserExtData data
     UserExtPrebid prebid
+    ConsentedProvidersSettings consentedProvidersSettings
+    @JsonProperty("ConsentedProvidersSettings")
+    ConsentedProvidersSettings consentedProvidersSettingsCamelCase
 
     static UserExt getFPDUserExt() {
         new UserExt(data: UserExtData.FPDUserExtData)
diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy
index 79cf8ad9317..2fb7d75bbf7 100644
--- a/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/BidRejectionReason.groovy
@@ -9,6 +9,7 @@ enum BidRejectionReason {
     ERROR_TIMED_OUT(101),
     ERROR_INVALID_BID_RESPONSE(102),
     ERROR_BIDDER_UNREACHABLE(103),
+    ERROR_REQUEST(104),
 
     REQUEST_BLOCKED_GENERAL(200),
     REQUEST_BLOCKED_UNSUPPORTED_CHANNEL(201),
diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy
index 3f1594380f4..c5c1a828f98 100644
--- a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy
@@ -14,4 +14,5 @@ class InvocationResult {
     ResponseAction action
     HookId hookId
     AnalyticsPrebidTag analyticsTags
+    String message
 }
diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy
index 257b6287fcf..77c7ffd5ef8 100644
--- a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy
@@ -6,7 +6,7 @@ import groovy.transform.ToString
 @ToString
 enum InvocationStatus {
 
-    SUCCESS, FAILURE
+    SUCCESS, FAILURE, INVOCATION_FAILURE
 
     @JsonValue
     String getValue() {
diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy
index 3942b170875..8711bd395c6 100644
--- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy
@@ -5,7 +5,8 @@ import com.fasterxml.jackson.annotation.JsonValue
 enum ModuleActivityName {
 
     ORTB2_BLOCKING('enforce-blocking'),
-    REJECT_RICHMEDIA('reject-richmedia')
+    REJECT_RICHMEDIA('reject-richmedia'),
+    AB_TESTING('core-module-abtests')
 
     @JsonValue
     final String value
diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy
index e76e8fb3f54..9a1e9d1b440 100644
--- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy
@@ -4,11 +4,13 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies
 import com.fasterxml.jackson.databind.annotation.JsonNaming
 import groovy.transform.EqualsAndHashCode
 import groovy.transform.ToString
+import org.prebid.server.functional.model.ModuleName
 
 @ToString(includeNames = true, ignoreNulls = true)
 @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy)
 @EqualsAndHashCode
 class ModuleValue {
 
+   ModuleName module
    String richmediaFormat
 }
diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy
index 1a786670ba8..1bce783d048 100644
--- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ResponseAction.groovy
@@ -4,7 +4,7 @@ import com.fasterxml.jackson.annotation.JsonValue
 
 enum ResponseAction {
 
-    UPDATE, NO_ACTION
+    UPDATE, NO_ACTION, NO_INVOCATION
 
     @JsonValue
     String getValue() {
diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy
index f1a72a9e266..0f155bf55a1 100644
--- a/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy
+++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy
@@ -3,13 +3,12 @@ package org.prebid.server.functional.model.response.auction
 import com.fasterxml.jackson.databind.PropertyNamingStrategies
 import com.fasterxml.jackson.databind.annotation.JsonNaming
 import groovy.transform.ToString
-import org.prebid.server.functional.model.config.Stage
 
 @ToString(includeNames = true, ignoreNulls = true)
 @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy)
 class TraceOutcome {
 
-    Stage entity
+    String entity
     Long executionTimeMillis
     List<TraceGroup> groups
 }
diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy
index 5629826942b..b3f938a7ca0 100644
--- a/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy
+++ b/src/test/groovy/org/prebid/server/functional/testcontainers/container/PrebidServerContainer.groovy
@@ -91,7 +91,6 @@ class PrebidServerContainer extends GenericContainer<PrebidServerContainer> {
 
     private static String normalizeProperty(String property) {
         property.replace(".", "_")
-                .replace("-", "")
                 .replace("[", "_")
                 .replace("]", "_")
     }
diff --git a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy
index 1c47147f596..224f7c8b228 100644
--- a/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy
+++ b/src/test/groovy/org/prebid/server/functional/testcontainers/scaffolding/PrebidCache.groovy
@@ -52,6 +52,10 @@ class PrebidCache extends NetworkScaffolding {
                 .collect { decode(it.body.toString(), BidCacheRequest) }
     }
 
+    Map<String, List<String>> getRequestHeaders(String impId) {
+        getLastRecordedRequestHeaders(getRequest(impId))
+    }
+
     @Override
     HttpRequest getRequest() {
         request().withMethod("POST")
diff --git a/src/test/groovy/org/prebid/server/functional/tests/AliasSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AliasSpec.groovy
index 15cd0668f6c..d4b8f84c826 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/AliasSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/AliasSpec.groovy
@@ -1,16 +1,20 @@
 package org.prebid.server.functional.tests
 
 import org.prebid.server.functional.model.bidder.Generic
+import org.prebid.server.functional.model.bidder.Openx
 import org.prebid.server.functional.model.request.auction.BidRequest
 import org.prebid.server.functional.service.PrebidServerException
+import org.prebid.server.functional.testcontainers.Dependencies
 import org.prebid.server.functional.util.PBSUtils
 
 import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST
 import static org.prebid.server.functional.model.bidder.BidderName.ALIAS
 import static org.prebid.server.functional.model.bidder.BidderName.BOGUS
 import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
+import static org.prebid.server.functional.model.bidder.BidderName.GENER_X
+import static org.prebid.server.functional.model.bidder.BidderName.OPENX
 import static org.prebid.server.functional.model.bidder.CompressionType.GZIP
-import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer
+import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer
 import static org.prebid.server.functional.util.HttpUtil.CONTENT_ENCODING_HEADER
 
 class AliasSpec extends BaseSpec {
@@ -144,4 +148,101 @@ class AliasSpec extends BaseSpec {
         def bidderRequests = bidder.getBidderRequests(bidRequest.id)
         assert bidderRequests.size() == 2
     }
+
+    def "PBS should ignore alias logic when hardcoded alias endpoints are present"() {
+        given: "PBs server with aliases config"
+        def pbsConfig = ["adapters.generic.aliases.alias.enabled" : "true",
+                         "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/alias/auction".toString(),
+                         "adapters.openx.enabled"                 : "true",
+                         "adapters.openx.endpoint"                : "$networkServiceContainer.rootUri/openx/auction".toString()]
+        def pbsService = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with openx and alias bidder"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            imp[0].ext.prebid.bidder.alias = new Generic()
+            imp[0].ext.prebid.bidder.generic = new Generic()
+            imp[0].ext.prebid.bidder.openx = new Openx()
+            ext.prebid.aliases = [(ALIAS.value): OPENX]
+        }
+
+        when: "PBS processes auction request"
+        def bidResponse = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should call only generic bidder"
+        def responseDebug = bidResponse.ext.debug
+        assert responseDebug.httpcalls[GENERIC.value]
+
+        and: "PBS shouldn't call only opexn,alias bidder"
+        assert !responseDebug.httpcalls[OPENX.value]
+        assert !responseDebug.httpcalls[ALIAS.value]
+
+        and: "PBS should call only generic bidder"
+        assert bidder.getBidderRequest(bidRequest.id)
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS should ignore aliases for requests with a base adapter"() {
+        given: "PBs server with aliases config"
+        def pbsConfig = ["adapters.openx.enabled" : "true",
+                         "adapters.openx.endpoint": "$networkServiceContainer.rootUri/openx/auction".toString()]
+        def pbsService = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with openx and alias bidder"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            imp[0].ext.prebid.bidder.openx = Openx.defaultOpenx
+            imp[0].ext.prebid.bidder.generic = new Generic()
+            ext.prebid.aliases = [(OPENX.value): GENERIC]
+        }
+
+        when: "PBS processes auction request"
+        def bidResponse = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS contain two http calls and the different url for both"
+        def responseDebug = bidResponse.ext.debug
+        assert responseDebug.httpcalls.size() == 2
+        assert responseDebug.httpcalls[OPENX.value]*.uri == ["$networkServiceContainer.rootUri/openx/auction"]
+        assert responseDebug.httpcalls[GENERIC.value]*.uri == ["$networkServiceContainer.rootUri/auction"]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS should invoke as aliases when alias is unknown and core bidder is specified"() {
+        given: "Default bid request with generic and alias bidder"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            imp[0].ext.prebid.bidder.generX = new Generic()
+            ext.prebid.aliases = [(GENER_X.value): GENERIC]
+        }
+
+        when: "PBS processes auction request"
+        def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS contain two http calls and the same url for both"
+        def responseDebug = bidResponse.ext.debug
+        assert responseDebug.httpcalls.size() == 2
+        assert responseDebug.httpcalls[GENER_X.value]*.uri == responseDebug.httpcalls[GENERIC.value]*.uri
+
+        and: "Bidder request should contain request per-alies"
+        def bidderRequests = bidder.getBidderRequests(bidRequest.id)
+        assert bidderRequests.size() == 2
+    }
+
+    def "PBS should invoke aliases when alias is unknown and no core bidder is specified"() {
+        given: "Default bid request with generic and alias bidder"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            imp[0].ext.prebid.bidder.generX = new Generic()
+            imp[0].ext.prebid.bidder.generic = null
+            ext.prebid.aliases = [(GENER_X.value): GENERIC]
+        }
+
+        when: "PBS processes auction request"
+        def bidResponse = defaultPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS contain two http calls and the same url for both"
+        def responseDebug = bidResponse.ext.debug
+        assert responseDebug.httpcalls.size() == 1
+        assert responseDebug.httpcalls[GENER_X.value]
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/AmpFpdSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AmpFpdSpec.groovy
index 762c55fe879..e376f611e84 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/AmpFpdSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/AmpFpdSpec.groovy
@@ -13,6 +13,7 @@ import org.prebid.server.functional.model.request.auction.Geo
 import org.prebid.server.functional.model.request.auction.ImpExtContext
 import org.prebid.server.functional.model.request.auction.ImpExtContextData
 import org.prebid.server.functional.model.request.auction.ImpExtContextDataAdServer
+import org.prebid.server.functional.model.request.auction.Publisher
 import org.prebid.server.functional.model.request.auction.Site
 import org.prebid.server.functional.model.request.auction.User
 import org.prebid.server.functional.service.PrebidServerException
@@ -333,11 +334,14 @@ class AmpFpdSpec extends BaseSpec {
         given: "AMP request"
         def ampRequest = new AmpRequest(tagId: PBSUtils.randomString)
 
+        and: "Amp stored request with FPD data"
+        def fpdSite = Site.rootFPDSite
+        def fpdUser = User.rootFPDUser
         def ampStoredRequest = BidRequest.getDefaultBidRequest(SITE).tap {
             ext.prebid.tap {
                 data = new ExtRequestPrebidData(bidders: [extRequestPrebidDataBidder])
                 bidderConfig = [new ExtPrebidBidderConfig(bidders: [prebidBidderConfigBidder], config: new BidderConfig(
-                        ortb2: new BidderConfigOrtb(site: Site.configFPDSite, user: User.configFPDUser)))]
+                        ortb2: new BidderConfigOrtb(site: fpdSite, user: fpdUser)))]
             }
         }
 
@@ -350,25 +354,24 @@ class AmpFpdSpec extends BaseSpec {
 
         then: "Bidder request should contain certain FPD field from the stored request"
         def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id)
-        def ortb2 = ampStoredRequest.ext.prebid.bidderConfig[0].config.ortb2
         verifyAll(bidderRequest) {
-            ortb2.site.name == site.name
-            ortb2.site.domain == site.domain
-            ortb2.site.cat == site.cat
-            ortb2.site.sectionCat == site.sectionCat
-            ortb2.site.pageCat == site.pageCat
-            ortb2.site.page == site.page
-            ortb2.site.ref == site.ref
-            ortb2.site.search == site.search
-            ortb2.site.keywords == site.keywords
-            ortb2.site.ext.data.language == site.ext.data.language
-
-            ortb2.user.yob == user.yob
-            ortb2.user.gender == user.gender
-            ortb2.user.keywords == user.keywords
-            ortb2.user.ext.data.keywords == user.ext.data.keywords
-            ortb2.user.ext.data.buyeruid == user.ext.data.buyeruid
-            ortb2.user.ext.data.buyeruids == user.ext.data.buyeruids
+            it.site.name == fpdSite.name
+            it.site.domain == fpdSite.domain
+            it.site.cat == fpdSite.cat
+            it.site.sectionCat == fpdSite.sectionCat
+            it.site.pageCat == fpdSite.pageCat
+            it.site.page == fpdSite.page
+            it.site.ref == fpdSite.ref
+            it.site.search == fpdSite.search
+            it.site.keywords == fpdSite.keywords
+            it.site.ext.data.language == fpdSite.ext.data.language
+
+            it.user.yob == fpdUser.yob
+            it.user.gender == fpdUser.gender
+            it.user.keywords == fpdUser.keywords
+            it.user.ext.data.keywords == fpdUser.ext.data.keywords
+            it.user.ext.data.buyeruid == fpdUser.ext.data.buyeruid
+            it.user.ext.data.buyeruids == fpdUser.ext.data.buyeruids
         }
 
         and: "Bidder request shouldn't contain imp[0].ext.rp"
@@ -413,17 +416,18 @@ class AmpFpdSpec extends BaseSpec {
         }
     }
 
-    def "PBS should fill unknown FPD when unknown FPD data present"() {
+    def "PBS should ignore any not FPD data value in bidderconfig.config when merging values"() {
         given: "AMP request"
         def ampRequest = new AmpRequest(tagId: PBSUtils.randomString)
 
         and: "Stored request"
-        def fpdGeo = Geo.FPDGeo
+        def fpdSite = Site.rootFPDSite
+        def fpdUser = User.rootFPDUser
         def ampStoredRequest = BidRequest.getDefaultBidRequest(SITE).tap {
-            site = Site.rootFPDSite
-            user = User.rootFPDUser
+            site = fpdSite
+            user = fpdUser
             ext.prebid.bidderConfig = [new ExtPrebidBidderConfig(bidders: [GENERIC], config: new BidderConfig(ortb2:
-                    new BidderConfigOrtb(user: new User(geo: fpdGeo))))]
+                    new BidderConfigOrtb(user: new User(geo: Geo.FPDGeo), site: new Site(publisher: new Publisher(name: PBSUtils.randomString)))))]
         }
 
         and: "Save stored request in DB"
@@ -433,13 +437,32 @@ class AmpFpdSpec extends BaseSpec {
         when: "PBS processes amp request"
         defaultPbsService.sendAmpRequest(ampRequest)
 
-        then: "Bidder request should contain certain FPD field from the stored request"
+        then: "Bidder request should contain FPD field from the stored request"
         def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id)
         verifyAll(bidderRequest) {
-            user.ext.data.geo.country == fpdGeo.country
-            user.ext.data.geo.zip == fpdGeo.zip
-            user.geo.country == ampStoredRequest.user.geo.country
-            user.geo.zip == ampStoredRequest.user.geo.zip
+            it.site.name == fpdSite.name
+            it.site.domain == fpdSite.domain
+            it.site.cat == fpdSite.cat
+            it.site.sectionCat == fpdSite.sectionCat
+            it.site.pageCat == fpdSite.pageCat
+            it.site.page == fpdSite.page
+            it.site.ref == fpdSite.ref
+            it.site.search == fpdSite.search
+            it.site.keywords == fpdSite.keywords
+            it.site.ext.data.language == fpdSite.ext.data.language
+
+            it.user.yob == fpdUser.yob
+            it.user.gender == fpdUser.gender
+            it.user.keywords == fpdUser.keywords
+            it.user.ext.data.keywords == fpdUser.ext.data.keywords
+            it.user.ext.data.buyeruid == fpdUser.ext.data.buyeruid
+            it.user.ext.data.buyeruids == fpdUser.ext.data.buyeruids
+        }
+
+        and: "Should should ignore any non FPD data"
+        verifyAll(bidderRequest) {
+            !it.user.ext.data.geo
+            !it.site.ext.data.publisher
         }
     }
 
diff --git a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy
index ac3ba146010..96a78df89a8 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/AmpSpec.groovy
@@ -4,9 +4,12 @@ import org.prebid.server.functional.model.db.StoredRequest
 import org.prebid.server.functional.model.db.StoredResponse
 import org.prebid.server.functional.model.request.amp.AmpRequest
 import org.prebid.server.functional.model.request.auction.BidRequest
+import org.prebid.server.functional.model.request.auction.ConsentedProvidersSettings
 import org.prebid.server.functional.model.request.auction.DistributionChannel
 import org.prebid.server.functional.model.request.auction.Site
 import org.prebid.server.functional.model.request.auction.StoredAuctionResponse
+import org.prebid.server.functional.model.request.auction.User
+import org.prebid.server.functional.model.request.auction.UserExt
 import org.prebid.server.functional.model.response.auction.SeatBid
 import org.prebid.server.functional.service.PrebidServerException
 import org.prebid.server.functional.util.PBSUtils
@@ -56,7 +59,7 @@ class AmpSpec extends BaseSpec {
         assert exception.responseBody == "Invalid request format: request.${channel.value.toLowerCase()} must not exist in AMP stored requests."
 
         where:
-        channel  << [DistributionChannel.APP, DistributionChannel.DOOH]
+        channel << [DistributionChannel.APP, DistributionChannel.DOOH]
     }
 
     def "PBS should return info from the stored response when it's defined in the stored request"() {
@@ -180,4 +183,79 @@ class AmpSpec extends BaseSpec {
         assert bidderRequest.imp[0]?.banner?.format[0]?.weight == ampStoredRequest.imp[0].banner.format[0].weight
         assert bidderRequest.regs?.gdpr == ampStoredRequest.regs.gdpr
     }
+
+    def "PBS should pass addtl_consent to user.ext.{consented_providers_settings/ConsentedProvidersSettings}.consented_providers"() {
+        given: "Default amp request with addtlConsent"
+        def randomAddtlConsent = PBSUtils.randomString
+        def ampRequest = AmpRequest.defaultAmpRequest.tap {
+            addtlConsent = randomAddtlConsent
+        }
+
+        and: "Save storedRequest into DB"
+        def ampStoredRequest = BidRequest.defaultBidRequest.tap {
+            user = new User(ext: new UserExt(
+                    consentedProvidersSettingsCamelCase: new ConsentedProvidersSettings(consentedProviders: PBSUtils.randomString),
+                    consentedProvidersSettings: new ConsentedProvidersSettings(consentedProviders: PBSUtils.randomString)))
+        }
+        def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+        storedRequestDao.save(storedRequest)
+
+        when: "PBS processes amp request"
+        defaultPbsService.sendAmpRequest(ampRequest)
+
+        then: "Bidder request should contain addtl consent"
+        def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id)
+        assert bidderRequest.user.ext.consentedProvidersSettingsCamelCase.consentedProviders == randomAddtlConsent
+        assert bidderRequest.user.ext.consentedProvidersSettings.consentedProviders == randomAddtlConsent
+    }
+
+    def "PBS should process original user.ext.{consented_providers_settings/ConsentedProvidersSettings}.consented_providers when ampRequest doesn't contain addtl_consent"() {
+        given: "Default amp request with addtlConsent"
+        def ampRequest = AmpRequest.defaultAmpRequest.tap {
+            addtlConsent = null
+        }
+
+        and: "Save storedRequest into DB"
+        def consentProvidersKebabCase = PBSUtils.randomString
+        def consentProviders = PBSUtils.randomString
+        def ampStoredRequest = BidRequest.defaultBidRequest.tap {
+            user = new User(ext: new UserExt(
+                    consentedProvidersSettingsCamelCase: new ConsentedProvidersSettings(consentedProviders: consentProvidersKebabCase),
+                    consentedProvidersSettings: new ConsentedProvidersSettings(consentedProviders: consentProviders)))
+        }
+        def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+        storedRequestDao.save(storedRequest)
+
+        when: "PBS processes amp request"
+        defaultPbsService.sendAmpRequest(ampRequest)
+
+        then: "Bidder request should contain requested consent"
+        def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id)
+        assert bidderRequest.user.ext.consentedProvidersSettingsCamelCase.consentedProviders == consentProvidersKebabCase
+        assert bidderRequest.user.ext.consentedProvidersSettings.consentedProviders == consentProviders
+    }
+
+    def "PBS should left user.ext.{consented_providers_settings/ConsentedProvidersSettings}.consented_providers empty when addtl_consent and original fields are empty"() {
+        given: "Default amp request with addtlConsent"
+        def ampRequest = AmpRequest.defaultAmpRequest.tap {
+            addtlConsent = null
+        }
+
+        and: "Save storedRequest into DB"
+        def ampStoredRequest = BidRequest.defaultBidRequest.tap {
+            user = new User(ext: new UserExt(
+                    consentedProvidersSettingsCamelCase: new ConsentedProvidersSettings(consentedProviders: null),
+                    consentedProvidersSettings: new ConsentedProvidersSettings(consentedProviders: null)))
+        }
+        def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+        storedRequestDao.save(storedRequest)
+
+        when: "PBS processes amp request"
+        defaultPbsService.sendAmpRequest(ampRequest)
+
+        then: "Bidder request shouldn't contain consent"
+        def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id)
+        assert !bidderRequest.user.ext.consentedProvidersSettingsCamelCase.consentedProviders
+        assert !bidderRequest.user.ext.consentedProvidersSettings.consentedProviders
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy
index 20536435161..79eb960787f 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/BidAdjustmentSpec.groovy
@@ -1,22 +1,75 @@
 package org.prebid.server.functional.tests
 
+import org.prebid.server.functional.model.Currency
+import org.prebid.server.functional.model.config.AccountAuctionConfig
+import org.prebid.server.functional.model.config.AccountConfig
+import org.prebid.server.functional.model.db.Account
+import org.prebid.server.functional.model.mock.services.currencyconversion.CurrencyConversionRatesResponse
+import org.prebid.server.functional.model.request.auction.AdjustmentRule
+import org.prebid.server.functional.model.request.auction.AdjustmentType
+import org.prebid.server.functional.model.request.auction.BidAdjustment
 import org.prebid.server.functional.model.request.auction.BidAdjustmentFactors
+import org.prebid.server.functional.model.request.auction.BidAdjustmentRule
 import org.prebid.server.functional.model.request.auction.BidRequest
+import org.prebid.server.functional.model.request.auction.Imp
+import org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes
+import org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype
 import org.prebid.server.functional.model.response.auction.BidResponse
 import org.prebid.server.functional.service.PrebidServerException
+import org.prebid.server.functional.service.PrebidServerService
+import org.prebid.server.functional.testcontainers.scaffolding.CurrencyConversion
 import org.prebid.server.functional.util.PBSUtils
 
+import java.math.RoundingMode
+import java.time.Instant
+
 import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST
+import static org.prebid.server.functional.model.Currency.EUR
+import static org.prebid.server.functional.model.Currency.GBP
+import static org.prebid.server.functional.model.Currency.USD
 import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS
 import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
 import static org.prebid.server.functional.model.bidder.BidderName.RUBICON
+import static org.prebid.server.functional.model.request.auction.AdjustmentType.CPM
+import static org.prebid.server.functional.model.request.auction.AdjustmentType.MULTIPLIER
+import static org.prebid.server.functional.model.request.auction.AdjustmentType.STATIC
+import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.ANY
+import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.AUDIO
 import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.BANNER
 import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.NATIVE
+import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.UNKNOWN
 import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO
+import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_IN_STREAM
+import static org.prebid.server.functional.model.request.auction.BidAdjustmentMediaType.VIDEO_OUT_STREAM
 import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE
+import static org.prebid.server.functional.model.request.auction.VideoPlacementSubtypes.IN_STREAM as IN_PLACEMENT_STREAM
+import static org.prebid.server.functional.model.request.auction.VideoPlcmtSubtype.IN_STREAM as IN_PLCMT_STREAM
+import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID
+import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer
+import static org.prebid.server.functional.util.PBSUtils.getRandomDecimal
 
 class BidAdjustmentSpec extends BaseSpec {
 
+    private static final String WILDCARD = '*'
+    private static final BigDecimal MIN_ADJUST_VALUE = 0
+    private static final BigDecimal MAX_MULTIPLIER_ADJUST_VALUE = 99
+    private static final BigDecimal MAX_CPM_ADJUST_VALUE = Integer.MAX_VALUE
+    private static final BigDecimal MAX_STATIC_ADJUST_VALUE = Integer.MAX_VALUE
+    private static final Currency DEFAULT_CURRENCY = USD
+    private static final int BID_ADJUST_PRECISION = 4
+    private static final int PRICE_PRECISION = 3
+    private static final VideoPlacementSubtypes RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlacementSubtypes, [IN_PLACEMENT_STREAM])
+    private static final VideoPlcmtSubtype RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM = PBSUtils.getRandomEnum(VideoPlcmtSubtype, [IN_PLCMT_STREAM])
+    private static final Map<Currency, Map<Currency, BigDecimal>> DEFAULT_CURRENCY_RATES = [(USD): [(EUR): 0.9124920156948626,
+                                                                                                    (GBP): 0.793776804452961],
+                                                                                            (GBP): [(USD): 1.2597999770088517,
+                                                                                                    (EUR): 1.1495574203931487],
+                                                                                            (EUR): [(USD): 1.3429368029739777]]
+    private static final CurrencyConversion currencyConversion = new CurrencyConversion(networkServiceContainer).tap {
+        setCurrencyConversionRatesResponse(CurrencyConversionRatesResponse.getDefaultCurrencyConversionRatesResponse(DEFAULT_CURRENCY_RATES))
+    }
+    private static final PrebidServerService pbsService = pbsServiceFactory.getService(externalCurrencyConverterConfig)
+
     def "PBS should adjust bid price for matching bidder when request has per-bidder bid adjustment factors"() {
         given: "Default bid request with bid adjustment"
         def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap {
@@ -28,10 +81,10 @@ class BidAdjustmentSpec extends BaseSpec {
         bidder.setResponse(bidRequest.id, bidResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Final bid price should be adjusted"
-        assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price *
+        assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price *
                 bidAdjustmentFactor
 
         where:
@@ -40,7 +93,7 @@ class BidAdjustmentSpec extends BaseSpec {
 
     def "PBS should prefer bid price adjustment based on media type when request has per-media-type bid adjustment factors"() {
         given: "Default bid request with bid adjustment"
-        def bidAdjustment = PBSUtils.randomDecimal
+        def bidAdjustment = randomDecimal
         def mediaTypeBidAdjustment = bidAdjustmentFactor
         def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap {
             ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors().tap {
@@ -54,10 +107,10 @@ class BidAdjustmentSpec extends BaseSpec {
         bidder.setResponse(bidRequest.id, bidResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Final bid price should be adjusted"
-        assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price *
+        assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price *
                 mediaTypeBidAdjustment
 
         where:
@@ -66,7 +119,7 @@ class BidAdjustmentSpec extends BaseSpec {
 
     def "PBS should adjust bid price for bidder only when request contains bid adjustment for corresponding bidder"() {
         given: "Default bid request with bid adjustment"
-        def bidAdjustment = PBSUtils.randomDecimal
+        def bidAdjustment = randomDecimal
         def bidRequest = BidRequest.getDefaultBidRequest(SITE).tap {
             ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors().tap {
                 adjustments = [(adjustmentBidder): bidAdjustment]
@@ -78,10 +131,10 @@ class BidAdjustmentSpec extends BaseSpec {
         bidder.setResponse(bidRequest.id, bidResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Final bid price should not be adjusted"
-        assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price
+        assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price
 
         where:
         adjustmentBidder << [RUBICON, APPNEXUS]
@@ -102,10 +155,10 @@ class BidAdjustmentSpec extends BaseSpec {
         bidder.setResponse(bidRequest.id, bidResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Final bid price should not be adjusted"
-        assert response?.seatbid?.first()?.bid?.first()?.price == bidResponse.seatbid.first().bid.first().price
+        assert response?.seatbid?.first?.bid?.first?.price == bidResponse.seatbid.first.bid.first.price
 
         where:
         adjustmentMediaType << [VIDEO, NATIVE]
@@ -125,7 +178,7 @@ class BidAdjustmentSpec extends BaseSpec {
         bidder.setResponse(bidRequest.id, bidResponse)
 
         when: "PBS processes auction request"
-        defaultPbsService.sendAuctionRequest(bidRequest)
+        pbsService.sendAuctionRequest(bidRequest)
 
         then: "PBS should fail the request"
         def exception = thrown(PrebidServerException)
@@ -133,6 +186,971 @@ class BidAdjustmentSpec extends BaseSpec {
         assert exception.responseBody.contains("Invalid request format: request.ext.prebid.bidadjustmentfactors.$bidderName.value must be a positive number")
 
         where:
-        bidAdjustmentFactor << [0, PBSUtils.randomNegativeNumber]
+        bidAdjustmentFactor << [MIN_ADJUST_VALUE, PBSUtils.randomNegativeNumber]
+    }
+
+    def "PBS should adjust bid price for matching bidder when request has bidAdjustments config"() {
+        given: "Default BidRequest with ext.prebid.bidAdjustments"
+        def currency = USD
+        def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]])
+        bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule)
+        bidRequest.cur = [currency]
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "Final bid price should be adjusted"
+        assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)
+        assert response.cur == bidResponse.cur
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "Bidder request should contain default currency"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+
+        where:
+        adjustmentType | ruleValue                                                       | mediaType        | bidRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER           | BidRequest.defaultBidRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO            | BidRequest.defaultAudioRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE           | BidRequest.defaultNativeRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY              | BidRequest.defaultBidRequest
+
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | BANNER           | BidRequest.defaultBidRequest
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | AUDIO            | BidRequest.defaultAudioRequest
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | NATIVE           | BidRequest.defaultNativeRequest
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | ANY              | BidRequest.defaultBidRequest
+
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | BANNER           | BidRequest.defaultBidRequest
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | AUDIO            | BidRequest.defaultAudioRequest
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | NATIVE           | BidRequest.defaultNativeRequest
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | ANY              | BidRequest.defaultBidRequest
+    }
+
+    def "PBS should adjust bid price for matching bidder with specific dealId when request has bidAdjustments config"() {
+        given: "Default BidRequest with ext.prebid.bidAdjustments"
+        def dealId = PBSUtils.randomString
+        def currency = USD
+        def rule = new BidAdjustmentRule(generic: [(dealId): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]])
+        bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule)
+        bidRequest.imp.add(Imp.defaultImpression)
+        bidRequest.cur = [currency]
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+            seatbid.first.bid.first.dealid = dealId
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "Final bid price should be adjusted for big with dealId"
+        response.seatbid.first.bid.find { it.dealid == dealId }
+        assert response.seatbid.first.bid.findAll() { it.dealid == dealId }.price == [getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)]
+
+        and: "Price shouldn't be updated for bid with different dealId"
+        assert response.seatbid.first.bid.findAll() { it.dealid != dealId }.price == bidResponse.seatbid.first.bid.findAll() { it.dealid != dealId }.price
+
+        and: "Response currency should stay the same"
+        assert response.cur == bidResponse.cur
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        assert response.seatbid.first.bid.ext.origbidcpm.sort() == bidResponse.seatbid.first.bid.price.sort()
+        assert response.seatbid.first.bid.ext.first.origbidcur == bidResponse.cur
+        assert response.seatbid.first.bid.ext.last.origbidcur == bidResponse.cur
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+
+        where:
+        adjustmentType | ruleValue                                                       | mediaType        | bidRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER           | BidRequest.defaultBidRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO            | BidRequest.defaultAudioRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE           | BidRequest.defaultNativeRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY              | BidRequest.defaultBidRequest
+
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | BANNER           | BidRequest.defaultBidRequest
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | AUDIO            | BidRequest.defaultAudioRequest
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | NATIVE           | BidRequest.defaultNativeRequest
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | ANY              | BidRequest.defaultBidRequest
+
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | BANNER           | BidRequest.defaultBidRequest
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | AUDIO            | BidRequest.defaultAudioRequest
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | NATIVE           | BidRequest.defaultNativeRequest
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | ANY              | BidRequest.defaultBidRequest
+    }
+
+    def "PBS should adjust bid price for matching bidder when account config has bidAdjustments"() {
+        given: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def currency = USD
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        and: "Account in the DB with bidAdjustments"
+        def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]])
+        def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule))
+        def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig))
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "Final bid price should be adjusted"
+        assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)
+        assert response.cur == bidResponse.cur
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+
+        where:
+        adjustmentType | ruleValue                                                       | mediaType        | bidRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER           | BidRequest.defaultBidRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO            | BidRequest.defaultAudioRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE           | BidRequest.defaultNativeRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY              | BidRequest.defaultBidRequest
+
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | BANNER           | BidRequest.defaultBidRequest
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | AUDIO            | BidRequest.defaultAudioRequest
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | NATIVE           | BidRequest.defaultNativeRequest
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | ANY              | BidRequest.defaultBidRequest
+
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | BANNER           | BidRequest.defaultBidRequest
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | AUDIO            | BidRequest.defaultAudioRequest
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | NATIVE           | BidRequest.defaultNativeRequest
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | ANY              | BidRequest.defaultBidRequest
+    }
+
+    def "PBS should prioritize BidAdjustmentRule from request when account and request config bidAdjustments conflict"() {
+        given: "Default BidRequest with ext.prebid.bidAdjustments"
+        def currency = USD
+        def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]])
+        bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule)
+        bidRequest.cur = [currency]
+
+        and: "Account in the DB with bidAdjustments"
+        def accountRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]])
+        def accountConfig = new AccountAuctionConfig(bidAdjustments: BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, accountRule))
+        def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountConfig))
+        accountDao.save(account)
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "Final bid price should be adjusted according to request config"
+        assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, ruleValue as BigDecimal, adjustmentType)
+        assert response.cur == bidResponse.cur
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+
+        where:
+        adjustmentType | ruleValue                                                       | mediaType        | bidRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | BANNER           | BidRequest.defaultBidRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | AUDIO            | BidRequest.defaultAudioRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | NATIVE           | BidRequest.defaultNativeRequest
+        MULTIPLIER     | getRandomDecimal(MIN_ADJUST_VALUE, MAX_MULTIPLIER_ADJUST_VALUE) | ANY              | BidRequest.defaultBidRequest
+
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | BANNER           | BidRequest.defaultBidRequest
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | AUDIO            | BidRequest.defaultAudioRequest
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | NATIVE           | BidRequest.defaultNativeRequest
+        CPM            | getRandomDecimal(MIN_ADJUST_VALUE, MAX_CPM_ADJUST_VALUE)        | ANY              | BidRequest.defaultBidRequest
+
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | BANNER           | BidRequest.defaultBidRequest
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | AUDIO            | BidRequest.defaultAudioRequest
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | NATIVE           | BidRequest.defaultNativeRequest
+        STATIC         | getRandomDecimal(MIN_ADJUST_VALUE, MAX_STATIC_ADJUST_VALUE)     | ANY              | BidRequest.defaultBidRequest
+    }
+
+    def "PBS should prioritize exact bid price adjustment for matching bidder when request has exact and general bidAdjustment"() {
+        given: "Default BidRequest with ext.prebid.bidAdjustments"
+        def exactRulePrice = PBSUtils.randomPrice
+        def currency = USD
+        def exactRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: exactRulePrice, currency: currency)]])
+        def generalRule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: currency)]])
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            cur = [currency]
+            ext.prebid.bidAdjustments = new BidAdjustment(mediaType: [(BANNER): exactRule, (ANY): generalRule])
+        }
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "Final bid price should be adjusted according to exact rule"
+        assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, exactRulePrice, STATIC)
+        assert response.cur == bidResponse.cur
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+    }
+
+    def "PBS should adjust bid price for matching bidder in provided order when bidAdjustments have multiple matching rules"() {
+        given: "Default BidRequest with ext.prebid.bidAdjustments"
+        def currency = USD
+        def firstRule = new AdjustmentRule(adjustmentType: firstRuleType, value: PBSUtils.randomPrice, currency: currency)
+        def secondRule = new AdjustmentRule(adjustmentType: secondRuleType, value: PBSUtils.randomPrice, currency: currency)
+        def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [firstRule, secondRule]])
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            cur = [currency]
+            ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule)
+        }
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "Final bid price should be adjusted"
+        def rawAdjustedBidPrice = getAdjustedPrice(originalPrice, firstRule.value as BigDecimal, firstRule.adjustmentType)
+        def adjustedBidPrice = getAdjustedPrice(rawAdjustedBidPrice, secondRule.value as BigDecimal, secondRule.adjustmentType)
+        assert response.seatbid.first.bid.first.price == adjustedBidPrice
+        assert response.cur == bidResponse.cur
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+
+        where:
+        firstRuleType | secondRuleType
+        MULTIPLIER    | CPM
+        MULTIPLIER    | STATIC
+        MULTIPLIER    | MULTIPLIER
+        CPM           | CPM
+        CPM           | STATIC
+        CPM           | MULTIPLIER
+        STATIC        | CPM
+        STATIC        | STATIC
+        STATIC        | MULTIPLIER
+    }
+
+    def "PBS should convert CPM currency before adjustment when it different from original response currency"() {
+        given: "Default BidRequest with ext.prebid.bidAdjustments"
+        def adjustmentRule = new AdjustmentRule(adjustmentType: CPM, value: PBSUtils.randomPrice, currency: GBP)
+        def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]])
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            cur = [EUR]
+            ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule)
+        }
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = USD
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "Final bid price should be adjusted"
+        def convertedAdjustment = convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidResponse.cur)
+        def adjustedBidPrice = getAdjustedPrice(originalPrice, convertedAdjustment, adjustmentRule.adjustmentType)
+        assert response.seatbid.first.bid.first.price == convertCurrency(adjustedBidPrice, bidResponse.cur, bidRequest.cur.first)
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == bidRequest.cur
+    }
+
+    def "PBS should change original currency when static bidAdjustments and original response have different currencies"() {
+        given: "Default BidRequest with ext.prebid.bidAdjustments"
+        def adjustmentRule = new AdjustmentRule(adjustmentType: STATIC, value: PBSUtils.randomPrice, currency: GBP)
+        def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]])
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            cur = [EUR]
+            ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule)
+        }
+
+        and: "Default bid response with JPY currency"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = USD
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "Final bid price should be adjusted and converted to original request cur"
+        assert response.seatbid.first.bid.first.price == convertCurrency(adjustmentRule.value, adjustmentRule.currency, bidRequest.cur.first)
+        assert response.cur == bidRequest.cur.first
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == bidRequest.cur
+    }
+
+    def "PBS should apply bidAdjustments after bidAdjustmentFactors when both are present"() {
+        given: "Default BidRequest with ext.prebid.bidAdjustments"
+        def currency = USD
+        def bidAdjustmentFactorsPrice = PBSUtils.randomPrice
+        def adjustmentRule = new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency)
+        def bidAdjustmentMultyRule = new BidAdjustmentRule(generic: [(WILDCARD): [adjustmentRule]])
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            cur = [currency]
+            ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, bidAdjustmentMultyRule)
+            ext.prebid.bidAdjustmentFactors = new BidAdjustmentFactors(adjustments: [(GENERIC): bidAdjustmentFactorsPrice])
+        }
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "Final bid price should be adjusted"
+        def bidAdjustedPrice = originalPrice * bidAdjustmentFactorsPrice
+        assert response.seatbid.first.bid.first.price == getAdjustedPrice(bidAdjustedPrice, adjustmentRule.value, adjustmentType)
+        assert response.cur == bidResponse.cur
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+
+        where:
+        adjustmentType << [MULTIPLIER, CPM, STATIC]
+    }
+
+    def "PBS shouldn't adjust bid price for matching bidder when request has invalid value bidAdjustments config"() {
+        given: "Start time"
+        def startTime = Instant.now()
+
+        and: "Default BidRequest with ext.prebid.bidAdjustments"
+        def currency = USD
+        def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: ruleValue, currency: currency)]])
+        bidRequest.ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(mediaType, rule)
+        bidRequest.cur = [currency]
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should ignore bidAdjustments for this request"
+        assert response.seatbid.first.bid.first.price == originalPrice
+        assert response.cur == bidResponse.cur
+
+        and: "Should add a warning when in debug mode"
+        def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " +
+                "value=${ruleValue}, currency=${currency}] in ${mediaType.value}.generic.* is invalid" as String
+        assert response.ext.warnings[PREBID]?.code == [999]
+        assert response.ext.warnings[PREBID]?.message == [errorMessage]
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "PBS log should contain error"
+        def logs = pbsService.getLogsByTime(startTime)
+        assert getLogsByText(logs, errorMessage)
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+
+        where:
+        adjustmentType | ruleValue                       | mediaType        | bidRequest
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | BANNER           | BidRequest.defaultBidRequest
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | AUDIO            | BidRequest.defaultAudioRequest
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | NATIVE           | BidRequest.defaultNativeRequest
+        MULTIPLIER     | MIN_ADJUST_VALUE - 1            | ANY              | BidRequest.defaultNativeRequest
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | BANNER           | BidRequest.defaultBidRequest
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | AUDIO            | BidRequest.defaultAudioRequest
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | NATIVE           | BidRequest.defaultNativeRequest
+        MULTIPLIER     | MAX_MULTIPLIER_ADJUST_VALUE + 1 | ANY              | BidRequest.defaultNativeRequest
+
+        CPM            | MIN_ADJUST_VALUE - 1            | BANNER           | BidRequest.defaultBidRequest
+        CPM            | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        CPM            | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        CPM            | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        CPM            | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        CPM            | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | MIN_ADJUST_VALUE - 1            | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | MIN_ADJUST_VALUE - 1            | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        CPM            | MIN_ADJUST_VALUE - 1            | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | MIN_ADJUST_VALUE - 1            | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        CPM            | MIN_ADJUST_VALUE - 1            | AUDIO            | BidRequest.defaultAudioRequest
+        CPM            | MIN_ADJUST_VALUE - 1            | NATIVE           | BidRequest.defaultNativeRequest
+        CPM            | MIN_ADJUST_VALUE - 1            | ANY              | BidRequest.defaultNativeRequest
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | BANNER           | BidRequest.defaultBidRequest
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | AUDIO            | BidRequest.defaultAudioRequest
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | NATIVE           | BidRequest.defaultNativeRequest
+        CPM            | MAX_CPM_ADJUST_VALUE + 1        | ANY              | BidRequest.defaultNativeRequest
+
+        STATIC         | MIN_ADJUST_VALUE - 1            | BANNER           | BidRequest.defaultBidRequest
+        STATIC         | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        STATIC         | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        STATIC         | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        STATIC         | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        STATIC         | MIN_ADJUST_VALUE - 1            | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | MIN_ADJUST_VALUE - 1            | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | MIN_ADJUST_VALUE - 1            | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        STATIC         | MIN_ADJUST_VALUE - 1            | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | MIN_ADJUST_VALUE - 1            | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        STATIC         | MIN_ADJUST_VALUE - 1            | AUDIO            | BidRequest.defaultAudioRequest
+        STATIC         | MIN_ADJUST_VALUE - 1            | NATIVE           | BidRequest.defaultNativeRequest
+        STATIC         | MIN_ADJUST_VALUE - 1            | ANY              | BidRequest.defaultNativeRequest
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | BANNER           | BidRequest.defaultBidRequest
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlacement(IN_PLACEMENT_STREAM)
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmt(IN_PLCMT_STREAM)
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, IN_PLACEMENT_STREAM)
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, IN_PLACEMENT_STREAM)
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | VIDEO_IN_STREAM  | getDefaultVideoRequestWithPlcmtAndPlacement(IN_PLCMT_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM, RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmtAndPlacement(null, null)
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlacement(RANDOM_VIDEO_PLACEMENT_EXCEPT_IN_STREAM)
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | VIDEO_OUT_STREAM | getDefaultVideoRequestWithPlcmt(RANDOM_VIDEO_PLCMT_EXCEPT_IN_STREAM)
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | AUDIO            | BidRequest.defaultAudioRequest
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | NATIVE           | BidRequest.defaultNativeRequest
+        STATIC         | MAX_STATIC_ADJUST_VALUE + 1     | ANY              | BidRequest.defaultNativeRequest
+    }
+
+    def "PBS shouldn't adjust bid price for matching bidder when request has different bidder name in bidAdjustments config"() {
+        given: "Default BidRequest with ext.prebid.bidAdjustments"
+        def currency = USD
+        def rule = new BidAdjustmentRule(alias: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: PBSUtils.randomPrice, currency: currency)]])
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            cur = [currency]
+            ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule)
+        }
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should ignore bidAdjustments for this request"
+        assert response.seatbid.first.bid.first.price == originalPrice
+        assert response.cur == bidResponse.cur
+
+        and: "Response shouldn't contain any warnings"
+        assert !response.ext.warnings
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+
+        where:
+        adjustmentType << [MULTIPLIER, CPM, STATIC]
+    }
+
+    def "PBS shouldn't adjust bid price for matching bidder when cpm or static bidAdjustments doesn't have currency value"() {
+        given: "Start time"
+        def startTime = Instant.now()
+
+        and: "Default BidRequest with ext.prebid.bidAdjustments"
+        def currency = USD
+        def adjustmentPrice = PBSUtils.randomPrice.toDouble()
+        def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]])
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            cur = [currency]
+            ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule)
+        }
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should ignore bidAdjustments for this request"
+        assert response.seatbid.first.bid.first.price == originalPrice
+        assert response.cur == bidResponse.cur
+
+        and: "Should add a warning when in debug mode"
+        def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=${adjustmentType}, " +
+                "value=${adjustmentPrice}, currency=null] in banner.generic.* is invalid" as String
+        assert response.ext.warnings[PREBID]?.code == [999]
+        assert response.ext.warnings[PREBID]?.message == [errorMessage]
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "PBS log should contain error"
+        def logs = pbsService.getLogsByTime(startTime)
+        assert getLogsByText(logs, errorMessage)
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+
+        where:
+        adjustmentType << [CPM, STATIC]
+    }
+
+    def "PBS shouldn't adjust bid price for matching bidder when bidAdjustments have unknown mediatype"() {
+        given: "Default BidRequest with ext.prebid.bidAdjustments"
+        def adjustmentPrice = PBSUtils.randomPrice
+        def currency = USD
+        def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: adjustmentType, value: adjustmentPrice, currency: null)]])
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            cur = [currency]
+            ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(UNKNOWN, rule)
+        }
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should ignore bidAdjustments for this request"
+        assert response.seatbid.first.bid.first.price == originalPrice
+        assert response.cur == bidResponse.cur
+
+        and: "Response shouldn't contain any warnings"
+        assert !response.ext.warnings
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+
+        where:
+        adjustmentType << [MULTIPLIER, CPM, STATIC]
+    }
+
+    def "PBS shouldn't adjust bid price for matching bidder when bidAdjustments have unknown adjustmentType"() {
+        given: "Start time"
+        def startTime = Instant.now()
+
+        and: "Default BidRequest with ext.prebid.bidAdjustments"
+        def currency = USD
+        def adjustmentPrice = PBSUtils.randomPrice.toDouble()
+        def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: AdjustmentType.UNKNOWN, value: adjustmentPrice, currency: currency)]])
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            cur = [currency]
+            ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule)
+        }
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should ignore bidAdjustments for this request"
+        assert response.seatbid.first.bid.first.price == originalPrice
+        assert response.cur == bidResponse.cur
+
+        and: "Should add a warning when in debug mode"
+        def errorMessage = "bid adjustment from request was invalid: the found rule [adjtype=UNKNOWN, " +
+                "value=$adjustmentPrice, currency=$currency] in banner.generic.* is invalid" as String
+        assert response.ext.warnings[PREBID]?.code == [999]
+        assert response.ext.warnings[PREBID]?.message == [errorMessage]
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "PBS log should contain error"
+        def logs = pbsService.getLogsByTime(startTime)
+        assert getLogsByText(logs, errorMessage)
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+    }
+
+    def "PBS shouldn't adjust bid price for matching bidder when multiplier bidAdjustments doesn't have currency value"() {
+        given: "Default BidRequest with ext.prebid.bidAdjustments"
+        def currency = USD
+        def adjustmentPrice = PBSUtils.randomPrice
+        def rule = new BidAdjustmentRule(generic: [(WILDCARD): [new AdjustmentRule(adjustmentType: MULTIPLIER, value: adjustmentPrice, currency: null)]])
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            cur = [currency]
+            ext.prebid.bidAdjustments = BidAdjustment.getDefaultWithSingleMediaTypeRule(BANNER, rule)
+        }
+
+        and: "Default bid response"
+        def originalPrice = PBSUtils.randomPrice
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            cur = currency
+            seatbid.first.bid.first.price = originalPrice
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsService.sendAuctionRequest(bidRequest)
+
+        then: "Final bid price should be adjusted"
+        assert response.seatbid.first.bid.first.price == getAdjustedPrice(originalPrice, adjustmentPrice, MULTIPLIER)
+        assert response.cur == bidResponse.cur
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "Response shouldn't contain any warnings"
+        assert !response.ext.warnings
+
+        and: "Original bid price and currency should be presented in bid.ext"
+        verifyAll(response.seatbid.first.bid.first.ext) {
+            origbidcpm == originalPrice
+            origbidcur == bidResponse.cur
+        }
+
+        and: "Bidder request should contain currency from request"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.cur == [currency]
+
+        where:
+        adjustmentType << [CPM, STATIC]
+    }
+
+    private static Map<String, String> getExternalCurrencyConverterConfig() {
+        ["auction.ad-server-currency"                          : DEFAULT_CURRENCY as String,
+         "currency-converter.external-rates.enabled"           : "true",
+         "currency-converter.external-rates.url"               : "$networkServiceContainer.rootUri/currency".toString(),
+         "currency-converter.external-rates.default-timeout-ms": "4000",
+         "currency-converter.external-rates.refresh-period-ms" : "900000"]
+    }
+
+    private static BigDecimal convertCurrency(BigDecimal price, Currency fromCurrency, Currency toCurrency) {
+        return (price * getConversionRate(fromCurrency, toCurrency)).setScale(PRICE_PRECISION, RoundingMode.HALF_EVEN)
+    }
+
+    private static BigDecimal getConversionRate(Currency fromCurrency, Currency toCurrency) {
+        def conversionRate
+        if (fromCurrency == toCurrency) {
+            conversionRate = 1
+        } else if (toCurrency in DEFAULT_CURRENCY_RATES?[fromCurrency]) {
+            conversionRate = DEFAULT_CURRENCY_RATES[fromCurrency][toCurrency]
+        } else if (fromCurrency in DEFAULT_CURRENCY_RATES?[toCurrency]) {
+            conversionRate = 1 / DEFAULT_CURRENCY_RATES[toCurrency][fromCurrency]
+        } else {
+            conversionRate = getCrossConversionRate(fromCurrency, toCurrency)
+        }
+        conversionRate
+    }
+
+    private static BigDecimal getCrossConversionRate(Currency fromCurrency, Currency toCurrency) {
+        for (Map<Currency, BigDecimal> rates : DEFAULT_CURRENCY_RATES.values()) {
+            def fromRate = rates?[fromCurrency]
+            def toRate = rates?[toCurrency]
+
+            if (fromRate && toRate) {
+                return toRate / fromRate
+            }
+        }
+
+        null
+    }
+
+    private static BigDecimal getAdjustedPrice(BigDecimal originalPrice,
+                                               BigDecimal adjustedValue,
+                                               AdjustmentType adjustmentType) {
+        switch (adjustmentType) {
+            case MULTIPLIER:
+                return PBSUtils.roundDecimal(originalPrice * adjustedValue, BID_ADJUST_PRECISION)
+            case CPM:
+                return PBSUtils.roundDecimal(originalPrice - adjustedValue, BID_ADJUST_PRECISION)
+            case STATIC:
+                return adjustedValue
+            default:
+                return originalPrice
+        }
+    }
+
+    private static BidRequest getDefaultVideoRequestWithPlacement(VideoPlacementSubtypes videoPlacementSubtypes) {
+        BidRequest.defaultVideoRequest.tap {
+            imp.first.video.tap {
+                placement = videoPlacementSubtypes
+            }
+        }
+    }
+
+    private static BidRequest getDefaultVideoRequestWithPlcmt(VideoPlcmtSubtype videoPlcmtSubtype) {
+        BidRequest.defaultVideoRequest.tap {
+            imp.first.video.tap {
+                plcmt = videoPlcmtSubtype
+            }
+        }
+    }
+
+    private static BidRequest getDefaultVideoRequestWithPlcmtAndPlacement(VideoPlcmtSubtype videoPlcmtSubtype,
+                                                                          VideoPlacementSubtypes videoPlacementSubtypes) {
+        BidRequest.defaultVideoRequest.tap {
+            imp.first.video.tap {
+                plcmt = videoPlcmtSubtype
+                placement = videoPlacementSubtypes
+            }
+        }
     }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy
index cc877f847a0..6ca55f4b7bc 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/BidExpResponseSpec.groovy
@@ -4,17 +4,41 @@ import org.prebid.server.functional.model.config.AccountAuctionConfig
 import org.prebid.server.functional.model.config.AccountConfig
 import org.prebid.server.functional.model.db.Account
 import org.prebid.server.functional.model.request.auction.BidRequest
+import org.prebid.server.functional.model.request.auction.Imp
 import org.prebid.server.functional.model.request.auction.PrebidCache
 import org.prebid.server.functional.model.request.auction.PrebidCacheSettings
 import org.prebid.server.functional.model.response.auction.BidResponse
 import org.prebid.server.functional.util.PBSUtils
 
+import static org.prebid.server.functional.model.response.auction.MediaType.BANNER
+import static org.prebid.server.functional.model.response.auction.MediaType.VIDEO
+import static org.prebid.server.functional.model.response.auction.MediaType.NATIVE
+import static org.prebid.server.functional.model.response.auction.MediaType.AUDIO
+
 class BidExpResponseSpec extends BaseSpec {
 
-    private static def hostBannerTtl = PBSUtils.randomNumber
-    private static def hostVideoTtl = PBSUtils.randomNumber
-    private static def cacheTtlService = pbsServiceFactory.getService(['cache.banner-ttl-seconds': hostBannerTtl as String,
-                                                                       'cache.video-ttl-seconds' : hostVideoTtl as String])
+    private static final def BANNER_TTL_HOST_CACHE = PBSUtils.randomNumber
+    private static final def VIDEO_TTL_HOST_CACHE = PBSUtils.randomNumber
+    private static final def BANNER_TTL_DEFAULT_CACHE = PBSUtils.randomNumber
+    private static final def VIDEO_TTL_DEFAULT_CACHE = PBSUtils.randomNumber
+    private static final def AUDIO_TTL_DEFAULT_CACHE = PBSUtils.randomNumber
+    private static final def NATIVE_TTL_DEFAULT_CACHE = PBSUtils.randomNumber
+    private static final Map<String, String> CACHE_TTL_HOST_CONFIG = ["cache.banner-ttl-seconds": BANNER_TTL_HOST_CACHE as String,
+                                                                      "cache.video-ttl-seconds" : VIDEO_TTL_HOST_CACHE as String]
+    private static final Map<String, String> DEFAULT_CACHE_TTL_CONFIG = ["cache.default-ttl-seconds.banner": BANNER_TTL_DEFAULT_CACHE as String,
+                                                                         "cache.default-ttl-seconds.video" : VIDEO_TTL_DEFAULT_CACHE as String,
+                                                                         "cache.default-ttl-seconds.native": NATIVE_TTL_DEFAULT_CACHE as String,
+                                                                         "cache.default-ttl-seconds.audio" : AUDIO_TTL_DEFAULT_CACHE as String]
+    private static final Map<String, String> EMPTY_CACHE_TTL_CONFIG = ["cache.default-ttl-seconds.banner": "",
+                                                                       "cache.default-ttl-seconds.video" : "",
+                                                                       "cache.default-ttl-seconds.native": "",
+                                                                       "cache.default-ttl-seconds.audio" : ""]
+    private static final Map<String, String> EMPTY_CACHE_TTL_HOST_CONFIG = ["cache.banner-ttl-seconds": "",
+                                                                            "cache.video-ttl-seconds" : ""]
+    private static def pbsOnlyHostCacheTtlService = pbsServiceFactory.getService(CACHE_TTL_HOST_CONFIG + EMPTY_CACHE_TTL_CONFIG)
+    private static def pbsEmptyTtlService = pbsServiceFactory.getService(EMPTY_CACHE_TTL_CONFIG + EMPTY_CACHE_TTL_HOST_CONFIG)
+    private static def pbsHostAndDefaultCacheTtlService = pbsServiceFactory.getService(CACHE_TTL_HOST_CONFIG + DEFAULT_CACHE_TTL_CONFIG)
+
 
     def "PBS auction should resolve bid.exp from response that is set by the bidderโ€™s adapter"() {
         given: "Default basicResponse with exp"
@@ -131,25 +155,6 @@ class BidExpResponseSpec extends BaseSpec {
         assert response.seatbid.bid.first.exp == [bidRequestExp]
     }
 
-    def "PBS auction shouldn't resolve exp from request.ext.prebid.cache for request when it have invalid type"() {
-        given: "Set bidder response without exp"
-        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
-            seatbid[0].bid[0].exp = null
-        }
-        bidder.setResponse(bidRequest.id, bidResponse)
-
-        when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
-
-        then: "Bid response shouldn't contain exp data"
-        assert !response.seatbid.first.bid.first.exp
-
-        where:
-        bidRequest                     | cache
-        BidRequest.defaultBidRequest   | new PrebidCache(vastXml: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber))
-        BidRequest.defaultVideoRequest | new PrebidCache(bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber))
-    }
-
     def "PBS auction should resolve exp from account config for banner request when it have value"() {
         given: "default bidRequest"
         def bidRequest = BidRequest.defaultBidRequest
@@ -173,28 +178,6 @@ class BidExpResponseSpec extends BaseSpec {
         assert response.seatbid.bid.first.exp == [accountCacheTtl]
     }
 
-    def "PBS auction shouldn't resolve exp from account videoCacheTtl config when bidRequest type doesn't matching"() {
-        given: "default bidRequest"
-        def bidRequest = BidRequest.defaultBidRequest
-
-        and: "Account in the DB"
-        def auctionConfig = new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber)
-        def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig))
-        accountDao.save(account)
-
-        and: "Set bidder response without exp"
-        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
-            seatbid[0].bid[0].exp = null
-        }
-        bidder.setResponse(bidRequest.id, bidResponse)
-
-        when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
-
-        then: "Bid response shouldn't contain exp data"
-        assert !response.seatbid.first.bid.first.exp
-    }
-
     def "PBS auction should resolve exp from account videoCacheTtl config for video request when it have value"() {
         given: "default bidRequest"
         def bidRequest = BidRequest.defaultVideoRequest
@@ -218,55 +201,15 @@ class BidExpResponseSpec extends BaseSpec {
         assert response.seatbid.bid.first.exp == [accountCacheTtl]
     }
 
-    def "PBS auction should resolve exp from account bannerCacheTtl config for video request when it have value"() {
-        given: "default bidRequest"
-        def bidRequest = BidRequest.defaultVideoRequest
-
-        and: "Account in the DB"
-        def accountCacheTtl = PBSUtils.randomNumber
-        def auctionConfig = new AccountAuctionConfig(bannerCacheTtl: accountCacheTtl)
-        def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig))
-        accountDao.save(account)
-
-        and: "Set bidder response without exp"
-        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
-            seatbid[0].bid[0].exp = null
-        }
-        bidder.setResponse(bidRequest.id, bidResponse)
-
-        when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
-
-        then: "Bid response should contain exp data"
-        assert response.seatbid.bid.first.exp == [accountCacheTtl]
-    }
-
     def "PBS auction should resolve exp from global banner config for banner request"() {
         given: "Default bidRequest"
         def bidRequest = BidRequest.defaultBidRequest
 
         when: "PBS processes auction request"
-        def response = cacheTtlService.sendAuctionRequest(bidRequest)
-
-        then: "Bid response should contain exp data"
-        assert response.seatbid.bid.first.exp == [hostBannerTtl]
-    }
-
-    def "PBS auction should resolve exp from global config for video request based on highest value"() {
-        given: "Default bidRequest"
-        def bidRequest = BidRequest.defaultVideoRequest
-
-        and: "Set bidder response without exp"
-        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
-            seatbid[0].bid[0].exp = null
-        }
-        bidder.setResponse(bidRequest.id, bidResponse)
-
-        when: "PBS processes auction request"
-        def response = cacheTtlService.sendAuctionRequest(bidRequest)
+        def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest)
 
         then: "Bid response should contain exp data"
-        assert response.seatbid.bid.first.exp == [Math.max(hostVideoTtl, hostBannerTtl)]
+        assert response.seatbid.bid.first.exp == [BANNER_TTL_HOST_CACHE]
     }
 
     def "PBS auction should prioritize value from bid.exp rather than request.imp[].exp"() {
@@ -356,9 +299,348 @@ class BidExpResponseSpec extends BaseSpec {
         bidder.setResponse(bidRequest.id, bidResponse)
 
         when: "PBS processes auction request"
-        def response = cacheTtlService.sendAuctionRequest(bidRequest)
+        def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest)
 
         then: "Bid response should contain exp data"
         assert response.seatbid.bid.first.exp == [accountCacheTtl]
     }
+
+    def "PBS auction should prioritize bid.exp from the response over all other fields from the request and account config"() {
+        given: "Default bid request with specific imp media type"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            imp[0] = Imp.getDefaultImpression(mediaType).tap {
+                exp = PBSUtils.randomNumber
+            }
+            ext.prebid.cache = new PrebidCache(
+                    vastXml: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber),
+                    bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber))
+        }
+
+        and: "Default bid response with bid.exp"
+        def randomExp = PBSUtils.randomNumber
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            seatbid[0].bid[0].exp = randomExp
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        and: "Account in the DB"
+        def auctionConfig = new AccountAuctionConfig(
+                videoCacheTtl: PBSUtils.randomNumber,
+                bannerCacheTtl: PBSUtils.randomNumber)
+        def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig))
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response should contain exp data"
+        assert response.seatbid.first.bid.first.exp == randomExp
+
+        where:
+        mediaType << [BANNER, VIDEO, NATIVE, AUDIO]
+    }
+
+    def "PBS auction shouldn't resolve bid.exp for #mediaType when the response, request, and account config don't include such data"() {
+        given: "Default bid request with specific imp media type"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            imp[0] = Imp.getDefaultImpression(mediaType)
+        }
+
+        and: "Default bid response with bid.exp"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            seatbid[0].bid[0].exp = null
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response shouldn't contain exp data"
+        assert !response.seatbid.first.bid.first.exp
+
+        where:
+        mediaType << [BANNER, VIDEO, NATIVE, AUDIO]
+    }
+
+    def "PBS auction should prioritize imp.exp and resolve bid.exp for #mediaType when request and account config include multiple exp sources"() {
+        given: "Default bid request"
+        def randomExp = PBSUtils.randomNumber
+        def bidRequest = BidRequest.getDefaultBidRequest().tap {
+            imp[0] = Imp.getDefaultImpression(mediaType).tap {
+                exp = randomExp
+            }
+            ext.prebid.cache = new PrebidCache(
+                    vastXml: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber),
+                    bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber))
+        }
+
+        and: "Default bid response without bid.exp"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            seatbid[0].bid[0].exp = null
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        and: "Account in the DB"
+        def auctionConfig = new AccountAuctionConfig(
+                videoCacheTtl: PBSUtils.randomNumber,
+                bannerCacheTtl: PBSUtils.randomNumber)
+        def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig))
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response should contain exp data"
+        assert response.seatbid.first.bid.first.exp == randomExp
+
+        where:
+        mediaType << [BANNER, VIDEO, NATIVE, AUDIO]
+    }
+
+    def "PBS auction shouldn't resolve bid.exp from ext.prebid.cache.vastxml.ttlseconds when request has #mediaType as mediaType"() {
+        given: "Default bid request"
+        def randomExp = PBSUtils.randomNumber
+        def bidRequest = BidRequest.getDefaultBidRequest().tap {
+            enableCache()
+            imp[0] = Imp.getDefaultImpression(mediaType)
+            ext.prebid.cache = new PrebidCache(vastXml: new PrebidCacheSettings(ttlSeconds: randomExp))
+        }
+
+        and: "Default bid response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        and: "Account in the DB"
+        def auctionConfig = new AccountAuctionConfig(
+                videoCacheTtl: PBSUtils.randomNumber)
+        def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig))
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response shouldn't contain exp data"
+        assert !response?.seatbid?.first?.bid?.first?.exp
+
+        where:
+        mediaType << [BANNER, NATIVE, AUDIO]
+    }
+
+    def "PBS auction should resolve bid.exp from ext.prebid.cache.vastxml.ttlseconds when request has video as mediaType"() {
+        given: "Default bid request"
+        def bidsTtlSeconds = PBSUtils.randomNumber
+        def vastXmTtlSeconds = bidsTtlSeconds + 1
+        def bidRequest = BidRequest.getDefaultBidRequest().tap {
+            enableCache()
+            imp[0] = Imp.getDefaultImpression(VIDEO)
+
+            ext.prebid.cache = new PrebidCache(
+                    vastXml: new PrebidCacheSettings(ttlSeconds: vastXmTtlSeconds),
+                    bids: new PrebidCacheSettings(ttlSeconds: bidsTtlSeconds))
+        }
+
+        and: "Default bid response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        and: "Account in the DB"
+        def auctionConfig = new AccountAuctionConfig(
+                videoCacheTtl: PBSUtils.randomNumber,
+                bannerCacheTtl: PBSUtils.randomNumber)
+        def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig))
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response should contain exp data"
+        assert response.seatbid.first.bid.first.exp == vastXmTtlSeconds
+    }
+
+    def "PBS auction should resolve bid.exp when ext.prebid.cache.bids.ttlseconds is specified and no higher-priority fields are present"() {
+        given: "Default bid request"
+        def randomExp = PBSUtils.randomNumber
+        def bidRequest = BidRequest.getDefaultBidRequest().tap {
+            enableCache()
+            imp[0] = Imp.getDefaultImpression(mediaType)
+            ext.prebid.cache = new PrebidCache(bids: new PrebidCacheSettings(ttlSeconds: randomExp))
+        }
+
+        and: "Default bid response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        and: "Account in the DB"
+        def auctionConfig = new AccountAuctionConfig(
+                videoCacheTtl: PBSUtils.randomNumber,
+                bannerCacheTtl: PBSUtils.randomNumber)
+        def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig))
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsHostAndDefaultCacheTtlService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response should contain exp data"
+        assert response.seatbid.first.bid.first.exp == randomExp
+
+        where:
+        mediaType << [BANNER, VIDEO, NATIVE, AUDIO]
+    }
+
+    def "PBS auction shouldn't resolve bid.exp when the account config and request imp type do not match"() {
+        given: "Default bid request"
+        def bidRequest = BidRequest.getDefaultBidRequest().tap {
+            imp[0] = Imp.getDefaultImpression(mediaType)
+        }
+
+        and: "Default bid response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        and: "Account in the DB"
+        def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: auctionConfig))
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response shouldn't contain exp data"
+        assert !response.seatbid.first.bid.first.exp
+
+        where:
+        mediaType | auctionConfig
+        VIDEO     | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber)
+        VIDEO     | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber, videoCacheTtl: null)
+        BANNER    | new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber)
+        BANNER    | new AccountAuctionConfig(bannerCacheTtl: null, videoCacheTtl: PBSUtils.randomNumber)
+        NATIVE    | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber, videoCacheTtl: PBSUtils.randomNumber)
+        NATIVE    | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber)
+        NATIVE    | new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber)
+        AUDIO     | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber, videoCacheTtl: PBSUtils.randomNumber)
+        AUDIO     | new AccountAuctionConfig(bannerCacheTtl: PBSUtils.randomNumber)
+        AUDIO     | new AccountAuctionConfig(videoCacheTtl: PBSUtils.randomNumber)
+    }
+
+    def "PBS auction shouldn't resolve bid.exp when account config and request imp type match but account config for cache-ttl is not specified"() {
+        given: "Default bid request"
+        def bidRequest = BidRequest.getDefaultBidRequest().tap {
+            enableCache()
+            imp[0] = Imp.getDefaultImpression(mediaType)
+        }
+
+        and: "Default bid response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        and: "Account in the DB"
+        def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: new AccountAuctionConfig(bannerCacheTtl: null, videoCacheTtl: null)))
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response shouldn't contain exp data"
+        assert !response.seatbid.first.bid.first.exp
+
+        where:
+        mediaType << [VIDEO, BANNER, NATIVE, AUDIO]
+    }
+
+    def "PBS auction should resolve bid.exp when account.auction.{banner/video}-cache-ttl and banner bid specified"() {
+        given: "Default bid request"
+        def bidRequest = BidRequest.getDefaultBidRequest().tap {
+            enableCache()
+            imp[0] = Imp.getDefaultImpression(mediaType)
+        }
+
+        and: "Default bid response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        and: "Account in the DB"
+        def account = new Account(uuid: bidRequest.accountId, config: new AccountConfig(auction: accountAuctionConfig))
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsEmptyTtlService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response should contain exp data"
+        assert response.seatbid.first.bid.first.exp == accountCacheTtl
+
+        where:
+        mediaType | accountCacheTtl       | accountAuctionConfig
+        BANNER    | PBSUtils.randomNumber | new AccountAuctionConfig(bannerCacheTtl: accountCacheTtl)
+        VIDEO     | PBSUtils.randomNumber | new AccountAuctionConfig(videoCacheTtl: accountCacheTtl)
+    }
+
+    def "PBS auction should resolve bid.exp when cache.{banner/video}-ttl-seconds config specified"() {
+        given: "Default bid request"
+        def bidRequest = BidRequest.getDefaultBidRequest().tap {
+            imp[0] = Imp.getDefaultImpression(mediaType)
+            enableCache()
+        }
+
+        and: "Set bidder response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = pbsOnlyHostCacheTtlService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response should contain exp data"
+        assert response.seatbid.first.bid.first.exp == expValue
+
+        where:
+        mediaType | expValue
+        BANNER    | BANNER_TTL_HOST_CACHE
+        VIDEO     | VIDEO_TTL_HOST_CACHE
+    }
+
+    def "PBS auction shouldn't resolve bid.exp when cache ttl-seconds is specified for #mediaType mediaType request"() {
+        given: "Default bid request"
+        def bidRequest = BidRequest.getDefaultBidRequest().tap {
+            imp[0] = Imp.getDefaultImpression(mediaType)
+            ext.prebid.cache = new PrebidCache(bids: new PrebidCacheSettings(ttlSeconds: PBSUtils.randomNumber))
+        }
+
+        when: "PBS processes auction request"
+        def response = pbsOnlyHostCacheTtlService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response shouldn't contain exp data"
+        assert !response.seatbid.first.bid.first.exp
+
+        where:
+        mediaType << [NATIVE, AUDIO]
+    }
+
+    def "PBS auction should resolve bid.exp when cache.default-ttl-seconds.{banner,video,audio,native} is specified and no higher-priority fields are present"() {
+        given: "Prebid server with empty host config and default cache ttl config"
+        def config = EMPTY_CACHE_TTL_HOST_CONFIG + DEFAULT_CACHE_TTL_CONFIG
+        def prebidServerService = pbsServiceFactory.getService(config)
+
+        and: "Default bid request"
+        def bidRequest = BidRequest.getDefaultBidRequest().tap {
+            imp[0] = Imp.getDefaultImpression(mediaType)
+        }
+
+        and: "Set bidder response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = prebidServerService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response should contain exp data"
+        assert response.seatbid.first.bid.first.exp == bidExpValue
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(config)
+
+        where:
+        mediaType | bidExpValue
+        BANNER    | BANNER_TTL_DEFAULT_CACHE
+        VIDEO     | VIDEO_TTL_DEFAULT_CACHE
+        AUDIO     | AUDIO_TTL_DEFAULT_CACHE
+        NATIVE    | NATIVE_TTL_DEFAULT_CACHE
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy
index b2cda35dde1..579a8e16597 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/BidValidationSpec.groovy
@@ -18,6 +18,8 @@ import spock.lang.PendingFeature
 import java.time.Instant
 
 import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
+import static org.prebid.server.functional.model.request.auction.DebugCondition.DISABLED
+import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED
 import static org.prebid.server.functional.model.request.auction.DistributionChannel.DOOH
 import static org.prebid.server.functional.util.HttpUtil.REFERER_HEADER
 
@@ -133,7 +135,7 @@ class BidValidationSpec extends BaseSpec {
             dooh.id = null
             dooh.venueType = null
         }
-        bidDoohRequest.ext.prebid.debug = 1
+        bidDoohRequest.ext.prebid.debug = ENABLED
 
         when: "PBS processes auction request"
         defaultPbsService.sendAuctionRequest(bidDoohRequest)
@@ -148,7 +150,7 @@ class BidValidationSpec extends BaseSpec {
         given: "Default basic BidRequest"
         def bidRequest = BidRequest.defaultBidRequest
         bidRequest.site = new Site(id: null, name: PBSUtils.randomString, page: null)
-        bidRequest.ext.prebid.debug = 1
+        bidRequest.ext.prebid.debug = ENABLED
 
         when: "PBS processes auction request"
         defaultPbsService.sendAuctionRequest(bidRequest)
@@ -159,9 +161,9 @@ class BidValidationSpec extends BaseSpec {
     }
 
     def "PBS should treat bids with 0 price as valid when deal id is present"() {
-        given: "Default basic BidRequest with generic bidder"
+        given: "Default basic BidRequest with generic bidder and enabled debug"
         def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.ext.prebid.debug = 1
+        bidRequest.ext.prebid.debug = ENABLED
 
         and: "Bid response with 0 price bid"
         def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
@@ -183,16 +185,21 @@ class BidValidationSpec extends BaseSpec {
     }
 
     def "PBS should drop invalid bid and emit debug error when bid price is #bidPrice and deal id is #dealId"() {
-        given: "Default basic BidRequest with generic bidder"
-        def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.ext.prebid.debug = 1
+        given: "Default basic BidRequest with generic bidder and enabled debug"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            it.ext.prebid.debug = debug
+            it.test = test
+        }
 
         and: "Bid response"
-        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
-        def bid = bidResponse.seatbid.first().bid.first()
-        bid.dealid = dealId
-        bid.price = bidPrice
-        def bidId = bid.id
+        def bidId = PBSUtils.randomString
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            it.seatbid.first.bid.first.tap {
+                id = bidId
+                dealid = dealId
+                price = bidPrice
+            }
+        }
 
         and: "Set bidder response"
         bidder.setResponse(bidRequest.id, bidResponse)
@@ -201,13 +208,61 @@ class BidValidationSpec extends BaseSpec {
         def response = defaultPbsService.sendAuctionRequest(bidRequest)
 
         then: "Invalid bid should be deleted"
-        assert response.seatbid.size() == 0
+        assert !response.seatbid
+        assert !response.ext.seatnonbid
 
         and: "PBS should emit an error"
         assert response.ext?.warnings[ErrorType.PREBID]*.code == [999]
         assert response.ext?.warnings[ErrorType.PREBID]*.message ==
                 ["Dropped bid '$bidId'. Does not contain a positive (or zero if there is a deal) 'price'" as String]
 
+        where:
+        debug    | test     | bidPrice                      | dealId
+        DISABLED | ENABLED  | PBSUtils.randomNegativeNumber | null
+        DISABLED | ENABLED  | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber
+        DISABLED | ENABLED  | 0                             | null
+        DISABLED | ENABLED  | null                          | PBSUtils.randomNumber
+        DISABLED | ENABLED  | null                          | null
+        ENABLED  | DISABLED | PBSUtils.randomNegativeNumber | null
+        ENABLED  | DISABLED | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber
+        ENABLED  | DISABLED | 0                             | null
+        ENABLED  | DISABLED | null                          | PBSUtils.randomNumber
+        ENABLED  | DISABLED | null                          | null
+    }
+
+    def "PBS should drop invalid bid without debug error when request debug disabled and bid price is #bidPrice and deal id is #dealId"() {
+        given: "Default basic BidRequest with generic bidder"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            test = DISABLED
+            ext.prebid.debug = DISABLED
+        }
+
+        and: "Bid response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            it.seatbid.first.bid.first.tap {
+                dealid = dealId
+                price = bidPrice
+            }
+        }
+
+        and: "Set bidder response"
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+
+        then: "Invalid bid should be deleted"
+        assert !response.seatbid
+        assert !response.ext.seatnonbid
+
+        and: "PBS shouldn't emit an error"
+        assert !response.ext?.warnings
+        assert !response.ext?.warnings
+
+        and: "PBS should call bidder"
+        def bidderRequests = bidder.getBidderRequests(bidResponse.id)
+        assert bidderRequests.size() == 1
+
         where:
         bidPrice                      | dealId
         PBSUtils.randomNegativeNumber | null
@@ -220,7 +275,7 @@ class BidValidationSpec extends BaseSpec {
     def "PBS should only drop invalid bid without discarding whole seat"() {
         given: "Default basic  BidRequest with generic bidder"
         def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.ext.prebid.debug = 1
+        bidRequest.ext.prebid.debug = ENABLED
         bidRequest.ext.prebid.multibid = [new MultiBid(bidder: GENERIC, maxBids: 2)]
 
         and: "Bid response with 2 bids"
@@ -239,7 +294,7 @@ class BidValidationSpec extends BaseSpec {
         when: "PBS processes auction request"
         def response = defaultPbsService.sendAuctionRequest(bidRequest)
 
-        then: "Invalid bids should be deleted"
+        then: "Bid response contains only valid bid"
         assert response.seatbid?.first()?.bid*.id == [validBidId]
 
         and: "PBS should emit an error"
@@ -247,6 +302,53 @@ class BidValidationSpec extends BaseSpec {
         assert response.ext?.warnings[ErrorType.PREBID]*.message ==
                 ["Dropped bid '$invalidBid.id'. Does not contain a positive (or zero if there is a deal) 'price'" as String]
 
+        where:
+        debug | test | bidPrice                      | dealId
+        0     | 1    | PBSUtils.randomNegativeNumber | null
+        0     | 1    | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber
+        0     | 1    | 0                             | null
+        0     | 1    | null                          | PBSUtils.randomNumber
+        0     | 1    | null                          | null
+        1     | 0    | PBSUtils.randomNegativeNumber | null
+        1     | 0    | PBSUtils.randomNegativeNumber | PBSUtils.randomNumber
+        1     | 0    | 0                             | null
+        1     | 0    | null                          | PBSUtils.randomNumber
+        1     | 0    | null                          | null
+    }
+
+    def "PBS should only drop invalid bid without discarding whole seat without debug error when request debug disabled "() {
+        given: "Default basic  BidRequest with generic bidder"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            test = DISABLED
+            ext.prebid.tap {
+                debug = DISABLED
+                multibid = [new MultiBid(bidder: GENERIC, maxBids: 2)]
+            }
+        }
+
+        and: "Bid response with 2 bids"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidResponse.seatbid[0].bid << Bid.getDefaultBid(bidRequest.imp.first())
+
+        and: "One of the bids is invalid"
+        def invalidBid = bidResponse.seatbid.first().bid.first()
+        invalidBid.dealid = dealId
+        invalidBid.price = bidPrice
+        def validBidId = bidResponse.seatbid.first().bid.last().id
+
+        and: "Set bidder response"
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+
+        then: "Bid response contains only valid bid"
+        assert response.seatbid?.first()?.bid*.id == [validBidId]
+
+        and: "PBS shouldn't emit an error"
+        assert !response.ext?.warnings
+        assert !response.ext?.warnings
+
         where:
         bidPrice                      | dealId
         PBSUtils.randomNegativeNumber | null
@@ -257,10 +359,7 @@ class BidValidationSpec extends BaseSpec {
     }
 
     def "PBS should update 'adapter.generic.requests.bid_validation' metric when bid validation error appears"() {
-        given: "Initial 'adapter.generic.requests.bid_validation' metric value"
-        def initialMetricValue = getCurrentMetricValue(defaultPbsService, "adapter.generic.requests.bid_validation")
-
-        and: "Bid request"
+        given: "Bid request"
         def bidRequest = BidRequest.defaultBidRequest
 
         and: "Set invalid bid response"
diff --git a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy
index ccd5f8b9cf0..4f8dcf7675e 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/CacheSpec.groovy
@@ -19,6 +19,8 @@ import static org.prebid.server.functional.model.response.auction.MediaType.VIDE
 
 class CacheSpec extends BaseSpec {
 
+    private final static String PBS_API_HEADER = 'x-pbc-api-key'
+
     def "PBS should update prebid_cache.creative_size.xml metric when xml creative is received"() {
         given: "Current value of metric prebid_cache.requests.ok"
         def initialValue = getCurrentMetricValue(defaultPbsService, "prebid_cache.requests.ok")
@@ -87,6 +89,51 @@ class CacheSpec extends BaseSpec {
 
         then: "PBS should call PBC"
         assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1
+
+        and: "PBS call shouldn't include api-key"
+        assert !prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER]
+    }
+
+    def "PBS should cache bids without api-key header when targeting is specified and api-key-secured disabled"() {
+        given: "Pbs config with disabled api-key-secured and pbc.api.key"
+        def apiKey = PBSUtils.randomString
+        def pbsService = pbsServiceFactory.getService(['pbc.api.key': apiKey, 'cache.api-key-secured': 'false'])
+
+        and: "Default BidRequest with cache, targeting"
+        def bidRequest = BidRequest.defaultBidRequest
+        bidRequest.enableCache()
+        bidRequest.ext.prebid.targeting = new Targeting()
+
+        when: "PBS processes auction request"
+        pbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should call PBC"
+        prebidCache.getRequest()
+        assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1
+
+        and: "PBS call shouldn't include api-key"
+        assert !prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER]
+    }
+
+    def "PBS should cache bids with api-key header when targeting is specified and api-key-secured enabled"() {
+        given: "Pbs config with api-key-secured and pbc.api.key"
+        def apiKey = PBSUtils.randomString
+        def pbsService = pbsServiceFactory.getService(['pbc.api.key': apiKey, 'cache.api-key-secured': 'true'])
+
+        and: "Default BidRequest with cache, targeting"
+        def bidRequest = BidRequest.defaultBidRequest
+        bidRequest.enableCache()
+        bidRequest.ext.prebid.targeting = new Targeting()
+
+        when: "PBS processes auction request"
+        pbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should call PBC"
+        prebidCache.getRequest()
+        assert prebidCache.getRequestCount(bidRequest.imp[0].id) == 1
+
+        and: "PBS call should include api-key"
+        assert prebidCache.getRequestHeaders(bidRequest.imp[0].id)[PBS_API_HEADER] == [apiKey]
     }
 
     def "PBS should not cache bids when targeting isn't specified"() {
diff --git a/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy
index 3f7682eb708..ea169ad00ee 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/DebugSpec.groovy
@@ -3,41 +3,63 @@ package org.prebid.server.functional.tests
 import org.apache.commons.lang3.StringUtils
 import org.prebid.server.functional.model.config.AccountAuctionConfig
 import org.prebid.server.functional.model.config.AccountConfig
+import org.prebid.server.functional.model.config.AccountMetricsConfig
 import org.prebid.server.functional.model.db.Account
 import org.prebid.server.functional.model.db.StoredRequest
 import org.prebid.server.functional.model.db.StoredResponse
 import org.prebid.server.functional.model.request.amp.AmpRequest
 import org.prebid.server.functional.model.request.auction.BidRequest
+import org.prebid.server.functional.model.request.auction.Site
 import org.prebid.server.functional.model.request.auction.StoredBidResponse
 import org.prebid.server.functional.model.response.auction.BidResponse
 import org.prebid.server.functional.model.response.auction.ErrorType
+import org.prebid.server.functional.service.PrebidServerException
 import org.prebid.server.functional.util.PBSUtils
 import spock.lang.PendingFeature
 
 import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
+import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.BASIC
+import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.DETAILED
+import static org.prebid.server.functional.model.config.AccountMetricsVerbosityLevel.NONE
+import static org.prebid.server.functional.model.request.auction.DebugCondition.DISABLED
+import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED
 import static org.prebid.server.functional.model.response.auction.BidderCallType.STORED_BID_RESPONSE
 
 class DebugSpec extends BaseSpec {
 
     private static final String overrideToken = PBSUtils.randomString
+    private static final String ACCOUNT_METRICS_PREFIX_NAME = "account"
+    private static final String DEBUG_REQUESTS_METRIC = "debug_requests"
+    private static final String ACCOUNT_DEBUG_REQUESTS_METRIC = "account.%s.debug_requests"
+    private static final String REQUEST_OK_WEB_METRICS = "requests.ok.openrtb2-web"
 
-    def "PBS should return debug information when debug flag is #debug and test flag is #test"() {
+    def "PBS should return debug information and emit metrics when debug flag is #debug and test flag is #test"() {
         given: "Default BidRequest with test flag"
         def bidRequest = BidRequest.defaultBidRequest
         bidRequest.ext.prebid.debug = debug
         bidRequest.test = test
 
+        and: "Flash metrics"
+        flushMetrics(defaultPbsService)
+
         when: "PBS processes auction request"
         def response = defaultPbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain ext.debug"
         assert response.ext?.debug
 
+        and: "Debug metrics should be incremented"
+        def metricsRequest = defaultPbsService.sendCollectedMetricsRequest()
+        assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1
+
+        and: "Account debug metrics shouldn't be incremented"
+        assert !metricsRequest.keySet().contains(ACCOUNT_METRICS_PREFIX_NAME)
+
         where:
-        debug | test
-        1     | null
-        1     | 0
-        null  | 1
+        debug   | test
+        ENABLED | null
+        ENABLED | DISABLED
+        null    | ENABLED
     }
 
     def "PBS shouldn't return debug information when debug flag is #debug and test flag is #test"() {
@@ -46,16 +68,27 @@ class DebugSpec extends BaseSpec {
         bidRequest.ext.prebid.debug = test
         bidRequest.test = test
 
+        and: "Flash metrics"
+        flushMetrics(defaultPbsService)
+
         when: "PBS processes auction request"
         def response = defaultPbsService.sendAuctionRequest(bidRequest)
 
         then: "Response shouldn't contain ext.debug"
         assert !response.ext?.debug
 
+        and: "Debug metrics shouldn't be populated"
+        def metricsRequest = defaultPbsService.sendCollectedMetricsRequest()
+        assert !metricsRequest[DEBUG_REQUESTS_METRIC]
+        assert !metricsRequest.keySet().contains(ACCOUNT_METRICS_PREFIX_NAME)
+
+        and: "General metrics should be present"
+        assert metricsRequest[REQUEST_OK_WEB_METRICS] == 1
+
         where:
-        debug | test
-        0     | null
-        null  | 0
+        debug    | test
+        DISABLED | null
+        null     | DISABLED
     }
 
     def "PBS should not return debug information when bidder-level setting debug.allowed = false"() {
@@ -64,7 +97,7 @@ class DebugSpec extends BaseSpec {
 
         and: "Default basic generic BidRequest"
         def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.ext.prebid.debug = 1
+        bidRequest.ext.prebid.debug = ENABLED
 
         when: "PBS processes auction request"
         def response = pbsService.sendAuctionRequest(bidRequest)
@@ -84,7 +117,7 @@ class DebugSpec extends BaseSpec {
 
         and: "Default basic generic BidRequest"
         def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.ext.prebid.debug = 1
+        bidRequest.ext.prebid.debug = ENABLED
 
         when: "PBS processes auction request"
         def response = pbsService.sendAuctionRequest(bidRequest)
@@ -102,7 +135,7 @@ class DebugSpec extends BaseSpec {
 
         and: "Default basic generic BidRequest"
         def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.ext.prebid.debug = 1
+        bidRequest.ext.prebid.debug = ENABLED
 
         and: "Account in the DB"
         def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig)
@@ -132,7 +165,7 @@ class DebugSpec extends BaseSpec {
 
         and: "Default basic generic BidRequest"
         def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.ext.prebid.debug = 1
+        bidRequest.ext.prebid.debug = ENABLED
 
         and: "Account in the DB"
         def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig)
@@ -161,7 +194,7 @@ class DebugSpec extends BaseSpec {
 
         and: "Default basic generic BidRequest"
         def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.ext.prebid.debug = 1
+        bidRequest.ext.prebid.debug = ENABLED
 
         and: "Account in the DB"
         def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: false))
@@ -183,7 +216,7 @@ class DebugSpec extends BaseSpec {
     def "PBS should use default values = true for bidder-level setting debug.allow and account-level setting debug-allowed when they are not specified"() {
         given: "Default basic generic BidRequest"
         def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.ext.prebid.debug = 1
+        bidRequest.ext.prebid.debug = ENABLED
 
         when: "PBS processes auction request"
         def response = defaultPbsService.sendAuctionRequest(bidRequest)
@@ -201,7 +234,7 @@ class DebugSpec extends BaseSpec {
 
         and: "Default basic generic BidRequest"
         def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.ext.prebid.debug = 1
+        bidRequest.ext.prebid.debug = ENABLED
 
         and: "Account in the DB"
         def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: debugAllowedAccount))
@@ -233,7 +266,7 @@ class DebugSpec extends BaseSpec {
 
         and: "Default basic generic BidRequest"
         def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.ext.prebid.debug = 1
+        bidRequest.ext.prebid.debug = ENABLED
 
         and: "Account in the DB"
         def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(debugAllow: false))
@@ -278,11 +311,11 @@ class DebugSpec extends BaseSpec {
         assert response.ext?.debug
 
         where:
-        requestDebug || storedRequestDebug
-        1            || 0
-        1            || 1
-        1            || null
-        null         || 1
+        requestDebug | storedRequestDebug
+        ENABLED      | DISABLED
+        ENABLED      | ENABLED
+        ENABLED      | null
+        null         | ENABLED
     }
 
     def "PBS AMP shouldn't return debug information when request flag is #requestDebug and stored request flag is #storedRequestDebug"() {
@@ -307,12 +340,12 @@ class DebugSpec extends BaseSpec {
         assert !response.ext?.debug
 
         where:
-        requestDebug || storedRequestDebug
-        0            || 1
-        0            || 0
-        0            || null
-        null         || 0
-        null         || null
+        requestDebug | storedRequestDebug
+        DISABLED     | ENABLED
+        DISABLED     | DISABLED
+        DISABLED     | null
+        null         | DISABLED
+        null         | null
     }
 
     def "PBS shouldn't populate call type when it's default bidder call"() {
@@ -349,4 +382,157 @@ class DebugSpec extends BaseSpec {
         and: "Response should not contain ext.warnings"
         assert !response.ext?.warnings
     }
+
+    def "PBS should return debug information and emit metrics when account debug enabled and verbosity detailed"() {
+        given: "Default basic generic bid request"
+        def bidRequest = BidRequest.defaultBidRequest
+
+        and: "Account in the DB"
+        def accountConfig = new AccountConfig(
+                metrics: new AccountMetricsConfig(verbosityLevel: DETAILED),
+                auction: new AccountAuctionConfig(debugAllow: true))
+        def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flash metrics"
+        flushMetrics(defaultPbsService)
+
+        when: "PBS processes auction request"
+        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+
+        then: "Response should contain ext.debug"
+        assert response.ext?.debug
+
+        and: "Debug metrics should be incremented"
+        def metricsRequest = defaultPbsService.sendCollectedMetricsRequest()
+        assert metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(bidRequest.accountId)] == 1
+        assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1
+    }
+
+    def "PBS shouldn't return debug information and emit metrics when account debug enabled and verbosity #verbosityLevel"() {
+        given: "Default basic generic bid request"
+        def bidRequest = BidRequest.defaultBidRequest
+
+        and: "Account in the DB"
+        def accountConfig = new AccountConfig(
+                metrics: new AccountMetricsConfig(verbosityLevel: verbosityLevel),
+                auction: new AccountAuctionConfig(debugAllow: true))
+        def account = new Account(uuid: bidRequest.site.publisher.id, config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flash metrics"
+        flushMetrics(defaultPbsService)
+
+        when: "PBS processes auction request"
+        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+
+        then: "Response should contain ext.debug"
+        assert response.ext?.debug
+
+        and: "Account debug metrics shouldn't be incremented"
+        def metricsRequest = defaultPbsService.sendCollectedMetricsRequest()
+        assert !metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(bidRequest.accountId)]
+
+        and: "Request debug metrics should be incremented"
+        assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1
+
+        where:
+        verbosityLevel << [NONE, BASIC]
+    }
+
+    def "PBS amp should return debug information and emit metrics when account debug enabled and verbosity detailed"() {
+        given: "Default AMP request"
+        def ampRequest = AmpRequest.defaultAmpRequest
+
+        and: "Default stored request"
+        def ampStoredRequest = BidRequest.defaultStoredRequest
+
+        and: "Account in the DB"
+        def accountConfig = new AccountConfig(
+                metrics: new AccountMetricsConfig(verbosityLevel: DETAILED),
+                auction: new AccountAuctionConfig(debugAllow: true))
+        def account = new Account(uuid: ampRequest.account, config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flash metrics"
+        flushMetrics(defaultPbsService)
+
+        and: "Save storedRequest into DB"
+        def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+        storedRequestDao.save(storedRequest)
+
+        when: "PBS processes amp request"
+        def response = defaultPbsService.sendAmpRequest(ampRequest)
+
+        then: "Response should contain ext.debug"
+        assert response.ext?.debug
+
+        and: "Debug metrics should be incremented"
+        def metricsRequest = defaultPbsService.sendCollectedMetricsRequest()
+        assert metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(ampRequest.account)] == 1
+        assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1
+    }
+
+    def "PBS amp should return debug information and emit metrics when account debug enabled and verbosity #verbosityLevel"() {
+        given: "Default AMP request"
+        def ampRequest = AmpRequest.defaultAmpRequest
+
+        and: "Default stored request"
+        def ampStoredRequest = BidRequest.defaultStoredRequest
+
+        and: "Account in the DB"
+        def accountConfig = new AccountConfig(
+                metrics: new AccountMetricsConfig(verbosityLevel: verbosityLevel),
+                auction: new AccountAuctionConfig(debugAllow: true))
+        def account = new Account(uuid: ampRequest.account, config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flash metrics"
+        flushMetrics(defaultPbsService)
+
+        and: "Save storedRequest into DB"
+        def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+        storedRequestDao.save(storedRequest)
+
+        when: "PBS processes amp request"
+        def response = defaultPbsService.sendAmpRequest(ampRequest)
+
+        then: "Response should contain ext.debug"
+        assert response.ext?.debug
+
+        and: "Account debug metrics shouldn't be incremented"
+        def metricsRequest = defaultPbsService.sendCollectedMetricsRequest()
+        assert !metricsRequest[ACCOUNT_DEBUG_REQUESTS_METRIC.formatted(ampRequest.account)]
+
+        and: "Debug metrics should be incremented"
+        assert metricsRequest[DEBUG_REQUESTS_METRIC] == 1
+
+        where:
+        verbosityLevel << [NONE, BASIC]
+    }
+
+    def "PBS shouldn't emit auction request metric when incoming request invalid"() {
+        given: "Default basic BidRequest"
+        def bidRequest = BidRequest.defaultBidRequest
+        bidRequest.site = new Site(id: null, name: PBSUtils.randomString, page: null)
+        bidRequest.ext.prebid.debug = ENABLED
+
+        and: "Flash metrics"
+        flushMetrics(defaultPbsService)
+
+        when: "PBS processes auction request"
+        defaultPbsService.sendAuctionRequest(bidRequest)
+
+        then: "Request should fail with error"
+        def exception = thrown(PrebidServerException)
+        assert exception.responseBody.contains("request.site should include at least one of request.site.id or request.site.page")
+
+        and: "Debug metrics shouldn't be populated"
+        def metricsRequest = defaultPbsService.sendCollectedMetricsRequest()
+        assert !metricsRequest[DEBUG_REQUESTS_METRIC]
+        assert !metricsRequest.keySet().contains(ACCOUNT_METRICS_PREFIX_NAME)
+
+        and: "General metrics shouldn't be present"
+        assert !metricsRequest[REQUEST_OK_WEB_METRICS]
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy
index 085bd19a690..4cc3b728449 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/ImpRequestSpec.groovy
@@ -1,5 +1,6 @@
 package org.prebid.server.functional.tests
 
+import org.prebid.server.functional.model.bidder.Generic
 import org.prebid.server.functional.model.bidder.Openx
 import org.prebid.server.functional.model.db.StoredImp
 import org.prebid.server.functional.model.request.auction.BidRequest
@@ -103,8 +104,10 @@ class ImpRequestSpec extends BaseSpec {
             imp.first.tap {
                 pmp = Pmp.defaultPmp
                 ext.prebid.imp = [(aliasName): new Imp(pmp: extPmp)]
+                ext.prebid.bidder.generic = null
+                ext.prebid.bidder.alias = new Generic()
             }
-            ext.prebid.aliases = [(aliasName.value): GENERIC]
+            ext.prebid.aliases = [(aliasName.value): bidderName]
         }
 
         when: "Requesting PBS auction"
diff --git a/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy
index 7acfb56d2ca..f6b02f22e76 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/SeatNonBidSpec.groovy
@@ -23,6 +23,8 @@ import static org.mockserver.model.HttpStatusCode.PROCESSING_102
 import static org.mockserver.model.HttpStatusCode.SERVICE_UNAVAILABLE_503
 import static org.prebid.server.functional.model.AccountStatus.ACTIVE
 import static org.prebid.server.functional.model.config.BidValidationEnforcement.ENFORCE
+import static org.prebid.server.functional.model.request.auction.DebugCondition.DISABLED
+import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED
 import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE
 import static org.prebid.server.functional.model.request.auction.SecurityLevel.SECURE
 import static org.prebid.server.functional.model.response.auction.BidRejectionReason.ERROR_BIDDER_UNREACHABLE
@@ -170,6 +172,9 @@ class SeatNonBidSpec extends BaseSpec {
         assert seatNonBid.seat == GENERIC.value
         assert seatNonBid.nonBid[0].impId == bidRequest.imp[0].id
         assert seatNonBid.nonBid[0].statusCode == RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE
+
+        and: "PBS response shouldn't contain seatBid"
+        assert !response.seatbid
     }
 
     def "PBS shouldn't populate seatNonBid when returnAllBidStatus=true and bidder successfully bids"() {
@@ -219,7 +224,7 @@ class SeatNonBidSpec extends BaseSpec {
         assert !response.seatbid
 
         where:
-        debug << [1, 0, null]
+        debug << [ENABLED, DISABLED, null]
     }
 
     def "PBS shouldn't populate seatNonBid when returnAllBidStatus=false and debug=#debug and requested bidder didn't bid for any reason"() {
@@ -245,7 +250,7 @@ class SeatNonBidSpec extends BaseSpec {
         assert !response.seatbid
 
         where:
-        debug << [1, 0, null]
+        debug << [ENABLED, DISABLED, null]
     }
 
     def "PBS should populate seatNonBid when bidder is rejected due to timeout"() {
diff --git a/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy
index b9e310c2b26..1e33542e564 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/SetUidSpec.groovy
@@ -12,10 +12,16 @@ import spock.lang.Shared
 import java.time.Clock
 import java.time.ZonedDateTime
 
+import static org.prebid.server.functional.model.bidder.BidderName.ALIAS
+import static org.prebid.server.functional.model.bidder.BidderName.ALIAS_CAMEL_CASE
 import static org.prebid.server.functional.model.bidder.BidderName.APPNEXUS
+import static org.prebid.server.functional.model.bidder.BidderName.EMPTY
 import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
+import static org.prebid.server.functional.model.bidder.BidderName.GENERIC_CAMEL_CASE
 import static org.prebid.server.functional.model.bidder.BidderName.OPENX
 import static org.prebid.server.functional.model.bidder.BidderName.RUBICON
+import static org.prebid.server.functional.model.bidder.BidderName.UNKNOWN
+import static org.prebid.server.functional.model.bidder.BidderName.WILDCARD
 import static org.prebid.server.functional.model.request.setuid.UidWithExpiry.defaultUidWithExpiry
 import static org.prebid.server.functional.model.response.cookiesync.UserSyncInfo.Type.REDIRECT
 import static org.prebid.server.functional.testcontainers.Dependencies.networkServiceContainer
@@ -37,9 +43,13 @@ class SetUidSpec extends BaseSpec {
              "adapters.${APPNEXUS.value}.usersync.cookie-family-name"                 : APPNEXUS.value,
              "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url"         : USER_SYNC_URL,
              "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()]
+    private static final Map<String, String> GENERIC_ALIAS_CONFIG = ["adapters.generic.aliases.alias.enabled" : "true",
+                                                                       "adapters.generic.aliases.alias.endpoint": "$networkServiceContainer.rootUri/auction".toString()]
+    private static final String TCF_ERROR_MESSAGE = "The gdpr_consent param prevents cookies from being saved"
+    private static final int UNAVAILABLE_FOR_LEGAL_REASONS_CODE = 451
 
     @Shared
-    PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG)
+    PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GENERIC_ALIAS_CONFIG)
 
     def "PBS should set uids cookie"() {
         given: "Default SetuidRequest"
@@ -75,8 +85,8 @@ class SetUidSpec extends BaseSpec {
 
     def "PBS setuid should return requested uids cookie when priority bidder not present in config"() {
         given: "PBS config"
-        def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG +
-                ["cookie-sync.pri": null])
+        def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": null]
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
 
         and: "Setuid request"
         def request = SetuidRequest.defaultSetuidRequest.tap {
@@ -92,13 +102,16 @@ class SetUidSpec extends BaseSpec {
         then: "Response should contain requested uids"
         assert response.uidsCookie.tempUIDs[GENERIC]
         assert response.uidsCookie.tempUIDs[RUBICON]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
     }
 
     def "PBS setuid should return prioritized uids bidder when size is full"() {
         given: "PBS config"
         def genericBidder = GENERIC
-        def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG +
-                ["cookie-sync.pri": genericBidder.value])
+        def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": genericBidder.value]
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
 
         and: "Setuid request"
         def request = SetuidRequest.defaultSetuidRequest.tap {
@@ -117,12 +130,15 @@ class SetUidSpec extends BaseSpec {
         then: "Response should contain uids cookies"
         assert response.uidsCookie.tempUIDs[rubiconBidder]
         assert response.uidsCookie.tempUIDs[genericBidder]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
     }
 
     def "PBS setuid should remove earliest expiration bidder when size is full"() {
         given: "PBS config"
-        def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG +
-                ["cookie-sync.pri": GENERIC.value])
+        def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": GENERIC.value]
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
 
         and: "Setuid request"
         def request = SetuidRequest.defaultSetuidRequest.tap {
@@ -145,12 +161,15 @@ class SetUidSpec extends BaseSpec {
         then: "Response should contain uids cookies"
         assert response.uidsCookie.tempUIDs[APPNEXUS]
         assert response.uidsCookie.tempUIDs[GENERIC]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
     }
 
     def "PBS setuid should ignore requested bidder and log metric when cookie's filled and requested bidder not in prioritize list"() {
         given: "PBS config"
-        def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG +
-                ["cookie-sync.pri": APPNEXUS.value])
+        def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": APPNEXUS.value]
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
 
         and: "Setuid request"
         def bidderName = GENERIC
@@ -174,14 +193,17 @@ class SetUidSpec extends BaseSpec {
         and: "Response should contain uids cookies"
         assert response.uidsCookie.tempUIDs[APPNEXUS]
         assert response.uidsCookie.tempUIDs[RUBICON]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
     }
 
     def "PBS setuid should reject bidder when cookie's filled and requested bidder in pri and rejected by tcf"() {
         given: "Setuid request"
         def bidderName = RUBICON
-        def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG
-                + ["gdpr.host-vendor-id": RUBICON_VENDOR_ID.toString(),
-                   "cookie-sync.pri"    : bidderName.value])
+        def pbsConfig = PBS_CONFIG + ["gdpr.host-vendor-id": RUBICON_VENDOR_ID.toString(),
+                                      "cookie-sync.pri"    : bidderName.value]
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
 
         def request = SetuidRequest.defaultSetuidRequest.tap {
             it.bidder = bidderName
@@ -199,18 +221,21 @@ class SetUidSpec extends BaseSpec {
 
         then: "Request should fail with error"
         def exception = thrown(PrebidServerException)
-        assert exception.statusCode == 451
-        assert exception.responseBody == "The gdpr_consent param prevents cookies from being saved"
+        assert exception.statusCode == UNAVAILABLE_FOR_LEGAL_REASONS_CODE
+        assert exception.responseBody == TCF_ERROR_MESSAGE
 
         and: "usersync.FAMILY.tcf.blocked metric should be updated"
         def metric = prebidServerService.sendCollectedMetricsRequest()
         assert metric["usersync.${bidderName.value}.tcf.blocked"] == 1
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
     }
 
     def "PBS setuid should remove oldest uid and log metric when cookie's filled and oldest uid's not on the pri"() {
         given: "PBS config"
-        def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG +
-                ["cookie-sync.pri": GENERIC.value])
+        def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": GENERIC.value]
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
 
         and: "Flush metrics"
         flushMetrics(prebidServerService)
@@ -239,12 +264,15 @@ class SetUidSpec extends BaseSpec {
         then: "Response should contain uids cookies"
         assert response.uidsCookie.tempUIDs[APPNEXUS]
         assert response.uidsCookie.tempUIDs[GENERIC]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
     }
 
     def "PBS SetUid should remove oldest bidder from uids cookie in favor of prioritized bidder"() {
         given: "PBS config"
-        def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG +
-                ["cookie-sync.pri": "$OPENX.value, $GENERIC.value" as String])
+        def pbsConfig = PBS_CONFIG + ["cookie-sync.pri": "$OPENX.value, $GENERIC.value" as String]
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
 
         and: "Set uid request"
         def request = SetuidRequest.defaultSetuidRequest.tap {
@@ -279,5 +307,26 @@ class SetUidSpec extends BaseSpec {
 
         and: "usersync.FAMILY.sets metric should be updated"
         assert metricsRequest["usersync.${OPENX.value}.sets"] == 1
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS  setuid should reject request when requested bidder mismatching with cookie-family-name"() {
+        given: "Default SetuidRequest"
+        def request = SetuidRequest.getDefaultSetuidRequest().tap {
+            it.bidder = bidderName
+        }
+
+        when: "PBS processes setuid request"
+        prebidServerService.sendSetUidRequest(request, UidsCookie.defaultUidsCookie)
+
+        then: "Request should fail with error"
+        def exception = thrown(PrebidServerException)
+        assert exception.statusCode == 400
+        assert exception.responseBody == 'Invalid request format: "bidder" query param is invalid'
+
+        where:
+        bidderName << [UNKNOWN, WILDCARD, GENERIC_CAMEL_CASE, ALIAS, ALIAS_CAMEL_CASE]
     }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy
index fccb14c8bab..767c4b8e544 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/StoredResponseSpec.groovy
@@ -10,6 +10,7 @@ import org.prebid.server.functional.model.response.auction.BidResponse
 import org.prebid.server.functional.model.response.auction.ErrorType
 import org.prebid.server.functional.model.response.auction.SeatBid
 import org.prebid.server.functional.service.PrebidServerException
+import org.prebid.server.functional.service.PrebidServerService
 import org.prebid.server.functional.util.PBSUtils
 import spock.lang.PendingFeature
 
@@ -17,6 +18,8 @@ import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
 
 class StoredResponseSpec extends BaseSpec {
 
+    private final PrebidServerService pbsService = pbsServiceFactory.getService(["cache.default-ttl-seconds.banner": ""])
+
     @PendingFeature
     def "PBS should not fail auction with storedAuctionResponse when request bidder params doesn't satisfy json-schema"() {
         given: "BidRequest with bad bidder datatype and storedAuctionResponse"
@@ -33,7 +36,7 @@ class StoredResponseSpec extends BaseSpec {
         storedResponseDao.save(storedResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should not contain errors and warnings"
         assert !response.ext?.errors
@@ -56,7 +59,7 @@ class StoredResponseSpec extends BaseSpec {
         storedResponseDao.save(storedResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain information from stored auction response"
         assert response.id == bidRequest.id
@@ -82,7 +85,7 @@ class StoredResponseSpec extends BaseSpec {
         storedResponseDao.save(storedResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain information from stored bid response"
         assert response.id == bidRequest.id
@@ -111,7 +114,7 @@ class StoredResponseSpec extends BaseSpec {
         storedResponseDao.save(storedResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain information from stored bid response and change bid.impId on imp.id"
         assert response.id == bidRequest.id
@@ -140,7 +143,7 @@ class StoredResponseSpec extends BaseSpec {
         storedResponseDao.save(storedResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain warning information"
         assert response.ext?.warnings[ErrorType.PREBID]*.code == [999]
@@ -161,7 +164,7 @@ class StoredResponseSpec extends BaseSpec {
         }
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain same stored auction response as requested"
         assert response.seatbid == [storedAuctionResponse]
@@ -190,7 +193,7 @@ class StoredResponseSpec extends BaseSpec {
         storedResponseDao.save(storedResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain same stored auction response as requested"
         assert response.seatbid == [storedAuctionResponse]
@@ -214,7 +217,7 @@ class StoredResponseSpec extends BaseSpec {
         storedResponseDao.save(storedResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain same stored auction response as requested"
         assert response.seatbid
@@ -244,7 +247,7 @@ class StoredResponseSpec extends BaseSpec {
         storedResponseDao.save(storedResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain warning information"
         assert response.ext?.warnings[ErrorType.PREBID]*.message.contains('SeatBid can\'t be null in stored response')
@@ -257,10 +260,10 @@ class StoredResponseSpec extends BaseSpec {
         given: "Default basic BidRequest with stored response"
         def bidRequest = BidRequest.defaultBidRequest
         def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest)
-        bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject:  storedAuctionResponse)
+        bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: storedAuctionResponse)
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain same stored auction response as requested"
         assert convertToComparableSeatBid(response.seatbid) == [storedAuctionResponse]
@@ -272,10 +275,10 @@ class StoredResponseSpec extends BaseSpec {
     def "PBS should throw error when imp.ext.prebid.storedBidResponse.seatbidobj is with empty seatbid"() {
         given: "Default basic BidRequest with empty stored response"
         def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject:  new SeatBid())
+        bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: new SeatBid())
 
         when: "PBS processes auction request"
-        defaultPbsService.sendAuctionRequest(bidRequest)
+        pbsService.sendAuctionRequest(bidRequest)
 
         then: "PBS throws an exception"
         def exception = thrown(PrebidServerException)
@@ -289,10 +292,10 @@ class StoredResponseSpec extends BaseSpec {
     def "PBS should throw error when imp.ext.prebid.storedBidResponse.seatbidobj is with empty bids"() {
         given: "Default basic BidRequest with empty bids for stored response"
         def bidRequest = BidRequest.defaultBidRequest
-        bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject:  new SeatBid(bid: [], seat: GENERIC))
+        bidRequest.imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse(seatBidObject: new SeatBid(bid: [], seat: GENERIC))
 
         when: "PBS processes auction request"
-        defaultPbsService.sendAuctionRequest(bidRequest)
+        pbsService.sendAuctionRequest(bidRequest)
 
         then: "PBS throws an exception"
         def exception = thrown(PrebidServerException)
@@ -313,7 +316,7 @@ class StoredResponseSpec extends BaseSpec {
         }
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain same stored auction response as requested"
         assert convertToComparableSeatBid(response.seatbid) == [storedAuctionResponse]
@@ -329,7 +332,7 @@ class StoredResponseSpec extends BaseSpec {
         }
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain same stored auction response bids as requested"
         assert convertToComparableSeatBid(response.seatbid).bid.flatten().sort() ==
@@ -343,7 +346,7 @@ class StoredResponseSpec extends BaseSpec {
         given: "Default basic BidRequest with stored response"
         def bidRequest = BidRequest.defaultBidRequest
         def storedAuctionResponse = SeatBid.getStoredResponse(bidRequest)
-        bidRequest.tap{
+        bidRequest.tap {
             imp[0].ext.prebid.storedAuctionResponse = new StoredAuctionResponse().tap {
                 seatBidObject = SeatBid.getStoredResponse(bidRequest)
             }
@@ -351,7 +354,7 @@ class StoredResponseSpec extends BaseSpec {
         }
 
         when: "PBS processes auction request"
-        def response = defaultPbsService.sendAuctionRequest(bidRequest)
+        def response = pbsService.sendAuctionRequest(bidRequest)
 
         then: "Response should contain same stored auction response as requested"
         assert response.seatbid == [storedAuctionResponse]
diff --git a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy
index 67ff7907901..e24d22b4b8f 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/TargetingSpec.groovy
@@ -4,6 +4,7 @@ import org.prebid.server.functional.model.bidder.Generic
 import org.prebid.server.functional.model.bidder.Openx
 import org.prebid.server.functional.model.config.AccountAuctionConfig
 import org.prebid.server.functional.model.config.AccountConfig
+import org.prebid.server.functional.model.config.PriceGranularityType
 import org.prebid.server.functional.model.db.Account
 import org.prebid.server.functional.model.db.StoredRequest
 import org.prebid.server.functional.model.db.StoredResponse
@@ -17,13 +18,17 @@ import org.prebid.server.functional.model.request.auction.StoredBidResponse
 import org.prebid.server.functional.model.request.auction.Targeting
 import org.prebid.server.functional.model.response.auction.Bid
 import org.prebid.server.functional.model.response.auction.BidResponse
+import org.prebid.server.functional.service.PrebidServerException
 import org.prebid.server.functional.service.PrebidServerService
 import org.prebid.server.functional.util.PBSUtils
 
 import java.math.RoundingMode
 import java.nio.charset.StandardCharsets
 
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST
+import static org.prebid.server.functional.model.AccountStatus.ACTIVE
 import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
+import static org.prebid.server.functional.model.config.PriceGranularityType.UNKNOWN
 import static org.prebid.server.functional.model.response.auction.ErrorType.TARGETING
 import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer
 
@@ -1121,6 +1126,230 @@ class TargetingSpec extends BaseSpec {
         assert targeting["hb_env"] == HB_ENV_AMP
     }
 
+    def "PBS auction should throw error when price granularity from original request is empty"() {
+        given: "Default bidRequest with empty price granularity"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            ext.prebid.targeting = new Targeting(priceGranularity: PriceGranularity.getDefault(UNKNOWN))
+        }
+
+        and: "Account in the DB"
+        def account = createAccountWithPriceGranularity(bidRequest.accountId, PBSUtils.getRandomEnum(PriceGranularityType))
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        defaultPbsService.sendAuctionRequest(bidRequest)
+
+        then: "Request should fail with an error"
+        def exception = thrown(PrebidServerException)
+        assert exception.statusCode == BAD_REQUEST.code()
+        assert exception.responseBody == 'Invalid request format: Price granularity error: empty granularity definition supplied'
+    }
+
+    def "PBS auction should prioritize price granularity from original request over account config"() {
+        given: "Default bidRequest with price granularity"
+        def requestPriceGranularity = PriceGranularity.getDefault(priceGranularity as PriceGranularityType)
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            ext.prebid.targeting = new Targeting(priceGranularity: requestPriceGranularity)
+        }
+
+        and: "Account in the DB"
+        def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: PBSUtils.getRandomEnum(PriceGranularityType))
+        def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig)
+        def account = new Account(uuid: bidRequest.accountId, config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        defaultPbsService.sendAuctionRequest(bidRequest)
+
+        then: "BidderRequest should include price granularity from bidRequest"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity
+
+        where:
+        priceGranularity << (PriceGranularityType.values() - UNKNOWN  as List<PriceGranularityType>)
+    }
+
+    def "PBS amp should prioritize price granularity from original request over account config"() {
+        given: "Default AmpRequest"
+        def ampRequest = AmpRequest.defaultAmpRequest
+
+        and: "Default ampStoredRequest"
+        def requestPriceGranularity = PriceGranularity.getDefault(priceGranularity)
+        def ampStoredRequest = BidRequest.defaultBidRequest.tap {
+            ext.prebid.targeting = new Targeting(priceGranularity: requestPriceGranularity)
+            setAccountId(ampRequest.account)
+        }
+
+        and: "Create and save stored request into DB"
+        def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+        storedRequestDao.save(storedRequest)
+
+        and: "Account in the DB"
+        def account = createAccountWithPriceGranularity(ampRequest.account, PBSUtils.getRandomEnum(PriceGranularityType))
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        defaultPbsService.sendAmpRequest(ampRequest)
+
+        then: "BidderRequest should include price granularity from bidRequest"
+        def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id)
+        assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == requestPriceGranularity
+
+        where:
+        priceGranularity << (PriceGranularityType.values() - UNKNOWN  as List<PriceGranularityType>)
+    }
+
+    def "PBS auction should include price granularity from account config when original request doesn't contain price granularity"() {
+        given: "Default basic BidRequest"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false)
+        }
+
+        and: "Account in the DB"
+        def account = createAccountWithPriceGranularity(bidRequest.accountId, priceGranularity)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        defaultPbsService.sendAuctionRequest(bidRequest)
+
+        then: "BidderRequest should include price granularity from account config"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity)
+
+        where:
+        priceGranularity << (PriceGranularityType.values() - UNKNOWN  as List<PriceGranularityType>)
+    }
+
+    def "PBS auction should include price granularity from account config with different name case when original request doesn't contain price granularity"() {
+        given: "Default basic BidRequest"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false)
+        }
+
+        and: "Account in the DB"
+        def account = createAccountWithPriceGranularity(bidRequest.accountId, priceGranularity)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        defaultPbsService.sendAuctionRequest(bidRequest)
+
+        then: "BidderRequest should include price granularity from account config"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity)
+
+        where:
+        priceGranularity << (PriceGranularityType.values() - UNKNOWN  as List<PriceGranularityType>)
+    }
+
+    def "PBS auction should include price granularity from default account config when original request doesn't contain price granularity"() {
+        given: "Pbs with default account that include privacySandbox configuration"
+        def priceGranularity = PBSUtils.getRandomEnum(PriceGranularityType, [UNKNOWN])
+        def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity)
+        def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig)
+        def pbsService = pbsServiceFactory.getService(
+                ["settings.default-account-config": encode(accountConfig)])
+
+        and: "Default basic BidRequest"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false)
+        }
+
+        when: "PBS processes auction request"
+        pbsService.sendAuctionRequest(bidRequest)
+
+        then: "BidderRequest should include price granularity from account config"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity)
+    }
+
+    def "PBS auction should include include default price granularity when original request and account config doesn't contain price granularity"() {
+        given: "Default basic BidRequest"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false)
+        }
+
+        and: "Account in the DB"
+        def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig)
+        def account = new Account(uuid: bidRequest.accountId, config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        defaultPbsService.sendAuctionRequest(bidRequest)
+
+        then: "BidderRequest should include default price granularity"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.default
+
+        where:
+        accountAuctionConfig << [
+                null,
+                new AccountAuctionConfig(),
+                new AccountAuctionConfig(priceGranularity: UNKNOWN)]
+    }
+
+    def "PBS amp should throw error when price granularity from original request is empty"() {
+        given: "Default AmpRequest"
+        def ampRequest = AmpRequest.defaultAmpRequest
+
+        and: "Default ampStoredRequest with empty price granularity"
+        def ampStoredRequest = BidRequest.defaultBidRequest.tap {
+            ext.prebid.targeting = new Targeting(priceGranularity: PriceGranularity.getDefault(UNKNOWN))
+            setAccountId(ampRequest.account)
+        }
+
+        and: "Create and save stored request into DB"
+        def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+        storedRequestDao.save(storedRequest)
+
+
+        and: "Account in the DB"
+        def account = createAccountWithPriceGranularity(ampRequest.account, PBSUtils.getRandomEnum(PriceGranularityType))
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        defaultPbsService.sendAmpRequest(ampRequest)
+
+        then: "Request should fail with an error"
+        def exception = thrown(PrebidServerException)
+        assert exception.statusCode == BAD_REQUEST.code()
+        assert exception.responseBody == 'Invalid request format: Price granularity error: empty granularity definition supplied'
+    }
+
+    def "PBS amp should include price granularity from account config when original request doesn't contain price granularity"() {
+        given: "Default AmpRequest"
+        def ampRequest = AmpRequest.defaultAmpRequest
+
+        and: "Default ampStoredRequest"
+        def ampStoredRequest = BidRequest.defaultBidRequest.tap {
+            ext.prebid.targeting = Targeting.createWithAllValuesSetTo(false)
+            setAccountId(ampRequest.account)
+        }
+
+        and: "Account in the DB"
+        def account = createAccountWithPriceGranularity(ampRequest.account, priceGranularity)
+        accountDao.save(account)
+
+        and: "Create and save stored request into DB"
+        def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+        storedRequestDao.save(storedRequest)
+
+        when: "PBS processes amp request"
+        defaultPbsService.sendAmpRequest(ampRequest)
+
+        then: "BidderRequest should include price granularity from account config"
+        def bidderRequest = bidder.getBidderRequest(ampStoredRequest.id)
+        assert bidderRequest?.ext?.prebid?.targeting?.priceGranularity == PriceGranularity.getDefault(priceGranularity)
+
+        where:
+        priceGranularity << (PriceGranularityType.values() - UNKNOWN  as List<PriceGranularityType>)
+    }
+
+    def createAccountWithPriceGranularity(String accountId, PriceGranularityType priceGranularity) {
+        def accountAuctionConfig = new AccountAuctionConfig(priceGranularity: priceGranularity)
+        def accountConfig = new AccountConfig(status: ACTIVE, auction: accountAuctionConfig)
+        return new Account(uuid: accountId, config: accountConfig)
+    }
+
     private static PrebidServerService getEnabledWinBidsPbsService() {
         pbsServiceFactory.getService(["auction.cache.only-winning-bids": "true"])
     }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy
index 99fa3b31a0e..cff45989db1 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/TimeoutSpec.groovy
@@ -16,8 +16,10 @@ class TimeoutSpec extends BaseSpec {
 
     private static final int DEFAULT_TIMEOUT = getRandomTimeout()
     private static final int MIN_TIMEOUT = PBSUtils.getRandomNumber(50, 150)
-    private static final Map PBS_CONFIG = ["auction.biddertmax.max"    : MAX_TIMEOUT as String,
-                                           "auction.biddertmax.min"    : MIN_TIMEOUT as String]
+    private static final Long MAX_AUCTION_BIDDER_TIMEOUT = 3000
+    private static final Long MIN_AUCTION_BIDDER_TIMEOUT = 1000
+    private static final Map PBS_CONFIG = ["auction.biddertmax.max": MAX_AUCTION_BIDDER_TIMEOUT as String,
+                                           "auction.biddertmax.min": MIN_AUCTION_BIDDER_TIMEOUT as String]
 
     @Shared
     PrebidServerService prebidServerService = pbsServiceFactory.getService(PBS_CONFIG)
@@ -136,9 +138,10 @@ class TimeoutSpec extends BaseSpec {
 
         and: "Pbs config with default request"
         def pbsContainer = new PrebidServerContainer(
-                ["default-request.file.path" : APP_WORKDIR + defaultRequest.fileName,
-                 "auction.biddertmax.max"    : MAX_TIMEOUT as String]).tap {
-            withCopyFileToContainer(MountableFile.forHostPath(defaultRequest), APP_WORKDIR) }
+                ["default-request.file.path": APP_WORKDIR + defaultRequest.fileName,
+                 "auction.biddertmax.max"   : MAX_TIMEOUT as String]).tap {
+            withCopyFileToContainer(MountableFile.forHostPath(defaultRequest), APP_WORKDIR)
+        }
         pbsContainer.start()
         def pbsService = new PrebidServerService(pbsContainer)
 
@@ -284,8 +287,9 @@ class TimeoutSpec extends BaseSpec {
     def "PBS should choose min timeout form config for bidder request when in request value lowest that in auction.biddertmax.min"() {
         given: "PBS config with percent"
         def minBidderTmax = PBSUtils.getRandomNumber(MIN_TIMEOUT, MAX_TIMEOUT)
-        def prebidServerService = pbsServiceFactory.getService(["auction.biddertmax.min"    : minBidderTmax as String,
-                                                                                "auction.biddertmax.max"    : MAX_TIMEOUT as String])
+        def prebidServerService = pbsServiceFactory.getService(
+                ["auction.biddertmax.min": minBidderTmax as String,
+                 "auction.biddertmax.max": MAX_TIMEOUT as String])
 
         and: "Default basic BidRequest"
         def timeout = PBSUtils.getRandomNumber(0, minBidderTmax)
@@ -307,11 +311,14 @@ class TimeoutSpec extends BaseSpec {
     def "PBS should change timeout for bidder due to percent in auction.biddertmax.percent"() {
         given: "PBS config with percent"
         def percent = PBSUtils.getRandomNumber(2, 98)
-        def prebidServerService = pbsServiceFactory.getService(["auction.biddertmax.percent": percent as String]
-                + PBS_CONFIG)
+        def pbsConfig = ["auction.biddertmax.percent": percent as String,
+                         "auction.biddertmax.max"    : MAX_TIMEOUT as String,
+                         "auction.biddertmax.min"    : MIN_TIMEOUT as String]
+        def prebidServerService = pbsServiceFactory.getService(
+                pbsConfig)
 
         and: "Default basic BidRequest with generic bidder"
-        def timeout = getRandomTimeout()
+        def timeout = randomTimeout
         def bidRequest = BidRequest.defaultBidRequest.tap {
             tmax = timeout
         }
@@ -321,17 +328,97 @@ class TimeoutSpec extends BaseSpec {
 
         then: "Bidder request should contain percent of request value"
         def bidderRequest = bidder.getBidderRequest(bidRequest.id)
-        assert isInternalProcessingTime(bidderRequest.tmax, getPercentOfValue(percent,timeout))
+        assert isInternalProcessingTime(bidderRequest.tmax, getPercentOfValue(percent, timeout))
 
         and: "PBS response should contain tmax from request"
         assert bidResponse?.ext?.tmaxrequest == timeout as Long
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS should apply auction.biddertmax.max timeout when adapters.generic.tmax-deduction-ms exceeds valid top range"() {
+        given: "PBS config with adapters.generic.tmax-deduction-ms"
+        def pbsConfig = PBS_CONFIG + ["adapters.generic.tmax-deduction-ms": PBSUtils.getRandomNumber(MAX_AUCTION_BIDDER_TIMEOUT as int) as String]
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default basic BidRequest with generic bidder"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            tmax = randomTimeout
+        }
+
+        when: "PBS processes auction request"
+        def bidResponse = prebidServerService.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain min"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.tmax == MIN_AUCTION_BIDDER_TIMEOUT
+
+        and: "PBS response should contain tmax"
+        assert bidResponse?.ext?.tmaxrequest == MAX_AUCTION_BIDDER_TIMEOUT
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS should resolve timeout as usual when adapters.generic.tmax-deduction-ms specifies zero"() {
+        given: "PBS config with adapters.generic.tmax-deduction-ms"
+        def pbsConfig = ["adapters.generic.tmax-deduction-ms": "0"] + PBS_CONFIG
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default basic BidRequest with generic bidder"
+        def timeout = PBSUtils.getRandomNumber(
+                MIN_AUCTION_BIDDER_TIMEOUT as int,
+                MAX_AUCTION_BIDDER_TIMEOUT as int)
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            tmax = timeout
+        }
+
+        when: "PBS processes auction request"
+        def bidResponse = prebidServerService.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain right value in tmax"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert isInternalProcessingTime(bidderRequest.tmax, timeout)
+
+        and: "PBS response should contain tmax"
+        assert bidResponse?.ext?.tmaxrequest == timeout as Long
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS should properly resolve tmax deduction ms when adapters.generic.tmax-deduction-ms specified"() {
+        given: "PBS config with adapters.generic.tmax-deduction-ms"
+        def genericDeductionMs = PBSUtils.getRandomNumber(100, 300)
+        def randomTimeout = PBSUtils.getRandomNumber(MIN_AUCTION_BIDDER_TIMEOUT + genericDeductionMs as int, MAX_AUCTION_BIDDER_TIMEOUT as int)
+        def pbsConfig = PBS_CONFIG + ["adapters.generic.tmax-deduction-ms": genericDeductionMs as String]
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default basic BidRequest with generic bidder"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            tmax = randomTimeout
+        }
+
+        when: "PBS processes auction request"
+        def bidResponse = prebidServerService.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain right value in tmax"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert isInternalProcessingTime(bidderRequest.tmax, randomTimeout)
+
+        and: "PBS response should contain tmax"
+        assert bidResponse?.ext?.tmaxrequest == randomTimeout as Long
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
     }
 
     private static long getPercentOfValue(int percent, int value) {
         (percent * value) / 100.0 as Long
     }
 
-    private static boolean isInternalProcessingTime(long bidderRequestTimeout, long requestTimeout){
+    private static boolean isInternalProcessingTime(long bidderRequestTimeout, long requestTimeout) {
         0 < requestTimeout - bidderRequestTimeout
     }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy
new file mode 100644
index 00000000000..9ca353e0088
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy
@@ -0,0 +1,1157 @@
+package org.prebid.server.functional.tests.module
+
+import org.prebid.server.functional.model.ModuleName
+import org.prebid.server.functional.model.config.AbTest
+import org.prebid.server.functional.model.config.AccountConfig
+import org.prebid.server.functional.model.config.AccountHooksConfiguration
+import org.prebid.server.functional.model.config.ExecutionPlan
+import org.prebid.server.functional.model.config.Stage
+import org.prebid.server.functional.model.db.Account
+import org.prebid.server.functional.model.request.auction.BidRequest
+import org.prebid.server.functional.model.request.auction.FetchStatus
+import org.prebid.server.functional.model.request.auction.TraceLevel
+import org.prebid.server.functional.model.response.auction.AnalyticResult
+import org.prebid.server.functional.model.response.auction.InvocationResult
+import org.prebid.server.functional.service.PrebidServerService
+import org.prebid.server.functional.util.PBSUtils
+
+import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION
+import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION
+import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_BIDDER_REQUEST
+import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_RAW_BIDDER_RESPONSE
+import static org.prebid.server.functional.model.config.ModuleHookImplementation.RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES
+import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES
+import static org.prebid.server.functional.model.config.Stage.BIDDER_REQUEST
+import static org.prebid.server.functional.model.config.Stage.RAW_BIDDER_RESPONSE
+import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultBidRequest
+import static org.prebid.server.functional.model.response.auction.InvocationStatus.INVOCATION_FAILURE
+import static org.prebid.server.functional.model.response.auction.InvocationStatus.SUCCESS
+import static org.prebid.server.functional.model.response.auction.ModuleActivityName.AB_TESTING
+import static org.prebid.server.functional.model.response.auction.ModuleActivityName.ORTB2_BLOCKING
+import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_ACTION
+import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_INVOCATION
+
+class AbTestingModuleSpec extends ModuleBaseSpec {
+
+    private final static String NO_INVOCATION_METRIC = "modules.module.%s.stage.%s.hook.%s.success.no-invocation"
+    private final static String CALL_METRIC = "modules.module.%s.stage.%s.hook.%s.call"
+    private final static String EXECUTION_ERROR_METRIC = "modules.module.%s.stage.%s.hook.%s.execution-error"
+    private final static Integer MIN_PERCENT_AB = 0
+    private final static Integer MAX_PERCENT_AB = 100
+    private final static String INVALID_HOOK_MESSAGE = "Hook implementation does not exist or disabled"
+
+    private final static Map<Stage, List<ModuleName>> ORTB_STAGES = [(BIDDER_REQUEST)     : [ModuleName.ORTB2_BLOCKING],
+                                                                     (RAW_BIDDER_RESPONSE): [ModuleName.ORTB2_BLOCKING]]
+    private final static Map<Stage, List<ModuleName>> RESPONSE_STAGES = [(ALL_PROCESSED_BID_RESPONSES): [PB_RESPONSE_CORRECTION]]
+    private final static Map<Stage, List<ModuleName>> MODULES_STAGES = ORTB_STAGES + RESPONSE_STAGES
+
+    private final static Map<String, String> MULTI_MODULE_CONFIG = getResponseCorrectionConfig() + getOrtb2BlockingSettings() +
+            ['hooks.host-execution-plan': null]
+
+    private final static PrebidServerService ortbModulePbsService = pbsServiceFactory.getService(getOrtb2BlockingSettings())
+    private final static PrebidServerService pbsServiceWithMultipleModules = pbsServiceFactory.getService(MULTI_MODULE_CONFIG)
+
+    def "PBS shouldn't apply a/b test config when config of ab test is disabled"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(ortbModulePbsService)
+
+        and: "Save account with ab test config"
+        def abTest = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+            enabled = false
+        }
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap {
+            it.abTests = [abTest]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = ortbModulePbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION]
+        }
+
+        and: "Shouldn't include any analytics tags"
+        assert (invocationResults.analyticsTags.activities.flatten() as List<AnalyticResult>).findAll { it.name != AB_TESTING.value }
+
+        and: "Metric for specified module should be as default call"
+        def metrics = ortbModulePbsService.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+    }
+
+    def "PBS shouldn't apply valid a/b test config when module is disabled"() {
+        given: "PBS service with disabled module config"
+        def pbsConfig = getOrtb2BlockingSettings(false)
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(prebidServerService)
+
+        and: "Save account with ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code)]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = prebidServerService.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationResults) {
+            it.status == [INVOCATION_FAILURE, INVOCATION_FAILURE]
+            it.action == [null, null]
+            it.analyticsTags == [null, null]
+            it.message == [INVALID_HOOK_MESSAGE, INVALID_HOOK_MESSAGE]
+        }
+
+        and: "Metric for specified module should be with error call"
+        def metrics = prebidServerService.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[EXECUTION_ERROR_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[EXECUTION_ERROR_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS shouldn't apply a/b test config when module name is not matched"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(ortbModulePbsService)
+
+        and: "Save account with ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap {
+            abTests = [AbTest.getDefault(moduleName)]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = ortbModulePbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION]
+        }
+
+        and: "Shouldn't include any analytics tags"
+        assert (invocationResults.analyticsTags.activities.flatten() as List<AnalyticResult>).findAll { it.name != AB_TESTING.value }
+
+        and: "Metric for specified module should be as default call"
+        def metrics = ortbModulePbsService.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+
+        where:
+        moduleName << [ModuleName.ORTB2_BLOCKING.code.toUpperCase(), PBSUtils.randomString]
+    }
+
+    def "PBS should apply a/b test config for each module when multiple config are presents and set to allow modules"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        and: "Save account with ab test config"
+        def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+            it.percentActive = MAX_PERCENT_AB
+        }
+        def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code).tap {
+            it.percentActive = MAX_PERCENT_AB
+        }
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap {
+            abTests = [ortb2AbTestConfig, richMediaAbTestConfig]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS should apply ab test config for specified module"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING)
+        verifyAll(ortbBlockingInvocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION]
+            it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort()
+            it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort()
+            it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].value.sort()
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "PBS should not apply ab test config for other module"
+        def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION)
+        verifyAll(responseCorrectionInvocationResults) {
+            it.status == [SUCCESS]
+            it.action == [NO_ACTION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.RUN].value
+            it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION]
+        }
+
+        and: "Metric for allowed to run ortb2blocking module should be updated based on ab test config"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+
+        and: "Metric for allowed to run response-correction module should be updated based on ab test config"
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+    }
+
+    def "PBS should apply a/b test config for each module when multiple config are presents and set to skip modules"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        and: "Save account with ab test config"
+        def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+            it.percentActive = MIN_PERCENT_AB
+        }
+        def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code).tap {
+            it.percentActive = MIN_PERCENT_AB
+        }
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap {
+            abTests = [ortb2AbTestConfig, richMediaAbTestConfig]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS should apply ab test config for ortb2blocking module"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING)
+        verifyAll(ortbBlockingInvocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_INVOCATION, NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "PBS should apply ab test config for response-correction module"
+        def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION)
+        verifyAll(responseCorrectionInvocationResults) {
+            it.status == [SUCCESS]
+            it.action == [NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION]
+        }
+
+        and: "Metric for skipped ortb2blocking module should be updated based on ab test config"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+        assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+
+        and: "Metric for skipped response-correction module should be updated based on ab test config"
+        assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+        assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+    }
+
+    def "PBS should apply a/b test config for each module when multiple config are presents with different percentage"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        and: "Save account with ab test config"
+        def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+            it.percentActive = MIN_PERCENT_AB
+        }
+        def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code).tap {
+            it.percentActive = MAX_PERCENT_AB
+        }
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap {
+            abTests = [ortb2AbTestConfig, richMediaAbTestConfig]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS should apply ab test config for ortb2blocking module"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING)
+        verifyAll(ortbBlockingInvocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_INVOCATION, NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "PBS should not apply ab test config for response-correction module"
+        def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION)
+        verifyAll(responseCorrectionInvocationResults) {
+            it.status == [SUCCESS]
+            it.action == [NO_ACTION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.RUN].value
+            it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION]
+        }
+
+        and: "Metric for skipped ortb2blocking module should be updated based on ab test config"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        and: "Metric for allowed to run response-correction module should be updated based on ab test config"
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+    }
+
+    def "PBS should ignore accounts property for a/b test config when ab test config specialize for specific account"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(ortbModulePbsService)
+
+        and: "Save account with ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, [PBSUtils.randomNumber]).tap {
+                percentActive = MIN_PERCENT_AB
+            }]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = ortbModulePbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_INVOCATION, NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "Metric for specified module should be updated based on ab test config"
+        def metrics = ortbModulePbsService.sendCollectedMetricsRequest()
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+    }
+
+    def "PBS should apply a/b test config and run module when config is on max percentage or default value"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(ortbModulePbsService)
+
+        and: "Save account with ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+                it.percentActive = percentActive
+                it.percentActiveSnakeCase = percentActiveSnakeCase
+            }]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = ortbModulePbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should apply ab test config for ortb module and run module"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION]
+            it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort()
+            it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort()
+            it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].value.sort()
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "Metric for specified module should be as default call"
+        def metrics = ortbModulePbsService.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+
+        where:
+        percentActive  | percentActiveSnakeCase
+        MAX_PERCENT_AB | null
+        null           | MAX_PERCENT_AB
+        null           | null
+    }
+
+    def "PBS should apply a/b test config and skip module when config is on min percentage"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(ortbModulePbsService)
+
+        and: "Save account with ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+                it.percentActive = percentActive
+                it.percentActiveSnakeCase = percentActiveSnakeCase
+            }]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = ortbModulePbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should apply ab test config for ortb module and skip this module"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_INVOCATION, NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "Metric for specified module should be updated based on ab test config"
+        def metrics = ortbModulePbsService.sendCollectedMetricsRequest()
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        where:
+        percentActive  | percentActiveSnakeCase
+        MIN_PERCENT_AB | null
+        null           | MIN_PERCENT_AB
+    }
+
+    def "PBS shouldn't apply a/b test config without warnings and errors when percent config is out of lover range"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(ortbModulePbsService)
+
+        and: "Save account with ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+                it.percentActive = percentActive
+                it.percentActiveSnakeCase = percentActiveSnakeCase
+            }]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = ortbModulePbsService.sendAuctionRequest(bidRequest)
+
+        then: "No error or warning should be emitted"
+        assert !response.ext.errors
+        assert !response.ext.warnings
+
+        and: "PBS response should include trace information about called modules"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_INVOCATION, NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "Metric for specified module should be updated based on ab test config"
+        def metrics = ortbModulePbsService.sendCollectedMetricsRequest()
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        where:
+        percentActive                 | percentActiveSnakeCase
+        PBSUtils.randomNegativeNumber | null
+        null                          | PBSUtils.randomNegativeNumber
+    }
+
+    def "PBS should apply a/b test config and run module without warnings and errors when percent config is out of appear range"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(ortbModulePbsService)
+
+        and: "Save account with ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+                it.percentActive = percentActive
+                it.percentActiveSnakeCase = percentActiveSnakeCase
+            }]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = ortbModulePbsService.sendAuctionRequest(bidRequest)
+
+        then: "No error or warning should be emitted"
+        assert !response.ext.errors
+        assert !response.ext.warnings
+
+        and: "PBS should apply ab test config for ortb module and run it"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION]
+
+            it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort()
+            it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort()
+            it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].value.sort()
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "Metric for specified module should be as default call"
+        def metrics = ortbModulePbsService.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+
+        where:
+        percentActive                            | percentActiveSnakeCase
+        PBSUtils.getRandomNumber(MAX_PERCENT_AB) | null
+        null                                     | PBSUtils.getRandomNumber(MAX_PERCENT_AB)
+    }
+
+    def "PBS should include analytics tags when a/b test config when logAnalyticsTag is enabled or empty"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(ortbModulePbsService)
+
+        and: "Save account with ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+                percentActive = MIN_PERCENT_AB
+                it.logAnalyticsTag = logAnalyticsTag
+                it.logAnalyticsTagSnakeCase = logAnalyticsTagSnakeCase
+            }]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = ortbModulePbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should apply ab test config for specified module without analytics tags"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_INVOCATION, NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "Metric for specified module should be updated based on ab test config"
+        def metrics = ortbModulePbsService.sendCollectedMetricsRequest()
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        where:
+        logAnalyticsTag | logAnalyticsTagSnakeCase
+        true            | null
+        null            | true
+        null            | null
+    }
+
+    def "PBS shouldn't include analytics tags when a/b test config when logAnalyticsTag is disabled and is applied by percentage"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(ortbModulePbsService)
+
+        and: "Save account with ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+                percentActive = MIN_PERCENT_AB
+                it.logAnalyticsTag = logAnalyticsTag
+                it.logAnalyticsTagSnakeCase = logAnalyticsTagSnakeCase
+            }]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = ortbModulePbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_INVOCATION, NO_INVOCATION]
+        }
+
+        and: "Shouldn't include any analytics tags"
+        assert !invocationResults?.analyticsTags?.any()
+
+        and: "Metric for specified module should be updated based on ab test config"
+        def metrics = ortbModulePbsService.sendCollectedMetricsRequest()
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        where:
+        logAnalyticsTag | logAnalyticsTagSnakeCase
+        false           | null
+        null            | false
+    }
+
+    def "PBS shouldn't include analytics tags when a/b test config when logAnalyticsTag is disabled and is non-applied by percentage"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(ortbModulePbsService)
+
+        and: "Save account with ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+                percentActive = MAX_PERCENT_AB
+                it.logAnalyticsTag = logAnalyticsTag
+                it.logAnalyticsTagSnakeCase = logAnalyticsTagSnakeCase
+            }]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = ortbModulePbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION]
+        }
+
+        and: "Shouldn't include any analytics tags"
+        assert (invocationResults.analyticsTags.activities.flatten() as List<AnalyticResult>).findAll { it.name != AB_TESTING.value }
+
+        and: "Metric for specified module should be as default call"
+        def metrics = ortbModulePbsService.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+
+        where:
+        logAnalyticsTag | logAnalyticsTagSnakeCase
+        false           | null
+        null            | false
+    }
+
+    def "PBS shouldn't apply analytics tags for all module stages when module contain multiple stages"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(ortbModulePbsService)
+
+        and: "Save account with ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+                percentActive = PBSUtils.getRandomNumber(MIN_PERCENT_AB, MAX_PERCENT_AB)
+            }]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = ortbModulePbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should apply ab test config for all stages of specified module"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationResults) {
+            it.status.every { status -> status == it.status.first() }
+            it.action.every { action -> action == it.action.first() }
+        }
+
+        and: "All resonances have same analytics"
+        def abTestingInvocationResults = (invocationResults.analyticsTags.activities.flatten() as List<AnalyticResult>).findAll { it.name == AB_TESTING.value }
+        verifyAll(abTestingInvocationResults) {
+            it.status.flatten().every { status -> status == it.status.flatten().first() }
+            it.results.status.flatten().every { status -> status == it.results.status.flatten().first() }
+            it.results.values.module.flatten().every { module -> module == it.results.values.module.flatten().first() }
+        }
+    }
+
+    def "PBS should apply a/b test config from host config when accounts is not specified when account config and default account doesn't include a/b test config"() {
+        given: "PBS service with specific ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, accouns).tap {
+                percentActive = MIN_PERCENT_AB
+            }]
+        }
+        def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)]
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS should apply ab test config for specified module"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING)
+        verifyAll(ortbBlockingInvocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_INVOCATION, NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "PBS should not apply ab test config for other module"
+        def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION)
+        verifyAll(responseCorrectionInvocationResults) {
+            it.status == [SUCCESS]
+            it.action == [NO_ACTION]
+
+            it.analyticsTags.every { it == null }
+        }
+
+        and: "Metric for specified module should be updated based on ab test config"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        and: "Metric for non specified module should be as default call"
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+
+        where:
+        accouns << [null, []]
+    }
+
+    def "PBS should apply a/b test config from host config for specific accounts and only specified module when account config and default account doesn't include a/b test config"() {
+        given: "PBS service with specific ab test config"
+        def accountId = PBSUtils.randomNumber
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, [PBSUtils.randomNumber, accountId]).tap {
+                percentActive = MIN_PERCENT_AB
+            }]
+        }
+        def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)]
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+            setAccountId(accountId as String)
+        }
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS should apply ab test config for specified module"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING)
+        verifyAll(ortbBlockingInvocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_INVOCATION, NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "PBS should not apply ab test config for other module"
+        def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION)
+        verifyAll(responseCorrectionInvocationResults) {
+            it.status == [SUCCESS]
+            it.action == [NO_ACTION]
+
+            it.analyticsTags.every { it == null }
+        }
+
+        and: "Metric for specified module should be updated based on ab test config"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        and: "Metric for non specified module should be as default call"
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS should apply a/b test config from host config for specific account and general config when account config and default account doesn't include a/b test config"() {
+        given: "PBS service with specific ab test config"
+        def accountId = PBSUtils.randomNumber
+        def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, []).tap {
+            it.percentActive = MIN_PERCENT_AB
+        }
+        def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code, [accountId]).tap {
+            it.percentActive = MIN_PERCENT_AB
+        }
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap {
+            abTests = [ortb2AbTestConfig, richMediaAbTestConfig]
+        }
+        def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)]
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+            setAccountId(accountId as String)
+        }
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS should apply ab test config for ortb2blocking module"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING)
+        verifyAll(ortbBlockingInvocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_INVOCATION, NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "PBS should apply ab test config for response-correction module"
+        def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION)
+        verifyAll(responseCorrectionInvocationResults) {
+            it.status == [SUCCESS]
+            it.action == [NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION]
+        }
+
+        and: "Metric for skipped ortb2blocking module should be updated based on ab test config"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+        assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+
+        and: "Metric for skipped response-correction module should be updated based on ab test config"
+        assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+        assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS shouldn't apply a/b test config from host config for non specified accounts when account config and default account doesn't include a/b test config"() {
+        given: "PBS service with specific ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, [PBSUtils.randomNumber]).tap {
+                percentActive = MIN_PERCENT_AB
+            }]
+        }
+        def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)]
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS should apply ab test config for specified module"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING)
+        verifyAll(ortbBlockingInvocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION]
+
+            it.analyticsTags.activities.name.flatten() == [ORTB2_BLOCKING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SUCCESS_ALLOW].value
+            it.analyticsTags.activities.results.values.module.flatten().every { it == null }
+        }
+
+        and: "PBS should not apply ab test config for other module"
+        def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION)
+        verifyAll(responseCorrectionInvocationResults) {
+            it.status == [SUCCESS]
+            it.action == [NO_ACTION]
+
+            it.analyticsTags.every { it == null }
+        }
+
+        and: "Metric for specified module should be as default call"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+
+        and: "Metric for non specified module should be as default call"
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS should prioritise a/b test config from default account and only specified module when host and default account contains a/b test configs"() {
+        given: "PBS service with specific ab test config"
+        def accountExecutionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+                percentActive = MIN_PERCENT_AB
+            }]
+        }
+        def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap {
+            hooks = new AccountHooksConfiguration(executionPlan: accountExecutionPlan)
+        }
+
+        def hostExecutionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code)]
+        }
+        def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(hostExecutionPlan)] + ["settings.default-account-config": encode(defaultAccountConfigSettings)]
+
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS should apply ab test config for specified module and call it based on all execution plans"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING)
+        verifyAll(ortbBlockingInvocationResults) {
+            it.status == [SUCCESS, SUCCESS, SUCCESS, SUCCESS]
+            it.action == [NO_INVOCATION, NO_INVOCATION, NO_INVOCATION, NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING, AB_TESTING, AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED, FetchStatus.SKIPPED, FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "PBS should not apply ab test config for other modules and call them based on all execution plans"
+        def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION)
+        verifyAll(responseCorrectionInvocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION]
+
+            it.analyticsTags.every { it == null }
+        }
+
+        and: "Metric for specified module should be updated based on ab test config"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 2
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 2
+
+        and: "Metric for non specified module should be as default call"
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 2
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 2
+
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS should prioritise a/b test config from account over default account and only specified module when specific account and default account contains a/b test configs"() {
+        given: "PBS service with specific ab test config"
+        def accountExecutionPlan = new ExecutionPlan(abTests: [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code)])
+        def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap {
+            hooks = new AccountHooksConfiguration(executionPlan: accountExecutionPlan)
+        }
+
+        def pbsConfig = MULTI_MODULE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)]
+
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = getBidRequestWithTrace()
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        and: "Save account with ab test config"
+        def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap {
+            abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap {
+                percentActive = MIN_PERCENT_AB
+            }]
+        }
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+
+        and: "PBS should apply ab test config for specified module"
+        def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING)
+        verifyAll(ortbBlockingInvocationResults) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_INVOCATION, NO_INVOCATION]
+            it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value
+            it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS]
+            it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value
+            it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING]
+        }
+
+        and: "PBS should not apply ab test config for other module"
+        def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION)
+        verifyAll(responseCorrectionInvocationResults) {
+            it.status == [SUCCESS]
+            it.action == [NO_ACTION]
+
+            it.analyticsTags.every { it == null }
+        }
+
+        and: "Metric for specified module should be updated based on ab test config"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        and: "Metric for non specified module should be as default call"
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1
+
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    private static List<InvocationResult> filterInvocationResultsByModule(List<InvocationResult> invocationResults, ModuleName moduleName) {
+        invocationResults.findAll { it.hookId.moduleCode == moduleName.code }
+    }
+
+    private static BidRequest getBidRequestWithTrace() {
+        defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+    }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy
new file mode 100644
index 00000000000..82727b16a56
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy
@@ -0,0 +1,532 @@
+package org.prebid.server.functional.tests.module
+
+import org.prebid.server.functional.model.ModuleName
+import org.prebid.server.functional.model.config.AccountConfig
+import org.prebid.server.functional.model.config.AccountHooksConfiguration
+import org.prebid.server.functional.model.config.AdminConfig
+import org.prebid.server.functional.model.config.ExecutionPlan
+import org.prebid.server.functional.model.config.Ortb2BlockingConfig
+import org.prebid.server.functional.model.config.PbResponseCorrection
+import org.prebid.server.functional.model.config.PbsModulesConfig
+import org.prebid.server.functional.model.config.Stage
+import org.prebid.server.functional.model.db.Account
+import org.prebid.server.functional.model.request.auction.RichmediaFilter
+import org.prebid.server.functional.model.request.auction.TraceLevel
+import org.prebid.server.functional.model.response.auction.InvocationResult
+import org.prebid.server.functional.service.PrebidServerService
+import org.prebid.server.functional.util.PBSUtils
+
+import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING
+import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER
+import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION
+import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_BIDDER_REQUEST
+import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_RAW_BIDDER_RESPONSE
+import static org.prebid.server.functional.model.config.ModuleHookImplementation.PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES
+import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES
+import static org.prebid.server.functional.model.config.Stage.BIDDER_REQUEST
+import static org.prebid.server.functional.model.config.Stage.RAW_BIDDER_RESPONSE
+import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultBidRequest
+import static org.prebid.server.functional.model.response.auction.InvocationStatus.SUCCESS
+import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_ACTION
+
+class GeneralModuleSpec extends ModuleBaseSpec {
+
+    private final static String CALL_METRIC = "modules.module.%s.stage.%s.hook.%s.call"
+    private final static String NOOP_METRIC = "modules.module.%s.stage.%s.hook.%s.success.noop"
+
+    private final static Map<String, String> DISABLED_INVOKE_CONFIG = ['settings.modules.require-config-to-invoke': 'false']
+    private final static Map<String, String> ENABLED_INVOKE_CONFIG = ['settings.modules.require-config-to-invoke': 'true']
+
+    private final static Map<Stage, List<ModuleName>> ORTB_STAGES = [(BIDDER_REQUEST)     : [ORTB2_BLOCKING],
+                                                                     (RAW_BIDDER_RESPONSE): [ORTB2_BLOCKING]]
+    private final static Map<Stage, List<ModuleName>> RESPONSE_STAGES = [(ALL_PROCESSED_BID_RESPONSES): [PB_RICHMEDIA_FILTER]]
+    private final static Map<Stage, List<ModuleName>> MODULES_STAGES = ORTB_STAGES + RESPONSE_STAGES
+    private final static Map<String, String> MULTI_MODULE_CONFIG = getRichMediaFilterSettings(PBSUtils.randomString) +
+            getOrtb2BlockingSettings() +
+            ['hooks.host-execution-plan': encode(ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES))]
+
+    private final static PrebidServerService pbsServiceWithMultipleModule = pbsServiceFactory.getService(MULTI_MODULE_CONFIG + DISABLED_INVOKE_CONFIG)
+    private final static PrebidServerService pbsServiceWithMultipleModuleWithRequireInvoke = pbsServiceFactory.getService(MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG)
+
+    def "PBS should call all modules and traces response when account config is empty and require-config-to-invoke is disabled"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+
+        and: "Save account without modules config"
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModule)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModule.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>) {
+            it.status == [SUCCESS, SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION, NO_ACTION]
+            it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING, PB_RICHMEDIA_FILTER].code.sort()
+        }
+
+        and: "Ortb2blocking module call metrics should be updated"
+        def metrics = pbsServiceWithMultipleModule.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        and: "RB-Richmedia-Filter module call metrics should be updated"
+        assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1
+
+        where:
+        modulesConfig << [null, new PbsModulesConfig()]
+    }
+
+    def "PBS should call all modules and traces response when account includes modules config and require-config-to-invoke is disabled"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+
+        and: "Save account without modules config"
+        def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: pbRichmediaFilterConfig, pbResponseCorrection: pbResponseCorrectionConfig)
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: pbsModulesConfig))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModule)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModule.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>) {
+            it.status == [SUCCESS, SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION, NO_ACTION]
+            it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort()
+        }
+
+        and: "Ortb2blocking module call metrics should be updated"
+        def metrics = pbsServiceWithMultipleModule.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        and: "RB-Richmedia-Filter module call metrics should be updated"
+        assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1
+
+        where:
+        pbRichmediaFilterConfig                | pbResponseCorrectionConfig
+        new RichmediaFilter()                  | new PbResponseCorrection()
+        new RichmediaFilter()                  | new PbResponseCorrection(enabled: false)
+        new RichmediaFilter()                  | new PbResponseCorrection(enabled: true)
+        new RichmediaFilter(filterMraid: true) | new PbResponseCorrection()
+        new RichmediaFilter(filterMraid: true) | new PbResponseCorrection(enabled: true)
+    }
+
+    def "PBS should call all modules and traces response when default-account includes modules config and require-config-to-invoke is enabled"() {
+        given: "PBS service with  module config"
+        def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: new RichmediaFilter(), ortb2Blocking: new Ortb2BlockingConfig())
+        def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap {
+            hooks = new AccountHooksConfiguration(modules: pbsModulesConfig)
+        }
+
+        def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)]
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>) {
+            it.status == [SUCCESS, SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION, NO_ACTION]
+            it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort()
+        }
+
+        and: "Ortb2blocking module call metrics should be updated"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        and: "RB-Richmedia-Filter module call metrics should be updated"
+        assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS should call all modules and traces response when account includes modules config and require-config-to-invoke is enabled"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+
+        and: "Save account with enabled response correction module"
+        def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: pbRichmediaFilterConfig, ortb2Blocking: ortb2BlockingConfig)
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: pbsModulesConfig))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>) {
+            it.status == [SUCCESS, SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION, NO_ACTION]
+            it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort()
+        }
+
+        and: "Ortb2blocking module call metrics should be updated"
+        def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        and: "RB-Richmedia-Filter module call metrics should be updated"
+        assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1
+
+        where:
+        pbRichmediaFilterConfig                | ortb2BlockingConfig
+        new RichmediaFilter()                  | new Ortb2BlockingConfig()
+        new RichmediaFilter()                  | new Ortb2BlockingConfig(attributes: [:] as Map)
+        new RichmediaFilter()                  | new Ortb2BlockingConfig(attributes: [:] as Map)
+        new RichmediaFilter(filterMraid: true) | new Ortb2BlockingConfig()
+        new RichmediaFilter(filterMraid: true) | new Ortb2BlockingConfig(attributes: [:] as Map)
+    }
+
+    def "PBS should call specified module and traces response when account config includes that module and require-config-to-invoke is enabled"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+
+        and: "Save account with enabled response correction module"
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig(pbRichmediaFilter: new RichmediaFilter())))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called module"
+        def invocationTrace = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>
+        verifyAll(invocationTrace.findAll { it -> it.hookId.moduleCode == PB_RICHMEDIA_FILTER.code }) {
+            it.status == [SUCCESS]
+            it.action == [NO_ACTION]
+            it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER].code.sort()
+        }
+
+        and: "Ortb2blocking module call metrics should be updated"
+        def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest()
+        assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+        assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+
+        and: "RB-Richmedia-Filter module call metrics should be updated"
+        assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1
+    }
+
+    def "PBS shouldn't call any modules and traces that in response when account config is empty and require-config-to-invoke is enabled"() {
+        given: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+
+        and: "Save account without modules config"
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest)
+
+        then: "PBS response shouldn't include trace information about no-called modules"
+        assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten()
+
+        and: "Ortb2blocking module call metrics shouldn't be updated"
+        def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest()
+        assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+        assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+
+        and: "RB-Richmedia-Filter module call metrics shouldn't be updated"
+        assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)]
+
+        where:
+        modulesConfig << [null, new PbsModulesConfig()]
+    }
+
+    def "PBS should call all modules without account config when modules enabled in module-execution host config"() {
+        given: "PBS service with module-execution config"
+        def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG +
+                [("hooks.admin.module-execution.${ORTB2_BLOCKING.code}".toString()): 'true']
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION]
+            it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort()
+        }
+
+        and: "Ortb2blocking module call metrics should be updated"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS shouldn't call any module without account config when modules disabled in module-execution host config"() {
+        given: "PBS service with module-execution config"
+        def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG +
+                [("hooks.admin.module-execution.${ORTB2_BLOCKING.code}".toString()): 'false']
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS response shouldn't include trace information about no-called modules"
+        assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten()
+
+        and: "Ortb2blocking module call metrics shouldn't be updated"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+        assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS shouldn't call module and not override host config when default-account module-execution config enabled module"() {
+        given: "PBS service with module-execution and default account configs"
+        def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap {
+            hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): true]))
+        }
+        def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] +
+        [("hooks.admin.module-execution.${ORTB2_BLOCKING.code}".toString()): 'false']
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+
+        and: "Save account without modules config"
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: null))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS response shouldn't include trace information about no-called modules"
+        assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten()
+
+        and: "Ortb2blocking module call metrics shouldn't be updated"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+        assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS should call module without account module config when default-account module-execution config enabling module"() {
+        given: "PBS service with module-execution and default account configs"
+        def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap {
+            hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): true]))
+        }
+        def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)]
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+
+        and: "Save account without modules config"
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: null))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION]
+            it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort()
+        }
+
+        and: "Ortb2blocking module call metrics should be updated"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS shouldn't call any modules without account config when default-account module-execution config not enabling module"() {
+        given: "PBS service with module-execution and default account configs"
+        def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap {
+            hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): moduleExecutionStatus]))
+        }
+        def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)]
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+
+        and: "Save account without modules config"
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: null))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS response shouldn't include trace information about no-called modules"
+        assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten()
+
+        and: "Ortb2blocking module call metrics shouldn't be updated"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+        assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)]
+        assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)]
+
+        and: "RB-Richmedia-Filter module call metrics shouldn't be updated"
+        assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+
+        where:
+        moduleExecutionStatus << [false, null]
+    }
+
+    def "PBS should prioritize specific account module-execution config over default-account module-execution config when both are present"() {
+        given: "PBS service with default account config"
+        def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap {
+            hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): false]))
+        }
+        def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)]
+        def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default bid request with verbose trace"
+        def bidRequest = defaultBidRequest.tap {
+            ext.prebid.trace = TraceLevel.VERBOSE
+        }
+
+        and: "Save account without modules config"
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): true])))
+        def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig)
+        accountDao.save(account)
+
+        and: "Flush metrics"
+        flushMetrics(pbsServiceWithMultipleModules)
+
+        when: "PBS processes auction request"
+        def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest)
+
+        then: "PBS response should include trace information about called modules"
+        verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List<InvocationResult>) {
+            it.status == [SUCCESS, SUCCESS]
+            it.action == [NO_ACTION, NO_ACTION]
+            it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort()
+        }
+
+        and: "Ortb2blocking module call metrics should be updated"
+        def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest()
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1
+        assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1
+
+        and: "RB-Richmedia-Filter module call metrics shouldn't be updated"
+        assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)]
+        assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)]
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy
index 19cb2cd53de..453de43aa3c 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/module/ModuleBaseSpec.groovy
@@ -2,13 +2,16 @@ package org.prebid.server.functional.tests.module
 
 import org.prebid.server.functional.model.config.Endpoint
 import org.prebid.server.functional.model.config.ExecutionPlan
+import org.prebid.server.functional.model.config.Stage
 import org.prebid.server.functional.tests.BaseSpec
 
 import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING
+import static org.prebid.server.functional.model.ModuleName.PB_REQUEST_CORRECTION
 import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION
 import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER
 import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION
 import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES
+import static org.prebid.server.functional.model.config.Stage.PROCESSED_AUCTION_REQUEST
 
 class ModuleBaseSpec extends BaseSpec {
 
@@ -24,19 +27,20 @@ class ModuleBaseSpec extends BaseSpec {
     }
 
     protected static Map<String, String> getResponseCorrectionConfig(Endpoint endpoint = OPENRTB2_AUCTION) {
-        ["hooks.${PB_RESPONSE_CORRECTION.code}.enabled"        : true,
-         "hooks.host-execution-plan"                           : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_RESPONSE_CORRECTION, [ALL_PROCESSED_BID_RESPONSES]))]
+        ["hooks.${PB_RESPONSE_CORRECTION.code}.enabled": true,
+         "hooks.host-execution-plan"                   : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, [(ALL_PROCESSED_BID_RESPONSES): [PB_RESPONSE_CORRECTION]]))]
                 .collectEntries { key, value -> [(key.toString()): value.toString()] }
     }
 
     protected static Map<String, String> getRichMediaFilterSettings(String scriptPattern,
-                                                                    boolean filterMraidEnabled = true,
+                                                                    Boolean filterMraidEnabled = true,
                                                                     Endpoint endpoint = OPENRTB2_AUCTION) {
 
         ["hooks.${PB_RICHMEDIA_FILTER.code}.enabled"                     : true,
          "hooks.modules.${PB_RICHMEDIA_FILTER.code}.mraid-script-pattern": scriptPattern,
          "hooks.modules.${PB_RICHMEDIA_FILTER.code}.filter-mraid"        : filterMraidEnabled,
-         "hooks.host-execution-plan"                                     : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_RICHMEDIA_FILTER, [ALL_PROCESSED_BID_RESPONSES]))]
+         "hooks.host-execution-plan"                                     : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, [(ALL_PROCESSED_BID_RESPONSES): [PB_RICHMEDIA_FILTER]]))]
+                .findAll { it.value != null }
                 .collectEntries { key, value -> [(key.toString()): value.toString()] }
     }
 
@@ -51,4 +55,9 @@ class ModuleBaseSpec extends BaseSpec {
     protected static Map<String, String> getOrtb2BlockingSettings(boolean isEnabled = true) {
         ["hooks.${ORTB2_BLOCKING.code}.enabled": isEnabled as String]
     }
+
+    protected static Map<String, String> getRequestCorrectionSettings(Endpoint endpoint = OPENRTB2_AUCTION, Stage stage = PROCESSED_AUCTION_REQUEST) {
+        ["hooks.${PB_REQUEST_CORRECTION.code}.enabled": "true",
+         "hooks.host-execution-plan"                  : encode(ExecutionPlan.getSingleEndpointExecutionPlan(endpoint, PB_REQUEST_CORRECTION, [stage]))]
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy
index b37cae6a067..bb644508090 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/module/ortb2blocking/Ortb2BlockingSpec.groovy
@@ -2,6 +2,7 @@ package org.prebid.server.functional.tests.module.ortb2blocking
 
 import org.prebid.server.functional.model.bidder.BidderName
 import org.prebid.server.functional.model.bidder.Generic
+import org.prebid.server.functional.model.config.AccountAuctionConfig
 import org.prebid.server.functional.model.config.AccountConfig
 import org.prebid.server.functional.model.config.AccountHooksConfiguration
 import org.prebid.server.functional.model.config.ExecutionPlan
@@ -16,6 +17,8 @@ import org.prebid.server.functional.model.db.Account
 import org.prebid.server.functional.model.request.auction.Asset
 import org.prebid.server.functional.model.request.auction.Audio
 import org.prebid.server.functional.model.request.auction.Banner
+import org.prebid.server.functional.model.request.auction.BidderControls
+import org.prebid.server.functional.model.request.auction.GenericPreferredBidder
 import org.prebid.server.functional.model.request.auction.Ix
 import org.prebid.server.functional.model.request.auction.BidRequest
 import org.prebid.server.functional.model.request.auction.Imp
@@ -53,12 +56,12 @@ import static org.prebid.server.functional.testcontainers.Dependencies.getNetwor
 
 class Ortb2BlockingSpec extends ModuleBaseSpec {
 
+    private static final String WILDCARD = '*'
     private static final Map IX_CONFIG = ["adapters.ix.enabled" : "true",
                                           "adapters.ix.endpoint": "$networkServiceContainer.rootUri/auction".toString()]
-    private static final String WILDCARD = '*'
 
     private final PrebidServerService pbsServiceWithEnabledOrtb2Blocking = pbsServiceFactory.getService(ortb2BlockingSettings + IX_CONFIG +
-            ["adapters.generic.ortb.multiformat-supported": "true"])
+            ['adapter-defaults.ortb.multiformat-supported': 'false'])
 
     def "PBS should send original array ortb2 attribute to bidder when enforce blocking is disabled"() {
         given: "Default bid request with proper ortb attribute"
@@ -131,6 +134,134 @@ class Ortb2BlockingSpec extends ModuleBaseSpec {
         PBSUtils.randomNumber | BTYPE
     }
 
+    def "PBS shouldn't be able to send original battr ortb2 attribute when bid request imps type doesn't match with attribute type"() {
+        given: "Account in the DB with blocking configuration"
+        def ortb2Attribute = PBSUtils.randomNumber
+        def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attribute], attributeName)
+        accountDao.save(account)
+
+        and: "Default bidder response with ortb2 attributes"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName)]
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes the auction request"
+        def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest)
+
+        then: "PBS request shouldn't contain ortb2 attributes from account config for any media-type"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert !bidderRequest?.imp?.first?.banner?.battr
+        assert !bidderRequest?.imp?.first?.video?.battr
+        assert !bidderRequest?.imp?.first?.audio?.battr
+
+        and: "PBS request should contain single media type"
+        assert bidderRequest.imp.first.mediaTypes.size() == 1
+
+        and: "PBS response shouldn't contain any module errors"
+        assert !response?.ext?.prebid?.modules?.errors
+
+        and: "PBS response shouldn't contain any module warning"
+        assert !response?.ext?.prebid?.modules?.warnings
+
+        where:
+        bidRequest                     | attributeName
+        BidRequest.defaultVideoRequest | BANNER_BATTR
+        BidRequest.defaultAudioRequest | VIDEO_BATTR
+        BidRequest.defaultBidRequest   | AUDIO_BATTR
+    }
+
+    def "PBS shouldn't be able to send original battr ortb2 attribute when preferredMediaType doesn't match with attribute type"() {
+        given: "Default bid request with multiply types"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            imp.first.banner = Banner.defaultBanner
+            imp.first.video = Video.defaultVideo
+            imp.first.audio = Audio.defaultAudio
+            ext.prebid.bidderControls = new BidderControls(generic: new GenericPreferredBidder(preferredMediaType: preferredMediaType))
+        }
+
+        and: "Account in the DB with blocking configuration"
+        def ortb2Attribute = PBSUtils.randomNumber
+        def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attribute], attributeName)
+        accountDao.save(account)
+
+        and: "Default bidder response with ortb2 attributes"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName)]
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes the auction request"
+        def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest)
+
+        then: "PBS request shouldn't contain ortb2 attributes from account config for any media-type"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert !bidderRequest?.imp?.first?.banner?.battr
+        assert !bidderRequest?.imp?.first?.video?.battr
+        assert !bidderRequest?.imp?.first?.audio?.battr
+
+        and: "PBS request should contain only preferred media type"
+        assert bidderRequest.imp.first.mediaTypes == [preferredMediaType]
+
+        and: "PBS response shouldn't contain any module errors"
+        assert !response?.ext?.prebid?.modules?.errors
+
+        and: "PBS response shouldn't contain any module warning"
+        assert !response?.ext?.prebid?.modules?.warnings
+
+        where:
+        preferredMediaType | attributeName
+        VIDEO              | BANNER_BATTR
+        AUDIO              | VIDEO_BATTR
+        BANNER             | AUDIO_BATTR
+    }
+
+    def "PBS shouldn't be able to send original battr ortb2 attribute when account level preferredMediaType doesn't match with attribute type"() {
+        given: "Default bid request with multiply types"
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            imp.first.banner = Banner.defaultBanner
+            imp.first.video = Video.defaultVideo
+            imp.first.audio = Audio.defaultAudio
+        }
+
+        and: "Account in the DB with blocking configuration"
+        def ortb2Attribute = PBSUtils.randomNumber
+        def account = getAccountWithOrtb2BlockingConfig(bidRequest.accountId, [ortb2Attribute], attributeName).tap {
+            config.auction = new AccountAuctionConfig(preferredMediaType: [(GENERIC): preferredMediaType])
+        }
+        accountDao.save(account)
+
+        and: "Default bidder response with ortb2 attributes"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            it.seatbid.first.bid = [getBidWithOrtb2Attribute(bidRequest.imp.first, ortb2Attribute, attributeName)]
+        }
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes the auction request"
+        def response = pbsServiceWithEnabledOrtb2Blocking.sendAuctionRequest(bidRequest)
+
+        then: "PBS request shouldn't contain ortb2 attributes from account config for any media-type"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert !bidderRequest?.imp?.first?.banner?.battr
+        assert !bidderRequest?.imp?.first?.video?.battr
+        assert !bidderRequest?.imp?.first?.audio?.battr
+
+        and: "PBS request should contain only preferred media type"
+        assert bidderRequest.imp.first.mediaTypes == [preferredMediaType]
+
+        and: "PBS response shouldn't contain any module errors"
+        assert !response?.ext?.prebid?.modules?.errors
+
+        and: "PBS response shouldn't contain any module warning"
+        assert !response?.ext?.prebid?.modules?.warnings
+
+        where:
+        preferredMediaType | attributeName
+        VIDEO              | BANNER_BATTR
+        AUDIO              | VIDEO_BATTR
+        BANNER             | AUDIO_BATTR
+    }
+
     def "PBS shouldn't send original single ortb2 attribute to bidder when enforce blocking is disabled"() {
         given: "Default bid request with proper ortb attribute"
         def bidRequest = getBidRequestForOrtbAttribute(attributeName)
diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/pbrequestcorrection/PbRequestCorrectionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/pbrequestcorrection/PbRequestCorrectionSpec.groovy
new file mode 100644
index 00000000000..68b00bdd0d1
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/tests/module/pbrequestcorrection/PbRequestCorrectionSpec.groovy
@@ -0,0 +1,454 @@
+package org.prebid.server.functional.tests.module.pbrequestcorrection
+
+import org.prebid.server.functional.model.config.AccountConfig
+import org.prebid.server.functional.model.config.AccountHooksConfiguration
+import org.prebid.server.functional.model.config.PbRequestCorrectionConfig
+import org.prebid.server.functional.model.config.PbsModulesConfig
+import org.prebid.server.functional.model.db.Account
+import org.prebid.server.functional.model.request.auction.BidRequest
+import org.prebid.server.functional.model.request.auction.AppExt
+import org.prebid.server.functional.model.request.auction.AppPrebid
+import org.prebid.server.functional.model.request.auction.Device
+import org.prebid.server.functional.model.request.auction.Imp
+import org.prebid.server.functional.model.request.auction.OperationState
+import org.prebid.server.functional.service.PrebidServerService
+import org.prebid.server.functional.tests.module.ModuleBaseSpec
+import org.prebid.server.functional.util.PBSUtils
+
+import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP
+import static org.prebid.server.functional.model.request.auction.OperationState.YES
+
+class PbRequestCorrectionSpec extends ModuleBaseSpec {
+
+    private static final String PREBID_MOBILE = "prebid-mobile"
+    private static final String DEVICE_PREBID_MOBILE_PATTERN = "PrebidMobile/"
+    private static final String ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD = PBSUtils.getRandomVersion("0.0", "2.1.5")
+    private static final String ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD = PBSUtils.getRandomVersion("0.0", "2.2.3")
+    private static final String ANDROID = "android"
+    private static final String IOS = "IOS"
+
+    private PrebidServerService pbsServiceWithRequestCorrectionModule = pbsServiceFactory.getService(requestCorrectionSettings)
+
+    def "PBS should remove positive instl from imps for android app when request correction is enabled for account"() {
+        given: "Android APP bid request with version lover then version threshold"
+        def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD)
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            imp = imps
+            app.bundle = PBSUtils.getRandomCase(bundle)
+            app.ext = new AppExt(prebid: prebid)
+        }
+
+        and: "Account in the DB"
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request shouldn't contain imp.instl"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.imp.instl.every { it == null }
+
+        where:
+        imps                                                                                    | bundle                                                                   | requestCorrectionConfig
+        [Imp.defaultImpression.tap { instl = YES }]                                             | "$ANDROID${PBSUtils.randomString}"                                       | PbRequestCorrectionConfig.defaultConfigWithInterstitial
+        [Imp.defaultImpression.tap { instl = null }, Imp.defaultImpression.tap { instl = YES }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.randomString}"               | PbRequestCorrectionConfig.defaultConfigWithInterstitial
+        [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = null }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.getRandomNumber()}"          | PbRequestCorrectionConfig.defaultConfigWithInterstitial
+        [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = YES }]  | "$ANDROID${PBSUtils.randomString}_$ANDROID${PBSUtils.getRandomNumber()}" | PbRequestCorrectionConfig.defaultConfigWithInterstitial
+        [Imp.defaultImpression.tap { instl = YES }]                                             | "$ANDROID${PBSUtils.randomString}"                                       | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true)
+        [Imp.defaultImpression.tap { instl = null }, Imp.defaultImpression.tap { instl = YES }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.randomString}"               | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true)
+        [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = null }] | "${PBSUtils.randomString}$ANDROID${PBSUtils.getRandomNumber()}"          | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true)
+        [Imp.defaultImpression.tap { instl = YES }, Imp.defaultImpression.tap { instl = YES }]  | "$ANDROID${PBSUtils.randomString}_$ANDROID${PBSUtils.getRandomNumber()}" | new PbRequestCorrectionConfig(enabled: true, interstitialCorrectionEnabledKebabCase: true)
+    }
+
+    def "PBS shouldn't remove negative instl from imps for android app when request correction is enabled for account"() {
+        given: "Android APP bid request with version lover then version threshold"
+        def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD)
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            imp = imps
+            app.bundle = PBSUtils.getRandomCase(ANDROID)
+            app.ext = new AppExt(prebid: prebid)
+        }
+
+        and: "Account in the DB"
+        def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain original imp.instl"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.imp.instl == bidRequest.imp.instl
+
+        where:
+        imps << [[Imp.defaultImpression.tap { instl = OperationState.NO }],
+                 [Imp.defaultImpression.tap { instl = null }, Imp.defaultImpression.tap { instl = OperationState.NO }],
+                 [Imp.defaultImpression.tap { instl = OperationState.NO }, Imp.defaultImpression.tap { instl = null }],
+                 [Imp.defaultImpression.tap { instl = OperationState.NO }, Imp.defaultImpression.tap { instl = OperationState.NO }]]
+    }
+
+    def "PBS shouldn't remove positive instl from imps for not android or not prebid-mobile app when request correction is enabled for account"() {
+        given: "Android APP bid request with version lover then version threshold"
+        def prebid = new AppPrebid(source: PBSUtils.getRandomCase(source), version: PBSUtils.getRandomVersion(ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD))
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            imp.first.instl = YES
+            app.bundle = PBSUtils.getRandomCase(bundle)
+            app.ext = new AppExt(prebid: prebid)
+        }
+
+        and: "Account in the DB"
+        def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain original imp.instl"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.imp.instl == bidRequest.imp.instl
+
+        where:
+        bundle                | source
+        IOS                   | PREBID_MOBILE
+        PBSUtils.randomString | PREBID_MOBILE
+        ANDROID               | PBSUtils.randomString
+        ANDROID               | PBSUtils.randomString + PREBID_MOBILE
+        ANDROID               | PREBID_MOBILE + PBSUtils.randomString
+    }
+
+    def "PBS shouldn't remove positive instl from imps for app when request correction is enabled for account but some required parameter is empty"() {
+        given: "Android APP bid request with version lover then version threshold"
+        def prebid = new AppPrebid(source: source, version: version)
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            imp.first.instl = instl
+            app.bundle = bundle
+            app.ext = new AppExt(prebid: prebid)
+        }
+
+        and: "Account in the DB"
+        def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain original imp.instl"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.imp.instl == bidRequest.imp.instl
+
+        where:
+        bundle  | source        | version                                   | instl
+        null    | PREBID_MOBILE | ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD | YES
+        ANDROID | null          | ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD | YES
+        ANDROID | PREBID_MOBILE | null                                      | YES
+        ANDROID | PREBID_MOBILE | ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD | null
+    }
+
+    def "PBS shouldn't remove positive instl from imps for android app when request correction is enabled for account and version is threshold"() {
+        given: "Android APP bid request with version threshold"
+        def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: "2.2.3")
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            imp.first.instl = YES
+            app.bundle = PBSUtils.getRandomCase(ANDROID)
+            app.ext = new AppExt(prebid: prebid)
+        }
+
+        and: "Account in the DB"
+        def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain original imp.instl"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.imp.instl == bidRequest.imp.instl
+    }
+
+    def "PBS shouldn't remove positive instl from imps for android app when request correction is enabled for account and version is higher then threshold"() {
+        given: "Android APP bid request with version higher then version threshold"
+        def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: PBSUtils.getRandomVersion("2.2.4"))
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            imp.first.instl = YES
+            app.bundle = PBSUtils.getRandomCase(ANDROID)
+            app.ext = new AppExt(prebid: prebid)
+        }
+
+        and: "Account in the DB"
+        def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithInterstitial
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain original imp.instl"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.imp.instl == bidRequest.imp.instl
+    }
+
+    def "PBS shouldn't remove positive instl from imps for android app when request correction is disabled for account"() {
+        given: "Android APP bid request with version lover then version threshold"
+        def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD)
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            imp.first.instl = YES
+            app.bundle = PBSUtils.getRandomCase(ANDROID)
+            app.ext = new AppExt(prebid: prebid)
+        }
+
+        and: "Account in the DB"
+        def requestCorrectionConfig = PbRequestCorrectionConfig.getDefaultConfigWithInterstitial(interstitialCorrectionEnabled, enabled)
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain original imp.instl"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.imp.instl == bidRequest.imp.instl
+
+        where:
+        enabled | interstitialCorrectionEnabled
+        false   | true
+        null    | true
+        true    | false
+        true    | null
+        null    | null
+    }
+
+    def "PBS shouldn't remove positive instl from imps for android app when request correction is not applied for account"() {
+        given: "Android APP bid request with version lover then version threshold"
+        def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_INSTL_VERSION_THRESHOLD)
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            imp.first.instl = YES
+            app.bundle = PBSUtils.getRandomCase(ANDROID)
+            app.ext = new AppExt(prebid: prebid)
+        }
+
+        and: "Account in the DB"
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig()))
+        def account = new Account(uuid: bidRequest.accountId, config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain original imp.instl"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.imp.instl == bidRequest.imp.instl
+    }
+
+    def "PBS should remove pattern device.ua when request correction is enabled for account and user agent correction enabled"() {
+        given: "Android APP bid request with version lover then version threshold"
+        def prebid = new AppPrebid(source: PREBID_MOBILE, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD)
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            app.ext = new AppExt(prebid: prebid)
+            device = new Device(ua: deviceUa)
+        }
+
+        and: "Account in the DB"
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request shouldn't contain device.ua"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert !bidderRequest.device.ua
+
+        where:
+        deviceUa                                                                          | requestCorrectionConfig
+        "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}"                         | PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection
+        "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}${PBSUtils.randomString}" | PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection
+        "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}"                         | new PbRequestCorrectionConfig(enabled: true, userAgentCorrectionEnabledKebabCase: true)
+        "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}${PBSUtils.randomString}" | new PbRequestCorrectionConfig(enabled: true, userAgentCorrectionEnabledKebabCase: true)
+    }
+
+    def "PBS should remove only pattern device.ua when request correction is enabled for account and user agent correction enabled"() {
+        given: "Android APP bid request with version lover then version threshold"
+        def prebid = new AppPrebid(source: PREBID_MOBILE, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD)
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            app.ext = new AppExt(prebid: prebid)
+            device = new Device(ua: deviceUa)
+        }
+
+        and: "Account in the DB"
+        def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain device.ua"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.device.ua.contains(deviceUa.replaceAll("PrebidMobile/[0-9][^ ]*", '').trim())
+
+        where:
+        deviceUa << ["${PBSUtils.randomNumber} ${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber} ${PBSUtils.randomString}",
+                     "${PBSUtils.randomString} ${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}${PBSUtils.randomString} ${PBSUtils.randomString}",
+                     "${DEVICE_PREBID_MOBILE_PATTERN}",
+                     "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}",
+                     "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber} ${PBSUtils.randomString}"
+        ]
+    }
+
+    def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and user agent correction disabled"() {
+        given: "Android APP bid request with version lover then version threshold"
+        def deviceUserAgent = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}"
+        def prebid = new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD)
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            app.ext = new AppExt(prebid: prebid)
+            device = new Device(ua: deviceUserAgent)
+        }
+
+        and: "Account in the DB"
+        def requestCorrectionConfig = PbRequestCorrectionConfig.getDefaultConfigWithUserAgentCorrection(userAgentCorrectionEnabled, enabled)
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain device.ua"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.device.ua == deviceUserAgent
+
+        where:
+        enabled | userAgentCorrectionEnabled
+        false   | true
+        null    | true
+        true    | false
+        true    | null
+        null    | null
+    }
+
+    def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and source not a prebid-mobile"() {
+        given: "Android APP bid request with version lover then version threshold"
+        def randomDeviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}"
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            app.ext = new AppExt(prebid: new AppPrebid(source: source, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD))
+            device = new Device(ua: randomDeviceUa)
+        }
+
+        and: "Account in the DB"
+        def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain device.ua"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.device.ua == randomDeviceUa
+
+        where:
+        source << ["prebid",
+                   "mobile",
+                   PREBID_MOBILE + PBSUtils.randomString,
+                   PBSUtils.randomString + PREBID_MOBILE,
+                   "mobile-prebid",
+                   PBSUtils.randomString]
+    }
+
+    def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and version biggest that threshold"() {
+        given: "Android APP bid request with version higher then version threshold"
+        def randomDeviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}"
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            app.ext = new AppExt(prebid: new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: PBSUtils.getRandomVersion("2.1.6")))
+            device = new Device(ua: randomDeviceUa)
+        }
+
+        and: "Account in the DB"
+        def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain device.ua"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.device.ua == randomDeviceUa
+    }
+
+    def "PBS shouldn't remove pattern device.ua when request correction is enabled for account and version threshold"() {
+        given: "Android APP bid request with version threshold"
+        def randomDeviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}"
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            app.ext = new AppExt(prebid: new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: "2.1.6"))
+            device = new Device(ua: randomDeviceUa)
+        }
+
+        and: "Account in the DB"
+        def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain device.ua"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.device.ua == randomDeviceUa
+    }
+
+    def "PBS shouldn't remove device.ua pattern when request correction is enabled for account and version threshold"() {
+        given: "Android APP bid request with version higher then version threshold"
+        def randomDeviceUa = PBSUtils.randomString
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            app.ext = new AppExt(prebid: new AppPrebid(source: PBSUtils.getRandomCase(PREBID_MOBILE), version: PBSUtils.getRandomVersion("2.1.6")))
+            device = new Device(ua: randomDeviceUa)
+        }
+
+        and: "Account in the DB"
+        def requestCorrectionConfig = PbRequestCorrectionConfig.defaultConfigWithUserAgentCorrection
+        def account = createAccountWithRequestCorrectionConfig(bidRequest, requestCorrectionConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain device.ua"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.device.ua == randomDeviceUa
+    }
+
+    def "PBS shouldn't remove device.ua pattern from device for android app when request correction is not applied for account"() {
+        given: "Android APP bid request with version lover then version threshold"
+        def prebid = new AppPrebid(source: PREBID_MOBILE, version: ACCEPTABLE_DEVICE_UA_VERSION_THRESHOLD)
+        def deviceUa = "${DEVICE_PREBID_MOBILE_PATTERN}${PBSUtils.randomNumber}"
+        def bidRequest = BidRequest.getDefaultBidRequest(APP).tap {
+            app.ext = new AppExt(prebid: prebid)
+            device = new Device(ua: deviceUa)
+        }
+
+        and: "Account in the DB"
+        def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig()))
+        def account = new Account(uuid: bidRequest.accountId, config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        pbsServiceWithRequestCorrectionModule.sendAuctionRequest(bidRequest)
+
+        then: "Bidder request should contain request device ua"
+        def bidderRequest = bidder.getBidderRequest(bidRequest.id)
+        assert bidderRequest.device.ua == deviceUa
+    }
+
+    private static Account createAccountWithRequestCorrectionConfig(BidRequest bidRequest,
+                                                                    PbRequestCorrectionConfig requestCorrectionConfig) {
+        def pbsModulesConfig = new PbsModulesConfig(pbRequestCorrection: requestCorrectionConfig)
+        def accountHooksConfig = new AccountHooksConfiguration(modules: pbsModulesConfig)
+        def accountConfig = new AccountConfig(hooks: accountHooksConfig)
+        new Account(uuid: bidRequest.accountId, config: accountConfig)
+    }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy
index b12ff9644b4..c49743b275b 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/module/richmedia/RichMediaFilterSpec.groovy
@@ -35,6 +35,51 @@ class RichMediaFilterSpec extends ModuleBaseSpec {
                     .collectEntries { key, value -> [(key.toString()): value.toString()] })
     private final PrebidServerService pbsServiceWithDisabledMediaFilter = pbsServiceFactory.getService(getRichMediaFilterSettings(PATTERN_NAME, false))
 
+    def "PBS should process request without rich media module when host config have empty settings"() {
+        given: "Prebid server with empty settings for module"
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
+
+        and: "BidRequest with stored response"
+        def storedResponseId = PBSUtils.randomNumber
+        def bidRequest = BidRequest.defaultBidRequest.tap {
+            ext.prebid.returnAllBidStatus = true
+            it.ext.prebid.trace = VERBOSE
+            it.imp.first().ext.prebid.storedBidResponse = [new StoredBidResponse(id: storedResponseId, bidder: GENERIC)]
+        }
+
+        and: "Stored bid response in DB"
+        def storedBidResponse = BidResponse.getDefaultBidResponse(bidRequest).tap {
+            it.seatbid[0].bid[0].adm = PBSUtils.randomString
+        }
+        def storedResponse = new StoredResponse(responseId: storedResponseId, storedBidResponse: storedBidResponse)
+        storedResponseDao.save(storedResponse)
+
+        and: "Account in the DB"
+        def account = new Account(uuid: bidRequest.getAccountId())
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = prebidServerService.sendAuctionRequest(bidRequest)
+
+        then: "Response header should contain seatbid"
+        assert response.seatbid.size() == 1
+
+        and: "Response shouldn't contain errors of invalid creation"
+        assert !response.ext.errors
+
+        and: "Response shouldn't contain analytics"
+        assert !getAnalyticResults(response)
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+
+        where:
+        pbsConfig << [getRichMediaFilterSettings(PBSUtils.randomString, null),
+                      getRichMediaFilterSettings(null, true),
+                      getRichMediaFilterSettings(null, false),
+                      getRichMediaFilterSettings(null, null)]
+    }
+
     def "PBS should process request without analytics when adm matches with pattern name and filter set to disabled in host config"() {
         given: "BidRequest with stored response"
         def storedResponseId = PBSUtils.randomNumber
diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy
index 71c7621eb20..8b3f5d936bd 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsBaseSpec.groovy
@@ -25,6 +25,7 @@ import org.prebid.server.functional.util.PBSUtils
 
 import java.math.RoundingMode
 
+import static org.prebid.server.functional.model.request.auction.DebugCondition.ENABLED
 import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE
 import static org.prebid.server.functional.model.request.auction.FetchStatus.INPROGRESS
 import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer
@@ -36,14 +37,14 @@ abstract class PriceFloorsBaseSpec extends BaseSpec {
     public static final Map<String, String> FLOORS_CONFIG = ["price-floors.enabled"           : "true",
                                                              "settings.default-account-config": encode(defaultAccountConfigSettings)]
 
-    protected static final String basicFetchUrl = networkServiceContainer.rootUri + FloorsProvider.FLOORS_ENDPOINT
-    protected static final FloorsProvider floorsProvider = new FloorsProvider(networkServiceContainer)
+    protected static final String BASIC_FETCH_URL = networkServiceContainer.rootUri + FloorsProvider.FLOORS_ENDPOINT
     protected static final int MAX_MODEL_WEIGHT = 100
 
     private static final int DEFAULT_MODEL_WEIGHT = 1
     private static final int CURRENCY_CONVERSION_PRECISION = 3
     private static final int FLOOR_VALUE_PRECISION = 4
 
+    protected static final FloorsProvider floorsProvider = new FloorsProvider(networkServiceContainer)
     protected final PrebidServerService floorsPbsService = pbsServiceFactory.getService(FLOORS_CONFIG + GENERIC_ALIAS_CONFIG)
 
     def setupSpec() {
@@ -56,19 +57,22 @@ abstract class PriceFloorsBaseSpec extends BaseSpec {
                 maxRules: 0,
                 maxFileSizeKb: 200,
                 maxAgeSec: 86400,
-                periodSec: 3600)
+                periodSec: 3600,
+                maxSchemaDims: 5)
         def floors = new AccountPriceFloorsConfig(enabled: true,
                 fetch: fetch,
                 enforceFloorsRate: 100,
                 enforceDealFloors: true,
                 adjustForBidAdjustment: true,
-                useDynamicData: true)
+                useDynamicData: true,
+                maxRules: 0,
+                maxSchemaDims: 3)
         new AccountConfig(auction: new AccountAuctionConfig(priceFloors: floors))
     }
 
     protected static Account getAccountWithEnabledFetch(String accountId) {
         def priceFloors = new AccountPriceFloorsConfig(enabled: true,
-                fetch: new PriceFloorsFetch(url: basicFetchUrl + accountId, enabled: true))
+                fetch: new PriceFloorsFetch(url: BASIC_FETCH_URL + accountId, enabled: true))
         def accountConfig = new AccountConfig(auction: new AccountAuctionConfig(priceFloors: priceFloors))
         new Account(uuid: accountId, config: accountConfig)
     }
@@ -85,7 +89,7 @@ abstract class PriceFloorsBaseSpec extends BaseSpec {
     static BidRequest getStoredRequestWithFloors(DistributionChannel channel = SITE) {
         channel == SITE
                 ? BidRequest.defaultStoredRequest.tap { ext.prebid.floors = ExtPrebidFloors.extPrebidFloors }
-                : new BidRequest(ext: new BidRequestExt(prebid: new Prebid(debug: 1, floors: ExtPrebidFloors.extPrebidFloors)))
+                : new BidRequest(ext: new BidRequestExt(prebid: new Prebid(debug: ENABLED, floors: ExtPrebidFloors.extPrebidFloors)))
 
     }
 
diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy
index 581a71644a5..786a70a6b86 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsCurrencySpec.groovy
@@ -243,7 +243,7 @@ class PriceFloorsCurrencySpec extends PriceFloorsBaseSpec {
         def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.enabled = priceFloors
             config.auction.priceFloorsSnakeCase = new AccountPriceFloorsConfig(enabled: true,
-                    fetch: new PriceFloorsFetch(url: basicFetchUrl + bidRequest.accountId, enabled: priceFloorsSnakeCase))
+                    fetch: new PriceFloorsFetch(url: BASIC_FETCH_URL + bidRequest.accountId, enabled: priceFloorsSnakeCase))
         }
         accountDao.save(account)
 
diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy
index 8316ee54233..55180fe60d4 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsFetchingSpec.groovy
@@ -17,28 +17,31 @@ import java.time.Instant
 import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400
 import static org.prebid.server.functional.model.Currency.EUR
 import static org.prebid.server.functional.model.Currency.JPY
-import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
 import static org.prebid.server.functional.model.pricefloors.Country.MULTIPLE
 import static org.prebid.server.functional.model.pricefloors.MediaType.BANNER
 import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP
+import static org.prebid.server.functional.model.request.auction.DistributionChannel.SITE
 import static org.prebid.server.functional.model.request.auction.FetchStatus.ERROR
 import static org.prebid.server.functional.model.request.auction.FetchStatus.NONE
 import static org.prebid.server.functional.model.request.auction.FetchStatus.SUCCESS
 import static org.prebid.server.functional.model.request.auction.Location.FETCH
+import static org.prebid.server.functional.model.request.auction.Location.NO_DATA
 import static org.prebid.server.functional.model.request.auction.Location.REQUEST
 import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID
 
 class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
-    private static final int MAX_ENFORCE_FLOORS_RATE = 100
+    private static final int ENFORCE_FLOORS_RATE_MAX = 100
     private static final int DEFAULT_MAX_AGE_SEC = 600
     private static final int DEFAULT_PERIOD_SEC = 300
-    private static final int MIN_TIMEOUT_MS = 10
-    private static final int MAX_TIMEOUT_MS = 10000
-    private static final int MIN_SKIP_RATE = 0
-    private static final int MAX_SKIP_RATE = 100
-    private static final int MIN_DEFAULT_FLOOR_VALUE = 0
-    private static final int MIN_FLOOR_MIN = 0
+    private static final int TIMEOUT_MS_MIN = 10
+    private static final int TIMEOUT_MS_MAX = 10000
+    private static final int SKIP_RATE_MIN = 0
+    private static final int SKIP_RATE_MAX = 100
+    private static final int USE_FETCH_DATA_RATE_MIN = 0
+    private static final int USE_FETCH_DATA_RATE_MAX = 100
+    private static final int DEFAULT_FLOOR_VALUE_MIN = 0
+    private static final int FLOOR_MIN = 0
 
     private static final Closure<String> INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" }
     private static final String FETCH_FAILURE_METRIC = "price-floors.fetch.failure"
@@ -51,7 +54,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch and fetch.url in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id)
+        def account = getAccountWithEnabledFetch(bidRequest.accountId)
         accountDao.save(account)
 
         and: "PBS fetch rules from floors provider"
@@ -61,7 +64,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         pbsService.sendAuctionRequest(bidRequest)
 
         then: "PBS should fetch data"
-        assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1
+        assert floorsProvider.getRequestCount(bidRequest.accountId) == 1
 
         and: "PBS should signal bids"
         def bidderRequest = bidder.getBidderRequests(bidRequest.id).last()
@@ -76,7 +79,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch and fetch.url in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.enabled = accountConfigEnabled
         }
         accountDao.save(account)
@@ -88,7 +91,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         pbsService.sendAuctionRequest(bidRequest)
 
         then: "PBS should no fetching, no signaling, no enforcing"
-        assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 0
+        assert floorsProvider.getRequestCount(bidRequest.accountId) == 0
         def bidderRequest = bidder.getBidderRequests(bidRequest.id).last()
         assert !bidderRequest.imp[0].bidFloor
 
@@ -106,7 +109,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.defaultBidRequest
 
         and: "Account with enabled fetch, without fetch.url in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.url = null
         }
         accountDao.save(account)
@@ -116,9 +119,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         then: "PBS should log error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, bidRequest.site.publisher.id)
+        def floorsLogs = getLogsByText(logs, bidRequest.accountId)
         assert floorsLogs.size() == 1
-        assert floorsLogs.first().contains("Malformed fetch.url: 'null', passed for account $bidRequest.site.publisher.id")
+        assert floorsLogs.first().contains("Malformed fetch.url: 'null', passed for account $bidRequest.accountId")
 
         and: "PBS floors validation failure should not reject the entire auction"
         assert !response.seatbid.isEmpty()
@@ -133,8 +136,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch, fetch.url, maxAgeSec in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap {
-            config.auction.priceFloors.fetch = fetchConfig(bidRequest.app.publisher.id, DEFAULT_MAX_AGE_SEC - 1)
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+            config.auction.priceFloors.fetch = fetchConfig(bidRequest.accountId, DEFAULT_MAX_AGE_SEC - 1)
         }
         accountDao.save(account)
 
@@ -143,14 +146,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         then: "Metric alerts.account_config.ACCOUNT.price-floors should be update"
         def metrics = floorsPbsService.sendCollectedMetricsRequest()
-        assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1
+        assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
 
         and: "PBS floors validation failure should not reject the entire auction"
         assert !response.seatbid.isEmpty()
 
         where:
-        fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxAgeSec: max) },
-                        { String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxAgeSecSnakeCase: max) }]
+        fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxAgeSec: max) },
+                        { String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxAgeSecSnakeCase: max) }]
     }
 
     def "PBS should validate fetch.period-sec from account config"() {
@@ -158,7 +161,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch, fetch.url, periodSec in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch = fetchConfig(DEFAULT_PERIOD_SEC,
                     defaultAccountConfigSettings.auction.priceFloors.fetch.maxAgeSec)
         }
@@ -169,7 +172,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         then: "Metric alerts.account_config.ACCOUNT.price-floors  should be update"
         def metrics = floorsPbsService.sendCollectedMetricsRequest()
-        assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1
+        assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
 
         and: "PBS floors validation failure should not reject the entire auction"
         assert !response.seatbid?.isEmpty()
@@ -186,7 +189,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch, fetch.url, maxFileSizeKb in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.maxFileSizeKb = PBSUtils.randomNegativeNumber
         }
         accountDao.save(account)
@@ -196,7 +199,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         then: "Metric alerts.account_config.ACCOUNT.price-floors should be update"
         def metrics = floorsPbsService.sendCollectedMetricsRequest()
-        assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1
+        assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
 
         and: "PBS floors validation failure should not reject the entire auction"
         assert !response.seatbid?.isEmpty()
@@ -207,7 +210,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch, fetch.url, maxRules in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.maxRules = PBSUtils.randomNegativeNumber
         }
         accountDao.save(account)
@@ -217,7 +220,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         then: "Metric alerts.account_config.ACCOUNT.price-floors  should be update"
         def metrics = floorsPbsService.sendCollectedMetricsRequest()
-        assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1
+        assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
 
         and: "PBS floors validation failure should not reject the entire auction"
         assert !response.seatbid?.isEmpty()
@@ -228,8 +231,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch, fetch.url, timeoutMs in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap {
-            config.auction.priceFloors.fetch = fetchConfig(MIN_TIMEOUT_MS, MAX_TIMEOUT_MS)
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+            config.auction.priceFloors.fetch = fetchConfig(TIMEOUT_MS_MIN, TIMEOUT_MS_MAX)
         }
         accountDao.save(account)
 
@@ -238,7 +241,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         then: "Metric alerts.account_config.ACCOUNT.price-floors  should be update"
         def metrics = floorsPbsService.sendCollectedMetricsRequest()
-        assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1
+        assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
 
         and: "PBS floors validation failure should not reject the entire auction"
         assert !response.seatbid?.isEmpty()
@@ -255,7 +258,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch, fetch.url, enforceFloorsRate in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.tap {
                 it.enforceFloorsRate = enforceFloorsRate
                 it.enforceFloorsRateSnakeCase = enforceFloorsRateSnakeCase
@@ -268,7 +271,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         then: "Metric alerts.account_config.ACCOUNT.price-floors  should be update"
         def metrics = floorsPbsService.sendCollectedMetricsRequest()
-        assert metrics[INVALID_CONFIG_METRIC(bidRequest.app.publisher.id) as String] == 1
+        assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
 
         and: "PBS floors validation failure should not reject the entire auction"
         assert !response.seatbid?.isEmpty()
@@ -276,9 +279,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         where:
         enforceFloorsRate             | enforceFloorsRateSnakeCase
         PBSUtils.randomNegativeNumber | null
-        MAX_ENFORCE_FLOORS_RATE + 1   | null
+        ENFORCE_FLOORS_RATE_MAX + 1   | null
         null                          | PBSUtils.randomNegativeNumber
-        null                          | MAX_ENFORCE_FLOORS_RATE + 1
+        null                          | ENFORCE_FLOORS_RATE_MAX + 1
     }
 
     def "PBS should fetch data from provider when price-floors.fetch.enabled = true in account config"() {
@@ -288,7 +291,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         }
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.enabled = true
         }
         accountDao.save(account)
@@ -297,7 +300,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         floorsPbsService.sendAuctionRequest(bidRequest)
 
         then: "PBS should fetch data from floors provider"
-        assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1
+        assert floorsProvider.getRequestCount(bidRequest.accountId) == 1
     }
 
     def "PBS should process floors from request when price-floors.fetch.enabled = false in account config"() {
@@ -305,7 +308,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = bidRequestWithFloors
 
         and: "Account with fetch.enabled, fetch.url in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch = fetch
         }
         accountDao.save(account)
@@ -320,17 +323,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         floorsPbsService.sendAuctionRequest(bidRequest)
 
         then: "PBS should not fetch data from floors provider"
-        assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 0
+        assert floorsProvider.getRequestCount(bidRequest.accountId) == 0
 
         and: "Bidder request should contain bidFloor from request"
         def bidderRequest = bidder.getBidderRequest(bidRequest.id)
         assert bidderRequest.imp[0].bidFloor == bidRequest.imp[0].bidFloor
 
         where:
-        fetch << [new PriceFloorsFetch(enabled: false, url: basicFetchUrl), new PriceFloorsFetch(url: basicFetchUrl)]
+        fetch << [new PriceFloorsFetch(enabled: false, url: BASIC_FETCH_URL), new PriceFloorsFetch(url: BASIC_FETCH_URL)]
     }
 
-    def "PBS should fetch data from provider when use-dynamic-data = true"() {
+    def "PBS should fetch data from provider when use-dynamic-data enabled"() {
         given: "Pbs with PF configuration with useDynamicData"
         def defaultAccountConfigSettings = defaultAccountConfigSettings.tap {
             auction.priceFloors.tap {
@@ -347,7 +350,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         }
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.useDynamicData = accountUseDynamicData
             config.auction.priceFloors.useDynamicDataSnakeCase = accountUseDynamicDataSnakeCase
         }
@@ -358,13 +361,13 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def floorsResponse = PriceFloorData.priceFloorData.tap {
             modelGroups[0].values = [(rule): floorValue]
         }
-        floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse)
+        floorsProvider.setResponse(bidRequest.accountId, floorsResponse)
 
         when: "PBS cache rules and processes auction request"
         cacheFloorsProviderRules(bidRequest, floorValue, pbsService)
 
         then: "PBS should fetch data from floors provider"
-        assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1
+        assert floorsProvider.getRequestCount(bidRequest.accountId) == 1
 
         and: "Bidder request should contain bidFloor from request"
         def bidderRequest = bidder.getBidderRequests(bidRequest.id).last()
@@ -382,6 +385,211 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         null                    | null                  | null                             | true
     }
 
+    def "PBS should fetch data from provider when use-dynamic-data enabled and useFetchDataRate at max value"() {
+        given: "Default BidRequest"
+        def bidRequest = BidRequest.defaultBidRequest
+
+        and: "Account with enabled use-dynamic-data parameter"
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+            config.auction.priceFloors.useDynamicData = true
+        }
+        accountDao.save(account)
+
+        and: "Set Floors Provider response"
+        def floorValue = PBSUtils.randomFloorValue
+        def floorsResponse = PriceFloorData.priceFloorData.tap {
+            modelGroups[0].values = [(rule): floorValue]
+            useFetchDataRate = USE_FETCH_DATA_RATE_MAX
+        }
+        floorsProvider.setResponse(bidRequest.accountId, floorsResponse)
+
+        and: "PBS fetch rules from floors provider"
+        cacheFloorsProviderRules(bidRequest, floorsPbsService)
+
+        when: "PBS processes auction request"
+        floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should fetch data"
+        assert floorsProvider.getRequestCount(bidRequest.accountId) == 1
+
+        and: "Bidder request should contain floors data from floors provider"
+        def bidderRequest = bidder.getBidderRequests(bidRequest.id).last
+        verifyAll(bidderRequest) {
+            imp[0].bidFloor == floorValue
+            imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency
+
+            imp[0].ext?.prebid?.floors?.floorRule == floorsResponse.modelGroups[0].values.keySet()[0]
+            imp[0].ext?.prebid?.floors?.floorRuleValue == floorValue
+            imp[0].ext?.prebid?.floors?.floorValue == floorValue
+
+            ext?.prebid?.floors?.location == FETCH
+            ext?.prebid?.floors?.fetchStatus == SUCCESS
+            ext?.prebid?.floors?.floorProvider == floorsResponse.floorProvider
+
+            ext?.prebid?.floors?.skipRate == floorsResponse.skipRate
+            ext?.prebid?.floors?.data == floorsResponse
+        }
+    }
+
+    def "PBS shouldn't fetch data from provider when use-dynamic-data disabled and useFetchDataRate at max value"() {
+        given: "Default BidRequest"
+        def bidRequest = BidRequest.defaultBidRequest
+
+        and: "Account with disabled use-dynamic-data parameter"
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+            config.auction.priceFloors.useDynamicData = false
+        }
+        accountDao.save(account)
+
+        and: "Set Floors Provider response"
+        def floorValue = PBSUtils.randomFloorValue
+        def floorsResponse = PriceFloorData.priceFloorData.tap {
+            modelGroups[0].values = [(rule): floorValue]
+            useFetchDataRate = USE_FETCH_DATA_RATE_MAX
+        }
+        floorsProvider.setResponse(bidRequest.accountId, floorsResponse)
+
+        and: "PBS fetch rules from floors provider"
+        cacheFloorsProviderRules(bidRequest, floorsPbsService)
+
+        when: "PBS processes auction request"
+        floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should fetch data"
+        assert floorsProvider.getRequestCount(bidRequest.accountId) == 1
+
+        and: "Bidder request shouldn't contain floors data from floors provider"
+        def bidderRequest = bidder.getBidderRequests(bidRequest.id).last
+        verifyAll(bidderRequest) {
+            !imp[0].bidFloor
+            !imp[0].bidFloorCur
+
+            !imp[0].ext?.prebid?.floors?.floorRule
+            !imp[0].ext?.prebid?.floors?.floorRuleValue
+            !imp[0].ext?.prebid?.floors?.floorValue
+
+            !ext?.prebid?.floors?.skipRate
+            !ext?.prebid?.floors?.data
+            !ext?.prebid?.floors?.floorProvider
+            ext?.prebid?.floors?.location == NO_DATA
+            ext?.prebid?.floors?.fetchStatus == SUCCESS
+        }
+    }
+
+    def "PBS shouldn't fetch data from provider when use-dynamic-data enabled and useFetchDataRate at min value"() {
+        given: "Default BidRequest"
+        def bidRequest = BidRequest.defaultBidRequest
+
+        and: "Account with enabled use-dynamic-data parameter"
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+            config.auction.priceFloors.useDynamicData = true
+        }
+        accountDao.save(account)
+
+        and: "Set Floors Provider response"
+        def floorValue = PBSUtils.randomFloorValue
+        def floorsResponse = PriceFloorData.priceFloorData.tap {
+            modelGroups[0].values = [(rule): floorValue]
+            useFetchDataRate = USE_FETCH_DATA_RATE_MIN
+        }
+        floorsProvider.setResponse(bidRequest.accountId, floorsResponse)
+
+        and: "PBS fetch rules from floors provider"
+        cacheFloorsProviderRules(bidRequest, floorsPbsService)
+
+        when: "PBS processes auction request"
+        floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should fetch data"
+        assert floorsProvider.getRequestCount(bidRequest.accountId) == 1
+
+        and: "Bidder request shouldn't contain floors data from floors provider"
+        def bidderRequest = bidder.getBidderRequests(bidRequest.id).last
+        verifyAll(bidderRequest) {
+            !imp[0].bidFloor
+            !imp[0].bidFloorCur
+
+            !imp[0].ext?.prebid?.floors?.floorRule
+            !imp[0].ext?.prebid?.floors?.floorRuleValue
+            !imp[0].ext?.prebid?.floors?.floorValue
+
+            !ext?.prebid?.floors?.skipRate
+            !ext?.prebid?.floors?.data
+            !ext?.prebid?.floors?.floorProvider
+            ext?.prebid?.floors?.location == NO_DATA
+            ext?.prebid?.floors?.fetchStatus == SUCCESS
+        }
+    }
+
+    def "PBS should log error and increase metrics when useFetchDataRate have invalid value"() {
+        given: "Test start time"
+        def startTime = Instant.now()
+
+        and: "Default BidRequest"
+        def bidRequest = BidRequest.defaultBidRequest
+
+        and: "Account with enabled fetch and fetch.url in the DB"
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+            config.auction.priceFloors.useDynamicData = true
+        }
+        accountDao.save(account)
+
+        and: "Flush metrics"
+        flushMetrics(floorsPbsService)
+
+        and: "Set Floors Provider response"
+        def floorValue = PBSUtils.randomFloorValue
+        def floorsResponse = PriceFloorData.priceFloorData.tap {
+            modelGroups[0].values = [(rule): floorValue]
+            useFetchDataRate = accounntUseFetchDataRate
+        }
+        floorsProvider.setResponse(bidRequest.accountId, floorsResponse)
+
+        and: "PBS fetch rules from floors provider"
+        cacheFloorsProviderRules(bidRequest, floorsPbsService)
+
+        when: "PBS processes auction request"
+        def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "metric should be updated"
+        def metrics = floorsPbsService.sendCollectedMetricsRequest()
+        assert metrics[FETCH_FAILURE_METRIC] == 1
+
+        then: "PBS should fetch data"
+        assert floorsProvider.getRequestCount(bidRequest.accountId) == 1
+
+        and: "PBS log should contain error"
+        def logs = floorsPbsService.getLogsByTime(startTime)
+        def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$bidRequest.accountId")
+        assert floorsLogs.size() == 1
+        assert floorsLogs[0].contains("reason : Price floor data useFetchDataRate must be in range(0-100), but was $accounntUseFetchDataRate")
+
+        and: "Floors validation failure cannot reject the entire auction"
+        assert !response.seatbid?.isEmpty()
+
+        and: "Bidder request should contain floors data from floors provider"
+        def bidderRequest = bidder.getBidderRequests(bidRequest.id).last
+        verifyAll(bidderRequest) {
+            !imp[0].bidFloor
+            !imp[0].bidFloorCur
+
+            !imp[0].ext?.prebid?.floors?.floorRule
+            !imp[0].ext?.prebid?.floors?.floorRuleValue
+            !imp[0].ext?.prebid?.floors?.floorValue
+
+            !ext?.prebid?.floors?.floorProvider
+            !ext?.prebid?.floors?.skipRate
+            !ext?.prebid?.floors?.data
+            ext?.prebid?.floors?.location == NO_DATA
+            ext?.prebid?.floors?.fetchStatus == ERROR
+        }
+
+        where:
+        accounntUseFetchDataRate << [PBSUtils.getRandomNegativeNumber(-USE_FETCH_DATA_RATE_MAX, USE_FETCH_DATA_RATE_MIN),
+                                     PBSUtils.getRandomNumber(USE_FETCH_DATA_RATE_MAX + 1)
+        ]
+    }
+
     def "PBS should process floors from request when use-dynamic-data = false"() {
         given: "Pbs with PF configuration with useDynamicData"
         def defaultAccountConfigSettings = defaultAccountConfigSettings.tap {
@@ -394,7 +602,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = bidRequestWithFloors
 
         and: "Account with fetch.enabled, fetch.url, useDynamicData in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.useDynamicData = accountUseDynamicData
         }
         accountDao.save(account)
@@ -406,7 +614,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         pbsService.sendAuctionRequest(bidRequest)
 
         then: "PBS should fetch data from floors provider"
-        assert floorsProvider.getRequestCount(bidRequest.site.publisher.id) == 1
+        assert floorsProvider.getRequestCount(bidRequest.accountId) == 1
 
         and: "Bidder request should contain bidFloor from request"
         def bidderRequest = bidder.getBidderRequests(bidRequest.id).last()
@@ -430,7 +638,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def accountId = bidRequest.app.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -451,10 +659,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         and: "PBS log should contain error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, basicFetchUrl)
+        def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL)
         assert floorsLogs.size() == 1
         assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
-                "'$basicFetchUrl$accountId', account = $accountId with a reason : Failed to request for " +
+                "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Failed to request for " +
                 "account $accountId, provider respond with status 400")
 
         and: "Floors validation failure cannot reject the entire auction"
@@ -472,7 +680,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def accountId = bidRequest.app.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -491,10 +699,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         and: "PBS log should contain error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId")
+        def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId")
         assert floorsLogs.size() == 1
         assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
-                "'$basicFetchUrl$accountId', account = $accountId with a reason : Failed to parse price floor " +
+                "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Failed to parse price floor " +
                 "response for account $accountId, cause: DecodeException: Failed to decode")
 
         and: "Floors validation failure cannot reject the entire auction"
@@ -512,7 +720,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def accountId = bidRequest.app.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -530,10 +738,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         and: "PBS log should contain error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId)
+        def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId)
         assert floorsLogs.size() == 1
         assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
-                "'$basicFetchUrl$accountId', account = $accountId with a reason : Failed to parse price floor " +
+                "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Failed to parse price floor " +
                 "response for account $accountId, response body can not be empty" as String)
 
         and: "Floors validation failure cannot reject the entire auction"
@@ -551,7 +759,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account in the DB"
-        def accountId = bidRequest.app.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -572,10 +780,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         and: "PBS log should contain error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId)
+        def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId)
         assert floorsLogs.size() == 1
         assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
-                "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor rules should contain " +
+                "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor rules should contain " +
                 "at least one model group " as String)
 
         and: "Floors validation failure cannot reject the entire auction"
@@ -593,7 +801,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account in the DB"
-        def accountId = bidRequest.app.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -614,10 +822,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         and: "PBS log should contain error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId)
+        def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId)
         assert floorsLogs.size() == 1
         assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
-                "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor rules values can't " +
+                "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor rules values can't " +
                 "be null or empty, but were null" as String)
 
         and: "Floors validation failure cannot reject the entire auction"
@@ -635,7 +843,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account in the DB"
-        def accountId = bidRequest.app.publisher.id
+        def accountId = bidRequest.accountId
         def maxRules = 1
         def account = getAccountWithEnabledFetch(accountId).tap {
             config.auction.priceFloors.fetch = fetchConfig(accountId, maxRules)
@@ -659,18 +867,18 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         and: "PBS log should contain error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId)
+        def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId)
         assert floorsLogs.size() == 1
         assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
-                "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor rules number " +
+                "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor rules number " +
                 "2 exceeded its maximum number $maxRules")
 
         and: "Floors validation failure cannot reject the entire auction"
         assert !response.seatbid?.isEmpty()
 
         where:
-        fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxRules: max) },
-                        { String id, int max -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxRulesSnakeCase: max) }]
+        fetchConfig << [{ String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxRules: max) },
+                        { String id, int max -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxRulesSnakeCase: max) }]
     }
 
     def "PBS should log error and increase #FETCH_FAILURE_METRIC when fetch request exceeds fetch.timeout-ms"() {
@@ -687,7 +895,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account in the DB"
-        def accountId = bidRequest.app.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -705,9 +913,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         and: "PBS log should contain error"
         def logs = pbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, basicFetchUrl)
+        def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL)
         assert floorsLogs.size() == 1
-        assert floorsLogs[0].contains("Fetch price floor request timeout for fetch.url: '$basicFetchUrl$accountId', " +
+        assert floorsLogs[0].contains("Fetch price floor request timeout for fetch.url: '$BASIC_FETCH_URL$accountId', " +
                 "account $accountId exceeded")
 
         and: "Floors validation failure cannot reject the entire auction"
@@ -725,7 +933,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with maxFileSizeKb in the DB"
-        def accountId = bidRequest.app.publisher.id
+        def accountId = bidRequest.accountId
         def maxSize = PBSUtils.getRandomNumber(1, 5)
         def account = getAccountWithEnabledFetch(accountId).tap {
             config.auction.priceFloors.fetch = fetchConfig(accountId, maxSize)
@@ -748,35 +956,36 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         and: "PBS log should contain error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, basicFetchUrl + accountId)
+        def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId)
         assert floorsLogs.size() == 1
         assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
-                "'$basicFetchUrl$accountId', account = $accountId with a reason : Response size " +
+                "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Response size " +
                 "$responseSize exceeded ${convertKilobyteSizeToByte(maxSize)} bytes limit")
 
         and: "Floors validation failure cannot reject the entire auction"
         assert !response.seatbid?.isEmpty()
 
         where:
-        fetchConfig << [{ String id, int maxKbSize -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxFileSizeKb: maxKbSize) },
-                        { String id, int maxKbSize -> new PriceFloorsFetch(url: basicFetchUrl + id, enabled: true, maxFileSizeKbSnakeCase: maxKbSize) }]
+        fetchConfig << [{ String id, int maxKbSize -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxFileSizeKb: maxKbSize) },
+                        { String id, int maxKbSize -> new PriceFloorsFetch(url: BASIC_FETCH_URL + id, enabled: true, maxFileSizeKbSnakeCase: maxKbSize) }]
     }
 
     def "PBS should prefer data from stored request when request doesn't contain floors data"() {
         given: "Default BidRequest with storedRequest"
+        def storedRequestId = PBSUtils.randomNumber as String
         def bidRequest = request.tap {
-            ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomNumber)
+            ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId)
         }
 
         and: "Default stored request with floors"
         def storedRequestModel = bidRequestWithFloors
 
         and: "Save storedRequest into DB"
-        def storedRequest = StoredRequest.getStoredRequest(bidRequest, storedRequestModel)
+        def storedRequest = StoredRequest.getStoredRequest(bidRequest.accountId, storedRequestId, storedRequestModel)
         storedRequestDao.save(storedRequest)
 
         and: "Account with disabled fetch in the DB"
-        def account = getAccountWithEnabledFetch(accountId).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.enabled = false
         }
         accountDao.save(account)
@@ -805,9 +1014,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         }
 
         where:
-        request                              | accountId                 | bidRequestWithFloors
-        BidRequest.defaultBidRequest         | request.site.publisher.id | bidRequestWithFloors
-        BidRequest.getDefaultBidRequest(APP) | request.app.publisher.id  | getBidRequestWithFloors(APP)
+        request                              | bidRequestWithFloors
+        BidRequest.defaultBidRequest         | getBidRequestWithFloors(SITE)
+        BidRequest.getDefaultBidRequest(APP) | getBidRequestWithFloors(APP)
     }
 
     def "PBS should prefer data from request when fetch is disabled in account config"() {
@@ -815,7 +1024,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = bidRequestWithFloors
 
         and: "Account with disabled fetch in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.enabled = false
         }
         accountDao.save(account)
@@ -851,7 +1060,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         given: "Default AmpRequest"
         def ampRequest = AmpRequest.defaultAmpRequest
 
-        and: "Default stored request with floors "
+        and: "Default stored request with floors"
         def ampStoredRequest = storedRequestWithFloors
         def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
         storedRequestDao.save(storedRequest)
@@ -888,8 +1097,9 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
     def "PBS should prefer data from floors provider when floors data is defined in both request and stored request"() {
         given: "BidRequest with storedRequest"
+        def storedRequestId = PBSUtils.randomNumber as String
         def bidRequest = bidRequestWithFloors.tap {
-            ext.prebid.storedRequest = new PrebidStoredRequest(id: PBSUtils.randomNumber)
+            ext.prebid.storedRequest = new PrebidStoredRequest(id: storedRequestId)
             ext.prebid.floors.floorMin = FLOOR_MIN
         }
 
@@ -897,11 +1107,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def storedRequestModel = bidRequestWithFloors
 
         and: "Save storedRequest into DB"
-        def storedRequest = StoredRequest.getStoredRequest(bidRequest, storedRequestModel)
+        def storedRequest = StoredRequest.getStoredRequest(bidRequest.accountId, storedRequestId, storedRequestModel)
         storedRequestDao.save(storedRequest)
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id)
+        def account = getAccountWithEnabledFetch(bidRequest.accountId)
         accountDao.save(account)
 
         and: "Set Floors Provider response"
@@ -909,13 +1119,13 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def floorsResponse = PriceFloorData.priceFloorData.tap {
             modelGroups[0].values = [(rule): floorValue]
         }
-        floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse)
+        floorsProvider.setResponse(bidRequest.accountId, floorsResponse)
 
         when: "PBS cache rules and processes auction request"
         cacheFloorsProviderRules(bidRequest, floorValue, floorsPbsService)
 
         then: "Bidder request should contain floors data from floors provider"
-        def bidderRequest = bidder.getBidderRequests(bidRequest.id).last()
+        def bidderRequest = bidder.getBidderRequests(bidRequest.id).last
         verifyAll(bidderRequest) {
             imp[0].bidFloor == floorValue
             imp[0].bidFloorCur == floorsResponse.modelGroups[0].currency
@@ -988,20 +1198,20 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id)
+        def account = getAccountWithEnabledFetch(bidRequest.accountId)
         accountDao.save(account)
 
         and: "Set Floors Provider #description response"
-        floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse)
+        floorsProvider.setResponse(bidRequest.accountId, floorsResponse)
 
         when: "PBS processes auction request"
         pbsService.sendAuctionRequest(bidRequest)
 
         then: "PBS should cache data from data provider"
-        assert floorsProvider.getRequestCount(bidRequest.app.publisher.id) == 1
+        assert floorsProvider.getRequestCount(bidRequest.accountId) == 1
 
         and: "PBS should periodically fetch data from data provider"
-        PBSUtils.waitUntil({ floorsProvider.getRequestCount(bidRequest.app.publisher.id) > 1 }, 7000, 3000)
+        PBSUtils.waitUntil({ floorsProvider.getRequestCount(bidRequest.accountId) > 1 }, 7000, 3000)
 
         where:
         description | floorsResponse
@@ -1021,7 +1231,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.app.publisher.id)
+        def account = getAccountWithEnabledFetch(bidRequest.accountId)
         accountDao.save(account)
 
         and: "Set Floors Provider #description response"
@@ -1029,7 +1239,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def floorsResponse = PriceFloorData.priceFloorData.tap {
             modelGroups[0].values = [(rule): floorValue]
         }
-        floorsProvider.setResponse(bidRequest.app.publisher.id, floorsResponse)
+        floorsProvider.setResponse(bidRequest.accountId, floorsResponse)
 
         when: "PBS processes auction request"
         pbsService.sendAuctionRequest(bidRequest)
@@ -1067,14 +1277,14 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
     def "PBS should validate rules from request when floorMin from request is invalid"() {
         given: "Default BidRequest with floorMin"
         def floorValue = PBSUtils.randomFloorValue
-        def invalidFloorMin = MIN_FLOOR_MIN - 1
+        def invalidFloorMin = FLOOR_MIN - 1
         def bidRequest = bidRequestWithFloors.tap {
             imp[0].bidFloor = floorValue
             ext.prebid.floors.floorMin = invalidFloorMin
         }
 
         and: "Account with disabled fetch in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.enabled = false
         }
         accountDao.save(account)
@@ -1089,8 +1299,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         and: "Response should contain error"
         assert response.ext?.errors[PREBID]*.code == [999]
         assert response.ext?.errors[PREBID]*.message ==
-                ["Failed to parse price floors from request, with a reason : Price floor floorMin " +
-                         "must be positive float, but was $invalidFloorMin "]
+                ["Failed to parse price floors from request, with a reason: Price floor floorMin " +
+                         "must be positive float, but was $invalidFloorMin"]
     }
 
     def "PBS should validate rules from request when request doesn't contain modelGroups"() {
@@ -1102,7 +1312,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         }
 
         and: "Account with disabled fetch in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.enabled = false
         }
         accountDao.save(account)
@@ -1117,8 +1327,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         and: "Response should contain error"
         assert response.ext?.errors[PREBID]*.code == [999]
         assert response.ext?.errors[PREBID]*.message ==
-                ["Failed to parse price floors from request, with a reason : Price floor rules " +
-                         "should contain at least one model group "]
+                ["Failed to parse price floors from request, with a reason: Price floor rules " +
+                         "should contain at least one model group"]
     }
 
     def "PBS should validate rules from request when request doesn't contain values"() {
@@ -1130,7 +1340,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         }
 
         and: "Account with disabled fetch in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.enabled = false
         }
         accountDao.save(account)
@@ -1145,8 +1355,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         and: "Response should contain error"
         assert response.ext?.errors[PREBID]*.code == [999]
         assert response.ext?.errors[PREBID]*.message ==
-                ["Failed to parse price floors from request, with a reason : Price floor rules values " +
-                         "can't be null or empty, but were null "]
+                ["Failed to parse price floors from request, with a reason: Price floor rules values " +
+                         "can't be null or empty, but were null"]
     }
 
     def "PBS should validate rules from request when modelWeight from request is invalid"() {
@@ -1162,7 +1372,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         }
 
         and: "Account with disabled fetch in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.enabled = false
         }
         accountDao.save(account)
@@ -1177,8 +1387,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         and: "Response should contain error"
         assert response.ext?.errors[PREBID]*.code == [999]
         assert response.ext?.errors[PREBID]*.message ==
-                ["Failed to parse price floors from request, with a reason : Price floor modelGroup modelWeight " +
-                         "must be in range(1-100), but was $invalidModelWeight "]
+                ["Failed to parse price floors from request, with a reason: Price floor modelGroup modelWeight " +
+                         "must be in range(1-100), but was $invalidModelWeight"]
         where:
         invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1]
     }
@@ -1216,8 +1426,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         and: "Response should contain error"
         assert response.ext?.errors[PREBID]*.code == [999]
         assert response.ext?.errors[PREBID]*.message ==
-                ["Failed to parse price floors from request, with a reason : Price floor modelGroup modelWeight " +
-                         "must be in range(1-100), but was $invalidModelWeight "]
+                ["Failed to parse price floors from request, with a reason: Price floor modelGroup modelWeight " +
+                         "must be in range(1-100), but was $invalidModelWeight"]
 
         where:
         invalidModelWeight << [0, MAX_MODEL_WEIGHT + 1]
@@ -1238,7 +1448,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         }
 
         and: "Account with disabled fetch in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.enabled = false
         }
         accountDao.save(account)
@@ -1256,11 +1466,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         and: "Response should contain error"
         assert response.ext?.errors[PREBID]*.code == [999]
         assert response.ext?.errors[PREBID]*.message ==
-                ["Failed to parse price floors from request, with a reason : Price floor root skipRate " +
-                         "must be in range(0-100), but was $invalidSkipRate "]
+                ["Failed to parse price floors from request, with a reason: Price floor root skipRate " +
+                         "must be in range(0-100), but was $invalidSkipRate"]
 
         where:
-        invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1]
+        invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1]
     }
 
     def "PBS should reject fetch when data skipRate from request is invalid"() {
@@ -1278,7 +1488,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         }
 
         and: "Account with disabled fetch in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.enabled = false
         }
         accountDao.save(account)
@@ -1296,11 +1506,11 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         and: "Response should contain error"
         assert response.ext?.errors[PREBID]*.code == [999]
         assert response.ext?.errors[PREBID]*.message ==
-                ["Failed to parse price floors from request, with a reason : Price floor data skipRate " +
-                         "must be in range(0-100), but was $invalidSkipRate "]
+                ["Failed to parse price floors from request, with a reason: Price floor data skipRate " +
+                         "must be in range(0-100), but was $invalidSkipRate"]
 
         where:
-        invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1]
+        invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1]
     }
 
     def "PBS should reject fetch when modelGroup skipRate from request is invalid"() {
@@ -1318,7 +1528,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         }
 
         and: "Account with disabled fetch in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.enabled = false
         }
         accountDao.save(account)
@@ -1336,28 +1546,28 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         and: "Response should contain error"
         assert response.ext?.errors[PREBID]*.code == [999]
         assert response.ext?.errors[PREBID]*.message ==
-                ["Failed to parse price floors from request, with a reason : Price floor modelGroup skipRate " +
-                         "must be in range(0-100), but was $invalidSkipRate "]
+                ["Failed to parse price floors from request, with a reason: Price floor modelGroup skipRate " +
+                         "must be in range(0-100), but was $invalidSkipRate"]
 
         where:
-        invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1]
+        invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1]
     }
 
     def "PBS should validate rules from request when default floor value from request is invalid"() {
         given: "Default BidRequest with default floor value"
         def floorValue = PBSUtils.randomFloorValue
-        def invalidDefaultFloorValue = MIN_DEFAULT_FLOOR_VALUE - 1
+        def invalidDefaultFloorValue = DEFAULT_FLOOR_VALUE_MIN - 1
         def bidRequest = bidRequestWithFloors.tap {
             imp[0].bidFloor = floorValue
             ext.prebid.floors.data.modelGroups << ModelGroup.modelGroup
             ext.prebid.floors.data.modelGroups.first().values = [(rule): floorValue + 0.1]
             ext.prebid.floors.data.modelGroups[0].defaultFloor = invalidDefaultFloorValue
             ext.prebid.floors.data.modelGroups.last().values = [(rule): floorValue + 0.2]
-            ext.prebid.floors.data.modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE
+            ext.prebid.floors.data.modelGroups.last().defaultFloor = DEFAULT_FLOOR_VALUE_MIN
         }
 
         and: "Account with disabled fetch in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id).tap {
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
             config.auction.priceFloors.fetch.enabled = false
         }
         accountDao.save(account)
@@ -1372,8 +1582,8 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         and: "Response should contain error"
         assert response.ext?.errors[PREBID]*.code == [999]
         assert response.ext?.errors[PREBID]*.message ==
-                ["Failed to parse price floors from request, with a reason : Price floor modelGroup default " +
-                         "must be positive float, but was $invalidDefaultFloorValue "]
+                ["Failed to parse price floors from request, with a reason: Price floor modelGroup default " +
+                         "must be positive float, but was $invalidDefaultFloorValue"]
     }
 
     def "PBS should not invalidate previously good fetched data when floors provider return invalid data"() {
@@ -1385,7 +1595,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.getDefaultBidRequest(APP)
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def accountId = bidRequest.app.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -1437,7 +1647,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.defaultBidRequest
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def accountId = bidRequest.site.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -1471,10 +1681,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         and: "PBS log should contain error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, basicFetchUrl)
+        def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL)
         assert floorsLogs.size() == 1
         assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
-                "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup modelWeight" +
+                "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor modelGroup modelWeight" +
                 " must be in range(1-100), but was $invalidModelWeight")
 
         and: "Floors validation failure cannot reject the entire auction"
@@ -1495,7 +1705,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.defaultBidRequest
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def accountId = bidRequest.site.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -1530,17 +1740,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         and: "PBS log should contain error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId")
+        def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId")
         assert floorsLogs.size() == 1
         assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
-                "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor data skipRate" +
+                "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor data skipRate" +
                 " must be in range(0-100), but was $invalidSkipRate")
 
         and: "Floors validation failure cannot reject the entire auction"
         assert !response.seatbid?.isEmpty()
 
         where:
-        invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1]
+        invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1]
     }
 
     def "PBS should reject fetch when modelGroup skipRate from floors provider is invalid"() {
@@ -1554,7 +1764,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.defaultBidRequest
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def accountId = bidRequest.site.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -1589,17 +1799,17 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         and: "PBS log should contain error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId")
+        def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId")
         assert floorsLogs.size() == 1
         assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
-                "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup skipRate" +
+                "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor modelGroup skipRate" +
                 " must be in range(0-100), but was $invalidSkipRate")
 
         and: "Floors validation failure cannot reject the entire auction"
         assert !response.seatbid?.isEmpty()
 
         where:
-        invalidSkipRate << [MIN_SKIP_RATE - 1, MAX_SKIP_RATE + 1]
+        invalidSkipRate << [SKIP_RATE_MIN - 1, SKIP_RATE_MAX + 1]
     }
 
     def "PBS should reject fetch when default floor value from floors provider is invalid"() {
@@ -1613,19 +1823,19 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.defaultBidRequest
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def accountId = bidRequest.site.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
         and: "Set Floors Provider response"
         def floorValue = PBSUtils.randomFloorValue
-        def invalidDefaultFloor = MIN_DEFAULT_FLOOR_VALUE - 1
+        def invalidDefaultFloor = DEFAULT_FLOOR_VALUE_MIN - 1
         def floorsResponse = PriceFloorData.priceFloorData.tap {
             modelGroups << ModelGroup.modelGroup
             modelGroups.first().values = [(rule): floorValue + 0.1]
             modelGroups[0].defaultFloor = invalidDefaultFloor
             modelGroups.last().values = [(rule): floorValue]
-            modelGroups.last().defaultFloor = MIN_DEFAULT_FLOOR_VALUE
+            modelGroups.last().defaultFloor = DEFAULT_FLOOR_VALUE_MIN
         }
         floorsProvider.setResponse(accountId, floorsResponse)
 
@@ -1648,10 +1858,10 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         and: "PBS log should contain error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, "$basicFetchUrl$accountId")
+        def floorsLogs = getLogsByText(logs, "$BASIC_FETCH_URL$accountId")
         assert floorsLogs.size() == 1
         assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
-                "'$basicFetchUrl$accountId', account = $accountId with a reason : Price floor modelGroup default" +
+                "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor modelGroup default" +
                 " must be positive float, but was $invalidDefaultFloor")
 
         and: "Floors validation failure cannot reject the entire auction"
@@ -1663,7 +1873,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = bidRequestWithFloors
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def account = getAccountWithEnabledFetch(bidRequest.site.publisher.id)
+        def account = getAccountWithEnabledFetch(bidRequest.accountId)
         accountDao.save(account)
 
         and: "Set Floors Provider response"
@@ -1673,7 +1883,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
             modelGroups[0].currency = modelGroupCurrency
             currency = dataCurrency
         }
-        floorsProvider.setResponse(bidRequest.site.publisher.id, floorsResponse)
+        floorsProvider.setResponse(bidRequest.accountId, floorsResponse)
 
         and: "PBS fetch rules from floors provider"
         cacheFloorsProviderRules(bidRequest)
@@ -1699,7 +1909,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.defaultBidRequest
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def accountId = bidRequest.site.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -1719,7 +1929,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
 
         then: "PBS log should not contain error"
         def logs = floorsPbsService.getLogsByTime(startTime)
-        def floorsLogs = getLogsByText(logs, basicFetchUrl)
+        def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL)
         assert floorsLogs.size() == 0
 
         and: "Bidder request should contain floors data from floors provider"
@@ -1733,7 +1943,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         def bidRequest = BidRequest.defaultBidRequest
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def accountId = bidRequest.site.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -1769,7 +1979,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         }
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def accountId = bidRequest.site.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -1807,7 +2017,7 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         }
 
         and: "Account with enabled fetch, fetch.url in the DB"
-        def accountId = bidRequest.site.publisher.id
+        def accountId = bidRequest.accountId
         def account = getAccountWithEnabledFetch(accountId)
         accountDao.save(account)
 
@@ -1836,6 +2046,91 @@ class PriceFloorsFetchingSpec extends PriceFloorsBaseSpec {
         }
     }
 
+    def "PBS should validate fetch.max-schema-dims from account config and not reject entire auction"() {
+        given: "Default BidRequest"
+        def bidRequest = BidRequest.defaultBidRequest
+
+        and: "Account with enabled fetch, maxSchemaDims in the DB"
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+            config.auction.priceFloors.fetch.maxSchemaDims = maxSchemaDims
+            config.auction.priceFloors.fetch.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase
+        }
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "Metric alerts.account_config.ACCOUNT.price-floors should be update"
+        def metrics = floorsPbsService.sendCollectedMetricsRequest()
+        assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
+
+        and: "PBS floors validation failure should not reject the entire auction"
+        assert !response.seatbid?.isEmpty()
+
+        where:
+        maxSchemaDims                 | maxSchemaDimsSnakeCase
+        null                          | PBSUtils.randomNegativeNumber
+        null                          | PBSUtils.getRandomNumber(20)
+        PBSUtils.randomNegativeNumber | null
+        PBSUtils.getRandomNumber(20)  | null
+    }
+
+    def "PBS should validate price-floor.max-rules from account config and not reject entire auction"() {
+        given: "Default BidRequest"
+        def bidRequest = BidRequest.defaultBidRequest
+
+        and: "Account with enabled fetch, maxRules in the DB"
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+            config.auction.priceFloors.maxRules = maxRules
+            config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase
+        }
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "Metric alerts.account_config.ACCOUNT.price-floors should be update"
+        def metrics = floorsPbsService.sendCollectedMetricsRequest()
+        assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
+
+        and: "PBS floors validation failure should not reject the entire auction"
+        assert !response.seatbid?.isEmpty()
+
+        where:
+        maxRules                      | maxRulesSnakeCase
+        null                          | PBSUtils.randomNegativeNumber
+        PBSUtils.randomNegativeNumber | null
+    }
+
+    def "PBS should validate price-floor.max-schema-dims from account config and not reject entire auction"() {
+        given: "Default BidRequest"
+        def bidRequest = BidRequest.defaultBidRequest
+
+        and: "Account with enabled fetch, maxSchemaDims in the DB"
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+            config.auction.priceFloors.maxSchemaDims = maxSchemaDims
+            config.auction.priceFloors.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase
+        }
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "Metric alerts.account_config.ACCOUNT.price-floors should be update"
+        def metrics = floorsPbsService.sendCollectedMetricsRequest()
+        assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
+
+        and: "PBS floors validation failure should not reject the entire auction"
+        assert !response.seatbid?.isEmpty()
+
+        where:
+        maxSchemaDims                 | maxSchemaDimsSnakeCase
+        null                          | PBSUtils.randomNegativeNumber
+        null                          | PBSUtils.getRandomNumber(20)
+        PBSUtils.randomNegativeNumber | null
+        PBSUtils.getRandomNumber(20)  | null
+    }
+
     static int convertKilobyteSizeToByte(int kilobyteSize) {
         kilobyteSize * 1024
     }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy
index 478248d3f46..d10f6b07b1b 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsRulesSpec.groovy
@@ -277,10 +277,8 @@ class PriceFloorsRulesSpec extends PriceFloorsBaseSpec {
 
         where:
         bidRequest                       | bothFloorValue            | bannerFloorValue          | videoFloorValue
-        bidRequestWithMultipleMediaTypes | 0.6                       | PBSUtils.randomFloorValue |
-                PBSUtils.randomFloorValue
-        BidRequest.defaultBidRequest     | PBSUtils.randomFloorValue | 0.6                       |
-                PBSUtils.randomFloorValue
+        bidRequestWithMultipleMediaTypes | 0.6                       | PBSUtils.randomFloorValue | PBSUtils.randomFloorValue
+        BidRequest.defaultBidRequest     | PBSUtils.randomFloorValue | 0.6                       | PBSUtils.randomFloorValue
         BidRequest.defaultVideoRequest   | PBSUtils.randomFloorValue | PBSUtils.randomFloorValue | 0.6
     }
 
diff --git a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy
index 737aed289e4..f1385b1649d 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/pricefloors/PriceFloorsSignalingSpec.groovy
@@ -18,6 +18,8 @@ import org.prebid.server.functional.model.response.auction.BidResponse
 import org.prebid.server.functional.model.response.auction.MediaType
 import org.prebid.server.functional.util.PBSUtils
 
+import java.time.Instant
+
 import static org.mockserver.model.HttpStatusCode.BAD_REQUEST_400
 import static org.prebid.server.functional.model.Currency.USD
 import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
@@ -27,9 +29,14 @@ import static org.prebid.server.functional.model.pricefloors.MediaType.VIDEO
 import static org.prebid.server.functional.model.pricefloors.PriceFloorField.MEDIA_TYPE
 import static org.prebid.server.functional.model.pricefloors.PriceFloorField.SITE_DOMAIN
 import static org.prebid.server.functional.model.request.auction.DistributionChannel.APP
+import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID
 
 class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
 
+    private static final Closure<String> INVALID_CONFIG_METRIC = { account -> "alerts.account_config.${account}.price-floors" }
+    private static final int MAX_SCHEMA_DIMENSIONS_SIZE = 1
+    private static final int MAX_RULES_SIZE = 1
+
     def "PBS should skip signalling for request with rules when ext.prebid.floors.enabled = false in request"() {
         given: "Default BidRequest with disabled floors"
         def bidRequest = bidRequestWithFloors.tap {
@@ -511,4 +518,318 @@ class PriceFloorsSignalingSpec extends PriceFloorsBaseSpec {
         assert bidderRequest.imp.first().bidFloor == bannerFloorValue
         assert bidderRequest.imp.last().bidFloor == videoFloorValue
     }
+
+    def "PBS shouldn't emit errors when request schema.fields than floor-config.max-schema-dims"() {
+        given: "Bid request with schema 2 fields"
+        def bidRequest = bidRequestWithFloors.tap {
+            ext.prebid.floors.maxSchemaDims = PBSUtils.getRandomNumber(2)
+        }
+
+        and: "Account with maxSchemaDims in the DB"
+        def accountId = bidRequest.site.publisher.id
+        def account = getAccountWithEnabledFetch(accountId)
+        accountDao.save(account)
+
+        and: "Set bidder response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS shouldn't log a errors"
+        assert !response.ext?.errors
+    }
+
+    def "PBS should emit errors when request has more rules than price-floor.max-rules"() {
+        given: "BidRequest with 2 rules"
+        def requestFloorValue = PBSUtils.randomFloorValue
+        def bidRequest = bidRequestWithFloors.tap {
+            ext.prebid.floors.data.modelGroups[0].values =
+                    [(rule)                                                       : requestFloorValue + 0.1,
+                     (new Rule(mediaType: BANNER, country: Country.MULTIPLE).rule): requestFloorValue]
+        }
+
+        and: "Account with maxRules in the DB"
+        def accountId = bidRequest.site.publisher.id
+        def account = getAccountWithEnabledFetch(accountId).tap {
+            config.auction.priceFloors.maxRules = maxRules
+            config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase
+        }
+        accountDao.save(account)
+
+        and: "Set bidder response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidResponse.seatbid.first().bid.first().price = requestFloorValue
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should log a errors"
+        assert response.ext?.errors[PREBID]*.code == [999]
+        assert response.ext?.errors[PREBID]*.message ==
+                ["Failed to parse price floors from request, with a reason: " +
+                         "Price floor rules number ${getRuleSize(bidRequest)} exceeded its maximum number ${MAX_RULES_SIZE}"]
+
+        where:
+        maxRules       | maxRulesSnakeCase
+        MAX_RULES_SIZE | null
+        null           | MAX_RULES_SIZE
+    }
+
+    def "PBS should emit errors when request has more schema.fields than floor-config.max-schema-dims"() {
+        given: "BidRequest with schema 2 fields"
+        def bidRequest = bidRequestWithFloors
+
+        and: "Account with maxSchemaDims in the DB"
+        def accountId = bidRequest.site.publisher.id
+        def account = getAccountWithEnabledFetch(accountId).tap {
+            config.auction.priceFloors.maxSchemaDims = maxSchemaDims
+            config.auction.priceFloors.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase
+        }
+        accountDao.save(account)
+
+        and: "Set bidder response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should log a errors"
+        assert response.ext?.errors[PREBID]*.code == [999]
+        assert response.ext?.errors[PREBID]*.message ==
+                ["Failed to parse price floors from request, with a reason: " +
+                         "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"]
+
+        where:
+        maxSchemaDims              | maxSchemaDimsSnakeCase
+        MAX_SCHEMA_DIMENSIONS_SIZE | null
+        null                       | MAX_SCHEMA_DIMENSIONS_SIZE
+    }
+
+    def "PBS should emit errors when request has more schema.fields than default-account.max-schema-dims"() {
+        given: "Floor config with default account"
+        def accountConfig = getDefaultAccountConfigSettings().tap {
+            auction.priceFloors.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE
+        }
+        def pbsFloorConfig = GENERIC_ALIAS_CONFIG + ["price-floors.enabled"           : "true",
+                                                     "settings.default-account-config": encode(accountConfig)]
+
+        and: "Prebid server with floor config"
+        def floorsPbsService = pbsServiceFactory.getService(pbsFloorConfig)
+
+        and: "BidRequest with schema 2 fields"
+        def bidRequest = bidRequestWithFloors
+
+        and: "Account with maxSchemaDims in the DB"
+        def accountId = bidRequest.site.publisher.id
+        def account = getAccountWithEnabledFetch(accountId).tap {
+            config.auction.priceFloors.maxSchemaDims = PBSUtils.randomNegativeNumber
+        }
+        accountDao.save(account)
+
+        and: "Set bidder response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should log a errors"
+        assert response.ext?.errors[PREBID]*.code == [999]
+        assert response.ext?.errors[PREBID]*.message ==
+                ["Failed to parse price floors from request, with a reason: " +
+                         "Price floor schema dimensions ${getSchemaSize(bidRequest)} " +
+                         "exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"]
+
+        and: "Metric alerts.account_config.ACCOUNT.price-floors should be update"
+        def metrics = floorsPbsService.sendCollectedMetricsRequest()
+        assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsFloorConfig)
+    }
+
+    def "PBS should emit errors when request has more schema.fields than default-account.fetch.max-schema-dims"() {
+        given: "Test start time"
+        def startTime = Instant.now()
+
+        and: "BidRequest with schema 2 fields"
+        def bidRequest = bidRequestWithFloors
+
+        and: "Floor config with default account"
+        def accountConfig = getDefaultAccountConfigSettings().tap {
+            auction.priceFloors.fetch.enabled = true
+            auction.priceFloors.fetch.url = BASIC_FETCH_URL + bidRequest.site.publisher.id
+            auction.priceFloors.fetch.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE
+            auction.priceFloors.maxSchemaDims = null
+        }
+        def pbsFloorConfig = GENERIC_ALIAS_CONFIG + ["price-floors.enabled"           : "true",
+                                                     "settings.default-account-config": encode(accountConfig)]
+
+        and: "Prebid server with floor config"
+        def floorsPbsService = pbsServiceFactory.getService(pbsFloorConfig)
+
+        and: "Flush metrics"
+        flushMetrics(floorsPbsService)
+
+        and: "Account with maxSchemaDims in the DB"
+        def accountId = bidRequest.site.publisher.id
+        def account = getAccountWithEnabledFetch(accountId).tap {
+            config.auction.priceFloors.fetch.maxSchemaDims = PBSUtils.randomNegativeNumber
+        }
+        accountDao.save(account)
+
+        and: "Set bidder response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should log a errors"
+        def logs = floorsPbsService.getLogsByTime(startTime)
+        def floorsLogs = getLogsByText(logs, BASIC_FETCH_URL + accountId)
+        assert floorsLogs.size() == 1
+        assert floorsLogs[0].contains("Failed to fetch price floor from provider for fetch.url: " +
+                "'$BASIC_FETCH_URL$accountId', account = $accountId with a reason : Price floor schema dimensions ${getSchemaSize(bidRequest)} " +
+                "exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}")
+
+        and: "Metric alerts.account_config.ACCOUNT.price-floors should be update"
+        def metrics = floorsPbsService.sendCollectedMetricsRequest()
+        assert metrics[INVALID_CONFIG_METRIC(bidRequest.accountId) as String] == 1
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsFloorConfig)
+    }
+
+    def "PBS should emit errors when request has more schema.fields than fetch.max-schema-dims"() {
+        given: "Default BidRequest with floorMin"
+        def bidRequest = bidRequestWithFloors
+
+        and: "Account with disabled fetch in the DB"
+        def account = getAccountWithEnabledFetch(bidRequest.accountId).tap {
+            config.auction.priceFloors.maxSchemaDims = maxSchemaDims
+            config.auction.priceFloors.maxSchemaDimsSnakeCase = maxSchemaDimsSnakeCase
+        }
+        accountDao.save(account)
+
+        when: "PBS processes auction request"
+        def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should log a errors"
+        assert response.ext?.errors[PREBID]*.code == [999]
+        assert response.ext?.errors[PREBID]*.message ==
+                ["Failed to parse price floors from request, with a reason: " +
+                         "Price floor schema dimensions ${getSchemaSize(bidRequest)} exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"]
+
+        where:
+        maxSchemaDims              | maxSchemaDimsSnakeCase
+        MAX_SCHEMA_DIMENSIONS_SIZE | null
+        null                       | MAX_SCHEMA_DIMENSIONS_SIZE
+    }
+
+    def "PBS should fail with error and maxSchemaDims take precede over fetch.maxSchemaDims when requested both"() {
+        given: "BidRequest with schema 2 fields"
+        def bidRequest = bidRequestWithFloors
+
+        and: "Account with maxSchemaDims in the DB"
+        def accountId = bidRequest.site.publisher.id
+        def floorSchemaFilesSize = getSchemaSize(bidRequest)
+        def account = getAccountWithEnabledFetch(accountId).tap {
+            config.auction.priceFloors.maxSchemaDims = MAX_SCHEMA_DIMENSIONS_SIZE
+            config.auction.priceFloors.fetch.maxSchemaDims = floorSchemaFilesSize
+        }
+        accountDao.save(account)
+
+        and: "Set bidder response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS should log a errors"
+        assert response.ext?.errors[PREBID]*.code == [999]
+        assert response.ext?.errors[PREBID]*.message ==
+                ["Failed to parse price floors from request, with a reason: " +
+                         "Price floor schema dimensions ${floorSchemaFilesSize} " +
+                         "exceeded its maximum number ${MAX_SCHEMA_DIMENSIONS_SIZE}"]
+    }
+
+    def "PBS shouldn't fail with error and maxSchemaDims take precede over fetch.maxSchemaDims when requested both"() {
+        given: "BidRequest with schema 2 fields"
+        def bidRequest = bidRequestWithFloors
+
+        and: "Account with maxSchemaDims in the DB"
+        def accountId = bidRequest.site.publisher.id
+        def account = getAccountWithEnabledFetch(accountId).tap {
+            config.auction.priceFloors.maxSchemaDims = getSchemaSize(bidRequest)
+            config.auction.priceFloors.fetch.maxSchemaDims = getSchemaSize(bidRequest) - 1
+        }
+        accountDao.save(account)
+
+        and: "Set bidder response"
+        def bidResponse = BidResponse.getDefaultBidResponse(bidRequest)
+        bidder.setResponse(bidRequest.id, bidResponse)
+
+        when: "PBS processes auction request"
+        def response = floorsPbsService.sendAuctionRequest(bidRequest)
+
+        then: "PBS shouldn't log a errors"
+        assert !response.ext?.errors
+    }
+
+    def "PBS should emit errors when stored request has more rules than price-floor.max-rules for amp request"() {
+        given: "Default AmpRequest"
+        def ampRequest = AmpRequest.defaultAmpRequest
+
+        and: "Default stored request with 2 rules "
+        def requestFloorValue = PBSUtils.randomFloorValue
+        def ampStoredRequest = BidRequest.defaultStoredRequest.tap {
+            ext.prebid.floors = ExtPrebidFloors.extPrebidFloors
+            ext.prebid.floors.data.modelGroups[0].values =
+                    [(rule)                                                       : requestFloorValue + 0.1,
+                     (new Rule(mediaType: BANNER, country: Country.MULTIPLE).rule): requestFloorValue]
+        }
+        def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+        storedRequestDao.save(storedRequest)
+
+        and: "Account with maxRules in the DB"
+        def account = getAccountWithEnabledFetch(ampRequest.account as String).tap {
+            config.auction.priceFloors.maxRules = maxRules
+            config.auction.priceFloors.maxRulesSnakeCase = maxRulesSnakeCase
+        }
+        accountDao.save(account)
+
+        and: "Set bidder response"
+        def bidResponse = BidResponse.getDefaultBidResponse(ampStoredRequest)
+        bidResponse.seatbid.first().bid.first().price = requestFloorValue
+        bidder.setResponse(ampStoredRequest.id, bidResponse)
+
+        when: "PBS processes amp request"
+        def response = floorsPbsService.sendAmpRequest(ampRequest)
+
+        then: "PBS should log a errors"
+        assert response.ext?.errors[PREBID]*.code == [999]
+        assert response.ext?.errors[PREBID]*.message ==
+                ["Failed to parse price floors from request, with a reason: " +
+                         "Price floor rules number ${getRuleSize(ampStoredRequest)} " +
+                         "exceeded its maximum number ${MAX_RULES_SIZE}"]
+
+        where:
+        maxRules       | maxRulesSnakeCase
+        MAX_RULES_SIZE | null
+        null           | MAX_RULES_SIZE
+    }
+
+    private static int getSchemaSize(BidRequest bidRequest) {
+        bidRequest?.ext?.prebid?.floors?.data?.modelGroups[0].schema.fields.size()
+    }
+
+    private static int getRuleSize(BidRequest bidRequest) {
+        bidRequest?.ext?.prebid?.floors?.data?.modelGroups[0].values.size()
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy
index 309bba2eea6..e0c3b279200 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/ActivityTraceLogSpec.groovy
@@ -121,7 +121,7 @@ class ActivityTraceLogSpec extends PrivacyBaseSpec {
         accountDao.save(account)
 
         when: "PBS processes auction requests"
-        def bidResponse = pbsServiceFactory.getService(PBS_CONFIG).sendAuctionRequest(bidRequest)
+        def bidResponse = activityPbsService.sendAuctionRequest(bidRequest)
 
         then: "Bid response should contain basic info in debug"
         def infrastructure = bidResponse.ext.debug.trace.activityInfrastructure
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy
index d4cfc7bbda9..771fac9bd84 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAmpSpec.groovy
@@ -8,9 +8,11 @@ import org.prebid.server.functional.model.config.AccountPrivacyConfig
 import org.prebid.server.functional.model.config.PurposeConfig
 import org.prebid.server.functional.model.db.Account
 import org.prebid.server.functional.model.db.StoredRequest
+import org.prebid.server.functional.model.pricefloors.Country
 import org.prebid.server.functional.model.request.auction.BidRequest
-import org.prebid.server.functional.service.PrebidServerService
-import org.prebid.server.functional.testcontainers.container.PrebidServerContainer
+import org.prebid.server.functional.model.request.auction.DistributionChannel
+import org.prebid.server.functional.model.request.auction.Regs
+import org.prebid.server.functional.model.request.auction.RegsExt
 import org.prebid.server.functional.util.PBSUtils
 import org.prebid.server.functional.util.privacy.BogusConsent
 import org.prebid.server.functional.util.privacy.CcpaConsent
@@ -27,6 +29,7 @@ import static org.prebid.server.functional.model.config.Purpose.P4
 import static org.prebid.server.functional.model.config.PurposeEnforcement.BASIC
 import static org.prebid.server.functional.model.config.PurposeEnforcement.NO
 import static org.prebid.server.functional.model.mock.services.vendorlist.GvlSpecificationVersion.V3
+import static org.prebid.server.functional.model.pricefloors.Country.BULGARIA
 import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_ADAPTER_DISALLOWED_COUNT
 import static org.prebid.server.functional.model.privacy.Metric.TEMPLATE_REQUEST_DISALLOWED_COUNT
 import static org.prebid.server.functional.model.request.amp.ConsentType.BOGUS
@@ -36,6 +39,7 @@ import static org.prebid.server.functional.model.request.auction.ActivityType.FE
 import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_EIDS
 import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO
 import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD
+import static org.prebid.server.functional.model.request.auction.PublicCountryIp.BGR_IP
 import static org.prebid.server.functional.model.response.auction.ErrorType.PREBID
 import static org.prebid.server.functional.util.privacy.CcpaConsent.Signal.ENFORCED
 import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID
@@ -314,10 +318,8 @@ class GdprAmpSpec extends PrivacyBaseSpec {
         def startTime = Instant.now()
 
         and: "Create new container"
-        def serverContainer = new PrebidServerContainer(GDPR_VENDOR_LIST_CONFIG +
-                ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String])
-        serverContainer.start()
-        def privacyPbsService = new PrebidServerService(serverContainer)
+        def config = GDPR_VENDOR_LIST_CONFIG + ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String]
+        def defaultPrivacyPbsService = pbsServiceFactory.getService(config)
 
         and: "Prepare tcf consent string"
         def tcfConsent = new TcfConsent.Builder()
@@ -341,21 +343,21 @@ class GdprAmpSpec extends PrivacyBaseSpec {
         vendorListResponse.setResponse(tcfPolicyVersion)
 
         when: "PBS processes amp request"
-        privacyPbsService.sendAmpRequest(ampRequest)
+        defaultPrivacyPbsService.sendAmpRequest(ampRequest)
 
         then: "Used vendor list have proper specification version of GVL"
         def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString())
-        PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) }
-        def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class)
+        PBSUtils.waitUntil { defaultPrivacyPbsService.isFileExist(properVendorListPath) }
+        def vendorList = defaultPrivacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class)
         assert vendorList.tcfPolicyVersion == tcfPolicyVersion.vendorListVersion
 
         and: "Logs should contain proper vendor list version"
-        def logs = privacyPbsService.getLogsByTime(startTime)
+        def logs = defaultPrivacyPbsService.getLogsByTime(startTime)
         assert getLogsByText(logs, "Created new TCF 2 vendor list for version " +
                 "v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion}")
 
-        cleanup: "Stop container with default request"
-        serverContainer.stop()
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(config)
 
         where:
         tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5]
@@ -404,7 +406,10 @@ class GdprAmpSpec extends PrivacyBaseSpec {
     }
 
     def "PBS amp should emit the same error without a second GVL list request if a retry is too soon for the exponential-backoff"() {
-        given: "Test start time"
+        given: "Prebid server with privacy settings"
+        def defaultPrivacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG)
+
+        and: "Test start time"
         def startTime = Instant.now()
 
         and: "Prepare tcf consent string"
@@ -432,14 +437,14 @@ class GdprAmpSpec extends PrivacyBaseSpec {
         vendorListResponse.setResponse(tcfPolicyVersion, Delay.seconds(EXPONENTIAL_BACKOFF_MAX_DELAY + 3))
 
         when: "PBS processes amp request"
-        privacyPbsService.sendAmpRequest(ampRequest)
+        defaultPrivacyPbsService.sendAmpRequest(ampRequest)
 
         then: "PBS shouldn't fetch vendor list"
         def vendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString())
-        assert !privacyPbsService.isFileExist(vendorListPath)
+        assert !defaultPrivacyPbsService.isFileExist(vendorListPath)
 
         and: "Logs should contain proper vendor list version"
-        def logs = privacyPbsService.getLogsByTime(startTime)
+        def logs = defaultPrivacyPbsService.getLogsByTime(startTime)
         def tcfError = "TCF 2 vendor list for version v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion} not found, started downloading."
         assert getLogsByText(logs, tcfError)
 
@@ -447,18 +452,21 @@ class GdprAmpSpec extends PrivacyBaseSpec {
         def secondStartTime = Instant.now()
 
         when: "PBS processes amp request"
-        privacyPbsService.sendAmpRequest(ampRequest)
+        defaultPrivacyPbsService.sendAmpRequest(ampRequest)
 
         then: "PBS shouldn't fetch vendor list"
-        assert !privacyPbsService.isFileExist(vendorListPath)
+        assert !defaultPrivacyPbsService.isFileExist(vendorListPath)
 
         and: "Logs should contain proper vendor list version"
-        def logsSecond = privacyPbsService.getLogsByTime(secondStartTime)
+        def logsSecond = defaultPrivacyPbsService.getLogsByTime(secondStartTime)
         assert getLogsByText(logsSecond, tcfError)
 
         and: "Reset vendor list response"
         vendorListResponse.reset()
 
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG)
+
         where:
         tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5]
     }
@@ -653,7 +661,10 @@ class GdprAmpSpec extends PrivacyBaseSpec {
     }
 
     def "PBS amp should set 3 for tcfPolicyVersion when tcfPolicyVersion is #tcfPolicyVersion"() {
-        given: "Tcf consent setup"
+        given: "Prebid server with privacy settings"
+        def defaultPrivacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG)
+
+        and: "Tcf consent setup"
         def tcfConsent = new TcfConsent.Builder()
                 .setPurposesLITransparency(BASIC_ADS)
                 .setTcfPolicyVersion(tcfPolicyVersion.value)
@@ -675,15 +686,176 @@ class GdprAmpSpec extends PrivacyBaseSpec {
         vendorListResponse.setResponse(tcfPolicyVersion)
 
         when: "PBS processes amp request"
-        privacyPbsService.sendAmpRequest(ampRequest)
+        defaultPrivacyPbsService.sendAmpRequest(ampRequest)
 
         then: "Used vendor list have proper specification version of GVL"
         def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString())
-        PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) }
-        def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class)
+        PBSUtils.waitUntil { defaultPrivacyPbsService.isFileExist(properVendorListPath) }
+        def vendorList = defaultPrivacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class)
         assert vendorList.gvlSpecificationVersion == V3
 
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG)
+
         where:
         tcfPolicyVersion << [TCF_POLICY_V4, TCF_POLICY_V5]
     }
+
+    def "PBS should process with GDPR enforcement when GDPR and COPPA configurations are present in request"() {
+        given: "Valid consent string without basic ads"
+        def validConsentString = new TcfConsent.Builder()
+                .setPurposesLITransparency(DEVICE_ACCESS)
+                .setVendorLegitimateInterest([GENERIC_VENDOR_ID])
+                .build()
+
+        and: "Amp default request"
+        def ampRequest = getGdprAmpRequest(validConsentString)
+
+        and: "Bid request with gdpr and coppa config"
+        def ampStoredRequest = getGdprBidRequest(DistributionChannel.SITE, validConsentString).tap {
+            regs = new Regs(gdpr: gdpr, coppa: coppa, ext: new RegsExt(gdpr: extGdpr, coppa: extCoppa))
+            setAccountId(ampRequest.account)
+        }
+
+        and: "Save account config without eea countries into DB"
+        def accountGdprConfig = new AccountGdprConfig(enabled: true, eeaCountries: PBSUtils.getRandomEnum(Country.class, [BULGARIA]))
+        def account = getAccountWithGdpr(ampRequest.account, accountGdprConfig)
+        accountDao.save(account)
+
+        and: "Stored request in DB"
+        def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+        storedRequestDao.save(storedRequest)
+
+        and: "Flush metrics"
+        flushMetrics(privacyPbsService)
+
+        when: "PBS processes amp request"
+        privacyPbsService.sendAmpRequest(ampRequest)
+
+        then: "Bidder shouldn't be called"
+        assert !bidder.getBidderRequests(ampStoredRequest.id)
+
+        then: "Metrics processed across activities should be updated"
+        def metrics = privacyPbsService.sendCollectedMetricsRequest()
+        assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1
+        assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1
+
+        where:
+        gdpr | coppa | extGdpr | extCoppa
+        1    | 1     | 1       | 1
+        1    | 1     | 1       | 0
+        1    | 1     | 1       | null
+        1    | 1     | 0       | 1
+        1    | 1     | 0       | 0
+        1    | 1     | 0       | null
+        1    | 1     | null    | 1
+        1    | 1     | null    | 0
+        1    | 1     | null    | null
+        1    | 0     | 1       | 1
+        1    | 0     | 1       | 0
+        1    | 0     | 1       | null
+        1    | 0     | 0       | 1
+        1    | 0     | 0       | 0
+        1    | 0     | 0       | null
+        1    | 0     | null    | 1
+        1    | 0     | null    | 0
+        1    | 0     | null    | null
+        1    | null  | 1       | 1
+        1    | null  | 1       | 0
+        1    | null  | 1       | null
+        1    | null  | 0       | 1
+        1    | null  | 0       | 0
+        1    | null  | 0       | null
+        1    | null  | null    | 1
+        1    | null  | null    | 0
+        1    | null  | null    | null
+
+        null | 1     | 1       | 1
+        null | 1     | 1       | 0
+        null | 1     | 1       | null
+        null | 0     | 1       | 1
+        null | 0     | 1       | 0
+        null | 0     | 1       | null
+        null | null  | 1       | 1
+        null | null  | 1       | 0
+        null | null  | 1       | null
+    }
+
+    def "PBS should process with GDPR enforcement when request comes from EEA IP with COPPA enabled"() {
+        given: "Valid consent string without basic ads"
+        def validConsentString = new TcfConsent.Builder()
+                .setPurposesLITransparency(DEVICE_ACCESS)
+                .setVendorLegitimateInterest([GENERIC_VENDOR_ID])
+                .build()
+
+        and: "Amp default request"
+        def ampRequest = getGdprAmpRequest(validConsentString)
+
+        and: "Bid request with gdpr and coppa config"
+        def ampStoredRequest = getGdprBidRequest(DistributionChannel.SITE, validConsentString).tap {
+            regs = new Regs(gdpr: 1, coppa: 1, ext: new RegsExt(gdpr: 1, coppa: 1))
+            device.geo.country = requestCountry
+            device.geo.region = null
+            device.ip = requestIpV4
+            device.ipv6 = requestIpV6
+        }
+
+        and: "Save account config without eea countries into DB"
+        def accountGdprConfig = new AccountGdprConfig(enabled: true, eeaCountries: accountCountry)
+        def account = getAccountWithGdpr(ampRequest.account, accountGdprConfig)
+        accountDao.save(account)
+
+        and: "Stored request in DB"
+        def storedRequest = StoredRequest.getStoredRequest(ampRequest, ampStoredRequest)
+        storedRequestDao.save(storedRequest)
+
+        and: "Flush metrics"
+        flushMetrics(privacyPbsService)
+
+        when: "PBS processes amp request"
+        privacyPbsService.sendAmpRequest(ampRequest, header)
+
+        then: "Bidder shouldn't be called"
+        assert !bidder.getBidderRequests(ampStoredRequest.id)
+
+        then: "Metrics processed across activities should be updated"
+        def metrics = privacyPbsService.sendCollectedMetricsRequest()
+        assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1
+        assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(ampStoredRequest, FETCH_BIDS)] == 1
+
+        where:
+        requestCountry | accountCountry | requestIpV4 | requestIpV6 | header
+        BULGARIA       | BULGARIA       | BGR_IP.v4   | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | null           | BGR_IP.v4   | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | BULGARIA       | BGR_IP.v4   | null        | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | null           | BGR_IP.v4   | null        | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | BULGARIA       | null        | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | null           | null        | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | BULGARIA       | null        | null        | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | null           | null        | null        | ["X-Forwarded-For": BGR_IP.v4]
+        null           | BULGARIA       | BGR_IP.v4   | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        null           | null           | BGR_IP.v4   | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        null           | BULGARIA       | BGR_IP.v4   | null        | ["X-Forwarded-For": BGR_IP.v4]
+        null           | null           | BGR_IP.v4   | null        | ["X-Forwarded-For": BGR_IP.v4]
+        null           | BULGARIA       | null        | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        null           | null           | null        | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        null           | BULGARIA       | null        | null        | ["X-Forwarded-For": BGR_IP.v4]
+        null           | null           | null        | null        | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | BULGARIA       | BGR_IP.v4   | BGR_IP.v6   | [:]
+        BULGARIA       | null           | BGR_IP.v4   | BGR_IP.v6   | [:]
+        BULGARIA       | BULGARIA       | BGR_IP.v4   | null        | [:]
+        BULGARIA       | null           | BGR_IP.v4   | null        | [:]
+        BULGARIA       | BULGARIA       | null        | BGR_IP.v6   | [:]
+        BULGARIA       | null           | null        | BGR_IP.v6   | [:]
+        BULGARIA       | BULGARIA       | null        | null        | [:]
+        BULGARIA       | null           | null        | null        | [:]
+        null           | BULGARIA       | BGR_IP.v4   | BGR_IP.v6   | [:]
+        null           | null           | BGR_IP.v4   | BGR_IP.v6   | [:]
+        null           | BULGARIA       | BGR_IP.v4   | null        | [:]
+        null           | null           | BGR_IP.v4   | null        | [:]
+        null           | BULGARIA       | null        | BGR_IP.v6   | [:]
+        null           | null           | null        | BGR_IP.v6   | [:]
+        null           | BULGARIA       | null        | null        | [:]
+        null           | null           | null        | null        | [:]
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy
index c4a21342ab9..351875e5f90 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprAuctionSpec.groovy
@@ -5,10 +5,11 @@ import org.prebid.server.functional.model.ChannelType
 import org.prebid.server.functional.model.config.AccountGdprConfig
 import org.prebid.server.functional.model.config.PurposeConfig
 import org.prebid.server.functional.model.config.PurposeEnforcement
+import org.prebid.server.functional.model.pricefloors.Country
 import org.prebid.server.functional.model.request.auction.DistributionChannel
+import org.prebid.server.functional.model.request.auction.Regs
+import org.prebid.server.functional.model.request.auction.RegsExt
 import org.prebid.server.functional.model.response.auction.ErrorType
-import org.prebid.server.functional.service.PrebidServerService
-import org.prebid.server.functional.testcontainers.container.PrebidServerContainer
 import org.prebid.server.functional.util.PBSUtils
 import org.prebid.server.functional.util.privacy.BogusConsent
 import org.prebid.server.functional.util.privacy.TcfConsent
@@ -36,6 +37,7 @@ import static org.prebid.server.functional.model.request.auction.ActivityType.TR
 import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_PRECISE_GEO
 import static org.prebid.server.functional.model.request.auction.ActivityType.TRANSMIT_UFPD
 import static org.prebid.server.functional.model.request.auction.Prebid.Channel
+import static org.prebid.server.functional.model.request.auction.PublicCountryIp.BGR_IP
 import static org.prebid.server.functional.model.request.auction.TraceLevel.BASIC
 import static org.prebid.server.functional.model.request.auction.TraceLevel.VERBOSE
 import static org.prebid.server.functional.model.response.auction.BidRejectionReason.REQUEST_BLOCKED_PRIVACY
@@ -274,10 +276,8 @@ class GdprAuctionSpec extends PrivacyBaseSpec {
         def startTime = Instant.now()
 
         and: "Create new container"
-        def serverContainer = new PrebidServerContainer(GDPR_VENDOR_LIST_CONFIG +
-                ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String])
-        serverContainer.start()
-        def privacyPbsService = new PrebidServerService(serverContainer)
+        def config = GDPR_VENDOR_LIST_CONFIG + ["adapters.generic.meta-info.vendor-id": GENERIC_VENDOR_ID as String]
+        def defaultPrivacyPbsService = pbsServiceFactory.getService(config)
 
         and: "Tcf consent setup"
         def tcfConsent = new TcfConsent.Builder()
@@ -294,21 +294,21 @@ class GdprAuctionSpec extends PrivacyBaseSpec {
         vendorListResponse.setResponse(tcfPolicyVersion)
 
         when: "PBS processes auction request"
-        privacyPbsService.sendAuctionRequest(bidRequest)
+        defaultPrivacyPbsService.sendAuctionRequest(bidRequest)
 
         then: "Used vendor list have proper specification version of GVL"
         def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString())
-        PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) }
-        def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class)
+        PBSUtils.waitUntil { defaultPrivacyPbsService.isFileExist(properVendorListPath) }
+        def vendorList = defaultPrivacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class)
         assert vendorList.tcfPolicyVersion == tcfPolicyVersion.vendorListVersion
 
         and: "Logs should contain proper vendor list version"
-        def logs = privacyPbsService.getLogsByTime(startTime)
+        def logs = defaultPrivacyPbsService.getLogsByTime(startTime)
         assert getLogsByText(logs, "Created new TCF 2 vendor list for version " +
                 "v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion}")
 
-        cleanup: "Stop container with default request"
-        serverContainer.stop()
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(config)
 
         where:
         tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5]
@@ -353,7 +353,10 @@ class GdprAuctionSpec extends PrivacyBaseSpec {
     }
 
     def "PBS auction should emit the same error without a second GVL list request if a retry is too soon for the exponential-backoff"() {
-        given: "Test start time"
+        given: "Prebid server with privacy settings"
+        def defaultPrivacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG)
+
+        and: "Test start time"
         def startTime = Instant.now()
 
         and: "Tcf consent setup"
@@ -374,14 +377,14 @@ class GdprAuctionSpec extends PrivacyBaseSpec {
         vendorListResponse.setResponse(tcfPolicyVersion, Delay.seconds(EXPONENTIAL_BACKOFF_MAX_DELAY + 3))
 
         when: "PBS processes auction request"
-        privacyPbsService.sendAuctionRequest(bidRequest)
+        defaultPrivacyPbsService.sendAuctionRequest(bidRequest)
 
         then: "Used vendor list have proper specification version of GVL"
         def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString())
-        assert !privacyPbsService.isFileExist(properVendorListPath)
+        assert !defaultPrivacyPbsService.isFileExist(properVendorListPath)
 
         and: "Logs should contain proper vendor list version"
-        def logs = privacyPbsService.getLogsByTime(startTime)
+        def logs = defaultPrivacyPbsService.getLogsByTime(startTime)
         def tcfError = "TCF 2 vendor list for version v${tcfPolicyVersion.vendorListVersion}.${tcfPolicyVersion.vendorListVersion} not found, started downloading."
         assert getLogsByText(logs, tcfError)
 
@@ -389,18 +392,21 @@ class GdprAuctionSpec extends PrivacyBaseSpec {
         def secondStartTime = Instant.now()
 
         when: "PBS processes amp request"
-        privacyPbsService.sendAuctionRequest(bidRequest)
+        defaultPrivacyPbsService.sendAuctionRequest(bidRequest)
 
         then: "PBS shouldn't fetch vendor list"
-        assert !privacyPbsService.isFileExist(properVendorListPath)
+        assert !defaultPrivacyPbsService.isFileExist(properVendorListPath)
 
         and: "Logs should contain proper vendor list version"
-        def logsSecond = privacyPbsService.getLogsByTime(secondStartTime)
+        def logsSecond = defaultPrivacyPbsService.getLogsByTime(secondStartTime)
         assert getLogsByText(logsSecond, tcfError)
 
         and: "Reset vendor list response"
         vendorListResponse.reset()
 
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG)
+
         where:
         tcfPolicyVersion << [TCF_POLICY_V2, TCF_POLICY_V4, TCF_POLICY_V5]
     }
@@ -779,7 +785,10 @@ class GdprAuctionSpec extends PrivacyBaseSpec {
     }
 
     def "PBS auction should set 3 for tcfPolicyVersion when tcfPolicyVersion is #tcfPolicyVersion"() {
-        given: "Tcf consent setup"
+        given: "Prebid server with privacy settings"
+        def defaultPrivacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG)
+
+        and: "Tcf consent setup"
         def tcfConsent = new TcfConsent.Builder()
                 .setPurposesLITransparency(BASIC_ADS)
                 .setTcfPolicyVersion(tcfPolicyVersion.value)
@@ -794,15 +803,161 @@ class GdprAuctionSpec extends PrivacyBaseSpec {
         vendorListResponse.setResponse(tcfPolicyVersion)
 
         when: "PBS processes auction request"
-        privacyPbsService.sendAuctionRequest(bidRequest)
+        defaultPrivacyPbsService.sendAuctionRequest(bidRequest)
 
         then: "Used vendor list have proper specification version of GVL"
         def properVendorListPath = VENDOR_LIST_PATH.replace("{VendorVersion}", tcfPolicyVersion.vendorListVersion.toString())
-        PBSUtils.waitUntil { privacyPbsService.isFileExist(properVendorListPath) }
-        def vendorList = privacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class)
+        PBSUtils.waitUntil { defaultPrivacyPbsService.isFileExist(properVendorListPath) }
+        def vendorList = defaultPrivacyPbsService.getValueFromContainer(properVendorListPath, VendorListConsent.class)
         assert vendorList.gvlSpecificationVersion == V3
 
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(GENERAL_PRIVACY_CONFIG)
+
         where:
         tcfPolicyVersion << [TCF_POLICY_V4, TCF_POLICY_V5]
     }
+
+    def "PBS should process with GDPR enforcement when GDPR and COPPA configurations are present in request"() {
+        given: "Valid consent string without basic ads"
+        def validConsentString = new TcfConsent.Builder()
+                .setPurposesLITransparency(DEVICE_ACCESS)
+                .setVendorLegitimateInterest([GENERIC_VENDOR_ID])
+                .build()
+
+        and: "Bid request with gdpr and coppa config"
+        def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap {
+            regs = new Regs(gdpr: gdpr, coppa: coppa, ext: new RegsExt(gdpr: extGdpr, coppa: extCoppa))
+        }
+
+        and: "Save account config without eea countries into DB"
+        def accountGdprConfig = new AccountGdprConfig(enabled: true, eeaCountries: PBSUtils.getRandomEnum(Country.class, [BULGARIA]))
+        def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig)
+        accountDao.save(account)
+
+        and: "Flush metrics"
+        flushMetrics(privacyPbsService)
+
+        when: "PBS processes auction request"
+        privacyPbsService.sendAuctionRequest(bidRequest)
+
+        then: "Bidder shouldn't be called"
+        assert !bidder.getBidderRequests(bidRequest.id)
+
+        then: "Metrics processed across activities should be updated"
+        def metrics = privacyPbsService.sendCollectedMetricsRequest()
+        assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1
+        assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1
+
+        where:
+        gdpr | coppa | extGdpr | extCoppa
+        1    | 1     | 1       | 1
+        1    | 1     | 1       | 0
+        1    | 1     | 1       | null
+        1    | 1     | 0       | 1
+        1    | 1     | 0       | 0
+        1    | 1     | 0       | null
+        1    | 1     | null    | 1
+        1    | 1     | null    | 0
+        1    | 1     | null    | null
+        1    | 0     | 1       | 1
+        1    | 0     | 1       | 0
+        1    | 0     | 1       | null
+        1    | 0     | 0       | 1
+        1    | 0     | 0       | 0
+        1    | 0     | 0       | null
+        1    | 0     | null    | 1
+        1    | 0     | null    | 0
+        1    | 0     | null    | null
+        1    | null  | 1       | 1
+        1    | null  | 1       | 0
+        1    | null  | 1       | null
+        1    | null  | 0       | 1
+        1    | null  | 0       | 0
+        1    | null  | 0       | null
+        1    | null  | null    | 1
+        1    | null  | null    | 0
+        1    | null  | null    | null
+
+        null | 1     | 1       | 1
+        null | 1     | 1       | 0
+        null | 1     | 1       | null
+        null | 0     | 1       | 1
+        null | 0     | 1       | 0
+        null | 0     | 1       | null
+        null | null  | 1       | 1
+        null | null  | 1       | 0
+        null | null  | 1       | null
+    }
+
+    def "PBS should process with GDPR enforcement when request comes from EEA IP with COPPA enabled"() {
+        given: "Valid consent string without basic ads"
+        def validConsentString = new TcfConsent.Builder()
+                .setPurposesLITransparency(DEVICE_ACCESS)
+                .setVendorLegitimateInterest([GENERIC_VENDOR_ID])
+                .build()
+
+        and: "Bid request with gdpr and coppa config"
+        def bidRequest = getGdprBidRequest(DistributionChannel.APP, validConsentString).tap {
+            regs = new Regs(gdpr: 1, coppa: 1, ext: new RegsExt(gdpr: 1, coppa: 1))
+            device.geo.country = requestCountry
+            device.geo.region = null
+            device.ip = requestIpV4
+            device.ipv6 = requestIpV6
+        }
+
+        and: "Save account config without eea countries into DB"
+        def accountGdprConfig = new AccountGdprConfig(enabled: true, eeaCountries: accountCountry)
+        def account = getAccountWithGdpr(bidRequest.accountId, accountGdprConfig)
+        accountDao.save(account)
+
+        and: "Flush metrics"
+        flushMetrics(privacyPbsService)
+
+        when: "PBS processes auction request"
+        privacyPbsService.sendAuctionRequest(bidRequest, header)
+
+        then: "Bidder shouldn't be called"
+        assert !bidder.getBidderRequests(bidRequest.id)
+
+        then: "Metrics processed across activities should be updated"
+        def metrics = privacyPbsService.sendCollectedMetricsRequest()
+        assert metrics[TEMPLATE_ADAPTER_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1
+        assert metrics[TEMPLATE_REQUEST_DISALLOWED_COUNT.getValue(bidRequest, FETCH_BIDS)] == 1
+
+        where:
+        requestCountry | accountCountry | requestIpV4 | requestIpV6 | header
+        BULGARIA       | BULGARIA       | BGR_IP.v4   | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | null           | BGR_IP.v4   | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | BULGARIA       | BGR_IP.v4   | null        | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | null           | BGR_IP.v4   | null        | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | BULGARIA       | null        | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | null           | null        | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | BULGARIA       | null        | null        | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | null           | null        | null        | ["X-Forwarded-For": BGR_IP.v4]
+        null           | BULGARIA       | BGR_IP.v4   | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        null           | null           | BGR_IP.v4   | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        null           | BULGARIA       | BGR_IP.v4   | null        | ["X-Forwarded-For": BGR_IP.v4]
+        null           | null           | BGR_IP.v4   | null        | ["X-Forwarded-For": BGR_IP.v4]
+        null           | BULGARIA       | null        | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        null           | null           | null        | BGR_IP.v6   | ["X-Forwarded-For": BGR_IP.v4]
+        null           | BULGARIA       | null        | null        | ["X-Forwarded-For": BGR_IP.v4]
+        null           | null           | null        | null        | ["X-Forwarded-For": BGR_IP.v4]
+        BULGARIA       | BULGARIA       | BGR_IP.v4   | BGR_IP.v6   | [:]
+        BULGARIA       | null           | BGR_IP.v4   | BGR_IP.v6   | [:]
+        BULGARIA       | BULGARIA       | BGR_IP.v4   | null        | [:]
+        BULGARIA       | null           | BGR_IP.v4   | null        | [:]
+        BULGARIA       | BULGARIA       | null        | BGR_IP.v6   | [:]
+        BULGARIA       | null           | null        | BGR_IP.v6   | [:]
+        BULGARIA       | BULGARIA       | null        | null        | [:]
+        BULGARIA       | null           | null        | null        | [:]
+        null           | BULGARIA       | BGR_IP.v4   | BGR_IP.v6   | [:]
+        null           | null           | BGR_IP.v4   | BGR_IP.v6   | [:]
+        null           | BULGARIA       | BGR_IP.v4   | null        | [:]
+        null           | null           | BGR_IP.v4   | null        | [:]
+        null           | BULGARIA       | null        | BGR_IP.v6   | [:]
+        null           | null           | null        | BGR_IP.v6   | [:]
+        null           | BULGARIA       | null        | null        | [:]
+        null           | null           | null        | null        | [:]
+    }
 }
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprSetUidSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprSetUidSpec.groovy
new file mode 100644
index 00000000000..ce8d308c3a2
--- /dev/null
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GdprSetUidSpec.groovy
@@ -0,0 +1,333 @@
+package org.prebid.server.functional.tests.privacy
+
+import org.prebid.server.functional.model.UidsCookie
+import org.prebid.server.functional.model.config.AccountAuctionConfig
+import org.prebid.server.functional.model.config.AccountConfig
+import org.prebid.server.functional.model.config.AccountGdprConfig
+import org.prebid.server.functional.model.config.AccountPrivacyConfig
+import org.prebid.server.functional.model.config.PurposeConfig
+import org.prebid.server.functional.model.db.Account
+import org.prebid.server.functional.model.request.setuid.SetuidRequest
+import org.prebid.server.functional.model.response.cookiesync.UserSyncInfo
+import org.prebid.server.functional.service.PrebidServerException
+import org.prebid.server.functional.service.PrebidServerService
+import org.prebid.server.functional.util.PBSUtils
+import org.prebid.server.functional.util.privacy.TcfConsent
+import org.prebid.server.util.ResourceUtil
+
+import static org.prebid.server.functional.model.AccountStatus.ACTIVE
+import static org.prebid.server.functional.model.bidder.BidderName.GENERIC
+import static org.prebid.server.functional.model.bidder.BidderName.GENER_X
+import static org.prebid.server.functional.model.config.Purpose.P1
+import static org.prebid.server.functional.model.config.PurposeEnforcement.FULL
+import static org.prebid.server.functional.model.config.PurposeEnforcement.NO
+import static org.prebid.server.functional.model.request.setuid.UidWithExpiry.getDefaultUidWithExpiry
+import static org.prebid.server.functional.model.response.cookiesync.UserSyncInfo.Type.REDIRECT
+import static org.prebid.server.functional.testcontainers.Dependencies.getNetworkServiceContainer
+import static org.prebid.server.functional.util.privacy.TcfConsent.GENERIC_VENDOR_ID
+import static org.prebid.server.functional.util.privacy.TcfConsent.PurposeId.DEVICE_ACCESS
+
+class GdprSetUidSpec extends PrivacyBaseSpec {
+
+    private static final boolean CORS_SUPPORT = false
+    private static final String USER_SYNC_URL = "$networkServiceContainer.rootUri/generic-usersync"
+    private static final UserSyncInfo.Type USER_SYNC_TYPE = REDIRECT
+    private static final Map<String, String> VENDOR_GENERIC_PBS_CONFIG = GENERIC_VENDOR_CONFIG +
+            ["gdpr.purposes.p1.enforce-purpose"                                       : NO.value,
+             "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.url"         : USER_SYNC_URL,
+             "adapters.${GENERIC.value}.usersync.${USER_SYNC_TYPE.value}.support-cors": CORS_SUPPORT.toString()]
+    private static final String TCF_ERROR_MESSAGE = "The gdpr_consent param prevents cookies from being saved"
+    private static final int UNAVAILABLE_FOR_LEGAL_REASONS_CODE = 451
+
+    private static final PrebidServerService prebidServerService = pbsServiceFactory.getService(VENDOR_GENERIC_PBS_CONFIG)
+
+    def "PBS setuid shouldn't failed with tcf when purpose access device not enforced"() {
+        given: "Default setuid request with account"
+        def setuidRequest = SetuidRequest.defaultSetuidRequest.tap {
+            it.account = PBSUtils.randomNumber.toString()
+            it.uid = UUID.randomUUID().toString()
+            it.bidder = GENERIC
+            it.gdpr = "1"
+            it.gdprConsent = new TcfConsent.Builder()
+                    .setPurposesLITransparency(DEVICE_ACCESS)
+                    .setVendorLegitimateInterest([GENERIC_VENDOR_ID])
+                    .build()
+        }
+
+        and: "Default uids cookie with gener_x bidder"
+        def uidsCookie = UidsCookie.defaultUidsCookie.tap {
+            it.tempUIDs = [(GENERIC): defaultUidWithExpiry]
+        }
+
+        and: "Save account config with purpose into DB"
+        def accountConfig = new AccountConfig(
+                auction: new AccountAuctionConfig(debugAllow: true),
+                privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): new PurposeConfig(enforcePurpose: NO)], enabled: true)))
+        def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes setuid request"
+        def response = prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie)
+
+        then: "Response should contain tempUids cookie and headers"
+        assert response.headers.size() == 7
+        assert response.uidsCookie.tempUIDs[GENERIC].uid == setuidRequest.uid
+        assert response.responseBody ==
+                ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png")
+    }
+
+    def "PBS setuid shouldn't failed with tcf when bidder name and cookie-family-name mismatching"() {
+        given: "PBS with different cookie-family-name"
+        def pbsConfig = VENDOR_GENERIC_PBS_CONFIG +
+                ["adapters.${GENERIC.value}.usersync.cookie-family-name": GENER_X.value]
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Setuid request with account"
+        def setuidRequest = SetuidRequest.defaultSetuidRequest.tap {
+            it.account = PBSUtils.randomNumber.toString()
+            it.uid = UUID.randomUUID().toString()
+            it.bidder = GENER_X
+            it.gdpr = "1"
+            it.gdprConsent = new TcfConsent.Builder()
+                    .setPurposesLITransparency(DEVICE_ACCESS)
+                    .setVendorLegitimateInterest([GENERIC_VENDOR_ID])
+                    .build()
+        }
+
+        and: "Default uids cookie with gener_x bidder"
+        def uidsCookie = UidsCookie.defaultUidsCookie.tap {
+            it.tempUIDs = [(GENER_X): defaultUidWithExpiry]
+        }
+
+        and: "Save account config with purpose into DB"
+        def accountConfig = new AccountConfig(
+                auction: new AccountAuctionConfig(debugAllow: true),
+                privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): new PurposeConfig(enforcePurpose: NO)], enabled: true)))
+        def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes setuid request"
+        def response = prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie)
+
+        then: "Response should contain tempUids cookie and headers"
+        assert response.headers.size() == 7
+        assert response.uidsCookie.tempUIDs[GENER_X].uid == setuidRequest.uid
+        assert response.responseBody ==
+                ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png")
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS setuid should failed with tcf when dgpr value is invalid"() {
+        given: "Default setuid request with account"
+        def setuidRequest = SetuidRequest.defaultSetuidRequest.tap {
+            it.account = PBSUtils.randomNumber.toString()
+            it.uid = UUID.randomUUID().toString()
+            it.bidder = GENERIC
+            it.gdpr = "1"
+            it.gdprConsent = new TcfConsent.Builder()
+                    .setPurposesLITransparency(DEVICE_ACCESS)
+                    .setVendorLegitimateInterest([PBSUtils.getRandomNumberWithExclusion(GENERIC_VENDOR_ID, 0, 65534)])
+                    .build()
+        }
+
+        and: "Flush metrics"
+        flushMetrics(prebidServerService)
+
+        and: "Default uids cookie with generic bidder"
+        def uidsCookie = UidsCookie.defaultUidsCookie.tap {
+            it.tempUIDs = [(GENERIC): defaultUidWithExpiry]
+        }
+
+        and: "Save account config with purpose into DB"
+        def accountConfig = new AccountConfig(
+                auction: new AccountAuctionConfig(debugAllow: true),
+                privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): new PurposeConfig(enforcePurpose: NO)], enabled: true)))
+        def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes setuid request"
+        prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie)
+
+        then: "Request should fail with error"
+        def exception = thrown(PrebidServerException)
+        assert exception.statusCode == UNAVAILABLE_FOR_LEGAL_REASONS_CODE
+        assert exception.responseBody == TCF_ERROR_MESSAGE
+
+        and: "Metric should be increased usersync.FAMILY.tcf.blocked"
+        def metric = prebidServerService.sendCollectedMetricsRequest()
+        assert metric["usersync.${GENERIC.value}.tcf.blocked"] == 1
+    }
+
+    def "PBS setuid should failed with tcf when purpose access device enforced for account"() {
+        given: "Default setuid request with account"
+        def setuidRequest = SetuidRequest.defaultSetuidRequest.tap {
+            it.account = PBSUtils.randomNumber.toString()
+            it.uid = UUID.randomUUID().toString()
+            it.bidder = GENERIC
+            it.gdpr = "1"
+            it.gdprConsent = new TcfConsent.Builder()
+                    .setPurposesLITransparency(DEVICE_ACCESS)
+                    .setVendorLegitimateInterest([GENERIC_VENDOR_ID])
+                    .build()
+        }
+
+        and: "Default uids cookie with generic bidder"
+        def uidsCookie = UidsCookie.defaultUidsCookie.tap {
+            it.tempUIDs = [(GENERIC): defaultUidWithExpiry]
+        }
+
+        and: "Flush metrics"
+        flushMetrics(prebidServerService)
+
+        and: "Save account config with purpose into DB"
+        def accountConfig = new AccountConfig(
+                auction: new AccountAuctionConfig(debugAllow: true),
+                privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): new PurposeConfig(enforcePurpose: FULL)], enabled: true)))
+        def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes setuid request"
+        prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie)
+
+        then: "Request should fail with error"
+        def exception = thrown(PrebidServerException)
+        assert exception.statusCode == UNAVAILABLE_FOR_LEGAL_REASONS_CODE
+        assert exception.responseBody == TCF_ERROR_MESSAGE
+
+        and: "Metric should be increased usersync.FAMILY.tcf.blocked"
+        def metric = prebidServerService.sendCollectedMetricsRequest()
+        assert metric["usersync.${GENERIC.value}.tcf.blocked"] == 1
+    }
+
+    def "PBS setuid should failed with tcf when purpose access device enforced for host"() {
+        given: "PBS config"
+        def pbsConfig = VENDOR_GENERIC_PBS_CONFIG + ["gdpr.purposes.p1.enforce-purpose": FULL.value]
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default setuid request with account"
+        def setuidRequest = SetuidRequest.defaultSetuidRequest.tap {
+            it.account = PBSUtils.randomNumber.toString()
+            it.uid = UUID.randomUUID().toString()
+            it.bidder = GENERIC
+            it.gdpr = "1"
+            it.gdprConsent = new TcfConsent.Builder()
+                    .setPurposesLITransparency(DEVICE_ACCESS)
+                    .setVendorLegitimateInterest([GENERIC_VENDOR_ID])
+                    .build()
+        }
+
+        and: "Default uids cookie with generic bidder"
+        def uidsCookie = UidsCookie.defaultUidsCookie.tap {
+            it.tempUIDs = [(GENERIC): defaultUidWithExpiry]
+        }
+
+        and: "Flush metrics"
+        flushMetrics(prebidServerService)
+
+        and: "Save account config with purpose into DB"
+        def accountConfig = new AccountConfig(
+                auction: new AccountAuctionConfig(debugAllow: true),
+                privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): new PurposeConfig(enforcePurpose: NO)], enabled: true)))
+        def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes setuid request"
+        prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie)
+
+        then: "Request should fail with error"
+        def exception = thrown(PrebidServerException)
+        assert exception.statusCode == UNAVAILABLE_FOR_LEGAL_REASONS_CODE
+        assert exception.responseBody == TCF_ERROR_MESSAGE
+
+        and: "Metric should be increased usersync.FAMILY.tcf.blocked"
+        def metric = prebidServerService.sendCollectedMetricsRequest()
+        assert metric["usersync.${GENERIC.value}.tcf.blocked"] == 1
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS setuid shouldn't failed with tcf when purpose access device not enforced for host and host-vendor-id empty"() {
+        given: "PBS config"
+        def pbsConfig = VENDOR_GENERIC_PBS_CONFIG + ["gdpr.purposes.p1.enforce-purpose": NO.value,
+                                                     "gdpr.host-vendor-id"             : ""]
+        def prebidServerService = pbsServiceFactory.getService(pbsConfig)
+
+        and: "Default setuid request with account"
+        def setuidRequest = SetuidRequest.defaultSetuidRequest.tap {
+            it.account = PBSUtils.randomNumber.toString()
+            it.uid = UUID.randomUUID().toString()
+            it.bidder = GENERIC
+            it.gdpr = "1"
+            it.gdprConsent = new TcfConsent.Builder()
+                    .setPurposesLITransparency(DEVICE_ACCESS)
+                    .setVendorLegitimateInterest([GENERIC_VENDOR_ID])
+                    .build()
+        }
+
+        and: "Default uids cookie with generic bidder"
+        def uidsCookie = UidsCookie.defaultUidsCookie.tap {
+            it.tempUIDs = [(GENERIC): defaultUidWithExpiry]
+        }
+
+        and: "Flush metrics"
+        flushMetrics(prebidServerService)
+
+        and: "Save account config with purpose into DB"
+        def accountConfig = new AccountConfig(
+                auction: new AccountAuctionConfig(debugAllow: true),
+                privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): new PurposeConfig(enforcePurpose: NO)], enabled: true)))
+        def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes setuid request"
+        def response = prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie)
+
+        then: "Response should contain tempUids cookie and headers"
+        assert response.headers.size() == 7
+        assert response.uidsCookie.tempUIDs[GENERIC].uid == setuidRequest.uid
+        assert response.responseBody ==
+                ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png")
+
+        cleanup: "Stop and remove pbs container"
+        pbsServiceFactory.removeContainer(pbsConfig)
+    }
+
+    def "PBS setuid shouldn't failed with purpose access device enforced for account when bidder included in vendorExceptions"() {
+        given: "Default setuid request with account"
+        def setuidRequest = SetuidRequest.defaultSetuidRequest.tap {
+            it.account = PBSUtils.randomNumber.toString()
+            it.uid = UUID.randomUUID().toString()
+            it.bidder = GENERIC
+            it.gdpr = "1"
+            it.gdprConsent = new TcfConsent.Builder()
+                    .setPurposesLITransparency(DEVICE_ACCESS)
+                    .setVendorLegitimateInterest([GENERIC_VENDOR_ID])
+                    .build()
+        }
+
+        and: "Default uids cookie with generic bidder"
+        def uidsCookie = UidsCookie.defaultUidsCookie.tap {
+            it.tempUIDs = [(GENERIC): defaultUidWithExpiry]
+        }
+
+        and: "Save account config with purpose into DB"
+        def purposeConfig = new PurposeConfig(enforcePurpose: FULL, vendorExceptions: [GENERIC.value])
+        def accountConfig = new AccountConfig(
+                auction: new AccountAuctionConfig(debugAllow: true),
+                privacy: new AccountPrivacyConfig(gdpr: new AccountGdprConfig(purposes: [(P1): purposeConfig], enabled: true)))
+        def account = new Account(status: ACTIVE, uuid: setuidRequest.account, config: accountConfig)
+        accountDao.save(account)
+
+        when: "PBS processes setuid request"
+        def response = prebidServerService.sendSetUidRequest(setuidRequest, uidsCookie)
+
+        then: "Response should contain tempUids cookie and headers"
+        assert response.headers.size() == 7
+        assert response.uidsCookie.tempUIDs[GENERIC].uid == setuidRequest.uid
+        assert response.responseBody ==
+                ResourceUtil.readByteArrayFromClassPath("org/prebid/server/functional/tracking-pixel.png")
+    }
+}
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy
index 602fbb87347..f684cb10313 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/GppSyncUserActivitiesSpec.groovy
@@ -1778,7 +1778,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec {
 
     def "PBS cookie sync should process rule when geo doesn't intersection"() {
         given: "Pbs config with geo location"
-        def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION +
+        def prebidServerService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GEO_LOCATION +
                 ["geolocation.configurations.geo-info.[0].country": countyConfig,
                  "geolocation.configurations.geo-info.[0].region" : regionConfig])
 
@@ -1830,7 +1830,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec {
 
     def "PBS setuid should process rule when geo doesn't intersection"() {
         given: "Pbs config with geo location"
-        def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION +
+        def prebidServerService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GEO_LOCATION +
                 ["geolocation.configurations.[0].geo-info.country": countyConfig,
                  "geolocation.configurations.[0].geo-info.region" : regionConfig])
 
@@ -1885,7 +1885,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec {
 
     def "PBS cookie sync should disallowed rule when device.geo intersection"() {
         given: "Pbs config with geo location"
-        def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION +
+        def prebidServerService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GEO_LOCATION +
                 ["geolocation.configurations.[0].geo-info.country": countyConfig,
                  "geolocation.configurations.[0].geo-info.region" : regionConfig])
 
@@ -1936,7 +1936,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec {
 
     def "PBS setuid should disallowed rule when device.geo intersection"() {
         given: "Pbs config with geo location"
-        def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION +
+        def prebidServerService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GEO_LOCATION +
                 ["geolocation.configurations.[0].geo-info.country": countyConfig,
                  "geolocation.configurations.[0].geo-info.region" : regionConfig])
 
@@ -1986,7 +1986,7 @@ class GppSyncUserActivitiesSpec extends PrivacyBaseSpec {
 
     def "PBS cookie sync should fetch geo once when gpp sync user and account require geo look up"() {
         given: "Pbs config with geo location"
-        def prebidServerService = pbsServiceFactory.getService(PBS_CONFIG + GEO_LOCATION +
+        def prebidServerService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GEO_LOCATION +
                 ["geolocation.configurations.[0].geo-info.country": USA.ISOAlpha3,
                  "geolocation.configurations.[0].geo-info.region" : ALABAMA.abbreviation])
 
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy
index 145a33336f9..d4cbf17e2dd 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/PrivacyBaseSpec.groovy
@@ -106,15 +106,14 @@ abstract class PrivacyBaseSpec extends BaseSpec {
     protected static final Integer MAX_INVALID_TCF_POLICY_VERSION = 63
     protected static final Integer MIN_INVALID_TCF_POLICY_VERSION = 6
 
-    @Shared
-    protected final PrebidServerService privacyPbsService = pbsServiceFactory.getService(GDPR_VENDOR_LIST_CONFIG +
-            GENERIC_CONFIG + GENERIC_VENDOR_CONFIG + RETRY_POLICY_EXPONENTIAL_CONFIG + GDPR_EEA_COUNTRY)
+    protected static final Map<String, String> GENERAL_PRIVACY_CONFIG =
+            GENERIC_CONFIG + GDPR_VENDOR_LIST_CONFIG + GENERIC_VENDOR_CONFIG + RETRY_POLICY_EXPONENTIAL_CONFIG
 
-    protected static final Map<String, String> PBS_CONFIG = OPENX_CONFIG +
-            GENERIC_CONFIG + GDPR_VENDOR_LIST_CONFIG + SETTING_CONFIG + GENERIC_VENDOR_CONFIG
+    @Shared
+    protected final PrebidServerService privacyPbsService = pbsServiceFactory.getService(GENERAL_PRIVACY_CONFIG + GDPR_EEA_COUNTRY)
 
     @Shared
-    protected final PrebidServerService activityPbsService = pbsServiceFactory.getService(PBS_CONFIG)
+    protected final PrebidServerService activityPbsService = pbsServiceFactory.getService(OPENX_CONFIG + SETTING_CONFIG + GENERAL_PRIVACY_CONFIG)
 
     def setupSpec() {
         vendorListResponse.setResponse()
diff --git a/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfFullTransmitEidsActivitiesSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfFullTransmitEidsActivitiesSpec.groovy
index 934aa69de33..d798bc4c478 100644
--- a/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfFullTransmitEidsActivitiesSpec.groovy
+++ b/src/test/groovy/org/prebid/server/functional/tests/privacy/TcfFullTransmitEidsActivitiesSpec.groovy
@@ -25,7 +25,7 @@ class TcfFullTransmitEidsActivitiesSpec extends PrivacyBaseSpec {
     private static PrebidServerService privacyPbsServiceWithMultipleGvl
 
     def setupSpec() {
-        privacyPbsContainerWithMultipleGvl = new PrebidServerContainer(PBS_CONFIG)
+        privacyPbsContainerWithMultipleGvl = new PrebidServerContainer(GENERAL_PRIVACY_CONFIG)
         def prepareEncodeResponseBodyWithPurposesOnly = getVendorListContent(true, false, false)
         def prepareEncodeResponseBodyWithLegIntPurposes = getVendorListContent(false, true, false)
         def prepareEncodeResponseBodyWithLegIntAndFlexiblePurposes = getVendorListContent(false, true, true)
diff --git a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy
index 0d72dcfe4c6..9ac2c148b5b 100644
--- a/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy
+++ b/src/test/groovy/org/prebid/server/functional/util/PBSUtils.groovy
@@ -107,9 +107,9 @@ class PBSUtils implements ObjectMapperWrapper {
         getRandomDecimal(min, max).setScale(scale, HALF_UP)
     }
 
-    static <T extends Enum<T>> T getRandomEnum(Class<T> anEnum) {
-        def values = anEnum.enumConstants
-        values[getRandomNumber(0, values.length - 1)]
+    static <T extends Enum<T>> T getRandomEnum(Class<T> anEnum, List<T> exclude = []) {
+        def values = anEnum.enumConstants.findAll { !exclude.contains(it) } as T[]
+        values[getRandomNumber(0, values.size() - 1)]
     }
 
     static String convertCase(String input, Case caseType) {
@@ -128,4 +128,27 @@ class PBSUtils implements ObjectMapperWrapper {
                 throw new IllegalArgumentException("Unknown case type: $caseType")
         }
     }
+
+    static String getRandomVersion(String minVersion = "0.0.0", String maxVersion = "99.99.99") {
+        def minParts = minVersion.split('\\.').collect { it.toInteger() }
+        def maxParts = maxVersion.split('\\.').collect { it.toInteger() }
+        def versionParts = []
+
+        def major = getRandomNumber(minParts[0], maxParts[0])
+        versionParts << major
+
+        def minorMin = (major == minParts[0]) ? minParts[1] : 0
+        def minorMax = (major == maxParts[0]) ? maxParts[1] : 99
+        def minor = getRandomNumber(minorMin, minorMax)
+        versionParts << minor
+
+        if (minParts.size() > 2 || maxParts.size() > 2) {
+            def patchMin = (major == minParts[0] && minor == minParts[1]) ? minParts[2] : 0
+            def patchMax = (major == maxParts[0] && minor == maxParts[1]) ? maxParts[2] : 99
+            def patch = getRandomNumber(patchMin, patchMax)
+            versionParts << patch
+        }
+        def version = versionParts.join('.')
+        return (version >= minVersion && version <= maxVersion) ? version : getRandomVersion(minVersion, maxVersion)
+    }
 }
diff --git a/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java
index 2427942605e..e7ce7126031 100644
--- a/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java
+++ b/src/test/java/org/prebid/server/analytics/reporter/agma/AgmaAnalyticsReporterTest.java
@@ -109,6 +109,21 @@ public void setUp() {
         target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx);
     }
 
+    @Test
+    public void processEventShouldNotSendAnythingWhenAuctionContextIsNull() {
+        // given
+        final AuctionEvent auctionEvent = AuctionEvent.builder()
+                .auctionContext(null)
+                .build();
+
+        // when
+        final Future<Void> result = target.processEvent(auctionEvent);
+
+        // then
+        verifyNoInteractions(httpClient);
+        assertThat(result.succeeded()).isTrue();
+    }
+
     @Test
     public void processEventShouldSendEventWhenEventIsAuctionEvent() {
         // given
@@ -408,6 +423,124 @@ public void processEventShouldNotSendAnythingWhenAccountsDoesNotHaveConfiguredPu
         assertThat(result.succeeded()).isTrue();
     }
 
+    @Test
+    public void processEventShouldSendWhenAccountsHasConfiguredAppsOrSites() {
+        // given
+        final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder()
+                .url("http://endpoint.com")
+                .gzip(false)
+                .bufferSize(100000)
+                .bufferTimeoutMs(10000L)
+                .maxEventsCount(0)
+                .httpTimeoutMs(1000L)
+                .accounts(Map.of("publisherId_bundleId", "accountCode"))
+                .build();
+
+        target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx);
+
+        // given
+        final App givenApp = App.builder().bundle("bundleId")
+                .publisher(Publisher.builder().id("publisherId").build()).build();
+        final Device givenDevice = Device.builder().build();
+        final User givenUser = User.builder().build();
+
+        final AuctionEvent auctionEvent = AuctionEvent.builder()
+                .auctionContext(AuctionContext.builder()
+                        .privacyContext(PrivacyContext.of(
+                                null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build()))
+                        .timeoutContext(TimeoutContext.of(clock.millis(), null, 1))
+                        .bidRequest(BidRequest.builder()
+                                .id("requestId")
+                                .app(givenApp)
+                                .app(givenApp)
+                                .device(givenDevice)
+                                .user(givenUser)
+                                .build())
+                        .build())
+                .build();
+
+        // when
+        final Future<Void> result = target.processEvent(auctionEvent);
+
+        // then
+        final AgmaEvent expectedEvent = AgmaEvent.builder()
+                .eventType("auction")
+                .accountCode("accountCode")
+                .requestId("requestId")
+                .app(givenApp)
+                .device(givenDevice)
+                .user(givenUser)
+                .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00"))
+                .build();
+
+        final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]";
+
+        verify(httpClient).request(
+                eq(POST),
+                eq("http://endpoint.com"),
+                any(),
+                eq(expectedEventPayload),
+                eq(1000L));
+    }
+
+    @Test
+    public void processEventShouldSendWhenAccountsHasConfiguredAppsOrSitesOnly() {
+        // given
+        final AgmaAnalyticsProperties properties = AgmaAnalyticsProperties.builder()
+                .url("http://endpoint.com")
+                .gzip(false)
+                .bufferSize(100000)
+                .bufferTimeoutMs(10000L)
+                .maxEventsCount(0)
+                .httpTimeoutMs(1000L)
+                .accounts(Map.of("_mySite", "accountCode"))
+                .build();
+
+        target = new AgmaAnalyticsReporter(properties, versionProvider, jacksonMapper, clock, httpClient, vertx);
+
+        // given
+        final Site givenSite = Site.builder().id("mySite").build();
+        final Device givenDevice = Device.builder().build();
+        final User givenUser = User.builder().build();
+
+        final AuctionEvent auctionEvent = AuctionEvent.builder()
+                .auctionContext(AuctionContext.builder()
+                        .privacyContext(PrivacyContext.of(
+                                null, TcfContext.builder().consent(PARSED_VALID_CONSENT).build()))
+                        .timeoutContext(TimeoutContext.of(clock.millis(), null, 1))
+                        .bidRequest(BidRequest.builder()
+                                .id("requestId")
+                                .site(givenSite)
+                                .device(givenDevice)
+                                .user(givenUser)
+                                .build())
+                        .build())
+                .build();
+
+        // when
+        final Future<Void> result = target.processEvent(auctionEvent);
+
+        // then
+        final AgmaEvent expectedEvent = AgmaEvent.builder()
+                .eventType("auction")
+                .accountCode("accountCode")
+                .requestId("requestId")
+                .site(givenSite)
+                .device(givenDevice)
+                .user(givenUser)
+                .startTime(ZonedDateTime.parse("2024-09-03T15:00:00+05:00"))
+                .build();
+
+        final String expectedEventPayload = "[" + jacksonMapper.encodeToString(expectedEvent) + "]";
+
+        verify(httpClient).request(
+                eq(POST),
+                eq("http://endpoint.com"),
+                any(),
+                eq(expectedEventPayload),
+                eq(1000L));
+    }
+
     @Test
     public void processEventShouldSendEncodingGzipHeaderAndCompressedPayload() {
         // given
diff --git a/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java
index 0e4e0e0e8fd..cccdbb4e149 100644
--- a/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java
+++ b/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java
@@ -1,6 +1,7 @@
 package org.prebid.server.analytics.reporter.greenbids;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.fasterxml.jackson.databind.node.TextNode;
 import com.iab.openrtb.request.Banner;
@@ -26,20 +27,43 @@
 import org.prebid.server.VertxTest;
 import org.prebid.server.analytics.model.AuctionEvent;
 import org.prebid.server.analytics.reporter.greenbids.model.CommonMessage;
+import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult;
 import org.prebid.server.analytics.reporter.greenbids.model.ExtBanner;
 import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAdUnit;
 import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsAnalyticsProperties;
 import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsBid;
 import org.prebid.server.analytics.reporter.greenbids.model.GreenbidsUnifiedCode;
 import org.prebid.server.analytics.reporter.greenbids.model.MediaTypes;
+import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult;
+import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpResult;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.BidRejectionReason;
 import org.prebid.server.auction.model.BidRejectionTracker;
+import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookExecutionContext;
+import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
 import org.prebid.server.json.EncodeException;
 import org.prebid.server.json.JacksonMapper;
 import org.prebid.server.model.HttpRequestContext;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
+import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse;
+import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid;
+import org.prebid.server.proto.openrtb.ext.response.ExtModules;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome;
 import org.prebid.server.util.HttpUtil;
 import org.prebid.server.version.PrebidVersionProvider;
 import org.prebid.server.vertx.httpclient.HttpClient;
@@ -50,6 +74,7 @@
 import java.time.Clock;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.EnumMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -113,6 +138,65 @@ public void setUp() {
                 prebidVersionProvider);
     }
 
+    @Test
+    public void shouldReceiveValidResponseOnAuctionContextWithAnalyticsTagForBanner() throws IOException {
+        // given
+        final Banner banner = givenBanner();
+
+        final ObjectNode impExtNode = mapper.createObjectNode();
+        impExtNode.set("gpid", TextNode.valueOf("gpidvalue"));
+        impExtNode.set("prebid", givenPrebidBidderParamsNode());
+
+        final Imp imp = Imp.builder()
+                .id("adunitcodevalue")
+                .ext(impExtNode)
+                .banner(banner)
+                .build();
+
+        final AuctionContext auctionContext = givenAuctionContextWithAnalyticsTag(
+                context -> context, List.of(imp), true, true);
+        final AuctionEvent event = AuctionEvent.builder()
+                .auctionContext(auctionContext)
+                .bidResponse(auctionContext.getBidResponse())
+                .build();
+
+        final HttpClientResponse mockResponse = mock(HttpClientResponse.class);
+        when(mockResponse.getStatusCode()).thenReturn(202);
+        when(httpClient.post(anyString(), any(MultiMap.class), anyString(), anyLong()))
+                .thenReturn(Future.succeededFuture(mockResponse));
+        final CommonMessage expectedCommonMessage = givenCommonMessageForBannerWithRtb2Imp();
+
+        // when
+        final Future<Void> result = target.processEvent(event);
+
+        // then
+        assertThat(result.succeeded()).isTrue();
+        verify(httpClient).post(
+                eq(greenbidsAnalyticsProperties.getAnalyticsServerUrl()),
+                headersCaptor.capture(),
+                jsonCaptor.capture(),
+                eq(greenbidsAnalyticsProperties.getTimeoutMs()));
+
+        final String capturedJson = jsonCaptor.getValue();
+        final CommonMessage capturedCommonMessage = jacksonMapper.mapper()
+                .readValue(capturedJson, CommonMessage.class);
+
+        assertThat(capturedCommonMessage).usingRecursiveComparison()
+                .ignoringFields("billingId", "greenbidsId")
+                .isEqualTo(expectedCommonMessage);
+        assertThat(capturedCommonMessage.getGreenbidsId()).isNotNull();
+        assertThat(capturedCommonMessage.getBillingId()).isNotNull();
+        capturedCommonMessage.getAdUnits().forEach(adUnit -> {
+            assertThat(adUnit.getOrtb2ImpResult().getExt().getGreenbids().getFingerprint()).isNotNull();
+            assertThat(adUnit.getOrtb2ImpResult().getExt().getTid()).isNotNull();
+        });
+
+        assertThat(headersCaptor.getValue().get(HttpUtil.ACCEPT_HEADER))
+                .isEqualTo(HttpHeaderValues.APPLICATION_JSON.toString());
+        assertThat(headersCaptor.getValue().get(HttpUtil.CONTENT_TYPE_HEADER))
+                .isEqualTo(HttpHeaderValues.APPLICATION_JSON.toString());
+    }
+
     @Test
     public void shouldReceiveValidResponseOnAuctionContextForBanner() throws IOException {
         // given
@@ -508,6 +592,28 @@ private static AuctionContext givenAuctionContext(
         return auctionContextCustomizer.apply(auctionContextBuilder).build();
     }
 
+    private static AuctionContext givenAuctionContextWithAnalyticsTag(
+            UnaryOperator<AuctionContext.AuctionContextBuilder> auctionContextCustomizer,
+            List<Imp> imps,
+            boolean includeBidResponse,
+            boolean includeHookExecutionContextWithAnalyticsTag) {
+        final AuctionContext.AuctionContextBuilder auctionContextBuilder = AuctionContext.builder()
+                .httpRequest(HttpRequestContext.builder().build())
+                .bidRequest(givenBidRequest(request -> request, imps))
+                .bidRejectionTrackers(Map.of("seat3", givenBidRejectionTracker()));
+
+        if (includeHookExecutionContextWithAnalyticsTag) {
+            final HookExecutionContext hookExecutionContext = givenHookExecutionContextWithAnalyticsTag();
+            auctionContextBuilder.hookExecutionContext(hookExecutionContext);
+        }
+
+        if (includeBidResponse) {
+            auctionContextBuilder.bidResponse(givenBidResponse(response -> response));
+        }
+
+        return auctionContextCustomizer.apply(auctionContextBuilder).build();
+    }
+
     private static BidRequest givenBidRequest(
             UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
             List<Imp> imps) {
@@ -533,6 +639,37 @@ private static String givenUserAgent() {
                 + "AppleWebKit/537.13 (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2";
     }
 
+    private static HookExecutionContext givenHookExecutionContextWithAnalyticsTag() {
+        final ObjectNode analyticsResultNode = mapper.valueToTree(
+                singletonMap(
+                        "adunitcodevalue",
+                        createAnalyticsResultNode()));
+
+        final ActivityImpl activity = ActivityImpl.of(
+                "greenbids-filter",
+                "success",
+                Collections.singletonList(
+                        ResultImpl.of("success", analyticsResultNode, null)));
+
+        final TagsImpl tags = TagsImpl.of(Collections.singletonList(activity));
+
+        final HookExecutionOutcome hookExecutionOutcome = HookExecutionOutcome.builder()
+                .hookId(HookId.of("greenbids-real-time-data", null))
+                .analyticsTags(tags)
+                .build();
+
+        final GroupExecutionOutcome groupExecutionOutcome = GroupExecutionOutcome.of(
+                Collections.singletonList(hookExecutionOutcome));
+
+        final StageExecutionOutcome stageExecutionOutcome = StageExecutionOutcome.of(
+                "auction-request", Collections.singletonList(groupExecutionOutcome));
+
+        final EnumMap<Stage, List<StageExecutionOutcome>> stageOutcomes = new EnumMap<>(Stage.class);
+        stageOutcomes.put(Stage.processed_auction_request, Collections.singletonList(stageExecutionOutcome));
+
+        return HookExecutionContext.of(null, stageOutcomes);
+    }
+
     private static BidResponse givenBidResponse(UnaryOperator<BidResponse.BidResponseBuilder> bidResponseCustomizer) {
         return bidResponseCustomizer.apply(BidResponse.builder()
                 .id("response1")
@@ -546,10 +683,77 @@ private static BidResponse givenBidResponse(UnaryOperator<BidResponse.BidRespons
                 .cur("USD")).build();
     }
 
+    private static BidResponse givenBidResponseWithAnalyticsTag(
+            UnaryOperator<BidResponse.BidResponseBuilder> bidResponseCustomizer) {
+        final ObjectNode analyticsResultNode = mapper.valueToTree(
+                singletonMap(
+                        "adunitcodevalue",
+                        createAnalyticsResultNode()));
+
+        final ExtModulesTraceAnalyticsTags analyticsTags = ExtModulesTraceAnalyticsTags.of(
+                Collections.singletonList(
+                        ExtModulesTraceAnalyticsActivity.of(
+                                null, null, Collections.singletonList(
+                                        ExtModulesTraceAnalyticsResult.of(
+                                                null, analyticsResultNode, null)))));
+
+        final ExtModulesTraceInvocationResult invocationResult = ExtModulesTraceInvocationResult.builder()
+                .hookId(HookId.of("greenbids-real-time-data", null))
+                .analyticsTags(analyticsTags)
+                .build();
+
+        final ExtModulesTraceStageOutcome outcome = ExtModulesTraceStageOutcome.of(
+                "auction-request", null,
+                Collections.singletonList(ExtModulesTraceGroup.of(
+                        null, Collections.singletonList(invocationResult))));
+
+        final ExtModulesTraceStage stage = ExtModulesTraceStage.of(
+                Stage.processed_auction_request, null,
+                Collections.singletonList(outcome));
+
+        final ExtModulesTrace modulesTrace = ExtModulesTrace.of(null, Collections.singletonList(stage));
+
+        final ExtModules modules = ExtModules.of(null, null, modulesTrace);
+
+        final ExtBidResponsePrebid prebid = ExtBidResponsePrebid.builder().modules(modules).build();
+
+        final ExtBidResponse extBidResponse = ExtBidResponse.builder().prebid(prebid).build();
+
+        return bidResponseCustomizer.apply(BidResponse.builder()
+                .id("response2")
+                .seatbid(List.of(
+                        givenSeatBid(
+                                seatBid -> seatBid.seat("seat1"),
+                                bid -> bid.id("bid1").price(BigDecimal.valueOf(1.5))),
+                        givenSeatBid(
+                                seatBid -> seatBid.seat("seat2"),
+                                bid -> bid.id("bid2").price(BigDecimal.valueOf(0.5)))))
+                .cur("USD")
+                .ext(extBidResponse)).build();
+    }
+
+    private static ObjectNode createAnalyticsResultNode() {
+        final ObjectNode keptInAuctionNode = new ObjectNode(JsonNodeFactory.instance);
+        keptInAuctionNode.put("seat1", true);
+        keptInAuctionNode.put("seat2", true);
+        keptInAuctionNode.put("seat3", true);
+
+        final ObjectNode explorationResultNode = new ObjectNode(JsonNodeFactory.instance);
+        explorationResultNode.put("fingerprint", "4f8d2e76-87fe-47c7-993f-d905b5fe2aa7");
+        explorationResultNode.set("keptInAuction", keptInAuctionNode);
+        explorationResultNode.put("isExploration", false);
+
+        final ObjectNode analyticsResultNode = new ObjectNode(JsonNodeFactory.instance);
+        analyticsResultNode.set("greenbids", explorationResultNode);
+        analyticsResultNode.put("tid", "c65c165d-f4ea-4301-bb91-982ce813dd3e");
+
+        return analyticsResultNode;
+    }
+
     private static SeatBid givenSeatBid(UnaryOperator<SeatBid.SeatBidBuilder> seatBidCostumizer,
                                         UnaryOperator<Bid.BidBuilder>... bidCustomizers) {
         return seatBidCostumizer.apply(SeatBid.builder()
-                        .bid(givenBids(bidCustomizers))).build();
+                .bid(givenBids(bidCustomizers))).build();
     }
 
     private static List<Bid> givenBids(UnaryOperator<Bid.BidBuilder>... bidCustomizers) {
@@ -569,7 +773,7 @@ private static BidRejectionTracker givenBidRejectionTracker() {
                 "seat3",
                 Set.of("adunitcodevalue"),
                 1.0);
-        bidRejectionTracker.reject("imp1", BidRejectionReason.NO_BID);
+        bidRejectionTracker.rejectImp("imp1", BidRejectionReason.NO_BID);
         return bidRejectionTracker;
     }
 
@@ -621,6 +825,16 @@ private static CommonMessage expectedCommonMessageForBanner() {
                         .bids(expectedGreenbidBids()));
     }
 
+    private static CommonMessage givenCommonMessageForBannerWithRtb2Imp() {
+        return expectedCommonMessage(
+                adUnit -> adUnit
+                        .code("adunitcodevalue")
+                        .unifiedCode(GreenbidsUnifiedCode.of("gpidvalue", "gpidSource"))
+                        .mediaTypes(MediaTypes.of(givenExtBanner(320, 50, null), null, null))
+                        .bids(expectedGreenbidBids())
+                        .ortb2ImpResult(givenOrtb2Imp()));
+    }
+
     private static CommonMessage expectedCommonMessageForVideo() {
         return expectedCommonMessage(
                 adUnit -> adUnit
@@ -718,4 +932,17 @@ private static Video givenVideo() {
                 .plcmt(1)
                 .build();
     }
+
+    private static Ortb2ImpResult givenOrtb2Imp() {
+        return Ortb2ImpResult.of(
+                Ortb2ImpExtResult.of(
+                        ExplorationResult.of(
+                                "4f8d2e76-87fe-47c7-993f-d905b5fe2aa7",
+                                Map.of("seat1", true, "seat2", true, "seat3", true),
+                                false
+                        ),
+                        "c65c165d-f4ea-4301-bb91-982ce813dd3e"
+                )
+        );
+    }
 }
diff --git a/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java b/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java
index 4fc744e4b43..b7418e7b777 100644
--- a/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java
+++ b/src/test/java/org/prebid/server/analytics/reporter/pubstack/PubstackEventHandlerTest.java
@@ -16,7 +16,7 @@
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.TimeoutContext;
 import org.prebid.server.cookie.UidsCookie;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.vertx.httpclient.HttpClient;
 import org.prebid.server.vertx.httpclient.model.HttpClientResponse;
 import org.springframework.test.util.ReflectionTestUtils;
diff --git a/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java b/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java
index d1de6726c42..3c05d00af37 100644
--- a/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java
+++ b/src/test/java/org/prebid/server/auction/BasicCategoryMappingServiceTest.java
@@ -18,8 +18,8 @@
 import org.prebid.server.bidder.model.BidderBid;
 import org.prebid.server.bidder.model.BidderSeatBid;
 import org.prebid.server.exception.InvalidRequestException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory;
 import org.prebid.server.proto.openrtb.ext.request.ExtImp;
 import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java
index 17de10e38b5..19181bf3503 100644
--- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java
+++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java
@@ -20,8 +20,6 @@
 import com.iab.openrtb.response.Response;
 import com.iab.openrtb.response.SeatBid;
 import io.vertx.core.Future;
-import lombok.Value;
-import lombok.experimental.Accessors;
 import org.apache.commons.collections4.MapUtils;
 import org.assertj.core.api.InstanceOfAssertFactories;
 import org.junit.jupiter.api.BeforeEach;
@@ -62,12 +60,12 @@
 import org.prebid.server.events.EventsContext;
 import org.prebid.server.events.EventsService;
 import org.prebid.server.exception.InvalidRequestException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.hooks.execution.HookStageExecutor;
 import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
 import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl;
-import org.prebid.server.hooks.v1.bidder.BidderResponsePayload;
+import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl;
 import org.prebid.server.identity.IdGenerator;
 import org.prebid.server.identity.IdGeneratorType;
 import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory;
@@ -113,6 +111,7 @@
 import org.prebid.server.settings.model.AccountAuctionEventConfig;
 import org.prebid.server.settings.model.AccountEventsConfig;
 import org.prebid.server.settings.model.VideoStoredDataResult;
+import org.prebid.server.spring.config.model.CacheDefaultTtlProperties;
 import org.prebid.server.vast.VastModifier;
 
 import java.math.BigDecimal;
@@ -156,6 +155,7 @@
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAdservertargetingRule.Source.xStatic;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.audio;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
@@ -190,6 +190,8 @@ public class BidResponseCreatorTest extends VertxTest {
     private ActivityInfrastructure activityInfrastructure;
     @Mock(strictness = LENIENT)
     private CacheTtl mediaTypeCacheTtl;
+    @Mock(strictness = LENIENT)
+    private CacheDefaultTtlProperties cacheDefaultProperties;
 
     @Spy
     private WinningBidComparatorFactory winningBidComparatorFactory;
@@ -209,6 +211,11 @@ public void setUp() {
         given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(null);
         given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(null);
 
+        given(cacheDefaultProperties.getBannerTtl()).willReturn(null);
+        given(cacheDefaultProperties.getVideoTtl()).willReturn(null);
+        given(cacheDefaultProperties.getAudioTtl()).willReturn(null);
+        given(cacheDefaultProperties.getNativeTtl()).willReturn(null);
+
         given(categoryMappingService.createCategoryMapping(any(), any(), any()))
                 .willAnswer(invocationOnMock -> Future.succeededFuture(
                         CategoryMappingResult.of(emptyMap(), emptyMap(), invocationOnMock.getArgument(0), null)));
@@ -1640,7 +1647,8 @@ public void shouldTruncateTargetingKeywordsByGlobalConfig() {
                 20,
                 clock,
                 jacksonMapper,
-                mediaTypeCacheTtl);
+                mediaTypeCacheTtl,
+                cacheDefaultProperties);
 
         // when
         final BidResponse bidResponse = target.create(auctionContext, CACHE_INFO, MULTI_BIDS).result();
@@ -3567,7 +3575,7 @@ public void shouldDropFledgeResponsesReferencingUnknownImps() {
     public void shouldPopulateExtPrebidSeatNonBidWhenReturnAllBidStatusFlagIsTrue() {
         // given
         final BidRejectionTracker bidRejectionTracker = mock(BidRejectionTracker.class);
-        given(bidRejectionTracker.getRejectionReasons()).willReturn(singletonMap("impId2", BidRejectionReason.NO_BID));
+        given(bidRejectionTracker.getRejectedImps()).willReturn(singletonMap("impId2", BidRejectionReason.NO_BID));
 
         final Bid bid = Bid.builder().id("bidId").price(BigDecimal.valueOf(3.67)).impid("impId").build();
         final List<BidderResponse> bidderResponses = singletonList(
@@ -3807,7 +3815,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() {
         final Imp imp = Imp.builder().id("impId").exp(20).build();
         final List<BidderResponse> bidderResponses = asList(BidderResponse.of(
                 "bidder1",
-                givenSeatBid(BidderBid.of(bid, banner, "USD")),
+                givenSeatBid(BidderBid.of(bid, video, "USD")),
                 100));
 
         final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
@@ -3815,17 +3823,18 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() {
                 .shouldCacheBids(true)
                 .shouldCacheVideoBids(true)
                 .cacheBidsTtl(30)
-                .cacheVideoBidsTtl(40)
+                .cacheVideoBidsTtl(31)
                 .build();
 
         final AuctionContext auctionContext = givenAuctionContext(
                 givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
-                                .events(mapper.createObjectNode())
-                                .build())), imp),
+                        .events(mapper.createObjectNode())
+                        .build())), imp),
                 builder -> builder.account(Account.builder()
                         .id("accountId")
                         .auction(AccountAuctionConfig.builder()
-                                .bannerCacheTtl(60)
+                                .bannerCacheTtl(40)
+                                .videoCacheTtl(41)
                                 .events(AccountEventsConfig.of(true))
                                 .build())
                         .build()))
@@ -3834,6 +3843,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() {
         // just a stub to get through method call chain
         givenCacheServiceResult(singletonList(CacheInfo.empty()));
         given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50);
+        given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51);
+        given(cacheDefaultProperties.getBannerTtl()).willReturn(60);
+        given(cacheDefaultProperties.getVideoTtl()).willReturn(61);
+        given(cacheDefaultProperties.getAudioTtl()).willReturn(62);
+        given(cacheDefaultProperties.getNativeTtl()).willReturn(63);
 
         // when
         final Future<BidResponse> response = target.create(auctionContext, cacheInfo, MULTI_BIDS);
@@ -3855,6 +3869,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() {
         final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
         assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
         assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(10);
+        assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(10);
         assertThat(contextArgumentCaptor.getValue())
                 .satisfies(context -> {
                     assertThat(context.isShouldCacheBids()).isTrue();
@@ -3869,7 +3884,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() {
         final Imp imp = Imp.builder().id("impId").exp(20).build();
         final List<BidderResponse> bidderResponses = asList(BidderResponse.of(
                 "bidder1",
-                givenSeatBid(BidderBid.of(bid, banner, "USD")),
+                givenSeatBid(BidderBid.of(bid, video, "USD")),
                 100));
 
         final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
@@ -3877,7 +3892,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() {
                 .shouldCacheBids(true)
                 .shouldCacheVideoBids(true)
                 .cacheBidsTtl(30)
-                .cacheVideoBidsTtl(40)
+                .cacheVideoBidsTtl(31)
                 .build();
 
         final AuctionContext auctionContext = givenAuctionContext(
@@ -3887,7 +3902,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() {
                 builder -> builder.account(Account.builder()
                         .id("accountId")
                         .auction(AccountAuctionConfig.builder()
-                                .bannerCacheTtl(60)
+                                .bannerCacheTtl(40)
+                                .videoCacheTtl(41)
                                 .events(AccountEventsConfig.of(true))
                                 .build())
                         .build()))
@@ -3896,6 +3912,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() {
         // just a stub to get through method call chain
         givenCacheServiceResult(singletonList(CacheInfo.empty()));
         given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50);
+        given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51);
+        given(cacheDefaultProperties.getBannerTtl()).willReturn(60);
+        given(cacheDefaultProperties.getVideoTtl()).willReturn(61);
+        given(cacheDefaultProperties.getAudioTtl()).willReturn(62);
+        given(cacheDefaultProperties.getNativeTtl()).willReturn(63);
 
         // when
         final Future<BidResponse> response = target.create(auctionContext, cacheInfo, MULTI_BIDS);
@@ -3917,6 +3938,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromImp() {
         final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
         assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
         assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(20);
+        assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(20);
         assertThat(contextArgumentCaptor.getValue())
                 .satisfies(context -> {
                     assertThat(context.isShouldCacheBids()).isTrue();
@@ -3931,7 +3953,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() {
         final Imp imp = Imp.builder().id("impId").exp(null).build();
         final List<BidderResponse> bidderResponses = asList(BidderResponse.of(
                 "bidder1",
-                givenSeatBid(BidderBid.of(bid, banner, "USD")),
+                givenSeatBid(BidderBid.of(bid, video, "USD")),
                 100));
 
         final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
@@ -3939,7 +3961,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() {
                 .shouldCacheBids(true)
                 .shouldCacheVideoBids(true)
                 .cacheBidsTtl(30)
-                .cacheVideoBidsTtl(40)
+                .cacheVideoBidsTtl(31)
                 .build();
 
         final AuctionContext auctionContext = givenAuctionContext(
@@ -3949,7 +3971,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() {
                 builder -> builder.account(Account.builder()
                         .id("accountId")
                         .auction(AccountAuctionConfig.builder()
-                                .bannerCacheTtl(60)
+                                .bannerCacheTtl(40)
+                                .videoCacheTtl(41)
                                 .events(AccountEventsConfig.of(true))
                                 .build())
                         .build()))
@@ -3958,6 +3981,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() {
         // just a stub to get through method call chain
         givenCacheServiceResult(singletonList(CacheInfo.empty()));
         given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50);
+        given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51);
+        given(cacheDefaultProperties.getBannerTtl()).willReturn(60);
+        given(cacheDefaultProperties.getVideoTtl()).willReturn(61);
+        given(cacheDefaultProperties.getAudioTtl()).willReturn(62);
+        given(cacheDefaultProperties.getNativeTtl()).willReturn(63);
 
         // when
         final Future<BidResponse> response = target.create(auctionContext, cacheInfo, MULTI_BIDS);
@@ -3968,7 +3996,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() {
 
         assertThat(response.succeeded()).isTrue();
         assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp)
-                .containsExactly(30);
+                .containsExactly(31);
 
         verify(coreCacheService).cacheBidsOpenrtb(
                 bidsArgumentCaptor.capture(),
@@ -3979,6 +4007,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() {
         final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
         assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
         assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(30);
+        assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(31);
         assertThat(contextArgumentCaptor.getValue())
                 .satisfies(context -> {
                     assertThat(context.isShouldCacheBids()).isTrue();
@@ -3987,7 +4016,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromRequest() {
     }
 
     @Test
-    public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBannerTtl() {
+    public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBannerTtlForBannerBid() {
         // given
         final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build();
         final Imp imp = Imp.builder().id("impId").exp(null).build();
@@ -4001,7 +4030,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne
                 .shouldCacheBids(true)
                 .shouldCacheVideoBids(true)
                 .cacheBidsTtl(null)
-                .cacheVideoBidsTtl(40)
+                .cacheVideoBidsTtl(31)
                 .build();
 
         final AuctionContext auctionContext = givenAuctionContext(
@@ -4011,7 +4040,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne
                 builder -> builder.account(Account.builder()
                         .id("accountId")
                         .auction(AccountAuctionConfig.builder()
-                                .bannerCacheTtl(60)
+                                .bannerCacheTtl(40)
+                                .videoCacheTtl(41)
                                 .events(AccountEventsConfig.of(true))
                                 .build())
                         .build()))
@@ -4020,6 +4050,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne
         // just a stub to get through method call chain
         givenCacheServiceResult(singletonList(CacheInfo.empty()));
         given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50);
+        given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51);
+        given(cacheDefaultProperties.getBannerTtl()).willReturn(60);
+        given(cacheDefaultProperties.getVideoTtl()).willReturn(61);
+        given(cacheDefaultProperties.getAudioTtl()).willReturn(62);
+        given(cacheDefaultProperties.getNativeTtl()).willReturn(63);
 
         // when
         final Future<BidResponse> response = target.create(auctionContext, cacheInfo, MULTI_BIDS);
@@ -4030,7 +4065,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne
 
         assertThat(response.succeeded()).isTrue();
         assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp)
-                .containsExactly(60);
+                .containsExactly(40);
 
         verify(coreCacheService).cacheBidsOpenrtb(
                 bidsArgumentCaptor.capture(),
@@ -4040,7 +4075,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne
 
         final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
         assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
-        assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(60);
+        assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(40);
+        assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull();
         assertThat(contextArgumentCaptor.getValue())
                 .satisfies(context -> {
                     assertThat(context.isShouldCacheBids()).isTrue();
@@ -4049,7 +4085,76 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountBanne
     }
 
     @Test
-    public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl() {
+    public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromAccountVideoTtlForVideoBid() {
+        // given
+        final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build();
+        final Imp imp = Imp.builder().id("impId").exp(null).build();
+        final List<BidderResponse> bidderResponses = asList(BidderResponse.of(
+                "bidder1",
+                givenSeatBid(BidderBid.of(bid, video, "USD")),
+                100));
+
+        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
+                .doCaching(true)
+                .shouldCacheBids(true)
+                .shouldCacheVideoBids(true)
+                .cacheBidsTtl(null)
+                .cacheVideoBidsTtl(null)
+                .build();
+
+        final AuctionContext auctionContext = givenAuctionContext(
+                givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .events(mapper.createObjectNode())
+                        .build())), imp),
+                builder -> builder.account(Account.builder()
+                        .id("accountId")
+                        .auction(AccountAuctionConfig.builder()
+                                .bannerCacheTtl(40)
+                                .videoCacheTtl(41)
+                                .events(AccountEventsConfig.of(true))
+                                .build())
+                        .build()))
+                .with(toAuctionParticipant(bidderResponses));
+
+        // just a stub to get through method call chain
+        givenCacheServiceResult(singletonList(CacheInfo.empty()));
+        given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50);
+        given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51);
+        given(cacheDefaultProperties.getBannerTtl()).willReturn(60);
+        given(cacheDefaultProperties.getVideoTtl()).willReturn(61);
+        given(cacheDefaultProperties.getAudioTtl()).willReturn(62);
+        given(cacheDefaultProperties.getNativeTtl()).willReturn(63);
+
+        // when
+        final Future<BidResponse> response = target.create(auctionContext, cacheInfo, MULTI_BIDS);
+
+        // then
+        final ArgumentCaptor<CacheContext> contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class);
+        final ArgumentCaptor<List<BidInfo>> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
+
+        assertThat(response.succeeded()).isTrue();
+        assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp)
+                .containsExactly(41);
+
+        verify(coreCacheService).cacheBidsOpenrtb(
+                bidsArgumentCaptor.capture(),
+                same(auctionContext),
+                contextArgumentCaptor.capture(),
+                any());
+
+        final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
+        assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
+        assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(41);
+        assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(41);
+        assertThat(contextArgumentCaptor.getValue())
+                .satisfies(context -> {
+                    assertThat(context.isShouldCacheBids()).isTrue();
+                    assertThat(context.isShouldCacheVideoBids()).isTrue();
+                });
+    }
+
+    @Test
+    public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtlForBannerBid() {
         // given
         final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build();
         final Imp imp = Imp.builder().id("impId").exp(null).build();
@@ -4063,7 +4168,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl
                 .shouldCacheBids(true)
                 .shouldCacheVideoBids(true)
                 .cacheBidsTtl(null)
-                .cacheVideoBidsTtl(40)
+                .cacheVideoBidsTtl(null)
                 .build();
 
         final AuctionContext auctionContext = givenAuctionContext(
@@ -4074,6 +4179,7 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl
                         .id("accountId")
                         .auction(AccountAuctionConfig.builder()
                                 .bannerCacheTtl(null)
+                                .videoCacheTtl(41)
                                 .events(AccountEventsConfig.of(true))
                                 .build())
                         .build()))
@@ -4082,6 +4188,11 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl
         // just a stub to get through method call chain
         givenCacheServiceResult(singletonList(CacheInfo.empty()));
         given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50);
+        given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51);
+        given(cacheDefaultProperties.getBannerTtl()).willReturn(60);
+        given(cacheDefaultProperties.getVideoTtl()).willReturn(61);
+        given(cacheDefaultProperties.getAudioTtl()).willReturn(62);
+        given(cacheDefaultProperties.getNativeTtl()).willReturn(63);
 
         // when
         final Future<BidResponse> response = target.create(auctionContext, cacheInfo, MULTI_BIDS);
@@ -4103,6 +4214,352 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtl
         final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
         assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
         assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(50);
+        assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull();
+        assertThat(contextArgumentCaptor.getValue())
+                .satisfies(context -> {
+                    assertThat(context.isShouldCacheBids()).isTrue();
+                    assertThat(context.isShouldCacheVideoBids()).isTrue();
+                });
+    }
+
+    @Test
+    public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromMediaTypeTtlForVideoBid() {
+        // given
+        final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build();
+        final Imp imp = Imp.builder().id("impId").exp(null).build();
+        final List<BidderResponse> bidderResponses = asList(BidderResponse.of(
+                "bidder1",
+                givenSeatBid(BidderBid.of(bid, video, "USD")),
+                100));
+
+        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
+                .doCaching(true)
+                .shouldCacheBids(true)
+                .shouldCacheVideoBids(true)
+                .cacheBidsTtl(null)
+                .cacheVideoBidsTtl(null)
+                .build();
+
+        final AuctionContext auctionContext = givenAuctionContext(
+                givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .events(mapper.createObjectNode())
+                        .build())), imp),
+                builder -> builder.account(Account.builder()
+                        .id("accountId")
+                        .auction(AccountAuctionConfig.builder()
+                                .bannerCacheTtl(40)
+                                .videoCacheTtl(null)
+                                .events(AccountEventsConfig.of(true))
+                                .build())
+                        .build()))
+                .with(toAuctionParticipant(bidderResponses));
+
+        // just a stub to get through method call chain
+        givenCacheServiceResult(singletonList(CacheInfo.empty()));
+        given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50);
+        given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51);
+        given(cacheDefaultProperties.getBannerTtl()).willReturn(60);
+        given(cacheDefaultProperties.getVideoTtl()).willReturn(61);
+        given(cacheDefaultProperties.getAudioTtl()).willReturn(62);
+        given(cacheDefaultProperties.getNativeTtl()).willReturn(63);
+
+        // when
+        final Future<BidResponse> response = target.create(auctionContext, cacheInfo, MULTI_BIDS);
+
+        // then
+        final ArgumentCaptor<CacheContext> contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class);
+        final ArgumentCaptor<List<BidInfo>> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
+
+        assertThat(response.succeeded()).isTrue();
+        assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp)
+                .containsExactly(51);
+
+        verify(coreCacheService).cacheBidsOpenrtb(
+                bidsArgumentCaptor.capture(),
+                same(auctionContext),
+                contextArgumentCaptor.capture(),
+                any());
+
+        final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
+        assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
+        assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(51);
+        assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(51);
+        assertThat(contextArgumentCaptor.getValue())
+                .satisfies(context -> {
+                    assertThat(context.isShouldCacheBids()).isTrue();
+                    assertThat(context.isShouldCacheVideoBids()).isTrue();
+                });
+    }
+
+    @Test
+    public void createShouldSendCacheRequestWithExpectedTtlAndSetDefaultTtlForBannerBid() {
+        // given
+        final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build();
+        final Imp imp = Imp.builder().id("impId").exp(null).build();
+        final List<BidderResponse> bidderResponses = asList(BidderResponse.of(
+                "bidder1",
+                givenSeatBid(BidderBid.of(bid, banner, "USD")),
+                100));
+
+        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
+                .doCaching(true)
+                .shouldCacheBids(true)
+                .shouldCacheVideoBids(true)
+                .cacheBidsTtl(null)
+                .cacheVideoBidsTtl(null)
+                .build();
+
+        final AuctionContext auctionContext = givenAuctionContext(
+                givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .events(mapper.createObjectNode())
+                        .build())), imp),
+                builder -> builder.account(Account.builder()
+                        .id("accountId")
+                        .auction(AccountAuctionConfig.builder()
+                                .bannerCacheTtl(null)
+                                .videoCacheTtl(41)
+                                .events(AccountEventsConfig.of(true))
+                                .build())
+                        .build()))
+                .with(toAuctionParticipant(bidderResponses));
+
+        // just a stub to get through method call chain
+        givenCacheServiceResult(singletonList(CacheInfo.empty()));
+        given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(null);
+        given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51);
+        given(cacheDefaultProperties.getBannerTtl()).willReturn(60);
+        given(cacheDefaultProperties.getVideoTtl()).willReturn(61);
+        given(cacheDefaultProperties.getAudioTtl()).willReturn(62);
+        given(cacheDefaultProperties.getNativeTtl()).willReturn(63);
+
+        // when
+        final Future<BidResponse> response = target.create(auctionContext, cacheInfo, MULTI_BIDS);
+
+        // then
+        final ArgumentCaptor<CacheContext> contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class);
+        final ArgumentCaptor<List<BidInfo>> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
+
+        assertThat(response.succeeded()).isTrue();
+        assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp)
+                .containsExactly(60);
+
+        verify(coreCacheService).cacheBidsOpenrtb(
+                bidsArgumentCaptor.capture(),
+                same(auctionContext),
+                contextArgumentCaptor.capture(),
+                any());
+
+        final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
+        assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
+        assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(60);
+        assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull();
+        assertThat(contextArgumentCaptor.getValue())
+                .satisfies(context -> {
+                    assertThat(context.isShouldCacheBids()).isTrue();
+                    assertThat(context.isShouldCacheVideoBids()).isTrue();
+                });
+    }
+
+    @Test
+    public void createShouldSendCacheRequestWithExpectedTtlAndSetDefaultTtlForVideoBid() {
+        // given
+        final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build();
+        final Imp imp = Imp.builder().id("impId").exp(null).build();
+        final List<BidderResponse> bidderResponses = asList(BidderResponse.of(
+                "bidder1",
+                givenSeatBid(BidderBid.of(bid, video, "USD")),
+                100));
+
+        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
+                .doCaching(true)
+                .shouldCacheBids(true)
+                .shouldCacheVideoBids(true)
+                .cacheBidsTtl(null)
+                .cacheVideoBidsTtl(null)
+                .build();
+
+        final AuctionContext auctionContext = givenAuctionContext(
+                givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .events(mapper.createObjectNode())
+                        .build())), imp),
+                builder -> builder.account(Account.builder()
+                        .id("accountId")
+                        .auction(AccountAuctionConfig.builder()
+                                .bannerCacheTtl(40)
+                                .videoCacheTtl(null)
+                                .events(AccountEventsConfig.of(true))
+                                .build())
+                        .build()))
+                .with(toAuctionParticipant(bidderResponses));
+
+        // just a stub to get through method call chain
+        givenCacheServiceResult(singletonList(CacheInfo.empty()));
+        given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50);
+        given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(null);
+        given(cacheDefaultProperties.getBannerTtl()).willReturn(60);
+        given(cacheDefaultProperties.getVideoTtl()).willReturn(61);
+        given(cacheDefaultProperties.getAudioTtl()).willReturn(62);
+        given(cacheDefaultProperties.getNativeTtl()).willReturn(63);
+
+        // when
+        final Future<BidResponse> response = target.create(auctionContext, cacheInfo, MULTI_BIDS);
+
+        // then
+        final ArgumentCaptor<CacheContext> contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class);
+        final ArgumentCaptor<List<BidInfo>> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
+
+        assertThat(response.succeeded()).isTrue();
+        assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp)
+                .containsExactly(61);
+
+        verify(coreCacheService).cacheBidsOpenrtb(
+                bidsArgumentCaptor.capture(),
+                same(auctionContext),
+                contextArgumentCaptor.capture(),
+                any());
+
+        final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
+        assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
+        assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(61);
+        assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnly(61);
+        assertThat(contextArgumentCaptor.getValue())
+                .satisfies(context -> {
+                    assertThat(context.isShouldCacheBids()).isTrue();
+                    assertThat(context.isShouldCacheVideoBids()).isTrue();
+                });
+    }
+
+    @Test
+    public void createShouldSendCacheRequestWithExpectedTtlAndSetDefaultTtlForAudioBid() {
+        // given
+        final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build();
+        final Imp imp = Imp.builder().id("impId").exp(null).build();
+        final List<BidderResponse> bidderResponses = asList(BidderResponse.of(
+                "bidder1",
+                givenSeatBid(BidderBid.of(bid, audio, "USD")),
+                100));
+
+        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
+                .doCaching(true)
+                .shouldCacheBids(true)
+                .shouldCacheVideoBids(true)
+                .cacheBidsTtl(null)
+                .cacheVideoBidsTtl(null)
+                .build();
+
+        final AuctionContext auctionContext = givenAuctionContext(
+                givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .events(mapper.createObjectNode())
+                        .build())), imp),
+                builder -> builder.account(Account.builder()
+                        .id("accountId")
+                        .auction(AccountAuctionConfig.builder()
+                                .bannerCacheTtl(40)
+                                .videoCacheTtl(41)
+                                .events(AccountEventsConfig.of(true))
+                                .build())
+                        .build()))
+                .with(toAuctionParticipant(bidderResponses));
+
+        // just a stub to get through method call chain
+        givenCacheServiceResult(singletonList(CacheInfo.empty()));
+        given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50);
+        given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51);
+        given(cacheDefaultProperties.getBannerTtl()).willReturn(60);
+        given(cacheDefaultProperties.getVideoTtl()).willReturn(61);
+        given(cacheDefaultProperties.getAudioTtl()).willReturn(62);
+        given(cacheDefaultProperties.getNativeTtl()).willReturn(63);
+
+        // when
+        final Future<BidResponse> response = target.create(auctionContext, cacheInfo, MULTI_BIDS);
+
+        // then
+        final ArgumentCaptor<CacheContext> contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class);
+        final ArgumentCaptor<List<BidInfo>> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
+
+        assertThat(response.succeeded()).isTrue();
+        assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp)
+                .containsExactly(62);
+
+        verify(coreCacheService).cacheBidsOpenrtb(
+                bidsArgumentCaptor.capture(),
+                same(auctionContext),
+                contextArgumentCaptor.capture(),
+                any());
+
+        final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
+        assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
+        assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(62);
+        assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull();
+        assertThat(contextArgumentCaptor.getValue())
+                .satisfies(context -> {
+                    assertThat(context.isShouldCacheBids()).isTrue();
+                    assertThat(context.isShouldCacheVideoBids()).isTrue();
+                });
+    }
+
+    @Test
+    public void createShouldSendCacheRequestWithExpectedTtlAndSetDefaultTtlForNativeBid() {
+        // given
+        final Bid bid = Bid.builder().id("bidId").impid("impId").exp(null).price(BigDecimal.valueOf(5.67)).build();
+        final Imp imp = Imp.builder().id("impId").exp(null).build();
+        final List<BidderResponse> bidderResponses = asList(BidderResponse.of(
+                "bidder1",
+                givenSeatBid(BidderBid.of(bid, xNative, "USD")),
+                100));
+
+        final BidRequestCacheInfo cacheInfo = BidRequestCacheInfo.builder()
+                .doCaching(true)
+                .shouldCacheBids(true)
+                .shouldCacheVideoBids(true)
+                .cacheBidsTtl(null)
+                .cacheVideoBidsTtl(null)
+                .build();
+
+        final AuctionContext auctionContext = givenAuctionContext(
+                givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .events(mapper.createObjectNode())
+                        .build())), imp),
+                builder -> builder.account(Account.builder()
+                        .id("accountId")
+                        .auction(AccountAuctionConfig.builder()
+                                .bannerCacheTtl(40)
+                                .videoCacheTtl(41)
+                                .events(AccountEventsConfig.of(true))
+                                .build())
+                        .build()))
+                .with(toAuctionParticipant(bidderResponses));
+
+        // just a stub to get through method call chain
+        givenCacheServiceResult(singletonList(CacheInfo.empty()));
+        given(mediaTypeCacheTtl.getBannerCacheTtl()).willReturn(50);
+        given(mediaTypeCacheTtl.getVideoCacheTtl()).willReturn(51);
+        given(cacheDefaultProperties.getBannerTtl()).willReturn(60);
+        given(cacheDefaultProperties.getVideoTtl()).willReturn(61);
+        given(cacheDefaultProperties.getAudioTtl()).willReturn(62);
+        given(cacheDefaultProperties.getNativeTtl()).willReturn(63);
+
+        // when
+        final Future<BidResponse> response = target.create(auctionContext, cacheInfo, MULTI_BIDS);
+
+        // then
+        final ArgumentCaptor<CacheContext> contextArgumentCaptor = ArgumentCaptor.forClass(CacheContext.class);
+        final ArgumentCaptor<List<BidInfo>> bidsArgumentCaptor = ArgumentCaptor.forClass(List.class);
+
+        assertThat(response.succeeded()).isTrue();
+        assertThat(response.result().getSeatbid()).flatExtracting(SeatBid::getBid).extracting(Bid::getExp)
+                .containsExactly(63);
+
+        verify(coreCacheService).cacheBidsOpenrtb(
+                bidsArgumentCaptor.capture(),
+                same(auctionContext),
+                contextArgumentCaptor.capture(),
+                any());
+
+        final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
+        assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
+        assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsOnly(63);
+        assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsNull();
         assertThat(contextArgumentCaptor.getValue())
                 .satisfies(context -> {
                     assertThat(context.isShouldCacheBids()).isTrue();
@@ -4277,7 +4734,7 @@ public void createShouldSendCacheRequestWithVideoBidWithTtlMaxOfTtlAndVideoTtl()
         final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
         assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
         assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsExactly(30);
-        assertThat(capturedBidInfo).extracting(BidInfo::getVideoTtl).containsExactly(40);
+        assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsExactly(40);
         assertThat(contextArgumentCaptor.getValue())
                 .satisfies(context -> {
                     assertThat(context.isShouldCacheBids()).isTrue();
@@ -4336,7 +4793,7 @@ public void createShouldSendCacheRequestWithBannerBidWithTtlMaxOfTtlAndVideoTtl(
         final List<BidInfo> capturedBidInfo = bidsArgumentCaptor.getValue();
         assertThat(capturedBidInfo).extracting(bidInfo -> bidInfo.getBid().getId()).containsOnly("bidId");
         assertThat(capturedBidInfo).extracting(BidInfo::getTtl).containsExactly(30);
-        assertThat(capturedBidInfo).extracting(BidInfo::getVideoTtl).containsOnlyNulls();
+        assertThat(capturedBidInfo).extracting(BidInfo::getVastTtl).containsOnlyNulls();
         assertThat(contextArgumentCaptor.getValue())
                 .satisfies(context -> {
                     assertThat(context.isShouldCacheBids()).isTrue();
@@ -4576,7 +5033,8 @@ private BidResponseCreator givenBidResponseCreator(int truncateAttrChars) {
                 truncateAttrChars,
                 clock,
                 jacksonMapper,
-                mediaTypeCacheTtl);
+                mediaTypeCacheTtl,
+                cacheDefaultProperties);
     }
 
     private static String toTargetingByKey(Bid bid, String targetingKey) {
@@ -4604,11 +5062,4 @@ private static ObjectNode extWithTargeting(String targetBidderCode, Map<String,
     private static <T> List<T> mutableList(T... values) {
         return Arrays.stream(values).collect(Collectors.toCollection(ArrayList::new));
     }
-
-    @Accessors(fluent = true)
-    @Value(staticConstructor = "of")
-    private static class BidderResponsePayloadImpl implements BidderResponsePayload {
-
-        List<BidderBid> bids;
-    }
 }
diff --git a/src/test/java/org/prebid/server/auction/BidderAliasesTest.java b/src/test/java/org/prebid/server/auction/BidderAliasesTest.java
index e75a3c9f0f4..30fc23a21fa 100644
--- a/src/test/java/org/prebid/server/auction/BidderAliasesTest.java
+++ b/src/test/java/org/prebid/server/auction/BidderAliasesTest.java
@@ -1,5 +1,6 @@
 package org.prebid.server.auction;
 
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
@@ -10,13 +11,22 @@
 
 import static java.util.Collections.singletonMap;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mock.Strictness.LENIENT;
 
 @ExtendWith(MockitoExtension.class)
 public class BidderAliasesTest {
 
-    @Mock
+    @Mock(strictness = LENIENT)
     private BidderCatalog bidderCatalog;
 
+    @BeforeEach
+    public void before() {
+        given(bidderCatalog.isValidName(any())).willReturn(false);
+        given(bidderCatalog.isActive(any())).willReturn(false);
+    }
+
     @Test
     public void isAliasDefinedShouldReturnFalseWhenNoAliasesInRequest() {
         // given
@@ -53,6 +63,16 @@ public void resolveBidderShouldReturnInputWhenNoAliasesInRequest() {
         assertThat(aliases.resolveBidder("alias")).isEqualTo("alias");
     }
 
+    @Test
+    public void resolveBidderShouldReturnInputWhenNoAliasesInRequestButAliasIsValidInBidderCatalog() {
+        // given
+        given(bidderCatalog.isValidName("alias")).willReturn(true);
+        final BidderAliases aliases = BidderAliases.of(null, null, bidderCatalog);
+
+        // when and then
+        assertThat(aliases.resolveBidder("alias")).isEqualTo("alias");
+    }
+
     @Test
     public void resolveBidderShouldReturnInputWhenAliasIsNotDefinedInRequest() {
         // given
@@ -62,6 +82,16 @@ public void resolveBidderShouldReturnInputWhenAliasIsNotDefinedInRequest() {
         assertThat(aliases.resolveBidder("alias")).isEqualTo("alias");
     }
 
+    @Test
+    public void resolveBidderShouldReturnInputWhenAliasIsNotDefinedInRequestButAliasIsValidInBidderCatalog() {
+        // given
+        given(bidderCatalog.isValidName("alias")).willReturn(true);
+        final BidderAliases aliases = BidderAliases.of(singletonMap("anotherAlias", "bidder"), null, bidderCatalog);
+
+        // when and then
+        assertThat(aliases.resolveBidder("alias")).isEqualTo("alias");
+    }
+
     @Test
     public void resolveBidderShouldDetectAliasInRequest() {
         // given
@@ -71,6 +101,16 @@ public void resolveBidderShouldDetectAliasInRequest() {
         assertThat(aliases.resolveBidder("alias")).isEqualTo("bidder");
     }
 
+    @Test
+    public void resolveBidderShouldDetectInBidderCatalogWhenItIsValid() {
+        // given
+        given(bidderCatalog.isValidName("alias")).willReturn(true);
+        final BidderAliases aliases = BidderAliases.of(singletonMap("alias", "bidder"), null, bidderCatalog);
+
+        // when and then
+        assertThat(aliases.resolveBidder("alias")).isEqualTo("alias");
+    }
+
     @Test
     public void isSameShouldReturnTrueIfBiddersSameConsideringAliases() {
         // given
@@ -110,6 +150,17 @@ public void resolveAliasVendorIdShouldReturnNullWhenNoVendorIdsInRequest() {
         assertThat(aliases.resolveAliasVendorId("alias")).isNull();
     }
 
+    @Test
+    public void resolveAliasVendorIdShouldReturnVendorIdFromBidderCatalogWhenNoVendorIdsInRequest() {
+        // given
+        given(bidderCatalog.isActive("alias")).willReturn(true);
+        given(bidderCatalog.vendorIdByName("alias")).willReturn(3);
+        final BidderAliases aliases = BidderAliases.of(null, null, bidderCatalog);
+
+        // when and then
+        assertThat(aliases.resolveAliasVendorId("alias")).isEqualTo(3);
+    }
+
     @Test
     public void resolveAliasVendorIdShouldReturnNullWhenVendorIdIsNotDefinedInRequest() {
         // given
@@ -120,11 +171,24 @@ public void resolveAliasVendorIdShouldReturnNullWhenVendorIdIsNotDefinedInReques
     }
 
     @Test
-    public void resolveAliasVendorIdShouldDetectVendorIdInRequest() {
+    public void resolveAliasVendorIdShouldReturnVendorIdFromBidderCatalogWhenVendorIdIsNotDefinedInRequest() {
+        // given
+        given(bidderCatalog.isActive("alias")).willReturn(true);
+        given(bidderCatalog.vendorIdByName("alias")).willReturn(3);
+        final BidderAliases aliases = BidderAliases.of(null, singletonMap("anotherAlias", 2), bidderCatalog);
+
+        // when and then
+        assertThat(aliases.resolveAliasVendorId("alias")).isEqualTo(3);
+    }
+
+    @Test
+    public void resolveAliasVendorIdShouldReturnVendorIdFromBidderCatalogWhenVendorIdIsInRequest() {
         // given
+        given(bidderCatalog.isActive("alias")).willReturn(true);
+        given(bidderCatalog.vendorIdByName("alias")).willReturn(3);
         final BidderAliases aliases = BidderAliases.of(null, singletonMap("alias", 2), bidderCatalog);
 
         // when and then
-        assertThat(aliases.resolveAliasVendorId("alias")).isEqualTo(2);
+        assertThat(aliases.resolveAliasVendorId("alias")).isEqualTo(3);
     }
 }
diff --git a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java
index 8b7dd0589b0..9bfbc9cb143 100644
--- a/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java
+++ b/src/test/java/org/prebid/server/auction/BidsAdjusterTest.java
@@ -1,9 +1,7 @@
 package org.prebid.server.auction;
 
-import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.iab.openrtb.request.BidRequest;
 import com.iab.openrtb.request.Imp;
-import com.iab.openrtb.request.Video;
 import com.iab.openrtb.response.Bid;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -11,35 +9,26 @@
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.VertxTest;
-import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.AuctionParticipation;
 import org.prebid.server.auction.model.BidRejectionTracker;
 import org.prebid.server.auction.model.BidderRequest;
 import org.prebid.server.auction.model.BidderResponse;
+import org.prebid.server.bidadjustments.BidAdjustmentsProcessor;
 import org.prebid.server.bidder.model.BidderBid;
 import org.prebid.server.bidder.model.BidderError;
 import org.prebid.server.bidder.model.BidderSeatBid;
-import org.prebid.server.currency.CurrencyConversionService;
-import org.prebid.server.exception.PreBidException;
 import org.prebid.server.floors.PriceFloorEnforcer;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
-import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors;
-import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
-import org.prebid.server.proto.openrtb.ext.request.ImpMediaType;
-import org.prebid.server.proto.openrtb.ext.response.BidType;
 import org.prebid.server.validation.ResponseBidValidator;
 import org.prebid.server.validation.model.ValidationResult;
 
 import java.math.BigDecimal;
-import java.util.EnumMap;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
-import java.util.function.Function;
 import java.util.function.UnaryOperator;
 
 import static java.util.Collections.emptyMap;
@@ -48,16 +37,10 @@
 import static java.util.function.UnaryOperator.identity;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mock.Strictness.LENIENT;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
-import static org.prebid.server.proto.openrtb.ext.response.BidType.audio;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
-import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
-import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
 
 @ExtendWith(MockitoExtension.class)
 public class BidsAdjusterTest extends VertxTest {
@@ -65,9 +48,6 @@ public class BidsAdjusterTest extends VertxTest {
     @Mock(strictness = LENIENT)
     private ResponseBidValidator responseBidValidator;
 
-    @Mock(strictness = LENIENT)
-    private CurrencyConversionService currencyService;
-
     @Mock(strictness = LENIENT)
     private PriceFloorEnforcer priceFloorEnforcer;
 
@@ -75,7 +55,7 @@ public class BidsAdjusterTest extends VertxTest {
     private DsaEnforcer dsaEnforcer;
 
     @Mock(strictness = LENIENT)
-    private BidAdjustmentFactorResolver bidAdjustmentFactorResolver;
+    private BidAdjustmentsProcessor bidAdjustmentsProcessor;
 
     private BidsAdjuster target;
 
@@ -83,696 +63,51 @@ public class BidsAdjusterTest extends VertxTest {
     public void setUp() {
         given(responseBidValidator.validate(any(), any(), any(), any())).willReturn(ValidationResult.success());
 
-        given(currencyService.convertCurrency(any(), any(), any(), any()))
-                .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
-
         given(priceFloorEnforcer.enforce(any(), any(), any(), any())).willAnswer(inv -> inv.getArgument(1));
         given(dsaEnforcer.enforce(any(), any(), any())).willAnswer(inv -> inv.getArgument(1));
-        given(bidAdjustmentFactorResolver.resolve(any(ImpMediaType.class), any(), any())).willReturn(null);
-
-        givenTarget();
-    }
-
-    private void givenTarget() {
-        target = new BidsAdjuster(
-                responseBidValidator,
-                currencyService,
-                bidAdjustmentFactorResolver,
-                priceFloorEnforcer,
-                dsaEnforcer,
-                jacksonMapper);
-    }
-
-    @Test
-    public void shouldReturnBidsWithUpdatedPriceCurrencyConversion() {
-        // given
-        final BidderResponse bidderResponse = givenBidderResponse(
-                Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build());
-        final BidRequest bidRequest = givenBidRequest(
-                singletonList(givenImp(singletonMap("bidder", 2), identity())), identity());
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        final BigDecimal updatedPrice = BigDecimal.valueOf(5.0);
-        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        assertThat(result)
-                .extracting(AuctionParticipation::getBidderResponse)
-                .extracting(BidderResponse::getSeatBid)
-                .flatExtracting(BidderSeatBid::getBids)
-                .extracting(BidderBid::getBid)
-                .extracting(Bid::getPrice)
-                .containsExactly(updatedPrice);
-    }
-
-    @Test
-    public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() {
-        // given
-        final BidderResponse bidderResponse = givenBidderResponse(
-                Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build());
-        final BidRequest bidRequest = givenBidRequest(
-                singletonList(givenImp(singletonMap("bidder", 2), identity())), identity());
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        given(currencyService.convertCurrency(any(), any(), any(), any()))
-                .willAnswer(invocation -> invocation.getArgument(0));
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        assertThat(result)
-                .extracting(AuctionParticipation::getBidderResponse)
-                .extracting(BidderResponse::getSeatBid)
-                .flatExtracting(BidderSeatBid::getBids)
-                .extracting(BidderBid::getBid)
-                .extracting(Bid::getPrice)
-                .containsExactly(BigDecimal.valueOf(2.0));
-    }
-
-    @Test
-    public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() {
-        // given
-        final BidderResponse bidderResponse = givenBidderResponse(
-                Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build());
-        final BidRequest bidRequest = givenBidRequest(
-                singletonList(givenImp(singletonMap("bidder", 2), identity())), identity());
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        given(currencyService.convertCurrency(any(), any(), any(), any()))
-                .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD"));
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-
-        final BidderError expectedError =
-                BidderError.generic("Unable to convert bid currency CUR to desired ad server currency USD");
-        final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid();
-        assertThat(firstSeatBid.getBids()).isEmpty();
-        assertThat(firstSeatBid.getErrors()).containsOnly(expectedError);
-    }
-
-    @Test
-    public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactor() {
-        // given
-        final BidderResponse bidderResponse = givenBidderResponse(
-                Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build());
-
-        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build();
-        givenAdjustments.addFactor("bidder", BigDecimal.TEN);
-
-        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
-                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
-                        .aliases(emptyMap())
-                        .bidadjustmentfactors(givenAdjustments)
-                        .auctiontimestamp(1000L)
-                        .build())));
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder"))
-                .willReturn(BigDecimal.TEN);
-        given(currencyService.convertCurrency(any(), any(), any(), any()))
-                .willReturn(BigDecimal.TEN);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
+        given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any(), any()))
+                .willAnswer(inv -> inv.getArgument(0));
 
-        // then
-        final BigDecimal updatedPrice = BigDecimal.valueOf(100);
-        final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid();
-        assertThat(firstSeatBid.getBids())
-                .extracting(BidderBid::getBid)
-                .flatExtracting(Bid::getPrice)
-                .containsOnly(updatedPrice);
-        assertThat(firstSeatBid.getErrors()).isEmpty();
+        target = new BidsAdjuster(responseBidValidator, priceFloorEnforcer, bidAdjustmentsProcessor, dsaEnforcer);
     }
 
     @Test
-    public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForSecondBid() {
+    public void shouldReturnBidsAdjustedByBidAdjustmentsProcessor() {
         // given
-        final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0);
-        final BigDecimal secondBidderPrice = BigDecimal.valueOf(3.0);
-        final BidderResponse bidderResponse = BidderResponse.of(
-                "bidder",
-                BidderSeatBid.builder()
-                        .bids(List.of(
-                                givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "CUR1"),
-                                givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR2")
-                        ))
-                        .build(),
-                1);
-
-        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
-                identity());
-
-        final BigDecimal updatedPrice = BigDecimal.valueOf(10.0);
-        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice)
-                .willThrow(
-                        new PreBidException("Unable to convert bid currency CUR2 to desired ad server currency USD"));
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("CUR1"), any());
-        verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR2"), any());
-
-        assertThat(result).hasSize(1);
-
-        final ObjectNode expectedBidExt = mapper.createObjectNode();
-        expectedBidExt.put("origbidcpm", new BigDecimal("2.0"));
-        expectedBidExt.put("origbidcur", "CUR1");
-        final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build();
-        final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "CUR1");
-        final BidderError expectedError =
-                BidderError.generic("Unable to convert bid currency CUR2 to desired ad server currency USD");
-
-        final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid();
-        assertThat(firstSeatBid.getBids()).containsOnly(expectedBidderBid);
-        assertThat(firstSeatBid.getErrors()).containsOnly(expectedError);
-    }
-
-    @Test
-    public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupportedCurrency() {
-        // given
-        final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0);
-        final BigDecimal secondBidderPrice = BigDecimal.valueOf(10.0);
-        final BidderResponse bidderResponse = BidderResponse.of(
-                "bidder",
-                BidderSeatBid.builder()
-                        .bids(List.of(
-                                givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "USD"),
-                                givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR")
-                        ))
-                        .build(),
-                1);
-
-        final BidRequest bidRequest = BidRequest.builder()
-                .cur(singletonList("BAD"))
-                .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3),
-                        identity()))).build();
-
-        final BigDecimal updatedPrice = BigDecimal.valueOf(20);
-        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice);
-        given(currencyService.convertCurrency(any(), any(), eq("CUR"), eq("BAD")))
-                .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency BAD"));
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("USD"), eq("BAD"));
-        verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR"), eq("BAD"));
-
-        assertThat(result).hasSize(1);
-
-        final ObjectNode expectedBidExt = mapper.createObjectNode();
-        expectedBidExt.put("origbidcpm", new BigDecimal("2.0"));
-        expectedBidExt.put("origbidcur", "USD");
-        final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build();
-        final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "USD");
-        assertThat(result)
-                .extracting(AuctionParticipation::getBidderResponse)
-                .extracting(BidderResponse::getSeatBid)
-                .flatExtracting(BidderSeatBid::getBids)
-                .containsOnly(expectedBidderBid);
-
-        final BidderError expectedError =
-                BidderError.generic("Unable to convert bid currency CUR to desired ad server currency BAD");
-        assertThat(result)
-                .extracting(AuctionParticipation::getBidderResponse)
-                .extracting(BidderResponse::getSeatBid)
-                .flatExtracting(BidderSeatBid::getErrors)
-                .containsOnly(expectedError);
-    }
-
-    @Test
-    public void shouldUpdateBidPriceWithCurrencyConversionAndAddWarningAboutMultipleCurrency() {
-        // given
-        final BigDecimal bidderPrice = BigDecimal.valueOf(2.0);
-        final BidderResponse bidderResponse = BidderResponse.of(
-                "bidder",
-                BidderSeatBid.builder()
-                        .bids(List.of(
-                                givenBidderBid(Bid.builder().impid("impId1").price(bidderPrice).build(), "USD")
-                        ))
-                        .build(),
-                1);
-
-        final BidRequest bidRequest = givenBidRequest(
-                singletonList(givenImp(singletonMap("bidder", 2), identity())),
-                builder -> builder.cur(List.of("CUR1", "CUR2", "CUR2")));
-
-        final BigDecimal updatedPrice = BigDecimal.valueOf(10.0);
-        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice);
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        verify(currencyService).convertCurrency(eq(bidderPrice), eq(bidRequest), eq("USD"), eq("CUR1"));
-
-        assertThat(result).hasSize(1);
-
-        final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid();
-        assertThat(firstSeatBid.getBids())
-                .extracting(BidderBid::getBid)
-                .flatExtracting(Bid::getPrice)
-                .containsOnly(updatedPrice);
-
-        final BidderError expectedWarning = BidderError.badInput(
-                "a single currency (CUR1) has been chosen for the request. "
-                        + "ORTB 2.6 requires that all responses are in the same currency.");
-        assertThat(firstSeatBid.getWarnings()).containsOnly(expectedWarning);
-    }
-
-    @Test
-    public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() {
-        // given
-        final BigDecimal bidder1Price = BigDecimal.valueOf(1.5);
-        final BigDecimal bidder2Price = BigDecimal.valueOf(2);
-        final BigDecimal bidder3Price = BigDecimal.valueOf(3);
+        final BidderBid bidToAdjust =
+                givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), "USD");
         final BidderResponse bidderResponse = BidderResponse.of(
                 "bidder",
-                BidderSeatBid.builder()
-                        .bids(List.of(
-                                givenBidderBid(Bid.builder().impid("impId1").price(bidder1Price).build(), "EUR"),
-                                givenBidderBid(Bid.builder().impid("impId2").price(bidder2Price).build(), "GBP"),
-                                givenBidderBid(Bid.builder().impid("impId3").price(bidder3Price).build(), "USD")
-                        ))
-                        .build(),
+                BidderSeatBid.builder().bids(List.of(bidToAdjust)).build(),
                 1);
 
         final BidRequest bidRequest = givenBidRequest(
-                singletonList(givenImp(Map.of("bidder1", 1), identity())),
-                builder -> builder.cur(singletonList("USD")));
-
-        final BigDecimal updatedPrice = BigDecimal.valueOf(10.0);
-        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice);
-        given(currencyService.convertCurrency(any(), any(), eq("USD"), any())).willReturn(bidder3Price);
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("EUR"), eq("USD"));
-        verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("GBP"), eq("USD"));
-        verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD"));
-        verifyNoMoreInteractions(currencyService);
-
-        assertThat(result)
-                .extracting(AuctionParticipation::getBidderResponse)
-                .extracting(BidderResponse::getSeatBid)
-                .flatExtracting(BidderSeatBid::getBids)
-                .extracting(BidderBid::getBid)
-                .extracting(Bid::getPrice)
-                .containsOnly(bidder3Price, updatedPrice, updatedPrice);
-    }
-
-    @Test
-    public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() {
-        // given
-        final BidderResponse bidderResponse = givenBidderResponse(
-                Bid.builder().impid("impId").price(BigDecimal.valueOf(2)).build());
-
-        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build();
-        givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468));
-        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder"))
-                .willReturn(BigDecimal.valueOf(2.468));
-
-        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
-                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
-                        .aliases(emptyMap())
-                        .bidadjustmentfactors(givenAdjustments)
-                        .auctiontimestamp(1000L)
-                        .build())));
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        assertThat(result)
-                .extracting(AuctionParticipation::getBidderResponse)
-                .extracting(BidderResponse::getSeatBid)
-                .flatExtracting(BidderSeatBid::getBids)
-                .extracting(BidderBid::getBid)
-                .extracting(Bid::getPrice)
-                .containsExactly(BigDecimal.valueOf(4.936));
-    }
-
-    @Test
-    public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementEqualsOne() {
-        // given
-        final BidderResponse bidderResponse = BidderResponse.of(
-                "bidder",
-                BidderSeatBid.builder()
-                        .bids(List.of(
-                                givenBidderBid(Bid.builder()
-                                                .impid("123")
-                                                .price(BigDecimal.valueOf(2)).build(),
-                                        "USD", video)
-                        ))
-                        .build(),
-                1);
-
-        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
-                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video,
-                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
-                .build();
-        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder"))
-                .willReturn(BigDecimal.valueOf(3.456));
-
-        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder ->
-                        impBuilder.id("123").video(Video.builder().placement(1).build()))),
-                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
-                        .aliases(emptyMap())
-                        .bidadjustmentfactors(givenAdjustments)
-                        .auctiontimestamp(1000L)
-                        .build())));
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        assertThat(result)
-                .extracting(AuctionParticipation::getBidderResponse)
-                .extracting(BidderResponse::getSeatBid)
-                .flatExtracting(BidderSeatBid::getBids)
-                .extracting(BidderBid::getBid)
-                .extracting(Bid::getPrice)
-                .containsExactly(BigDecimal.valueOf(6.912));
-    }
-
-    @Test
-    public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementIsMissing() {
-        // given
-        final BidderResponse bidderResponse = BidderResponse.of(
-                "bidder",
-                BidderSeatBid.builder()
-                        .bids(List.of(
-                                givenBidderBid(Bid.builder()
-                                                .impid("123")
-                                                .price(BigDecimal.valueOf(2)).build(),
-                                        "USD", video)
-                        ))
-                        .build(),
-                1);
-
-        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
-                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video,
-                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
-                .build();
-        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder"))
-                .willReturn(BigDecimal.valueOf(3.456));
-
-        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder ->
-                        impBuilder.id("123").video(Video.builder().build()))),
-                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
-                        .aliases(emptyMap())
-                        .bidadjustmentfactors(givenAdjustments)
-                        .auctiontimestamp(1000L)
-                        .build())));
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        assertThat(result)
-                .extracting(AuctionParticipation::getBidderResponse)
-                .extracting(BidderResponse::getSeatBid)
-                .flatExtracting(BidderSeatBid::getBids)
-                .extracting(BidderBid::getBid)
-                .extracting(Bid::getPrice)
-                .containsExactly(BigDecimal.valueOf(6.912));
-    }
-
-    @Test
-    public void shouldReturnBidAdjustmentMediaTypeNullIfImpIdNotEqualBidImpId() {
-        // given
-        final BidderResponse bidderResponse = BidderResponse.of(
-                "bidder",
-                BidderSeatBid.builder()
-                        .bids(List.of(
-                                givenBidderBid(Bid.builder()
-                                                .impid("123")
-                                                .price(BigDecimal.valueOf(2)).build(),
-                                        "USD", video)
-                        ))
-                        .build(),
-                1);
-
-        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
-                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video,
-                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
-                .build();
-
-        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder ->
-                        impBuilder.id("123").video(Video.builder().placement(10).build()))),
-                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
-                        .aliases(emptyMap())
-                        .bidadjustmentfactors(givenAdjustments)
-                        .auctiontimestamp(1000L)
-                        .build())));
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        assertThat(result)
-                .extracting(AuctionParticipation::getBidderResponse)
-                .extracting(BidderResponse::getSeatBid)
-                .flatExtracting(BidderSeatBid::getBids)
-                .extracting(BidderBid::getBid)
-                .extracting(Bid::getPrice)
-                .containsExactly(BigDecimal.valueOf(2));
-    }
-
-    @Test
-    public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpIdAndPopulatedPlacement() {
-        // given
-        final BidderResponse bidderResponse = BidderResponse.of(
-                "bidder",
-                BidderSeatBid.builder()
-                        .bids(List.of(
-                                givenBidderBid(Bid.builder()
-                                                .impid("123")
-                                                .price(BigDecimal.valueOf(2)).build(),
-                                        "USD", video)
-                        ))
-                        .build(),
-                1);
-
-        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
-                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video,
-                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
-                .build();
-
-        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder ->
-                        impBuilder.id("123").video(Video.builder().placement(10).build()))),
-                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
-                        .aliases(emptyMap())
-                        .bidadjustmentfactors(givenAdjustments)
-                        .auctiontimestamp(1000L)
-                        .build())));
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        assertThat(result)
-                .extracting(AuctionParticipation::getBidderResponse)
-                .extracting(BidderResponse::getSeatBid)
-                .flatExtracting(BidderSeatBid::getBids)
-                .extracting(BidderBid::getBid)
-                .extracting(Bid::getPrice)
-                .containsExactly(BigDecimal.valueOf(2));
-    }
-
-    @Test
-    public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() {
-        // given
-        final BidderResponse bidderResponse = BidderResponse.of(
-                "bidder",
-                BidderSeatBid.builder()
-                        .bids(List.of(
-                                givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build(), "USD", banner),
-                                givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", xNative),
-                                givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", audio)))
-                        .build(),
-                1);
-
-        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
-                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner,
-                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
-                .build();
-        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder"))
-                .willReturn(BigDecimal.valueOf(3.456));
-
-        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
-                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
-                        .aliases(emptyMap())
-                        .bidadjustmentfactors(givenAdjustments)
-                        .auctiontimestamp(1000L)
-                        .build())));
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        assertThat(result)
-                .extracting(AuctionParticipation::getBidderResponse)
-                .extracting(BidderResponse::getSeatBid)
-                .flatExtracting(BidderSeatBid::getBids)
-                .extracting(BidderBid::getBid)
-                .extracting(Bid::getPrice)
-                .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1));
-    }
-
-    @Test
-    public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() {
-        // given
-        final BidderResponse bidderResponse = BidderResponse.of(
-                "bidder",
-                BidderSeatBid.builder()
-                        .bids(List.of(
-                                givenBidderBid(Bid.builder()
-                                                .impid("123")
-                                                .price(BigDecimal.valueOf(2)).build(),
-                                        "USD")
-                        ))
-                        .build(),
-                1);
-
-        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
-                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner,
-                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
-                .build();
-        givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468));
-        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder"))
-                .willReturn(BigDecimal.valueOf(3.456));
-
-        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
-                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
-                        .aliases(emptyMap())
-                        .bidadjustmentfactors(givenAdjustments)
-                        .auctiontimestamp(1000L)
-                        .build())));
-
-        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
-        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
-
-        // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
-
-        // then
-        assertThat(result)
-                .extracting(AuctionParticipation::getBidderResponse)
-                .extracting(BidderResponse::getSeatBid)
-                .flatExtracting(BidderSeatBid::getBids)
-                .extracting(BidderBid::getBid)
-                .extracting(Bid::getPrice)
-                .containsExactly(BigDecimal.valueOf(6.912));
-    }
-
-    @Test
-    public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresentForBidder() {
-        // given
-        final BidderResponse bidderResponse = BidderResponse.of(
-                "bidder",
-                BidderSeatBid.builder()
-                        .bids(List.of(
-                                givenBidderBid(Bid.builder()
-                                                .impid("123")
-                                                .price(BigDecimal.ONE).build(),
-                                        "USD")
-                        ))
-                        .build(),
-                1);
+                List.of(givenImp(singletonMap("bidder1", 1), builder -> builder.id("impId1"))),
+                identity());
 
-        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build();
-        givenAdjustments.addFactor("some-other-bidder", BigDecimal.TEN);
+        final BidderBid adjustedBid =
+                givenBidderBid(Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.TEN).build(), "USD");
 
-        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
-                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
-                        .aliases(emptyMap())
-                        .auctiontimestamp(1000L)
-                        .currency(ExtRequestCurrency.of(null, false))
-                        .bidadjustmentfactors(givenAdjustments)
-                        .build())));
+        given(bidAdjustmentsProcessor.enrichWithAdjustedBids(any(), any(), any()))
+                .willReturn(AuctionParticipation.builder()
+                        .bidder("bidder1")
+                        .bidderResponse(BidderResponse.of(
+                                "bidder1", BidderSeatBid.of(singletonList(adjustedBid)), 0))
+                        .build());
 
         final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
         final AuctionContext auctionContext = givenAuctionContext(bidRequest);
 
         // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
+        final List<AuctionParticipation> result = target.validateAndAdjustBids(
+                auctionParticipations, auctionContext, null);
 
         // then
         assertThat(result)
                 .extracting(AuctionParticipation::getBidderResponse)
                 .extracting(BidderResponse::getSeatBid)
                 .flatExtracting(BidderSeatBid::getBids)
-                .extracting(BidderBid::getBid)
-                .extracting(Bid::getPrice)
-                .containsExactly(BigDecimal.ONE);
+                .containsExactly(adjustedBid);
     }
 
     @Test
@@ -806,8 +141,8 @@ public void shouldReturnBidsAcceptedByPriceFloorEnforcer() {
         final AuctionContext auctionContext = givenAuctionContext(bidRequest);
 
         // when
-        final List<AuctionParticipation> result = target
-                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
+        final List<AuctionParticipation> result = target.validateAndAdjustBids(
+                auctionParticipations, auctionContext, null);
 
         // then
         assertThat(result)
@@ -945,13 +280,37 @@ public void shouldTolerateResponseBidValidationWarnings() {
                         "BidId `bidId1` validation messages: Warning: Error: bid validation warning."));
     }
 
-    private BidderResponse givenBidderResponse(Bid bid) {
-        return BidderResponse.of(
+    @Test
+    public void shouldAddWarningAboutMultipleCurrency() {
+        // given
+        final BidderResponse bidderResponse = BidderResponse.of(
                 "bidder",
                 BidderSeatBid.builder()
-                        .bids(singletonList(givenBidderBid(bid)))
+                        .bids(List.of(
+                                givenBidderBid(Bid.builder().impid("impId1").price(BigDecimal.valueOf(2.0)).build(),
+                                        "CUR1")))
                         .build(),
                 1);
+
+        final BidRequest bidRequest = givenBidRequest(
+                singletonList(givenImp(singletonMap("bidder", 2), identity())),
+                builder -> builder.cur(List.of("CUR1", "CUR2", "CUR2")));
+
+        final List<AuctionParticipation> auctionParticipations = givenAuctionParticipation(bidderResponse, bidRequest);
+        final AuctionContext auctionContext = givenAuctionContext(bidRequest);
+
+        // when
+        final List<AuctionParticipation> result = target
+                .validateAndAdjustBids(auctionParticipations, auctionContext, null);
+
+        // then
+        assertThat(result).hasSize(1);
+
+        final BidderSeatBid firstSeatBid = result.getFirst().getBidderResponse().getSeatBid();
+        final BidderError expectedWarning = BidderError.badInput(
+                "a single currency (CUR1) has been chosen for the request. "
+                        + "ORTB 2.6 requires that all responses are in the same currency.");
+        assertThat(firstSeatBid.getWarnings()).containsOnly(expectedWarning);
     }
 
     private List<AuctionParticipation> givenAuctionParticipation(
@@ -983,7 +342,7 @@ private static BidRequest givenBidRequest(List<Imp> imp,
                 .build();
     }
 
-    private static <T> Imp givenImp(T ext, Function<Imp.ImpBuilder, Imp.ImpBuilder> impBuilderCustomizer) {
+    private static <T> Imp givenImp(T ext, UnaryOperator<Imp.ImpBuilder> impBuilderCustomizer) {
         return impBuilderCustomizer.apply(Imp.builder()
                         .id(UUID.randomUUID().toString())
                         .ext(mapper.valueToTree(singletonMap(
@@ -998,15 +357,4 @@ private static BidderBid givenBidderBid(Bid bid) {
     private static BidderBid givenBidderBid(Bid bid, String currency) {
         return BidderBid.of(bid, banner, currency);
     }
-
-    private static BidderBid givenBidderBid(Bid bid, String currency, BidType type) {
-        return BidderBid.of(bid, type, currency);
-    }
-
-    private static <K, V> Map<K, V> doubleMap(K key1, V value1, K key2, V value2) {
-        final Map<K, V> map = new HashMap<>();
-        map.put(key1, value1);
-        map.put(key2, value2);
-        return map;
-    }
 }
diff --git a/src/test/java/org/prebid/server/auction/DsaEnforcerTest.java b/src/test/java/org/prebid/server/auction/DsaEnforcerTest.java
index 3c42e27b3ed..5254ea4edda 100644
--- a/src/test/java/org/prebid/server/auction/DsaEnforcerTest.java
+++ b/src/test/java/org/prebid/server/auction/DsaEnforcerTest.java
@@ -104,7 +104,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsNotRequiredAndDsaRespons
                 .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100))
                 .build();
         assertThat(actual).isEqualTo(expectedParticipation);
-        verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
+        verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
     }
 
     @Test
@@ -137,7 +137,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsNotRequiredAndDsaRespons
                 .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100))
                 .build();
         assertThat(actual).isEqualTo(expectedParticipation);
-        verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
+        verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
     }
 
     @Test
@@ -333,7 +333,7 @@ public void enforceShouldRejectBidAndAddWarningWhenBidExtHasEmptyDsaAndDsaIsRequ
                 .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100))
                 .build();
         assertThat(actual).isEqualTo(expectedParticipation);
-        verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
+        verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
     }
 
     @Test
@@ -367,7 +367,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndDsaResponseHa
                 .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100))
                 .build();
         assertThat(actual).isEqualTo(expectedParticipation);
-        verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
+        verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
     }
 
     @Test
@@ -401,7 +401,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndDsaResponseHa
                 .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100))
                 .build();
         assertThat(actual).isEqualTo(expectedParticipation);
-        verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
+        verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
     }
 
     @Test
@@ -436,7 +436,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndPublisherAndA
                 .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100))
                 .build();
         assertThat(actual).isEqualTo(expectedParticipation);
-        verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
+        verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
     }
 
     @Test
@@ -471,7 +471,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndPublisherAndA
                 .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100))
                 .build();
         assertThat(actual).isEqualTo(expectedParticipation);
-        verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
+        verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
     }
 
     @Test
@@ -505,7 +505,7 @@ public void enforceShouldRejectBidAndAddWarningWhenDsaIsRequiredAndPublisherNotR
                 .bidderResponse(BidderResponse.of("bidder", expectedSeatBid, 100))
                 .build();
         assertThat(actual).isEqualTo(expectedParticipation);
-        verify(bidRejectionTracker).reject("imp_id", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
+        verify(bidRejectionTracker).rejectBid(bid, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
     }
 
     private static ExtRegs givenExtRegs(DsaRequired dsaRequired, DsaPublisherRender pubRender) {
diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java
index 7eb1a351531..11346bc5957 100644
--- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java
+++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java
@@ -70,8 +70,8 @@
 import org.prebid.server.cookie.UidsCookie;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.floors.PriceFloorAdjuster;
 import org.prebid.server.floors.PriceFloorProcessor;
 import org.prebid.server.hooks.execution.HookStageExecutor;
@@ -84,13 +84,13 @@
 import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
 import org.prebid.server.hooks.execution.model.Stage;
 import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
 import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl;
 import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl;
 import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl;
-import org.prebid.server.hooks.v1.analytics.ActivityImpl;
-import org.prebid.server.hooks.v1.analytics.AppliedToImpl;
-import org.prebid.server.hooks.v1.analytics.ResultImpl;
-import org.prebid.server.hooks.v1.analytics.TagsImpl;
 import org.prebid.server.log.CriteriaLogManager;
 import org.prebid.server.log.HttpInteractionLogger;
 import org.prebid.server.metric.MetricName;
@@ -188,7 +188,6 @@
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mock.Strictness.LENIENT;
@@ -197,7 +196,6 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
@@ -312,7 +310,8 @@ public void setUp() {
                 false,
                 false,
                 CompressionType.NONE,
-                Ortb.of(false)));
+                Ortb.of(false),
+                0L));
 
         given(privacyEnforcementService.mask(any(), argThat(MapUtils::isNotEmpty), any()))
                 .willAnswer(inv ->
@@ -383,7 +382,7 @@ public void setUp() {
         given(criteriaLogManager.traceResponse(any(), any(), any(), anyBoolean()))
                 .willAnswer(inv -> inv.getArgument(1));
 
-        given(timeoutResolver.adjustForBidder(anyLong(), anyInt(), anyLong()))
+        given(timeoutResolver.adjustForBidder(anyLong(), anyInt(), anyLong(), anyLong()))
                 .willAnswer(invocation -> invocation.getArgument(0));
 
         given(timeoutResolver.adjustForRequest(anyLong(), anyLong()))
@@ -1088,6 +1087,8 @@ public void shouldReturnProperStoredResponseIfAvailableOnlySingleImpRequests() {
     @Test
     public void shouldExtractRequestByAliasForCorrectBidder() {
         // given
+        given(bidderCatalog.isValidName("bidderAlias")).willReturn(false);
+
         final Bidder<?> bidder = mock(Bidder.class);
         givenBidder("bidder", bidder, givenEmptySeatBid());
 
@@ -1110,9 +1111,36 @@ public void shouldExtractRequestByAliasForCorrectBidder() {
                 .contains(1);
     }
 
+    @Test
+    public void shouldExtractRequestByAliasForHardcodedBidderAlias() {
+        // given
+        final Bidder<?> bidder = mock(Bidder.class);
+        givenBidder("bidderAlias", bidder, givenEmptySeatBid());
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(
+                        givenImp(singletonMap("bidderAlias", 1), identity())),
+                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .aliases(singletonMap("bidderAlias", "bidder"))
+                        .auctiontimestamp(1000L)
+                        .build())));
+
+        // when
+        target.holdAuction(givenRequestContext(bidRequest));
+
+        // then
+        final ArgumentCaptor<BidderRequest> bidRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
+        verify(httpBidderRequester)
+                .requestBids(same(bidder), bidRequestCaptor.capture(), any(), any(), any(), any(), anyBoolean());
+        assertThat(bidRequestCaptor.getValue().getBidRequest().getImp()).hasSize(1)
+                .extracting(imp -> imp.getExt().get("bidder").asInt())
+                .contains(1);
+    }
+
     @Test
     public void shouldExtractMultipleRequestsForTheSameBidderIfAliasesWereUsed() {
         // given
+        given(bidderCatalog.isValidName("bidderAlias")).willReturn(false);
+
         final Bidder<?> bidder = mock(Bidder.class);
         givenBidder("bidder", bidder, givenEmptySeatBid());
 
@@ -1138,6 +1166,39 @@ public void shouldExtractMultipleRequestsForTheSameBidderIfAliasesWereUsed() {
                 .containsOnly(2, 1);
     }
 
+    @Test
+    public void shouldExtractMultipleRequestsForBidderAndItsHardcodedAlias() {
+        // given
+        final Bidder<?> bidder = mock(Bidder.class);
+        final Bidder<?> bidderAlias = mock(Bidder.class);
+        givenBidder("bidder", bidder, givenEmptySeatBid());
+        givenBidder("bidderAlias", bidderAlias, givenEmptySeatBid());
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(
+                        givenImp(doubleMap("bidder", 1, "bidderAlias", 2), identity())),
+                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .aliases(singletonMap("bidderAlias", "bidder"))
+                        .auctiontimestamp(1000L)
+                        .build())));
+
+        // when
+        target.holdAuction(givenRequestContext(bidRequest));
+
+        // then
+        final ArgumentCaptor<BidderRequest> bidRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
+        verify(httpBidderRequester)
+                .requestBids(same(bidder), bidRequestCaptor.capture(), any(), any(), any(), any(), anyBoolean());
+        verify(httpBidderRequester)
+                .requestBids(same(bidderAlias), bidRequestCaptor.capture(), any(), any(), any(), any(), anyBoolean());
+
+        final List<BidderRequest> capturedBidderRequests = bidRequestCaptor.getAllValues();
+
+        assertThat(capturedBidderRequests).hasSize(2)
+                .extracting(BidderRequest::getBidRequest)
+                .extracting(capturedBidRequest -> capturedBidRequest.getImp().getFirst().getExt().get("bidder").asInt())
+                .containsOnly(2, 1);
+    }
+
     @Test
     public void shouldTolerateBidderResultWithoutBids() {
         // given
@@ -2617,12 +2678,12 @@ public void shouldUseConcreteOverGeneralSiteWithExtPrebidBidderConfigIgnoringCas
 
         final ObjectNode siteWithPage = mapper.valueToTree(Site.builder().page("testPage").build());
         final ExtBidderConfig extBidderConfig = ExtBidderConfig.of(
-                null, ExtBidderConfigOrtb.of(siteWithPage, null, null, null));
+                ExtBidderConfigOrtb.of(siteWithPage, null, null, null));
         final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of(
                 singletonList("SoMeBiDdEr"), extBidderConfig);
         final ObjectNode siteWithDomain = mapper.valueToTree(Site.builder().domain("notUsed").build());
         final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of(
-                null, ExtBidderConfigOrtb.of(siteWithDomain, null, null, null));
+                ExtBidderConfigOrtb.of(siteWithDomain, null, null, null));
         final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(singletonList("*"),
                 allExtBidderConfig);
 
@@ -2664,12 +2725,12 @@ public void shouldUseConcreteOverGeneralDoohWithExtPrebidBidderConfig() {
 
         final ObjectNode doohWithVenueType = mapper.valueToTree(Dooh.builder().venuetype(List.of("venuetype")).build());
         final ExtBidderConfig extBidderConfig = ExtBidderConfig.of(
-                null, ExtBidderConfigOrtb.of(null, null, doohWithVenueType, null));
+                ExtBidderConfigOrtb.of(null, null, doohWithVenueType, null));
         final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of(
                 singletonList("someBidder"), extBidderConfig);
         final ObjectNode doohWithDomain = mapper.valueToTree(Dooh.builder().domain("notUsed").build());
         final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of(
-                null, ExtBidderConfigOrtb.of(null, null, doohWithDomain, null));
+                ExtBidderConfigOrtb.of(null, null, doohWithDomain, null));
         final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(
                 singletonList("*"),
                 allExtBidderConfig);
@@ -2713,7 +2774,7 @@ public void shouldUseConcreteOverGeneralAppWithExtPrebidBidderConfigIgnoringCase
         final Publisher publisherWithId = Publisher.builder().id("testId").build();
         final ObjectNode appWithPublisherId = mapper.valueToTree(App.builder().publisher(publisherWithId).build());
         final ExtBidderConfig extBidderConfig = ExtBidderConfig.of(
-                null, ExtBidderConfigOrtb.of(null, appWithPublisherId, null, null));
+                ExtBidderConfigOrtb.of(null, appWithPublisherId, null, null));
         final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of(
                 singletonList("SoMeBiDdEr"), extBidderConfig);
 
@@ -2721,7 +2782,7 @@ public void shouldUseConcreteOverGeneralAppWithExtPrebidBidderConfigIgnoringCase
         final ObjectNode appWithUpdatedPublisher = mapper.valueToTree(
                 App.builder().publisher(publisherWithIdAndDomain).build());
         final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of(
-                null, ExtBidderConfigOrtb.of(null, appWithUpdatedPublisher, null, null));
+                ExtBidderConfigOrtb.of(null, appWithUpdatedPublisher, null, null));
         final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(singletonList("*"),
                 allExtBidderConfig);
 
@@ -2760,13 +2821,13 @@ public void shouldUseConcreteOverGeneralUserWithExtPrebidBidderConfig() {
         givenBidder("someBidder", bidder, givenEmptySeatBid());
         final ObjectNode bidderConfigUser = mapper.valueToTree(User.builder().id("userFromConfig").build());
         final ExtBidderConfig extBidderConfig = ExtBidderConfig.of(
-                null, ExtBidderConfigOrtb.of(null, null, null, bidderConfigUser));
+                ExtBidderConfigOrtb.of(null, null, null, bidderConfigUser));
         final ExtRequestPrebidBidderConfig concreteFpdConfig = ExtRequestPrebidBidderConfig.of(
                 singletonList("SomMeBiDdEr"), extBidderConfig);
 
         final ObjectNode emptyUser = mapper.valueToTree(User.builder().build());
         final ExtBidderConfig allExtBidderConfig = ExtBidderConfig.of(
-                null, ExtBidderConfigOrtb.of(null, null, null, emptyUser));
+                ExtBidderConfigOrtb.of(null, null, null, emptyUser));
         final ExtRequestPrebidBidderConfig allFpdConfig = ExtRequestPrebidBidderConfig.of(singletonList("*"),
                 allExtBidderConfig);
         final User requestUser = User.builder().id("erased").buyeruid("testBuyerId").build();
@@ -2964,6 +3025,8 @@ public void shouldNotAddExtPrebidEventsWhenEventsServiceReturnsEmptyEventsServic
     @Test
     public void shouldIncrementCommonMetrics() {
         // given
+        given(bidderCatalog.isValidName("someAlias")).willReturn(false);
+
         given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean()))
                 .willReturn(Future.succeededFuture(givenSeatBid(singletonList(
                         givenBidderBid(Bid.builder().impid("impId").price(TEN).build())))));
@@ -2979,6 +3042,8 @@ public void shouldIncrementCommonMetrics() {
         target.holdAuction(givenRequestContext(bidRequest));
 
         // then
+        verify(metrics).updateDebugRequestMetrics(false);
+        verify(metrics).updateAccountDebugRequestMetrics(any(), eq(false));
         verify(metrics).updateRequestBidderCardinalityMetric(1);
         verify(metrics).updateAccountRequestMetrics(any(), eq(MetricName.openrtb2web));
         verify(metrics).updateAdapterRequestTypeAndNoCookieMetrics(
@@ -3565,124 +3630,6 @@ public void shouldReturnBidResponseWithWarningWhenAnalyticsTagsDisabledAndReques
                         ExtBidderError.of(999, "analytics.options.enableclientdetails not enabled for account"));
     }
 
-    @Test
-    public void shouldIncrementHooksGlobalMetrics() {
-        // given
-        final AuctionContext auctionContext = AuctionContext.builder()
-                .hookExecutionContext(HookExecutionContext.of(
-                        Endpoint.openrtb2_auction,
-                        stageOutcomes(givenAppliedToImpl(identity()))))
-                .debugContext(DebugContext.empty())
-                .requestRejected(true)
-                .build();
-
-        // when
-        target.holdAuction(auctionContext);
-
-        // then
-        verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any());
-        verify(metrics).updateHooksMetrics(
-                eq("module1"),
-                eq(Stage.entrypoint),
-                eq("hook1"),
-                eq(ExecutionStatus.success),
-                eq(4L),
-                eq(ExecutionAction.update));
-        verify(metrics).updateHooksMetrics(
-                eq("module1"),
-                eq(Stage.entrypoint),
-                eq("hook2"),
-                eq(ExecutionStatus.invocation_failure),
-                eq(6L),
-                isNull());
-        verify(metrics).updateHooksMetrics(
-                eq("module1"),
-                eq(Stage.entrypoint),
-                eq("hook2"),
-                eq(ExecutionStatus.success),
-                eq(4L),
-                eq(ExecutionAction.no_action));
-        verify(metrics).updateHooksMetrics(
-                eq("module2"),
-                eq(Stage.entrypoint),
-                eq("hook1"),
-                eq(ExecutionStatus.timeout),
-                eq(6L),
-                isNull());
-        verify(metrics).updateHooksMetrics(
-                eq("module3"),
-                eq(Stage.auction_response),
-                eq("hook1"),
-                eq(ExecutionStatus.success),
-                eq(4L),
-                eq(ExecutionAction.update));
-        verify(metrics).updateHooksMetrics(
-                eq("module3"),
-                eq(Stage.auction_response),
-                eq("hook2"),
-                eq(ExecutionStatus.success),
-                eq(4L),
-                eq(ExecutionAction.no_action));
-        verify(metrics, never()).updateAccountHooksMetrics(any(), any(), any(), any());
-        verify(metrics, never()).updateAccountModuleDurationMetric(any(), any(), any());
-    }
-
-    @Test
-    public void shouldIncrementHooksGlobalAndAccountMetrics() {
-        // given
-        given(httpBidderRequester.requestBids(any(), any(), any(), any(), any(), any(), anyBoolean()))
-                .willReturn(Future.succeededFuture(givenSeatBid(emptyList())));
-
-        final BidRequest bidRequest = givenBidRequest(givenSingleImp(singletonMap("bidder", 2)));
-        final AuctionContext auctionContext = givenRequestContext(bidRequest).toBuilder()
-                .hookExecutionContext(HookExecutionContext.of(
-                        Endpoint.openrtb2_auction,
-                        stageOutcomes(givenAppliedToImpl(identity()))))
-                .debugContext(DebugContext.empty())
-                .build();
-
-        // when
-        target.holdAuction(auctionContext);
-
-        // then
-        verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any());
-        verify(metrics, times(6)).updateAccountHooksMetrics(any(), any(), any(), any());
-        verify(metrics).updateAccountHooksMetrics(
-                any(),
-                eq("module1"),
-                eq(ExecutionStatus.success),
-                eq(ExecutionAction.update));
-        verify(metrics).updateAccountHooksMetrics(
-                any(),
-                eq("module1"),
-                eq(ExecutionStatus.invocation_failure),
-                isNull());
-        verify(metrics).updateAccountHooksMetrics(
-                any(),
-                eq("module1"),
-                eq(ExecutionStatus.success),
-                eq(ExecutionAction.no_action));
-        verify(metrics).updateAccountHooksMetrics(
-                any(),
-                eq("module2"),
-                eq(ExecutionStatus.timeout),
-                isNull());
-        verify(metrics).updateAccountHooksMetrics(
-                any(),
-                eq("module3"),
-                eq(ExecutionStatus.success),
-                eq(ExecutionAction.update));
-        verify(metrics).updateAccountHooksMetrics(
-                any(),
-                eq("module3"),
-                eq(ExecutionStatus.success),
-                eq(ExecutionAction.no_action));
-        verify(metrics, times(3)).updateAccountModuleDurationMetric(any(), any(), any());
-        verify(metrics).updateAccountModuleDurationMetric(any(), eq("module1"), eq(14L));
-        verify(metrics).updateAccountModuleDurationMetric(any(), eq("module2"), eq(6L));
-        verify(metrics).updateAccountModuleDurationMetric(any(), eq("module3"), eq(8L));
-    }
-
     @Test
     public void shouldProperPopulateImpExtPrebidEvenIfInExtImpPrebidContainNotCorrectField() {
         // given
@@ -3892,7 +3839,9 @@ public void shouldResponseWithEmptySeatBidIfBidderNotSupportRequestCurrency() {
                 false,
                 false,
                 CompressionType.NONE,
-                Ortb.of(false)));
+                Ortb.of(false),
+                0L));
+
         given(bidResponseCreator.create(
                 argThat(argument -> argument.getAuctionParticipations().getFirst()
                         .getBidderResponse()
@@ -3919,7 +3868,7 @@ public void shouldResponseWithEmptySeatBidIfBidderNotSupportRequestCurrency() {
         assertThat(result.result())
                 .extracting(AuctionContext::getBidRejectionTrackers)
                 .extracting(rejectionTrackers -> rejectionTrackers.get("bidder1"))
-                .extracting(BidRejectionTracker::getRejectionReasons)
+                .extracting(BidRejectionTracker::getRejectedImps)
                 .isEqualTo(Map.of("impId1", BidRejectionReason.REQUEST_BLOCKED_UNACCEPTABLE_CURRENCY));
 
     }
@@ -3955,17 +3904,35 @@ public void shouldConvertBidRequestOpenRTBVersionToConfiguredByBidder() {
     @Test
     public void shouldPassAdjustedTimeoutToAdapterAndToBidResponseCreator() {
         // given
-        given(timeoutResolver.adjustForBidder(anyLong(), eq(90), anyLong()))
-                .willReturn(400L);
-        given(timeoutResolver.adjustForRequest(anyLong(), anyLong()))
-                .willReturn(450L);
+        given(bidderCatalog.bidderInfoByName(anyString())).willReturn(BidderInfo.create(
+                true,
+                null,
+                false,
+                null,
+                null,
+                null,
+                null,
+                null,
+                null,
+                null,
+                0,
+                null,
+                false,
+                false,
+                CompressionType.NONE,
+                Ortb.of(false),
+                100L));
+
+        given(timeoutResolver.adjustForBidder(anyLong(), eq(90), eq(200L), eq(100L))).willReturn(400L);
+        given(timeoutResolver.adjustForRequest(anyLong(), eq(200L))).willReturn(450L);
 
         final BidRequest bidRequest = givenBidRequest(
                 givenSingleImp(singletonMap("bidderName", 1)),
                 request -> request.source(Source.builder().tid("uniqTid").build()));
 
         // when
-        target.holdAuction(givenRequestContext(bidRequest));
+        target.holdAuction(givenRequestContext(bidRequest).toBuilder()
+                .timeoutContext(TimeoutContext.of(clock.millis() - 200L, timeout, 90)).build());
 
         // then
         final ArgumentCaptor<BidderRequest> bidderRequestCaptor = ArgumentCaptor.forClass(BidderRequest.class);
@@ -3984,7 +3951,34 @@ public void shouldPassAdjustedTimeoutToAdapterAndToBidResponseCreator() {
     }
 
     @Test
-    public void shouldDropBidsWithInvalidPriceAndAddDebugWarnings() {
+    public void shouldDropBidsWithInvalidPrice() {
+        // given
+        final Bidder<?> bidder = mock(Bidder.class);
+        final List<Bid> bids = List.of(
+                Bid.builder().id("valid_bid").impid("impId").price(BigDecimal.valueOf(2.0)).build(),
+                Bid.builder().id("invalid_bid_1").impid("impId").price(null).build(),
+                Bid.builder().id("invalid_bid_2").impid("impId").price(BigDecimal.ZERO).build(),
+                Bid.builder().id("invalid_bid_3").impid("impId").price(BigDecimal.valueOf(-0.01)).build());
+        final BidderSeatBid seatBid = givenSeatBid(bids.stream().map(ExchangeServiceTest::givenBidderBid).toList());
+
+        givenBidder("bidder", bidder, seatBid);
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
+                identity());
+        final AuctionContext givenContext = givenRequestContext(bidRequest).with(DebugContext.empty());
+
+        // when
+        final AuctionContext result = target.holdAuction(givenContext).result();
+
+        // then
+        assertThat(result.getBidResponse().getSeatbid())
+                .flatExtracting(SeatBid::getBid).hasSize(1);
+        assertThat(givenContext.getDebugWarnings()).isEmpty();
+        verify(metrics, times(3)).updateAdapterRequestErrorMetric("bidder", MetricName.unknown_error);
+    }
+
+    @Test
+    public void shouldDropBidsWithInvalidPriceAndAddDebugWarningsWhenDebugEnabled() {
         // given
         final Bidder<?> bidder = mock(Bidder.class);
         final List<Bid> bids = List.of(
@@ -3998,7 +3992,8 @@ public void shouldDropBidsWithInvalidPriceAndAddDebugWarnings() {
 
         final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
                 identity());
-        final AuctionContext givenContext = givenRequestContext(bidRequest);
+        final AuctionContext givenContext = givenRequestContext(bidRequest)
+                .with(DebugContext.of(true, false, null));
 
         // when
         final AuctionContext result = target.holdAuction(givenContext).result();
diff --git a/src/test/java/org/prebid/server/auction/FpdResolverTest.java b/src/test/java/org/prebid/server/auction/FpdResolverTest.java
index 07909bd09f5..d3b7530ab84 100644
--- a/src/test/java/org/prebid/server/auction/FpdResolverTest.java
+++ b/src/test/java/org/prebid/server/auction/FpdResolverTest.java
@@ -23,7 +23,6 @@
 
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
@@ -67,16 +66,14 @@ public void resolveUserShouldOverrideFpdFieldsFromFpdUser() {
 
         // then
         assertThat(resultUser).isEqualTo(User.builder()
-                .id("id")
+                .id("fpdid")
                 .keywords("fpdkeywords")
                 .yob(2)
                 .gender("fpdgender")
-                .buyeruid("buyeruid")
-                .customdata("customdata")
-                .geo(Geo.builder().country("country").build())
+                .buyeruid("fpdbuyeruid")
+                .customdata("fpdcustomdata")
+                .geo(Geo.builder().country("fpdcountry").build())
                 .data(Collections.singletonList(Data.builder().id("fpdid").build()))
-                .ext(ExtUser.builder().data(mapper.createObjectNode()
-                        .set("geo", mapper.createObjectNode().put("country", "fpdcountry"))).build())
                 .build());
     }
 
@@ -92,69 +89,6 @@ public void resolveUserShouldReturnFpdUserIfOriginUserIsNull() {
                 .isEqualTo(User.builder().gender("male").build());
     }
 
-    @Test
-    public void resolveUserShouldAddExtDataAttributesIfOriginDoesNotHaveExtData() {
-        // given
-        final User originUser = User.builder()
-                .build();
-
-        final User fpdUser = User.builder()
-                .geo(Geo.builder().country("country").build())
-                .build();
-
-        // when
-        final User resultUser = target.resolveUser(originUser, mapper.valueToTree(fpdUser));
-
-        // then
-        assertThat(resultUser).isEqualTo(User.builder()
-                .ext(ExtUser.builder().data(mapper.createObjectNode()
-                                .set("geo", mapper.createObjectNode().put("country", "country")))
-                        .build())
-                .build());
-    }
-
-    @Test
-    public void resolveAppShouldAddExtDataAttributesIfOriginDoesNotHaveExtData() {
-        // given
-        final App originApp = App.builder().build();
-        final App fpdApp = App.builder().id("id").build();
-
-        // when
-        final App resultApp = target.resolveApp(originApp, mapper.valueToTree(fpdApp));
-
-        // then
-        assertThat(resultApp).isEqualTo(App.builder().ext(ExtApp.of(null, mapper.createObjectNode()
-                .put("id", "id"))).build());
-    }
-
-    @Test
-    public void resolveSiteShouldAddExtDataAttributesIfOriginDoesNotHaveExtData() {
-        // given
-        final Site originSite = Site.builder().build();
-        final Site fpdSite = Site.builder().id("id").build();
-
-        // when
-        final Site resultSite = target.resolveSite(originSite, mapper.valueToTree(fpdSite));
-
-        // then
-        assertThat(resultSite).isEqualTo(Site.builder().ext(ExtSite.of(null, mapper.createObjectNode()
-                .put("id", "id"))).build());
-    }
-
-    @Test
-    public void resolveDoohShouldAddExtDataAttributesIfOriginDoesNotHaveExtData() {
-        // given
-        final Dooh originDooh = Dooh.builder().build();
-        final Dooh fpdDooh = Dooh.builder().id("id").build();
-
-        // when
-        final Dooh resultDooh = target.resolveDooh(originDooh, mapper.valueToTree(fpdDooh));
-
-        // then
-        assertThat(resultDooh).isEqualTo(
-                Dooh.builder().ext(ExtDooh.of(mapper.createObjectNode().put("id", "id"))).build());
-    }
-
     @Test
     public void resolveUserShouldNotChangeOriginExtDataIfFPDDoesNotHaveExt() {
         // given
@@ -218,6 +152,7 @@ public void resolveAppShouldOverrideFpdFieldsFromFpdApp() {
                 .publisher(Publisher.builder().id("originId").build())
                 .content(Content.builder().language("originLan").build())
                 .keywords("originKeywords")
+                .ext(ExtApp.of(ExtAppPrebid.of("originalSource", null), mapper.createObjectNode()))
                 .build();
 
         final App fpdApp = App.builder()
@@ -235,17 +170,18 @@ public void resolveAppShouldOverrideFpdFieldsFromFpdApp() {
                 .publisher(Publisher.builder().id("fpdId").build())
                 .content(Content.builder().language("fpdLan").build())
                 .keywords("fpdKeywords")
+                .ext(ExtApp.of(
+                        ExtAppPrebid.of(null, "fpdVersion"),
+                        mapper.createObjectNode().put("data", "fpdData")))
                 .build();
+
         // when
         final App resultApp = target.resolveApp(originApp, mapper.valueToTree(fpdApp));
 
         // then
-        final ObjectNode dataResult = mapper.createObjectNode().put("id", "fpdId").put("privacypolicy", 2)
-                .set("publisher", mapper.createObjectNode().put("id", "fpdId"));
-        dataResult.set("content", mapper.createObjectNode().put("language", "fpdLan"));
         assertThat(resultApp)
                 .isEqualTo(App.builder()
-                        .id("originId")
+                        .id("fpdId")
                         .name("fpdName")
                         .bundle("fpdBundle")
                         .domain("fpdDomain")
@@ -253,13 +189,15 @@ public void resolveAppShouldOverrideFpdFieldsFromFpdApp() {
                         .cat(Collections.singletonList("fpdCat"))
                         .sectioncat(Collections.singletonList("fpdSectionCat"))
                         .pagecat(Collections.singletonList("fpdPageCat"))
-                        .publisher(Publisher.builder().id("originId").build())
-                        .content(Content.builder().language("originLan").build())
-                        .ver("originVer")
-                        .privacypolicy(1)
-                        .paid(1)
+                        .publisher(Publisher.builder().id("fpdId").build())
+                        .content(Content.builder().language("fpdLan").build())
+                        .ver("fpdVer")
+                        .privacypolicy(2)
+                        .paid(2)
                         .keywords("fpdKeywords")
-                        .ext(ExtApp.of(null, dataResult))
+                        .ext(ExtApp.of(
+                                ExtAppPrebid.of("originalSource", "fpdVersion"),
+                                mapper.createObjectNode().put("data", "fpdData")))
                         .build());
     }
 
@@ -343,9 +281,6 @@ public void resolveSiteShouldOverrideFpdFieldsFromFpdSite() {
         final Site resultSite = target.resolveSite(originSite, mapper.valueToTree(fpdSite));
 
         // then
-        final ObjectNode extData = mapper.createObjectNode().put("id", "fpdId").put("privacypolicy", 2).put("mobile", 2)
-                .set("publisher", mapper.createObjectNode().put("id", "fpdId"));
-        extData.set("content", mapper.createObjectNode().put("language", "fpdLan"));
         assertThat(resultSite).isEqualTo(
                 Site.builder()
                         .name("fpdName")
@@ -357,12 +292,11 @@ public void resolveSiteShouldOverrideFpdFieldsFromFpdSite() {
                         .ref("fpdRef")
                         .search("fpdSearch")
                         .keywords("fpdKeywords")
-                        .id("originId")
-                        .content(Content.builder().language("originLan").build())
-                        .publisher(Publisher.builder().id("originId").build())
-                        .privacypolicy(1)
-                        .mobile(1)
-                        .ext(ExtSite.of(null, extData))
+                        .id("fpdId")
+                        .content(Content.builder().language("fpdLan").build())
+                        .publisher(Publisher.builder().id("fpdId").build())
+                        .privacypolicy(2)
+                        .mobile(2)
                         .build());
     }
 
@@ -434,22 +368,16 @@ public void resolveDoohShouldOverrideFpdFieldsFromFpdDooh() {
         final Dooh resultDooh = target.resolveDooh(originDooh, mapper.valueToTree(fpdDooh));
 
         // then
-        final ObjectNode extData = mapper.createObjectNode()
-                .put("id", "fpdId")
-                .setAll(Map.of(
-                        "publisher", mapper.createObjectNode().put("id", "fpdId"),
-                        "content", mapper.createObjectNode().put("language", "fpdLan")));
         assertThat(resultDooh).isEqualTo(
                 Dooh.builder()
+                        .id("fpdId")
                         .name("fpdName")
                         .domain("fpdDomain")
-                        .keywords("fpdKeywords")
-                        .id("originId")
                         .venuetype(List.of("fpdVenuetype"))
                         .venuetypetax(2)
-                        .content(Content.builder().language("originLan").build())
-                        .publisher(Publisher.builder().id("originId").build())
-                        .ext(ExtDooh.of(extData))
+                        .publisher(Publisher.builder().id("fpdId").build())
+                        .content(Content.builder().language("fpdLan").build())
+                        .keywords("fpdKeywords")
                         .build());
     }
 
diff --git a/src/test/java/org/prebid/server/auction/GeoLocationServiceWrapperTest.java b/src/test/java/org/prebid/server/auction/GeoLocationServiceWrapperTest.java
index 12fed4e7f19..be7348abb74 100644
--- a/src/test/java/org/prebid/server/auction/GeoLocationServiceWrapperTest.java
+++ b/src/test/java/org/prebid/server/auction/GeoLocationServiceWrapperTest.java
@@ -15,8 +15,8 @@
 import org.prebid.server.auction.model.IpAddress.IP;
 import org.prebid.server.auction.model.TimeoutContext;
 import org.prebid.server.auction.requestfactory.Ortb2ImplicitParametersResolver;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.geolocation.GeoLocationService;
 import org.prebid.server.geolocation.model.GeoInfo;
 import org.prebid.server.metric.Metrics;
diff --git a/src/test/java/org/prebid/server/auction/HooksMetricsServiceTest.java b/src/test/java/org/prebid/server/auction/HooksMetricsServiceTest.java
new file mode 100644
index 00000000000..4d90cc637d7
--- /dev/null
+++ b/src/test/java/org/prebid/server/auction/HooksMetricsServiceTest.java
@@ -0,0 +1,260 @@
+package org.prebid.server.auction;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.VertxTest;
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.auction.model.debug.DebugContext;
+import org.prebid.server.hooks.execution.model.ExecutionAction;
+import org.prebid.server.hooks.execution.model.ExecutionStatus;
+import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookExecutionContext;
+import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.metric.Metrics;
+import org.prebid.server.model.Endpoint;
+import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.AccountAuctionConfig;
+import org.prebid.server.settings.model.AccountEventsConfig;
+
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+public class HooksMetricsServiceTest extends VertxTest {
+
+    @Mock
+    private Metrics metrics;
+
+    private HooksMetricsService target;
+
+    @BeforeEach
+    public void before() {
+        target = new HooksMetricsService(metrics);
+    }
+
+    @Test
+    public void shouldIncrementHooksGlobalMetrics() {
+        // given
+        final AuctionContext auctionContext = AuctionContext.builder()
+                .hookExecutionContext(HookExecutionContext.of(
+                        Endpoint.openrtb2_auction,
+                        stageOutcomes(givenAppliedToImpl())))
+                .debugContext(DebugContext.empty())
+                .requestRejected(true)
+                .build();
+
+        // when
+        target.updateHooksMetrics(auctionContext);
+
+        // then
+        verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any());
+        verify(metrics).updateHooksMetrics(
+                eq("module1"),
+                eq(Stage.entrypoint),
+                eq("hook1"),
+                eq(ExecutionStatus.success),
+                eq(4L),
+                eq(ExecutionAction.update));
+        verify(metrics).updateHooksMetrics(
+                eq("module1"),
+                eq(Stage.entrypoint),
+                eq("hook2"),
+                eq(ExecutionStatus.invocation_failure),
+                eq(6L),
+                isNull());
+        verify(metrics).updateHooksMetrics(
+                eq("module1"),
+                eq(Stage.entrypoint),
+                eq("hook2"),
+                eq(ExecutionStatus.success),
+                eq(4L),
+                eq(ExecutionAction.no_action));
+        verify(metrics).updateHooksMetrics(
+                eq("module2"),
+                eq(Stage.entrypoint),
+                eq("hook1"),
+                eq(ExecutionStatus.timeout),
+                eq(6L),
+                isNull());
+        verify(metrics).updateHooksMetrics(
+                eq("module3"),
+                eq(Stage.auction_response),
+                eq("hook1"),
+                eq(ExecutionStatus.success),
+                eq(4L),
+                eq(ExecutionAction.update));
+        verify(metrics).updateHooksMetrics(
+                eq("module3"),
+                eq(Stage.auction_response),
+                eq("hook2"),
+                eq(ExecutionStatus.success),
+                eq(4L),
+                eq(ExecutionAction.no_action));
+        verify(metrics, never()).updateAccountHooksMetrics(any(), any(), any(), any());
+        verify(metrics, never()).updateAccountModuleDurationMetric(any(), any(), any());
+    }
+
+    @Test
+    public void shouldIncrementHooksGlobalAndAccountMetrics() {
+        // given
+        final AuctionContext auctionContext = AuctionContext.builder()
+                .hookExecutionContext(HookExecutionContext.of(
+                        Endpoint.openrtb2_auction,
+                        stageOutcomes(givenAppliedToImpl())))
+                .debugContext(DebugContext.empty())
+                .requestRejected(true)
+                .account(Account.builder()
+                        .id("accountId")
+                        .auction(AccountAuctionConfig.builder()
+                                .events(AccountEventsConfig.of(true))
+                                .build())
+                        .build())
+                .build();
+
+        // when
+        target.updateHooksMetrics(auctionContext);
+
+        // then
+        verify(metrics, times(6)).updateHooksMetrics(anyString(), any(), any(), any(), any(), any());
+        verify(metrics, times(6)).updateAccountHooksMetrics(any(), any(), any(), any());
+        verify(metrics).updateAccountHooksMetrics(
+                any(),
+                eq("module1"),
+                eq(ExecutionStatus.success),
+                eq(ExecutionAction.update));
+        verify(metrics).updateAccountHooksMetrics(
+                any(),
+                eq("module1"),
+                eq(ExecutionStatus.invocation_failure),
+                isNull());
+        verify(metrics).updateAccountHooksMetrics(
+                any(),
+                eq("module1"),
+                eq(ExecutionStatus.success),
+                eq(ExecutionAction.no_action));
+        verify(metrics).updateAccountHooksMetrics(
+                any(),
+                eq("module2"),
+                eq(ExecutionStatus.timeout),
+                isNull());
+        verify(metrics).updateAccountHooksMetrics(
+                any(),
+                eq("module3"),
+                eq(ExecutionStatus.success),
+                eq(ExecutionAction.update));
+        verify(metrics).updateAccountHooksMetrics(
+                any(),
+                eq("module3"),
+                eq(ExecutionStatus.success),
+                eq(ExecutionAction.no_action));
+        verify(metrics, times(3)).updateAccountModuleDurationMetric(any(), any(), any());
+        verify(metrics).updateAccountModuleDurationMetric(any(), eq("module1"), eq(14L));
+        verify(metrics).updateAccountModuleDurationMetric(any(), eq("module2"), eq(6L));
+        verify(metrics).updateAccountModuleDurationMetric(any(), eq("module3"), eq(8L));
+    }
+
+    private static AppliedToImpl givenAppliedToImpl() {
+        return AppliedToImpl.builder()
+                .impIds(asList("impId1", "impId2"))
+                .request(true)
+                .build();
+    }
+
+    private static EnumMap<Stage, List<StageExecutionOutcome>> stageOutcomes(AppliedToImpl appliedToImp) {
+        final Map<Stage, List<StageExecutionOutcome>> stageOutcomes = new HashMap<>();
+
+        stageOutcomes.put(Stage.entrypoint, singletonList(StageExecutionOutcome.of(
+                "http-request",
+                asList(
+                        GroupExecutionOutcome.of(asList(
+                                HookExecutionOutcome.builder()
+                                        .hookId(HookId.of("module1", "hook1"))
+                                        .executionTime(4L)
+                                        .status(ExecutionStatus.success)
+                                        .message("Message 1-1")
+                                        .action(ExecutionAction.update)
+                                        .errors(asList("error message 1-1 1", "error message 1-1 2"))
+                                        .warnings(asList("warning message 1-1 1", "warning message 1-1 2"))
+                                        .debugMessages(asList("debug message 1-1 1", "debug message 1-1 2"))
+                                        .analyticsTags(TagsImpl.of(singletonList(
+                                                ActivityImpl.of(
+                                                        "some-activity",
+                                                        "success",
+                                                        singletonList(ResultImpl.of(
+                                                                "success",
+                                                                mapper.createObjectNode(),
+                                                                appliedToImp))))))
+                                        .build(),
+                                HookExecutionOutcome.builder()
+                                        .hookId(HookId.of("module1", "hook2"))
+                                        .executionTime(6L)
+                                        .status(ExecutionStatus.invocation_failure)
+                                        .message("Message 1-2")
+                                        .errors(asList("error message 1-2 1", "error message 1-2 2"))
+                                        .warnings(asList("warning message 1-2 1", "warning message 1-2 2"))
+                                        .build())),
+                        GroupExecutionOutcome.of(asList(
+                                HookExecutionOutcome.builder()
+                                        .hookId(HookId.of("module1", "hook2"))
+                                        .executionTime(4L)
+                                        .status(ExecutionStatus.success)
+                                        .message("Message 1-2")
+                                        .action(ExecutionAction.no_action)
+                                        .errors(asList("error message 1-2 3", "error message 1-2 4"))
+                                        .warnings(asList("warning message 1-2 3", "warning message 1-2 4"))
+                                        .build(),
+                                HookExecutionOutcome.builder()
+                                        .hookId(HookId.of("module2", "hook1"))
+                                        .executionTime(6L)
+                                        .status(ExecutionStatus.timeout)
+                                        .message("Message 2-1")
+                                        .errors(asList("error message 2-1 1", "error message 2-1 2"))
+                                        .warnings(asList("warning message 2-1 1", "warning message 2-1 2"))
+                                        .build()))))));
+
+        stageOutcomes.put(Stage.auction_response, singletonList(StageExecutionOutcome.of(
+                "auction-response",
+                singletonList(
+                        GroupExecutionOutcome.of(asList(
+                                HookExecutionOutcome.builder()
+                                        .hookId(HookId.of("module3", "hook1"))
+                                        .executionTime(4L)
+                                        .status(ExecutionStatus.success)
+                                        .message("Message 3-1")
+                                        .action(ExecutionAction.update)
+                                        .errors(asList("error message 3-1 1", "error message 3-1 2"))
+                                        .warnings(asList("warning message 3-1 1", "warning message 3-1 2"))
+                                        .build(),
+                                HookExecutionOutcome.builder()
+                                        .hookId(HookId.of("module3", "hook2"))
+                                        .executionTime(4L)
+                                        .status(ExecutionStatus.success)
+                                        .action(ExecutionAction.no_action)
+                                        .build()))))));
+
+        return new EnumMap<>(stageOutcomes);
+    }
+
+}
diff --git a/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java b/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java
index f49a03fcdee..7c0797ffacd 100644
--- a/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java
+++ b/src/test/java/org/prebid/server/auction/OrtbTypesResolverTest.java
@@ -133,7 +133,7 @@ public void normalizeTargetingShouldNormalizeFieldsForUser() {
     }
 
     @Test
-    public void normalizeTargetingShouldNormalizeFieldsForAppExceptId() {
+    public void normalizeTargetingShouldNormalizeFieldsForApp() {
         // given
         final ObjectNode app = mapper.createObjectNode();
         app.set("id", array("id1", "id2"));
@@ -149,16 +149,16 @@ public void normalizeTargetingShouldNormalizeFieldsForAppExceptId() {
 
         // then
         assertThat(containerNode).isEqualTo(mapper.createObjectNode().set("app", mapper.createObjectNode()
+                .put("id", "id1")
                 .put("name", "name1")
                 .put("bundle", "bundle1")
                 .put("storeurl", "storeurl1")
                 .put("domain", "domain1")
-                .put("keywords", "keyword1,keyword2")
-                .set("id", array("id1", "id2"))));
+                .put("keywords", "keyword1,keyword2")));
     }
 
     @Test
-    public void normalizeTargetingShouldNormalizeFieldsForSiteExceptId() {
+    public void normalizeTargetingShouldNormalizeFieldsForSite() {
         // given
         final ObjectNode site = mapper.createObjectNode();
         site.set("id", array("id1", "id2"));
@@ -175,14 +175,13 @@ public void normalizeTargetingShouldNormalizeFieldsForSiteExceptId() {
 
         // then
         assertThat(containerNode).isEqualTo(mapper.createObjectNode().set("site", mapper.createObjectNode()
+                .put("id", "id1")
                 .put("name", "name1")
                 .put("page", "page1")
                 .put("ref", "ref1")
                 .put("domain", "domain1")
                 .put("search", "search1")
-                .put("keywords", "keyword1,keyword2")
-                .set("id", array("id1", "id2")))
-        );
+                .put("keywords", "keyword1,keyword2")));
     }
 
     @Test
@@ -409,22 +408,22 @@ public void normalizeBidRequestShouldResolveEmptyOrtbWithFpdFieldsWithIdForReque
 
         assertThat(ortb2.path("site"))
                 .isEqualTo(mapper.createObjectNode()
+                        .put("id", "id1")
                         .put("name", "name1")
                         .put("domain", "domain1")
                         .put("page", "page1")
                         .put("ref", "ref1")
                         .put("search", "search1")
-                        .put("keywords", "keyword1,keyword2")
-                        .set("id", array("id1", "id2")));
+                        .put("keywords", "keyword1,keyword2"));
 
         assertThat(ortb2.path("app"))
                 .isEqualTo(mapper.createObjectNode()
+                        .put("id", "id1")
                         .put("name", "name1")
                         .put("bundle", "bundle1")
                         .put("storeurl", "storeurl1")
                         .put("domain", "domain1")
-                        .put("keywords", "keyword1,keyword2")
-                        .set("id", array("id1", "id2")));
+                        .put("keywords", "keyword1,keyword2"));
 
         assertThat(ortb2.path("user"))
                 .isEqualTo(mapper.createObjectNode()
@@ -472,12 +471,12 @@ public void normalizeBidRequestShouldBeMergedWithFpdContextToOrtbSite() {
                 .put("fpdData", "data_value");
 
         final ObjectNode expectedOrtb = mapper.createObjectNode()
-                .put("name", "name1")
-                .put("domain", "domain1")
-                .put("page", "page1")
-                .put("ref", "ortb_ref1")
-                .put("keywords", "ortb_keyword1,ortb_keyword2");
-        expectedOrtb.set("id", array("id1", "id2"));
+                 .put("id", "ortb_id")
+                 .put("name", "name1")
+                 .put("domain", "ortb_domain1")
+                 .put("page", "ortb_page1")
+                 .put("ref", "ortb_ref1")
+                 .put("keywords", "ortb_keyword1,ortb_keyword2");
         expectedOrtb.set("ext", obj("data", expectedOrtbExtData));
 
         assertThat(ortb2.path("site")).isEqualTo(expectedOrtb);
@@ -518,7 +517,7 @@ public void normalizeBidRequestShouldBeMergedWithFpdUserToOrtbUser() {
 
         assertThat(ortb2.path("user"))
                 .isEqualTo(mapper.createObjectNode()
-                        .put("gender", "gender1")
+                        .put("gender", "ortb_gender1")
                         .put("keywords", "ortb_keyword1,ortb_keyword2")
                         .set("ext", obj("data", expectedOrtbExtData)));
 
diff --git a/src/test/java/org/prebid/server/auction/PriceGranularityTest.java b/src/test/java/org/prebid/server/auction/PriceGranularityTest.java
index 8c3fd9ee24b..338af30f830 100644
--- a/src/test/java/org/prebid/server/auction/PriceGranularityTest.java
+++ b/src/test/java/org/prebid/server/auction/PriceGranularityTest.java
@@ -26,6 +26,19 @@ public void createFromStringShouldThrowPrebidExceptionIfInvalidStringType() {
         assertThatExceptionOfType(PreBidException.class).isThrownBy(() -> PriceGranularity.createFromString("invalid"));
     }
 
+    @Test
+    public void createFromStringOrDefaultShouldCreateMedPriceGranularityWhenInvalidStringType() {
+        // given and when
+        final PriceGranularity defaultPriceGranularity = PriceGranularity.createFromStringOrDefault(
+                "invalid");
+
+        // then
+        assertThat(defaultPriceGranularity.getRangesMax()).isEqualByComparingTo(BigDecimal.valueOf(20));
+        assertThat(defaultPriceGranularity.getPrecision()).isEqualTo(2);
+        assertThat(defaultPriceGranularity.getRanges()).containsOnly(
+                ExtGranularityRange.of(BigDecimal.valueOf(20), BigDecimal.valueOf(0.1)));
+    }
+
     @Test
     public void createCustomPriceGranularityByStringLow() {
         // given and when
diff --git a/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java b/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java
index adcd1e9a296..3976a4bbd8f 100644
--- a/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java
+++ b/src/test/java/org/prebid/server/auction/SkippedAuctionServiceTest.java
@@ -13,7 +13,7 @@
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.StoredResponseResult;
 import org.prebid.server.auction.model.TimeoutContext;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
 import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse;
diff --git a/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java
index 8cbd5bc8b70..9b9aa5aba48 100644
--- a/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java
+++ b/src/test/java/org/prebid/server/auction/StoredRequestProcessorTest.java
@@ -19,7 +19,7 @@
 import org.prebid.server.VertxTest;
 import org.prebid.server.auction.model.AuctionStoredResult;
 import org.prebid.server.exception.InvalidRequestException;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.identity.IdGenerator;
 import org.prebid.server.json.JsonMerger;
 import org.prebid.server.metric.Metrics;
diff --git a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java b/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java
index 3b625ddf379..a137578ef14 100644
--- a/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java
+++ b/src/test/java/org/prebid/server/auction/StoredResponseProcessorTest.java
@@ -23,8 +23,8 @@
 import org.prebid.server.bidder.model.BidderSeatBid;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.proto.openrtb.ext.request.ExtImp;
 import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebid;
 import org.prebid.server.proto.openrtb.ext.request.ExtStoredAuctionResponse;
diff --git a/src/test/java/org/prebid/server/auction/TimeoutResolverTest.java b/src/test/java/org/prebid/server/auction/TimeoutResolverTest.java
index 746b7b8cb4a..d1ebf4d3f0a 100644
--- a/src/test/java/org/prebid/server/auction/TimeoutResolverTest.java
+++ b/src/test/java/org/prebid/server/auction/TimeoutResolverTest.java
@@ -53,12 +53,12 @@ public void limitToMaxShouldReturnMaxTimeout() {
 
     @Test
     public void adjustForBidderShouldReturnExpectedResult() {
-        assertThat(timeoutResolver.adjustForBidder(200L, 70, 10L)).isEqualTo(120L);
+        assertThat(timeoutResolver.adjustForBidder(300L, 70, 10L, 50L)).isEqualTo(140L);
     }
 
     @Test
     public void adjustForBidderShouldReturnMinTimeout() {
-        assertThat(timeoutResolver.adjustForBidder(200L, 50, 10L)).isEqualTo(MIN_TIMEOUT);
+        assertThat(timeoutResolver.adjustForBidder(200L, 50, 10L, 100L)).isEqualTo(MIN_TIMEOUT);
     }
 
     @Test
diff --git a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java
index 3946162ff92..66442d47c93 100644
--- a/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java
+++ b/src/test/java/org/prebid/server/auction/VideoStoredRequestProcessorTest.java
@@ -24,7 +24,7 @@
 import org.prebid.server.VertxTest;
 import org.prebid.server.auction.model.WithPodErrors;
 import org.prebid.server.exception.InvalidRequestException;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.json.JsonMerger;
 import org.prebid.server.metric.Metrics;
 import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory;
diff --git a/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java b/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java
index 60ff60318e1..16875667fae 100644
--- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java
+++ b/src/test/java/org/prebid/server/auction/mediatypeprocessor/BidderMediaTypeProcessorTest.java
@@ -175,7 +175,8 @@ private static BidderInfo givenBidderInfo(List<MediaType> appMediaTypes,
                 false,
                 false,
                 CompressionType.NONE,
-                Ortb.of(false));
+                Ortb.of(false),
+                0L);
     }
 
     private static BidRequest givenBidRequest(UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
diff --git a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java b/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java
index e2394769585..61bf49b8745 100644
--- a/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java
+++ b/src/test/java/org/prebid/server/auction/mediatypeprocessor/MultiFormatMediaTypeProcessorTest.java
@@ -279,7 +279,8 @@ private static BidderInfo givenBidderInfo(boolean multiFormatSupported) {
                 false,
                 false,
                 CompressionType.NONE,
-                Ortb.of(multiFormatSupported));
+                Ortb.of(multiFormatSupported),
+                0L);
     }
 
     private static BidRequest givenBidRequest(UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
diff --git a/src/test/java/org/prebid/server/auction/model/BidRejectionTrackerTest.java b/src/test/java/org/prebid/server/auction/model/BidRejectionTrackerTest.java
index 5b9cc66779e..502b09108b5 100644
--- a/src/test/java/org/prebid/server/auction/model/BidRejectionTrackerTest.java
+++ b/src/test/java/org/prebid/server/auction/model/BidRejectionTrackerTest.java
@@ -1,14 +1,19 @@
 package org.prebid.server.auction.model;
 
+import com.iab.openrtb.response.Bid;
+import org.apache.commons.lang3.tuple.Pair;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.prebid.server.bidder.model.BidderBid;
 
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 import static java.util.Collections.singleton;
-import static java.util.Collections.singletonMap;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.entry;
 
 public class BidRejectionTrackerTest {
 
@@ -16,90 +21,214 @@ public class BidRejectionTrackerTest {
 
     @BeforeEach
     public void setUp() {
-        target = new BidRejectionTracker("bidder", singleton("1"), 0);
+        target = new BidRejectionTracker("bidder", singleton("impId1"), 0);
     }
 
     @Test
-    public void succeedShouldRestoreBidderFromRejection() {
+    public void succeedShouldRestoreImpFromImpRejection() {
         // given
-        target.reject("1", BidRejectionReason.ERROR_GENERAL);
+        target.rejectImp("impId1", BidRejectionReason.ERROR_GENERAL);
 
         // when
-        target.succeed("1");
+        final BidderBid bid = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build();
+        target.succeed(singleton(bid));
 
         // then
-        assertThat(target.getRejectionReasons()).isEmpty();
+        assertThat(target.getRejectedImps()).isEmpty();
+        assertThat(target.getRejectedBids())
+                .containsOnly(entry("impId1", List.of(Pair.of(null, BidRejectionReason.ERROR_GENERAL))));
     }
 
     @Test
-    public void succeedShouldIgnoreUninvolvedImpIds() {
+    public void succeedShouldRestoreImpFromBidRejection() {
         // given
-        target.reject("1", BidRejectionReason.ERROR_GENERAL);
+        final BidderBid bid = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build();
+        target.rejectBid(bid, BidRejectionReason.ERROR_GENERAL);
 
         // when
-        target.succeed("2");
+        target.succeed(singleton(bid));
 
         // then
-        assertThat(target.getRejectionReasons())
-                .isEqualTo(singletonMap("1", BidRejectionReason.ERROR_GENERAL));
+        assertThat(target.getRejectedImps()).isEmpty();
+        assertThat(target.getRejectedBids())
+                .containsOnly(entry("impId1", List.of(Pair.of(bid, BidRejectionReason.ERROR_GENERAL))));
     }
 
     @Test
-    public void rejectShouldRecordRejectionFirstTimeIfImpIdIsInvolved() {
+    public void succeedShouldIgnoreUninvolvedImpIdsOnImpRejection() {
+        // given
+        target.rejectImp("impId1", BidRejectionReason.ERROR_GENERAL);
+
+        // when
+        final BidderBid bid = BidderBid.builder().bid(Bid.builder().id("bidId2").impid("impId2").build()).build();
+        target.succeed(singleton(bid));
+
+        // then
+        assertThat(target.getRejectedImps()).containsOnly(entry("impId1", BidRejectionReason.ERROR_GENERAL));
+        assertThat(target.getRejectedBids())
+                .containsOnly(entry("impId1", List.of(Pair.of(null, BidRejectionReason.ERROR_GENERAL))));
+    }
+
+    @Test
+    public void succeedShouldIgnoreUninvolvedImpIdsOnBidRejection() {
+        // given
+        final BidderBid bid1 = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build();
+        target.rejectBid(bid1, BidRejectionReason.ERROR_GENERAL);
+
+        // when
+        final BidderBid bid2 = BidderBid.builder().bid(Bid.builder().id("bidId2").impid("impId2").build()).build();
+        target.succeed(singleton(bid2));
+
+        // then
+        assertThat(target.getRejectedImps()).containsOnly(entry("impId1", BidRejectionReason.ERROR_GENERAL));
+        assertThat(target.getRejectedBids())
+                .containsOnly(entry("impId1", List.of(Pair.of(bid1, BidRejectionReason.ERROR_GENERAL))));
+    }
+
+    @Test
+    public void rejectImpShouldRecordImpRejectionFirstTimeIfImpIdIsInvolved() {
+        // when
+        target.rejectImp("impId1", BidRejectionReason.ERROR_GENERAL);
+
+        // then
+        assertThat(target.getRejectedImps()).containsOnly(entry("impId1", BidRejectionReason.ERROR_GENERAL));
+        assertThat(target.getRejectedBids())
+                .containsOnly(entry("impId1", List.of(Pair.of(null, BidRejectionReason.ERROR_GENERAL))));
+    }
+
+    @Test
+    public void rejectBidShouldRecordBidRejectionFirstTimeIfImpIdIsInvolved() {
         // when
-        target.reject("1", BidRejectionReason.ERROR_GENERAL);
+        final BidderBid bid = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build();
+        target.rejectBid(bid, BidRejectionReason.ERROR_GENERAL);
 
         // then
-        assertThat(target.getRejectionReasons())
-                .isEqualTo(singletonMap("1", BidRejectionReason.ERROR_GENERAL));
+        assertThat(target.getRejectedImps()).containsOnly(entry("impId1", BidRejectionReason.ERROR_GENERAL));
+        assertThat(target.getRejectedBids())
+                .containsOnly(entry("impId1", List.of(Pair.of(bid, BidRejectionReason.ERROR_GENERAL))));
     }
 
     @Test
-    public void rejectShouldNotRecordRejectionIfImpIdIsNotInvolved() {
+    public void rejectBidShouldRecordBidRejectionAfterPreviouslySucceededBid() {
+        // given
+        final BidderBid bid1 = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build();
+        final BidderBid bid2 = BidderBid.builder().bid(Bid.builder().id("bidId2").impid("impId1").build()).build();
+        target.succeed(Set.of(bid1, bid2));
+
+        // when
+        target.rejectBid(bid1, BidRejectionReason.ERROR_GENERAL);
+
+        // then
+        assertThat(target.getRejectedImps()).isEmpty();
+        assertThat(target.getRejectedBids())
+                .containsOnly(entry("impId1", List.of(Pair.of(bid1, BidRejectionReason.ERROR_GENERAL))));
+    }
+
+    @Test
+    public void rejectImpShouldNotRecordImpRejectionIfImpIdIsAlreadyRejected() {
+        // given
+        target.rejectImp("impId1", BidRejectionReason.ERROR_GENERAL);
+
+        // when
+        target.rejectImp("impId1", BidRejectionReason.ERROR_INVALID_BID_RESPONSE);
+
+        // then
+        assertThat(target.getRejectedImps()).containsOnly(entry("impId1", BidRejectionReason.ERROR_GENERAL));
+        assertThat(target.getRejectedBids())
+                .containsOnly(entry("impId1", List.of(
+                        Pair.of(null, BidRejectionReason.ERROR_GENERAL),
+                        Pair.of(null, BidRejectionReason.ERROR_INVALID_BID_RESPONSE))));
+    }
+
+    @Test
+    public void rejectBidShouldNotRecordImpRejectionButRecordBidRejectionEvenIfImpIsAlreadyRejected() {
+        // given
+        final BidderBid bid1 = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build();
+        target.rejectBid(bid1, BidRejectionReason.RESPONSE_REJECTED_GENERAL);
+
         // when
-        target.reject("2", BidRejectionReason.ERROR_GENERAL);
+        final BidderBid bid2 = BidderBid.builder().bid(Bid.builder().id("bidId2").impid("impId1").build()).build();
+        target.rejectBid(bid2, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR);
 
         // then
-        assertThat(target.getRejectionReasons()).doesNotContainKey("2");
+        assertThat(target.getRejectedImps())
+                .containsOnly(entry("impId1", BidRejectionReason.RESPONSE_REJECTED_GENERAL));
+        assertThat(target.getRejectedBids())
+                .containsOnly(entry("impId1", List.of(
+                        Pair.of(bid1, BidRejectionReason.RESPONSE_REJECTED_GENERAL),
+                        Pair.of(bid2, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR))));
     }
 
     @Test
-    public void rejectShouldNotRecordRejectionIfImpIdIsAlreadyRejected() {
+    public void rejectAllImpsShouldTryRejectingEachImpId() {
         // given
-        target.reject("1", BidRejectionReason.ERROR_GENERAL);
+        target = new BidRejectionTracker("bidder", Set.of("impId1", "impId2", "impId3"), 0);
+        target.rejectImp("impId1", BidRejectionReason.NO_BID);
 
         // when
-        target.reject("1", BidRejectionReason.ERROR_INVALID_BID_RESPONSE);
+        target.rejectAllImps(BidRejectionReason.ERROR_TIMED_OUT);
 
         // then
-        assertThat(target.getRejectionReasons())
-                .isEqualTo(singletonMap("1", BidRejectionReason.ERROR_GENERAL));
+        assertThat(target.getRejectedImps())
+                .isEqualTo(Map.of(
+                        "impId1", BidRejectionReason.NO_BID,
+                        "impId2", BidRejectionReason.ERROR_TIMED_OUT,
+                        "impId3", BidRejectionReason.ERROR_TIMED_OUT));
+
+        assertThat(target.getRejectedBids())
+                .containsOnly(
+                        entry("impId1", List.of(
+                                Pair.of(null, BidRejectionReason.NO_BID),
+                                Pair.of(null, BidRejectionReason.ERROR_TIMED_OUT))),
+                        entry("impId2", List.of(Pair.of(null, BidRejectionReason.ERROR_TIMED_OUT))),
+                        entry("impId3", List.of(Pair.of(null, BidRejectionReason.ERROR_TIMED_OUT))));
     }
 
     @Test
-    public void rejectAllShouldTryRejectingEachImpId() {
+    public void rejectBidsShouldTryRejectingEachBid() {
         // given
-        target = new BidRejectionTracker("bidder", Set.of("1", "2", "3"), 0);
-        target.reject("1", BidRejectionReason.NO_BID);
+        target = new BidRejectionTracker("bidder", Set.of("impId1", "impId2", "impId3"), 0);
+        final BidderBid bid0 = BidderBid.builder().bid(Bid.builder().id("bidId0").impid("impId1").build()).build();
+        target.rejectBid(bid0, BidRejectionReason.RESPONSE_REJECTED_GENERAL);
 
         // when
-        target.rejectAll(BidRejectionReason.ERROR_TIMED_OUT);
+        final BidderBid bid1 = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId1").build()).build();
+        final BidderBid bid2 = BidderBid.builder().bid(Bid.builder().id("bidId2").impid("impId2").build()).build();
+        final BidderBid bid3 = BidderBid.builder().bid(Bid.builder().id("bidId3").impid("impId3").build()).build();
+        target.rejectBids(Set.of(bid1, bid2, bid3), BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY);
 
         // then
-        assertThat(target.getRejectionReasons())
+        assertThat(target.getRejectedImps())
                 .isEqualTo(Map.of(
-                        "1", BidRejectionReason.NO_BID,
-                        "2", BidRejectionReason.ERROR_TIMED_OUT,
-                        "3", BidRejectionReason.ERROR_TIMED_OUT));
+                        "impId1", BidRejectionReason.RESPONSE_REJECTED_GENERAL,
+                        "impId2", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY,
+                        "impId3", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY));
+
+        assertThat(target.getRejectedBids())
+                .containsOnly(
+                        entry("impId1", List.of(
+                                Pair.of(bid0, BidRejectionReason.RESPONSE_REJECTED_GENERAL),
+                                Pair.of(bid1, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY))),
+                        entry("impId2", List.of(Pair.of(bid2, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY))),
+                        entry("impId3", List.of(Pair.of(bid3, BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY))));
     }
 
     @Test
-    public void getRejectionReasonsShouldTreatUnsuccessfulBidsAsNoBidRejection() {
+    public void getRejectedImpsShouldTreatUnsuccessfulImpsAsNoBidRejection() {
         // given
-        target = new BidRejectionTracker("bidder", Set.of("1", "2"), 0);
-        target.succeed("2");
+        target = new BidRejectionTracker("bidder", Set.of("impId1", "impId2"), 0);
+        final BidderBid bid = BidderBid.builder().bid(Bid.builder().id("bidId1").impid("impId2").build()).build();
+        target.succeed(singleton(bid));
 
         // then
-        assertThat(target.getRejectionReasons()).isEqualTo(singletonMap("1", BidRejectionReason.NO_BID));
+        assertThat(target.getRejectedImps()).containsOnly(entry("impId1", BidRejectionReason.NO_BID));
+    }
+
+    @Test
+    public void rejectImpShouldFailRejectingWithReasonThatImpliesExistingBidToReject() {
+        assertThatThrownBy(() -> target.rejectImp("impId1", BidRejectionReason.RESPONSE_REJECTED_DSA_PRIVACY))
+                .isInstanceOf(IllegalArgumentException.class)
+                .hasMessage("The non-bid code 300 and higher assumes "
+                        + "that there is a rejected bid that shouldn't be lost");
     }
 }
diff --git a/src/test/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactoryTest.java b/src/test/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactoryTest.java
index fd005459913..477e67ffd0b 100644
--- a/src/test/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactoryTest.java
+++ b/src/test/java/org/prebid/server/auction/privacy/contextfactory/CookieSyncPrivacyContextFactoryTest.java
@@ -14,7 +14,7 @@
 import org.prebid.server.auction.ImplicitParametersExtractor;
 import org.prebid.server.auction.IpAddressHelper;
 import org.prebid.server.auction.model.IpAddress;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.privacy.PrivacyExtractor;
 import org.prebid.server.privacy.gdpr.TcfDefinerService;
 import org.prebid.server.privacy.gdpr.model.TcfContext;
diff --git a/src/test/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactoryTest.java b/src/test/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactoryTest.java
index 453210aabd1..1cf5961ff4e 100644
--- a/src/test/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactoryTest.java
+++ b/src/test/java/org/prebid/server/auction/privacy/contextfactory/SetuidPrivacyContextFactoryTest.java
@@ -14,7 +14,7 @@
 import org.prebid.server.auction.ImplicitParametersExtractor;
 import org.prebid.server.auction.IpAddressHelper;
 import org.prebid.server.auction.model.IpAddress;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.privacy.PrivacyExtractor;
 import org.prebid.server.privacy.gdpr.TcfDefinerService;
 import org.prebid.server.privacy.gdpr.model.TcfContext;
diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcementTest.java
index a6c2e3f3ae9..6e4662a501d 100644
--- a/src/test/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcementTest.java
+++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/ActivityEnforcementTest.java
@@ -9,6 +9,7 @@
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
+import org.prebid.server.auction.BidderAliases;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.BidderPrivacyResult;
 import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
@@ -32,6 +33,9 @@ public class ActivityEnforcementTest {
     @Mock
     private ActivityInfrastructure activityInfrastructure;
 
+    @Mock
+    private BidderAliases bidderAliases;
+
     @BeforeEach
     public void setUp() {
         target = new ActivityEnforcement(userFpdActivityMask);
@@ -58,7 +62,8 @@ public void enforceShouldReturnExpectedResult() {
         final AuctionContext context = givenAuctionContext();
 
         // when
-        final List<BidderPrivacyResult> result = target.enforce(singletonList(bidderPrivacyResult), context).result();
+        final List<BidderPrivacyResult> result =
+                target.enforce(context, bidderAliases, singletonList(bidderPrivacyResult)).result();
 
         //then
         assertThat(result).allSatisfy(privacyResult -> {
diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java
index 5459e232386..178e48ff97d 100644
--- a/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java
+++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/CcpaEnforcementTest.java
@@ -30,7 +30,6 @@
 
 import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.function.UnaryOperator;
 
@@ -80,7 +79,8 @@ public void setUp() {
                         true,
                         false,
                         null,
-                        Ortb.of(false)));
+                        Ortb.of(false),
+                        0L));
 
         target = new CcpaEnforcement(userFpdCcpaMask, bidderCatalog, metrics, true);
 
@@ -89,16 +89,17 @@ public void setUp() {
     }
 
     @Test
-    public void enforceShouldReturnEmptyListWhenCcpaNotEnforced() {
+    public void enforceShouldNotModifyListWhenCcpaIsNotEnforced() {
         // given
         final AuctionContext auctionContext = givenAuctionContext(context -> context
                 .privacyContext(PrivacyContext.of(Privacy.builder().ccpa(Ccpa.of("1YN-")).build(), null, null)));
+        final List<BidderPrivacyResult> initialResults = givenPrivacyResults(givenUser(), givenDevice());
 
         // when
-        final List<BidderPrivacyResult> result = target.enforce(auctionContext, null, aliases).result();
+        final List<BidderPrivacyResult> result = target.enforce(auctionContext, aliases, initialResults).result();
 
         // then
-        assertThat(result).isEmpty();
+        assertThat(result).containsExactlyInAnyOrderElementsOf(initialResults);
         verify(metrics).updatePrivacyCcpaMetrics(
                 eq(activityInfrastructure),
                 eq(true),
@@ -112,13 +113,15 @@ public void enforceShouldConsiderEnforceCcpaConfigurationProperty() {
         // given
         final AuctionContext auctionContext = givenAuctionContext(context -> context.account(Account.empty("id")));
 
+        final List<BidderPrivacyResult> initialResults = givenPrivacyResults(givenUser(), givenDevice());
+
         target = new CcpaEnforcement(userFpdCcpaMask, bidderCatalog, metrics, false);
 
         // when
-        final List<BidderPrivacyResult> result = target.enforce(auctionContext, null, aliases).result();
+        final List<BidderPrivacyResult> result = target.enforce(auctionContext, aliases, initialResults).result();
 
         // then
-        assertThat(result).isEmpty();
+        assertThat(result).containsExactlyInAnyOrderElementsOf(initialResults);
         verify(metrics).updatePrivacyCcpaMetrics(
                 eq(activityInfrastructure),
                 eq(true),
@@ -136,12 +139,13 @@ public void enforceShouldConsiderAccountCcpaEnabledProperty() {
                                 .ccpa(AccountCcpaConfig.builder().enabled(false).build())
                                 .build())
                         .build()));
+        final List<BidderPrivacyResult> initialResults = givenPrivacyResults(givenUser(), givenDevice());
 
         // when
-        final List<BidderPrivacyResult> result = target.enforce(auctionContext, null, aliases).result();
+        final List<BidderPrivacyResult> result = target.enforce(auctionContext, aliases, initialResults).result();
 
         // then
-        assertThat(result).isEmpty();
+        assertThat(result).containsExactlyInAnyOrderElementsOf(initialResults);
         verify(metrics).updatePrivacyCcpaMetrics(
                 eq(activityInfrastructure),
                 eq(true),
@@ -155,12 +159,13 @@ public void enforceShouldConsiderAccountCcpaEnabledForRequestTypeProperty() {
         // given
         final AuctionContext auctionContext = givenAuctionContext(context -> context
                 .requestTypeMetric(MetricName.openrtb2app));
+        final List<BidderPrivacyResult> initialResults = givenPrivacyResults(givenUser(), givenDevice());
 
         // when
-        final List<BidderPrivacyResult> result = target.enforce(auctionContext, null, aliases).result();
+        final List<BidderPrivacyResult> result = target.enforce(auctionContext, aliases, initialResults).result();
 
         // then
-        assertThat(result).isEmpty();
+        assertThat(result).containsExactlyInAnyOrderElementsOf(initialResults);
         verify(metrics).updatePrivacyCcpaMetrics(
                 eq(activityInfrastructure),
                 eq(true),
@@ -174,21 +179,21 @@ public void enforceShouldTreatAllBiddersAsNoSale() {
         // given
         final AuctionContext auctionContext = givenAuctionContext(context -> context
                 .bidRequest(BidRequest.builder()
-                        .device(Device.builder().ip("originalDevice").build())
+                        .device(givenDevice())
                         .ext(ExtRequest.of(ExtRequestPrebid.builder()
                                 .nosale(singletonList("*"))
                                 .build()))
                         .build()));
 
-        final Map<String, User> bidderToUser = Map.of(
-                "bidder", User.builder().id("originalUser").build(),
-                "noSale", User.builder().id("originalUser").build());
+        final List<BidderPrivacyResult> initialResults = List.of(
+                BidderPrivacyResult.builder().requestBidder("bidder").user(givenUser()).device(givenDevice()).build(),
+                BidderPrivacyResult.builder().requestBidder("noSale").user(givenUser()).device(givenDevice()).build());
 
         // when
-        final List<BidderPrivacyResult> result = target.enforce(auctionContext, bidderToUser, aliases).result();
+        final List<BidderPrivacyResult> result = target.enforce(auctionContext, aliases, initialResults).result();
 
         // then
-        assertThat(result).isEmpty();
+        assertThat(result).containsExactlyInAnyOrderElementsOf(initialResults);
         verify(metrics).updatePrivacyCcpaMetrics(
                 eq(activityInfrastructure),
                 eq(true),
@@ -218,19 +223,28 @@ public void enforceShouldSkipNoSaleBiddersAndNotEnforcedByBidderConfig() {
                         false,
                         false,
                         null,
-                        Ortb.of(false)));
+                        Ortb.of(false),
+                        0L));
 
         final AuctionContext auctionContext = givenAuctionContext(identity());
 
-        final Map<String, User> bidderToUser = Map.of(
-                "bidderAlias", User.builder().id("originalUser").build(),
-                "noSale", User.builder().id("originalUser").build());
+        final List<BidderPrivacyResult> initialResults = List.of(
+                BidderPrivacyResult.builder()
+                        .requestBidder("bidderAlias")
+                        .user(givenUser())
+                        .device(givenDevice())
+                        .build(),
+                BidderPrivacyResult.builder()
+                        .requestBidder("noSale")
+                        .user(givenUser())
+                        .device(givenDevice())
+                        .build());
 
         // when
-        final List<BidderPrivacyResult> result = target.enforce(auctionContext, bidderToUser, aliases).result();
+        final List<BidderPrivacyResult> result = target.enforce(auctionContext, aliases, initialResults).result();
 
         // then
-        assertThat(result).isEmpty();
+        assertThat(result).containsExactlyInAnyOrderElementsOf(initialResults);
         verify(metrics).updatePrivacyCcpaMetrics(
                 eq(activityInfrastructure),
                 eq(true),
@@ -250,20 +264,18 @@ public void enforceShouldReturnExpectedResult() {
 
         final AuctionContext auctionContext = givenAuctionContext(identity());
 
-        final Map<String, User> bidderToUser = Map.of(
-                "bidder", User.builder().id("originalUser").build(),
-                "noSale", User.builder().id("originalUser").build());
+        final List<BidderPrivacyResult> initialResults = List.of(
+                BidderPrivacyResult.builder().requestBidder("bidder").user(givenUser()).device(givenDevice()).build(),
+                BidderPrivacyResult.builder().requestBidder("noSale").user(givenUser()).device(givenDevice()).build());
 
         // when
-        final List<BidderPrivacyResult> result = target.enforce(auctionContext, bidderToUser, aliases).result();
+        final List<BidderPrivacyResult> result = target.enforce(auctionContext, aliases, initialResults).result();
 
         // then
-        assertThat(result)
-                .hasSize(1)
-                .allSatisfy(privacyResult -> {
-                    assertThat(privacyResult.getUser()).isSameAs(maskedUser);
-                    assertThat(privacyResult.getDevice()).isSameAs(maskedDevice);
-                });
+        assertThat(result).containsExactlyInAnyOrder(
+                BidderPrivacyResult.builder().requestBidder("bidder").user(maskedUser).device(maskedDevice).build(),
+                BidderPrivacyResult.builder().requestBidder("noSale").user(givenUser()).device(givenDevice()).build());
+
         verify(metrics).updatePrivacyCcpaMetrics(
                 eq(activityInfrastructure),
                 eq(true),
@@ -278,7 +290,7 @@ private AuctionContext givenAuctionContext(
         final AuctionContext.AuctionContextBuilder initialContext = AuctionContext.builder()
                 .activityInfrastructure(activityInfrastructure)
                 .bidRequest(BidRequest.builder()
-                        .device(Device.builder().ip("originalDevice").build())
+                        .device(givenDevice())
                         .ext(ExtRequest.of(ExtRequestPrebid.builder()
                                 .nosale(singletonList("noSale"))
                                 .build()))
@@ -301,4 +313,16 @@ private AuctionContext givenAuctionContext(
 
         return auctionContextCustomizer.apply(initialContext).build();
     }
+
+    private static List<BidderPrivacyResult> givenPrivacyResults(User user, Device device) {
+        return singletonList(BidderPrivacyResult.builder().requestBidder("bidder").user(user).device(device).build());
+    }
+
+    private static User givenUser() {
+        return User.builder().id("originalUser").build();
+    }
+
+    private static Device givenDevice() {
+        return Device.builder().ip("originalDevice").build();
+    }
 }
diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcementTest.java
index 4b7e7de8628..9e9d47d0488 100644
--- a/src/test/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcementTest.java
+++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/CoppaEnforcementTest.java
@@ -9,6 +9,7 @@
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
+import org.prebid.server.auction.BidderAliases;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.BidderPrivacyResult;
 import org.prebid.server.auction.privacy.enforcement.mask.UserFpdCoppaMask;
@@ -17,13 +18,14 @@
 import org.prebid.server.privacy.model.PrivacyContext;
 
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
+import static java.util.Collections.singletonList;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 
 @ExtendWith(MockitoExtension.class)
 public class CoppaEnforcementTest {
@@ -34,6 +36,8 @@ public class CoppaEnforcementTest {
     private Metrics metrics;
     @Mock
     private ActivityInfrastructure activityInfrastructure;
+    @Mock
+    private BidderAliases bidderAliases;
 
     private CoppaEnforcement target;
 
@@ -43,29 +47,33 @@ public void setUp() {
     }
 
     @Test
-    public void isApplicableShouldReturnFalse() {
+    public void enforceShouldNotMaskDataWhenNotApplicable() {
         // given
         final AuctionContext auctionContext = AuctionContext.builder()
+                .activityInfrastructure(activityInfrastructure)
                 .privacyContext(PrivacyContext.of(Privacy.builder().coppa(0).build(), null, null))
+                .bidRequest(BidRequest.builder().build())
                 .build();
 
-        // when and then
-        assertThat(target.isApplicable(auctionContext)).isFalse();
-    }
+        final List<BidderPrivacyResult> initialResults = singletonList(
+                BidderPrivacyResult.builder()
+                        .requestBidder("bidder")
+                        .user(User.builder().id("originalUser").build())
+                        .device(Device.builder().ip("originalDevice").build())
+                        .build());
 
-    @Test
-    public void isApplicableShouldReturnTrue() {
-        // given
-        final AuctionContext auctionContext = AuctionContext.builder()
-                .privacyContext(PrivacyContext.of(Privacy.builder().coppa(1).build(), null, null))
-                .build();
+        // when
+        final List<BidderPrivacyResult> results =
+                target.enforce(auctionContext, bidderAliases, initialResults).result();
 
-        // when and then
-        assertThat(target.isApplicable(auctionContext)).isTrue();
+        // then
+        assertThat(results).containsExactlyInAnyOrderElementsOf(initialResults);
+        verifyNoInteractions(userFpdCoppaMask);
+        verifyNoInteractions(metrics);
     }
 
     @Test
-    public void enforceShouldReturnExpectedResultAndEmitMetrics() {
+    public void enforceShouldMaskDataAndEmitMetricsWhenApplicable() {
         // given
         final User maskedUser = User.builder().id("maskedUser").build();
         final Device maskedDevice = Device.builder().ip("maskedDevice").build();
@@ -75,12 +83,19 @@ public void enforceShouldReturnExpectedResultAndEmitMetrics() {
 
         final AuctionContext auctionContext = AuctionContext.builder()
                 .activityInfrastructure(activityInfrastructure)
-                .bidRequest(BidRequest.builder().device(Device.builder().ip("originalDevice").build()).build())
+                .privacyContext(PrivacyContext.of(Privacy.builder().coppa(1).build(), null, null))
+                .bidRequest(BidRequest.builder().build())
                 .build();
-        final Map<String, User> bidderToUser = Map.of("bidder", User.builder().id("originalUser").build());
+
+        final List<BidderPrivacyResult> initialResults = singletonList(
+                BidderPrivacyResult.builder()
+                        .requestBidder("bidder")
+                        .user(User.builder().id("originalUser").build())
+                        .device(Device.builder().ip("originalDevice").build())
+                        .build());
 
         // when
-        final List<BidderPrivacyResult> result = target.enforce(auctionContext, bidderToUser).result();
+        final List<BidderPrivacyResult> result = target.enforce(auctionContext, bidderAliases, initialResults).result();
 
         // then
         assertThat(result).allSatisfy(privacyResult -> {
diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java
index f1a49f91738..4333eebb5d8 100644
--- a/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java
+++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/PrivacyEnforcementServiceTest.java
@@ -1,98 +1,70 @@
 package org.prebid.server.auction.privacy.enforcement;
 
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
 import com.iab.openrtb.request.User;
 import io.vertx.core.Future;
-import org.apache.commons.collections4.ListUtils;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.auction.BidderAliases;
+import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.BidderPrivacyResult;
 
 import java.util.List;
 import java.util.Map;
 
-import static java.util.Collections.singleton;
 import static java.util.Collections.singletonList;
-import static org.assertj.core.api.Assertions.assertThat;
+import static java.util.Collections.singletonMap;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.BDDMockito.given;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
+import static org.prebid.server.assertion.FutureAssertion.assertThat;
 
 @ExtendWith(MockitoExtension.class)
 public class PrivacyEnforcementServiceTest {
 
     @Mock
-    private CoppaEnforcement coppaEnforcement;
-    @Mock
-    private CcpaEnforcement ccpaEnforcement;
-    @Mock
-    private TcfEnforcement tcfEnforcement;
-    @Mock
-    private ActivityEnforcement activityEnforcement;
-
-    private PrivacyEnforcementService target;
+    private PrivacyEnforcement firstEnforcement;
 
-    @BeforeEach
-    public void setUp() {
-        target = new PrivacyEnforcementService(
-                coppaEnforcement,
-                ccpaEnforcement,
-                tcfEnforcement,
-                activityEnforcement);
-    }
-
-    @Test
-    public void maskShouldUseCoppaEnforcementIfApplicable() {
-        // given
-        given(coppaEnforcement.isApplicable(any())).willReturn(true);
-
-        final List<BidderPrivacyResult> bidderPrivacyResults = singletonList(null);
-        given(coppaEnforcement.enforce(any(), any())).willReturn(Future.succeededFuture(bidderPrivacyResults));
-
-        // when
-        final List<BidderPrivacyResult> result = target.mask(null, null, null).result();
+    @Mock
+    private PrivacyEnforcement secondEnforcement;
 
-        // then
-        assertThat(result).isSameAs(bidderPrivacyResults);
-        verifyNoInteractions(ccpaEnforcement);
-        verifyNoInteractions(tcfEnforcement);
-        verifyNoInteractions(activityEnforcement);
-    }
+    @Mock
+    private BidderAliases bidderAliases;
 
     @Test
-    public void maskShouldReturnExpectedResult() {
+    public void maskShouldPassBidderPrivacyThroughAllEnforcements() {
         // given
-        given(coppaEnforcement.isApplicable(any())).willReturn(false);
+        final BidderPrivacyResult expectedResult = BidderPrivacyResult.builder()
+                .requestBidder("bidder")
+                .user(User.EMPTY)
+                .device(Device.builder().build())
+                .build();
 
-        given(ccpaEnforcement.enforce(any(), any(), any())).willReturn(Future.succeededFuture(
-                singletonList(BidderPrivacyResult.builder().requestBidder("bidder1").build())));
+        given(firstEnforcement.enforce(any(), any(), any()))
+                .willReturn(Future.succeededFuture(singletonList(expectedResult)));
+        given(secondEnforcement.enforce(any(), any(), any()))
+                .willReturn(Future.succeededFuture(singletonList(expectedResult)));
 
-        given(tcfEnforcement.enforce(any(), any(), eq(singleton("bidder0")), any()))
-                .willReturn(Future.succeededFuture(
-                        singletonList(BidderPrivacyResult.builder().requestBidder("bidder0").build())));
+        final PrivacyEnforcementService target = new PrivacyEnforcementService(
+                List.of(firstEnforcement, secondEnforcement));
 
-        given(activityEnforcement.enforce(any(), any()))
-                .willAnswer(invocation -> Future.succeededFuture(ListUtils.union(
-                        invocation.getArgument(0),
-                        singletonList(BidderPrivacyResult.builder().requestBidder("bidder2").build()))));
+        final AuctionContext auctionContext = AuctionContext.builder()
+                .bidRequest(BidRequest.builder().device(Device.builder().build()).build())
+                .build();
 
-        final Map<String, User> bidderToUser = Map.of(
-                "bidder0", User.builder().build(),
-                "bidder1", User.builder().build());
+        final User user = User.builder().id("originalUser").build();
+        final Map<String, User> bidderToUser = singletonMap("bidder", user);
 
         // when
-        final List<BidderPrivacyResult> result = target.mask(null, bidderToUser, null).result();
+        final Future<List<BidderPrivacyResult>> result = target.mask(auctionContext, bidderToUser, bidderAliases);
 
         // then
-        assertThat(result).containsExactly(
-                BidderPrivacyResult.builder().requestBidder("bidder1").build(),
-                BidderPrivacyResult.builder().requestBidder("bidder0").build(),
-                BidderPrivacyResult.builder().requestBidder("bidder2").build());
-        verify(coppaEnforcement, times(0)).enforce(any(), any());
+        assertThat(result)
+                .isSucceeded()
+                .unwrap()
+                .asList()
+                .containsExactlyInAnyOrder(expectedResult);
     }
 }
diff --git a/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java b/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java
index 0dad839fc87..0ed33b4cce3 100644
--- a/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java
+++ b/src/test/java/org/prebid/server/auction/privacy/enforcement/TcfEnforcementTest.java
@@ -119,20 +119,20 @@ public void enforceShouldEmitExpectedMetricsWhenUserAndDeviceHavePrivacyData() {
                 "bidder6", givenEnforcementAction(PrivacyEnforcementAction::setMaskDeviceInfo),
                 "bidder7", givenEnforcementAction(PrivacyEnforcementAction::setRemoveUserFpd)));
 
-        final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithPrivacyData());
-        final Map<String, User> bidderToUser = Map.of(
-                "bidder0", givenUserWithPrivacyData(),
-                "bidder1Alias", givenUserWithPrivacyData(),
-                "bidder2", givenUserWithPrivacyData(),
-                "bidder3", givenUserWithPrivacyData(),
-                "bidder4", givenUserWithPrivacyData(),
-                "bidder5", givenUserWithPrivacyData(),
-                "bidder6", givenUserWithPrivacyData(),
-                "bidder7", givenUserWithPrivacyData());
-        final Set<String> bidders = Set.of();
+        final Device device = givenDeviceWithPrivacyData();
+        final AuctionContext auctionContext = givenAuctionContext(device);
+        final List<BidderPrivacyResult> initialResults = List.of(
+                givenBidderPrivacyResult("bidder0", givenUserWithPrivacyData(), device),
+                givenBidderPrivacyResult("bidder1Alias", givenUserWithPrivacyData(), device),
+                givenBidderPrivacyResult("bidder2", givenUserWithPrivacyData(), device),
+                givenBidderPrivacyResult("bidder3", givenUserWithPrivacyData(), device),
+                givenBidderPrivacyResult("bidder4", givenUserWithPrivacyData(), device),
+                givenBidderPrivacyResult("bidder5", givenUserWithPrivacyData(), device),
+                givenBidderPrivacyResult("bidder6", givenUserWithPrivacyData(), device),
+                givenBidderPrivacyResult("bidder7", givenUserWithPrivacyData(), device));
 
         // when
-        target.enforce(auctionContext, bidderToUser, bidders, aliases);
+        target.enforce(auctionContext, aliases, initialResults);
 
         // then
         verifyMetric("bidder0", false, false, false, false, false, true);
@@ -155,14 +155,13 @@ public void enforceShouldEmitExpectedMetricsWhenUserHasPrivacyData() {
                 "bidder2", givenEnforcementAction(PrivacyEnforcementAction::setRemoveUserFpd)));
 
         final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithNoPrivacyData());
-        final Map<String, User> bidderToUser = Map.of(
-                "bidder0", givenUserWithPrivacyData(),
-                "bidder1", givenUserWithPrivacyData(),
-                "bidder2", givenUserWithPrivacyData());
-        final Set<String> bidders = Set.of();
+        final List<BidderPrivacyResult> initialResults = List.of(
+                givenBidderPrivacyResult("bidder0", givenUserWithPrivacyData(), givenDeviceWithNoPrivacyData()),
+                givenBidderPrivacyResult("bidder1", givenUserWithPrivacyData(), givenDeviceWithNoPrivacyData()),
+                givenBidderPrivacyResult("bidder2", givenUserWithPrivacyData(), givenDeviceWithNoPrivacyData()));
 
         // when
-        target.enforce(auctionContext, bidderToUser, bidders, aliases);
+        target.enforce(auctionContext, aliases, initialResults);
 
         // then
         verifyMetric("bidder0", false, false, true, false, false, false);
@@ -179,17 +178,17 @@ public void enforceShouldEmitExpectedMetricsWhenDeviceHavePrivacyData() {
                 "bidder2", givenEnforcementAction(PrivacyEnforcementAction::setMaskDeviceInfo),
                 "bidder3", givenEnforcementAction(PrivacyEnforcementAction::setRemoveUserFpd)));
 
-        final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithPrivacyData());
-        final Map<String, User> bidderToUser = Map.of(
-                "bidder0", givenUserWithNoPrivacyData(),
-                "bidder1", givenUserWithNoPrivacyData(),
-                "bidder2", givenUserWithNoPrivacyData(),
-                "bidder3", givenUserWithNoPrivacyData(),
-                "bidder4", givenUserWithNoPrivacyData());
-        final Set<String> bidders = Set.of();
+        final Device device = givenDeviceWithPrivacyData();
+        final AuctionContext auctionContext = givenAuctionContext(device);
+        final List<BidderPrivacyResult> initialResults = List.of(
+                givenBidderPrivacyResult("bidder0", givenUserWithNoPrivacyData(), device),
+                givenBidderPrivacyResult("bidder1", givenUserWithNoPrivacyData(), device),
+                givenBidderPrivacyResult("bidder2", givenUserWithNoPrivacyData(), device),
+                givenBidderPrivacyResult("bidder3", givenUserWithNoPrivacyData(), device),
+                givenBidderPrivacyResult("bidder4", givenUserWithNoPrivacyData(), device));
 
         // when
-        target.enforce(auctionContext, bidderToUser, bidders, aliases);
+        target.enforce(auctionContext, aliases, initialResults);
 
         // then
         verifyMetric("bidder0", false, false, true, false, false, true);
@@ -207,14 +206,14 @@ public void enforceShouldEmitExpectedMetricsWhenUserAndDeviceDoNotHavePrivacyDat
                         PrivacyEnforcementAction::setRemoveUserFpd,
                         PrivacyEnforcementAction::setMaskDeviceInfo)));
 
-        final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithNoPrivacyData());
-        final Map<String, User> bidderToUser = Map.of(
-                "bidder0", givenUserWithNoPrivacyData(),
-                "bidder1", givenUserWithNoPrivacyData());
-        final Set<String> bidders = Set.of();
+        final Device device = givenDeviceWithNoPrivacyData();
+        final AuctionContext auctionContext = givenAuctionContext(device);
+        final List<BidderPrivacyResult> initialResults = List.of(
+                givenBidderPrivacyResult("bidder0", givenUserWithNoPrivacyData(), device),
+                givenBidderPrivacyResult("bidder1", givenUserWithNoPrivacyData(), device));
 
         // when
-        target.enforce(auctionContext, bidderToUser, bidders, aliases);
+        target.enforce(auctionContext, aliases, initialResults);
 
         // then
         verifyMetric("bidder0", false, false, false, false, false, false);
@@ -226,12 +225,13 @@ public void enforceShouldEmitPrivacyLmtMetric() {
         // give
         givenPrivacyEnforcementActions(Map.of("bidder", givenEnforcementAction()));
 
-        final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithPrivacyData());
-        final Map<String, User> bidderToUser = Map.of("bidder", givenUserWithPrivacyData());
-        final Set<String> bidders = Set.of();
+        final Device device = givenDeviceWithPrivacyData();
+        final AuctionContext auctionContext = givenAuctionContext(device);
+        final List<BidderPrivacyResult> initialResults = List.of(
+                givenBidderPrivacyResult("bidder", givenUserWithPrivacyData(), device));
 
         // when
-        target.enforce(auctionContext, bidderToUser, bidders, aliases);
+        target.enforce(auctionContext, aliases, initialResults);
 
         // then
         verifyMetric("bidder", false, false, false, false, false, true);
@@ -242,12 +242,13 @@ public void enforceShouldNotEmitPrivacyLmtMetricWhenLmtNot1() {
         // give
         givenPrivacyEnforcementActions(Map.of("bidder", givenEnforcementAction()));
 
-        final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithNoPrivacyData());
-        final Map<String, User> bidderToUser = Map.of("bidder", givenUserWithPrivacyData());
-        final Set<String> bidders = Set.of();
+        final Device device = givenDeviceWithNoPrivacyData();
+        final AuctionContext auctionContext = givenAuctionContext(device);
+        final List<BidderPrivacyResult> initialResults = List.of(
+                givenBidderPrivacyResult("bidder", givenUserWithPrivacyData(), device));
 
         // when
-        target.enforce(auctionContext, bidderToUser, bidders, aliases);
+        target.enforce(auctionContext, aliases, initialResults);
 
         // then
         verifyMetric("bidder", false, false, false, false, false, false);
@@ -259,14 +260,15 @@ public void enforceShouldNotEmitPrivacyLmtMetricWhenLmtNotEnforced() {
         // give
         givenPrivacyEnforcementActions(Map.of("bidder", givenEnforcementAction()));
 
-        final AuctionContext auctionContext = givenAuctionContext(givenDeviceWithPrivacyData());
-        final Map<String, User> bidderToUser = Map.of("bidder", givenUserWithPrivacyData());
-        final Set<String> bidders = Set.of();
+        final Device device = givenDeviceWithPrivacyData();
+        final AuctionContext auctionContext = givenAuctionContext(device);
+        final List<BidderPrivacyResult> initialResults = List.of(
+                givenBidderPrivacyResult("bidder", givenUserWithPrivacyData(), device));
 
         target = new TcfEnforcement(tcfDefinerService, userFpdTcfMask, bidderCatalog, metrics, false);
 
         // when
-        target.enforce(auctionContext, bidderToUser, bidders, aliases);
+        target.enforce(auctionContext, aliases, initialResults);
 
         // then
         verifyMetric("bidder", false, false, false, false, false, false);
@@ -297,15 +299,15 @@ public void enforceShouldMaskUserAndDeviceWhenRestrictionsEnforcedAndLmtNotEnabl
                         PrivacyEnforcementAction::setBlockAnalyticsReport),
                 "bidder2", givenEnforcementAction()));
 
-        final AuctionContext context = givenAuctionContext(givenDeviceWithNoPrivacyData());
-        final Map<String, User> bidderToUser = Map.of(
-                "bidder0", givenUserWithPrivacyData(),
-                "bidder1", givenUserWithPrivacyData(),
-                "bidder2", givenUserWithPrivacyData());
-        final Set<String> bidders = Set.of("bidder0", "bidder1", "bidder2");
+        final Device device = givenDeviceWithNoPrivacyData();
+        final AuctionContext context = givenAuctionContext(device);
+        final List<BidderPrivacyResult> initialResults = List.of(
+                givenBidderPrivacyResult("bidder0", givenUserWithPrivacyData(), device),
+                givenBidderPrivacyResult("bidder1", givenUserWithPrivacyData(), device),
+                givenBidderPrivacyResult("bidder2", givenUserWithPrivacyData(), device));
 
         // when
-        final List<BidderPrivacyResult> result = target.enforce(context, bidderToUser, bidders, aliases).result();
+        final List<BidderPrivacyResult> result = target.enforce(context, aliases, initialResults).result();
 
         // then
         assertThat(result).containsExactlyInAnyOrder(
@@ -343,12 +345,13 @@ public void enforceShouldMaskUserAndDeviceWhenRestrictionsNotEnforcedAndLmtEnabl
 
         givenPrivacyEnforcementActions(Map.of("bidder", givenEnforcementAction()));
 
-        final AuctionContext context = givenAuctionContext(givenDeviceWithPrivacyData());
-        final Map<String, User> bidderToUser = Map.of("bidder", givenUserWithPrivacyData());
-        final Set<String> bidders = Set.of("bidder");
+        final Device device = givenDeviceWithPrivacyData();
+        final AuctionContext context = givenAuctionContext(device);
+        final List<BidderPrivacyResult> initialResults = List.of(
+                givenBidderPrivacyResult("bidder", givenUserWithPrivacyData(), device));
 
         // when
-        final List<BidderPrivacyResult> result = target.enforce(context, bidderToUser, bidders, aliases).result();
+        final List<BidderPrivacyResult> result = target.enforce(context, aliases, initialResults).result();
 
         // then
         assertThat(result).containsExactly(
@@ -378,6 +381,10 @@ private AuctionContext givenAuctionContext(Device device) {
                 .build();
     }
 
+    private static BidderPrivacyResult givenBidderPrivacyResult(String bidder, User user, Device device) {
+        return BidderPrivacyResult.builder().requestBidder(bidder).user(user).device(device).build();
+    }
+
     private static Device givenDeviceWithPrivacyData() {
         return Device.builder()
                 .ip("originalDevice")
diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java
index 7318d7906c2..1e0f63b9192 100644
--- a/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java
+++ b/src/test/java/org/prebid/server/auction/requestfactory/AmpRequestFactoryTest.java
@@ -62,6 +62,8 @@
 import org.prebid.server.proto.openrtb.ext.request.ExtStoredRequest;
 import org.prebid.server.proto.openrtb.ext.request.ExtUser;
 import org.prebid.server.proto.request.Targeting;
+import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.AccountAuctionConfig;
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
@@ -540,6 +542,33 @@ public void shouldReturnBidRequestWithDefaultPriceGranularityIfStoredBidRequestE
                                 ExtGranularityRange.of(BigDecimal.valueOf(20), BigDecimal.valueOf(0.1)))))));
     }
 
+    @Test
+    public void shouldReturnBidRequestWithAccountPriceGranularityIfStoredBidRequestExtTargetingHasNoPriceGranularity() {
+        // given
+        givenBidRequest(
+                builder -> builder
+                        .ext(givenRequestExt(ExtRequestTargeting.builder().includewinners(false).build())),
+                Imp.builder().build());
+
+        given(ortb2RequestFactory.fetchAccount(any())).willReturn(Future.succeededFuture(Account.builder()
+                .auction(AccountAuctionConfig.builder().priceGranularity("low").build())
+                .build()));
+
+        // when
+        final BidRequest request = target.fromRequest(routingContext, 0L).result().getBidRequest();
+
+        // then
+        assertThat(singletonList(request))
+                .extracting(BidRequest::getExt).isNotNull()
+                .extracting(ExtRequest::getPrebid)
+                .extracting(ExtRequestPrebid::getTargeting)
+                .extracting(ExtRequestTargeting::getIncludewinners, ExtRequestTargeting::getPricegranularity)
+                // assert that priceGranularity was set with default value and includeWinners remained unchanged
+                .containsExactly(
+                        tuple(false, mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(
+                                ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.5)))))));
+    }
+
     @Test
     public void shouldReturnBidRequestWithNotChangedExtRequestPrebidTargetingFields() {
         // given
@@ -1248,10 +1277,12 @@ public void shouldReturnBidRequestWithProvidersSettingsContainsAddtlConsentIfPar
         final BidRequest result = target.fromRequest(routingContext, 0L).result().getBidRequest();
 
         // then
+        final ConsentedProvidersSettings settings = ConsentedProvidersSettings.of("someConsent");
         assertThat(result.getUser())
                 .isEqualTo(User.builder()
                         .ext(ExtUser.builder()
-                                .consentedProvidersSettings(ConsentedProvidersSettings.of("someConsent"))
+                                .deprecatedConsentedProvidersSettings(settings)
+                                .consentedProvidersSettings(settings)
                                 .build())
                         .build());
     }
diff --git a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java
index dedb325e241..96889137331 100644
--- a/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java
+++ b/src/test/java/org/prebid/server/auction/requestfactory/AuctionRequestFactoryTest.java
@@ -36,6 +36,9 @@
 import org.prebid.server.auction.model.debug.DebugContext;
 import org.prebid.server.auction.privacy.contextfactory.AuctionPrivacyContextFactory;
 import org.prebid.server.auction.versionconverter.BidRequestOrtbVersionConversionManager;
+import org.prebid.server.bidadjustments.BidAdjustmentsRetriever;
+import org.prebid.server.bidadjustments.model.BidAdjustmentType;
+import org.prebid.server.bidadjustments.model.BidAdjustments;
 import org.prebid.server.cookie.CookieDeprecationService;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.geolocation.model.GeoInfo;
@@ -49,14 +52,18 @@
 import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
 import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidData;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidDataEidPermissions;
 import org.prebid.server.settings.model.Account;
 
 import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
 
 import static java.util.Collections.emptyList;
+import static java.util.Collections.emptyMap;
 import static java.util.Collections.singletonList;
 import static java.util.Collections.singletonMap;
 import static org.apache.commons.lang3.StringUtils.EMPTY;
@@ -102,6 +109,8 @@ public class AuctionRequestFactoryTest extends VertxTest {
     private DebugResolver debugResolver;
     @Mock(strictness = LENIENT)
     private GeoLocationServiceWrapper geoLocationServiceWrapper;
+    @Mock(strictness = LENIENT)
+    private BidAdjustmentsRetriever bidAdjustmentsRetriever;
 
     private AuctionRequestFactory target;
 
@@ -188,6 +197,7 @@ public void setUp() {
                 .will(invocationOnMock -> invocationOnMock.getArgument(0));
         given(geoLocationServiceWrapper.lookup(any()))
                 .willReturn(Future.succeededFuture(GeoInfo.builder().vendor("vendor").build()));
+        given(bidAdjustmentsRetriever.retrieve(any())).willReturn(BidAdjustments.of(emptyMap()));
 
         target = new AuctionRequestFactory(
                 Integer.MAX_VALUE,
@@ -203,7 +213,8 @@ public void setUp() {
                 auctionPrivacyContextFactory,
                 debugResolver,
                 jacksonMapper,
-                geoLocationServiceWrapper);
+                geoLocationServiceWrapper,
+                bidAdjustmentsRetriever);
     }
 
     @Test
@@ -238,7 +249,8 @@ public void shouldReturnFailedFutureIfRequestBodyExceedsMaxRequestSize() {
                 auctionPrivacyContextFactory,
                 debugResolver,
                 jacksonMapper,
-                geoLocationServiceWrapper);
+                geoLocationServiceWrapper,
+                bidAdjustmentsRetriever);
 
         given(routingContext.getBodyAsString()).willReturn("body");
 
@@ -714,6 +726,27 @@ public void shouldReturnPopulatedPrivacyContextAndGetWhenPrivacyEnforcementRetur
         assertThat(result.getGeoInfo()).isEqualTo(geoInfo);
     }
 
+    @Test
+    public void shouldReturnPopulatedBidAdjustments() {
+        // given
+        givenValidBidRequest();
+
+        final BidAdjustments bidAdjustments = BidAdjustments.of(Map.of(
+                "rule1", List.of(
+                        ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build()),
+                "rule2", List.of(
+                        ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.CPM).build(),
+                        ExtRequestBidAdjustmentsRule.builder().adjType(BidAdjustmentType.STATIC).build())));
+
+        given(bidAdjustmentsRetriever.retrieve(any())).willReturn(bidAdjustments);
+
+        // when
+        final AuctionContext result = target.enrichAuctionContext(defaultActionContext).result();
+
+        // then
+        assertThat(result.getBidAdjustments()).isEqualTo(bidAdjustments);
+    }
+
     @Test
     public void shouldConvertBidRequestToInternalOpenRTBVersion() {
         // given
diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java
index 81680a5ac7e..41fa98842e7 100644
--- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java
+++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2ImplicitParametersResolverTest.java
@@ -58,6 +58,8 @@
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidServer;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
 import org.prebid.server.proto.openrtb.ext.request.ExtSite;
+import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.AccountAuctionConfig;
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
@@ -1836,6 +1838,33 @@ public void shouldSetDefaultPriceGranularityIfPriceGranularityAndMediaTypePriceG
                         BigDecimal.valueOf(20), BigDecimal.valueOf(0.1))))));
     }
 
+    @Test
+    public void shouldSetAccountPriceGranularityIfPriceGranularityAndMediaTypePriceGranularityIsMissing() {
+        // given
+        final BidRequest bidRequest = BidRequest.builder()
+                .imp(singletonList(Imp.builder().video(Video.builder().build()).ext(mapper.createObjectNode()).build()))
+                .ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .targeting(ExtRequestTargeting.builder().build())
+                        .build()))
+                .build();
+
+        final AuctionContext givenAuctionContext = auctionContext.with(Account.builder()
+                .auction(AccountAuctionConfig.builder().priceGranularity("low").build())
+                .build());
+
+        // when
+        final BidRequest result = target.resolve(bidRequest, givenAuctionContext, ENDPOINT, false);
+
+        // then
+        assertThat(singletonList(result))
+                .extracting(BidRequest::getExt)
+                .extracting(ExtRequest::getPrebid)
+                .extracting(ExtRequestPrebid::getTargeting)
+                .extracting(ExtRequestTargeting::getPricegranularity)
+                .containsOnly(mapper.valueToTree(ExtPriceGranularity.of(2, singletonList(ExtGranularityRange.of(
+                        BigDecimal.valueOf(5), BigDecimal.valueOf(0.5))))));
+    }
+
     @Test
     public void shouldNotSetDefaultPriceGranularityIfThereIsAMediaTypePriceGranularityForImpType() {
         // given
diff --git a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java
index b38c305cd2b..23d485e99a0 100644
--- a/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java
+++ b/src/test/java/org/prebid/server/auction/requestfactory/Ortb2RequestFactoryTest.java
@@ -14,6 +14,7 @@
 import com.iab.openrtb.request.User;
 import io.vertx.core.Future;
 import io.vertx.core.MultiMap;
+import io.vertx.core.http.HttpMethod;
 import io.vertx.core.http.HttpServerRequest;
 import io.vertx.core.net.impl.SocketAddressImpl;
 import io.vertx.ext.web.RoutingContext;
@@ -39,8 +40,8 @@
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.PreBidException;
 import org.prebid.server.exception.UnauthorizedAccountException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.geolocation.CountryCodeMapper;
 import org.prebid.server.geolocation.model.GeoInfo;
 import org.prebid.server.hooks.execution.HookStageExecutor;
@@ -1079,6 +1080,7 @@ public void executeEntrypointHooksShouldReturnExpectedHttpRequest() {
 
         given(httpServerRequest.headers()).willReturn(MultiMap.caseInsensitiveMultiMap());
         given(httpServerRequest.absoluteURI()).willReturn("absoluteUri");
+        given(httpServerRequest.method()).willReturn(HttpMethod.POST);
         given(httpServerRequest.scheme()).willReturn("https");
         given(httpServerRequest.remoteAddress()).willReturn(new SocketAddressImpl(1234, "host"));
 
@@ -1107,6 +1109,7 @@ public void executeEntrypointHooksShouldReturnExpectedHttpRequest() {
         // then
         final HttpRequestContext httpRequest = result.result();
         assertThat(httpRequest.getAbsoluteUri()).isEqualTo("absoluteUri");
+        assertThat(httpRequest.getHttpMethod()).isEqualTo(HttpMethod.POST);
         assertThat(httpRequest.getQueryParams()).isSameAs(updatedQueryParam);
         assertThat(httpRequest.getHeaders()).isSameAs(headerParams);
         assertThat(httpRequest.getBody()).isEqualTo("{\"app\":{\"bundle\":\"org.company.application\"}}");
diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java
new file mode 100644
index 00000000000..0c98ff6af3b
--- /dev/null
+++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentRulesValidatorTest.java
@@ -0,0 +1,306 @@
+package org.prebid.server.bidadjustments;
+
+import org.junit.jupiter.api.Test;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule;
+import org.prebid.server.validation.ValidationException;
+
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM;
+import static org.prebid.server.bidadjustments.model.BidAdjustmentType.MULTIPLIER;
+import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC;
+import static org.prebid.server.bidadjustments.model.BidAdjustmentType.UNKNOWN;
+
+public class BidAdjustmentRulesValidatorTest {
+
+    @Test
+    public void validateShouldDoNothingWhenBidAdjustmentsIsNull() throws ValidationException {
+        // when & then
+        BidAdjustmentRulesValidator.validate(null);
+    }
+
+    @Test
+    public void validateShouldDoNothingWhenMediatypesIsEmpty() throws ValidationException {
+        // when & then
+        BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder().build());
+    }
+
+    @Test
+    public void validateShouldSkipMediatypeValidationWhenMediatypesIsNotSupported() throws ValidationException {
+        // given
+        final ExtRequestBidAdjustmentsRule invalidRule = ExtRequestBidAdjustmentsRule.builder()
+                .value(new BigDecimal("-999"))
+                .build();
+
+        // when & then
+        BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("invalid", Map.of("bidderName", Map.of("*", List.of(invalidRule)))))
+                .build());
+    }
+
+    @Test
+    public void validateShouldFailWhenBiddersAreAbsent() {
+        // given
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("banner", Collections.emptyMap()))
+                .build();
+
+        // when & then
+        assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments))
+                .isInstanceOf(ValidationException.class)
+                .hasMessage("no bidders found in banner");
+    }
+
+    @Test
+    public void validateShouldFailWhenDealsAreAbsent() {
+        // given
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("banner", Map.of("bidderName", Collections.emptyMap())))
+                .build();
+
+        // when & then
+        assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments))
+                .isInstanceOf(ValidationException.class)
+                .hasMessage("no deals found in banner.bidderName");
+    }
+
+    @Test
+    public void validateShouldFailWhenRulesIsEmpty() {
+        // given
+        final Map<String, List<ExtRequestBidAdjustmentsRule>> rules = new HashMap<>();
+        rules.put("*", null);
+
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("banner", Map.of("bidderName", rules)))
+                .build();
+
+        // when & then
+        assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments))
+                .isInstanceOf(ValidationException.class)
+                .hasMessage("no bid adjustment rules found in banner.bidderName.*");
+    }
+
+    @Test
+    public void validateShouldDoNothingWhenRulesAreEmpty() throws ValidationException {
+        // when & then
+        BidAdjustmentRulesValidator.validate(ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("video_instream", Map.of("bidderName", Map.of("*", List.of()))))
+                .build());
+    }
+
+    @Test
+    public void validateShouldFailWhenRuleHasUnknownType() {
+        // given
+        final Map<String, List<ExtRequestBidAdjustmentsRule>> rules = new HashMap<>();
+        rules.put("*", List.of(ExtRequestBidAdjustmentsRule.builder()
+                .adjType(UNKNOWN)
+                .value(BigDecimal.ONE)
+                .currency("USD")
+                .build()));
+
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("banner", Map.of("bidderName", rules)))
+                .build();
+
+        // when & then
+        assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments))
+                .isInstanceOf(ValidationException.class)
+                .hasMessage("the found rule [adjtype=UNKNOWN, value=1, currency=USD] "
+                        + "in banner.bidderName.* is invalid");
+    }
+
+    @Test
+    public void validateShouldFailWhenCpmRuleDoesNotHaveCurrency() {
+        // given
+        final Map<String, List<ExtRequestBidAdjustmentsRule>> rules = new HashMap<>();
+        rules.put("*", List.of(givenCpm("1", "USD"), givenCpm("1", null)));
+
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("banner", Map.of("bidderName", rules)))
+                .build();
+
+        // when & then
+        assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments))
+                .isInstanceOf(ValidationException.class)
+                .hasMessage("the found rule [adjtype=CPM, value=1, currency=null] in banner.bidderName.* is invalid");
+    }
+
+    @Test
+    public void validateShouldFailWhenCpmRuleDoesHasNegativeValue() {
+        // given
+        final Map<String, List<ExtRequestBidAdjustmentsRule>> rules = new HashMap<>();
+        rules.put("*", List.of(givenCpm("0", "USD"), givenCpm("-1", "USD")));
+
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("banner", Map.of("bidderName", rules)))
+                .build();
+
+        // when & then
+        assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments))
+                .isInstanceOf(ValidationException.class)
+                .hasMessage("the found rule [adjtype=CPM, value=-1, currency=USD] in banner.bidderName.* is invalid");
+    }
+
+    @Test
+    public void validateShouldFailWhenCpmRuleDoesHasValueMoreThanMaxInt() {
+        // given
+        final Map<String, List<ExtRequestBidAdjustmentsRule>> rules = new HashMap<>();
+        rules.put("*", List.of(givenCpm("0", "USD"), givenCpm("2147483647", "USD")));
+
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("banner", Map.of("bidderName", rules)))
+                .build();
+
+        // when & then
+        assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments))
+                .isInstanceOf(ValidationException.class)
+                .hasMessage("the found rule [adjtype=CPM, value=2147483647, currency=USD] "
+                        + "in banner.bidderName.* is invalid");
+    }
+
+    @Test
+    public void validateShouldFailWhenStaticRuleDoesNotHaveCurrency() {
+        // given
+        final Map<String, List<ExtRequestBidAdjustmentsRule>> rules = new HashMap<>();
+        rules.put("*", List.of(givenStatic("1", "USD"), givenStatic("1", null)));
+
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("banner", Map.of("bidderName", rules)))
+                .build();
+
+        // when & then
+        assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments))
+                .isInstanceOf(ValidationException.class)
+                .hasMessage("the found rule [adjtype=STATIC, value=1, currency=null] "
+                        + "in banner.bidderName.* is invalid");
+    }
+
+    @Test
+    public void validateShouldFailWhenStaticRuleDoesHasNegativeValue() {
+        // given
+        final Map<String, List<ExtRequestBidAdjustmentsRule>> rules = new HashMap<>();
+        rules.put("*", List.of(givenStatic("0", "USD"), givenStatic("-1", "USD")));
+
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("banner", Map.of("bidderName", rules)))
+                .build();
+
+        // when & then
+        assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments))
+                .isInstanceOf(ValidationException.class)
+                .hasMessage("the found rule [adjtype=STATIC, value=-1, currency=USD] "
+                        + "in banner.bidderName.* is invalid");
+    }
+
+    @Test
+    public void validateShouldFailWhenStaticRuleDoesHasValueMoreThanMaxInt() {
+        // given
+        final Map<String, List<ExtRequestBidAdjustmentsRule>> rules = new HashMap<>();
+        rules.put("*", List.of(givenStatic("0", "USD"), givenStatic("2147483647", "USD")));
+
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("banner", Map.of("bidderName", rules)))
+                .build();
+
+        // when & then
+        assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments))
+                .isInstanceOf(ValidationException.class)
+                .hasMessage("the found rule [adjtype=STATIC, value=2147483647, currency=USD] "
+                        + "in banner.bidderName.* is invalid");
+    }
+
+    @Test
+    public void validateShouldFailWhenMultiplierRuleDoesHasNegativeValue() {
+        // given
+        final Map<String, List<ExtRequestBidAdjustmentsRule>> rules = new HashMap<>();
+        rules.put("*", List.of(givenMultiplier("0"), givenMultiplier("-1")));
+
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("banner", Map.of("bidderName", rules)))
+                .build();
+
+        // when & then
+        assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments))
+                .isInstanceOf(ValidationException.class)
+                .hasMessage("the found rule [adjtype=MULTIPLIER, value=-1, currency=null] "
+                        + "in banner.bidderName.* is invalid");
+    }
+
+    @Test
+    public void validateShouldFailWhenMultiplierRuleDoesHasValueMoreThan100() {
+        // given
+        final Map<String, List<ExtRequestBidAdjustmentsRule>> rules = new HashMap<>();
+        rules.put("*", List.of(givenMultiplier("0"), givenMultiplier("100")));
+
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of("banner", Map.of("bidderName", rules)))
+                .build();
+
+        // when & then
+        assertThatThrownBy(() -> BidAdjustmentRulesValidator.validate(givenBidAdjustments))
+                .isInstanceOf(ValidationException.class)
+                .hasMessage("the found rule [adjtype=MULTIPLIER, value=100, currency=null] "
+                        + "in banner.bidderName.* is invalid");
+    }
+
+    @Test
+    public void validateShouldDoNothingWhenAllRulesAreValid() throws ValidationException {
+        // given
+        final List<ExtRequestBidAdjustmentsRule> givenRules = List.of(
+                givenMultiplier("1"),
+                givenCpm("2", "USD"),
+                givenStatic("3", "EUR"));
+
+        final Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>> givenRulesMap = Map.of(
+                "bidderName",
+                Map.of("dealId", givenRules));
+
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of(
+                        "audio", givenRulesMap,
+                        "native", givenRulesMap,
+                        "video-instream", givenRulesMap,
+                        "video-outstream", givenRulesMap,
+                        "banner", givenRulesMap,
+                        "video", givenRulesMap,
+                        "unknown", givenRulesMap,
+                        "*", Map.of(
+                                "*", Map.of("*", givenRules),
+                                "bidderName", Map.of(
+                                        "*", givenRules,
+                                        "dealId", givenRules))))
+                .build();
+
+        //when & then
+        BidAdjustmentRulesValidator.validate(givenBidAdjustments);
+    }
+
+    private static ExtRequestBidAdjustmentsRule givenStatic(String value, String currency) {
+        return ExtRequestBidAdjustmentsRule.builder()
+                .adjType(STATIC)
+                .currency(currency)
+                .value(new BigDecimal(value))
+                .build();
+    }
+
+    private static ExtRequestBidAdjustmentsRule givenCpm(String value, String currency) {
+        return ExtRequestBidAdjustmentsRule.builder()
+                .adjType(CPM)
+                .currency(currency)
+                .value(new BigDecimal(value))
+                .build();
+    }
+
+    private static ExtRequestBidAdjustmentsRule givenMultiplier(String value) {
+        return ExtRequestBidAdjustmentsRule.builder()
+                .adjType(MULTIPLIER)
+                .value(new BigDecimal(value))
+                .build();
+    }
+}
diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java
new file mode 100644
index 00000000000..2affb167eef
--- /dev/null
+++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsProcessorTest.java
@@ -0,0 +1,878 @@
+package org.prebid.server.bidadjustments;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import com.iab.openrtb.request.Video;
+import com.iab.openrtb.response.Bid;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.prebid.server.VertxTest;
+import org.prebid.server.auction.adjustment.BidAdjustmentFactorResolver;
+import org.prebid.server.auction.model.AuctionParticipation;
+import org.prebid.server.auction.model.BidderRequest;
+import org.prebid.server.auction.model.BidderResponse;
+import org.prebid.server.bidadjustments.model.BidAdjustments;
+import org.prebid.server.bidder.model.BidderBid;
+import org.prebid.server.bidder.model.BidderError;
+import org.prebid.server.bidder.model.BidderSeatBid;
+import org.prebid.server.bidder.model.Price;
+import org.prebid.server.currency.CurrencyConversionService;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentFactors;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestCurrency;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
+import org.prebid.server.proto.openrtb.ext.request.ImpMediaType;
+import org.prebid.server.proto.openrtb.ext.response.BidType;
+
+import java.math.BigDecimal;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.function.UnaryOperator;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static java.util.function.UnaryOperator.identity;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.audio;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class BidAdjustmentsProcessorTest extends VertxTest {
+
+    @Mock
+    private CurrencyConversionService currencyService;
+    @Mock
+    private BidAdjustmentFactorResolver bidAdjustmentFactorResolver;
+    @Mock
+    private BidAdjustmentsResolver bidAdjustmentsResolver;
+
+    private BidAdjustmentsProcessor target;
+
+    @BeforeEach
+    public void before() {
+        given(currencyService.convertCurrency(any(), any(), any(), any()))
+                .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
+        given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any()))
+                .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
+
+        target = new BidAdjustmentsProcessor(
+                currencyService,
+                bidAdjustmentFactorResolver,
+                bidAdjustmentsResolver,
+                jacksonMapper);
+    }
+
+    @Test
+    public void shouldReturnBidsWithUpdatedPriceCurrencyConversionAndAdjusted() {
+        // given
+        final BidderResponse bidderResponse = givenBidderResponse(
+                Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).dealid("dealId").build());
+        final BidRequest bidRequest = givenBidRequest(
+                singletonList(givenImp(singletonMap("bidder", 2), identity())), identity());
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        final Price adjustedPrice = Price.of("EUR", BigDecimal.valueOf(5.0));
+        given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())).willReturn(adjustedPrice);
+
+        final BigDecimal expectedPrice = new BigDecimal("123.5");
+        given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("UAH"))).willReturn(expectedPrice);
+
+        // when
+        final AuctionParticipation result = target.enrichWithAdjustedBids(
+                auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        assertThat(result.getBidderResponse().getSeatBid().getBids())
+                .extracting(BidderBid::getBidCurrency, bidderBid -> bidderBid.getBid().getPrice())
+                .containsExactly(tuple("UAH", expectedPrice));
+
+        verify(bidAdjustmentsResolver).resolve(
+                eq(Price.of("USD", BigDecimal.valueOf(2.0))),
+                eq(bidRequest),
+                eq(givenBidAdjustments()),
+                eq(ImpMediaType.banner),
+                eq("bidder"),
+                eq("dealId"));
+    }
+
+    @Test
+    public void shouldReturnSameBidPriceIfNoChangesAppliedToBidPrice() {
+        // given
+        final BidderResponse bidderResponse = givenBidderResponse(
+                Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build());
+        final BidRequest bidRequest = givenBidRequest(
+                singletonList(givenImp(singletonMap("bidder", 2), identity())), identity());
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        given(currencyService.convertCurrency(any(), any(), any(), any()))
+                .willAnswer(invocation -> invocation.getArgument(0));
+        given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any()))
+                .willAnswer(invocationOnMock -> invocationOnMock.getArgument(0));
+
+        // when
+        final AuctionParticipation result = target.enrichWithAdjustedBids(
+                auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        assertThat(result.getBidderResponse().getSeatBid().getBids())
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getPrice)
+                .containsExactly(BigDecimal.valueOf(2.0));
+    }
+
+    @Test
+    public void shouldDropBidIfPrebidExceptionWasThrownDuringCurrencyConversion() {
+        // given
+        final BidderResponse bidderResponse = givenBidderResponse(
+                Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build());
+        final BidRequest bidRequest = givenBidRequest(
+                singletonList(givenImp(singletonMap("bidder", 2), identity())), identity());
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        given(currencyService.convertCurrency(any(), any(), any(), any()))
+                .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD"));
+
+        // when
+        final AuctionParticipation result = target.enrichWithAdjustedBids(
+                auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        final BidderError expectedError = BidderError.generic(
+                "Unable to convert bid currency CUR to desired ad server currency USD");
+        final BidderSeatBid firstSeatBid = result.getBidderResponse().getSeatBid();
+        assertThat(firstSeatBid.getBids()).isEmpty();
+        assertThat(firstSeatBid.getErrors()).containsOnly(expectedError);
+    }
+
+    @Test
+    public void shouldDropBidIfPrebidExceptionWasThrownDuringBidAdjustmentResolving() {
+        // given
+        final BidderResponse bidderResponse = givenBidderResponse(
+                Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).build());
+        final BidRequest bidRequest = givenBidRequest(
+                singletonList(givenImp(singletonMap("bidder", 2), identity())), identity());
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        given(currencyService.convertCurrency(any(), any(), any(), any()))
+                .willAnswer(invocation -> invocation.getArgument(0));
+        given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any()))
+                .willThrow(new PreBidException("Unable to convert bid currency CUR to desired ad server currency USD"));
+
+        // when
+        final AuctionParticipation result = target.enrichWithAdjustedBids(
+                auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        final BidderError expectedError = BidderError.generic(
+                "Unable to convert bid currency CUR to desired ad server currency USD");
+        final BidderSeatBid firstSeatBid = result.getBidderResponse().getSeatBid();
+        assertThat(firstSeatBid.getBids()).isEmpty();
+        assertThat(firstSeatBid.getErrors()).containsOnly(expectedError);
+    }
+
+    @Test
+    public void shouldUpdateBidPriceWithCurrencyConversionAndPriceAdjustmentFactorAndBidAdjustments() {
+        // given
+        final BidderResponse bidderResponse = givenBidderResponse(
+                Bid.builder().impid("impId").price(BigDecimal.valueOf(2.0)).dealid("dealId").build());
+
+        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build();
+        givenAdjustments.addFactor("bidder", BigDecimal.TEN);
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
+                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .aliases(emptyMap())
+                        .bidadjustmentfactors(givenAdjustments)
+                        .auctiontimestamp(1000L)
+                        .build())));
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder"))
+                .willReturn(BigDecimal.TEN);
+        final Price adjustedPrice = Price.of("EUR", BigDecimal.valueOf(5.0));
+        given(bidAdjustmentsResolver.resolve(any(), any(), any(), any(), any(), any())).willReturn(adjustedPrice);
+        final BigDecimal expectedPrice = new BigDecimal("123.5");
+        given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("UAH"))).willReturn(expectedPrice);
+
+        // when
+        final AuctionParticipation result = target.enrichWithAdjustedBids(
+                auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        final BidderSeatBid seatBid = result.getBidderResponse().getSeatBid();
+        assertThat(seatBid.getBids())
+                .extracting(BidderBid::getBidCurrency, bidderBid -> bidderBid.getBid().getPrice())
+                .containsExactly(tuple("UAH", expectedPrice));
+        assertThat(seatBid.getErrors()).isEmpty();
+
+        verify(bidAdjustmentsResolver).resolve(
+                eq(Price.of("USD", BigDecimal.valueOf(20.0))),
+                eq(bidRequest),
+                eq(givenBidAdjustments()),
+                eq(ImpMediaType.banner),
+                eq("bidder"),
+                eq("dealId"));
+    }
+
+    @Test
+    public void shouldUpdatePriceForOneBidAndDropAnotherIfPrebidExceptionHappensForSecondBid() {
+        // given
+        final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0);
+        final BigDecimal secondBidderPrice = BigDecimal.valueOf(3.0);
+        final BidderResponse bidderResponse = BidderResponse.of(
+                "bidder",
+                BidderSeatBid.builder()
+                        .bids(List.of(
+                                givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "CUR1"),
+                                givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "CUR2")
+                        ))
+                        .build(),
+                1);
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
+                identity());
+
+        final BigDecimal updatedPrice = BigDecimal.valueOf(10.0);
+        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice)
+                .willThrow(
+                        new PreBidException("Unable to convert bid currency CUR2 to desired ad server currency USD"));
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        // when
+        final AuctionParticipation result = target
+                .enrichWithAdjustedBids(auctionParticipation, bidRequest, null);
+
+        // then
+        verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("CUR1"), any());
+        verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("CUR2"), any());
+
+        final ObjectNode expectedBidExt = mapper.createObjectNode();
+        expectedBidExt.put("origbidcpm", new BigDecimal("2.0"));
+        expectedBidExt.put("origbidcur", "CUR1");
+        final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build();
+        final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "UAH");
+        final BidderError expectedError =
+                BidderError.generic("Unable to convert bid currency CUR2 to desired ad server currency USD");
+
+        final BidderSeatBid firstSeatBid = result.getBidderResponse().getSeatBid();
+        assertThat(firstSeatBid.getBids()).containsOnly(expectedBidderBid);
+        assertThat(firstSeatBid.getErrors()).containsOnly(expectedError);
+    }
+
+    @Test
+    public void shouldRespondWithOneBidAndErrorWhenBidResponseContainsOneUnsupportedCurrency() {
+        // given
+        final BigDecimal firstBidderPrice = BigDecimal.valueOf(2.0);
+        final BigDecimal secondBidderPrice = BigDecimal.valueOf(10.0);
+        final BidderResponse bidderResponse = BidderResponse.of(
+                "bidder",
+                BidderSeatBid.builder()
+                        .bids(List.of(
+                                givenBidderBid(Bid.builder().impid("impId1").price(firstBidderPrice).build(), "USD"),
+                                givenBidderBid(Bid.builder().impid("impId2").price(secondBidderPrice).build(), "EUR")
+                        ))
+                        .build(),
+                1);
+
+        final BidRequest bidRequest = BidRequest.builder()
+                .cur(singletonList("CUR"))
+                .imp(singletonList(givenImp(doubleMap("bidder1", 2, "bidder2", 3),
+                        identity()))).build();
+
+        final BigDecimal updatedPrice = BigDecimal.valueOf(20);
+        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice);
+        given(currencyService.convertCurrency(any(), any(), eq("EUR"), eq("CUR")))
+                .willThrow(new PreBidException("Unable to convert bid currency EUR to desired ad server currency CUR"));
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        // when
+        final AuctionParticipation result = target.enrichWithAdjustedBids(
+                auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        verify(currencyService).convertCurrency(eq(firstBidderPrice), eq(bidRequest), eq("USD"), eq("CUR"));
+        verify(currencyService).convertCurrency(eq(secondBidderPrice), eq(bidRequest), eq("EUR"), eq("CUR"));
+
+        final ObjectNode expectedBidExt = mapper.createObjectNode();
+        expectedBidExt.put("origbidcpm", new BigDecimal("2.0"));
+        expectedBidExt.put("origbidcur", "USD");
+        final Bid expectedBid = Bid.builder().impid("impId1").price(updatedPrice).ext(expectedBidExt).build();
+        final BidderBid expectedBidderBid = BidderBid.of(expectedBid, banner, "CUR");
+        assertThat(result.getBidderResponse().getSeatBid().getBids()).containsOnly(expectedBidderBid);
+
+        final BidderError expectedError = BidderError.generic(
+                "Unable to convert bid currency EUR to desired ad server currency CUR");
+        assertThat(result.getBidderResponse().getSeatBid().getErrors()).containsOnly(expectedError);
+    }
+
+    @Test
+    public void shouldUpdateBidPriceWithCurrencyConversionForMultipleBid() {
+        // given
+        final BigDecimal bidder1Price = BigDecimal.valueOf(1.5);
+        final BigDecimal bidder2Price = BigDecimal.valueOf(2);
+        final BigDecimal bidder3Price = BigDecimal.valueOf(3);
+        final BidderResponse bidderResponse = BidderResponse.of(
+                "bidder",
+                BidderSeatBid.builder()
+                        .bids(List.of(
+                                givenBidderBid(Bid.builder().impid("impId1").price(bidder1Price).build(), "EUR"),
+                                givenBidderBid(Bid.builder().impid("impId2").price(bidder2Price).build(), "GBP"),
+                                givenBidderBid(Bid.builder().impid("impId3").price(bidder3Price).build(), "USD")
+                        ))
+                        .build(),
+                1);
+
+        final BidRequest bidRequest = givenBidRequest(
+                singletonList(givenImp(Map.of("bidder1", 1), identity())),
+                builder -> builder.cur(singletonList("USD")));
+
+        final BigDecimal updatedPrice = BigDecimal.valueOf(10.0);
+        given(currencyService.convertCurrency(any(), any(), any(), any())).willReturn(updatedPrice);
+        given(currencyService.convertCurrency(any(), any(), eq("USD"), any())).willReturn(bidder3Price);
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        // when
+        final AuctionParticipation result = target.enrichWithAdjustedBids(
+                auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        verify(currencyService).convertCurrency(eq(bidder1Price), eq(bidRequest), eq("EUR"), eq("USD"));
+        verify(currencyService).convertCurrency(eq(bidder2Price), eq(bidRequest), eq("GBP"), eq("USD"));
+        verify(currencyService).convertCurrency(eq(bidder3Price), eq(bidRequest), eq("USD"), eq("USD"));
+        verifyNoMoreInteractions(currencyService);
+
+        assertThat(result.getBidderResponse().getSeatBid().getBids())
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getPrice)
+                .containsOnly(bidder3Price, updatedPrice, updatedPrice);
+
+        verify(bidAdjustmentsResolver, times(3)).resolve(any(), any(), any(), any(), any(), any());
+    }
+
+    @Test
+    public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentFactorPresent() {
+        // given
+        final BidderResponse bidderResponse = givenBidderResponse(
+                Bid.builder().impid("impId").price(BigDecimal.valueOf(2)).dealid("dealId").build());
+
+        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build();
+        givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468));
+        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder"))
+                .willReturn(BigDecimal.valueOf(2.468));
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
+                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .aliases(emptyMap())
+                        .bidadjustmentfactors(givenAdjustments)
+                        .auctiontimestamp(1000L)
+                        .build())));
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        // when
+        final AuctionParticipation result = target.enrichWithAdjustedBids(
+                auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        assertThat(result.getBidderResponse().getSeatBid().getBids())
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getPrice)
+                .containsExactly(BigDecimal.valueOf(4.936));
+
+        verify(bidAdjustmentsResolver).resolve(
+                eq(Price.of("USD", BigDecimal.valueOf(4.936))),
+                eq(bidRequest),
+                eq(givenBidAdjustments()),
+                eq(ImpMediaType.banner),
+                eq("bidder"),
+                eq("dealId"));
+    }
+
+    @Test
+    public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlacementEqualsOne() {
+        // given
+        final BidderResponse bidderResponse = BidderResponse.of(
+                "bidder",
+                BidderSeatBid.builder()
+                        .bids(List.of(
+                                givenBidderBid(Bid.builder()
+                                                .impid("123")
+                                                .price(BigDecimal.valueOf(2))
+                                                .dealid("dealId")
+                                                .build(),
+                                        "USD", video)))
+                        .build(),
+                1);
+
+        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
+                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video,
+                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
+                .build();
+        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder"))
+                .willReturn(BigDecimal.valueOf(3.456));
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder ->
+                        impBuilder.id("123").video(Video.builder().placement(1).build()))),
+                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .aliases(emptyMap())
+                        .bidadjustmentfactors(givenAdjustments)
+                        .auctiontimestamp(1000L)
+                        .build())));
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        // when
+        final AuctionParticipation result = target.enrichWithAdjustedBids(
+                auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        assertThat(result.getBidderResponse().getSeatBid().getBids())
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getPrice)
+                .containsExactly(BigDecimal.valueOf(6.912));
+
+        verify(bidAdjustmentsResolver).resolve(
+                eq(Price.of("USD", BigDecimal.valueOf(6.912))),
+                eq(bidRequest),
+                eq(givenBidAdjustments()),
+                eq(ImpMediaType.video_instream),
+                eq("bidder"),
+                eq("dealId"));
+    }
+
+    @Test
+    public void shouldReturnBidsWithAdjustedPricesWithVideoInstreamMediaTypeIfVideoPlcmtEqualsOne() {
+        // given
+        final BidderResponse bidderResponse = BidderResponse.of(
+                "bidder",
+                BidderSeatBid.builder()
+                        .bids(List.of(
+                                givenBidderBid(Bid.builder()
+                                                .impid("123")
+                                                .price(BigDecimal.valueOf(2))
+                                                .dealid("dealId")
+                                                .build(),
+                                        "USD", video)))
+                        .build(),
+                1);
+
+        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
+                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video,
+                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
+                .build();
+        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video, givenAdjustments, "bidder"))
+                .willReturn(BigDecimal.valueOf(3.456));
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder ->
+                        impBuilder.id("123").video(Video.builder().plcmt(1).build()))),
+                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .aliases(emptyMap())
+                        .bidadjustmentfactors(givenAdjustments)
+                        .auctiontimestamp(1000L)
+                        .build())));
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        // when
+        final AuctionParticipation result = target.enrichWithAdjustedBids(
+                auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        assertThat(result.getBidderResponse().getSeatBid().getBids())
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getPrice)
+                .containsExactly(BigDecimal.valueOf(6.912));
+
+        verify(bidAdjustmentsResolver).resolve(
+                eq(Price.of("USD", BigDecimal.valueOf(6.912))),
+                eq(bidRequest),
+                eq(givenBidAdjustments()),
+                eq(ImpMediaType.video_instream),
+                eq("bidder"),
+                eq("dealId"));
+    }
+
+    @Test
+    public void shouldReturnBidsWithAdjustedPricesWithVideoOutstreamMediaTypeIfVideoPlacementAndPlcmtIsMissing() {
+        // given
+        final BidderResponse bidderResponse = BidderResponse.of(
+                "bidder",
+                BidderSeatBid.builder()
+                        .bids(List.of(
+                                givenBidderBid(Bid.builder()
+                                                .impid("123")
+                                                .price(BigDecimal.valueOf(2))
+                                                .dealid("dealId")
+                                                .build(),
+                                        "USD", video)))
+                        .build(),
+                1);
+
+        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
+                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video,
+                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
+                .build();
+        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.video_outstream, givenAdjustments, "bidder"))
+                .willReturn(BigDecimal.valueOf(3.456));
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder ->
+                        impBuilder.id("123").video(Video.builder().build()))),
+                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .aliases(emptyMap())
+                        .bidadjustmentfactors(givenAdjustments)
+                        .auctiontimestamp(1000L)
+                        .build())));
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+        // when
+        final AuctionParticipation result = target
+                .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        assertThat(result.getBidderResponse().getSeatBid().getBids())
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getPrice)
+                .containsExactly(BigDecimal.valueOf(6.912));
+
+        verify(bidAdjustmentsResolver).resolve(
+                eq(Price.of("USD", BigDecimal.valueOf(6.912))),
+                eq(bidRequest),
+                eq(givenBidAdjustments()),
+                eq(ImpMediaType.video_outstream),
+                eq("bidder"),
+                eq("dealId"));
+    }
+
+    @Test
+    public void shouldReturnBidAdjustmentMediaTypeVideoOutstreamIfImpIdNotEqualBidImpId() {
+        // given
+        final BidderResponse bidderResponse = BidderResponse.of(
+                "bidder",
+                BidderSeatBid.builder()
+                        .bids(List.of(
+                                givenBidderBid(Bid.builder()
+                                                .impid("125")
+                                                .price(BigDecimal.valueOf(2))
+                                                .dealid("dealId")
+                                                .build(),
+                                        "USD", video)))
+                        .build(),
+                1);
+
+        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
+                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video,
+                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
+                .build();
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder ->
+                        impBuilder.id("123").video(Video.builder().placement(10).build()))),
+                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .aliases(emptyMap())
+                        .bidadjustmentfactors(givenAdjustments)
+                        .auctiontimestamp(1000L)
+                        .build())));
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        // when
+        final AuctionParticipation result = target
+                .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        assertThat(result.getBidderResponse().getSeatBid().getBids())
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getPrice)
+                .containsExactly(BigDecimal.valueOf(2));
+
+        verify(bidAdjustmentFactorResolver).resolve(ImpMediaType.video_outstream, givenAdjustments, "bidder");
+        verify(bidAdjustmentsResolver).resolve(
+                eq(Price.of("USD", BigDecimal.valueOf(2))),
+                eq(bidRequest),
+                eq(givenBidAdjustments()),
+                eq(ImpMediaType.video_outstream),
+                eq("bidder"),
+                eq("dealId"));
+    }
+
+    @Test
+    public void shouldReturnBidAdjustmentMediaTypeVideoOutStreamIfImpIdEqualBidImpIdAndPopulatedPlacement() {
+        // given
+        final BidderResponse bidderResponse = BidderResponse.of(
+                "bidder",
+                BidderSeatBid.builder()
+                        .bids(List.of(
+                                givenBidderBid(Bid.builder()
+                                                .impid("123")
+                                                .price(BigDecimal.valueOf(2))
+                                                .dealid("dealId")
+                                                .build(),
+                                        "USD", video)))
+                        .build(),
+                1);
+
+        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
+                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.video,
+                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
+                .build();
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), impBuilder ->
+                        impBuilder.id("123").video(Video.builder().placement(10).build()))),
+                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .aliases(emptyMap())
+                        .bidadjustmentfactors(givenAdjustments)
+                        .auctiontimestamp(1000L)
+                        .build())));
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        // when
+        final AuctionParticipation result = target
+                .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        assertThat(result.getBidderResponse().getSeatBid().getBids())
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getPrice)
+                .containsExactly(BigDecimal.valueOf(2));
+
+        verify(bidAdjustmentsResolver).resolve(
+                eq(Price.of("USD", BigDecimal.valueOf(2))),
+                eq(bidRequest),
+                eq(givenBidAdjustments()),
+                eq(ImpMediaType.video_outstream),
+                eq("bidder"),
+                eq("dealId"));
+    }
+
+    @Test
+    public void shouldReturnBidsWithAdjustedPricesWhenAdjustmentMediaFactorPresent() {
+        // given
+        final BidderResponse bidderResponse = BidderResponse.of(
+                "bidder",
+                BidderSeatBid.builder()
+                        .bids(List.of(
+                                givenBidderBid(Bid.builder().price(BigDecimal.valueOf(2)).build(), "USD", banner),
+                                givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", xNative),
+                                givenBidderBid(Bid.builder().price(BigDecimal.ONE).build(), "USD", audio)))
+                        .build(),
+                1);
+
+        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
+                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner,
+                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
+                .build();
+        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder"))
+                .willReturn(BigDecimal.valueOf(3.456));
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
+                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .aliases(emptyMap())
+                        .bidadjustmentfactors(givenAdjustments)
+                        .auctiontimestamp(1000L)
+                        .build())));
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        // when
+        final AuctionParticipation result = target.enrichWithAdjustedBids(
+                auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        assertThat(result.getBidderResponse().getSeatBid().getBids())
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getPrice)
+                .containsExactly(BigDecimal.valueOf(6.912), BigDecimal.valueOf(1), BigDecimal.valueOf(1));
+
+        verify(bidAdjustmentsResolver, times(3))
+                .resolve(any(), any(), any(), any(), any(), any());
+    }
+
+    @Test
+    public void shouldAdjustPriceWithPriorityForMediaTypeAdjustment() {
+        // given
+        final BidderResponse bidderResponse = BidderResponse.of(
+                "bidder",
+                BidderSeatBid.builder()
+                        .bids(List.of(
+                                givenBidderBid(Bid.builder()
+                                                .impid("123")
+                                                .price(BigDecimal.valueOf(2))
+                                                .dealid("dealId")
+                                                .build(),
+                                        "USD")))
+                        .build(),
+                1);
+
+        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder()
+                .mediatypes(new EnumMap<>(singletonMap(ImpMediaType.banner,
+                        singletonMap("bidder", BigDecimal.valueOf(3.456)))))
+                .build();
+        givenAdjustments.addFactor("bidder", BigDecimal.valueOf(2.468));
+        given(bidAdjustmentFactorResolver.resolve(ImpMediaType.banner, givenAdjustments, "bidder"))
+                .willReturn(BigDecimal.valueOf(3.456));
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
+                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .aliases(emptyMap())
+                        .bidadjustmentfactors(givenAdjustments)
+                        .auctiontimestamp(1000L)
+                        .build())));
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        // when
+        final AuctionParticipation result = target.enrichWithAdjustedBids(
+                auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        assertThat(result.getBidderResponse().getSeatBid().getBids())
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getPrice)
+                .containsOnly(BigDecimal.valueOf(6.912));
+
+        verify(bidAdjustmentsResolver).resolve(
+                eq(Price.of("USD", BigDecimal.valueOf(6.912))),
+                eq(bidRequest),
+                eq(givenBidAdjustments()),
+                eq(ImpMediaType.banner),
+                eq("bidder"),
+                eq("dealId"));
+    }
+
+    @Test
+    public void shouldReturnBidsWithoutAdjustingPricesWhenAdjustmentFactorNotPresentForBidder() {
+        // given
+        final BidderResponse bidderResponse = BidderResponse.of(
+                "bidder",
+                BidderSeatBid.builder()
+                        .bids(List.of(
+                                givenBidderBid(Bid.builder()
+                                                .impid("123")
+                                                .price(BigDecimal.ONE)
+                                                .dealid("dealId")
+                                                .build(),
+                                        "USD")))
+                        .build(),
+                1);
+
+        final ExtRequestBidAdjustmentFactors givenAdjustments = ExtRequestBidAdjustmentFactors.builder().build();
+        givenAdjustments.addFactor("some-other-bidder", BigDecimal.TEN);
+
+        final BidRequest bidRequest = givenBidRequest(singletonList(givenImp(singletonMap("bidder", 2), identity())),
+                builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .aliases(emptyMap())
+                        .auctiontimestamp(1000L)
+                        .currency(ExtRequestCurrency.of(null, false))
+                        .bidadjustmentfactors(givenAdjustments)
+                        .build())));
+
+        final AuctionParticipation auctionParticipation = givenAuctionParticipation(bidderResponse, bidRequest);
+
+        // when
+        final AuctionParticipation result = target
+                .enrichWithAdjustedBids(auctionParticipation, bidRequest, givenBidAdjustments());
+
+        // then
+        assertThat(result.getBidderResponse().getSeatBid().getBids())
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getPrice)
+                .containsExactly(BigDecimal.ONE);
+
+        verify(bidAdjustmentsResolver).resolve(
+                eq(Price.of("USD", BigDecimal.ONE)),
+                eq(bidRequest),
+                eq(givenBidAdjustments()),
+                eq(ImpMediaType.banner),
+                eq("bidder"),
+                eq("dealId"));
+    }
+
+    private static BidRequest givenBidRequest(List<Imp> imp,
+                                              UnaryOperator<BidRequest.BidRequestBuilder> bidRequestBuilderCustomizer) {
+
+        return bidRequestBuilderCustomizer
+                .apply(BidRequest.builder().cur(singletonList("UAH")).imp(imp).tmax(500L))
+                .build();
+    }
+
+    private static <T> Imp givenImp(T ext, UnaryOperator<Imp.ImpBuilder> impBuilderCustomizer) {
+        return impBuilderCustomizer.apply(Imp.builder()
+                        .id(UUID.randomUUID().toString())
+                        .ext(mapper.valueToTree(singletonMap(
+                                "prebid", ext != null ? singletonMap("bidder", ext) : emptyMap()))))
+                .build();
+    }
+
+    private static BidderBid givenBidderBid(Bid bid, String currency) {
+        return BidderBid.of(bid, banner, currency);
+    }
+
+    private static BidderBid givenBidderBid(Bid bid, String currency, BidType type) {
+        return BidderBid.of(bid, type, currency);
+    }
+
+    private static <K, V> Map<K, V> doubleMap(K key1, V value1, K key2, V value2) {
+        final Map<K, V> map = new HashMap<>();
+        map.put(key1, value1);
+        map.put(key2, value2);
+        return map;
+    }
+
+    private static BidAdjustments givenBidAdjustments() {
+        return BidAdjustments.of(ExtRequestBidAdjustments.builder().build());
+    }
+
+    private BidderResponse givenBidderResponse(Bid bid) {
+        return BidderResponse.of(
+                "bidder",
+                BidderSeatBid.builder()
+                        .bids(singletonList(givenBidderBid(bid, "USD")))
+                        .build(),
+                1);
+    }
+
+    private AuctionParticipation givenAuctionParticipation(BidderResponse bidderResponse,
+                                                           BidRequest bidRequest) {
+
+        final BidderRequest bidderRequest = BidderRequest.builder()
+                .bidRequest(bidRequest)
+                .build();
+
+        return AuctionParticipation.builder()
+                .bidder("bidder")
+                .bidderRequest(bidderRequest)
+                .bidderResponse(bidderResponse)
+                .build();
+    }
+
+}
diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java
new file mode 100644
index 00000000000..97ca68e939e
--- /dev/null
+++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsResolverTest.java
@@ -0,0 +1,243 @@
+package org.prebid.server.bidadjustments;
+
+import com.iab.openrtb.request.BidRequest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.VertxTest;
+import org.prebid.server.bidadjustments.model.BidAdjustments;
+import org.prebid.server.bidder.model.Price;
+import org.prebid.server.currency.CurrencyConversionService;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule;
+import org.prebid.server.proto.openrtb.ext.request.ImpMediaType;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mock.Strictness.LENIENT;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM;
+import static org.prebid.server.bidadjustments.model.BidAdjustmentType.MULTIPLIER;
+import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC;
+
+@ExtendWith(MockitoExtension.class)
+public class BidAdjustmentsResolverTest extends VertxTest {
+
+    @Mock(strictness = LENIENT)
+    private CurrencyConversionService currencyService;
+
+    private BidAdjustmentsResolver target;
+
+    @BeforeEach
+    public void before() {
+        target = new BidAdjustmentsResolver(currencyService);
+
+        given(currencyService.convertCurrency(any(), any(), any(), any())).willAnswer(invocation -> {
+            final BigDecimal initialPrice = (BigDecimal) invocation.getArguments()[0];
+            return initialPrice.multiply(BigDecimal.TEN);
+        });
+    }
+
+    @Test
+    public void resolveShouldPickAndApplyRulesBySpecificMediaType() {
+        // given
+        final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of(
+                "banner|*|*", List.of(givenStatic("15", "EUR")),
+                "*|*|*", List.of(givenStatic("25", "UAH"))));
+
+        // when
+        final Price actual = target.resolve(
+                Price.of("USD", BigDecimal.ONE),
+                BidRequest.builder().build(),
+                givenBidAdjustments,
+                ImpMediaType.banner,
+                "bidderName",
+                "dealId");
+
+        // then
+        assertThat(actual).isEqualTo(Price.of("EUR", new BigDecimal("15")));
+        verifyNoInteractions(currencyService);
+    }
+
+    @Test
+    public void resolveShouldPickAndApplyRulesByWildcardMediaType() {
+        // given
+        final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of(
+                "banner|*|*", List.of(givenCpm("15", "EUR")),
+                "*|*|*", List.of(givenCpm("25", "UAH"))));
+
+        final BidRequest givenBidRequest = BidRequest.builder().build();
+
+        // when
+        final Price actual = target.resolve(
+                Price.of("USD", BigDecimal.ONE),
+                givenBidRequest,
+                givenBidAdjustments,
+                ImpMediaType.video_outstream,
+                "bidderName",
+                "dealId");
+
+        // then
+        assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("-249")));
+        verify(currencyService).convertCurrency(new BigDecimal("25"), givenBidRequest, "UAH", "USD");
+    }
+
+    @Test
+    public void resolveShouldPickAndApplyRulesBySpecificBidder() {
+        // given
+        final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of(
+                "*|bidderName|*", List.of(givenMultiplier("15")),
+                "*|*|*", List.of(givenMultiplier("25"))));
+
+        // when
+        final Price actual = target.resolve(
+                Price.of("USD", BigDecimal.ONE),
+                BidRequest.builder().build(),
+                givenBidAdjustments,
+                ImpMediaType.banner,
+                "bidderName",
+                "dealId");
+
+        // then
+        assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("15")));
+        verifyNoInteractions(currencyService);
+    }
+
+    @Test
+    public void resolveShouldPickAndApplyRulesByWildcardBidder() {
+        // given
+        final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of(
+                "*|bidderName|*", List.of(givenStatic("15", "EUR"), givenMultiplier("15")),
+                "*|*|*", List.of(givenStatic("25", "UAH"), givenMultiplier("25"))));
+
+        // when
+        final Price actual = target.resolve(
+                Price.of("USD", BigDecimal.ONE),
+                BidRequest.builder().build(),
+                givenBidAdjustments,
+                ImpMediaType.banner,
+                "anotherBidderName",
+                "dealId");
+
+        // then
+        assertThat(actual).isEqualTo(Price.of("UAH", new BigDecimal("625")));
+        verifyNoInteractions(currencyService);
+    }
+
+    @Test
+    public void resolveShouldPickAndApplyRulesBySpecificDealId() {
+        // given
+        final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of(
+                "*|*|dealId", List.of(givenCpm("15", "JPY"), givenStatic("15", "EUR")),
+                "*|*|*", List.of(givenCpm("25", "JPY"), givenStatic("25", "UAH"))));
+        final BidRequest givenBidRequest = BidRequest.builder().build();
+
+        // when
+        final Price actual = target.resolve(
+                Price.of("USD", BigDecimal.ONE),
+                givenBidRequest,
+                givenBidAdjustments,
+                ImpMediaType.banner,
+                "bidderName",
+                "dealId");
+
+        // then
+        assertThat(actual).isEqualTo(Price.of("EUR", new BigDecimal("15")));
+        verify(currencyService).convertCurrency(new BigDecimal("15"), givenBidRequest, "JPY", "USD");
+    }
+
+    @Test
+    public void resolveShouldPickAndApplyRulesByWildcardDealId() {
+        // given
+        final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of(
+                "*|*|dealId", List.of(givenMultiplier("15"), givenCpm("15", "EUR")),
+                "*|*|*", List.of(givenMultiplier("25"), givenCpm("25", "UAH"))));
+        final BidRequest givenBidRequest = BidRequest.builder().build();
+
+        // when
+        final Price actual = target.resolve(
+                Price.of("USD", BigDecimal.ONE),
+                givenBidRequest,
+                givenBidAdjustments,
+                ImpMediaType.banner,
+                "bidderName",
+                "anotherDealId");
+
+        // then
+        assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("-225")));
+        verify(currencyService).convertCurrency(new BigDecimal("25"), givenBidRequest, "UAH", "USD");
+    }
+
+    @Test
+    public void resolveShouldPickAndApplyRulesByWildcardDealIdWhenDealIdIsNull() {
+        // given
+        final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of(
+                "*|*|dealId", List.of(givenCpm("15", "EUR"), givenCpm("15", "JPY")),
+                "*|*|*", List.of(givenCpm("25", "UAH"), givenCpm("25", "JPY"))));
+        final BidRequest givenBidRequest = BidRequest.builder().build();
+
+        // when
+        final Price actual = target.resolve(
+                Price.of("USD", BigDecimal.ONE),
+                givenBidRequest,
+                givenBidAdjustments,
+                ImpMediaType.banner,
+                "bidderName",
+                null);
+
+        // then
+        assertThat(actual).isEqualTo(Price.of("USD", new BigDecimal("-499")));
+        verify(currencyService).convertCurrency(new BigDecimal("25"), givenBidRequest, "UAH", "USD");
+        verify(currencyService).convertCurrency(new BigDecimal("25"), givenBidRequest, "JPY", "USD");
+    }
+
+    @Test
+    public void resolveShouldReturnEmptyListWhenNoMatchFound() {
+        // given
+        final BidAdjustments givenBidAdjustments = BidAdjustments.of(Map.of(
+                "*|*|dealId", List.of(givenStatic("15", "EUR"))));
+
+        // when
+        final Price actual = target.resolve(
+                Price.of("USD", BigDecimal.ONE),
+                BidRequest.builder().build(),
+                givenBidAdjustments,
+                ImpMediaType.banner,
+                "bidderName",
+                null);
+
+        // then
+        assertThat(actual).isEqualTo(Price.of("USD", BigDecimal.ONE));
+        verifyNoInteractions(currencyService);
+    }
+
+    private static ExtRequestBidAdjustmentsRule givenStatic(String value, String currency) {
+        return ExtRequestBidAdjustmentsRule.builder()
+                .adjType(STATIC)
+                .currency(currency)
+                .value(new BigDecimal(value))
+                .build();
+    }
+
+    private static ExtRequestBidAdjustmentsRule givenCpm(String value, String currency) {
+        return ExtRequestBidAdjustmentsRule.builder()
+                .adjType(CPM)
+                .currency(currency)
+                .value(new BigDecimal(value))
+                .build();
+    }
+
+    private static ExtRequestBidAdjustmentsRule givenMultiplier(String value) {
+        return ExtRequestBidAdjustmentsRule.builder()
+                .adjType(MULTIPLIER)
+                .value(new BigDecimal(value))
+                .build();
+    }
+}
diff --git a/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java
new file mode 100644
index 00000000000..df6caa05abd
--- /dev/null
+++ b/src/test/java/org/prebid/server/bidadjustments/BidAdjustmentsRetrieverTest.java
@@ -0,0 +1,396 @@
+package org.prebid.server.bidadjustments;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.VertxTest;
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.auction.model.debug.DebugContext;
+import org.prebid.server.bidadjustments.model.BidAdjustments;
+import org.prebid.server.json.JsonMerger;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
+import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.AccountAuctionConfig;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM;
+import static org.prebid.server.bidadjustments.model.BidAdjustmentType.STATIC;
+
+public class BidAdjustmentsRetrieverTest extends VertxTest {
+
+    private BidAdjustmentsRetriever target;
+
+    @BeforeEach
+    public void before() {
+        target = new BidAdjustmentsRetriever(jacksonMapper, new JsonMerger(jacksonMapper), 0.0d);
+    }
+
+    @Test
+    public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestAndAccountAdjustmentsAreAbsent() {
+        // given
+        final List<String> debugMessages = new ArrayList<>();
+
+        // when
+        final BidAdjustments actual = target.retrieve(givenAuctionContext(
+                null, null, debugMessages, true));
+
+        // then
+        assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap()));
+        assertThat(debugMessages).isEmpty();
+    }
+
+    @Test
+    public void retrieveShouldReturnEmptyBidAdjustmentsWhenRequestIsInvalidAndAccountAdjustmentsAreAbsent()
+            throws JsonProcessingException {
+
+        // given
+        final List<String> debugMessages = new ArrayList<>();
+        final String requestAdjustments = """
+                {
+                  "mediatype": {
+                    "banner": {
+                      "invalid": {
+                        "invalid": [
+                          {
+                            "adjtype": "invalid",
+                            "value": 0.1,
+                            "currency": "USD"
+                          }
+                        ]
+                      }
+                    }
+                  }
+                }
+                """;
+
+        final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments);
+
+        // when
+        final BidAdjustments actual = target.retrieve(givenAuctionContext(
+                givenRequestAdjustments, null, debugMessages, true));
+
+        // then
+        assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap()));
+        assertThat(debugMessages)
+                .containsOnly("bid adjustment from request was invalid: the found rule "
+                        + "[adjtype=UNKNOWN, value=0.1, currency=USD] in banner.invalid.invalid is invalid");
+    }
+
+    @Test
+    public void retrieveShouldReturnRequestBidAdjustmentsWhenAccountAdjustmentsAreAbsent()
+            throws JsonProcessingException {
+
+        // given
+        final List<String> debugMessages = new ArrayList<>();
+        final String requestAdjustments = """
+                {
+                  "mediatype": {
+                    "banner": {
+                      "*": {
+                        "*": [
+                          {
+                            "adjtype": "cpm",
+                            "value": 0.1,
+                            "currency": "USD"
+                          }
+                        ]
+                      }
+                    }
+                  }
+                }
+                """;
+
+        final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments);
+
+        // when
+        final BidAdjustments actual = target.retrieve(givenAuctionContext(
+                givenRequestAdjustments, null, debugMessages, true));
+
+        // then
+        final BidAdjustments expected = BidAdjustments.of(Map.of(
+                "banner|*|*",
+                List.of(ExtRequestBidAdjustmentsRule.builder()
+                        .adjType(CPM)
+                        .currency("USD")
+                        .value(new BigDecimal("0.1"))
+                        .build())));
+
+        assertThat(actual).isEqualTo(expected);
+        assertThat(debugMessages).isEmpty();
+    }
+
+    @Test
+    public void retrieveShouldReturnAccountBidAdjustmentsWhenRequestAdjustmentsAreAbsent()
+            throws JsonProcessingException {
+
+        // given
+        final List<String> debugMessages = new ArrayList<>();
+        final String requestAdjustments = """
+                {
+                  "mediatype": {
+                    "banner": {
+                      "*": {
+                        "*": [
+                          {
+                            "adjtype": "invalid",
+                            "value": 0.1,
+                            "currency": "USD"
+                          }
+                        ]
+                      }
+                    }
+                  }
+                }
+                """;
+
+        final String accountAdjustments = """
+                {
+                  "mediatype": {
+                    "audio": {
+                      "bidder": {
+                        "*": [
+                          {
+                            "adjtype": "static",
+                            "value": 0.1,
+                            "currency": "USD"
+                          }
+                        ]
+                      }
+                    }
+                  }
+                }
+                """;
+
+        final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments);
+        final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments);
+
+        // when
+        final BidAdjustments actual = target.retrieve(givenAuctionContext(
+                givenRequestAdjustments, givenAccountAdjustments, debugMessages, true));
+
+        // then
+        final BidAdjustments expected = BidAdjustments.of(Map.of(
+                "audio|bidder|*",
+                List.of(ExtRequestBidAdjustmentsRule.builder()
+                        .adjType(STATIC)
+                        .currency("USD")
+                        .value(new BigDecimal("0.1"))
+                        .build())));
+
+        assertThat(actual).isEqualTo(expected);
+        assertThat(debugMessages)
+                .containsOnly("bid adjustment from request was invalid: the found rule "
+                        + "[adjtype=UNKNOWN, value=0.1, currency=USD] in banner.*.* is invalid");
+    }
+
+    @Test
+    public void retrieveShouldReturnEmptyBidAdjustmentsWhenAccountAndRequestAdjustmentsAreInvalid()
+            throws JsonProcessingException {
+
+        // given
+        final List<String> debugMessages = new ArrayList<>();
+        final String requestAdjustments = """
+                {
+                  "mediatype": {
+                    "banner": {
+                      "*": {
+                        "*": [
+                          {
+                            "adjtype": "invalid",
+                            "value": 0.1,
+                            "currency": "USD"
+                          }
+                        ]
+                      }
+                    }
+                  }
+                }
+                """;
+
+        final String accountAdjustments = """
+                {
+                  "mediatype": {
+                    "audio": {
+                      "bidder": {
+                        "*": [
+                          {
+                            "adjtype": "invalid",
+                            "value": 0.1,
+                            "currency": "USD"
+                          }
+                        ]
+                      }
+                    }
+                  }
+                }
+                """;
+
+        final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments);
+        final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments);
+
+        // when
+        final BidAdjustments actual = target.retrieve(givenAuctionContext(
+                givenRequestAdjustments, givenAccountAdjustments, debugMessages, true));
+
+        // then
+        assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap()));
+        assertThat(debugMessages).containsExactlyInAnyOrder(
+                        "bid adjustment from request was invalid: the found rule "
+                                + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid",
+                        "bid adjustment from account was invalid: the found rule "
+                                + "[adjtype=UNKNOWN, value=0.1, currency=USD] in audio.bidder.* is invalid");
+    }
+
+    @Test
+    public void retrieveShouldSkipAddingDebugMessagesWhenDebugIsDisabled() throws JsonProcessingException {
+        // given
+        final List<String> debugMessages = new ArrayList<>();
+        final String requestAdjustments = """
+                {
+                  "mediatype": {
+                    "banner": {
+                      "*": {
+                        "*": [
+                          {
+                            "adjtype": "invalid",
+                            "value": 0.1,
+                            "currency": "USD"
+                          }
+                        ]
+                      }
+                    }
+                  }
+                }
+                """;
+
+        final String accountAdjustments = """
+                {
+                  "mediatype": {
+                    "audio": {
+                      "bidder": {
+                        "*": [
+                          {
+                            "adjtype": "invalid",
+                            "value": 0.1,
+                            "currency": "USD"
+                          }
+                        ]
+                      }
+                    }
+                  }
+                }
+                """;
+
+        final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments);
+        final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments);
+
+        // when
+        final BidAdjustments actual = target.retrieve(givenAuctionContext(
+                givenRequestAdjustments, givenAccountAdjustments, debugMessages, false));
+
+        // then
+        assertThat(actual).isEqualTo(BidAdjustments.of(Collections.emptyMap()));
+        assertThat(debugMessages).isEmpty();
+    }
+
+    @Test
+    public void retrieveShouldReturnMergedAccountIntoRequestAdjustments() throws JsonProcessingException {
+        // given
+        final List<String> debugMessages = new ArrayList<>();
+        final String requestAdjustments = """
+                {
+                  "mediatype": {
+                    "banner": {
+                      "*": {
+                        "*": [
+                          {
+                            "adjtype": "cpm",
+                            "value": 0.1,
+                            "currency": "USD"
+                          }
+                        ]
+                      }
+                    }
+                  }
+                }
+                """;
+
+        final String accountAdjustments = """
+                {
+                  "mediatype": {
+                    "banner": {
+                      "*": {
+                        "dealId": [
+                          {
+                            "adjtype": "cpm",
+                            "value": 0.3,
+                            "currency": "USD"
+                          }
+                        ],
+                        "*": [
+                          {
+                            "adjtype": "static",
+                            "value": 0.2,
+                            "currency": "USD"
+                          }
+                        ]
+                      }
+                    }
+                  }
+                }
+                """;
+
+        final ObjectNode givenRequestAdjustments = (ObjectNode) mapper.readTree(requestAdjustments);
+        final ObjectNode givenAccountAdjustments = (ObjectNode) mapper.readTree(accountAdjustments);
+
+        // when
+        final BidAdjustments actual = target.retrieve(givenAuctionContext(
+                givenRequestAdjustments, givenAccountAdjustments, debugMessages, true));
+
+        // then
+        final BidAdjustments expected = BidAdjustments.of(Map.of(
+                "banner|*|dealId",
+                List.of(ExtRequestBidAdjustmentsRule.builder()
+                        .adjType(CPM)
+                        .currency("USD")
+                        .value(new BigDecimal("0.3"))
+                        .build()),
+                "banner|*|*",
+                List.of(ExtRequestBidAdjustmentsRule.builder()
+                        .adjType(CPM)
+                        .currency("USD")
+                        .value(new BigDecimal("0.1"))
+                        .build())));
+
+        assertThat(actual).isEqualTo(expected);
+        assertThat(debugMessages).isEmpty();
+    }
+
+    private static AuctionContext givenAuctionContext(ObjectNode requestBidAdjustments,
+                                                      ObjectNode accountBidAdjustments,
+                                                      List<String> debugWarnings,
+                                                      boolean debugEnabled) {
+
+        return AuctionContext.builder()
+                .debugContext(DebugContext.of(debugEnabled, false, null))
+                .bidRequest(BidRequest.builder()
+                        .ext(ExtRequest.of(ExtRequestPrebid.builder().bidadjustments(requestBidAdjustments).build()))
+                        .build())
+                .account(Account.builder()
+                        .auction(AccountAuctionConfig.builder().bidAdjustments(accountBidAdjustments).build())
+                        .build())
+                .debugWarnings(debugWarnings)
+                .build();
+    }
+
+}
diff --git a/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java b/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java
new file mode 100644
index 00000000000..6bc26d7ef1a
--- /dev/null
+++ b/src/test/java/org/prebid/server/bidadjustments/model/BidAdjustmentsTest.java
@@ -0,0 +1,65 @@
+package org.prebid.server.bidadjustments.model;
+
+import org.junit.jupiter.api.Test;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustments;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestBidAdjustmentsRule;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.prebid.server.bidadjustments.model.BidAdjustmentType.CPM;
+
+public class BidAdjustmentsTest {
+
+    @Test
+    public void shouldBuildRulesSet() {
+        // given
+        final List<ExtRequestBidAdjustmentsRule> givenRules = List.of(givenRule("1"), givenRule("2"));
+        final Map<String, Map<String, List<ExtRequestBidAdjustmentsRule>>> givenRulesMap = Map.of(
+                "bidderName",
+                Map.of("dealId", givenRules));
+
+        final ExtRequestBidAdjustments givenBidAdjustments = ExtRequestBidAdjustments.builder()
+                .mediatype(Map.of(
+                        "audio", givenRulesMap,
+                        "native", givenRulesMap,
+                        "video-instream", givenRulesMap,
+                        "video-outstream", givenRulesMap,
+                        "banner", givenRulesMap,
+                        "video", givenRulesMap,
+                        "unknown", givenRulesMap,
+                        "*", Map.of(
+                                "*", Map.of("*", givenRules),
+                                "bidderName", Map.of(
+                                        "*", givenRules,
+                                        "dealId", givenRules))))
+                .build();
+
+        // when
+        final BidAdjustments actual = BidAdjustments.of(givenBidAdjustments);
+
+        // then
+        final BidAdjustments expected = BidAdjustments.of(Map.of(
+                "audio|bidderName|dealId", givenRules,
+                "native|bidderName|dealId", givenRules,
+                "video-instream|bidderName|dealId", givenRules,
+                "video-outstream|bidderName|dealId", givenRules,
+                "banner|bidderName|dealId", givenRules,
+                "*|*|*", givenRules,
+                "*|bidderName|*", givenRules,
+                "*|bidderName|dealId", givenRules));
+
+        assertThat(actual).isEqualTo(expected);
+
+    }
+
+    private static ExtRequestBidAdjustmentsRule givenRule(String value) {
+        return ExtRequestBidAdjustmentsRule.builder()
+                .adjType(CPM)
+                .currency("USD")
+                .value(new BigDecimal(value))
+                .build();
+    }
+}
diff --git a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java
index f2e0207fc4a..44a3253c143 100644
--- a/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java
+++ b/src/test/java/org/prebid/server/bidder/BidderCatalogTest.java
@@ -99,7 +99,8 @@ public void metaInfoByNameShouldReturnMetaInfoForKnownBidderIgnoringCase() {
                 true,
                 false,
                 CompressionType.NONE,
-                Ortb.of(false));
+                Ortb.of(false),
+                0L);
 
         final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder()
                 .name("BIDder")
@@ -132,7 +133,8 @@ public void isAliasShouldReturnTrueForAliasIgnoringCase() {
                 true,
                 false,
                 CompressionType.NONE,
-                Ortb.of(false));
+                Ortb.of(false),
+                0L);
 
         final BidderInstanceDeps bidderInstanceDeps = BidderInstanceDeps.builder()
                 .name("BIDder")
@@ -156,7 +158,8 @@ public void isAliasShouldReturnTrueForAliasIgnoringCase() {
                 true,
                 false,
                 CompressionType.NONE,
-                Ortb.of(false));
+                Ortb.of(false),
+                0L);
 
         final BidderInstanceDeps aliasInstanceDeps = BidderInstanceDeps.builder()
                 .name("ALIas")
@@ -193,7 +196,8 @@ public void resolveBaseBidderShouldReturnBaseBidderName() {
                         true,
                         false,
                         CompressionType.NONE,
-                        Ortb.of(false)))
+                        Ortb.of(false),
+                        0L))
                 .deprecatedNames(emptyList())
                 .build()));
         target = new BidderCatalog(singletonList(bidderDeps));
@@ -260,7 +264,8 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() {
                 true,
                 false,
                 CompressionType.NONE,
-                Ortb.of(false));
+                Ortb.of(false),
+                0L);
 
         final BidderInfo infoOfBidderWithoutUsersyncConfig = BidderInfo.create(
                 true,
@@ -278,7 +283,8 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() {
                 true,
                 false,
                 CompressionType.NONE,
-                Ortb.of(false));
+                Ortb.of(false),
+                0L);
 
         final BidderInfo infoOfDisabledBidderWithUsersyncConfig = BidderInfo.create(
                 false,
@@ -296,7 +302,8 @@ public void usersyncReadyBiddersShouldReturnBiddersThatCanSync() {
                 true,
                 false,
                 CompressionType.NONE,
-                Ortb.of(false));
+                Ortb.of(false),
+                0L);
 
         final List<BidderDeps> bidderDeps = List.of(
                 BidderDeps.of(singletonList(BidderInstanceDeps.builder()
@@ -365,7 +372,8 @@ public void nameByVendorIdShouldReturnBidderNameForVendorId() {
                 true,
                 false,
                 CompressionType.NONE,
-                Ortb.of(false));
+                Ortb.of(false),
+                0L);
 
         final BidderDeps bidderDeps = BidderDeps.of(singletonList(BidderInstanceDeps.builder()
                 .name("BIDder")
diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java
index 38cad500e7b..4773319266a 100644
--- a/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java
+++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequestEnricherTest.java
@@ -174,7 +174,8 @@ public void shouldAddContentEncodingHeaderIfRequiredByBidderConfig() {
                 false,
                 false,
                 CompressionType.GZIP,
-                Ortb.of(false)));
+                Ortb.of(false),
+                0L));
 
         final CaseInsensitiveMultiMap originalHeaders = CaseInsensitiveMultiMap.builder().build();
 
@@ -212,7 +213,8 @@ public void shouldAddContentEncodingHeaderIfRequiredByBidderAliasConfig() {
                 false,
                 false,
                 CompressionType.GZIP,
-                Ortb.of(false)));
+                Ortb.of(false),
+                0L));
 
         final CaseInsensitiveMultiMap originalHeaders = CaseInsensitiveMultiMap.builder().build();
 
diff --git a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java
index 83079b4d825..d2ce145e7ef 100644
--- a/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java
+++ b/src/test/java/org/prebid/server/bidder/HttpBidderRequesterTest.java
@@ -33,8 +33,8 @@
 import org.prebid.server.bidder.model.HttpRequest;
 import org.prebid.server.bidder.model.HttpResponse;
 import org.prebid.server.bidder.model.Result;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.model.CaseInsensitiveMultiMap;
 import org.prebid.server.proto.openrtb.ext.response.ExtHttpCall;
 import org.prebid.server.proto.openrtb.ext.response.FledgeAuctionConfig;
@@ -117,7 +117,7 @@ public void setUp() {
         expiredTimeout = timeoutFactory.create(clock.instant().minusMillis(1500L).toEpochMilli(), 1000L);
 
         target = new HttpBidderRequester(
-                httpClient, null, bidderErrorNotifier, requestEnricher, jacksonMapper);
+                httpClient, null, bidderErrorNotifier, requestEnricher, jacksonMapper, 0.0);
         given(bidder.makeBidderResponse(any(BidderCall.class), any(BidRequest.class))).willCallRealMethod();
     }
 
@@ -226,8 +226,8 @@ public void shouldPassStoredResponseToBidderMakeBidsMethodAndReturnSeatBids() {
                 .isEqualTo("storedResponse");
         assertThat(bidderSeatBid.getBids()).hasSameElementsAs(bids);
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     @Test
@@ -265,8 +265,8 @@ public void shouldMakeRequestToBidderWhenStoredResponseDefinedButBidderCreatesMo
         // then
         verify(httpClient, times(2)).request(any(), anyString(), any(), any(byte[].class), anyLong());
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     @Test
@@ -300,8 +300,8 @@ public void shouldSendPopulatedGetRequestWithoutBody() {
         // then
         verify(httpClient).request(any(), anyString(), any(), (byte[]) isNull(), anyLong());
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     @Test
@@ -336,8 +336,8 @@ public void shouldSendMultipleRequests() throws JsonProcessingException {
         // then
         verify(httpClient, times(2)).request(any(), anyString(), any(), any(byte[].class), anyLong());
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     @Test
@@ -369,8 +369,8 @@ public void shouldReturnBidsCreatedByBidder() {
         // then
         assertThat(bidderSeatBid.getBids()).hasSameElementsAs(bids);
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     @Test
@@ -403,8 +403,8 @@ public void shouldReturnBidsCreatedByMakeBids() {
         // then
         assertThat(bidderSeatBid.getBids()).hasSameElementsAs(bids);
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     @Test
@@ -442,8 +442,8 @@ public void shouldReturnFledgeCreatedByBidder() {
         assertThat(bidderSeatBid.getBids()).hasSameElementsAs(bids);
         assertThat(bidderSeatBid.getFledgeAuctionConfigs()).hasSameElementsAs(fledgeAuctionConfigs);
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     @Test
@@ -478,8 +478,8 @@ public void shouldCompressRequestBodyIfContentEncodingHeaderIsGzip() {
         verify(httpClient).request(any(), anyString(), any(), actualRequestBody.capture(), anyLong());
         assertThat(actualRequestBody.getValue()).isNotSameAs(EMPTY_BYTE_BODY);
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     @Test
@@ -506,7 +506,8 @@ public void processBids(List<BidderBid> bids) {
                 },
                 bidderErrorNotifier,
                 requestEnricher,
-                jacksonMapper);
+                jacksonMapper,
+                0.0);
 
         final BidRequest bidRequest = bidRequestWithDeals("deal1", "deal2");
         final BidderRequest bidderRequest = BidderRequest.builder()
@@ -575,8 +576,8 @@ public void processBids(List<BidderBid> bids) {
 
         assertThat(bidderSeatBid.getBids()).containsOnly(bidderBidDeal1, bidderBidDeal2);
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     @Test
@@ -624,8 +625,8 @@ public void shouldFinishWhenAllDealRequestsAreFinishedAndNoDealsProvided() {
 
         assertThat(bidderSeatBid.getBids()).contains(bidderBid, bidderBid, bidderBid, bidderBid);
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     @Test
@@ -691,8 +692,8 @@ public void shouldReturnFullDebugInfoIfDebugEnabled() throws JsonProcessingExcep
                         .status(200)
                         .build());
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     @Test
@@ -756,8 +757,8 @@ public void shouldReturnRecordBidRejections() throws JsonProcessingException {
 
         // then
         verify(bidRejectionTracker, atLeast(1)).succeed(secondRequestBids);
-        verify(bidRejectionTracker).reject(singleton("1"), BidRejectionReason.REQUEST_BLOCKED_GENERAL);
-        verify(bidRejectionTracker).reject(singleton("3"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE);
+        verify(bidRejectionTracker).rejectImps(singleton("1"), BidRejectionReason.REQUEST_BLOCKED_GENERAL);
+        verify(bidRejectionTracker).rejectImps(singleton("3"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE);
     }
 
     @Test
@@ -801,8 +802,8 @@ public void shouldNotReturnSensitiveHeadersInFullDebugInfo()
                 .extracting(ExtHttpCall::getRequestheaders)
                 .containsExactly(singletonMap("headerKey", singletonList("headerValue")));
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     @Test
@@ -849,7 +850,7 @@ public void shouldReturnPartialDebugInfoIfDebugEnabledAndGlobalTimeoutAlreadyExp
                         .requestheaders(singletonMap("headerKey", singletonList("headerValue")))
                         .build());
 
-        verify(bidRejectionTracker).reject(singleton("impId"), BidRejectionReason.ERROR_TIMED_OUT);
+        verify(bidRejectionTracker).rejectImps(singleton("impId"), BidRejectionReason.ERROR_TIMED_OUT);
     }
 
     @Test
@@ -897,7 +898,7 @@ public void shouldReturnPartialDebugInfoIfDebugEnabledAndHttpErrorOccurs() throw
                         .requestheaders(singletonMap("headerKey", singletonList("headerValue")))
                         .build());
 
-        verify(bidRejectionTracker).reject(singleton("impId"), BidRejectionReason.ERROR_GENERAL);
+        verify(bidRejectionTracker).rejectImps(singleton("impId"), BidRejectionReason.ERROR_GENERAL);
     }
 
     @Test
@@ -950,7 +951,7 @@ public void shouldReturnFullDebugInfoIfDebugEnabledAndErrorStatus() throws JsonP
                 .extracting(BidderError::getMessage)
                 .containsExactly("Unexpected status code: 500. Run with request.test = 1 for more info");
 
-        verify(bidRejectionTracker).reject(singleton("impId"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE);
+        verify(bidRejectionTracker).rejectImps(singleton("impId"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE);
     }
 
     @Test
@@ -1003,7 +1004,7 @@ public void shouldReturnFullDebugInfoIfDebugEnabledAndBidderIsUnreachable() thro
                 .extracting(BidderError::getMessage)
                 .containsExactly("Unexpected status code: 503. Run with request.test = 1 for more info");
 
-        verify(bidRejectionTracker).reject(singleton("impId"), BidRejectionReason.ERROR_BIDDER_UNREACHABLE);
+        verify(bidRejectionTracker).rejectImps(singleton("impId"), BidRejectionReason.ERROR_BIDDER_UNREACHABLE);
     }
 
     @Test
@@ -1040,7 +1041,7 @@ public void shouldTolerateAlreadyExpiredGlobalTimeout() throws JsonProcessingExc
                 .containsOnly("Timeout has been exceeded");
         verifyNoInteractions(httpClient);
 
-        verify(bidRejectionTracker).reject(singleton("impId"), BidRejectionReason.ERROR_TIMED_OUT);
+        verify(bidRejectionTracker).rejectImps(singleton("impId"), BidRejectionReason.ERROR_TIMED_OUT);
     }
 
     @Test
@@ -1147,13 +1148,13 @@ public void shouldTolerateMultipleErrors() {
                 BidderError.badServerResponse("Unexpected status code: 404. Run with request.test = 1 for more info"),
                 BidderError.badServerResponse("makeBidsError"));
 
-        verify(bidRejectionTracker).reject(singleton("1"), BidRejectionReason.ERROR_GENERAL);
-        verify(bidRejectionTracker).reject(singleton("2"), BidRejectionReason.ERROR_TIMED_OUT);
-        verify(bidRejectionTracker).reject(singleton("3"), BidRejectionReason.ERROR_BIDDER_UNREACHABLE);
-        verify(bidRejectionTracker).reject(singleton("4"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE);
-        verify(bidRejectionTracker).reject(singleton("5"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE);
-        verify(bidRejectionTracker, never()).reject(eq(singleton("6")), any());
-        verify(bidRejectionTracker, never()).reject(eq(singleton("7")), any());
+        verify(bidRejectionTracker).rejectImps(singleton("1"), BidRejectionReason.ERROR_GENERAL);
+        verify(bidRejectionTracker).rejectImps(singleton("2"), BidRejectionReason.ERROR_TIMED_OUT);
+        verify(bidRejectionTracker).rejectImps(singleton("3"), BidRejectionReason.ERROR_BIDDER_UNREACHABLE);
+        verify(bidRejectionTracker).rejectImps(singleton("4"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE);
+        verify(bidRejectionTracker).rejectImps(singleton("5"), BidRejectionReason.ERROR_INVALID_BID_RESPONSE);
+        verify(bidRejectionTracker, never()).rejectImps(eq(singleton("6")), any());
+        verify(bidRejectionTracker, never()).rejectImps(eq(singleton("7")), any());
 
     }
 
@@ -1186,8 +1187,8 @@ public void shouldNotMakeBidsIfResponseStatusIs204() {
         verify(bidder, never()).makeBidderResponse(any(), any());
         verify(bidder, never()).makeBids(any(), any());
 
-        verify(bidRejectionTracker, never()).reject(anyString(), any());
-        verify(bidRejectionTracker, never()).reject(anyList(), any());
+        verify(bidRejectionTracker, never()).rejectImp(anyString(), any());
+        verify(bidRejectionTracker, never()).rejectImps(anyList(), any());
     }
 
     private static BidRequest givenBidRequest(UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer) {
diff --git a/src/test/java/org/prebid/server/bidder/displayio/DisplayioBidderTest.java b/src/test/java/org/prebid/server/bidder/displayio/DisplayioBidderTest.java
index a88bf18e406..fe403f14fb9 100644
--- a/src/test/java/org/prebid/server/bidder/displayio/DisplayioBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/displayio/DisplayioBidderTest.java
@@ -88,16 +88,22 @@ public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
     }
 
     @Test
-    public void makeHttpRequestsShouldReturnErrorWhenBidFloorIsMissing() {
+    public void makeHttpRequestsShouldSetDefaultCurrencyEvenWhenBidfloorIsAbsent() {
         // given
-        final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(null));
+        final BidRequest bidRequest = givenBidRequest(imp -> imp.bidfloor(null).bidfloorcur("EUR"));
 
         // when
         final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
 
         // then
-        assertThat(result.getValue()).isEmpty();
-        assertThat(result.getErrors()).containsOnly(BidderError.badInput("BidFloor should be defined"));
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getBidfloor, Imp::getBidfloorcur)
+                .containsOnly(tuple(null, "USD"));
+
+        verifyNoInteractions(currencyConversionService);
     }
 
     @Test
@@ -156,8 +162,7 @@ public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() {
         // given
         final BidRequest bidRequest = givenBidRequest(
                 imp -> imp.id("impId1").ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))),
-                imp -> imp.id("impId2").bidfloor(null),
-                imp -> imp.id("impId3"));
+                imp -> imp.id("impId2"));
 
         //when
         final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
@@ -167,7 +172,7 @@ public void shouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() {
                 .extracting(HttpRequest::getPayload)
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getId)
-                .containsExactly("impId3");
+                .containsExactly("impId2");
     }
 
     @Test
diff --git a/src/test/java/org/prebid/server/bidder/epsilon/EpsilonBidderTest.java b/src/test/java/org/prebid/server/bidder/epsilon/EpsilonBidderTest.java
index 91f0ab88d8c..872a04c0a96 100644
--- a/src/test/java/org/prebid/server/bidder/epsilon/EpsilonBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/epsilon/EpsilonBidderTest.java
@@ -2,6 +2,7 @@
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.Audio;
 import com.iab.openrtb.request.Banner;
 import com.iab.openrtb.request.BidRequest;
 import com.iab.openrtb.request.Imp;
@@ -41,6 +42,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.BDDMockito.given;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.audio;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
 
@@ -610,6 +612,25 @@ public void makeBidsShouldReturnVideoBidIfRequestImpHasVideo() throws JsonProces
                 .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "USD"));
     }
 
+    @Test
+    public void makeBidsShouldReturnAudioBidIfRequestImpHasAudio() throws JsonProcessingException {
+        // given
+        final BidderCall<BidRequest> httpCall = givenHttpCall(
+                givenBidRequest(builder -> builder.id("123")
+                        .audio(Audio.builder().build())
+                        .banner(Banner.builder().build())),
+                mapper.writeValueAsString(
+                        givenBidResponse(bidBuilder -> bidBuilder.impid("123"))));
+
+        // when
+        final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .containsExactly(BidderBid.of(Bid.builder().impid("123").build(), audio, "USD"));
+    }
+
     @Test
     public void makeBidsShouldUpdateBidWithUUIDIfGenerateBidIdIsTrue() throws JsonProcessingException {
         // given
diff --git a/src/test/java/org/prebid/server/bidder/flipp/FlippBidderTest.java b/src/test/java/org/prebid/server/bidder/flipp/FlippBidderTest.java
index a0aadb48e92..74bf3474a59 100644
--- a/src/test/java/org/prebid/server/bidder/flipp/FlippBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/flipp/FlippBidderTest.java
@@ -2,6 +2,7 @@
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.node.BooleanNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.iab.openrtb.request.Banner;
 import com.iab.openrtb.request.BidRequest;
 import com.iab.openrtb.request.Device;
@@ -917,15 +918,101 @@ public void makeBidsShouldPopulateBidWidthWithNullWhenInlineContentsDataWidthEmp
     }
 
     @Test
-    public void makeBidsShouldPopulateBidHeightWithZeroWhenInlineContentsIsPresent() throws JsonProcessingException {
+    public void makeBidsShouldPopulateBidDefaultStandardHeightWhenInlineCustomDataIsAbsent()
+            throws JsonProcessingException {
+
         // given
-        final BidRequest bidRequest = givenBidRequest(identity());
+        final BidRequest bidRequest = givenBidRequest(givenImp(identity(), extImp -> extImp
+                .options(ExtImpFlippOptions.of(false, null, null))));
+
+        final ObjectNode customData = mapper.createObjectNode()
+                .put("compactHeight", 20)
+                .put("standardHeight", 30);
 
-        // and
         final BidderCall<CampaignRequestBody> httpCall = givenHttpCall(CampaignRequestBody.builder().build(),
-                mapper.writeValueAsString(givenCampaignResponseBody(inlineBuilder ->
-                        inlineBuilder.contents(singletonList(
-                                Content.of("any", "custom", Data.of(null, 10, 20), "type"))))));
+                mapper.writeValueAsString(givenCampaignResponseBody(inlineBuilder -> inlineBuilder
+                        .contents(singletonList(Content.of(
+                                "any", "custom", Data.of(null, 10, 20), "type"))))));
+
+        // when
+        final Result<List<BidderBid>> result = target.makeBids(httpCall, bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getH)
+                .containsExactly(2400);
+    }
+
+    @Test
+    public void makeBidsShouldPopulateBidDefaultCompactHeightWhenInlineCustomDataIsAbsent()
+            throws JsonProcessingException {
+
+        // given
+        final BidRequest bidRequest = givenBidRequest(givenImp(identity(), extImp -> extImp
+                .options(ExtImpFlippOptions.of(true, null, null))));
+
+        final BidderCall<CampaignRequestBody> httpCall = givenHttpCall(CampaignRequestBody.builder().build(),
+                mapper.writeValueAsString(givenCampaignResponseBody(inlineBuilder -> inlineBuilder
+                        .contents(singletonList(Content.of(
+                                "any", "custom", Data.of(null, 10, 20), "type"))))));
+
+        // when
+        final Result<List<BidderBid>> result = target.makeBids(httpCall, bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getH)
+                .containsExactly(600);
+    }
+
+    @Test
+    public void makeBidsShouldPopulateBidCompactHeightFromCustomDataWhenStartCompactIsTrue()
+            throws JsonProcessingException {
+
+        // given
+        final BidRequest bidRequest = givenBidRequest(givenImp(identity(), extImp -> extImp
+                .options(ExtImpFlippOptions.of(true, null, null))));
+
+        final ObjectNode customData = mapper.createObjectNode()
+                .put("compactHeight", 20)
+                .put("standardHeight", 30);
+
+        final BidderCall<CampaignRequestBody> httpCall = givenHttpCall(CampaignRequestBody.builder().build(),
+                mapper.writeValueAsString(givenCampaignResponseBody(inlineBuilder -> inlineBuilder
+                        .contents(singletonList(Content.of(
+                                "any", "custom", Data.of(customData, 10, 20), "type"))))));
+
+        // when
+        final Result<List<BidderBid>> result = target.makeBids(httpCall, bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getH)
+                .containsExactly(20);
+    }
+
+    @Test
+    public void makeBidsShouldPopulateBidStandardHeightFromCustomDataWhenStartCompactIsFalse()
+            throws JsonProcessingException {
+
+        // given
+        final BidRequest bidRequest = givenBidRequest(givenImp(identity(), extImp -> extImp
+                .options(ExtImpFlippOptions.of(false, null, null))));
+
+        final ObjectNode customData = mapper.createObjectNode()
+                .put("compactHeight", 20)
+                .put("standardHeight", 30);
+
+        final BidderCall<CampaignRequestBody> httpCall = givenHttpCall(CampaignRequestBody.builder().build(),
+                mapper.writeValueAsString(givenCampaignResponseBody(inlineBuilder -> inlineBuilder
+                        .contents(singletonList(Content.of(
+                                "any", "custom", Data.of(customData, 10, 20), "type"))))));
 
         // when
         final Result<List<BidderBid>> result = target.makeBids(httpCall, bidRequest);
@@ -935,7 +1022,7 @@ public void makeBidsShouldPopulateBidHeightWithZeroWhenInlineContentsIsPresent()
         assertThat(result.getValue()).hasSize(1)
                 .extracting(BidderBid::getBid)
                 .extracting(Bid::getH)
-                .containsExactly(0);
+                .containsExactly(30);
     }
 
     @Test
@@ -1011,14 +1098,27 @@ private static BidRequest givenBidRequest(UnaryOperator<Imp.ImpBuilder> impCusto
         return givenBidRequest(identity(), impCustomizer);
     }
 
+    private static BidRequest givenBidRequest(Imp givenImp) {
+        return BidRequest.builder()
+                        .device(Device.builder().ip("anyId").build())
+                        .imp(singletonList(givenImp))
+                .build();
+    }
+
     private static Imp givenImp(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
+        return givenImp(impCustomizer, identity());
+    }
+
+    private static Imp givenImp(UnaryOperator<Imp.ImpBuilder> impCustomizer,
+                                UnaryOperator<ExtImpFlipp.ExtImpFlippBuilder> extImpBuilder) {
+
         return impCustomizer.apply(Imp.builder()
                         .id("123")
                         .banner(Banner.builder().w(23).h(25).build())
-                        .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpFlipp.builder()
+                        .ext(mapper.valueToTree(ExtPrebid.of(null, extImpBuilder.apply(ExtImpFlipp.builder()
                                 .publisherNameIdentifier("publisherName")
                                 .creativeType("Any")
-                                .zoneIds(List.of(12))
+                                .zoneIds(List.of(12)))
                                 .build()))))
                 .build();
     }
diff --git a/src/test/java/org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java b/src/test/java/org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java
index 1a083ed3522..41c38db12ad 100644
--- a/src/test/java/org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/gothamads/GothamAdsBidderTest.java
@@ -46,7 +46,7 @@
 
 public class GothamAdsBidderTest extends VertxTest {
 
-    private static final String ENDPOINT_URL = "https://test-url.com/?pass={{AccountId}}";
+    private static final String ENDPOINT_URL = "https://test-url.com/?pass={{AccountID}}";
 
     private final GothamAdsBidder target = new GothamAdsBidder(ENDPOINT_URL, jacksonMapper);
 
diff --git a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java
index b75ea763ec6..82c9ff1e659 100644
--- a/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/improvedigital/ImprovedigitalBidderTest.java
@@ -116,7 +116,7 @@ public void makeHttpRequestsShouldUseProperEndpoints() {
     public void makeHttpRequestsShouldProperProcessConsentedProvidersSetting() {
         // given
         final ExtUser extUser = ExtUser.builder()
-                .consentedProvidersSettings(ConsentedProvidersSettings.of("1~10.20.90"))
+                .deprecatedConsentedProvidersSettings(ConsentedProvidersSettings.of("1~10.20.90"))
                 .build();
 
         final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder
@@ -145,7 +145,7 @@ public void makeHttpRequestsShouldProperProcessConsentedProvidersSetting() {
     public void makeHttpRequestsShouldProperProcessConsentedProvidersSettingWithMultipleTilda() {
         // given
         final ExtUser extUser = ExtUser.builder()
-                .consentedProvidersSettings(ConsentedProvidersSettings.of("1~10.20.90~anything"))
+                .deprecatedConsentedProvidersSettings(ConsentedProvidersSettings.of("1~10.20.90~anything"))
                 .build();
 
         final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder
@@ -174,7 +174,7 @@ public void makeHttpRequestsShouldProperProcessConsentedProvidersSettingWithMult
     public void makeHttpRequestsShouldReturnUserExtIfConsentedProvidersIsNotProvided() {
         // given
         final ExtUser extUser = ExtUser.builder()
-                .consentedProvidersSettings(ConsentedProvidersSettings.of(null))
+                .deprecatedConsentedProvidersSettings(ConsentedProvidersSettings.of(null))
                 .build();
 
         final BidRequest bidRequest = givenBidRequest(bidRequestBuilder ->
@@ -192,28 +192,6 @@ public void makeHttpRequestsShouldReturnUserExtIfConsentedProvidersIsNotProvided
                 .containsExactly(extUser);
     }
 
-    @Test
-    public void makeHttpRequestsShouldReturnErrorIfCannotParseConsentedProviders() {
-        // given
-        final ExtUser extUser = ExtUser.builder()
-                .consentedProvidersSettings(ConsentedProvidersSettings.of("1~a.fv.90"))
-                .build();
-
-        final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder
-                        .user(User.builder().ext(extUser).build()).id("request_id"),
-                identity());
-
-        // when
-        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
-
-        // then
-        assertThat(result.getValue()).isEmpty();
-        assertThat(result.getErrors()).allSatisfy(error -> {
-            assertThat(error.getType()).isEqualTo(BidderError.Type.bad_input);
-            assertThat(error.getMessage()).startsWith("Cannot deserialize value of type");
-        });
-    }
-
     @Test
     public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
         // given
diff --git a/src/test/java/org/prebid/server/bidder/insticator/InsticatorBidderTest.java b/src/test/java/org/prebid/server/bidder/insticator/InsticatorBidderTest.java
new file mode 100644
index 00000000000..9a6326327a0
--- /dev/null
+++ b/src/test/java/org/prebid/server/bidder/insticator/InsticatorBidderTest.java
@@ -0,0 +1,596 @@
+package org.prebid.server.bidder.insticator;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Imp;
+import com.iab.openrtb.request.Publisher;
+import com.iab.openrtb.request.Site;
+import com.iab.openrtb.request.Video;
+import com.iab.openrtb.response.Bid;
+import com.iab.openrtb.response.BidResponse;
+import com.iab.openrtb.response.SeatBid;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.VertxTest;
+import org.prebid.server.bidder.model.BidderBid;
+import org.prebid.server.bidder.model.BidderCall;
+import org.prebid.server.bidder.model.BidderError;
+import org.prebid.server.bidder.model.HttpRequest;
+import org.prebid.server.bidder.model.HttpResponse;
+import org.prebid.server.bidder.model.Result;
+import org.prebid.server.currency.CurrencyConversionService;
+import org.prebid.server.exception.PreBidException;
+import org.prebid.server.proto.openrtb.ext.ExtPrebid;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.insticator.ExtImpInsticator;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
+
+import static java.util.Collections.singletonList;
+import static java.util.function.UnaryOperator.identity;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mock.Strictness.LENIENT;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
+import static org.prebid.server.util.HttpUtil.ACCEPT_HEADER;
+import static org.prebid.server.util.HttpUtil.APPLICATION_JSON_CONTENT_TYPE;
+import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER;
+import static org.prebid.server.util.HttpUtil.USER_AGENT_HEADER;
+import static org.prebid.server.util.HttpUtil.X_FORWARDED_FOR_HEADER;
+import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;
+
+@ExtendWith(MockitoExtension.class)
+public class InsticatorBidderTest extends VertxTest {
+
+    private static final String ENDPOINT_URL = "https://test.endpoint.com";
+
+    @Mock(strictness = LENIENT)
+    private CurrencyConversionService currencyConversionService;
+
+    private InsticatorBidder target;
+
+    @BeforeEach
+    public void before() {
+        target = new InsticatorBidder(currencyConversionService, ENDPOINT_URL, jacksonMapper);
+    }
+
+    @Test
+    public void creationShouldFailOnInvalidEndpointUrl() {
+        assertThatIllegalArgumentException().isThrownBy(() -> new InsticatorBidder(
+                currencyConversionService, "invalid_url", jacksonMapper));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnErrorIfImpExtCouldNotBeParsed() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                imp -> imp.ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))));
+
+        // when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getValue()).isEmpty();
+        assertThat(result.getErrors()).hasSize(1).allSatisfy(bidderError -> {
+            assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input);
+            assertThat(bidderError.getMessage()).startsWith("Cannot deserialize value");
+        });
+    }
+
+    @Test
+    public void makeHttpRequestsShouldMakeOneRequestPerAdUnitId() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                imp -> imp.id("givenImp1").ext(givenImpExt("1")),
+                imp -> imp.id("givenImp2").ext(givenImpExt("1")),
+                imp -> imp.id("givenImp3").ext(givenImpExt("2")));
+
+        //when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        //then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(2)
+                .extracting(HttpRequest::getPayload)
+                .extracting(payload -> payload.getImp().stream().map(Imp::getId).collect(Collectors.toList()))
+                .containsExactlyInAnyOrder(List.of("givenImp1", "givenImp2"), List.of("givenImp3"));
+
+        assertThat(result.getValue()).hasSize(2)
+                .extracting(HttpRequest::getImpIds)
+                .containsExactlyInAnyOrder(Set.of("givenImp1", "givenImp2"), Set.of("givenImp3"));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldHaveCorrectUri() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp"));
+
+        //when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        //then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(HttpRequest::getUri)
+                .containsExactlyInAnyOrder(ENDPOINT_URL);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnExpectedHeadersWithIpWhenDeviceHasIp() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(identity()).toBuilder()
+                .device(Device.builder().ip("ip").ua("ua").ipv6("ipv6").build())
+                .build();
+
+        // when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getValue()).hasSize(1).first()
+                .extracting(HttpRequest::getHeaders)
+                .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER))
+                        .isEqualTo(APPLICATION_JSON_CONTENT_TYPE))
+                .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER))
+                        .isEqualTo(APPLICATION_JSON_VALUE))
+                .satisfies(headers -> assertThat(headers.get(USER_AGENT_HEADER))
+                        .isEqualTo("ua"))
+                .satisfies(headers -> assertThat(headers.get("IP"))
+                        .isEqualTo("ip"))
+                .satisfies(headers -> assertThat(headers.get(X_FORWARDED_FOR_HEADER))
+                        .isEqualTo("ip"));
+        assertThat(result.getErrors()).isEmpty();
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnExpectedHeadersWithIpv6WhenDeviceHasIpv6AndDoesNotHaveIp() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(identity()).toBuilder()
+                .device(Device.builder().ip(null).ua("ua").ipv6("ipv6").build())
+                .build();
+
+        // when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getValue()).hasSize(1).first()
+                .extracting(HttpRequest::getHeaders)
+                .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER))
+                        .isEqualTo(APPLICATION_JSON_CONTENT_TYPE))
+                .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER))
+                        .isEqualTo(APPLICATION_JSON_VALUE))
+                .satisfies(headers -> assertThat(headers.get(USER_AGENT_HEADER))
+                        .isEqualTo("ua"))
+                .satisfies(headers -> assertThat(headers.get("IP"))
+                        .isNull())
+                .satisfies(headers -> assertThat(headers.get(X_FORWARDED_FOR_HEADER))
+                        .isEqualTo("ipv6"));
+        assertThat(result.getErrors()).isEmpty();
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnExpectedHeadersWhenDeviceIsAbsent() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(identity());
+
+        // when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getValue()).hasSize(1).first()
+                .extracting(HttpRequest::getHeaders)
+                .satisfies(headers -> assertThat(headers.get(CONTENT_TYPE_HEADER))
+                        .isEqualTo(APPLICATION_JSON_CONTENT_TYPE))
+                .satisfies(headers -> assertThat(headers.get(ACCEPT_HEADER))
+                        .isEqualTo(APPLICATION_JSON_VALUE))
+                .satisfies(headers -> assertThat(headers.get(USER_AGENT_HEADER)).isNull())
+                .satisfies(headers -> assertThat(headers.get("IP")).isNull())
+                .satisfies(headers -> assertThat(headers.get(X_FORWARDED_FOR_HEADER)).isNull());
+        assertThat(result.getErrors()).isEmpty();
+    }
+
+    @Test
+    public void makeHttpRequestsShouldConvertAndReturnProperBidFloorCur() {
+        // given
+        given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString()))
+                .willReturn(BigDecimal.ONE);
+
+        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
+                .bidfloorcur("EUR")
+                .bidfloor(BigDecimal.TEN));
+
+        // when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getImp)
+                .first()
+                .satisfies(imps -> {
+                    assertThat(imps.getBidfloorcur()).isEqualTo("USD");
+                    assertThat(imps.getBidfloor()).isEqualTo(BigDecimal.ONE);
+                });
+    }
+
+    @Test
+    public void makeHttpRequestsShouldNotConvertAndReturnUSDBidFloorCurWhenBidFloorNotPositiveNumber() {
+        // given
+        given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString()))
+                .willReturn(BigDecimal.ONE);
+
+        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
+                .bidfloorcur("EUR")
+                .bidfloor(BigDecimal.ZERO));
+
+        // when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getImp)
+                .first()
+                .satisfies(imp -> {
+                    assertThat(imp.getBidfloorcur()).isEqualTo("EUR");
+                    assertThat(imp.getBidfloor()).isEqualTo(BigDecimal.ZERO);
+                });
+
+        verifyNoInteractions(currencyConversionService);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldThrowErrorWhenCurrencyConvertCannotConvertInAnotherCurrency() {
+        // given
+        when(currencyConversionService.convertCurrency(any(), any(), any(), any())).thenThrow(
+                new PreBidException("Unable to convert from currency UAH to desired ad server currency USD"));
+
+        final BidRequest bidRequest = givenBidRequest(impBuilder -> impBuilder
+                .bidfloorcur("UAH")
+                .bidfloor(BigDecimal.TEN));
+
+        // when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getValue()).isEmpty();
+        assertThat(result.getErrors())
+                .extracting(BidderError::getMessage)
+                .containsExactly("Unable to convert from currency UAH to desired ad server currency USD");
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnImpWithUpdatedExt() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                imp -> imp.id("givenImp").ext(givenImpExt("adUnitId", "publisherId")));
+
+        //when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        //then
+        assertThat(result.getErrors()).isEmpty();
+
+        final ObjectNode expectedNode = mapper.createObjectNode();
+        expectedNode.set("adUnitId", TextNode.valueOf("adUnitId"));
+        expectedNode.set("publisherId", TextNode.valueOf("publisherId"));
+        final ObjectNode expectedImpExt = mapper.createObjectNode().set("insticator", expectedNode);
+
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getExt)
+                .containsExactly(expectedImpExt);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnExtRequestInsticatorWithDefaultCallerWhenInsticatorIsAbsent() {
+        // given
+        final ExtRequest givenExtRequest = ExtRequest.empty();
+        givenExtRequest.addProperty("insticator",
+                mapper.createObjectNode().set("caller",
+                        mapper.createArrayNode().add(mapper.createObjectNode()
+                                .put("name", "something")
+                                .put("version", "1.0"))));
+        final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp"))
+                .toBuilder()
+                .ext(givenExtRequest)
+                .build();
+
+        //when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        //then
+        assertThat(result.getErrors()).isEmpty();
+
+        final ExtRequest expectedExtRequest = ExtRequest.empty();
+        expectedExtRequest.addProperty("insticator",
+                mapper.createObjectNode().set("caller",
+                        mapper.createArrayNode()
+                                .add(mapper.createObjectNode()
+                                        .put("name", "something")
+                                        .put("version", "1.0"))
+                                .add(mapper.createObjectNode()
+                                        .put("name", "Prebid-Server")
+                                        .put("version", "n/a"))));
+
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getExt)
+                .containsExactly(expectedExtRequest);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldAddsToExtRequestInsticatorDefaultCaller() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp"))
+                .toBuilder()
+                .ext(ExtRequest.empty())
+                .build();
+
+        //when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        //then
+        assertThat(result.getErrors()).isEmpty();
+
+        final ExtRequest expectedExtRequest = ExtRequest.empty();
+        expectedExtRequest.addProperty("insticator",
+                mapper.createObjectNode().set("caller",
+                        mapper.createArrayNode().add(mapper.createObjectNode()
+                                .put("name", "Prebid-Server")
+                                .put("version", "n/a"))));
+
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getExt)
+                .containsExactly(expectedExtRequest);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldAddsToExtRequestInsticatorDefaultCallerWhenExistingInsticatorCanNotBeParsed() {
+        // given
+        final ExtRequest givenExtRequest = ExtRequest.empty();
+        givenExtRequest.addProperty("insticator", mapper.createArrayNode());
+        final BidRequest bidRequest = givenBidRequest(imp -> imp.id("givenImp"))
+                .toBuilder()
+                .ext(givenExtRequest)
+                .build();
+
+        //when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        //then
+        assertThat(result.getErrors()).hasSize(1).allSatisfy(bidderError -> {
+            assertThat(bidderError.getType()).isEqualTo(BidderError.Type.bad_input);
+            assertThat(bidderError.getMessage()).startsWith("Cannot deserialize value of type "
+                    + "`org.prebid.server.bidder.insticator.InsticatorExtRequest`");
+        });
+
+        final ExtRequest expectedExtRequest = ExtRequest.empty();
+        expectedExtRequest.addProperty("insticator",
+                mapper.createObjectNode().set("caller",
+                        mapper.createArrayNode().add(mapper.createObjectNode()
+                                .put("name", "Prebid-Server")
+                                .put("version", "n/a"))));
+
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getExt)
+                .containsExactly(expectedExtRequest);
+    }
+
+    @Test
+    public void makeHttpRequestsShouldModifyAppWithPublisherIdOfTheFirstImp() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                imp -> imp.id("givenImpId1").ext(givenImpExt("adUnitId1", "publisherId1")),
+                imp -> imp.id("givenImpId2").ext(givenImpExt("adUnitId2", "publisherId2")))
+                .toBuilder()
+                .app(App.builder().publisher(Publisher.builder().id("id").build()).build())
+                .build();
+
+        //when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        //then
+        assertThat(result.getValue()).hasSize(2)
+                .extracting(HttpRequest::getPayload)
+                .extracting(BidRequest::getApp)
+                .extracting(App::getPublisher)
+                .extracting(Publisher::getId)
+                .containsExactly("publisherId1", "publisherId1");
+    }
+
+    @Test
+    public void makeHttpRequestsShouldModifySiteWithPublisherIdOfTheFirstImp() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                imp -> imp.id("givenImpId1").ext(givenImpExt("adUnitId1", "publisherId1")),
+                imp -> imp.id("givenImpId2").ext(givenImpExt("adUnitId2", "publisherId2")))
+                .toBuilder()
+                .site(Site.builder().publisher(Publisher.builder().id("id").build()).build())
+                .build();
+
+        //when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        //then
+        assertThat(result.getValue()).hasSize(2)
+                .extracting(HttpRequest::getPayload)
+                .extracting(BidRequest::getSite)
+                .extracting(Site::getPublisher)
+                .extracting(Publisher::getId)
+                .containsExactly("publisherId1", "publisherId1");
+    }
+
+    @Test
+    public void makeHttpRequestsShouldMakeOneRequestWhenOneImpIsValidAndAnotherAreInvalid() {
+        // given
+        final BidRequest bidRequest = givenBidRequest(
+                imp -> imp.id("givenImpId1").ext(mapper.valueToTree(ExtPrebid.of(null, mapper.createArrayNode()))),
+                imp -> imp.id("givenImpId2"),
+                imp -> imp.id("givenImpId3").video(Video.builder().mimes(null).build()),
+                imp -> imp.id("givenImpId4").video(Video.builder().h(null).build()),
+                imp -> imp.id("givenImpId5").video(Video.builder().h(0).build()),
+                imp -> imp.id("givenImpId6").video(Video.builder().w(null).build()),
+                imp -> imp.id("givenImpId7").video(Video.builder().w(0).build()));
+
+        //when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        //then
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getId)
+                .containsExactly("givenImpId2");
+    }
+
+    @Test
+    public void makeBidsShouldReturnErrorWhenResponseCanNotBeParsed() {
+        // given
+        final BidderCall<BidRequest> httpCall = givenHttpCall("invalid");
+
+        // when
+        final Result<List<BidderBid>> actual = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(actual.getValue()).isEmpty();
+        assertThat(actual.getErrors()).hasSize(1)
+                .allSatisfy(error -> {
+                    assertThat(error.getMessage()).startsWith("Failed to decode: Unrecognized token 'invalid':");
+                    assertThat(error.getType()).isEqualTo(BidderError.Type.bad_server_response);
+                });
+    }
+
+    @Test
+    public void makeBidsShouldReturnEmptyBidsWhenResponseDoesNotHaveSeatBid() throws JsonProcessingException {
+        // given
+        final BidderCall<BidRequest> httpCall = givenHttpCall(mapper.writeValueAsString(BidResponse.builder().build()));
+
+        // when
+        final Result<List<BidderBid>> actual = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(actual.getValue()).isEmpty();
+        assertThat(actual.getErrors()).isEmpty();
+    }
+
+    @Test
+    public void makeBidsShouldReturnBannerBidSuccessfully() throws JsonProcessingException {
+        // given
+        final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidResponse(bid -> bid.impid("1").mtype(1)));
+
+        // when
+        final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .containsExactly(BidderBid.of(Bid.builder().mtype(1).impid("1").build(), banner, "USD"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnVideoBidSuccessfully() throws JsonProcessingException {
+        // given
+        final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidResponse(bid -> bid.impid("2").mtype(2)));
+
+        // when
+        final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .containsExactly(BidderBid.of(Bid.builder().mtype(2).impid("2").build(), video, "USD"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnBannerBidWhenMtypeIsUnknown() throws JsonProcessingException {
+        // given
+        final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidResponse(bid -> bid.impid("3").mtype(3)));
+
+        // when
+        final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .containsExactly(BidderBid.of(Bid.builder().mtype(3).impid("3").build(), banner, "USD"));
+    }
+
+    @Test
+    public void makeBidsShouldReturnBannerBidWhenMtypeIsNull() throws JsonProcessingException {
+        // given
+        final BidderCall<BidRequest> httpCall = givenHttpCall(givenBidResponse(bid -> bid.impid("3").mtype(null)));
+
+        // when
+        final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .containsExactly(BidderBid.of(Bid.builder().mtype(null).impid("3").build(), banner, "USD"));
+    }
+
+    private static BidRequest givenBidRequest(UnaryOperator<Imp.ImpBuilder>... impCustomizers) {
+        return BidRequest.builder()
+                .imp(Arrays.stream(impCustomizers).map(InsticatorBidderTest::givenImp).toList())
+                .build();
+    }
+
+    private static Imp givenImp(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
+        return impCustomizer.apply(Imp.builder()
+                        .id("impId")
+                        .bidfloor(BigDecimal.TEN)
+                        .bidfloorcur("USD")
+                        .ext(mapper.valueToTree(ExtPrebid.of(
+                                null,
+                                ExtImpInsticator.of("adUnitId", "publisherId")))))
+                .build();
+    }
+
+    private static ObjectNode givenImpExt(String adUnitId) {
+        return givenImpExt(adUnitId, "publisherId");
+    }
+
+    private static ObjectNode givenImpExt(String adUnitId, String publisherId) {
+        return mapper.valueToTree(ExtPrebid.of(null, ExtImpInsticator.of(adUnitId, publisherId)));
+    }
+
+    private static BidderCall<BidRequest> givenHttpCall(String body) {
+        return BidderCall.succeededHttp(
+                HttpRequest.<BidRequest>builder().payload(null).build(),
+                HttpResponse.of(200, null, body),
+                null);
+    }
+
+    private String givenBidResponse(UnaryOperator<Bid.BidBuilder> bidCustomizer) throws JsonProcessingException {
+        return mapper.writeValueAsString(BidResponse.builder()
+                .cur("USD")
+                .seatbid(singletonList(SeatBid.builder()
+                        .bid(singletonList(bidCustomizer.apply(Bid.builder()).build()))
+                        .build()))
+                .build());
+    }
+
+}
diff --git a/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java b/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java
index 18f020f2bc0..35726f5a205 100644
--- a/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/ix/IxBidderTest.java
@@ -27,6 +27,7 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.VertxTest;
 import org.prebid.server.bidder.ix.model.request.IxDiag;
+import org.prebid.server.bidder.ix.model.response.AuctionConfigExtBidResponse;
 import org.prebid.server.bidder.ix.model.response.IxBidResponse;
 import org.prebid.server.bidder.ix.model.response.IxExtBidResponse;
 import org.prebid.server.bidder.ix.model.response.NativeV11Wrapper;
@@ -49,7 +50,6 @@
 import org.prebid.server.version.PrebidVersionProvider;
 
 import java.util.List;
-import java.util.Map;
 import java.util.function.Function;
 import java.util.function.UnaryOperator;
 
@@ -778,7 +778,7 @@ public void makeBidderResponseShouldReturnFledgeAuctionConfig() throws JsonProce
         final IxBidResponse bidResponseWithFledge = IxBidResponse.builder()
                 .cur(bidResponse.getCur())
                 .seatbid(bidResponse.getSeatbid())
-                .ext(IxExtBidResponse.of(Map.of(impId, fledgeAuctionConfig)))
+                .ext(IxExtBidResponse.of(List.of(AuctionConfigExtBidResponse.of(impId, fledgeAuctionConfig))))
                 .build();
         final BidderCall<BidRequest> httpCall =
                 givenHttpCall(bidRequest, mapper.writeValueAsString(bidResponseWithFledge));
diff --git a/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java b/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java
index 1f218984f91..cbe1e22c7ca 100644
--- a/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/openx/OpenxBidderTest.java
@@ -91,30 +91,10 @@ public void makeHttpRequestsShouldReturnResultWithErrorWhenAudioImpsPresent() {
         assertThat(result.getValue()).isEmpty();
         assertThat(result.getErrors()).hasSize(2)
                 .containsExactly(
-                        BidderError.badInput("OpenX only supports banner and video imps. Ignoring imp id=impId1"),
                         BidderError.badInput(
-                                "OpenX only supports banner and video imps. Ignoring imp id=impId2"));
-    }
-
-    @Test
-    public void makeHttpRequestsShouldReturnResultWithErrorWhenNativeImpsPresent() {
-        // given
-        final BidRequest bidRequest = BidRequest.builder()
-                .imp(asList(
-                        Imp.builder().id("impId1").xNative(Native.builder().build()).build(),
-                        Imp.builder().id("impId2").xNative(Native.builder().build()).build()))
-                .build();
-
-        // when
-        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
-
-        // then
-        assertThat(result.getValue()).isEmpty();
-        assertThat(result.getErrors()).hasSize(2)
-                .containsExactly(
-                        BidderError.badInput("OpenX only supports banner and video imps. Ignoring imp id=impId1"),
+                                "OpenX only supports banner, video and native imps. Ignoring imp id=impId1"),
                         BidderError.badInput(
-                                "OpenX only supports banner and video imps. Ignoring imp id=impId2"));
+                                "OpenX only supports banner, video and native imps. Ignoring imp id=impId2"));
     }
 
     @Test
@@ -254,7 +234,7 @@ public void makeHttpRequestsShouldReturnResultWithExpectedFieldsSet() {
         // then
         assertThat(result.getErrors()).hasSize(1)
                 .containsExactly(BidderError.badInput(
-                        "OpenX only supports banner and video imps. Ignoring imp id=impId1"));
+                        "OpenX only supports banner, video and native imps. Ignoring imp id=impId1"));
 
         assertThat(result.getValue()).hasSize(3)
                 .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
@@ -344,6 +324,158 @@ public void makeHttpRequestsShouldReturnResultWithExpectedFieldsSet() {
                                 .build());
     }
 
+    @Test
+    public void makeHttpRequestsShouldReturnResultWithSingleBidRequestForMultipleBannerAndNativeImps() {
+        // given
+        final BidRequest bidRequest = BidRequest.builder()
+                .id("bidRequestId")
+                .imp(asList(
+                        Imp.builder()
+                                .id("impId4")
+                                .banner(Banner.builder().build())
+                                .ext(mapper.valueToTree(
+                                        ExtPrebid.of(null,
+                                                ExtImpOpenx.builder()
+                                                        .customParams(givenCustomParams("foo4", "bar4"))
+                                                        .delDomain("se-demo-d.openx.net")
+                                                        .unit("4").build()))).build(),
+                        Imp.builder()
+                                .id("impId5")
+                                .xNative(Native.builder().request("{\"testreq\":1}").build())
+                                .ext(mapper.valueToTree(
+                                        ExtPrebid.of(null,
+                                                ExtImpOpenx.builder()
+                                                        .customParams(givenCustomParams("foo5", "bar5"))
+                                                        .delDomain("se-demo-d.openx.net")
+                                                        .unit("5").build()))).build(),
+                        Imp.builder()
+                                .id("impId6")
+                                .xNative(Native.builder().build())
+                                .ext(mapper.valueToTree(
+                                        ExtPrebid.of(null,
+                                                ExtImpOpenx.builder()
+                                                        .customParams(givenCustomParams("foo6", "bar6"))
+                                                        .delDomain("se-demo-d.openx.net")
+                                                        .unit("6").build()))).build()))
+                .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build())
+                .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null, null)).build())
+                .build();
+
+        // when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+                .containsExactly(
+                        // check if all native and banner imps are part of single bidRequest
+                        BidRequest.builder()
+                                .id("bidRequestId")
+                                .imp(asList(
+                                        Imp.builder()
+                                                .id("impId4")
+                                                .tagid("4")
+                                                .banner(Banner.builder().build())
+                                                .ext(mapper.valueToTree(
+                                                        ExtImpOpenx.builder()
+                                                                .customParams(
+                                                                        givenCustomParams("foo4", "bar4"))
+                                                                .build()))
+                                                .build(),
+                                        Imp.builder()
+                                                .id("impId5")
+                                                .tagid("5")
+                                                .xNative(Native.builder().request("{\"testreq\":1}").build())
+                                                .ext(mapper.valueToTree(
+                                                        ExtImpOpenx.builder()
+                                                                .customParams(
+                                                                        givenCustomParams("foo5", "bar5"))
+                                                                .build()))
+                                                .build(),
+                                        Imp.builder()
+                                                .id("impId6")
+                                                .tagid("6")
+                                                .xNative(Native.builder().build())
+                                                .ext(mapper.valueToTree(
+                                                        ExtImpOpenx.builder()
+                                                                .customParams(
+                                                                        givenCustomParams("foo6", "bar6"))
+                                                                .build()))
+                                                .build()))
+                                .ext(jacksonMapper.fillExtension(
+                                        ExtRequest.empty(),
+                                        OpenxRequestExt.of("se-demo-d.openx.net", null, "hb_pbs_1.0.0")))
+                                .user(User.builder()
+                                        .ext(ExtUser.builder().consent("consent").build())
+                                        .build())
+                                .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null, null)).build())
+                                .build());
+    }
+
+    @Test
+    public void makeHttpRequestsShouldReturnResultWithSingleBidRequestForMultiFormatImps() {
+        // given
+        final BidRequest bidRequest = BidRequest.builder()
+                .id("bidRequestId")
+                .imp(asList(
+                        Imp.builder()
+                                .id("impId1")
+                                .banner(Banner.builder().w(320).h(200).build())
+                                .video(Video.builder().maxduration(10).build())
+                                .ext(mapper.valueToTree(
+                                        ExtPrebid.of(null, ExtImpOpenx.builder().unit("1").build())))
+                                .build(),
+                        Imp.builder()
+                                .id("impId2")
+                                .banner(Banner.builder().w(300).h(150).build())
+                                .xNative(Native.builder().request("{\"version\":1}").build())
+                                .ext(mapper.valueToTree(
+                                        ExtPrebid.of(null, ExtImpOpenx.builder().unit("2").build())))
+                                .build()))
+                .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build())
+                .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null, null)).build())
+                .build();
+
+        // when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+
+        assertThat(result.getValue()).hasSize(1)
+                .extracting(httpRequest -> mapper.readValue(httpRequest.getBody(), BidRequest.class))
+                .containsExactly(
+                        // check if all native and banner imps are part of single bidRequest
+                        BidRequest.builder()
+                                .id("bidRequestId")
+                                .imp(asList(
+                                        // verify banner and video media types are preserved in a single imp
+                                        Imp.builder()
+                                                .id("impId1")
+                                                .tagid("1")
+                                                .banner(Banner.builder().w(320).h(200).build())
+                                                .video(Video.builder().maxduration(10).build())
+                                                .ext(mapper.valueToTree(ExtImpOpenx.builder().build())).build(),
+                                        // verify banner and native media types are preserved in a single imp
+                                        Imp.builder()
+                                                .id("impId2")
+                                                .tagid("2")
+                                                .banner(Banner.builder().w(300).h(150).build())
+                                                .xNative(Native.builder().request("{\"version\":1}").build())
+                                                .ext(mapper.valueToTree(ExtImpOpenx.builder().build()))
+                                        .build()))
+                                .ext(jacksonMapper.fillExtension(
+                                        ExtRequest.empty(),
+                                        OpenxRequestExt.of(null, null, "hb_pbs_1.0.0")))
+                                .user(User.builder()
+                                        .ext(ExtUser.builder().consent("consent").build())
+                                        .build())
+                                .regs(Regs.builder().coppa(0).ext(ExtRegs.of(1, null, null, null)).build())
+                                .build());
+    }
+
     @Test
     public void makeHttpRequestsShouldPassThroughImpExt() {
         // given
@@ -523,6 +655,48 @@ public void makeBidsShouldReturnResultWithExpectedFields() throws JsonProcessing
                         .build());
     }
 
+    @Test
+    public void makeBidsShouldReturnResultForNativeBidsWithExpectedFields() throws JsonProcessingException {
+        // given
+        final BidderCall<BidRequest> httpCall = givenHttpCall(mapper.writeValueAsString(OpenxBidResponse.builder()
+                .seatbid(singletonList(SeatBid.builder()
+                        .bid(singletonList(Bid.builder()
+                                .w(200)
+                                .h(150)
+                                .price(BigDecimal.ONE)
+                                .impid("impId1")
+                                .adm("{\"ver\":\"1.2\"}")
+                                .build()))
+                        .build()))
+                .cur("UAH")
+                .ext(OpenxBidResponseExt.of(Map.of("impId1", mapper.createObjectNode().put("somevalue", 1))))
+                .build()));
+
+        final BidRequest bidRequest = BidRequest.builder()
+                .id("bidRequestId")
+                .imp(singletonList(Imp.builder()
+                        .id("impId1")
+                        .xNative(Native.builder().request("{\"ver\":\"1.2\",\"plcmttype\":3}").build())
+                        .build()))
+                .build();
+
+        // when
+        final CompositeBidderResponse result = target.makeBidderResponse(httpCall, bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getBids()).hasSize(1)
+                .containsOnly(BidderBid.of(
+                        Bid.builder()
+                                .impid("impId1")
+                                .price(BigDecimal.ONE)
+                                .w(200)
+                                .h(150)
+                                .adm("{\"ver\":\"1.2\"}")
+                                .build(),
+                        BidType.xNative, "UAH"));
+    }
+
     @Test
     public void makeBidsShouldReturnVideoInfoWhenAvailable() throws JsonProcessingException {
         // given
diff --git a/src/test/java/org/prebid/server/bidder/pgamssp/PgamSspBidderTest.java b/src/test/java/org/prebid/server/bidder/pgamssp/PgamSspBidderTest.java
index e2b8c24ebd2..c349fd62cbe 100644
--- a/src/test/java/org/prebid/server/bidder/pgamssp/PgamSspBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/pgamssp/PgamSspBidderTest.java
@@ -8,16 +8,23 @@
 import com.iab.openrtb.response.BidResponse;
 import com.iab.openrtb.response.SeatBid;
 import io.vertx.core.http.HttpMethod;
+import org.assertj.core.api.BDDAssertions;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.VertxTest;
 import org.prebid.server.bidder.model.BidderBid;
 import org.prebid.server.bidder.model.BidderCall;
 import org.prebid.server.bidder.model.HttpRequest;
 import org.prebid.server.bidder.model.HttpResponse;
 import org.prebid.server.bidder.model.Result;
+import org.prebid.server.currency.CurrencyConversionService;
 import org.prebid.server.proto.openrtb.ext.ExtPrebid;
 import org.prebid.server.proto.openrtb.ext.request.pgamssp.PgamSspImpExt;
 
+import java.math.BigDecimal;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
@@ -27,6 +34,9 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
 import static org.prebid.server.bidder.model.BidderError.badInput;
 import static org.prebid.server.bidder.model.BidderError.badServerResponse;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
@@ -37,15 +47,46 @@
 import static org.prebid.server.util.HttpUtil.CONTENT_TYPE_HEADER;
 import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;
 
+@ExtendWith(MockitoExtension.class)
 public class PgamSspBidderTest extends VertxTest {
 
     private static final String ENDPOINT_URL = "http://test-url.com";
 
-    private final PgamSspBidder target = new PgamSspBidder(ENDPOINT_URL, jacksonMapper);
+    @Mock
+    private CurrencyConversionService currencyConversionService;
+
+    private PgamSspBidder target;
+
+    @BeforeEach
+    public void setUp() {
+        target = new PgamSspBidder(ENDPOINT_URL, currencyConversionService, jacksonMapper);
+    }
 
     @Test
     public void creationShouldFailOnInvalidEndpointUrl() {
-        assertThatIllegalArgumentException().isThrownBy(() -> new PgamSspBidder("invalid_url", jacksonMapper));
+        assertThatIllegalArgumentException().isThrownBy(() ->
+                new PgamSspBidder("invalid_url", currencyConversionService, jacksonMapper));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldConvertCurrencyIfRequestCurrencyDoesNotMatchBidderCurrency() {
+        // given
+        given(currencyConversionService.convertCurrency(any(), any(), anyString(), anyString()))
+                .willReturn(BigDecimal.TEN);
+
+        final BidRequest bidRequest = givenBidRequest(
+                impBuilder -> impBuilder.bidfloor(BigDecimal.ONE).bidfloorcur("EUR"));
+
+        // when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getBidfloor, Imp::getBidfloorcur)
+                .containsExactly(BDDAssertions.tuple(BigDecimal.TEN, "USD"));
     }
 
     @Test
diff --git a/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java b/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java
index 2f0b5ccd49a..66a1888a2a4 100644
--- a/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/pubmatic/PubmaticBidderTest.java
@@ -34,6 +34,8 @@
 import org.prebid.server.bidder.pubmatic.model.response.PubmaticExtBidResponse;
 import org.prebid.server.bidder.pubmatic.model.response.VideoCreativeInfo;
 import org.prebid.server.proto.openrtb.ext.ExtPrebid;
+import org.prebid.server.proto.openrtb.ext.request.ExtApp;
+import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
 import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmatic;
@@ -198,9 +200,7 @@ public void makeHttpRequestsShouldReturnBidRequestExtIfAcatFieldIsValidAndTrimWh
         final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
 
         // then
-        final ExtRequest expectedExtRequest = ExtRequest.of(ExtRequestPrebid.builder()
-                .bidderparams(pubmaticNode)
-                .build());
+        final ExtRequest expectedExtRequest = ExtRequest.empty();
         expectedExtRequest.addProperty("acat",
                 mapper.createArrayNode().add("te st Value").add("test Value").add("Value"));
 
@@ -884,6 +884,54 @@ public void makeHttpRequestsShouldSetAppPublisherIdIfSiteIsNull() {
                 .containsExactly("pub id");
     }
 
+    @Test
+    public void makeHttpRequestsShouldSetDisplayManagerFieldsFromAppExtPrebid() {
+        // given
+        final ExtApp extApp = ExtApp.of(ExtAppPrebid.of("ext-prebid-source", "ext-prebid-version"), null);
+        extApp.addProperty("source", TextNode.valueOf("ext-source"));
+        extApp.addProperty("version", TextNode.valueOf("ext-version"));
+
+        final BidRequest bidRequest = givenBidRequest(
+                bidRequestBuilder -> bidRequestBuilder.app(App.builder().ext(extApp).build()),
+                identity(),
+                identity());
+
+        // when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getDisplaymanager, Imp::getDisplaymanagerver)
+                .containsExactly(tuple("ext-prebid-source", "ext-prebid-version"));
+    }
+
+    @Test
+    public void makeHttpRequestsShouldSetDisplayManagerFieldsFromAppExt() {
+        // given
+        final ExtApp extApp = ExtApp.of(ExtAppPrebid.of("ext-prebid-source", null), null);
+        extApp.addProperty("source", TextNode.valueOf("ext-source"));
+        extApp.addProperty("version", TextNode.valueOf("ext-version"));
+
+        final BidRequest bidRequest = givenBidRequest(
+                bidRequestBuilder -> bidRequestBuilder.app(App.builder().ext(extApp).build()),
+                identity(),
+                identity());
+
+        // when
+        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .extracting(HttpRequest::getPayload)
+                .flatExtracting(BidRequest::getImp)
+                .extracting(Imp::getDisplaymanager, Imp::getDisplaymanagerver)
+                .containsExactly(tuple("ext-source", "ext-version"));
+    }
+
     @Test
     public void makeHttpRequestsShouldSUpdateAppPublisherIdExtPublisherIdIsPresent() {
         // given
@@ -1279,7 +1327,11 @@ private static ExtRequest expectedBidRequestExt(ExtRequest originalExtRequest,
         final ObjectNode wrapperNode = mapper.createObjectNode()
                 .set("wrapper", mapper.valueToTree(PubmaticWrapper.of(wrapperProfile, wrapperVersion)));
 
-        return jacksonMapper.fillExtension(originalExtRequest, wrapperNode);
+        final ExtRequest extRequestWithoutPrebid = jacksonMapper.fillExtension(
+                ExtRequest.empty(),
+                originalExtRequest.getProperties());
+
+        return jacksonMapper.fillExtension(extRequestWithoutPrebid, wrapperNode);
     }
 
     private static BidRequest givenBidRequest(UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer,
diff --git a/src/test/java/org/prebid/server/bidder/rise/RiseBidderTest.java b/src/test/java/org/prebid/server/bidder/rise/RiseBidderTest.java
index eb9f661d046..ade89a7e95a 100644
--- a/src/test/java/org/prebid/server/bidder/rise/RiseBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/rise/RiseBidderTest.java
@@ -28,6 +28,7 @@
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.banner;
 import static org.prebid.server.proto.openrtb.ext.response.BidType.video;
+import static org.prebid.server.proto.openrtb.ext.response.BidType.xNative;
 
 public class RiseBidderTest extends VertxTest {
 
@@ -214,6 +215,21 @@ public void makeBidsShouldReturnVideoBidIfMTypeIsTwo() throws JsonProcessingExce
         assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(2).build(), video, "USD"));
     }
 
+    @Test
+    public void makeBidsShouldReturnNativeBidIfMTypeIsFour() throws JsonProcessingException {
+        // given
+        final BidderCall<BidRequest> httpCall = givenHttpCall(
+                BidRequest.builder().imp(singletonList(Imp.builder().id("123").build())).build(),
+                mapper.writeValueAsString(givenBidResponse(Bid.builder().mtype(4).build())));
+
+        // when
+        final Result<List<BidderBid>> result = target.makeBids(httpCall, null);
+
+        // then
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue()).containsOnly(BidderBid.of(Bid.builder().mtype(4).build(), xNative, "USD"));
+    }
+
     @Test
     public void makeBidsShouldReturnErrorsForBidsThatDoesNotContainMType() throws JsonProcessingException {
         // given
diff --git a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java
index 7658e5be17a..d49f874c0e8 100644
--- a/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/rubicon/RubiconBidderTest.java
@@ -3305,6 +3305,34 @@ public void makeBidsShouldReplaceNotPresentAdmWithAdmNative() throws JsonProcess
                 .containsExactly("{\"admNativeProperty\":\"admNativeValue\"}");
     }
 
+    @Test
+    public void makeBidsShouldSetSeatToMetaSeat() throws JsonProcessingException {
+        // given
+        final BidderCall<BidRequest> httpCall = givenHttpCall(
+                givenBidRequest(identity()),
+                mapper.writeValueAsString(RubiconBidResponse.builder()
+                        .cur("USD")
+                        .seatbid(singletonList(RubiconSeatBid.builder()
+                                .seat("seat")
+                                .bid(singletonList(givenRubiconBid(bid -> bid.price(ONE))))
+                                .build()))
+                        .build()));
+
+        // when
+        final Result<List<BidderBid>> result = target.makeBids(httpCall, givenBidRequest(identity()));
+
+        // then
+        final ObjectNode expectedBidExt = mapper.valueToTree(
+                ExtPrebid.of(ExtBidPrebid.builder()
+                        .meta(ExtBidPrebidMeta.builder().seat("seat").build())
+                        .build(), null));
+        assertThat(result.getErrors()).isEmpty();
+        assertThat(result.getValue())
+                .extracting(BidderBid::getBid)
+                .extracting(Bid::getExt)
+                .containsExactly(expectedBidExt);
+    }
+
     @Test
     public void makeBidsShouldSetSeatBuyerToMetaNetworkId() throws JsonProcessingException {
         // given
diff --git a/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java b/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java
index 18b1ba36e8a..d34fd2812a0 100644
--- a/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java
+++ b/src/test/java/org/prebid/server/bidder/smaato/SmaatoBidderTest.java
@@ -1,6 +1,7 @@
 package org.prebid.server.bidder.smaato;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.node.IntNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.fasterxml.jackson.databind.node.TextNode;
 import com.iab.openrtb.request.App;
@@ -136,7 +137,7 @@ public void makeHttpRequestsShouldSetExt() {
                 .extracting(HttpRequest::getPayload)
                 .extracting(BidRequest::getExt)
                 .containsExactly(jacksonMapper.fillExtension(ExtRequest.empty(),
-                        SmaatoBidRequestExt.of("prebid_server_1.1")));
+                        SmaatoBidRequestExt.of("prebid_server_1.2")));
     }
 
     @Test
@@ -352,7 +353,7 @@ public void makeIndividualHttpRequestsShouldCorrectlySplitImps() {
     }
 
     @Test
-    public void makeHttpShouldPassthouthImpExtSkadnWhenIsPresent() {
+    public void makeHttpShouldPassthouthImpExt() {
         // given
         final ObjectNode impExt = mapper.createObjectNode()
                 .set("bidder", mapper.createObjectNode()
@@ -364,6 +365,8 @@ public void makeHttpShouldPassthouthImpExtSkadnWhenIsPresent() {
                 .put("fieldOne", "123")
                 .put("fieldTwo", "321"));
 
+        impExt.set("gpid", IntNode.valueOf(1));
+
         // and
         final BidRequest bidRequest = givenBidRequest(identity(), impBuilder -> impBuilder.ext(impExt));
 
@@ -371,34 +374,16 @@ public void makeHttpShouldPassthouthImpExtSkadnWhenIsPresent() {
         final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
 
         // then
+        final ObjectNode expectedImpExt = mapper.createObjectNode();
+        expectedImpExt.set("gpid", IntNode.valueOf(1));
+        expectedImpExt.set("skadn", impExt.get("skadn").deepCopy());
+
         assertThat(result.getErrors()).isEmpty();
         assertThat(result.getValue()).hasSize(1)
                 .extracting(HttpRequest::getPayload)
                 .flatExtracting(BidRequest::getImp)
                 .extracting(Imp::getExt)
-                .containsExactly(mapper.createObjectNode().set("skadn", impExt.get("skadn").deepCopy()));
-    }
-
-    @Test
-    public void makeHttpShouldReturnErrorWhenImpExtSkadnInvalidPresent() {
-        // given
-        final ObjectNode impExt = mapper.createObjectNode()
-                .set("bidder", mapper.createObjectNode()
-                        .put("publisherId", "publisherId")
-                        .put("adspaceId", "adspaceId")
-                        .put("adbreakId", "adbreakId"));
-
-        impExt.put("skadn", "invalidValue");
-
-        // and
-        final BidRequest bidRequest = givenBidRequest(identity(), impBuilder -> impBuilder.ext(impExt));
-
-        // when
-        final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);
-
-        // then
-        assertThat(result.getValue()).isEmpty();
-        assertThat(result.getErrors()).containsExactly(BidderError.badInput("Invalid imp.ext.skadn"));
+                .containsExactly(expectedImpExt);
     }
 
     @Test
diff --git a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java
index 8a55773a0c0..058f958fa48 100644
--- a/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java
+++ b/src/test/java/org/prebid/server/cache/CoreCacheServiceTest.java
@@ -8,6 +8,7 @@
 import com.iab.openrtb.request.Video;
 import com.iab.openrtb.response.Bid;
 import io.vertx.core.Future;
+import io.vertx.core.MultiMap;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -31,8 +32,8 @@
 import org.prebid.server.events.EventsContext;
 import org.prebid.server.events.EventsService;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.identity.UUIDIdGenerator;
 import org.prebid.server.metric.MetricName;
 import org.prebid.server.metric.Metrics;
@@ -40,6 +41,7 @@
 import org.prebid.server.proto.openrtb.ext.response.BidType;
 import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
 import org.prebid.server.settings.model.Account;
+import org.prebid.server.util.HttpUtil;
 import org.prebid.server.vast.VastModifier;
 import org.prebid.server.vertx.httpclient.HttpClient;
 import org.prebid.server.vertx.httpclient.model.HttpClientResponse;
@@ -107,6 +109,8 @@ public void setUp() throws MalformedURLException, JsonProcessingException {
                 new URL("http://cache-service/cache"),
                 "http://cache-service-host/cache?uuid=",
                 100L,
+                null,
+                false,
                 vastModifier,
                 eventsService,
                 metrics,
@@ -371,6 +375,40 @@ public void cacheBidsOpenrtbShouldReturnExpectedDebugInfo() throws JsonProcessin
                         .build());
     }
 
+    @Test
+    public void cacheBidsOpenrtbShouldUseApiKeyWhenProvided() throws MalformedURLException {
+        // given
+        target = new CoreCacheService(
+                httpClient,
+                new URL("http://cache-service/cache"),
+                "http://cache-service-host/cache?uuid=",
+                100L,
+                "ApiKey",
+                true,
+                vastModifier,
+                eventsService,
+                metrics,
+                clock,
+                idGenerator,
+                jacksonMapper);
+        final BidInfo bidinfo = givenBidInfo(builder -> builder.id("bidId1"));
+
+        // when
+        final Future<CacheServiceResult> future = target.cacheBidsOpenrtb(
+                singletonList(bidinfo),
+                givenAuctionContext(),
+                CacheContext.builder()
+                        .shouldCacheBids(true)
+                        .build(),
+                eventsContext);
+
+        // then
+        assertThat(future.result().getHttpCall().getRequestHeaders().get(HttpUtil.X_PBC_API_KEY_HEADER.toString()))
+                .containsExactly("ApiKey");
+        assertThat(captureBidCacheRequestHeaders().get(HttpUtil.X_PBC_API_KEY_HEADER.toString()))
+                .isEqualTo("ApiKey");
+    }
+
     @Test
     public void cacheBidsOpenrtbShouldReturnExpectedCacheBids() {
         // given
@@ -694,7 +732,7 @@ public void cachePutObjectsShouldReturnResultWithEmptyListWhenPutObjectsIsEmpty(
     }
 
     @Test
-    public void cachePutObjectsShouldModifyVastAndCachePutObjects() throws IOException {
+    public void cachePutObjectsShould() throws IOException {
         // given
         final BidPutObject firstBidPutObject = BidPutObject.builder()
                 .type("json")
@@ -762,6 +800,45 @@ public void cachePutObjectsShouldModifyVastAndCachePutObjects() throws IOExcepti
                 .containsExactly(modifiedFirstBidPutObject, modifiedSecondBidPutObject, modifiedThirdBidPutObject);
     }
 
+    @Test
+    public void cachePutObjectsShouldUseApiKeyWhenProvided() throws MalformedURLException {
+        // given
+        target = new CoreCacheService(
+                httpClient,
+                new URL("http://cache-service/cache"),
+                "http://cache-service-host/cache?uuid=",
+                100L,
+                "ApiKey",
+                true,
+                vastModifier,
+                eventsService,
+                metrics,
+                clock,
+                idGenerator,
+                jacksonMapper);
+
+        final BidPutObject firstBidPutObject = BidPutObject.builder()
+                .type("json")
+                .bidid("bidId1")
+                .bidder("bidder1")
+                .timestamp(1L)
+                .value(new TextNode("vast"))
+                .build();
+
+        // when
+        target.cachePutObjects(
+                asList(firstBidPutObject),
+                true,
+                singleton("bidder1"),
+                "account",
+                "pbjs",
+                timeout);
+
+        // then
+        assertThat(captureBidCacheRequestHeaders().get(HttpUtil.X_PBC_API_KEY_HEADER.toString()))
+                .isEqualTo("ApiKey");
+    }
+
     private AuctionContext givenAuctionContext(UnaryOperator<Account.AccountBuilder> accountCustomizer,
                                                UnaryOperator<BidRequest.BidRequestBuilder> bidRequestCustomizer) {
 
@@ -850,6 +927,12 @@ private BidCacheRequest captureBidCacheRequest() throws IOException {
         return mapper.readValue(captor.getValue(), BidCacheRequest.class);
     }
 
+    private MultiMap captureBidCacheRequestHeaders() {
+        final ArgumentCaptor<MultiMap> captor = ArgumentCaptor.forClass(MultiMap.class);
+        verify(httpClient).post(anyString(), captor.capture(), anyString(), anyLong());
+        return captor.getValue();
+    }
+
     private Map<String, List<String>> givenDebugHeaders() {
         final Map<String, List<String>> headers = new HashMap<>();
         headers.put("Accept", singletonList("application/json"));
diff --git a/src/test/java/org/prebid/server/execution/file/supplier/LocalFileSupplierTest.java b/src/test/java/org/prebid/server/execution/file/supplier/LocalFileSupplierTest.java
new file mode 100644
index 00000000000..407e01c6bf5
--- /dev/null
+++ b/src/test/java/org/prebid/server/execution/file/supplier/LocalFileSupplierTest.java
@@ -0,0 +1,68 @@
+package org.prebid.server.execution.file.supplier;
+
+import io.vertx.core.Future;
+import io.vertx.core.file.FileProps;
+import io.vertx.core.file.FileSystem;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.assertion.FutureAssertion;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+
+@ExtendWith(MockitoExtension.class)
+public class LocalFileSupplierTest {
+
+    @Mock
+    private FileSystem fileSystem;
+
+    private LocalFileSupplier target;
+
+    @BeforeEach
+    public void setUp() {
+        given(fileSystem.exists(anyString())).willReturn(Future.succeededFuture(true));
+
+        target = new LocalFileSupplier("/path/to/file", fileSystem);
+    }
+
+    @Test
+    public void getShouldReturnFailedFutureIfFileNotFound() {
+        // given
+        given(fileSystem.exists(anyString())).willReturn(Future.succeededFuture(false));
+
+        // when and then
+        FutureAssertion.assertThat(target.get()).isFailed().hasMessage("File /path/to/file not found.");
+    }
+
+    @Test
+    public void getShouldReturnFilePath() {
+        // given
+        final FileProps fileProps = mock(FileProps.class);
+        given(fileSystem.props(anyString())).willReturn(Future.succeededFuture(fileProps));
+        given(fileProps.creationTime()).willReturn(1000L);
+
+        // when and then
+        assertThat(target.get().result()).isEqualTo("/path/to/file");
+    }
+
+    @Test
+    public void getShouldReturnNullIfFileNotModifiedSinceLastTry() {
+        // given
+        final FileProps fileProps = mock(FileProps.class);
+        given(fileSystem.props(anyString())).willReturn(Future.succeededFuture(fileProps));
+        given(fileProps.creationTime()).willReturn(1000L);
+
+        // when
+        target.get();
+        final Future<String> result = target.get();
+
+        // then
+        assertThat(result.succeeded()).isTrue();
+        assertThat(result.result()).isNull();
+    }
+}
diff --git a/src/test/java/org/prebid/server/execution/file/supplier/RemoteFileSupplierTest.java b/src/test/java/org/prebid/server/execution/file/supplier/RemoteFileSupplierTest.java
new file mode 100644
index 00000000000..d60ff696ba7
--- /dev/null
+++ b/src/test/java/org/prebid/server/execution/file/supplier/RemoteFileSupplierTest.java
@@ -0,0 +1,244 @@
+package org.prebid.server.execution.file.supplier;
+
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.core.file.AsyncFile;
+import io.vertx.core.file.CopyOptions;
+import io.vertx.core.file.FileProps;
+import io.vertx.core.file.FileSystem;
+import io.vertx.core.http.HttpClient;
+import io.vertx.core.http.HttpClientRequest;
+import io.vertx.core.http.HttpClientResponse;
+import io.vertx.core.http.HttpHeaders;
+import io.vertx.core.http.HttpMethod;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.prebid.server.assertion.FutureAssertion;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class RemoteFileSupplierTest {
+
+    private static final String SAVE_PATH = "/path/to/file";
+    private static final String BACKUP_PATH = SAVE_PATH + ".old";
+    private static final String TMP_PATH = "/path/to/tmp";
+
+    @Mock
+    private HttpClient httpClient;
+
+    @Mock
+    private FileSystem fileSystem;
+
+    private RemoteFileSupplier target;
+
+    @Mock
+    private HttpClientResponse getResponse;
+
+    @Mock
+    private HttpClientResponse headResponse;
+
+    @BeforeEach
+    public void setUp() {
+        final HttpClientRequest getRequest = mock(HttpClientRequest.class);
+        given(httpClient.request(argThat(requestOptions ->
+                requestOptions != null && requestOptions.getMethod().equals(HttpMethod.GET))))
+                .willReturn(Future.succeededFuture(getRequest));
+        given(getRequest.send()).willReturn(Future.succeededFuture(getResponse));
+
+        final HttpClientRequest headRequest = mock(HttpClientRequest.class);
+        given(httpClient.request(argThat(requestOptions ->
+                requestOptions != null && requestOptions.getMethod().equals(HttpMethod.HEAD))))
+                .willReturn(Future.succeededFuture(headRequest));
+        given(headRequest.send()).willReturn(Future.succeededFuture(headResponse));
+        given(headResponse.statusCode()).willReturn(200);
+
+        target = target(false);
+    }
+
+    private RemoteFileSupplier target(boolean checkRemoteFileSize) {
+        return new RemoteFileSupplier(
+                "https://download.url/",
+                SAVE_PATH,
+                TMP_PATH,
+                httpClient,
+                1000L,
+                checkRemoteFileSize,
+                fileSystem);
+    }
+
+    @Test
+    public void shouldCheckWritePermissionsForFiles() {
+        // given
+        reset(fileSystem);
+        final FileProps fileProps = mock(FileProps.class);
+        given(fileSystem.existsBlocking(anyString())).willReturn(true);
+        given(fileSystem.propsBlocking(anyString())).willReturn(fileProps);
+        given(fileProps.isDirectory()).willReturn(false);
+
+        // when
+        target(false);
+
+        // then
+        verify(fileSystem, times(3)).mkdirsBlocking(anyString());
+    }
+
+    @Test
+    public void getShouldReturnFailureWhenCanNotOpenTmpFile() {
+        // given
+        given(fileSystem.open(eq(TMP_PATH), any())).willReturn(Future.failedFuture("Failure."));
+        given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Promise.<Boolean>promise().future());
+
+        // when
+        final Future<String> result = target.get();
+
+        // then
+        FutureAssertion.assertThat(result).isFailed().hasMessage("Failure.");
+    }
+
+    @Test
+    public void getShouldReturnFailureOnNotOkStatusCode() {
+        // given
+        final AsyncFile tmpFile = mock(AsyncFile.class);
+        given(fileSystem.open(eq(TMP_PATH), any())).willReturn(Future.succeededFuture(tmpFile));
+        given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Promise.<Boolean>promise().future());
+
+        given(getResponse.statusCode()).willReturn(204);
+
+        // when
+        final Future<String> result = target.get();
+
+        // then
+        FutureAssertion.assertThat(result).isFailed()
+                .hasMessage("Got unexpected response from server with status code 204 and message null");
+    }
+
+    @Test
+    public void getShouldReturnExpectedResult() {
+        // given
+        final AsyncFile tmpFile = mock(AsyncFile.class);
+        given(fileSystem.open(eq(TMP_PATH), any())).willReturn(Future.succeededFuture(tmpFile));
+        given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Future.succeededFuture(true));
+        given(fileSystem.move(eq(SAVE_PATH), eq(BACKUP_PATH), Mockito.<CopyOptions>any()))
+                .willReturn(Future.succeededFuture());
+        given(fileSystem.move(eq(TMP_PATH), eq(SAVE_PATH), Mockito.<CopyOptions>any()))
+                .willReturn(Future.succeededFuture());
+
+        given(getResponse.statusCode()).willReturn(200);
+        given(getResponse.pipeTo(any())).willReturn(Future.succeededFuture());
+
+        // when
+        final Future<String> result = target.get();
+
+        // then
+        verify(tmpFile).close();
+        assertThat(result.result()).isEqualTo(SAVE_PATH);
+    }
+
+    @Test
+    public void getShouldReturnExpectedResultWhenCheckRemoteFileSizeIsTrue() {
+        // given
+        target = target(true);
+
+        final FileProps fileProps = mock(FileProps.class);
+        given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Future.succeededFuture(true));
+        given(fileSystem.props(eq(SAVE_PATH))).willReturn(Future.succeededFuture(fileProps));
+        given(fileProps.size()).willReturn(1000L);
+
+        given(headResponse.statusCode()).willReturn(200);
+        given(headResponse.getHeader(eq(HttpHeaders.CONTENT_LENGTH))).willReturn("1001");
+
+        final AsyncFile tmpFile = mock(AsyncFile.class);
+        given(fileSystem.open(eq(TMP_PATH), any())).willReturn(Future.succeededFuture(tmpFile));
+        given(fileSystem.move(eq(SAVE_PATH), eq(BACKUP_PATH), Mockito.<CopyOptions>any()))
+                .willReturn(Future.succeededFuture());
+        given(fileSystem.move(eq(TMP_PATH), eq(SAVE_PATH), Mockito.<CopyOptions>any()))
+                .willReturn(Future.succeededFuture());
+
+        given(getResponse.statusCode()).willReturn(200);
+        given(getResponse.pipeTo(any())).willReturn(Future.succeededFuture());
+
+        // when
+        final Future<String> result = target.get();
+
+        // then
+        verify(tmpFile).close();
+        assertThat(result.result()).isEqualTo(SAVE_PATH);
+    }
+
+    @Test
+    public void getShouldReturnNullWhenCheckRemoteFileSizeIsTrueAndSizeNotChanged() {
+        // given
+        target = target(true);
+
+        final FileProps fileProps = mock(FileProps.class);
+        given(fileSystem.exists(eq(SAVE_PATH))).willReturn(Future.succeededFuture(true));
+        given(fileSystem.props(eq(SAVE_PATH))).willReturn(Future.succeededFuture(fileProps));
+        given(fileProps.size()).willReturn(1000L);
+
+        given(headResponse.statusCode()).willReturn(200);
+        given(headResponse.getHeader(eq(HttpHeaders.CONTENT_LENGTH))).willReturn("1000");
+
+        // when
+        final Future<String> result = target.get();
+
+        // then
+        assertThat(result.result()).isNull();
+    }
+
+    @Test
+    public void clearTmpShouldCallExpectedMethods() {
+        // given
+        given(fileSystem.exists(eq(TMP_PATH))).willReturn(Future.succeededFuture(true));
+        given(fileSystem.delete(eq(TMP_PATH))).willReturn(Future.succeededFuture());
+
+        // when
+        target.clearTmp();
+
+        // then
+        verify(fileSystem).delete(TMP_PATH);
+    }
+
+    @Test
+    public void deleteBackupShouldCallExpectedMethods() {
+        // given
+        given(fileSystem.exists(eq(BACKUP_PATH))).willReturn(Future.succeededFuture(true));
+        given(fileSystem.delete(eq(BACKUP_PATH))).willReturn(Future.succeededFuture());
+
+        // when
+        target.deleteBackup();
+
+        // then
+        verify(fileSystem).delete(BACKUP_PATH);
+    }
+
+    @Test
+    public void restoreFromBackupShouldCallExpectedMethods() {
+        // given
+        given(fileSystem.exists(eq(BACKUP_PATH))).willReturn(Future.succeededFuture(true));
+        given(fileSystem.move(eq(BACKUP_PATH), eq(SAVE_PATH))).willReturn(Future.succeededFuture());
+        given(fileSystem.delete(eq(BACKUP_PATH))).willReturn(Future.succeededFuture());
+
+        // when
+        target.deleteBackup();
+
+        // then
+        verify(fileSystem).delete(BACKUP_PATH);
+    }
+}
diff --git a/src/test/java/org/prebid/server/execution/file/syncer/FileSyncerTest.java b/src/test/java/org/prebid/server/execution/file/syncer/FileSyncerTest.java
new file mode 100644
index 00000000000..39a8e8ae966
--- /dev/null
+++ b/src/test/java/org/prebid/server/execution/file/syncer/FileSyncerTest.java
@@ -0,0 +1,160 @@
+package org.prebid.server.execution.file.syncer;
+
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Promise;
+import io.vertx.core.Vertx;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.prebid.server.execution.file.FileProcessor;
+import org.prebid.server.execution.retry.FixedIntervalRetryPolicy;
+import org.prebid.server.execution.retry.NonRetryable;
+import org.prebid.server.execution.retry.RetryPolicy;
+import org.testcontainers.shaded.org.apache.commons.lang3.NotImplementedException;
+
+import java.util.concurrent.Callable;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class FileSyncerTest {
+
+    private static final String SAVE_PATH = "/path/to/file";
+
+    @Mock
+    private FileProcessor fileProcessor;
+
+    @Mock
+    private Vertx vertx;
+
+    @BeforeEach
+    public void setUp() {
+        given(vertx.executeBlocking(Mockito.<Callable<?>>any())).willAnswer(invocation -> {
+            try {
+                return Future.succeededFuture(((Callable<?>) invocation.getArgument(0)).call());
+            } catch (Throwable e) {
+                return Future.failedFuture(e);
+            }
+        });
+    }
+
+    @Test
+    public void syncShouldCallExpectedMethodsOnSuccessWhenNoReturnedFile() {
+        // given
+        final FileSyncer fileSyncer = fileSyncer(NonRetryable.instance());
+        given(fileSyncer.getFile()).willReturn(Future.succeededFuture());
+
+        // when
+        fileSyncer.sync();
+
+        // then
+        verifyNoInteractions(fileProcessor);
+        verify(fileSyncer).doOnSuccess();
+        verify(vertx).setTimer(eq(1000L), any());
+    }
+
+    @Test
+    public void syncShouldCallExpectedMethodsOnSuccess() {
+        // given
+        final FileSyncer fileSyncer = fileSyncer(NonRetryable.instance());
+        given(fileSyncer.getFile()).willReturn(Future.succeededFuture(SAVE_PATH));
+        given(fileProcessor.setDataPath(eq(SAVE_PATH))).willReturn(Future.succeededFuture());
+
+        // when
+        fileSyncer.sync();
+
+        // then
+        verify(fileProcessor).setDataPath(eq(SAVE_PATH));
+        verify(fileSyncer).doOnSuccess();
+        verify(vertx).setTimer(eq(1000L), any());
+    }
+
+    @Test
+    public void syncShouldCallExpectedMethodsOnFailure() {
+        // given
+        final FileSyncer fileSyncer = fileSyncer(NonRetryable.instance());
+        given(fileSyncer.getFile()).willReturn(Future.succeededFuture(SAVE_PATH));
+        given(fileProcessor.setDataPath(eq(SAVE_PATH))).willReturn(Future.failedFuture("Failure"));
+
+        // when
+        fileSyncer.sync();
+
+        // then
+        verify(fileProcessor).setDataPath(eq(SAVE_PATH));
+        verify(fileSyncer).doOnFailure(any());
+        verify(vertx).setTimer(eq(1000L), any());
+    }
+
+    @Test
+    public void syncShouldCallExpectedMethodsOnFailureWithRetryable() {
+        // given
+        final FileSyncer fileSyncer = fileSyncer(FixedIntervalRetryPolicy.limited(10L, 1));
+        given(fileSyncer.getFile()).willReturn(Future.succeededFuture(SAVE_PATH));
+        given(fileProcessor.setDataPath(eq(SAVE_PATH))).willReturn(Future.failedFuture("Failure"));
+
+        final Promise<Void> promise = Promise.promise();
+        given(vertx.setTimer(eq(10L), any())).willAnswer(invocation -> {
+            promise.future().onComplete(ignore -> ((Handler<Long>) invocation.getArgument(1)).handle(1L));
+            return 1L;
+        });
+
+        // when
+        fileSyncer.sync();
+
+        // then
+        verify(fileProcessor).setDataPath(eq(SAVE_PATH));
+        verify(fileSyncer).doOnFailure(any());
+        verify(vertx).setTimer(eq(10L), any());
+
+        // when
+        promise.complete();
+
+        // then
+        verify(fileProcessor, times(2)).setDataPath(eq(SAVE_PATH));
+        verify(fileSyncer, times(2)).doOnFailure(any());
+        verify(vertx).setTimer(eq(1000L), any());
+    }
+
+    private FileSyncer fileSyncer(RetryPolicy retryPolicy) {
+        return spy(new TestFileSyncer(fileProcessor, 1000L, retryPolicy, vertx));
+    }
+
+    private static class TestFileSyncer extends FileSyncer {
+
+        protected TestFileSyncer(FileProcessor fileProcessor,
+                                 long updatePeriod,
+                                 RetryPolicy retryPolicy,
+                                 Vertx vertx) {
+
+            super(fileProcessor, updatePeriod, retryPolicy, vertx);
+        }
+
+        @Override
+        public Future<String> getFile() {
+            return Future.failedFuture(new NotImplementedException());
+        }
+
+        @Override
+        protected Future<Void> doOnSuccess() {
+            return Future.succeededFuture();
+        }
+
+        @Override
+        protected Future<Void> doOnFailure(Throwable throwable) {
+            return Future.succeededFuture();
+        }
+    }
+}
diff --git a/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java b/src/test/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerTest.java
similarity index 87%
rename from src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java
rename to src/test/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerTest.java
index 9acd719d30f..2da614c4e50 100644
--- a/src/test/java/org/prebid/server/execution/RemoteFileSyncerTest.java
+++ b/src/test/java/org/prebid/server/execution/file/syncer/RemoteFileSyncerTest.java
@@ -1,4 +1,4 @@
-package org.prebid.server.execution;
+package org.prebid.server.execution.file.syncer;
 
 import io.netty.handler.codec.http.HttpResponseStatus;
 import io.vertx.core.Future;
@@ -17,14 +17,17 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
+import org.mockito.Mockito;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.stubbing.Answer;
 import org.prebid.server.VertxTest;
 import org.prebid.server.exception.PreBidException;
+import org.prebid.server.execution.file.FileProcessor;
 import org.prebid.server.execution.retry.FixedIntervalRetryPolicy;
 import org.prebid.server.execution.retry.RetryPolicy;
 
 import java.io.File;
+import java.util.concurrent.Callable;
 
 import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
 import static org.assertj.core.api.Assertions.assertThatNullPointerException;
@@ -57,7 +60,8 @@ public class RemoteFileSyncerTest extends VertxTest {
     private static final String TMP_FILE_PATH = String.join(File.separator, "tmp", "fake", "path", "to", "file.pdf");
     private static final String DIR_PATH = String.join(File.separator, "fake", "path", "to");
     private static final Long FILE_SIZE = 2131242L;
-    @Mock
+
+    @Mock(strictness = LENIENT)
     private Vertx vertx;
 
     @Mock(strictness = LENIENT)
@@ -67,7 +71,7 @@ public class RemoteFileSyncerTest extends VertxTest {
     private HttpClient httpClient;
 
     @Mock(strictness = LENIENT)
-    private RemoteFileProcessor remoteFileProcessor;
+    private FileProcessor fileProcessor;
     @Mock
     private AsyncFile asyncFile;
 
@@ -85,30 +89,38 @@ public class RemoteFileSyncerTest extends VertxTest {
     @BeforeEach
     public void setUp() {
         when(vertx.fileSystem()).thenReturn(fileSystem);
-        remoteFileSyncer = new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
+        given(vertx.executeBlocking(Mockito.<Callable<?>>any())).willAnswer(invocation -> {
+            try {
+                return Future.succeededFuture(((Callable<?>) invocation.getArgument(0)).call());
+            } catch (Throwable e) {
+                return Future.failedFuture(e);
+            }
+        });
+
+        remoteFileSyncer = new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
                 TIMEOUT, 0, httpClient, vertx);
     }
 
     @Test
     public void shouldThrowNullPointerExceptionWhenIllegalArgumentsWhenNullArguments() {
         assertThatNullPointerException().isThrownBy(
-                () -> new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, null, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT,
+                () -> new RemoteFileSyncer(fileProcessor, SOURCE_URL, null, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT,
                         UPDATE_INTERVAL, httpClient, vertx));
         assertThatNullPointerException().isThrownBy(
-                () -> new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
+                () -> new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
                         TIMEOUT, UPDATE_INTERVAL, null, vertx));
         assertThatNullPointerException().isThrownBy(
-                () -> new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
+                () -> new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
                         TIMEOUT, UPDATE_INTERVAL, httpClient, null));
     }
 
     @Test
     public void shouldThrowIllegalArgumentExceptionWhenIllegalArguments() {
         assertThatIllegalArgumentException().isThrownBy(
-                () -> new RemoteFileSyncer(remoteFileProcessor, null, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
+                () -> new RemoteFileSyncer(fileProcessor, null, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
                         TIMEOUT, UPDATE_INTERVAL, httpClient, vertx));
         assertThatIllegalArgumentException().isThrownBy(
-                () -> new RemoteFileSyncer(remoteFileProcessor, "bad url", FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
+                () -> new RemoteFileSyncer(fileProcessor, "bad url", FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
                         TIMEOUT, UPDATE_INTERVAL, httpClient, vertx));
     }
 
@@ -119,7 +131,7 @@ public void creteShouldCreateDirWithWritePermissionIfDirNotExist() {
         when(fileSystem.existsBlocking(eq(DIR_PATH))).thenReturn(false);
 
         // when
-        new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT,
+        new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT,
                 UPDATE_INTERVAL, httpClient, vertx);
 
         // then
@@ -136,7 +148,7 @@ public void createShouldCreateDirWithWritePermissionIfItsNotDir() {
         when(fileProps.isDirectory()).thenReturn(false);
 
         // when
-        new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT,
+        new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY, TIMEOUT,
                 UPDATE_INTERVAL, httpClient, vertx);
 
         // then
@@ -151,7 +163,7 @@ public void createShouldThrowPreBidExceptionWhenPropsThrowException() {
         when(fileSystem.propsBlocking(eq(DIR_PATH))).thenThrow(FileSystemException.class);
 
         // when and then
-        assertThatThrownBy(() -> new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH,
+        assertThatThrownBy(() -> new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH,
                 RETRY_POLICY, TIMEOUT, UPDATE_INTERVAL, httpClient, vertx))
                 .isInstanceOf(PreBidException.class);
     }
@@ -167,14 +179,14 @@ public void syncForFilepathShouldNotTriggerServiceWhenCantCheckIfUsableFileExist
 
         // then
         verify(fileSystem).exists(eq(FILE_PATH));
-        verifyNoInteractions(remoteFileProcessor);
+        verifyNoInteractions(fileProcessor);
         verifyNoInteractions(httpClient);
     }
 
     @Test
     public void syncForFilepathShouldNotUpdateWhenHeadRequestReturnInvalidHead() {
         // given
-        remoteFileSyncer = new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
+        remoteFileSyncer = new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
                 TIMEOUT, UPDATE_INTERVAL, httpClient, vertx);
 
         givenTriggerUpdate();
@@ -194,7 +206,7 @@ public void syncForFilepathShouldNotUpdateWhenHeadRequestReturnInvalidHead() {
         // then
         verify(fileSystem, times(2)).exists(eq(FILE_PATH));
         verify(httpClient).request(any());
-        verify(remoteFileProcessor).setDataPath(any());
+        verify(fileProcessor).setDataPath(any());
         verify(fileSystem, never()).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(), any());
         verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any());
         verifyNoMoreInteractions(httpClient);
@@ -203,7 +215,7 @@ public void syncForFilepathShouldNotUpdateWhenHeadRequestReturnInvalidHead() {
     @Test
     public void syncForFilepathShouldNotUpdateWhenPropsIsFailed() {
         // given
-        remoteFileSyncer = new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
+        remoteFileSyncer = new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
                 TIMEOUT, UPDATE_INTERVAL, httpClient, vertx);
 
         givenTriggerUpdate();
@@ -228,7 +240,7 @@ public void syncForFilepathShouldNotUpdateWhenPropsIsFailed() {
         verify(httpClient).request(any());
         verify(httpClientResponse).getHeader(eq(HttpHeaders.CONTENT_LENGTH));
         verify(fileSystem).props(eq(FILE_PATH));
-        verify(remoteFileProcessor).setDataPath(any());
+        verify(fileProcessor).setDataPath(any());
         verify(fileSystem, never()).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class));
         verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any());
         verifyNoMoreInteractions(httpClient);
@@ -237,7 +249,7 @@ public void syncForFilepathShouldNotUpdateWhenPropsIsFailed() {
     @Test
     public void syncForFilepathShouldNotUpdateServiceWhenSizeEqualsContentLength() {
         // given
-        remoteFileSyncer = new RemoteFileSyncer(remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
+        remoteFileSyncer = new RemoteFileSyncer(fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
                 TIMEOUT, UPDATE_INTERVAL, httpClient, vertx);
 
         givenTriggerUpdate();
@@ -264,7 +276,7 @@ public void syncForFilepathShouldNotUpdateServiceWhenSizeEqualsContentLength() {
         verify(httpClient).request(any());
         verify(httpClientResponse).getHeader(eq(HttpHeaders.CONTENT_LENGTH));
         verify(fileSystem).props(eq(FILE_PATH));
-        verify(remoteFileProcessor).setDataPath(any());
+        verify(fileProcessor).setDataPath(any());
         verify(fileSystem, never()).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class));
         verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any());
         verifyNoMoreInteractions(httpClient);
@@ -274,7 +286,7 @@ public void syncForFilepathShouldNotUpdateServiceWhenSizeEqualsContentLength() {
     public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() {
         // given
         remoteFileSyncer = new RemoteFileSyncer(
-                remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
+                fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
                 TIMEOUT, UPDATE_INTERVAL, httpClient, vertx);
 
         givenTriggerUpdate();
@@ -304,7 +316,7 @@ public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() {
         given(fileSystem.move(anyString(), any(), any(CopyOptions.class)))
                 .willReturn(Future.succeededFuture());
 
-        given(remoteFileProcessor.setDataPath(anyString()))
+        given(fileProcessor.setDataPath(anyString()))
                 .willReturn(Future.succeededFuture());
 
         // when
@@ -319,7 +331,7 @@ public void syncForFilepathShouldUpdateServiceWhenSizeNotEqualsContentLength() {
         verify(fileSystem).open(eq(TMP_FILE_PATH), any());
         verify(asyncFile).close();
 
-        verify(remoteFileProcessor, times(2)).setDataPath(any());
+        verify(fileProcessor, times(2)).setDataPath(any());
         verify(vertx).setPeriodic(eq(UPDATE_INTERVAL), any());
         verify(fileSystem).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class));
         verifyNoMoreInteractions(httpClient);
@@ -347,7 +359,7 @@ public void syncForFilepathShouldRetryAfterFailedDownload() {
         verify(fileSystem, times(RETRY_COUNT + 1)).open(eq(TMP_FILE_PATH), any());
 
         verifyNoInteractions(httpClient);
-        verifyNoInteractions(remoteFileProcessor);
+        verifyNoInteractions(fileProcessor);
     }
 
     @Test
@@ -368,7 +380,7 @@ public void syncForFilepathShouldRetryWhenFileOpeningFailed() {
                 .willAnswer(withSelfAndPassObjectToHandler(Future.succeededFuture()))
                 .willAnswer(withSelfAndPassObjectToHandler(Future.failedFuture(new RuntimeException())));
 
-        given(remoteFileProcessor.setDataPath(anyString()))
+        given(fileProcessor.setDataPath(anyString()))
                 .willReturn(Future.succeededFuture());
 
         // when
@@ -379,13 +391,13 @@ public void syncForFilepathShouldRetryWhenFileOpeningFailed() {
         verify(fileSystem, times(RETRY_COUNT + 1)).delete(eq(TMP_FILE_PATH));
 
         verifyNoInteractions(httpClient);
-        verifyNoInteractions(remoteFileProcessor);
+        verifyNoInteractions(fileProcessor);
     }
 
     @Test
     public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotSet() {
         // given
-        given(remoteFileProcessor.setDataPath(anyString()))
+        given(fileProcessor.setDataPath(anyString()))
                 .willReturn(Future.succeededFuture());
 
         given(fileSystem.exists(anyString()))
@@ -414,7 +426,7 @@ public void syncForFilepathShouldDownloadFilesAndNotUpdateWhenUpdatePeriodIsNotS
         verify(httpClient).request(any());
         verify(asyncFile).close();
         verify(httpClientResponse).statusCode();
-        verify(remoteFileProcessor).setDataPath(any());
+        verify(fileProcessor).setDataPath(any());
         verify(fileSystem).move(eq(TMP_FILE_PATH), eq(FILE_PATH), any(CopyOptions.class));
         verify(vertx, never()).setTimer(eq(UPDATE_INTERVAL), any());
         verifyNoMoreInteractions(httpClient);
@@ -452,7 +464,7 @@ public void syncForFilepathShouldRetryWhenTimeoutIsReached() {
         verify(httpClient, times(RETRY_COUNT + 1)).request(any());
         verify(asyncFile, times(RETRY_COUNT + 1)).close();
 
-        verifyNoInteractions(remoteFileProcessor);
+        verifyNoInteractions(fileProcessor);
     }
 
     @Test
@@ -484,7 +496,7 @@ public void syncShouldNotSaveFileWhenServerRespondsWithNonOkStatusCode() {
         verify(httpClient).request(any());
         verify(httpClientResponse).statusCode();
         verify(httpClientResponse, never()).pipeTo(any());
-        verify(remoteFileProcessor, never()).setDataPath(any());
+        verify(fileProcessor, never()).setDataPath(any());
         verify(vertx, never()).setTimer(eq(UPDATE_INTERVAL), any());
     }
 
@@ -492,7 +504,7 @@ public void syncShouldNotSaveFileWhenServerRespondsWithNonOkStatusCode() {
     public void syncShouldNotUpdateFileWhenServerRespondsWithNonOkStatusCode() {
         // given
         remoteFileSyncer = new RemoteFileSyncer(
-                remoteFileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
+                fileProcessor, SOURCE_URL, FILE_PATH, TMP_FILE_PATH, RETRY_POLICY,
                 TIMEOUT, UPDATE_INTERVAL, httpClient, vertx);
 
         givenTriggerUpdate();
@@ -528,7 +540,7 @@ private void givenTriggerUpdate() {
         given(fileSystem.exists(anyString()))
                 .willReturn(Future.succeededFuture(true));
 
-        given(remoteFileProcessor.setDataPath(anyString()))
+        given(fileProcessor.setDataPath(anyString()))
                 .willReturn(Future.succeededFuture());
 
         given(vertx.setPeriodic(eq(UPDATE_INTERVAL), any()))
diff --git a/src/test/java/org/prebid/server/execution/TimeoutFactoryTest.java b/src/test/java/org/prebid/server/execution/timeout/TimeoutFactoryTest.java
similarity index 97%
rename from src/test/java/org/prebid/server/execution/TimeoutFactoryTest.java
rename to src/test/java/org/prebid/server/execution/timeout/TimeoutFactoryTest.java
index e38e4c7304d..9e4a140cb7b 100644
--- a/src/test/java/org/prebid/server/execution/TimeoutFactoryTest.java
+++ b/src/test/java/org/prebid/server/execution/timeout/TimeoutFactoryTest.java
@@ -1,4 +1,4 @@
-package org.prebid.server.execution;
+package org.prebid.server.execution.timeout;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
diff --git a/src/test/java/org/prebid/server/execution/TimeoutTest.java b/src/test/java/org/prebid/server/execution/timeout/TimeoutTest.java
similarity index 95%
rename from src/test/java/org/prebid/server/execution/TimeoutTest.java
rename to src/test/java/org/prebid/server/execution/timeout/TimeoutTest.java
index c45589bc86a..c1c7dfd7ca3 100644
--- a/src/test/java/org/prebid/server/execution/TimeoutTest.java
+++ b/src/test/java/org/prebid/server/execution/timeout/TimeoutTest.java
@@ -1,4 +1,4 @@
-package org.prebid.server.execution;
+package org.prebid.server.execution.timeout;
 
 import org.junit.jupiter.api.Test;
 
diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java
index 61ecb9825ac..724aef1f391 100644
--- a/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java
+++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorEnforcerTest.java
@@ -347,7 +347,9 @@ public void shouldRejectBidsHavingPriceBelowFloor() {
         // then
         verify(priceFloorAdjuster, times(2)).revertAdjustmentForImp(any(), any(), any(), any());
 
-        verify(rejectionTracker).reject("impId1", BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR);
+        final BidderBid rejectedBid = BidderBid.of(
+                Bid.builder().id("bidId1").impid("impId1").price(BigDecimal.ONE).build(), null, null);
+        verify(rejectionTracker).rejectBid(rejectedBid, BidRejectionReason.RESPONSE_REJECTED_BELOW_FLOOR);
         assertThat(singleton(result))
                 .extracting(AuctionParticipation::getBidderResponse)
                 .extracting(BidderResponse::getSeatBid)
diff --git a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java
index 0990d97b5aa..78ce82dd3fd 100644
--- a/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java
+++ b/src/test/java/org/prebid/server/floors/BasicPriceFloorProcessorTest.java
@@ -11,13 +11,13 @@
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.VertxTest;
-import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.floors.model.PriceFloorData;
 import org.prebid.server.floors.model.PriceFloorEnforcement;
 import org.prebid.server.floors.model.PriceFloorLocation;
 import org.prebid.server.floors.model.PriceFloorModelGroup;
 import org.prebid.server.floors.model.PriceFloorResult;
 import org.prebid.server.floors.model.PriceFloorRules;
+import org.prebid.server.floors.model.PriceFloorSchema;
 import org.prebid.server.floors.proto.FetchResult;
 import org.prebid.server.floors.proto.FetchStatus;
 import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors;
@@ -30,6 +30,7 @@
 import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.function.UnaryOperator;
 
 import static java.util.Collections.singletonList;
@@ -40,6 +41,8 @@
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
+import static org.prebid.server.floors.model.PriceFloorField.siteDomain;
+import static org.prebid.server.floors.model.PriceFloorField.size;
 
 @ExtendWith(MockitoExtension.class)
 public class BasicPriceFloorProcessorTest extends VertxTest {
@@ -118,9 +121,12 @@ public void shouldUseFloorsDataFromProviderIfPresent() {
     }
 
     @Test
-    public void shouldUseFloorsFromProviderIfUseDynamicDataIsNotPresent() {
+    public void shouldUseFloorsFromProviderIfUseDynamicDataAndUseFetchDataRateAreAbsent() {
         // given
-        final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com"));
+        final PriceFloorData providerFloorsData = givenFloorData(floors -> floors
+                .floorProvider("provider.com")
+                .useFetchDataRate(null));
+
         given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
 
         // when
@@ -142,9 +148,65 @@ public void shouldUseFloorsFromProviderIfUseDynamicDataIsNotPresent() {
     }
 
     @Test
-    public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrue() {
+    public void shouldUseFloorsFromProviderIfUseDynamicDataIsAbsentAndUseFetchDataRateIs100() {
         // given
-        final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com"));
+        final PriceFloorData providerFloorsData = givenFloorData(floors -> floors
+                .floorProvider("provider.com")
+                .useFetchDataRate(100));
+
+        given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+
+        // when
+        final BidRequest result = target.enrichWithPriceFloors(
+                givenBidRequest(identity(), null),
+                givenAccount(floorsConfig -> floorsConfig.useDynamicData(null)),
+                "bidder",
+                new ArrayList<>(),
+                new ArrayList<>());
+
+        // then
+        assertThat(extractFloors(result)).isEqualTo(givenFloors(floors -> floors
+                .enabled(true)
+                .skipped(false)
+                .floorProvider("provider.com")
+                .data(providerFloorsData)
+                .fetchStatus(FetchStatus.success)
+                .location(PriceFloorLocation.fetch)));
+    }
+
+    @Test
+    public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsAbsentAndUseFetchDataRateIs0() {
+        // given
+        final PriceFloorData providerFloorsData = givenFloorData(floors -> floors
+                .floorProvider("provider.com")
+                .useFetchDataRate(0));
+
+        given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+
+        // when
+        final BidRequest result = target.enrichWithPriceFloors(
+                givenBidRequest(identity(), null),
+                givenAccount(floorsConfig -> floorsConfig.useDynamicData(null)),
+                "bidder",
+                new ArrayList<>(),
+                new ArrayList<>());
+
+        // then
+        final PriceFloorRules actualRules = extractFloors(result);
+        assertThat(actualRules)
+                .extracting(PriceFloorRules::getFetchStatus)
+                .isEqualTo(FetchStatus.success);
+        assertThat(actualRules)
+                .extracting(PriceFloorRules::getLocation)
+                .isEqualTo(PriceFloorLocation.noData);
+    }
+
+    @Test
+    public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrueAndUseFetchDataRateIsAbsent() {
+        // given
+        final PriceFloorData providerFloorsData = givenFloorData(floors -> floors
+                .floorProvider("provider.com")
+                .useFetchDataRate(null));
         given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
 
         // when
@@ -167,9 +229,37 @@ public void shouldUseFloorsFromProviderIfUseDynamicDataIsTrue() {
     }
 
     @Test
-    public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalse() {
+    public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalseAndUseFetchDataRateIsAbsent() {
         // given
-        final PriceFloorData providerFloorsData = givenFloorData(floors -> floors.floorProvider("provider.com"));
+        final PriceFloorData providerFloorsData = givenFloorData(floors -> floors
+                .floorProvider("provider.com")
+                .useFetchDataRate(null));
+        given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
+
+        // when
+        final BidRequest result = target.enrichWithPriceFloors(
+                givenBidRequest(identity(), null),
+                givenAccount(floorsConfig -> floorsConfig.useDynamicData(false)),
+                "bidder",
+                new ArrayList<>(),
+                new ArrayList<>());
+
+        // then
+        final PriceFloorRules actualRules = extractFloors(result);
+        assertThat(actualRules)
+                .extracting(PriceFloorRules::getFetchStatus)
+                .isEqualTo(FetchStatus.success);
+        assertThat(actualRules)
+                .extracting(PriceFloorRules::getLocation)
+                .isEqualTo(PriceFloorLocation.noData);
+    }
+
+    @Test
+    public void shouldNotUseFloorsFromProviderIfUseDynamicDataIsFalseAndUseFetchDataRateIs100() {
+        // given
+        final PriceFloorData providerFloorsData = givenFloorData(floors -> floors
+                .floorProvider("provider.com")
+                .useFetchDataRate(100));
         given(priceFloorFetcher.fetch(any())).willReturn(FetchResult.of(providerFloorsData, FetchStatus.success));
 
         // when
@@ -296,6 +386,70 @@ public void shouldUseFloorsFromRequestIfProviderFloorsMissing() {
                         .location(PriceFloorLocation.request)));
     }
 
+    @Test
+    public void shouldTolerateUsingFloorsFromRequestWhenRulesNumberMoreThanMaxRulesNumber() {
+        // given
+        given(priceFloorFetcher.fetch(any())).willReturn(null);
+        final ArrayList<String> errors = new ArrayList<>();
+
+        // when
+        final BidRequest result = target.enrichWithPriceFloors(
+                givenBidRequest(identity(), givenFloors(floors -> floors.data(
+                        PriceFloorData.builder()
+                                .modelGroups(singletonList(PriceFloorModelGroup.builder()
+                                        .values(Map.of("someKey", BigDecimal.ONE, "someKey2", BigDecimal.ONE))
+                                        .schema(PriceFloorSchema.of("|", List.of(size, siteDomain)))
+                                        .build()))
+                                .build())
+                )),
+                givenAccount(floorConfigBuilder -> floorConfigBuilder.maxRules(1L)),
+                "bidder",
+                errors,
+                new ArrayList<>());
+
+        // then
+        assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+                .enabled(true)
+                .skipped(false)
+                .location(PriceFloorLocation.noData)
+                .build());
+
+        assertThat(errors).containsOnly("Failed to parse price floors from request, with a reason: "
+                + "Price floor rules number 2 exceeded its maximum number 1");
+    }
+
+    @Test
+    public void shouldTolerateUsingFloorsFromRequestWhenDimensionsNumberMoreThanMaxDimensionsNumber() {
+        // given
+        given(priceFloorFetcher.fetch(any())).willReturn(null);
+        final ArrayList<String> errors = new ArrayList<>();
+
+        // when
+        final BidRequest result = target.enrichWithPriceFloors(
+                givenBidRequest(identity(), givenFloors(floors -> floors.data(
+                        PriceFloorData.builder()
+                                .modelGroups(singletonList(PriceFloorModelGroup.builder()
+                                        .value("someKey", BigDecimal.ONE)
+                                        .schema(PriceFloorSchema.of("|", List.of(size, siteDomain)))
+                                        .build()))
+                                .build())
+                )),
+                givenAccount(floorConfigBuilder -> floorConfigBuilder.maxSchemaDims(1L)),
+                "bidder",
+                errors,
+                new ArrayList<>());
+
+        // then
+        assertThat(extractFloors(result)).isEqualTo(PriceFloorRules.builder()
+                .enabled(true)
+                .skipped(false)
+                .location(PriceFloorLocation.noData)
+                .build());
+
+        assertThat(errors).containsOnly("Failed to parse price floors from request, with a reason: "
+                + "Price floor schema dimensions 2 exceeded its maximum number 1");
+    }
+
     @Test
     public void shouldTolerateMissingRequestAndProviderFloors() {
         // given
@@ -554,14 +708,6 @@ public void shouldTolerateFloorResolvingError() {
         assertThat(errors).containsOnly("Cannot resolve bid floor, error: error");
     }
 
-    private static AuctionContext givenAuctionContext(Account account, BidRequest bidRequest) {
-        return AuctionContext.builder()
-                .prebidErrors(new ArrayList<>())
-                .account(account)
-                .bidRequest(bidRequest)
-                .build();
-    }
-
     private static Account givenAccount(
             UnaryOperator<AccountPriceFloorsConfig.AccountPriceFloorsConfigBuilder> floorsConfigCustomizer) {
 
@@ -594,6 +740,7 @@ private static PriceFloorRules givenFloors(
                 .data(PriceFloorData.builder()
                         .modelGroups(singletonList(PriceFloorModelGroup.builder()
                                 .value("someKey", BigDecimal.ONE)
+                                .schema(PriceFloorSchema.of("|", List.of(size)))
                                 .build()))
                         .build())
         ).build();
@@ -605,6 +752,7 @@ private static PriceFloorData givenFloorData(
         return floorDataCustomizer.apply(PriceFloorData.builder()
                 .modelGroups(singletonList(PriceFloorModelGroup.builder()
                         .value("someKey", BigDecimal.ONE)
+                        .schema(PriceFloorSchema.of("|", List.of(size)))
                         .build()))).build();
     }
 
@@ -612,7 +760,8 @@ private static PriceFloorModelGroup givenModelGroup(
             UnaryOperator<PriceFloorModelGroup.PriceFloorModelGroupBuilder> modelGroupCustomizer) {
 
         return modelGroupCustomizer.apply(PriceFloorModelGroup.builder()
-                        .value("someKey", BigDecimal.ONE))
+                        .value("someKey", BigDecimal.ONE)
+                        .schema(PriceFloorSchema.of("|", List.of(size))))
                 .build();
     }
 
diff --git a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java
index 59253e200a7..4fe2e31180b 100644
--- a/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java
+++ b/src/test/java/org/prebid/server/floors/PriceFloorFetcherTest.java
@@ -12,10 +12,9 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.VertxTest;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.floors.model.PriceFloorData;
 import org.prebid.server.floors.model.PriceFloorDebugProperties;
-import org.prebid.server.floors.model.PriceFloorField;
 import org.prebid.server.floors.model.PriceFloorModelGroup;
 import org.prebid.server.floors.model.PriceFloorRules;
 import org.prebid.server.floors.model.PriceFloorSchema;
@@ -31,6 +30,7 @@
 import org.prebid.server.vertx.httpclient.model.HttpClientResponse;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.concurrent.TimeoutException;
 import java.util.function.UnaryOperator;
 
@@ -45,6 +45,9 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.prebid.server.floors.model.PriceFloorField.domain;
+import static org.prebid.server.floors.model.PriceFloorField.mediaType;
+import static org.prebid.server.floors.model.PriceFloorField.siteDomain;
 
 @ExtendWith(MockitoExtension.class)
 public class PriceFloorFetcherTest extends VertxTest {
@@ -497,6 +500,34 @@ public void fetchShouldReturnNullAndCreatePeriodicTimerWhenResponseExceededRules
         verifyNoMoreInteractions(vertx);
     }
 
+    @Test
+    public void fetchShouldReturnNullAndCreatePeriodicTimerWhenResponseExceededDimensionsNumber() {
+        // given
+        given(httpClient.get(anyString(), anyLong(), anyLong()))
+                .willReturn(Future.succeededFuture(HttpClientResponse.of(200,
+                        MultiMap.caseInsensitiveMultiMap(),
+                        jacksonMapper.encodeToString(PriceFloorData.builder()
+                                .modelGroups(singletonList(PriceFloorModelGroup.builder()
+                                        .schema(PriceFloorSchema.of("|", List.of(siteDomain, domain)))
+                                        .build()))
+                                .build()))));
+
+        // when
+        final FetchResult firstInvocationResult =
+                priceFloorFetcher.fetch(givenAccount(account -> account.maxSchemaDims(1L)));
+
+        // then
+        verify(httpClient).get(anyString(), anyLong(), anyLong());
+        assertThat(firstInvocationResult.getRulesData()).isNull();
+        assertThat(firstInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.inprogress);
+        verify(vertx).setTimer(eq(1200000L), any());
+        verify(vertx).setTimer(eq(1500000L), any());
+        final FetchResult secondInvocationResult = priceFloorFetcher.fetch(givenAccount(identity()));
+        assertThat(secondInvocationResult.getRulesData()).isNull();
+        assertThat(secondInvocationResult.getFetchStatus()).isEqualTo(FetchStatus.error);
+        verifyNoMoreInteractions(vertx);
+    }
+
     private Account givenAccount(UnaryOperator<
             AccountPriceFloorsFetchConfig.AccountPriceFloorsFetchConfigBuilder> configCustomizer) {
 
@@ -516,6 +547,7 @@ private static AccountPriceFloorsFetchConfig givenFetchConfig(
                         .enabled(true)
                         .url("http://test.host.com")
                         .maxRules(10L)
+                        .maxSchemaDims(10L)
                         .maxFileSizeKb(10L)
                         .timeoutMs(1300L)
                         .maxAgeSec(1500L)
@@ -528,7 +560,7 @@ private PriceFloorData givenPriceFloorData() {
                 .currency("USD")
                 .modelGroups(singletonList(PriceFloorModelGroup.builder()
                         .modelVersion("model version 1.0")
-                        .schema(PriceFloorSchema.of("|", singletonList(PriceFloorField.mediaType)))
+                        .schema(PriceFloorSchema.of("|", singletonList(mediaType)))
                         .value("banner", BigDecimal.TEN)
                         .currency("EUR").build()))
                 .build();
diff --git a/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java
index 77c4f56123c..f4ec32e1165 100644
--- a/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java
+++ b/src/test/java/org/prebid/server/floors/PriceFloorRulesValidatorTest.java
@@ -4,8 +4,10 @@
 import org.prebid.server.VertxTest;
 import org.prebid.server.exception.PreBidException;
 import org.prebid.server.floors.model.PriceFloorData;
+import org.prebid.server.floors.model.PriceFloorField;
 import org.prebid.server.floors.model.PriceFloorModelGroup;
 import org.prebid.server.floors.model.PriceFloorRules;
+import org.prebid.server.floors.model.PriceFloorSchema;
 
 import java.math.BigDecimal;
 import java.util.Arrays;
@@ -15,6 +17,7 @@
 import java.util.function.UnaryOperator;
 
 import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.prebid.server.floors.model.PriceFloorField.size;
 
 public class PriceFloorRulesValidatorTest extends VertxTest {
 
@@ -24,7 +27,7 @@ public void validateShouldThrowExceptionOnInvalidRootSkipRateWhenPresent() {
         final PriceFloorRules priceFloorRules = givenPriceFloorRules(rulesBuilder -> rulesBuilder.skipRate(-1));
 
         assertThatExceptionOfType(PreBidException.class)
-                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100))
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100))
                 .withMessage("Price floor root skipRate must be in range(0-100), but was -1");
     }
 
@@ -36,7 +39,7 @@ public void validateShouldThrowExceptionWhenFloorMinPresentAndLessThanZero() {
 
         // when and then
         assertThatExceptionOfType(PreBidException.class)
-                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100))
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100))
                 .withMessage("Price floor floorMin must be positive float, but was -1");
     }
 
@@ -47,7 +50,7 @@ public void validateShouldThrowExceptionWhenDataIsAbsent() {
 
         // when and then
         assertThatExceptionOfType(PreBidException.class)
-                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100))
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100))
                 .withMessage("Price floor rules data must be present");
     }
 
@@ -58,10 +61,22 @@ public void validateShouldThrowExceptionOnInvalidDataSkipRateWhenPresent() {
 
         // when and then
         assertThatExceptionOfType(PreBidException.class)
-                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100))
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100))
                 .withMessage("Price floor data skipRate must be in range(0-100), but was -1");
     }
 
+    @Test
+    public void validateShouldThrowExceptionOnInvalidUseFetchDataRateWhenPresent() {
+        // given
+        final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithData(
+                dataBuilder -> dataBuilder.useFetchDataRate(-1));
+
+        // when and then
+        assertThatExceptionOfType(PreBidException.class)
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100))
+                .withMessage("Price floor data useFetchDataRate must be in range(0-100), but was -1");
+    }
+
     @Test
     public void validateShouldThrowExceptionOnAbsentDataModelGroups() {
         // given
@@ -70,7 +85,7 @@ public void validateShouldThrowExceptionOnAbsentDataModelGroups() {
 
         // when and then
         assertThatExceptionOfType(PreBidException.class)
-                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100))
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100))
                 .withMessage("Price floor rules should contain at least one model group");
     }
 
@@ -82,7 +97,7 @@ public void validateShouldThrowExceptionOnEmptyDataModelGroups() {
 
         // when and then
         assertThatExceptionOfType(PreBidException.class)
-                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100))
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100))
                 .withMessage("Price floor rules should contain at least one model group");
     }
 
@@ -94,7 +109,7 @@ public void validateShouldThrowExceptionOnInvalidDataModelGroupModelWeightWhenPr
 
         // when and then
         assertThatExceptionOfType(PreBidException.class)
-                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100))
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100))
                 .withMessage("Price floor modelGroup modelWeight must be in range(1-100), but was -1");
     }
 
@@ -106,7 +121,7 @@ public void validateShouldThrowExceptionOnInvalidDataModelGroupSkipRateWhenPrese
 
         // when and then
         assertThatExceptionOfType(PreBidException.class)
-                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100))
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100))
                 .withMessage("Price floor modelGroup skipRate must be in range(0-100), but was -1");
     }
 
@@ -118,7 +133,7 @@ public void validateShouldThrowExceptionOnInvalidDataModelGroupDefaultFloorWhenP
 
         // when and then
         assertThatExceptionOfType(PreBidException.class)
-                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100))
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100))
                 .withMessage("Price floor modelGroup default must be positive float, but was -1");
     }
 
@@ -130,7 +145,7 @@ public void validateShouldThrowExceptionOnEmptyModelGroupValues() {
 
         // when and then
         assertThatExceptionOfType(PreBidException.class)
-                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100))
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100))
                 .withMessage("Price floor rules values can't be null or empty, but were {}");
     }
 
@@ -148,13 +163,46 @@ public void validateShouldThrowExceptionWhenModelGroupValuesSizeGreaterThanMaxRu
 
         // when and then
         assertThatExceptionOfType(PreBidException.class)
-                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, maxRules))
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, maxRules, 100))
                 .withMessage(
                         "Price floor rules number %s exceeded its maximum number %s",
                         modelGroupValues.size(),
                         maxRules);
     }
 
+    @Test
+    public void validateShouldThrowExceptionOnEmptyModelGroupFields() {
+        // given
+        final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups(
+                modelGroupBuilder -> modelGroupBuilder.schema(PriceFloorSchema.of("|", Collections.emptyList())));
+
+        // when and then
+        assertThatExceptionOfType(PreBidException.class)
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, 100))
+                .withMessage("Price floor dimensions can't be null or empty, but were []");
+    }
+
+    @Test
+    public void validateShouldThrowExceptionWhenModelGroupSchemaDimensionsSizeGreaterThanMaxDimensions() {
+        // given
+        final List<PriceFloorField> modelGroupSchemaFields = List.of(
+                size,
+                PriceFloorField.bundle);
+
+        final PriceFloorRules priceFloorRules = givenPriceFloorRulesWithDataModelGroups(
+                modelGroupBuilder -> modelGroupBuilder.schema(PriceFloorSchema.of("|", modelGroupSchemaFields)));
+
+        final int maxDimensions = modelGroupSchemaFields.size() - 1;
+
+        // when and then
+        assertThatExceptionOfType(PreBidException.class)
+                .isThrownBy(() -> PriceFloorRulesValidator.validateRules(priceFloorRules, 100, maxDimensions))
+                .withMessage(
+                        "Price floor schema dimensions %s exceeded its maximum number %s",
+                        modelGroupSchemaFields.size(),
+                        maxDimensions);
+    }
+
     private static PriceFloorRules givenPriceFloorRulesWithDataModelGroups(
             UnaryOperator<PriceFloorModelGroup.PriceFloorModelGroupBuilder>... modelGroupBuilders) {
 
@@ -163,6 +211,7 @@ private static PriceFloorRules givenPriceFloorRulesWithDataModelGroups(
                         .modelWeight(10)
                         .skipRate(10)
                         .defaultFloor(BigDecimal.TEN)
+                        .schema(PriceFloorSchema.of("|", List.of(size)))
                         .values(Map.of("value", BigDecimal.TEN));
 
         final List<PriceFloorModelGroup> modelGroups = Arrays.stream(modelGroupBuilders)
diff --git a/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java b/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java
index 0d3864395e6..9f975335cd9 100644
--- a/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java
+++ b/src/test/java/org/prebid/server/floors/PriceFloorsConfigResolverTest.java
@@ -188,6 +188,32 @@ public void resolveShouldReturnGivenAccountIfMaxRulesMoreThanMaximumValue() {
         verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors);
     }
 
+    @Test
+    public void resolveShouldReturnGivenAccountIfMaxDimensionsLessThanMinimumValue() {
+        // given
+        final Account givenAccount = accountWithFloorsFetchConfig(config -> config.maxSchemaDims(-1L));
+
+        // when
+        final Account actualAccount = target.resolve(givenAccount, defaultPriceConfig());
+
+        // then
+        assertThat(actualAccount).isEqualTo(fallbackAccount());
+        verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors);
+    }
+
+    @Test
+    public void resolveShouldReturnGivenAccountIfMaxDimensionsMoreThanMaximumValue() {
+        // given
+        final Account givenAccount = accountWithFloorsFetchConfig(config -> config.maxSchemaDims(20L));
+
+        // when
+        final Account actualAccount = target.resolve(givenAccount, defaultPriceConfig());
+
+        // then
+        assertThat(actualAccount).isEqualTo(fallbackAccount());
+        verify(metrics).updateAlertsConfigFailed("some-id", MetricName.price_floors);
+    }
+
     @Test
     public void resolveShouldReturnGivenAccountIfMaxFileSizeLessThanMinimumValue() {
         // given
diff --git a/src/test/java/org/prebid/server/geolocation/ConfigurationGeoLocationServiceTest.java b/src/test/java/org/prebid/server/geolocation/ConfigurationGeoLocationServiceTest.java
index 945d292970f..05643ce0e57 100644
--- a/src/test/java/org/prebid/server/geolocation/ConfigurationGeoLocationServiceTest.java
+++ b/src/test/java/org/prebid/server/geolocation/ConfigurationGeoLocationServiceTest.java
@@ -6,7 +6,7 @@
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.geolocation.model.GeoInfo;
 import org.prebid.server.geolocation.model.GeoInfoConfiguration;
 
diff --git a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java
index e5c71464d05..0b6027fcc92 100644
--- a/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java
+++ b/src/test/java/org/prebid/server/handler/CookieSyncHandlerTest.java
@@ -32,7 +32,7 @@
 import org.prebid.server.cookie.model.PartitionedCookie;
 import org.prebid.server.cookie.proto.Uids;
 import org.prebid.server.exception.InvalidAccountConfigException;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.metric.Metrics;
 import org.prebid.server.privacy.ccpa.Ccpa;
 import org.prebid.server.privacy.gdpr.model.TcfContext;
diff --git a/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java b/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java
index 9f27a7dde1f..c386ca753ef 100644
--- a/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java
+++ b/src/test/java/org/prebid/server/handler/NotificationEventHandlerTest.java
@@ -19,7 +19,7 @@
 import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator;
 import org.prebid.server.auction.model.Tuple2;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.model.CaseInsensitiveMultiMap;
 import org.prebid.server.model.HttpRequestContext;
 import org.prebid.server.settings.ApplicationSettings;
diff --git a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java
index 5df909ebd78..583c06e508b 100644
--- a/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java
+++ b/src/test/java/org/prebid/server/handler/SetuidHandlerTest.java
@@ -33,7 +33,7 @@
 import org.prebid.server.cookie.proto.Uids;
 import org.prebid.server.exception.InvalidAccountConfigException;
 import org.prebid.server.exception.InvalidRequestException;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.metric.Metrics;
 import org.prebid.server.privacy.HostVendorTcfDefinerService;
 import org.prebid.server.privacy.gdpr.model.HostVendorTcfResponse;
@@ -52,11 +52,10 @@
 import java.time.Instant;
 import java.time.ZoneId;
 import java.util.Base64;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 
-import static java.util.Arrays.asList;
 import static java.util.Collections.emptyMap;
 import static java.util.Collections.singleton;
 import static java.util.Collections.singletonMap;
@@ -77,12 +76,13 @@ public class SetuidHandlerTest extends VertxTest {
     private static final String RUBICON = "rubicon";
     private static final String FACEBOOK = "audienceNetwork";
     private static final String ADNXS = "adnxs";
+    private static final String APPNEXUS = "appnexus";
 
     @Mock(strictness = LENIENT)
     private UidsCookieService uidsCookieService;
     @Mock(strictness = LENIENT)
     private ApplicationSettings applicationSettings;
-    @Mock
+    @Mock(strictness = LENIENT)
     private BidderCatalog bidderCatalog;
     @Mock(strictness = LENIENT)
     private SetuidPrivacyContextFactory setuidPrivacyContextFactory;
@@ -98,6 +98,7 @@ public class SetuidHandlerTest extends VertxTest {
     private Metrics metrics;
 
     private SetuidHandler setuidHandler;
+
     @Mock(strictness = LENIENT)
     private RoutingContext routingContext;
     @Mock(strictness = LENIENT)
@@ -111,8 +112,10 @@ public class SetuidHandlerTest extends VertxTest {
 
     @BeforeEach
     public void setUp() {
-        final Map<Integer, PrivacyEnforcementAction> vendorIdToGdpr = singletonMap(1,
-                PrivacyEnforcementAction.allowAll());
+        final Map<String, PrivacyEnforcementAction> bidderToGdpr = Map.of(
+                RUBICON, PrivacyEnforcementAction.allowAll(),
+                APPNEXUS, PrivacyEnforcementAction.allowAll(),
+                FACEBOOK, PrivacyEnforcementAction.allowAll());
 
         tcfContext = TcfContext.builder().inGdprScope(false).build();
         given(setuidPrivacyContextFactory.contextFrom(any(), any(), any()))
@@ -122,8 +125,8 @@ public void setUp() {
                 .willAnswer(invocation -> invocation.getArgument(0));
         given(activityInfrastructureCreator.create(any(), any(), any()))
                 .willReturn(activityInfrastructure);
-        given(tcfDefinerService.resultForVendorIds(anySet(), any()))
-                .willReturn(Future.succeededFuture(TcfResponse.of(true, vendorIdToGdpr, null)));
+        given(tcfDefinerService.resultForBidderNames(anySet(), any(), any()))
+                .willReturn(Future.succeededFuture(TcfResponse.of(true, bidderToGdpr, null)));
         given(tcfDefinerService.isAllowedForHostVendorId(any()))
                 .willReturn(Future.succeededFuture(HostVendorTcfResponse.allowedVendor()));
         given(tcfDefinerService.getGdprHostVendorId()).willReturn(1);
@@ -138,13 +141,20 @@ public void setUp() {
 
         given(uidsCookieService.toCookie(any())).willReturn(Cookie.cookie("test", "test"));
 
-        given(bidderCatalog.names()).willReturn(new HashSet<>(asList("rubicon", "audienceNetwork")));
-        given(bidderCatalog.isActive(any())).willReturn(true);
+        given(bidderCatalog.usersyncReadyBidders()).willReturn(Set.of(RUBICON, FACEBOOK, APPNEXUS));
+        given(bidderCatalog.isAlias(any())).willReturn(false);
 
         given(bidderCatalog.usersyncerByName(eq(RUBICON))).willReturn(
                 Optional.of(Usersyncer.of(RUBICON, null, redirectMethod())));
+        given(bidderCatalog.cookieFamilyName(eq(RUBICON))).willReturn(Optional.of(RUBICON));
+
         given(bidderCatalog.usersyncerByName(eq(FACEBOOK))).willReturn(
                 Optional.of(Usersyncer.of(FACEBOOK, null, redirectMethod())));
+        given(bidderCatalog.cookieFamilyName(eq(FACEBOOK))).willReturn(Optional.of(FACEBOOK));
+
+        given(bidderCatalog.usersyncerByName(eq(APPNEXUS))).willReturn(
+                Optional.of(Usersyncer.of(ADNXS, null, redirectMethod())));
+        given(bidderCatalog.cookieFamilyName(eq(APPNEXUS))).willReturn(Optional.of(ADNXS));
 
         given(activityInfrastructure.isAllowed(any(), any()))
                 .willReturn(true);
@@ -312,9 +322,9 @@ public void shouldPassUnsuccessfulEventToAnalyticsReporterIfUidMissingInRequest(
     public void shouldRespondWithoutCookieIfGdprProcessingPreventsCookieSetting() {
         // given
         final PrivacyEnforcementAction privacyEnforcementAction = PrivacyEnforcementAction.restrictAll();
-        given(tcfDefinerService.resultForVendorIds(anySet(), any()))
+        given(tcfDefinerService.resultForBidderNames(anySet(), any(), any()))
                 .willReturn(Future.succeededFuture(
-                        TcfResponse.of(true, singletonMap(null, privacyEnforcementAction), null)));
+                        TcfResponse.of(true, singletonMap(RUBICON, privacyEnforcementAction), null)));
 
         given(uidsCookieService.parseFromRequest(any(RoutingContext.class)))
                 .willReturn(new UidsCookie(Uids.builder().uids(emptyMap()).build(), jacksonMapper));
@@ -338,7 +348,7 @@ public void shouldRespondWithoutCookieIfGdprProcessingPreventsCookieSetting() {
     @Test
     public void shouldRespondWithBadRequestStatusIfGdprProcessingFailsWithInvalidRequestException() {
         // given
-        given(tcfDefinerService.resultForVendorIds(anySet(), any()))
+        given(tcfDefinerService.resultForBidderNames(anySet(), any(), any()))
                 .willReturn(Future.failedFuture(new InvalidRequestException("gdpr exception")));
 
         given(uidsCookieService.parseFromRequest(any(RoutingContext.class)))
@@ -361,7 +371,7 @@ public void shouldRespondWithBadRequestStatusIfGdprProcessingFailsWithInvalidReq
     @Test
     public void shouldRespondWithInternalServerErrorStatusIfGdprProcessingFailsWithUnexpectedException() {
         // given
-        given(tcfDefinerService.resultForVendorIds(anySet(), any()))
+        given(tcfDefinerService.resultForBidderNames(anySet(), any(), any()))
                 .willReturn(Future.failedFuture("unexpected error TCF"));
 
         given(uidsCookieService.parseFromRequest(any(RoutingContext.class)))
@@ -457,6 +467,33 @@ public void shouldRespondWithCookieFromRequestParam() throws IOException {
         assertThat(decodedUids.getUids().get(RUBICON).getUid()).isEqualTo("J5VLCWQP-26-CWFT");
     }
 
+    @Test
+    public void shouldRespondWithCookieFromRequestParamWhenBidderAndCookieFamilyAreDifferent() throws IOException {
+        // given
+        final UidsCookie uidsCookie = emptyUidsCookie();
+        given(uidsCookieService.parseFromRequest(any(RoutingContext.class)))
+                .willReturn(uidsCookie);
+        given(uidsCookieService.updateUidsCookie(uidsCookie, ADNXS, "J5VLCWQP-26-CWFT"))
+                .willReturn(UidsCookieUpdateResult.updated(uidsCookie));
+
+        // {"tempUIDs":{"adnxs":{"uid":"J5VLCWQP-26-CWFT"}}}
+        given(uidsCookieService.toCookie(any())).willReturn(Cookie
+                .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJhZG54cyI6eyJ1aWQiOiJKNVZMQ1dRUC0yNi1DV0ZUIn19fQ=="));
+
+        given(httpRequest.getParam("bidder")).willReturn(ADNXS);
+        given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT");
+
+        // when
+        setuidHandler.handle(routingContext);
+
+        // then
+        verify(routingContext, never()).addCookie(any(Cookie.class));
+        final String encodedUidsCookie = getUidsCookie();
+        final Uids decodedUids = decodeUids(encodedUidsCookie);
+        assertThat(decodedUids.getUids()).hasSize(1);
+        assertThat(decodedUids.getUids().get(ADNXS).getUid()).isEqualTo("J5VLCWQP-26-CWFT");
+    }
+
     @Test
     public void shouldSendPixelWhenFParamIsEqualToIWhenTypeIsIframe() {
         // given
@@ -499,7 +536,7 @@ public void shouldSendEmptyResponseWhenFParamIsEqualToBWhenTypeIsRedirect() {
         given(httpRequest.getParam("bidder")).willReturn(RUBICON);
         given(httpRequest.getParam("f")).willReturn("b");
         given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT");
-        given(bidderCatalog.names()).willReturn(singleton(RUBICON));
+        given(bidderCatalog.usersyncReadyBidders()).willReturn(singleton(RUBICON));
         given(bidderCatalog.usersyncerByName(any()))
                 .willReturn(Optional.of(Usersyncer.of(RUBICON, null, redirectMethod())));
 
@@ -585,7 +622,7 @@ public void shouldSendPixelWhenFParamNotDefinedAndTypeIsRedirect() {
         given(uidsCookieService.toCookie(any())).willReturn(Cookie
                 .cookie("uids", "eyJ0ZW1wVUlEcyI6eyJydWJpY29uIjp7InVpZCI6Iko1VkxDV1FQLTI2LUNXRlQifX19"));
         given(httpRequest.getParam("bidder")).willReturn(RUBICON);
-        given(bidderCatalog.names()).willReturn(singleton(RUBICON));
+        given(bidderCatalog.usersyncReadyBidders()).willReturn(singleton(RUBICON));
         given(bidderCatalog.usersyncerByName(any()))
                 .willReturn(Optional.of(Usersyncer.of(RUBICON, null, redirectMethod())));
         given(httpRequest.getParam("uid")).willReturn("J5VLCWQP-26-CWFT");
@@ -775,17 +812,23 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() {
     }
 
     @Test
-    public void shouldThrowExceptionInCaseOfCookieFamilyNameDuplicates() {
+    public void shouldThrowExceptionInCaseOfBaseBidderCookieFamilyNameDuplicates() {
         // given
         final Clock clock = Clock.fixed(Instant.now(), ZoneId.systemDefault());
         final String firstDuplicateName = "firstBidderWithDuplicate";
         final String secondDuplicateName = "secondBidderWithDuplicate";
-        given(bidderCatalog.names())
-                .willReturn(new HashSet<>(asList(RUBICON, FACEBOOK, firstDuplicateName, secondDuplicateName)));
+        final String thirdDuplicateName = "thirdDuplicateName";
+
+        given(bidderCatalog.usersyncReadyBidders())
+                .willReturn(Set.of(RUBICON, FACEBOOK, firstDuplicateName, secondDuplicateName, thirdDuplicateName));
+        given(bidderCatalog.isAlias(thirdDuplicateName)).willReturn(true);
         given(bidderCatalog.usersyncerByName(eq(firstDuplicateName))).willReturn(
                 Optional.of(Usersyncer.of(RUBICON, iframeMethod(), redirectMethod())));
         given(bidderCatalog.usersyncerByName(eq(secondDuplicateName))).willReturn(
                 Optional.of(Usersyncer.of(FACEBOOK, iframeMethod(), redirectMethod())));
+        given(bidderCatalog.usersyncerByName(eq(thirdDuplicateName))).willReturn(
+                Optional.of(Usersyncer.of(FACEBOOK, iframeMethod(), redirectMethod())));
+
         final Executable exceptionSource = () -> new SetuidHandler(
                 2000,
                 uidsCookieService,
diff --git a/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java b/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java
index e0f2e5a966c..0674ab1d1ce 100644
--- a/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java
+++ b/src/test/java/org/prebid/server/handler/VtrackHandlerTest.java
@@ -21,7 +21,7 @@
 import org.prebid.server.cache.proto.response.bid.BidCacheResponse;
 import org.prebid.server.cache.proto.response.bid.CacheObject;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.settings.ApplicationSettings;
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.AccountAuctionConfig;
diff --git a/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java b/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java
index d8589113f31..36bd669610f 100644
--- a/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java
+++ b/src/test/java/org/prebid/server/handler/info/BidderDetailsHandlerTest.java
@@ -199,7 +199,8 @@ private static BidderInfo givenBidderInfo(boolean enabled, String endpoint, Stri
                 true,
                 false,
                 CompressionType.NONE,
-                Ortb.of(false));
+                Ortb.of(false),
+                0L);
     }
 
     private static BidderInfo givenBidderInfo() {
diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java
index 9f40ed1bc81..aee1397e4c9 100644
--- a/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java
+++ b/src/test/java/org/prebid/server/handler/openrtb2/AmpHandlerTest.java
@@ -27,6 +27,7 @@
 import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator;
 import org.prebid.server.auction.AmpResponsePostProcessor;
 import org.prebid.server.auction.ExchangeService;
+import org.prebid.server.auction.HooksMetricsService;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.TimeoutContext;
 import org.prebid.server.auction.model.debug.DebugContext;
@@ -39,36 +40,69 @@
 import org.prebid.server.exception.InvalidAccountConfigException;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.UnauthorizedAccountException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
+import org.prebid.server.hooks.execution.HookStageExecutor;
+import org.prebid.server.hooks.execution.model.ExecutionAction;
+import org.prebid.server.hooks.execution.model.ExecutionStatus;
+import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookExecutionContext;
+import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl;
 import org.prebid.server.log.HttpInteractionLogger;
 import org.prebid.server.metric.MetricName;
 import org.prebid.server.metric.Metrics;
 import org.prebid.server.model.CaseInsensitiveMultiMap;
+import org.prebid.server.model.Endpoint;
 import org.prebid.server.model.HttpRequestContext;
 import org.prebid.server.proto.openrtb.ext.ExtPrebid;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
+import org.prebid.server.proto.openrtb.ext.request.TraceLevel;
+import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics;
+import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags;
 import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
 import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse;
 import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid;
 import org.prebid.server.proto.openrtb.ext.response.ExtModules;
 import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome;
 import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug;
+import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.AccountAnalyticsConfig;
 import org.prebid.server.util.HttpUtil;
 import org.prebid.server.version.PrebidVersionProvider;
 
 import java.time.Clock;
 import java.time.Instant;
+import java.util.EnumMap;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Function;
+import java.util.function.UnaryOperator;
 
+import static java.util.Arrays.asList;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.emptyMap;
 import static java.util.Collections.singleton;
 import static java.util.Collections.singletonList;
 import static java.util.Collections.singletonMap;
-import static java.util.function.Function.identity;
+import static java.util.function.UnaryOperator.identity;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.tuple;
 import static org.mockito.ArgumentMatchers.any;
@@ -104,8 +138,15 @@ public class AmpHandlerTest extends VertxTest {
     private Clock clock;
     @Mock
     private HttpInteractionLogger httpInteractionLogger;
+    @Mock
+    private PrebidVersionProvider prebidVersionProvider;
+    @Mock(strictness = LENIENT)
+    private HooksMetricsService hooksMetricsService;
+    @Mock(strictness = LENIENT)
+    private HookStageExecutor hookStageExecutor;
+
+    private AmpHandler target;
 
-    private AmpHandler ampHandler;
     @Mock
     private RoutingContext routingContext;
     @Mock(strictness = LENIENT)
@@ -114,8 +155,6 @@ public class AmpHandlerTest extends VertxTest {
     private HttpServerResponse httpResponse;
     @Mock(strictness = LENIENT)
     private UidsCookie uidsCookie;
-    @Mock
-    private PrebidVersionProvider prebidVersionProvider;
 
     private Timeout timeout;
 
@@ -139,19 +178,28 @@ public void setUp() {
 
         given(prebidVersionProvider.getNameVersionRecord()).willReturn("pbs-java/1.00");
 
+        given(hookStageExecutor.executeExitpointStage(any(), any(), any()))
+                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
+                        false,
+                        ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))));
+
+        given(hooksMetricsService.updateHooksMetrics(any())).willAnswer(invocation -> invocation.getArgument(0));
+
         timeout = new TimeoutFactory(clock).create(2000L);
 
-        ampHandler = new AmpHandler(
+        target = new AmpHandler(
                 ampRequestFactory,
                 exchangeService,
                 analyticsReporterDelegator,
                 metrics,
+                hooksMetricsService,
                 clock,
                 bidderCatalog,
                 singleton("bidder1"),
                 new AmpResponsePostProcessor.NoOpAmpResponsePostProcessor(),
                 httpInteractionLogger,
                 prebidVersionProvider,
+                hookStageExecutor,
                 jacksonMapper,
                 0);
     }
@@ -165,7 +213,7 @@ public void shouldSetRequestTypeMetricToAuctionContext() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         final AuctionContext auctionContext = captureAuctionContext();
@@ -181,7 +229,7 @@ public void shouldUseTimeoutFromAuctionContext() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(captureAuctionContext())
@@ -203,7 +251,7 @@ public void shouldAddPrebidVersionResponseHeader() {
                         .build()));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(httpResponse.headers())
@@ -225,7 +273,7 @@ public void shouldAddObserveBrowsingTopicsResponseHeader() {
                         .build()));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(httpResponse.headers())
@@ -245,7 +293,7 @@ public void shouldComputeTimeoutBasedOnRequestProcessingStartTime() {
         given(clock.millis()).willReturn(now.toEpochMilli()).willReturn(now.plusMillis(50L).toEpochMilli());
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(captureAuctionContext())
@@ -263,7 +311,7 @@ public void shouldRespondWithBadRequestIfRequestIsInvalid() {
                 .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid")));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verifyNoInteractions(exchangeService);
@@ -276,6 +324,7 @@ public void shouldRespondWithBadRequestIfRequestIsInvalid() {
                         tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"),
                         tuple("x-prebid", "pbs-java/1.00"));
         verify(httpResponse).end(eq("Invalid request format: Request is invalid"));
+        verifyNoInteractions(hookStageExecutor, hooksMetricsService);
     }
 
     @Test
@@ -285,7 +334,7 @@ public void shouldRespondWithBadRequestIfRequestHasBlocklistedAccount() {
                 .willReturn(Future.failedFuture(new BlocklistedAccountException("Blocklisted account")));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verifyNoInteractions(exchangeService);
@@ -297,6 +346,7 @@ public void shouldRespondWithBadRequestIfRequestHasBlocklistedAccount() {
                         tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"),
                         tuple("x-prebid", "pbs-java/1.00"));
         verify(httpResponse).end(eq("Blocklisted: Blocklisted account"));
+        verifyNoInteractions(hookStageExecutor, hooksMetricsService);
     }
 
     @Test
@@ -306,7 +356,7 @@ public void shouldRespondWithBadRequestIfRequestHasBlocklistedApp() {
                 .willReturn(Future.failedFuture(new BlocklistedAppException("Blocklisted app")));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verifyNoInteractions(exchangeService);
@@ -318,6 +368,7 @@ public void shouldRespondWithBadRequestIfRequestHasBlocklistedApp() {
                         tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"),
                         tuple("x-prebid", "pbs-java/1.00"));
         verify(httpResponse).end(eq("Blocklisted: Blocklisted app"));
+        verifyNoInteractions(hookStageExecutor, hooksMetricsService);
     }
 
     @Test
@@ -327,7 +378,7 @@ public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() {
                 .willReturn(Future.failedFuture(new UnauthorizedAccountException("Account id is not provided", null)));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verifyNoInteractions(exchangeService);
@@ -339,6 +390,7 @@ public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() {
                         tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"),
                         tuple("x-prebid", "pbs-java/1.00"));
         verify(httpResponse).end(eq("Account id is not provided"));
+        verifyNoInteractions(hookStageExecutor, hooksMetricsService);
     }
 
     @Test
@@ -348,7 +400,7 @@ public void shouldRespondWithBadRequestOnInvalidAccountConfigException() {
                 .willReturn(Future.failedFuture(new InvalidAccountConfigException("Account is invalid")));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verifyNoInteractions(exchangeService);
@@ -361,6 +413,7 @@ public void shouldRespondWithBadRequestOnInvalidAccountConfigException() {
                         tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"),
                         tuple("x-prebid", "pbs-java/1.00"));
         verify(httpResponse).end(eq("Invalid account configuration: Account is invalid"));
+        verifyNoInteractions(hookStageExecutor, hooksMetricsService);
     }
 
     @Test
@@ -373,7 +426,7 @@ public void shouldRespondWithInternalServerErrorIfAuctionFails() {
                 .willThrow(new RuntimeException("Unexpected exception"));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).setStatusCode(eq(500));
@@ -384,6 +437,7 @@ public void shouldRespondWithInternalServerErrorIfAuctionFails() {
                         tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"),
                         tuple("x-prebid", "pbs-java/1.00"));
         verify(httpResponse).end(eq("Critical error while running the auction: Unexpected exception"));
+        verifyNoInteractions(hookStageExecutor, hooksMetricsService);
     }
 
     @Test
@@ -398,7 +452,7 @@ public void shouldRespondWithInternalServerErrorIfCannotExtractBidTargeting() {
         givenHoldAuction(givenBidResponse(ext));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).setStatusCode(eq(500));
@@ -410,6 +464,7 @@ public void shouldRespondWithInternalServerErrorIfCannotExtractBidTargeting() {
                         tuple("x-prebid", "pbs-java/1.00"));
         verify(httpResponse).end(
                 startsWith("Critical error while running the auction: Critical error while unpacking AMP targets:"));
+        verifyNoInteractions(hookStageExecutor, hooksMetricsService);
     }
 
     @Test
@@ -421,10 +476,11 @@ public void shouldNotSendResponseIfClientClosedConnection() {
         given(routingContext.response().closed()).willReturn(true);
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse, never()).end(anyString());
+        verifyNoInteractions(hookStageExecutor, hooksMetricsService);
     }
 
     @Test
@@ -442,7 +498,7 @@ public void shouldRespondWithExpectedResponse() {
         givenHoldAuction(givenBidResponse(mapper.valueToTree(extPrebid)));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(httpResponse.headers()).hasSize(4)
@@ -453,6 +509,68 @@ public void shouldRespondWithExpectedResponse() {
                         tuple("Content-Type", "application/json"),
                         tuple("x-prebid", "pbs-java/1.00"));
         verify(httpResponse).end(eq("{\"targeting\":{\"key1\":\"value1\",\"hb_cache_id_bidder1\":\"value2\"}}"));
+
+        final ArgumentCaptor<MultiMap> responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class);
+        verify(hookStageExecutor).executeExitpointStage(
+                responseHeadersCaptor.capture(),
+                eq("{\"targeting\":{\"key1\":\"value1\",\"hb_cache_id_bidder1\":\"value2\"}}"),
+                any());
+
+        assertThat(responseHeadersCaptor.getValue()).hasSize(4)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(
+                        tuple("AMP-Access-Control-Allow-Source-Origin", "http://example.com"),
+                        tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"),
+                        tuple("Content-Type", "application/json"),
+                        tuple("x-prebid", "pbs-java/1.00"));
+
+        verify(hooksMetricsService).updateHooksMetrics(any());
+    }
+
+    @Test
+    public void shouldRespondWithExpectedResponseWhenExitpointHookChangesResponseAndHeaders() {
+        // given
+        given(ampRequestFactory.fromRequest(any(), anyLong()))
+                .willReturn(Future.succeededFuture(givenAuctionContext(identity())));
+
+        final Map<String, String> targeting = new HashMap<>();
+        targeting.put("key1", "value1");
+        targeting.put("hb_cache_id_bidder1", "value2");
+        final ExtPrebid<ExtBidPrebid, Object> extPrebid = ExtPrebid.of(
+                ExtBidPrebid.builder().targeting(targeting).build(),
+                null);
+        givenHoldAuction(givenBidResponse(mapper.valueToTree(extPrebid)));
+
+        given(hookStageExecutor.executeExitpointStage(any(), any(), any()))
+                .willReturn(Future.succeededFuture(HookStageExecutionResult.success(
+                        ExitpointPayloadImpl.of(
+                                MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"),
+                                "{\"targeting\":{\"new-key\":\"new-value\"}}"))));
+
+        // when
+        target.handle(routingContext);
+
+        // then
+        assertThat(httpResponse.headers()).hasSize(1)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(tuple("New-Header", "New-Header-Value"));
+        verify(httpResponse).end(eq("{\"targeting\":{\"new-key\":\"new-value\"}}"));
+
+        final ArgumentCaptor<MultiMap> responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class);
+        verify(hookStageExecutor).executeExitpointStage(
+                responseHeadersCaptor.capture(),
+                eq("{\"targeting\":{\"key1\":\"value1\",\"hb_cache_id_bidder1\":\"value2\"}}"),
+                any());
+
+        assertThat(responseHeadersCaptor.getValue()).hasSize(4)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(
+                        tuple("AMP-Access-Control-Allow-Source-Origin", "http://example.com"),
+                        tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"),
+                        tuple("Content-Type", "application/json"),
+                        tuple("x-prebid", "pbs-java/1.00"));
+
+        verify(hooksMetricsService).updateHooksMetrics(any());
     }
 
     @Test
@@ -485,11 +603,18 @@ public void shouldRespondWithCustomTargetingIncluded() {
         willReturn(bidder).given(bidderCatalog).bidderByName(anyString());
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).end(eq("{\"targeting\":{\"key1\":\"value1\",\"rpfl_11078\":\"15_tier0030\","
                 + "\"hb_cache_id_bidder1\":\"value2\"}}"));
+        verify(hookStageExecutor).executeExitpointStage(
+                any(),
+                eq("{\"targeting\":{\"key1\":\"value1\",\"rpfl_11078\":\"15_tier0030\","
+                        + "\"hb_cache_id_bidder1\":\"value2\"}}"),
+                any());
+
+        verify(hooksMetricsService).updateHooksMetrics(any());
     }
 
     @Test
@@ -524,10 +649,15 @@ public void shouldRespondWithAdditionalTargetingIncludedWhenSeatBidExists() {
                 .build());
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).end(eq("{\"targeting\":{\"key\":\"value\",\"test-key\":\"test-value\"}}"));
+        verify(hookStageExecutor).executeExitpointStage(
+                any(),
+                eq("{\"targeting\":{\"key\":\"value\",\"test-key\":\"test-value\"}}"),
+                any());
+        verify(hooksMetricsService).updateHooksMetrics(any());
     }
 
     @Test
@@ -547,10 +677,15 @@ public void shouldRespondWithAdditionalTargetingIncludedWhenNoSeatBidExists() {
         givenHoldAuction(givenBidResponseWithExt(ExtBidResponse.builder().prebid(extBidResponsePrebid).build()));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).end(eq("{\"targeting\":{\"key\":\"value\",\"test-key\":\"test-value\"}}"));
+        verify(hookStageExecutor).executeExitpointStage(
+                any(),
+                eq("{\"targeting\":{\"key\":\"value\",\"test-key\":\"test-value\"}}"),
+                any());
+        verify(hooksMetricsService).updateHooksMetrics(any());
     }
 
     @Test
@@ -569,12 +704,19 @@ public void shouldRespondWithDebugInfoIncludedIfTestFlagIsTrue() {
                         .build()));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).end(eq(
                 "{\"targeting\":{},"
                         + "\"ext\":{\"debug\":{\"resolvedrequest\":{\"id\":\"reqId1\",\"imp\":[],\"tmax\":5000}}}}"));
+        verify(hookStageExecutor).executeExitpointStage(
+                any(),
+                eq("{\"targeting\":{},"
+                        + "\"ext\":{\"debug\":{\"resolvedrequest\":{\"id\":\"reqId1\",\"imp\":[],\"tmax\":5000}}}}"),
+                any());
+        verify(hooksMetricsService).updateHooksMetrics(any());
+
     }
 
     @Test
@@ -597,7 +739,7 @@ public void shouldRespondWithHooksDebugAndTraceOutput() {
                         .build()));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).end(eq(
@@ -606,6 +748,15 @@ public void shouldRespondWithHooksDebugAndTraceOutput() {
                         + "\"errors\":{\"module1\":{\"hook1\":[\"error1\"]}},"
                         + "\"warnings\":{\"module1\":{\"hook1\":[\"warning1\"]}},"
                         + "\"trace\":{\"executiontimemillis\":2,\"stages\":[]}}}}}"));
+        verify(hookStageExecutor).executeExitpointStage(
+                any(),
+                eq("{\"targeting\":{},"
+                        + "\"ext\":{\"prebid\":{\"modules\":{"
+                        + "\"errors\":{\"module1\":{\"hook1\":[\"error1\"]}},"
+                        + "\"warnings\":{\"module1\":{\"hook1\":[\"warning1\"]}},"
+                        + "\"trace\":{\"executiontimemillis\":2,\"stages\":[]}}}}}"),
+                any());
+        verify(hooksMetricsService).updateHooksMetrics(any());
     }
 
     @Test
@@ -618,7 +769,7 @@ public void shouldIncrementOkAmpRequestMetrics() {
                 ExtPrebid.of(ExtBidPrebid.builder().build(), null))));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.ok));
@@ -634,7 +785,7 @@ public void shouldIncrementAppRequestMetrics() {
                 ExtPrebid.of(ExtBidPrebid.builder().build(), null))));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyInt());
@@ -655,7 +806,7 @@ public void shouldIncrementNoCookieMetrics() {
                 + "AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7");
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), anyInt());
@@ -672,7 +823,7 @@ public void shouldIncrementImpsRequestedMetrics() {
                 ExtPrebid.of(ExtBidPrebid.builder().build(), null))));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), eq(1));
@@ -690,7 +841,7 @@ public void shouldIncrementImpsTypesMetrics() {
                 ExtPrebid.of(ExtBidPrebid.builder().build(), null))));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateImpTypesMetrics(same(imps));
@@ -703,7 +854,7 @@ public void shouldIncrementBadinputAmpRequestMetrics() {
                 .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid")));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.badinput));
@@ -716,7 +867,7 @@ public void shouldIncrementErrAmpRequestMetrics() {
                 .willReturn(Future.failedFuture(new RuntimeException()));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.err));
@@ -741,7 +892,7 @@ public void shouldUpdateRequestTimeMetric() {
         });
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTimeMetric(eq(MetricName.request_time), eq(500L));
@@ -754,7 +905,7 @@ public void shouldNotUpdateRequestTimeMetricIfRequestFails() {
                 .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid")));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse, never()).endHandler(any());
@@ -777,7 +928,7 @@ public void shouldUpdateNetworkErrorMetric() {
         });
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.networkerr));
@@ -793,7 +944,7 @@ public void shouldNotUpdateNetworkErrorMetricIfResponseSucceeded() {
                 ExtPrebid.of(ExtBidPrebid.builder().build(), null))));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics, never()).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.networkerr));
@@ -811,7 +962,7 @@ public void shouldUpdateNetworkErrorMetricIfClientClosedConnection() {
         given(routingContext.response().closed()).willReturn(true);
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTypeMetric(eq(MetricName.amp), eq(MetricName.networkerr));
@@ -824,7 +975,7 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid()
                 .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid")));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         final AmpEvent ampEvent = captureAmpEvent();
@@ -834,6 +985,8 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid()
                 .status(400)
                 .errors(singletonList("Invalid request format: Request is invalid"))
                 .build());
+
+        verifyNoInteractions(hookStageExecutor, hooksMetricsService);
     }
 
     @Test
@@ -847,7 +1000,7 @@ public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails(
                 .willThrow(new RuntimeException("Unexpected exception"));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         final AmpEvent ampEvent = captureAmpEvent();
@@ -862,6 +1015,8 @@ public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails(
                 .status(500)
                 .errors(singletonList("Unexpected exception"))
                 .build());
+
+        verifyNoInteractions(hookStageExecutor, hooksMetricsService);
     }
 
     @Test
@@ -876,7 +1031,7 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() {
                         null))));
 
         // when
-        ampHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         final AmpEvent ampEvent = captureAmpEvent();
@@ -889,33 +1044,317 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() {
                                 .build()))
                         .build()))
                 .build();
-        final AuctionContext expectedAuctionContext = auctionContext.toBuilder()
-                .requestTypeMetric(MetricName.amp)
-                .bidResponse(expectedBidResponse)
+
+        assertThat(ampEvent.getHttpContext()).isEqualTo(givenHttpContext(singletonMap("Origin", "http://example.com")));
+        assertThat(ampEvent.getBidResponse()).isEqualTo(expectedBidResponse);
+        assertThat(ampEvent.getTargeting())
+                .isEqualTo(singletonMap("hb_cache_id_bidder1", TextNode.valueOf("value1")));
+        assertThat(ampEvent.getOrigin()).isEqualTo("http://example.com");
+        assertThat(ampEvent.getStatus()).isEqualTo(200);
+        assertThat(ampEvent.getAuctionContext().getRequestTypeMetric()).isEqualTo(MetricName.amp);
+        assertThat(ampEvent.getAuctionContext().getBidResponse()).isEqualTo(expectedBidResponse);
+
+        final ArgumentCaptor<MultiMap> responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class);
+        verify(hookStageExecutor).executeExitpointStage(
+                responseHeadersCaptor.capture(),
+                eq("{\"targeting\":{\"hb_cache_id_bidder1\":\"value1\"}}"),
+                any());
+
+        assertThat(responseHeadersCaptor.getValue()).hasSize(4)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(
+                        tuple("AMP-Access-Control-Allow-Source-Origin", "http://example.com"),
+                        tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"),
+                        tuple("Content-Type", "application/json"),
+                        tuple("x-prebid", "pbs-java/1.00"));
+
+        verify(hooksMetricsService).updateHooksMetrics(any());
+    }
+
+    @Test
+    public void shouldPassSuccessfulEventToAnalyticsReporterWhenExitpointHookChangesResponseAndHeaders() {
+        // given
+        final AuctionContext auctionContext = givenAuctionContext(identity());
+        given(ampRequestFactory.fromRequest(any(), anyLong()))
+                .willReturn(Future.succeededFuture(auctionContext));
+
+        given(hookStageExecutor.executeExitpointStage(any(), any(), any()))
+                .willReturn(Future.succeededFuture(HookStageExecutionResult.success(
+                        ExitpointPayloadImpl.of(
+                                MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"),
+                                "{\"targeting\":{\"new-key\":\"new-value\"}}"))));
+
+        givenHoldAuction(givenBidResponse(mapper.valueToTree(
+                ExtPrebid.of(ExtBidPrebid.builder().targeting(singletonMap("hb_cache_id_bidder1", "value1")).build(),
+                        null))));
+
+        // when
+        target.handle(routingContext);
+
+        // then
+        final AmpEvent ampEvent = captureAmpEvent();
+        final BidResponse expectedBidResponse = BidResponse.builder().seatbid(singletonList(SeatBid.builder()
+                        .bid(singletonList(Bid.builder()
+                                .ext(mapper.valueToTree(ExtPrebid.of(
+                                        ExtBidPrebid.builder().targeting(singletonMap("hb_cache_id_bidder1", "value1"))
+                                                .build(),
+                                        null)))
+                                .build()))
+                        .build()))
                 .build();
 
-        assertThat(ampEvent).isEqualTo(AmpEvent.builder()
-                .httpContext(givenHttpContext(singletonMap("Origin", "http://example.com")))
-                .auctionContext(expectedAuctionContext)
-                .bidResponse(expectedBidResponse)
-                .targeting(singletonMap("hb_cache_id_bidder1", TextNode.valueOf("value1")))
-                .origin("http://example.com")
-                .status(200)
-                .errors(emptyList())
-                .build());
+        assertThat(ampEvent.getAuctionContext().getBidResponse()).isEqualTo(expectedBidResponse);
+
+        final ArgumentCaptor<MultiMap> responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class);
+        verify(hookStageExecutor).executeExitpointStage(
+                responseHeadersCaptor.capture(),
+                eq("{\"targeting\":{\"hb_cache_id_bidder1\":\"value1\"}}"),
+                any());
+
+        assertThat(responseHeadersCaptor.getValue()).hasSize(4)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(
+                        tuple("AMP-Access-Control-Allow-Source-Origin", "http://example.com"),
+                        tuple("Access-Control-Expose-Headers", "AMP-Access-Control-Allow-Source-Origin"),
+                        tuple("Content-Type", "application/json"),
+                        tuple("x-prebid", "pbs-java/1.00"));
+
+        verify(hooksMetricsService).updateHooksMetrics(any());
+    }
+
+    @Test
+    public void shouldReturnSendAmpEventWithAuctionContextBidResponseDebugInfoHoldingExitpointHookOutcome() {
+        // given
+        final AuctionContext auctionContext = givenAuctionContext(identity()).toBuilder()
+                .hookExecutionContext(HookExecutionContext.of(
+                        Endpoint.openrtb2_amp,
+                        stageOutcomes()))
+                .build();
+
+        given(ampRequestFactory.fromRequest(any(), anyLong()))
+                .willReturn(Future.succeededFuture(auctionContext));
+
+        given(hookStageExecutor.executeExitpointStage(any(), any(), any()))
+                .willAnswer(invocation -> {
+                    final AuctionContext context = invocation.getArgument(2, AuctionContext.class);
+                    final HookExecutionContext hookExecutionContext = context.getHookExecutionContext();
+                    hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of(
+                            "http-response",
+                            singletonList(
+                                    GroupExecutionOutcome.of(singletonList(
+                                            HookExecutionOutcome.builder()
+                                                    .hookId(HookId.of("exitpoint-module", "exitpoint-hook"))
+                                                    .executionTime(4L)
+                                                    .status(ExecutionStatus.success)
+                                                    .message("exitpoint hook has been executed")
+                                                    .action(ExecutionAction.update)
+                                                    .analyticsTags(TagsImpl.of(singletonList(
+                                                            ActivityImpl.of(
+                                                                    "some-activity",
+                                                                    "success",
+                                                                    singletonList(ResultImpl.of(
+                                                                            "success",
+                                                                            mapper.createObjectNode(),
+                                                                            givenAppliedToImpl()))))))
+                                                    .build()))))));
+                    return Future.succeededFuture(HookStageExecutionResult.success(
+                            ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1))));
+                });
+
+        givenHoldAuction(givenBidResponse(mapper.valueToTree(
+                ExtPrebid.of(ExtBidPrebid.builder().targeting(singletonMap("hb_cache_id_bidder1", "value1")).build(),
+                        null))));
+
+        // when
+        target.handle(routingContext);
+
+        // then
+        final AmpEvent ampEvent = captureAmpEvent();
+        final BidResponse bidResponse = ampEvent.getBidResponse();
+        final ExtModulesTraceAnalyticsTags expectedAnalyticsTags = ExtModulesTraceAnalyticsTags.of(singletonList(
+                ExtModulesTraceAnalyticsActivity.of(
+                        "some-activity",
+                        "success",
+                        singletonList(ExtModulesTraceAnalyticsResult.of(
+                                "success",
+                                mapper.createObjectNode(),
+                                givenExtModulesTraceAnalyticsAppliedTo())))));
+        assertThat(bidResponse.getExt().getPrebid().getModules().getTrace()).isEqualTo(ExtModulesTrace.of(
+                8L,
+                List.of(
+                        ExtModulesTraceStage.of(
+                                Stage.auction_response,
+                                4L,
+                                singletonList(ExtModulesTraceStageOutcome.of(
+                                        "auction-response",
+                                        4L,
+                                        singletonList(
+                                                ExtModulesTraceGroup.of(
+                                                        4L,
+                                                        asList(
+                                                                ExtModulesTraceInvocationResult.builder()
+                                                                        .hookId(HookId.of("module1", "hook1"))
+                                                                        .executionTime(4L)
+                                                                        .status(ExecutionStatus.success)
+                                                                        .message("module1 hook1")
+                                                                        .action(ExecutionAction.update)
+                                                                        .build(),
+                                                                ExtModulesTraceInvocationResult.builder()
+                                                                        .hookId(HookId.of("module1", "hook2"))
+                                                                        .executionTime(4L)
+                                                                        .status(ExecutionStatus.success)
+                                                                        .message("module1 hook2")
+                                                                        .action(ExecutionAction.no_action)
+                                                                        .build())))))),
+
+                        ExtModulesTraceStage.of(
+                                Stage.exitpoint,
+                                4L,
+                                singletonList(ExtModulesTraceStageOutcome.of(
+                                        "http-response",
+                                        4L,
+                                        singletonList(
+                                                ExtModulesTraceGroup.of(
+                                                        4L,
+                                                        singletonList(
+                                                                ExtModulesTraceInvocationResult.builder()
+                                                                        .hookId(HookId.of(
+                                                                                "exitpoint-module",
+                                                                                "exitpoint-hook"))
+                                                                        .executionTime(4L)
+                                                                        .status(ExecutionStatus.success)
+                                                                        .message("exitpoint hook has been executed")
+                                                                        .action(ExecutionAction.update)
+                                                                        .analyticsTags(expectedAnalyticsTags)
+                                                                        .build())))))))));
+    }
+
+    @Test
+    public void shouldReturnSendAmpEventWithAuctionContextBidResponseAnalyticsTagsHoldingExitpointHookOutcome() {
+        // given
+        final ObjectNode analyticsNode = mapper.createObjectNode();
+        final ObjectNode optionsNode = analyticsNode.putObject("options");
+        optionsNode.put("enableclientdetails", true);
+
+        final AuctionContext auctionContext = givenAuctionContext(
+                request -> request.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .analytics(analyticsNode)
+                        .build()))).toBuilder()
+                .hookExecutionContext(HookExecutionContext.of(
+                        Endpoint.openrtb2_amp,
+                        stageOutcomes()))
+                .build();
+
+        given(ampRequestFactory.fromRequest(any(), anyLong()))
+                .willReturn(Future.succeededFuture(auctionContext));
+
+        given(hookStageExecutor.executeExitpointStage(any(), any(), any()))
+                .willAnswer(invocation -> {
+                    final AuctionContext context = invocation.getArgument(2, AuctionContext.class);
+                    final HookExecutionContext hookExecutionContext = context.getHookExecutionContext();
+                    hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of(
+                            "http-response",
+                            singletonList(
+                                    GroupExecutionOutcome.of(singletonList(
+                                            HookExecutionOutcome.builder()
+                                                    .hookId(HookId.of(
+                                                            "exitpoint-module",
+                                                            "exitpoint-hook"))
+                                                    .executionTime(4L)
+                                                    .status(ExecutionStatus.success)
+                                                    .message("exitpoint hook has been executed")
+                                                    .action(ExecutionAction.update)
+                                                    .analyticsTags(TagsImpl.of(singletonList(
+                                                            ActivityImpl.of(
+                                                                    "some-activity",
+                                                                    "success",
+                                                                    singletonList(ResultImpl.of(
+                                                                            "success",
+                                                                            mapper.createObjectNode(),
+                                                                            givenAppliedToImpl()))))))
+                                                    .build()))))));
+                    return Future.succeededFuture(HookStageExecutionResult.success(
+                            ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1))));
+                });
+
+        givenHoldAuction(givenBidResponse(mapper.valueToTree(
+                ExtPrebid.of(ExtBidPrebid.builder().targeting(singletonMap("hb_cache_id_bidder1", "value1")).build(),
+                        null))));
+
+        // when
+        target.handle(routingContext);
+
+        // then
+        final AmpEvent ampEvent = captureAmpEvent();
+        final BidResponse bidResponse = ampEvent.getBidResponse();
+        assertThat(bidResponse.getExt())
+                .extracting(ExtBidResponse::getPrebid)
+                .extracting(ExtBidResponsePrebid::getAnalytics)
+                .extracting(ExtAnalytics::getTags)
+                .asInstanceOf(InstanceOfAssertFactories.list(ExtAnalyticsTags.class))
+                .hasSize(1)
+                .allSatisfy(extAnalyticsTags -> {
+                    assertThat(extAnalyticsTags.getStage()).isEqualTo(Stage.exitpoint);
+                    assertThat(extAnalyticsTags.getModule()).isEqualTo("exitpoint-module");
+                    assertThat(extAnalyticsTags.getAnalyticsTags()).isNotNull();
+                });
+    }
+
+    private static AppliedToImpl givenAppliedToImpl() {
+        return AppliedToImpl.builder()
+                .impIds(asList("impId1", "impId2"))
+                .request(true)
+                .build();
+    }
+
+    private static ExtModulesTraceAnalyticsAppliedTo givenExtModulesTraceAnalyticsAppliedTo() {
+        return ExtModulesTraceAnalyticsAppliedTo.builder()
+                .impIds(asList("impId1", "impId2"))
+                .request(true)
+                .build();
+    }
+
+    private static EnumMap<Stage, List<StageExecutionOutcome>> stageOutcomes() {
+        final Map<Stage, List<StageExecutionOutcome>> stageOutcomes = new HashMap<>();
+
+        stageOutcomes.put(Stage.auction_response, singletonList(StageExecutionOutcome.of(
+                "auction-response",
+                singletonList(
+                        GroupExecutionOutcome.of(asList(
+                                HookExecutionOutcome.builder()
+                                        .hookId(HookId.of("module1", "hook1"))
+                                        .executionTime(4L)
+                                        .status(ExecutionStatus.success)
+                                        .message("module1 hook1")
+                                        .action(ExecutionAction.update)
+                                        .build(),
+                                HookExecutionOutcome.builder()
+                                        .hookId(HookId.of("module1", "hook2"))
+                                        .executionTime(4L)
+                                        .message("module1 hook2")
+                                        .status(ExecutionStatus.success)
+                                        .action(ExecutionAction.no_action)
+                                        .build()))))));
+
+        return new EnumMap<>(stageOutcomes);
     }
 
     private AuctionContext givenAuctionContext(
-            Function<BidRequest.BidRequestBuilder, BidRequest.BidRequestBuilder> bidRequestBuilderCustomizer) {
+            UnaryOperator<BidRequest.BidRequestBuilder> bidRequestBuilderCustomizer) {
+
         final BidRequest bidRequest = bidRequestBuilderCustomizer.apply(BidRequest.builder()
                 .imp(emptyList()).tmax(5000L)).build();
 
         return AuctionContext.builder()
+                .account(Account.builder()
+                        .analytics(AccountAnalyticsConfig.of(true, null, null))
+                        .build())
                 .uidsCookie(uidsCookie)
                 .bidRequest(bidRequest)
                 .requestTypeMetric(MetricName.amp)
                 .timeoutContext(TimeoutContext.of(0, timeout, 0))
-                .debugContext(DebugContext.empty())
+                .debugContext(DebugContext.of(true, false, TraceLevel.verbose))
+                .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_amp))
                 .build();
     }
 
@@ -924,7 +1363,6 @@ private void givenHoldAuction(BidResponse bidResponse) {
                 .willAnswer(inv -> Future.succeededFuture(((AuctionContext) inv.getArgument(0)).toBuilder()
                         .bidResponse(bidResponse)
                         .build()));
-
     }
 
     private static BidResponse givenBidResponse(ObjectNode extBid) {
diff --git a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java
index c2a84bcc922..1618caea8d2 100644
--- a/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java
+++ b/src/test/java/org/prebid/server/handler/openrtb2/AuctionHandlerTest.java
@@ -1,5 +1,6 @@
 package org.prebid.server.handler.openrtb2;
 
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.iab.openrtb.request.App;
 import com.iab.openrtb.request.BidRequest;
 import com.iab.openrtb.request.Imp;
@@ -21,9 +22,11 @@
 import org.prebid.server.analytics.model.AuctionEvent;
 import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator;
 import org.prebid.server.auction.ExchangeService;
+import org.prebid.server.auction.HooksMetricsService;
 import org.prebid.server.auction.SkippedAuctionService;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.TimeoutContext;
+import org.prebid.server.auction.model.debug.DebugContext;
 import org.prebid.server.auction.requestfactory.AuctionRequestFactory;
 import org.prebid.server.cookie.UidsCookie;
 import org.prebid.server.exception.BlocklistedAccountException;
@@ -31,12 +34,28 @@
 import org.prebid.server.exception.InvalidAccountConfigException;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.UnauthorizedAccountException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
+import org.prebid.server.hooks.execution.HookStageExecutor;
+import org.prebid.server.hooks.execution.model.ExecutionAction;
+import org.prebid.server.hooks.execution.model.ExecutionStatus;
+import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookExecutionContext;
+import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl;
 import org.prebid.server.log.HttpInteractionLogger;
 import org.prebid.server.metric.MetricName;
 import org.prebid.server.metric.Metrics;
 import org.prebid.server.model.CaseInsensitiveMultiMap;
+import org.prebid.server.model.Endpoint;
 import org.prebid.server.model.HttpRequestContext;
 import org.prebid.server.proto.openrtb.ext.request.ExtGranularityRange;
 import org.prebid.server.proto.openrtb.ext.request.ExtMediaTypePriceGranularity;
@@ -44,18 +63,36 @@
 import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
 import org.prebid.server.proto.openrtb.ext.request.ExtRequestTargeting;
+import org.prebid.server.proto.openrtb.ext.request.TraceLevel;
+import org.prebid.server.proto.openrtb.ext.response.ExtAnalytics;
+import org.prebid.server.proto.openrtb.ext.response.ExtAnalyticsTags;
 import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse;
+import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTrace;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsActivity;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsAppliedTo;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsResult;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceAnalyticsTags;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceGroup;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceInvocationResult;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStage;
+import org.prebid.server.proto.openrtb.ext.response.ExtModulesTraceStageOutcome;
 import org.prebid.server.proto.openrtb.ext.response.ExtResponseDebug;
+import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.AccountAnalyticsConfig;
 import org.prebid.server.util.HttpUtil;
 import org.prebid.server.version.PrebidVersionProvider;
 
 import java.math.BigDecimal;
 import java.time.Clock;
 import java.time.Instant;
+import java.util.EnumMap;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.function.UnaryOperator;
 
+import static java.util.Arrays.asList;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singletonList;
 import static java.util.function.UnaryOperator.identity;
@@ -93,8 +130,12 @@ public class AuctionHandlerTest extends VertxTest {
     private HttpInteractionLogger httpInteractionLogger;
     @Mock
     private PrebidVersionProvider prebidVersionProvider;
+    @Mock(strictness = LENIENT)
+    private HooksMetricsService hooksMetricsService;
+    @Mock(strictness = LENIENT)
+    private HookStageExecutor hookStageExecutor;
 
-    private AuctionHandler auctionHandler;
+    private AuctionHandler target;
     @Mock
     private RoutingContext routingContext;
     @Mock
@@ -118,24 +159,34 @@ public void setUp() {
         given(httpResponse.setStatusCode(anyInt())).willReturn(httpResponse);
         given(httpResponse.headers()).willReturn(MultiMap.caseInsensitiveMultiMap());
 
-        given(skippedAuctionService.skipAuction(any())).willReturn(Future.failedFuture("Auction cannot be skipped"));
+        given(skippedAuctionService.skipAuction(any()))
+                .willReturn(Future.failedFuture("Auction cannot be skipped"));
 
         given(clock.millis()).willReturn(Instant.now().toEpochMilli());
 
         given(prebidVersionProvider.getNameVersionRecord()).willReturn("pbs-java/1.00");
 
+        given(hookStageExecutor.executeExitpointStage(any(), any(), any()))
+                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
+                        false,
+                        ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))));
+
+        given(hooksMetricsService.updateHooksMetrics(any())).willAnswer(invocation -> invocation.getArgument(0));
+
         timeout = new TimeoutFactory(clock).create(2000L);
 
-        auctionHandler = new AuctionHandler(
+        target = new AuctionHandler(
                 0.01,
                 auctionRequestFactory,
                 exchangeService,
                 skippedAuctionService,
                 analyticsReporterDelegator,
                 metrics,
+                hooksMetricsService,
                 clock,
                 httpInteractionLogger,
                 prebidVersionProvider,
+                hookStageExecutor,
                 jacksonMapper);
     }
 
@@ -150,7 +201,7 @@ public void shouldSetRequestTypeMetricToAuctionContext() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         final AuctionContext auctionContext = captureAuctionContext();
@@ -168,7 +219,7 @@ public void shouldUseTimeoutFromAuctionContext() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(captureAuctionContext())
@@ -194,7 +245,7 @@ public void shouldAddPrebidVersionResponseHeader() {
                         .build()));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(httpResponse.headers())
@@ -218,7 +269,7 @@ public void shouldAddObserveBrowsingTopicsResponseHeader() {
                         .build()));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(httpResponse.headers())
@@ -240,7 +291,7 @@ public void shouldComputeTimeoutBasedOnRequestProcessingStartTime() {
         given(clock.millis()).willReturn(now.toEpochMilli()).willReturn(now.plusMillis(50L).toEpochMilli());
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(captureAuctionContext())
@@ -260,13 +311,14 @@ public void shouldRespondWithServiceUnavailableIfBidRequestHasAccountBlocklisted
                 .willReturn(Future.failedFuture(new BlocklistedAccountException("Blocklisted account")));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).setStatusCode(eq(403));
         verify(httpResponse).end(eq("Blocklisted: Blocklisted account"));
 
         verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.blocklisted_account));
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
@@ -278,13 +330,14 @@ public void shouldRespondWithBadRequestIfBidRequestHasAccountWithInvalidConfig()
                 .willReturn(Future.failedFuture(new InvalidAccountConfigException("Invalid config")));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).setStatusCode(eq(400));
         verify(httpResponse).end(eq("Invalid config"));
 
         verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.bad_requests));
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
@@ -296,13 +349,14 @@ public void shouldRespondWithServiceUnavailableIfBidRequestHasAppBlocklisted() {
                 .willReturn(Future.failedFuture(new BlocklistedAppException("Blocklisted app")));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).setStatusCode(eq(403));
         verify(httpResponse).end(eq("Blocklisted: Blocklisted app"));
 
         verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.blocklisted_app));
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
@@ -314,13 +368,14 @@ public void shouldRespondWithBadRequestIfBidRequestIsInvalid() {
                 .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid")));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).setStatusCode(eq(400));
         verify(httpResponse).end(eq("Invalid request format: Request is invalid"));
 
         verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.badinput));
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
@@ -332,12 +387,13 @@ public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() {
                 .willReturn(Future.failedFuture(new UnauthorizedAccountException("Account id is not provided", null)));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verifyNoInteractions(exchangeService);
         verify(httpResponse).setStatusCode(eq(401));
         verify(httpResponse).end(eq("Account id is not provided"));
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
@@ -352,13 +408,14 @@ public void shouldRespondWithInternalServerErrorIfAuctionFails() {
                 .willThrow(new RuntimeException("Unexpected exception"));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).setStatusCode(eq(500));
         verify(httpResponse).end(eq("Critical error while running the auction: Unexpected exception"));
 
         verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.err));
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
@@ -372,28 +429,26 @@ public void shouldNotSendResponseIfClientClosedConnection() {
         given(routingContext.response().closed()).willReturn(true);
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse, never()).end(anyString());
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
     public void shouldRespondWithBidResponse() {
         // given
+        final AuctionContext auctionContext = givenAuctionContext(identity());
         given(auctionRequestFactory.parseRequest(any(), anyLong()))
-                .willReturn(Future.succeededFuture(givenAuctionContext(identity())));
+                .willReturn(Future.succeededFuture(auctionContext));
         given(auctionRequestFactory.enrichAuctionContext(any()))
                 .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0)));
-
-        final AuctionContext auctionContext = AuctionContext.builder()
-                .bidResponse(BidResponse.builder().build())
-                .build();
         given(exchangeService.holdAuction(any()))
-                .willReturn(Future.succeededFuture(auctionContext));
+                .willReturn(Future.succeededFuture(auctionContext.with(BidResponse.builder().build())));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(exchangeService).holdAuction(any());
@@ -404,13 +459,70 @@ public void shouldRespondWithBidResponse() {
                         tuple("x-prebid", "pbs-java/1.00"));
 
         verify(httpResponse).end(eq("{}"));
+
+        final ArgumentCaptor<MultiMap> responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class);
+        verify(hookStageExecutor).executeExitpointStage(
+                responseHeadersCaptor.capture(),
+                eq("{}"),
+                any());
+
+        assertThat(responseHeadersCaptor.getValue()).hasSize(2)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(
+                        tuple("Content-Type", "application/json"),
+                        tuple("x-prebid", "pbs-java/1.00"));
+
+        verify(hooksMetricsService).updateHooksMetrics(any());
+    }
+
+    @Test
+    public void shouldRespondWithBidResponseWhenExitpointChangesHeadersAndResponse() {
+        // given
+        final AuctionContext auctionContext = givenAuctionContext(identity());
+        given(auctionRequestFactory.parseRequest(any(), anyLong()))
+                .willReturn(Future.succeededFuture(auctionContext));
+        given(auctionRequestFactory.enrichAuctionContext(any()))
+                .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0)));
+        given(exchangeService.holdAuction(any()))
+                .willReturn(Future.succeededFuture(auctionContext.with(BidResponse.builder().build())));
+        given(hookStageExecutor.executeExitpointStage(any(), any(), any()))
+                .willReturn(Future.succeededFuture(HookStageExecutionResult.success(
+                        ExitpointPayloadImpl.of(
+                                MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"),
+                                "{\"response\":{}}"))));
+
+        // when
+        target.handle(routingContext);
+
+        // then
+        verify(exchangeService).holdAuction(any());
+        assertThat(httpResponse.headers()).hasSize(1)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsExactlyInAnyOrder(tuple("New-Header", "New-Header-Value"));
+
+        verify(httpResponse).end(eq("{\"response\":{}}"));
+
+        final ArgumentCaptor<MultiMap> responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class);
+        verify(hookStageExecutor).executeExitpointStage(
+                responseHeadersCaptor.capture(),
+                eq("{}"),
+                any());
+
+        assertThat(responseHeadersCaptor.getValue()).hasSize(2)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(
+                        tuple("Content-Type", "application/json"),
+                        tuple("x-prebid", "pbs-java/1.00"));
+
+        verify(hooksMetricsService).updateHooksMetrics(any());
     }
 
     @Test
     public void shouldRespondWithCorrectResolvedRequestMediaTypePriceGranularity() {
         // given
+        final AuctionContext auctionContext = givenAuctionContext(identity());
         given(auctionRequestFactory.parseRequest(any(), anyLong()))
-                .willReturn(Future.succeededFuture(givenAuctionContext(identity())));
+                .willReturn(Future.succeededFuture(auctionContext));
         given(auctionRequestFactory.enrichAuctionContext(any()))
                 .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0)));
 
@@ -430,20 +542,26 @@ public void shouldRespondWithCorrectResolvedRequestMediaTypePriceGranularity() {
                         .debug(ExtResponseDebug.of(null, resolvedRequest, null))
                         .build())
                 .build();
-        final AuctionContext auctionContext = AuctionContext.builder()
-                .bidResponse(bidResponse)
-                .build();
         given(exchangeService.holdAuction(any()))
-                .willReturn(Future.succeededFuture(auctionContext));
+                .willReturn(Future.succeededFuture(auctionContext.with(bidResponse)));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(exchangeService).holdAuction(any());
         verify(httpResponse).end(eq("{\"ext\":{\"debug\":{\"resolvedrequest\":{\"ext\":{\"prebid\":"
                 + "{\"targeting\":{\"mediatypepricegranularity\":{\"banner\":{\"precision\":1,\"ranges\":"
                 + "[{\"max\":10,\"increment\":1}]},\"native\":{}}},\"auctiontimestamp\":0}}}}}}"));
+
+        verify(hookStageExecutor).executeExitpointStage(
+                any(),
+                eq("{\"ext\":{\"debug\":{\"resolvedrequest\":{\"ext\":{\"prebid\":"
+                        + "{\"targeting\":{\"mediatypepricegranularity\":{\"banner\":{\"precision\":1,\"ranges\":"
+                        + "[{\"max\":10,\"increment\":1}]},\"native\":{}}},\"auctiontimestamp\":0}}}}}}"),
+                any());
+
+        verify(hooksMetricsService).updateHooksMetrics(any());
     }
 
     @Test
@@ -457,7 +575,7 @@ public void shouldIncrementOkOpenrtb2WebRequestMetrics() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.ok));
@@ -475,7 +593,7 @@ public void shouldIncrementOkOpenrtb2AppRequestMetrics() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2app), eq(MetricName.ok));
@@ -492,7 +610,7 @@ public void shouldIncrementAppRequestMetrics() {
                 .willReturn(Future.succeededFuture(givenAuctionContext(builder -> builder.app(App.builder().build()))));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(true), anyBoolean(), anyInt());
@@ -514,7 +632,7 @@ public void shouldIncrementNoCookieMetrics() {
                 + "AppleWebKit/601.7.7 (KHTML, like Gecko) Version/9.1.2 Safari/601.7.7");
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(eq(false), eq(false), anyInt());
@@ -532,7 +650,7 @@ public void shouldIncrementImpsRequestedMetrics() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateAppAndNoCookieAndImpsRequestedMetrics(anyBoolean(), anyBoolean(), eq(1));
@@ -551,7 +669,7 @@ public void shouldIncrementImpTypesMetrics() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateImpTypesMetrics(same(imps));
@@ -564,7 +682,7 @@ public void shouldIncrementBadinputOnParsingRequestOpenrtb2WebRequestMetrics() {
                 .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid")));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.badinput));
@@ -577,7 +695,7 @@ public void shouldIncrementErrOpenrtb2WebRequestMetrics() {
                 .willReturn(Future.failedFuture(new RuntimeException()));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.err));
@@ -604,7 +722,7 @@ public void shouldUpdateRequestTimeMetric() {
         });
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTimeMetric(eq(MetricName.request_time), eq(500L));
@@ -617,7 +735,7 @@ public void shouldNotUpdateRequestTimeMetricIfRequestFails() {
                 .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid")));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse, never()).endHandler(any());
@@ -641,7 +759,7 @@ public void shouldUpdateNetworkErrorMetric() {
         });
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.networkerr));
@@ -658,7 +776,7 @@ public void shouldNotUpdateNetworkErrorMetricIfResponseSucceeded() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics, never()).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.networkerr));
@@ -677,7 +795,7 @@ public void shouldUpdateNetworkErrorMetricIfClientClosedConnection() {
         given(routingContext.response().closed()).willReturn(true);
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.networkerr));
@@ -690,7 +808,7 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid()
                 .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid")));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         final AuctionEvent auctionEvent = captureAuctionEvent();
@@ -699,6 +817,7 @@ public void shouldPassBadRequestEventToAnalyticsReporterIfBidRequestIsInvalid()
                 .status(400)
                 .errors(singletonList("Invalid request format: Request is invalid"))
                 .build());
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
@@ -714,7 +833,7 @@ public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails(
                 .willThrow(new RuntimeException("Unexpected exception"));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         final AuctionEvent auctionEvent = captureAuctionEvent();
@@ -728,6 +847,8 @@ public void shouldPassInternalServerErrorEventToAnalyticsReporterIfAuctionFails(
                 .status(500)
                 .errors(singletonList("Unexpected exception"))
                 .build());
+
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
@@ -742,22 +863,71 @@ public void shouldPassSuccessfulEventToAnalyticsReporter() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         final AuctionEvent auctionEvent = captureAuctionEvent();
-        final AuctionContext expectedAuctionContext = auctionContext.toBuilder()
-                .requestTypeMetric(MetricName.openrtb2web)
-                .bidResponse(BidResponse.builder().build())
-                .build();
+        assertThat(auctionEvent.getHttpContext()).isEqualTo(givenHttpContext());
+        assertThat(auctionEvent.getBidResponse()).isEqualTo(BidResponse.builder().build());
+        assertThat(auctionEvent.getStatus()).isEqualTo(200);
+        assertThat(auctionEvent.getAuctionContext().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2web);
+        assertThat(auctionEvent.getAuctionContext().getBidResponse()).isEqualTo(BidResponse.builder().build());
+
+        final ArgumentCaptor<MultiMap> responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class);
+        verify(hookStageExecutor).executeExitpointStage(
+                responseHeadersCaptor.capture(),
+                eq("{}"),
+                any());
+
+        assertThat(responseHeadersCaptor.getValue()).hasSize(2)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(
+                        tuple("Content-Type", "application/json"),
+                        tuple("x-prebid", "pbs-java/1.00"));
 
-        assertThat(auctionEvent).isEqualTo(AuctionEvent.builder()
-                .httpContext(givenHttpContext())
-                .auctionContext(expectedAuctionContext)
-                .bidResponse(BidResponse.builder().build())
-                .status(200)
-                .errors(emptyList())
-                .build());
+        verify(hooksMetricsService).updateHooksMetrics(any());
+    }
+
+    @Test
+    public void shouldPassSuccessfulEventToAnalyticsReporterWhenExitpointHookChangesResponseAndHeaders() {
+        // given
+        final AuctionContext auctionContext = givenAuctionContext(identity());
+        given(auctionRequestFactory.parseRequest(any(), anyLong()))
+                .willReturn(Future.succeededFuture(auctionContext));
+        given(auctionRequestFactory.enrichAuctionContext(any()))
+                .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0)));
+        given(hookStageExecutor.executeExitpointStage(any(), any(), any()))
+                .willReturn(Future.succeededFuture(HookStageExecutionResult.success(
+                        ExitpointPayloadImpl.of(
+                                MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"),
+                                "{\"response\":{}}"))));
+
+        givenHoldAuction(BidResponse.builder().build());
+
+        // when
+        target.handle(routingContext);
+
+        // then
+        final AuctionEvent auctionEvent = captureAuctionEvent();
+        assertThat(auctionEvent.getHttpContext()).isEqualTo(givenHttpContext());
+        assertThat(auctionEvent.getBidResponse()).isEqualTo(BidResponse.builder().build());
+        assertThat(auctionEvent.getStatus()).isEqualTo(200);
+        assertThat(auctionEvent.getAuctionContext().getRequestTypeMetric()).isEqualTo(MetricName.openrtb2web);
+        assertThat(auctionEvent.getAuctionContext().getBidResponse()).isEqualTo(BidResponse.builder().build());
+
+        final ArgumentCaptor<MultiMap> responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class);
+        verify(hookStageExecutor).executeExitpointStage(
+                responseHeadersCaptor.capture(),
+                eq("{}"),
+                any());
+
+        assertThat(responseHeadersCaptor.getValue()).hasSize(2)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(
+                        tuple("Content-Type", "application/json"),
+                        tuple("x-prebid", "pbs-java/1.00"));
+
+        verify(hooksMetricsService).updateHooksMetrics(any());
     }
 
     @Test
@@ -774,7 +944,7 @@ public void shouldTolerateDuplicateQueryParamNames() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         final AuctionEvent auctionEvent = captureAuctionEvent();
@@ -798,7 +968,7 @@ public void shouldTolerateDuplicateHeaderNames() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         final AuctionEvent auctionEvent = captureAuctionEvent();
@@ -820,17 +990,236 @@ public void shouldSkipAuction() {
                         givenAuctionContext.skipAuction().with(BidResponse.builder().build())));
 
         // when
-        auctionHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(auctionRequestFactory, never()).enrichAuctionContext(any());
         verify(metrics).updateRequestTypeMetric(eq(MetricName.openrtb2web), eq(MetricName.ok));
-        verifyNoInteractions(exchangeService);
-        verifyNoInteractions(analyticsReporterDelegator);
+        verifyNoInteractions(exchangeService, analyticsReporterDelegator, hookStageExecutor);
+        verify(hooksMetricsService).updateHooksMetrics(any());
         verify(httpResponse).setStatusCode(eq(200));
         verify(httpResponse).end("{}");
     }
 
+    @Test
+    public void shouldReturnSendAuctionEventWithAuctionContextBidResponseDebugInfoHoldingExitpointHookOutcome() {
+        // given
+        final AuctionContext auctionContext = givenAuctionContext(identity()).toBuilder()
+                .hookExecutionContext(HookExecutionContext.of(
+                        Endpoint.openrtb2_amp,
+                        stageOutcomes()))
+                .build();
+
+        given(auctionRequestFactory.parseRequest(any(), anyLong()))
+                .willReturn(Future.succeededFuture(auctionContext));
+        given(auctionRequestFactory.enrichAuctionContext(any()))
+                .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0)));
+
+        given(hookStageExecutor.executeExitpointStage(any(), any(), any()))
+                .willAnswer(invocation -> {
+                    final AuctionContext context = invocation.getArgument(2, AuctionContext.class);
+                    final HookExecutionContext hookExecutionContext = context.getHookExecutionContext();
+                    hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of(
+                            "http-response",
+                            singletonList(
+                                    GroupExecutionOutcome.of(singletonList(
+                                            HookExecutionOutcome.builder()
+                                                    .hookId(HookId.of(
+                                                            "exitpoint-module",
+                                                            "exitpoint-hook"))
+                                                    .executionTime(4L)
+                                                    .status(ExecutionStatus.success)
+                                                    .message("exitpoint hook has been executed")
+                                                    .action(ExecutionAction.update)
+                                                    .analyticsTags(TagsImpl.of(singletonList(
+                                                            ActivityImpl.of(
+                                                                    "some-activity",
+                                                                    "success",
+                                                                    singletonList(ResultImpl.of(
+                                                                            "success",
+                                                                            mapper.createObjectNode(),
+                                                                            givenAppliedToImpl()))))))
+                                                    .build()))))));
+                    return Future.succeededFuture(HookStageExecutionResult.success(
+                            ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1))));
+                });
+
+        givenHoldAuction(BidResponse.builder().build());
+
+        // when
+        target.handle(routingContext);
+
+        // then
+        final AuctionEvent auctionEvent = captureAuctionEvent();
+        final BidResponse bidResponse = auctionEvent.getBidResponse();
+        final ExtModulesTraceAnalyticsTags expectedAnalyticsTags = ExtModulesTraceAnalyticsTags.of(singletonList(
+                ExtModulesTraceAnalyticsActivity.of(
+                        "some-activity",
+                        "success",
+                        singletonList(ExtModulesTraceAnalyticsResult.of(
+                                "success",
+                                mapper.createObjectNode(),
+                                givenExtModulesTraceAnalyticsAppliedTo())))));
+        assertThat(bidResponse.getExt().getPrebid().getModules().getTrace()).isEqualTo(ExtModulesTrace.of(
+                8L,
+                List.of(
+                        ExtModulesTraceStage.of(
+                                Stage.auction_response,
+                                4L,
+                                singletonList(ExtModulesTraceStageOutcome.of(
+                                        "auction-response",
+                                        4L,
+                                        singletonList(
+                                                ExtModulesTraceGroup.of(
+                                                        4L,
+                                                        asList(
+                                                                ExtModulesTraceInvocationResult.builder()
+                                                                        .hookId(HookId.of("module1", "hook1"))
+                                                                        .executionTime(4L)
+                                                                        .status(ExecutionStatus.success)
+                                                                        .message("module1 hook1")
+                                                                        .action(ExecutionAction.update)
+                                                                        .build(),
+                                                                ExtModulesTraceInvocationResult.builder()
+                                                                        .hookId(HookId.of("module1", "hook2"))
+                                                                        .executionTime(4L)
+                                                                        .status(ExecutionStatus.success)
+                                                                        .message("module1 hook2")
+                                                                        .action(ExecutionAction.no_action)
+                                                                        .build())))))),
+
+                        ExtModulesTraceStage.of(
+                                Stage.exitpoint,
+                                4L,
+                                singletonList(ExtModulesTraceStageOutcome.of(
+                                        "http-response",
+                                        4L,
+                                        singletonList(
+                                                ExtModulesTraceGroup.of(
+                                                        4L,
+                                                        singletonList(
+                                                                ExtModulesTraceInvocationResult.builder()
+                                                                        .hookId(HookId.of(
+                                                                                "exitpoint-module",
+                                                                                "exitpoint-hook"))
+                                                                        .executionTime(4L)
+                                                                        .status(ExecutionStatus.success)
+                                                                        .message("exitpoint hook has been executed")
+                                                                        .action(ExecutionAction.update)
+                                                                        .analyticsTags(expectedAnalyticsTags)
+                                                                        .build())))))))));
+    }
+
+    @Test
+    public void shouldReturnSendAuctionEventWithAuctionContextBidResponseAnalyticsTagsHoldingExitpointHookOutcome() {
+        // given
+        final ObjectNode analyticsNode = mapper.createObjectNode();
+        final ObjectNode optionsNode = analyticsNode.putObject("options");
+        optionsNode.put("enableclientdetails", true);
+
+        final AuctionContext givenAuctionContext = givenAuctionContext(
+                request -> request.ext(ExtRequest.of(ExtRequestPrebid.builder()
+                        .analytics(analyticsNode)
+                        .build()))).toBuilder()
+                .hookExecutionContext(HookExecutionContext.of(
+                        Endpoint.openrtb2_amp,
+                        stageOutcomes()))
+                .build();
+
+        given(auctionRequestFactory.parseRequest(any(), anyLong()))
+                .willReturn(Future.succeededFuture(givenAuctionContext));
+        given(auctionRequestFactory.enrichAuctionContext(any()))
+                .willAnswer(invocation -> Future.succeededFuture(invocation.getArgument(0)));
+
+        given(hookStageExecutor.executeExitpointStage(any(), any(), any()))
+                .willAnswer(invocation -> {
+                    final AuctionContext context = invocation.getArgument(2, AuctionContext.class);
+                    final HookExecutionContext hookExecutionContext = context.getHookExecutionContext();
+                    hookExecutionContext.getStageOutcomes().put(Stage.exitpoint, singletonList(StageExecutionOutcome.of(
+                            "http-response",
+                            singletonList(
+                                    GroupExecutionOutcome.of(singletonList(
+                                            HookExecutionOutcome.builder()
+                                                    .hookId(HookId.of(
+                                                            "exitpoint-module",
+                                                            "exitpoint-hook"))
+                                                    .executionTime(4L)
+                                                    .status(ExecutionStatus.success)
+                                                    .message("exitpoint hook has been executed")
+                                                    .action(ExecutionAction.update)
+                                                    .analyticsTags(TagsImpl.of(singletonList(
+                                                            ActivityImpl.of(
+                                                                    "some-activity",
+                                                                    "success",
+                                                                    singletonList(ResultImpl.of(
+                                                                            "success",
+                                                                            mapper.createObjectNode(),
+                                                                            givenAppliedToImpl()))))))
+                                                    .build()))))));
+                    return Future.succeededFuture(HookStageExecutionResult.success(
+                            ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1))));
+                });
+
+        givenHoldAuction(BidResponse.builder().build());
+
+        // when
+        target.handle(routingContext);
+
+        // then
+        final AuctionEvent auctionEvent = captureAuctionEvent();
+        final BidResponse bidResponse = auctionEvent.getBidResponse();
+        assertThat(bidResponse.getExt())
+                .extracting(ExtBidResponse::getPrebid)
+                .extracting(ExtBidResponsePrebid::getAnalytics)
+                .extracting(ExtAnalytics::getTags)
+                .asInstanceOf(InstanceOfAssertFactories.list(ExtAnalyticsTags.class))
+                .hasSize(1)
+                .allSatisfy(extAnalyticsTags -> {
+                    assertThat(extAnalyticsTags.getStage()).isEqualTo(Stage.exitpoint);
+                    assertThat(extAnalyticsTags.getModule()).isEqualTo("exitpoint-module");
+                    assertThat(extAnalyticsTags.getAnalyticsTags()).isNotNull();
+                });
+    }
+
+    private static AppliedToImpl givenAppliedToImpl() {
+        return AppliedToImpl.builder()
+                .impIds(asList("impId1", "impId2"))
+                .request(true)
+                .build();
+    }
+
+    private static ExtModulesTraceAnalyticsAppliedTo givenExtModulesTraceAnalyticsAppliedTo() {
+        return ExtModulesTraceAnalyticsAppliedTo.builder()
+                .impIds(asList("impId1", "impId2"))
+                .request(true)
+                .build();
+    }
+
+    private static EnumMap<Stage, List<StageExecutionOutcome>> stageOutcomes() {
+        final Map<Stage, List<StageExecutionOutcome>> stageOutcomes = new HashMap<>();
+
+        stageOutcomes.put(Stage.auction_response, singletonList(StageExecutionOutcome.of(
+                "auction-response",
+                singletonList(
+                        GroupExecutionOutcome.of(asList(
+                                HookExecutionOutcome.builder()
+                                        .hookId(HookId.of("module1", "hook1"))
+                                        .executionTime(4L)
+                                        .status(ExecutionStatus.success)
+                                        .message("module1 hook1")
+                                        .action(ExecutionAction.update)
+                                        .build(),
+                                HookExecutionOutcome.builder()
+                                        .hookId(HookId.of("module1", "hook2"))
+                                        .executionTime(4L)
+                                        .message("module1 hook2")
+                                        .status(ExecutionStatus.success)
+                                        .action(ExecutionAction.no_action)
+                                        .build()))))));
+
+        return new EnumMap<>(stageOutcomes);
+    }
+
     private AuctionContext captureAuctionContext() {
         final ArgumentCaptor<AuctionContext> captor = ArgumentCaptor.forClass(AuctionContext.class);
         verify(exchangeService).holdAuction(captor.capture());
@@ -862,9 +1251,14 @@ private AuctionContext givenAuctionContext(
                 .imp(emptyList())).build();
 
         final AuctionContext.AuctionContextBuilder auctionContextBuilder = AuctionContext.builder()
+                .account(Account.builder()
+                        .analytics(AccountAnalyticsConfig.of(true, null, null))
+                        .build())
                 .uidsCookie(uidsCookie)
                 .bidRequest(bidRequest)
                 .requestTypeMetric(MetricName.openrtb2web)
+                .debugContext(DebugContext.of(true, false, TraceLevel.verbose))
+                .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_auction))
                 .timeoutContext(TimeoutContext.of(0, timeout, 0));
 
         return auctionContextCustomizer.apply(auctionContextBuilder)
diff --git a/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java b/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java
index fe59fb31fcc..64efc34c093 100644
--- a/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java
+++ b/src/test/java/org/prebid/server/handler/openrtb2/VideoHandlerTest.java
@@ -19,19 +19,27 @@
 import org.prebid.server.analytics.model.VideoEvent;
 import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator;
 import org.prebid.server.auction.ExchangeService;
+import org.prebid.server.auction.HooksMetricsService;
 import org.prebid.server.auction.VideoResponseFactory;
 import org.prebid.server.auction.model.AuctionContext;
 import org.prebid.server.auction.model.CachedDebugLog;
 import org.prebid.server.auction.model.TimeoutContext;
 import org.prebid.server.auction.model.WithPodErrors;
+import org.prebid.server.auction.model.debug.DebugContext;
 import org.prebid.server.auction.requestfactory.VideoRequestFactory;
 import org.prebid.server.cache.CoreCacheService;
 import org.prebid.server.cookie.UidsCookie;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.UnauthorizedAccountException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
+import org.prebid.server.hooks.execution.HookStageExecutor;
+import org.prebid.server.hooks.execution.model.HookExecutionContext;
+import org.prebid.server.hooks.execution.model.HookStageExecutionResult;
+import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl;
 import org.prebid.server.metric.Metrics;
+import org.prebid.server.model.Endpoint;
+import org.prebid.server.proto.openrtb.ext.request.TraceLevel;
 import org.prebid.server.proto.response.VideoResponse;
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.AccountAuctionConfig;
@@ -56,6 +64,7 @@
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mock.Strictness.LENIENT;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 
@@ -86,8 +95,12 @@ public class VideoHandlerTest extends VertxTest {
     private UidsCookie uidsCookie;
     @Mock
     private PrebidVersionProvider prebidVersionProvider;
+    @Mock(strictness = LENIENT)
+    private HooksMetricsService hooksMetricsService;
+    @Mock(strictness = LENIENT)
+    private HookStageExecutor hookStageExecutor;
 
-    private VideoHandler videoHandler;
+    private VideoHandler target;
 
     private Timeout timeout;
 
@@ -107,16 +120,25 @@ public void setUp() {
 
         given(prebidVersionProvider.getNameVersionRecord()).willReturn("pbs-java/1.00");
 
+        given(hookStageExecutor.executeExitpointStage(any(), any(), any()))
+                .willAnswer(invocation -> Future.succeededFuture(HookStageExecutionResult.of(
+                        false,
+                        ExitpointPayloadImpl.of(invocation.getArgument(0), invocation.getArgument(1)))));
+
+        given(hooksMetricsService.updateHooksMetrics(any())).willAnswer(invocation -> invocation.getArgument(0));
+
         timeout = new TimeoutFactory(clock).create(2000L);
 
-        videoHandler = new VideoHandler(
+        target = new VideoHandler(
                 videoRequestFactory,
                 videoResponseFactory,
                 exchangeService, coreCacheService,
                 analyticsReporterDelegator,
                 metrics,
+                hooksMetricsService,
                 clock,
                 prebidVersionProvider,
+                hookStageExecutor,
                 jacksonMapper);
     }
 
@@ -130,7 +152,7 @@ public void shouldUseTimeoutFromAuctionContext() {
         givenHoldAuction(BidResponse.builder().build());
 
         // when
-        videoHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(captureAuctionContext())
@@ -154,7 +176,7 @@ public void shouldAddPrebidVersionResponseHeader() {
                         .build()));
 
         // when
-        videoHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(httpResponse.headers())
@@ -176,7 +198,7 @@ public void shouldAddObserveBrowsingTopicsResponseHeader() {
                         .build()));
 
         // when
-        videoHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(httpResponse.headers())
@@ -196,7 +218,7 @@ public void shouldComputeTimeoutBasedOnRequestProcessingStartTime() {
         given(clock.millis()).willReturn(now.toEpochMilli()).willReturn(now.plusMillis(50L).toEpochMilli());
 
         // when
-        videoHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         assertThat(captureAuctionContext())
@@ -214,11 +236,12 @@ public void shouldRespondWithBadRequestIfBidRequestIsInvalid() {
                 .willReturn(Future.failedFuture(new InvalidRequestException("Request is invalid")));
 
         // when
-        videoHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).setStatusCode(eq(400));
         verify(httpResponse).end(eq("Invalid request format: Request is invalid"));
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
@@ -228,12 +251,13 @@ public void shouldRespondWithUnauthorizedIfAccountIdIsInvalid() {
                 .willReturn(Future.failedFuture(new UnauthorizedAccountException("Account id is not provided", "1")));
 
         // when
-        videoHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verifyNoInteractions(exchangeService);
         verify(httpResponse).setStatusCode(eq(401));
         verify(httpResponse).end(eq("Unauthorised: Account id is not provided"));
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
@@ -246,11 +270,12 @@ public void shouldRespondWithInternalServerErrorIfAuctionFails() {
                 .willThrow(new RuntimeException("Unexpected exception"));
 
         // when
-        videoHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse).setStatusCode(eq(500));
         verify(httpResponse).end(eq("Critical error while running the auction: Unexpected exception"));
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
@@ -262,10 +287,11 @@ public void shouldNotSendResponseIfClientClosedConnection() {
         given(routingContext.response().closed()).willReturn(true);
 
         // when
-        videoHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(httpResponse, never()).end(anyString());
+        verifyNoInteractions(hooksMetricsService, hookStageExecutor);
     }
 
     @Test
@@ -280,10 +306,10 @@ public void shouldRespondWithBidResponse() {
                 .willReturn(VideoResponse.of(emptyList(), null));
 
         // when
-        videoHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
-        verify(videoResponseFactory).toVideoResponse(any(), any(), any());
+        verify(videoResponseFactory, times(2)).toVideoResponse(any(), any(), any());
 
         assertThat(httpResponse.headers()).hasSize(2)
                 .extracting(Map.Entry::getKey, Map.Entry::getValue)
@@ -291,6 +317,63 @@ public void shouldRespondWithBidResponse() {
                         tuple("Content-Type", "application/json"),
                         tuple("x-prebid", "pbs-java/1.00"));
         verify(httpResponse).end(eq("{\"adPods\":[]}"));
+
+        final ArgumentCaptor<MultiMap> responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class);
+        verify(hookStageExecutor).executeExitpointStage(
+                responseHeadersCaptor.capture(),
+                eq("{\"adPods\":[]}"),
+                any());
+
+        assertThat(responseHeadersCaptor.getValue()).hasSize(2)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(
+                        tuple("Content-Type", "application/json"),
+                        tuple("x-prebid", "pbs-java/1.00"));
+
+        verify(hooksMetricsService).updateHooksMetrics(any());
+    }
+
+    @Test
+    public void shouldRespondWithBidResponseWhenExitpointHookChangesResponseAndHeaders() {
+        // given
+        given(videoRequestFactory.fromRequest(any(), anyLong()))
+                .willReturn(Future.succeededFuture(givenAuctionContext(identity(), emptyList())));
+
+        givenHoldAuction(BidResponse.builder().build());
+
+        given(videoResponseFactory.toVideoResponse(any(), any(), any()))
+                .willReturn(VideoResponse.of(emptyList(), null));
+
+        given(hookStageExecutor.executeExitpointStage(any(), any(), any()))
+                .willReturn(Future.succeededFuture(HookStageExecutionResult.success(
+                        ExitpointPayloadImpl.of(
+                                MultiMap.caseInsensitiveMultiMap().add("New-Header", "New-Header-Value"),
+                                "{\"adPods\":[{\"something\":1}]}"))));
+
+        // when
+        target.handle(routingContext);
+
+        // then
+        verify(videoResponseFactory, times(2)).toVideoResponse(any(), any(), any());
+
+        assertThat(httpResponse.headers()).hasSize(1)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsExactlyInAnyOrder(tuple("New-Header", "New-Header-Value"));
+        verify(httpResponse).end(eq("{\"adPods\":[{\"something\":1}]}"));
+
+        final ArgumentCaptor<MultiMap> responseHeadersCaptor = ArgumentCaptor.forClass(MultiMap.class);
+        verify(hookStageExecutor).executeExitpointStage(
+                responseHeadersCaptor.capture(),
+                eq("{\"adPods\":[]}"),
+                any());
+
+        assertThat(responseHeadersCaptor.getValue()).hasSize(2)
+                .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                .containsOnly(
+                        tuple("Content-Type", "application/json"),
+                        tuple("x-prebid", "pbs-java/1.00"));
+
+        verify(hooksMetricsService).updateHooksMetrics(any());
     }
 
     @Test
@@ -309,7 +392,7 @@ public void shouldUpdateVideoEventWithCacheLogIdErrorAndCallCacheForDebugLogWhen
         given(coreCacheService.cacheVideoDebugLog(any(), anyInt())).willReturn("cacheKey");
 
         // when
-        videoHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(coreCacheService).cacheVideoDebugLog(any(), anyInt());
@@ -327,6 +410,7 @@ public void shouldCacheDebugLogWhenNoBidsWereReturnedAndDoesNotAddErrorToVideoEv
         final AuctionContext auctionContext = AuctionContext.builder()
                 .bidRequest(BidRequest.builder().imp(emptyList()).build())
                 .account(Account.builder().auction(AccountAuctionConfig.builder().videoCacheTtl(100).build()).build())
+                .debugContext(DebugContext.empty())
                 .cachedDebugLog(cachedDebugLog)
                 .build();
 
@@ -343,7 +427,7 @@ public void shouldCacheDebugLogWhenNoBidsWereReturnedAndDoesNotAddErrorToVideoEv
                 .willReturn(VideoResponse.of(emptyList(), null));
 
         // when
-        videoHandler.handle(routingContext);
+        target.handle(routingContext);
 
         // then
         verify(coreCacheService).cacheVideoDebugLog(any(), anyInt());
@@ -377,6 +461,8 @@ private WithPodErrors<AuctionContext> givenAuctionContext(
                 .uidsCookie(uidsCookie)
                 .bidRequest(bidRequest)
                 .timeoutContext(TimeoutContext.of(0, timeout, 0))
+                .debugContext(DebugContext.of(true, false, TraceLevel.verbose))
+                .hookExecutionContext(HookExecutionContext.of(Endpoint.openrtb2_video))
                 .build();
 
         return WithPodErrors.of(auctionContext, errors);
diff --git a/src/test/java/org/prebid/server/health/GeoLocationHealthCheckerTest.java b/src/test/java/org/prebid/server/health/GeoLocationHealthCheckerTest.java
index e7b8bff269e..b46abafac7e 100644
--- a/src/test/java/org/prebid/server/health/GeoLocationHealthCheckerTest.java
+++ b/src/test/java/org/prebid/server/health/GeoLocationHealthCheckerTest.java
@@ -9,7 +9,7 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.geolocation.GeoLocationService;
 import org.prebid.server.geolocation.model.GeoInfo;
 import org.prebid.server.health.model.StatusResponse;
diff --git a/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java b/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java
index 108605efe58..330e779a304 100644
--- a/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java
+++ b/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java
@@ -5,6 +5,7 @@
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.hooks.execution.model.HookId;
 import org.prebid.server.hooks.execution.model.StageWithHookType;
 import org.prebid.server.hooks.v1.Hook;
 import org.prebid.server.hooks.v1.InvocationContext;
@@ -19,6 +20,7 @@
 
 import static java.util.Collections.singleton;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
@@ -41,23 +43,17 @@ public void setUp() {
     }
 
     @Test
-    public void hookByIdShouldTolerateUnknownModule() {
-        // when
-        final EntrypointHook foundHook = hookCatalog.hookById(
-                "unknown-module", null, StageWithHookType.ENTRYPOINT);
-
-        // then
-        assertThat(foundHook).isNull();
+    public void hookByIdShouldThrowExceptionOnUnknownModule() {
+        // when and then
+        assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() ->
+                hookCatalog.hookById(HookId.of("unknown-module", null), StageWithHookType.ENTRYPOINT));
     }
 
     @Test
-    public void hookByIdShouldTolerateUnknownHook() {
-        // when
-        final EntrypointHook foundHook = hookCatalog.hookById(
-                "sample-module", "unknown-hook", StageWithHookType.ENTRYPOINT);
-
-        // then
-        assertThat(foundHook).isNull();
+    public void hookByIdShouldThrowExceptionOnUnknownHook() {
+        // when and then
+        assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() ->
+                hookCatalog.hookById(HookId.of("sample-module", "unknown-hook"), StageWithHookType.ENTRYPOINT));
     }
 
     @Test
@@ -67,7 +63,7 @@ public void hookByIdShouldReturnEntrypointHook() {
 
         // when
         final EntrypointHook foundHook = hookCatalog.hookById(
-                "sample-module", "sample-hook", StageWithHookType.ENTRYPOINT);
+                HookId.of("sample-module", "sample-hook"), StageWithHookType.ENTRYPOINT);
 
         // then
         assertThat(foundHook).isNotNull()
@@ -82,7 +78,7 @@ public void hookByIdShouldReturnRawAuctionRequestHook() {
 
         // when
         final RawAuctionRequestHook foundHook = hookCatalog.hookById(
-                "sample-module", "sample-hook", StageWithHookType.RAW_AUCTION_REQUEST);
+                HookId.of("sample-module", "sample-hook"), StageWithHookType.RAW_AUCTION_REQUEST);
 
         // then
         assertThat(foundHook).isNotNull()
@@ -97,7 +93,7 @@ public void hookByIdShouldReturnProcessedAuctionRequestHook() {
 
         // when
         final ProcessedAuctionRequestHook foundHook = hookCatalog.hookById(
-                "sample-module", "sample-hook", StageWithHookType.PROCESSED_AUCTION_REQUEST);
+                HookId.of("sample-module", "sample-hook"), StageWithHookType.PROCESSED_AUCTION_REQUEST);
 
         // then
         assertThat(foundHook).isNotNull()
@@ -112,7 +108,7 @@ public void hookByIdShouldReturnBidderRequestHook() {
 
         // when
         final BidderRequestHook foundHook = hookCatalog.hookById(
-                "sample-module", "sample-hook", StageWithHookType.BIDDER_REQUEST);
+                HookId.of("sample-module", "sample-hook"), StageWithHookType.BIDDER_REQUEST);
 
         // then
         assertThat(foundHook).isNotNull()
@@ -127,7 +123,7 @@ public void hookByIdShouldReturnRawBidderResponseHook() {
 
         // when
         final RawBidderResponseHook foundHook = hookCatalog.hookById(
-                "sample-module", "sample-hook", StageWithHookType.RAW_BIDDER_RESPONSE);
+                HookId.of("sample-module", "sample-hook"), StageWithHookType.RAW_BIDDER_RESPONSE);
 
         // then
         assertThat(foundHook).isNotNull()
@@ -142,7 +138,7 @@ public void hookByIdShouldReturnProcessedBidderResponseHook() {
 
         // when
         final ProcessedBidderResponseHook foundHook = hookCatalog.hookById(
-                "sample-module", "sample-hook", StageWithHookType.PROCESSED_BIDDER_RESPONSE);
+                HookId.of("sample-module", "sample-hook"), StageWithHookType.PROCESSED_BIDDER_RESPONSE);
 
         // then
         assertThat(foundHook).isNotNull()
@@ -157,7 +153,7 @@ public void hookByIdShouldReturnAuctionResponseHook() {
 
         // when
         final AuctionResponseHook foundHook = hookCatalog.hookById(
-                "sample-module", "sample-hook", StageWithHookType.AUCTION_RESPONSE);
+                HookId.of("sample-module", "sample-hook"), StageWithHookType.AUCTION_RESPONSE);
 
         // then
         assertThat(foundHook).isNotNull()
diff --git a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java
index 060855334cc..7f1d29925c1 100644
--- a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java
+++ b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java
@@ -2,10 +2,12 @@
 
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Site;
+import com.iab.openrtb.request.User;
 import com.iab.openrtb.response.Bid;
 import com.iab.openrtb.response.BidResponse;
-import io.vertx.core.CompositeFuture;
 import io.vertx.core.Future;
+import io.vertx.core.MultiMap;
 import io.vertx.core.Promise;
 import io.vertx.core.Vertx;
 import io.vertx.junit5.Checkpoint;
@@ -19,6 +21,7 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.VertxTest;
@@ -28,7 +31,8 @@
 import org.prebid.server.auction.model.debug.DebugContext;
 import org.prebid.server.bidder.model.BidderBid;
 import org.prebid.server.bidder.model.BidderSeatBid;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
+import org.prebid.server.hooks.execution.model.ABTest;
 import org.prebid.server.hooks.execution.model.EndpointExecutionPlan;
 import org.prebid.server.hooks.execution.model.ExecutionAction;
 import org.prebid.server.hooks.execution.model.ExecutionGroup;
@@ -43,21 +47,23 @@
 import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
 import org.prebid.server.hooks.execution.model.StageExecutionPlan;
 import org.prebid.server.hooks.execution.model.StageWithHookType;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
 import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
 import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl;
 import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl;
 import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl;
 import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl;
 import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl;
+import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationContext;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
 import org.prebid.server.hooks.v1.InvocationStatus;
-import org.prebid.server.hooks.v1.analytics.ActivityImpl;
-import org.prebid.server.hooks.v1.analytics.AppliedToImpl;
-import org.prebid.server.hooks.v1.analytics.ResultImpl;
-import org.prebid.server.hooks.v1.analytics.TagsImpl;
 import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
 import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
 import org.prebid.server.hooks.v1.auction.AuctionResponseHook;
@@ -74,14 +80,18 @@
 import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook;
 import org.prebid.server.hooks.v1.entrypoint.EntrypointHook;
 import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload;
+import org.prebid.server.hooks.v1.exitpoint.ExitpointHook;
+import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload;
 import org.prebid.server.model.CaseInsensitiveMultiMap;
 import org.prebid.server.model.Endpoint;
 import org.prebid.server.proto.openrtb.ext.response.BidType;
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.AccountHooksConfiguration;
+import org.prebid.server.settings.model.HooksAdminConfig;
 
 import java.time.Clock;
 import java.time.ZoneOffset;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -92,13 +102,14 @@
 import static java.util.Arrays.asList;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.emptyMap;
+import static java.util.Collections.singleton;
 import static java.util.Collections.singletonList;
 import static java.util.Collections.singletonMap;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assertions.entry;
+import static org.assertj.core.api.Assertions.tuple;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.BDDMockito.given;
 import static org.mockito.Mock.Strictness.LENIENT;
@@ -149,10 +160,10 @@ public void creationShouldFailWhenHostExecutionPlanHasUnknownHook() {
                                                 HookId.of("module-alpha", "hook-a"),
                                                 HookId.of("module-beta", "hook-a")))))))));
 
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT)))
-                .willReturn(null);
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.ENTRYPOINT)))
+                .willThrow(new IllegalArgumentException("Exception."));
 
-        givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultImpl.noAction()));
+        givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultUtils.noAction()));
 
         assertThatThrownBy(() -> createExecutor(hostPlan))
                 .isInstanceOf(IllegalArgumentException.class)
@@ -172,10 +183,10 @@ public void creationShouldFailWhenDefaultAccountExecutionPlanHasUnknownHook() {
                                                 HookId.of("module-alpha", "hook-a"),
                                                 HookId.of("module-beta", "hook-a")))))))));
 
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT)))
-                .willReturn(null);
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.ENTRYPOINT)))
+                .willThrow(new IllegalArgumentException("Exception."));
 
-        givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultImpl.noAction()));
+        givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultUtils.noAction()));
 
         assertThatThrownBy(() -> createExecutor(null, defaultAccountPlan))
                 .isInstanceOf(IllegalArgumentException.class)
@@ -233,7 +244,7 @@ public void shouldExecuteEntrypointHooksHappyPath(VertxTestContext context) {
         givenEntrypointHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(
+                immediateHook(InvocationResultUtils.succeeded(
                         payload -> EntrypointPayloadImpl.of(
                                 payload.queryParams(), payload.headers(), payload.body() + "-abc"),
                         "moduleAlphaContext")));
@@ -241,19 +252,19 @@ public void shouldExecuteEntrypointHooksHappyPath(VertxTestContext context) {
         givenEntrypointHook(
                 "module-alpha",
                 "hook-b",
-                delayedHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+                delayedHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                         payload.queryParams(), payload.headers(), payload.body() + "-def")), 40));
 
         givenEntrypointHook(
                 "module-beta",
                 "hook-a",
-                delayedHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+                delayedHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                         payload.queryParams(), payload.headers(), payload.body() + "-ghi")), 80));
 
         givenEntrypointHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(
+                immediateHook(InvocationResultUtils.succeeded(
                         payload -> EntrypointPayloadImpl.of(
                                 payload.queryParams(), payload.headers(), payload.body() + "-jkl"),
                         "moduleBetaContext")));
@@ -401,6 +412,70 @@ public void shouldBypassEntrypointHooksWhenNoPlanForStage(VertxTestContext conte
         }));
     }
 
+    @Test
+    public void shouldBypassEntrypointHooksThatAreDisabled(VertxTestContext context) {
+        // given
+        givenEntrypointHook(
+                "module-alpha",
+                "hook-a",
+                immediateHook(InvocationResultUtils.succeeded(
+                        payload -> EntrypointPayloadImpl.of(
+                                payload.queryParams(), payload.headers(), payload.body() + "-abc"),
+                        "moduleAlphaContext")));
+
+        givenEntrypointHook(
+                "module-alpha",
+                "hook-b",
+                delayedHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
+                        payload.queryParams(), payload.headers(), payload.body() + "-def")), 40));
+
+        givenEntrypointHook(
+                "module-beta",
+                "hook-a",
+                delayedHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
+                        payload.queryParams(), payload.headers(), payload.body() + "-ghi")), 80));
+
+        givenEntrypointHook(
+                "module-beta",
+                "hook-b",
+                immediateHook(InvocationResultUtils.succeeded(
+                        payload -> EntrypointPayloadImpl.of(
+                                payload.queryParams(), payload.headers(), payload.body() + "-jkl"),
+                        "moduleBetaContext")));
+
+        final HookStageExecutor executor = HookStageExecutor.create(
+                executionPlan(singletonMap(
+                        Endpoint.openrtb2_auction,
+                        EndpointExecutionPlan.of(singletonMap(Stage.entrypoint, execPlanTwoGroupsTwoHooksEach())))),
+                null,
+                Map.of("module-alpha", false),
+                hookCatalog,
+                timeoutFactory,
+                vertx,
+                clock,
+                jacksonMapper,
+                false);
+
+        final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
+
+        // when
+        final Future<HookStageExecutionResult<EntrypointPayload>> future = executor.executeEntrypointStage(
+                CaseInsensitiveMultiMap.empty(),
+                CaseInsensitiveMultiMap.empty(),
+                "body",
+                hookExecutionContext);
+
+        // then
+        future.onComplete(context.succeeding(result -> {
+            assertThat(result).isNotNull();
+            assertThat(result.isShouldReject()).isFalse();
+            assertThat(result.getPayload()).isNotNull().satisfies(payload ->
+                    assertThat(payload.body()).isEqualTo("body-ghi-jkl"));
+
+            context.completeNow();
+        }));
+    }
+
     @Test
     public void shouldExecuteEntrypointHooksToleratingMisbehavingHooks(VertxTestContext context) {
         // given
@@ -427,7 +502,7 @@ public void shouldExecuteEntrypointHooksToleratingMisbehavingHooks(VertxTestCont
         givenEntrypointHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                         payload.queryParams(), payload.headers(), payload.body() + "-jkl"))));
 
         final HookStageExecutor executor = createExecutor(
@@ -523,7 +598,7 @@ public void shouldExecuteEntrypointHooksToleratingTimeoutAndFailedFuture(VertxTe
                 "module-alpha",
                 "hook-b",
                 delayedHook(
-                        InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+                        InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                                 payload.queryParams(), payload.headers(), payload.body() + "-def")),
                         250));
 
@@ -532,14 +607,14 @@ public void shouldExecuteEntrypointHooksToleratingTimeoutAndFailedFuture(VertxTe
                 "module-beta",
                 "hook-a",
                 delayedHook(
-                        InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+                        InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                                 payload.queryParams(), payload.headers(), payload.body() + "-ghi")),
                         250));
 
         givenEntrypointHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                         payload.queryParams(), payload.headers(), payload.body() + "-jkl"))));
 
         final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
@@ -622,23 +697,23 @@ public void shouldExecuteEntrypointHooksHonoringStatusAndAction(VertxTestContext
         givenEntrypointHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.failed("Failed to contact service ACME")));
+                immediateHook(InvocationResultUtils.failed("Failed to contact service ACME")));
 
         givenEntrypointHook(
                 "module-alpha",
                 "hook-b",
-                immediateHook(InvocationResultImpl.noAction()));
+                immediateHook(InvocationResultUtils.noAction()));
 
         givenEntrypointHook(
                 "module-beta",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                         payload.queryParams(), payload.headers(), payload.body() + "-ghi"))));
 
         givenEntrypointHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                         payload.queryParams(), payload.headers(), payload.body() + "-jkl"))));
 
         final HookStageExecutor executor = createExecutor(
@@ -714,13 +789,13 @@ public void shouldExecuteEntrypointHooksWhenRequestIsRejectedByFirstGroup(VertxT
         givenEntrypointHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                         payload.queryParams(), payload.headers(), payload.body() + "-abc"))));
 
         givenEntrypointHook(
                 "module-beta",
                 "hook-a",
-                immediateHook(InvocationResultImpl.rejected("Request is of low quality")));
+                immediateHook(InvocationResultUtils.rejected("Request is of low quality")));
 
         final HookStageExecutor executor = createExecutor(
                 executionPlan(singletonMap(
@@ -786,24 +861,24 @@ public void shouldExecuteEntrypointHooksWhenRequestIsRejectedBySecondGroup(Vertx
         givenEntrypointHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                         payload.queryParams(), payload.headers(), payload.body() + "-abc"))));
 
         givenEntrypointHook(
                 "module-alpha",
                 "hook-b",
-                immediateHook(InvocationResultImpl.rejected("Request is of low quality")));
+                immediateHook(InvocationResultUtils.rejected("Request is of low quality")));
 
         givenEntrypointHook(
                 "module-beta",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                         payload.queryParams(), payload.headers(), payload.body() + "-def"))));
 
         givenEntrypointHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                         payload.queryParams(), payload.headers(), payload.body() + "-jkl"))));
 
         final HookStageExecutor executor = createExecutor(
@@ -902,7 +977,7 @@ public void shouldExecuteEntrypointHooksToleratingMisbehavingInvocationResult(Ve
         givenEntrypointHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> {
+                immediateHook(InvocationResultUtils.succeeded(payload -> {
                     throw new RuntimeException("Can not alter payload");
                 })));
 
@@ -1044,14 +1119,14 @@ public void shouldExecuteEntrypointHooksAndStoreResultInExecutionContext(VertxTe
     public void shouldExecuteEntrypointHooksAndPassInvocationContext(VertxTestContext context) {
         // given
         final EntrypointHookImpl hookImpl = spy(
-                EntrypointHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity()))));
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT)))
+                EntrypointHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity()))));
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.ENTRYPOINT)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.ENTRYPOINT)))
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.ENTRYPOINT)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT)))
+        given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.ENTRYPOINT)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.ENTRYPOINT)))
+        given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.ENTRYPOINT)))
                 .willReturn(hookImpl);
 
         final HookStageExecutor executor = createExecutor(
@@ -1107,8 +1182,8 @@ public void shouldExecuteEntrypointHooksAndPassInvocationContext(VertxTestContex
     public void shouldExecuteRawAuctionRequestHooksWhenNoExecutionPlanInAccount(VertxTestContext context) {
         // given
         final RawAuctionRequestHookImpl hookImpl = spy(
-                RawAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.noAction())));
-        given(hookCatalog.hookById(anyString(), anyString(), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+                RawAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.noAction())));
+        given(hookCatalog.hookById(any(), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
 
         final String hostPlan = executionPlan(singletonMap(
@@ -1140,9 +1215,9 @@ public void shouldExecuteRawAuctionRequestHooksWhenNoExecutionPlanInAccount(Vert
 
             verify(hookImpl, times(2)).call(any(), any());
             verify(hookCatalog, times(2))
-                    .hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST));
+                    .hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST));
             verify(hookCatalog, times(2))
-                    .hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST));
+                    .hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST));
 
             context.completeNow();
         }));
@@ -1152,8 +1227,8 @@ public void shouldExecuteRawAuctionRequestHooksWhenNoExecutionPlanInAccount(Vert
     public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan(VertxTestContext context) {
         // given
         final RawAuctionRequestHookImpl hookImpl = spy(
-                RawAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.noAction())));
-        given(hookCatalog.hookById(anyString(), anyString(), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+                RawAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.noAction())));
+        given(hookCatalog.hookById(any(), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
 
         final String hostPlan = executionPlan(singletonMap(
@@ -1169,14 +1244,14 @@ public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan
         final HookStageExecutor executor = createExecutor(hostPlan, defaultAccountPlan);
 
         final BidRequest bidRequest = BidRequest.builder().build();
-        final ExecutionPlan accountPlan = ExecutionPlan.of(singletonMap(
+        final ExecutionPlan accountPlan = ExecutionPlan.of(emptyList(), singletonMap(
                 Endpoint.openrtb2_auction,
                 EndpointExecutionPlan.of(singletonMap(
                         Stage.raw_auction_request,
                         execPlanOneGroupOneHook("module-beta", "hook-b")))));
         final Account account = Account.builder()
                 .id("accountId")
-                .hooks(AccountHooksConfiguration.of(accountPlan, null))
+                .hooks(AccountHooksConfiguration.of(accountPlan, null, null))
                 .build();
         final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
 
@@ -1196,11 +1271,11 @@ public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan
 
             verify(hookImpl, times(2)).call(any(), any());
             verify(hookCatalog, times(2))
-                    .hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST));
+                    .hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST));
             verify(hookCatalog)
-                    .hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST));
+                    .hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST));
             verify(hookCatalog)
-                    .hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST));
+                    .hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST));
 
             context.completeNow();
         }));
@@ -1209,18 +1284,18 @@ public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan
     @Test
     public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPlan(VertxTestContext context) {
         // given
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
-                .willReturn(null);
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+                .willThrow(new IllegalArgumentException("Hook implementation does not exist or disabled"));
 
         givenRawAuctionRequestHook(
                 "module-beta",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().id("id").build()))));
 
         final HookStageExecutor executor = createExecutor(null, null);
 
-        final ExecutionPlan accountPlan = ExecutionPlan.of(singletonMap(
+        final ExecutionPlan accountPlan = ExecutionPlan.of(emptyList(), singletonMap(
                 Endpoint.openrtb2_auction,
                 EndpointExecutionPlan.of(singletonMap(
                         Stage.raw_auction_request,
@@ -1232,7 +1307,7 @@ public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPla
                                                 HookId.of("module-beta", "hook-a")))))))));
         final Account account = Account.builder()
                 .id("accountId")
-                .hooks(AccountHooksConfiguration.of(accountPlan, null))
+                .hooks(AccountHooksConfiguration.of(accountPlan, null, null))
                 .build();
         final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
 
@@ -1287,31 +1362,264 @@ public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPla
         }));
     }
 
+    @Test
+    public void shouldNotExecuteRawAuctionRequestHooksWhenAccountConfigIsNotRequired(VertxTestContext context) {
+        // given
+        givenRawAuctionRequestHook(
+                "module-alpha",
+                "hook-a",
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                        payload.bidRequest().toBuilder().at(1).build()))));
+
+        givenRawAuctionRequestHook(
+                "module-beta",
+                "hook-a",
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                        payload.bidRequest().toBuilder().test(1).build()))));
+
+        givenRawAuctionRequestHook(
+                "module-gamma",
+                "hook-b",
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                        payload.bidRequest().toBuilder().id("id").build()))));
+
+        givenRawAuctionRequestHook(
+                "module-delta",
+                "hook-b",
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                        payload.bidRequest().toBuilder().tmax(1000L).build()))));
+
+        givenRawAuctionRequestHook(
+                "module-epsilon",
+                "hook-a",
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                        payload.bidRequest().toBuilder().site(Site.builder().build()).build()))));
+
+        givenRawAuctionRequestHook(
+                "module-zeta",
+                "hook-b",
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                        payload.bidRequest().toBuilder().user(User.builder().build()).build()))));
+
+        final StageExecutionPlan stageExecutionPlan = StageExecutionPlan.of(asList(
+                ExecutionGroup.of(
+                        200L,
+                        asList(
+                                HookId.of("module-alpha", "hook-a"),
+                                HookId.of("module-beta", "hook-a"),
+                                HookId.of("module-epsilon", "hook-a"))),
+                ExecutionGroup.of(
+                        200L,
+                        asList(
+                                HookId.of("module-gamma", "hook-b"),
+                                HookId.of("module-delta", "hook-b"),
+                                HookId.of("module-zeta", "hook-b")))));
+
+        final String hostExecutionPlan = executionPlan(singletonMap(
+                Endpoint.openrtb2_auction,
+                EndpointExecutionPlan.of(singletonMap(Stage.raw_auction_request, stageExecutionPlan))));
+
+        final HookStageExecutor executor = HookStageExecutor.create(
+                hostExecutionPlan,
+                null,
+                Map.of("module-epsilon", true, "module-zeta", false),
+                hookCatalog,
+                timeoutFactory,
+                vertx,
+                clock,
+                jacksonMapper,
+                false);
+
+        final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
+
+        // when
+        final BidRequest givenBidRequest = BidRequest.builder().build();
+        final Future<HookStageExecutionResult<AuctionRequestPayload>> future = executor.executeRawAuctionRequestStage(
+                AuctionContext.builder()
+                        .bidRequest(givenBidRequest)
+                        .account(Account.builder()
+                                .id("accountId")
+                                .hooks(AccountHooksConfiguration.of(
+                                        null,
+                                        Map.of("module-alpha", mapper.createObjectNode(),
+                                                "module-beta", mapper.createObjectNode(),
+                                                "module-gamma", mapper.createObjectNode(),
+                                                "module-zeta", mapper.createObjectNode()),
+                                        HooksAdminConfig.builder()
+                                                .moduleExecution(Map.of(
+                                                        "module-alpha", true,
+                                                        "module-beta", false,
+                                                        "module-epsilon", false))
+                                                .build()))
+                                .build())
+                        .hookExecutionContext(hookExecutionContext)
+                        .debugContext(DebugContext.empty())
+                        .build());
+
+        // then
+        future.onComplete(context.succeeding(result -> {
+            assertThat(result).isNotNull();
+            assertThat(result.getPayload()).isNotNull().satisfies(payload ->
+                    assertThat(payload.bidRequest()).isEqualTo(BidRequest.builder()
+                            .at(1)
+                            .id("id")
+                            .tmax(1000L)
+                            .site(Site.builder().build())
+                            .build()));
+
+            assertThat(hookExecutionContext.getStageOutcomes())
+                    .hasEntrySatisfying(
+                            Stage.raw_auction_request,
+                            stageOutcomes -> assertThat(stageOutcomes)
+                                    .hasSize(1)
+                                    .extracting(StageExecutionOutcome::getEntity)
+                                    .containsOnly("auction-request"));
+
+            context.completeNow();
+        }));
+    }
+
+    @Test
+    public void shouldExecuteRawAuctionRequestHooksWhenAccountConfigIsRequired(VertxTestContext context) {
+        // given
+        givenRawAuctionRequestHook(
+                "module-alpha",
+                "hook-a",
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                        payload.bidRequest().toBuilder().at(1).build()))));
+
+        givenRawAuctionRequestHook(
+                "module-beta",
+                "hook-a",
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                        payload.bidRequest().toBuilder().test(1).build()))));
+
+        givenRawAuctionRequestHook(
+                "module-gamma",
+                "hook-b",
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                        payload.bidRequest().toBuilder().id("id").build()))));
+
+        givenRawAuctionRequestHook(
+                "module-delta",
+                "hook-b",
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                        payload.bidRequest().toBuilder().tmax(1000L).build()))));
+
+        givenRawAuctionRequestHook(
+                "module-epsilon",
+                "hook-a",
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                        payload.bidRequest().toBuilder().site(Site.builder().build()).build()))));
+
+        givenRawAuctionRequestHook(
+                "module-zeta",
+                "hook-b",
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                        payload.bidRequest().toBuilder().user(User.builder().build()).build()))));
+
+        final StageExecutionPlan stageExecutionPlan = StageExecutionPlan.of(asList(
+                ExecutionGroup.of(
+                        200L,
+                        asList(
+                                HookId.of("module-alpha", "hook-a"),
+                                HookId.of("module-beta", "hook-a"),
+                                HookId.of("module-epsilon", "hook-a"))),
+                ExecutionGroup.of(
+                        200L,
+                        asList(
+                                HookId.of("module-gamma", "hook-b"),
+                                HookId.of("module-delta", "hook-b"),
+                                HookId.of("module-zeta", "hook-b")))));
+
+        final String hostExecutionPlan = executionPlan(singletonMap(
+                Endpoint.openrtb2_auction,
+                EndpointExecutionPlan.of(singletonMap(Stage.raw_auction_request, stageExecutionPlan))));
+
+        final HookStageExecutor executor = HookStageExecutor.create(
+                hostExecutionPlan,
+                null,
+                Map.of("module-epsilon", true, "module-zeta", false),
+                hookCatalog,
+                timeoutFactory,
+                vertx,
+                clock,
+                jacksonMapper,
+                true);
+
+        final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
+
+        // when
+        final BidRequest givenBidRequest = BidRequest.builder().build();
+        final Future<HookStageExecutionResult<AuctionRequestPayload>> future = executor.executeRawAuctionRequestStage(
+                AuctionContext.builder()
+                        .bidRequest(givenBidRequest)
+                        .account(Account.builder()
+                                .id("accountId")
+                                .hooks(AccountHooksConfiguration.of(
+                                        null,
+                                        Map.of("module-alpha", mapper.createObjectNode(),
+                                                "module-beta", mapper.createObjectNode(),
+                                                "module-gamma", mapper.createObjectNode(),
+                                                "module-zeta", mapper.createObjectNode()),
+                                        HooksAdminConfig.builder()
+                                                .moduleExecution(Map.of(
+                                                        "module-alpha", true,
+                                                        "module-beta", false,
+                                                        "module-epsilon", false))
+                                                .build()))
+                                .build())
+                        .hookExecutionContext(hookExecutionContext)
+                        .debugContext(DebugContext.empty())
+                        .build());
+
+        // then
+        future.onComplete(context.succeeding(result -> {
+            assertThat(result).isNotNull();
+            assertThat(result.getPayload()).isNotNull().satisfies(payload ->
+                    assertThat(payload.bidRequest()).isEqualTo(BidRequest.builder()
+                            .at(1)
+                            .id("id")
+                            .site(Site.builder().build())
+                            .build()));
+
+            assertThat(hookExecutionContext.getStageOutcomes())
+                    .hasEntrySatisfying(
+                            Stage.raw_auction_request,
+                            stageOutcomes -> assertThat(stageOutcomes)
+                                    .hasSize(1)
+                                    .extracting(StageExecutionOutcome::getEntity)
+                                    .containsOnly("auction-request"));
+
+            context.completeNow();
+        }));
+    }
+
     @Test
     public void shouldExecuteRawAuctionRequestHooksHappyPath(VertxTestContext context) {
         // given
         givenRawAuctionRequestHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().at(1).build()))));
 
         givenRawAuctionRequestHook(
                 "module-alpha",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().id("id").build()))));
 
         givenRawAuctionRequestHook(
                 "module-beta",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().test(1).build()))));
 
         givenRawAuctionRequestHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().tmax(1000L).build()))));
 
         final HookStageExecutor executor = createExecutor(
@@ -1359,14 +1667,14 @@ public void shouldExecuteRawAuctionRequestHooksHappyPath(VertxTestContext contex
     public void shouldExecuteRawAuctionRequestHooksAndPassAuctionInvocationContext(VertxTestContext context) {
         // given
         final RawAuctionRequestHookImpl hookImpl = spy(
-                RawAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity()))));
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+                RawAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity()))));
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
 
         final HookStageExecutor executor = createExecutor(
@@ -1389,7 +1697,7 @@ public void shouldExecuteRawAuctionRequestHooksAndPassAuctionInvocationContext(V
                 AuctionContext.builder()
                         .bidRequest(BidRequest.builder().build())
                         .account(Account.builder()
-                                .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration))
+                                .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration, null))
                                 .build())
                         .hookExecutionContext(hookExecutionContext)
                         .debugContext(DebugContext.empty())
@@ -1450,17 +1758,17 @@ public void shouldExecuteRawAuctionRequestHooksAndPassModuleContextBetweenHooks(
                                     .build()));
                     return promise.future();
                 }));
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-beta"), eq("hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-beta", "hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
 
         final HookStageExecutor executor = createExecutor(
@@ -1532,7 +1840,7 @@ public void shouldExecuteRawAuctionRequestHooksWhenRequestIsRejected(VertxTestCo
         givenRawAuctionRequestHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.rejected("Request is no good")));
+                immediateHook(InvocationResultUtils.rejected("Request is no good")));
 
         final HookStageExecutor executor = createExecutor(
                 executionPlan(singletonMap(
@@ -1565,25 +1873,25 @@ public void shouldExecuteProcessedAuctionRequestHooksHappyPath(VertxTestContext
         givenProcessedAuctionRequestHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().at(1).build()))));
 
         givenProcessedAuctionRequestHook(
                 "module-alpha",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().id("id").build()))));
 
         givenProcessedAuctionRequestHook(
                 "module-beta",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().test(1).build()))));
 
         givenProcessedAuctionRequestHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().tmax(1000L).build()))));
 
         final HookStageExecutor executor = createExecutor(
@@ -1632,14 +1940,14 @@ public void shouldExecuteProcessedAuctionRequestHooksHappyPath(VertxTestContext
     public void shouldExecuteProcessedAuctionRequestHooksAndPassAuctionInvocationContext(VertxTestContext context) {
         // given
         final ProcessedAuctionRequestHookImpl hookImpl = spy(
-                ProcessedAuctionRequestHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity()))));
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
+                ProcessedAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity()))));
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
 
         final HookStageExecutor executor = createExecutor(
@@ -1663,7 +1971,7 @@ public void shouldExecuteProcessedAuctionRequestHooksAndPassAuctionInvocationCon
                         AuctionContext.builder()
                                 .bidRequest(BidRequest.builder().build())
                                 .account(Account.builder()
-                                        .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration))
+                                        .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration, null))
                                         .build())
                                 .hookExecutionContext(hookExecutionContext)
                                 .debugContext(DebugContext.empty())
@@ -1724,17 +2032,17 @@ public void shouldExecuteProcessedAuctionRequestHooksAndPassModuleContextBetween
                                     .build()));
                     return promise.future();
                 }));
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
-        given(hookCatalog.hookById(eq("module-beta"), eq("hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook("module-beta", "hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
                 .willReturn(hookImpl);
 
         final HookStageExecutor executor = createExecutor(
@@ -1807,7 +2115,7 @@ public void shouldExecuteProcessedAuctionRequestHooksWhenRequestIsRejected(Vertx
         givenProcessedAuctionRequestHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.rejected("Request is no good")));
+                immediateHook(InvocationResultUtils.rejected("Request is no good")));
 
         final HookStageExecutor executor = createExecutor(
                 executionPlan(singletonMap(
@@ -1843,25 +2151,25 @@ public void shouldExecuteBidderRequestHooksHappyPath(VertxTestContext context) {
         givenBidderRequestHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().at(1).build()))));
 
         givenBidderRequestHook(
                 "module-alpha",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().id("id").build()))));
 
         givenBidderRequestHook(
                 "module-beta",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().test(1).build()))));
 
         givenBidderRequestHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> BidderRequestPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> BidderRequestPayloadImpl.of(
                         payload.bidRequest().toBuilder().tmax(1000L).build()))));
 
         final HookStageExecutor executor = createExecutor(
@@ -1928,8 +2236,8 @@ public void shouldExecuteBidderRequestHooksHappyPath(VertxTestContext context) {
     public void shouldExecuteBidderRequestHooksAndPassBidderInvocationContext(VertxTestContext context) {
         // given
         final BidderRequestHookImpl hookImpl = spy(
-                BidderRequestHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity()))));
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.BIDDER_REQUEST)))
+                BidderRequestHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity()))));
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.BIDDER_REQUEST)))
                 .willReturn(hookImpl);
 
         final HookStageExecutor executor = createExecutor(
@@ -1950,7 +2258,7 @@ public void shouldExecuteBidderRequestHooksAndPassBidderInvocationContext(VertxT
                         .bidRequest(BidRequest.builder().build())
                         .account(Account.builder()
                                 .hooks(AccountHooksConfiguration.of(
-                                        null, singletonMap("module-alpha", mapper.createObjectNode())))
+                                        null, singletonMap("module-alpha", mapper.createObjectNode()), null))
                                 .build())
                         .hookExecutionContext(hookExecutionContext)
                         .debugContext(DebugContext.empty())
@@ -1979,7 +2287,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex
         givenRawBidderResponseHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of(
                         payload.bids().stream()
                                 .map(bid -> BidderBid.of(
                                         bid.getBid().toBuilder().id("bidId").build(),
@@ -1990,7 +2298,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex
         givenRawBidderResponseHook(
                 "module-alpha",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of(
                         payload.bids().stream()
                                 .map(bid -> BidderBid.of(
                                         bid.getBid().toBuilder().adid("adId").build(),
@@ -2001,7 +2309,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex
         givenRawBidderResponseHook(
                 "module-beta",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of(
                         payload.bids().stream()
                                 .map(bid -> BidderBid.of(
                                         bid.getBid().toBuilder().cid("cid").build(),
@@ -2012,7 +2320,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex
         givenRawBidderResponseHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of(
                         payload.bids().stream()
                                 .map(bid -> BidderBid.of(
                                         bid.getBid().toBuilder().adm("adm").build(),
@@ -2066,7 +2374,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex
             checkpoint1.flag();
         }));
 
-        CompositeFuture.join(future1, future2).onComplete(context.succeeding(result -> {
+        Future.join(future1, future2).onComplete(context.succeeding(result -> {
             assertThat(hookExecutionContext.getStageOutcomes())
                     .hasEntrySatisfying(
                             Stage.raw_bidder_response,
@@ -2083,8 +2391,8 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex
     public void shouldExecuteRawBidderResponseHooksAndPassBidderInvocationContext(VertxTestContext context) {
         // given
         final RawBidderResponseHookImpl hookImpl = spy(
-                RawBidderResponseHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity()))));
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_BIDDER_RESPONSE)))
+                RawBidderResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity()))));
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_BIDDER_RESPONSE)))
                 .willReturn(hookImpl);
 
         final HookStageExecutor executor = createExecutor(
@@ -2105,7 +2413,7 @@ public void shouldExecuteRawBidderResponseHooksAndPassBidderInvocationContext(Ve
                         .bidRequest(BidRequest.builder().build())
                         .account(Account.builder()
                                 .hooks(AccountHooksConfiguration.of(
-                                        null, singletonMap("module-alpha", mapper.createObjectNode())))
+                                        null, singletonMap("module-alpha", mapper.createObjectNode()), null))
                                 .build())
                         .hookExecutionContext(hookExecutionContext)
                         .debugContext(DebugContext.empty())
@@ -2134,7 +2442,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext
         givenProcessedBidderResponseHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of(
                         payload.bids().stream()
                                 .map(bid -> BidderBid.of(
                                         bid.getBid().toBuilder().id("bidId").build(),
@@ -2145,7 +2453,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext
         givenProcessedBidderResponseHook(
                 "module-alpha",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of(
                         payload.bids().stream()
                                 .map(bid -> BidderBid.of(
                                         bid.getBid().toBuilder().adid("adId").build(),
@@ -2156,7 +2464,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext
         givenProcessedBidderResponseHook(
                 "module-beta",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of(
                         payload.bids().stream()
                                 .map(bid -> BidderBid.of(
                                         bid.getBid().toBuilder().cid("cid").build(),
@@ -2167,7 +2475,7 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext
         givenProcessedBidderResponseHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> BidderResponsePayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> BidderResponsePayloadImpl.of(
                         payload.bids().stream()
                                 .map(bid -> BidderBid.of(
                                         bid.getBid().toBuilder().adm("adm").build(),
@@ -2241,8 +2549,8 @@ public void shouldExecuteProcessedBidderResponseHooksHappyPath(VertxTestContext
     public void shouldExecuteProcessedBidderResponseHooksAndPassBidderInvocationContext(VertxTestContext context) {
         // given
         final ProcessedBidderResponseHookImpl hookImpl = spy(
-                ProcessedBidderResponseHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity()))));
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE)))
+                ProcessedBidderResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity()))));
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE)))
                 .willReturn(hookImpl);
 
         final HookStageExecutor executor = createExecutor(
@@ -2266,7 +2574,7 @@ public void shouldExecuteProcessedBidderResponseHooksAndPassBidderInvocationCont
                                 .bidRequest(BidRequest.builder().build())
                                 .account(Account.builder()
                                         .hooks(AccountHooksConfiguration.of(
-                                                null, singletonMap("module-alpha", mapper.createObjectNode())))
+                                                null, singletonMap("module-alpha", mapper.createObjectNode()), null))
                                         .build())
                                 .hookExecutionContext(hookExecutionContext)
                                 .debugContext(DebugContext.empty())
@@ -2305,7 +2613,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() {
         givenAllProcessedBidderResponsesHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of(
                         payload.bidResponses().stream()
                                 .map(bidModifierForResponse.apply(
                                         (bidder, bid) -> BidderBid.of(
@@ -2317,7 +2625,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() {
         givenAllProcessedBidderResponsesHook(
                 "module-alpha",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of(
                         payload.bidResponses().stream()
                                 .map(bidModifierForResponse.apply(
                                         (bidder, bid) -> BidderBid.of(
@@ -2329,7 +2637,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() {
         givenAllProcessedBidderResponsesHook(
                 "module-beta",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of(
                         payload.bidResponses().stream()
                                 .map(bidModifierForResponse.apply(
                                         (bidder, bid) -> BidderBid.of(
@@ -2341,7 +2649,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() {
         givenAllProcessedBidderResponsesHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AllProcessedBidResponsesPayloadImpl.of(
                         payload.bidResponses().stream()
                                 .map(bidModifierForResponse.apply(
                                         (bidder, bid) -> BidderBid.of(
@@ -2406,8 +2714,8 @@ public void shouldExecuteAllProcessedBidResponsesHooksHappyPath() {
     public void shouldExecuteAllProcessedBidResponsesHooksAndPassAuctionInvocationContext(VertxTestContext context) {
         // given
         final AllProcessedBidResponsesHookImpl hookImpl = spy(
-                AllProcessedBidResponsesHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity()))));
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES)))
+                AllProcessedBidResponsesHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity()))));
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES)))
                 .willReturn(hookImpl);
 
         final HookStageExecutor executor = createExecutor(
@@ -2431,7 +2739,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksAndPassAuctionInvocationCo
                                 .bidRequest(BidRequest.builder().build())
                                 .account(Account.builder()
                                         .hooks(AccountHooksConfiguration.of(
-                                                null, singletonMap("module-alpha", mapper.createObjectNode())))
+                                                null, singletonMap("module-alpha", mapper.createObjectNode()), null))
                                         .build())
                                 .hookExecutionContext(hookExecutionContext)
                                 .debugContext(DebugContext.empty())
@@ -2459,7 +2767,7 @@ public void shouldExecuteBidderRequestHooksWhenRequestIsRejected(VertxTestContex
         givenBidderRequestHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.rejected("Request is no good")));
+                immediateHook(InvocationResultUtils.rejected("Request is no good")));
 
         final HookStageExecutor executor = createExecutor(
                 executionPlan(singletonMap(
@@ -2495,25 +2803,25 @@ public void shouldExecuteAuctionResponseHooksHappyPath(VertxTestContext context)
         givenAuctionResponseHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of(
                         payload.bidResponse().toBuilder().id("id").build()))));
 
         givenAuctionResponseHook(
                 "module-alpha",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of(
                         payload.bidResponse().toBuilder().bidid("bidid").build()))));
 
         givenAuctionResponseHook(
                 "module-beta",
                 "hook-a",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of(
                         payload.bidResponse().toBuilder().cur("cur").build()))));
 
         givenAuctionResponseHook(
                 "module-beta",
                 "hook-b",
-                immediateHook(InvocationResultImpl.succeeded(payload -> AuctionResponsePayloadImpl.of(
+                immediateHook(InvocationResultUtils.succeeded(payload -> AuctionResponsePayloadImpl.of(
                         payload.bidResponse().toBuilder().nbr(1).build()))));
 
         final HookStageExecutor executor = createExecutor(
@@ -2554,8 +2862,8 @@ public void shouldExecuteAuctionResponseHooksHappyPath(VertxTestContext context)
     public void shouldExecuteAuctionResponseHooksAndPassAuctionInvocationContext(VertxTestContext context) {
         // given
         final AuctionResponseHookImpl hookImpl = spy(
-                AuctionResponseHookImpl.of(immediateHook(InvocationResultImpl.succeeded(identity()))));
-        given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.AUCTION_RESPONSE)))
+                AuctionResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity()))));
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.AUCTION_RESPONSE)))
                 .willReturn(hookImpl);
 
         final HookStageExecutor executor = createExecutor(
@@ -2573,7 +2881,7 @@ public void shouldExecuteAuctionResponseHooksAndPassAuctionInvocationContext(Ver
                         .bidRequest(BidRequest.builder().build())
                         .account(Account.builder()
                                 .hooks(AccountHooksConfiguration.of(
-                                        null, singletonMap("module-alpha", mapper.createObjectNode())))
+                                        null, singletonMap("module-alpha", mapper.createObjectNode()), null))
                                 .build())
                         .hookExecutionContext(hookExecutionContext)
                         .debugContext(DebugContext.empty())
@@ -2595,13 +2903,55 @@ null, singletonMap("module-alpha", mapper.createObjectNode())))
         }));
     }
 
+    @Test
+    public void shouldExecuteAuctionResponseHooksAndTolerateNullAccount(VertxTestContext context) {
+        // given
+        final AuctionResponseHookImpl hookImpl = spy(
+                AuctionResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity()))));
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.AUCTION_RESPONSE)))
+                .willReturn(hookImpl);
+
+        final HookStageExecutor executor = createExecutor(
+                executionPlan(singletonMap(
+                        Endpoint.openrtb2_auction,
+                        EndpointExecutionPlan.of(singletonMap(
+                                Stage.auction_response, execPlanOneGroupOneHook("module-alpha", "hook-a"))))));
+
+        final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
+
+        // when
+        final Future<HookStageExecutionResult<AuctionResponsePayload>> future = executor.executeAuctionResponseStage(
+                BidResponse.builder().build(),
+                AuctionContext.builder()
+                        .bidRequest(BidRequest.builder().build())
+                        .account(null)
+                        .hookExecutionContext(hookExecutionContext)
+                        .debugContext(DebugContext.empty())
+                        .build());
+
+        // then
+        future.onComplete(context.succeeding(result -> {
+            final ArgumentCaptor<AuctionInvocationContext> invocationContextCaptor =
+                    ArgumentCaptor.forClass(AuctionInvocationContext.class);
+            verify(hookImpl).call(any(), invocationContextCaptor.capture());
+
+            assertThat(invocationContextCaptor.getValue()).satisfies(invocationContext -> {
+                assertThat(invocationContext.endpoint()).isNotNull();
+                assertThat(invocationContext.timeout()).isNotNull();
+                assertThat(invocationContext.accountConfig()).isNull();
+            });
+
+            context.completeNow();
+        }));
+    }
+
     @Test
     public void shouldExecuteAuctionResponseHooksAndIgnoreRejection(VertxTestContext context) {
         // given
         givenAuctionResponseHook(
                 "module-alpha",
                 "hook-a",
-                immediateHook(InvocationResultImpl.rejected("Will not apply")));
+                immediateHook(InvocationResultUtils.rejected("Will not apply")));
 
         final HookStageExecutor executor = createExecutor(
                 executionPlan(singletonMap(
@@ -2651,8 +3001,263 @@ public void shouldExecuteAuctionResponseHooksAndIgnoreRejection(VertxTestContext
         }));
     }
 
+    @Test
+    public void shouldExecuteExitpointHooksHappyPath(VertxTestContext context) {
+        // given
+        givenExitpointHook(
+                "module-alpha",
+                "hook-a",
+                immediateHook(InvocationResultUtils.succeeded(payload -> ExitpointPayloadImpl.of(
+                        payload.responseHeaders().add("Header-alpha-a", "alpha-a"),
+                        "{\"execution1\":\"alpha-a\""))));
+
+        givenExitpointHook(
+                "module-alpha",
+                "hook-b",
+                immediateHook(InvocationResultUtils.succeeded(payload -> ExitpointPayloadImpl.of(
+                        payload.responseHeaders().add("Header-alpha-b", "alpha-b"),
+                        payload.responseBody() + ",\"execution4\":\"alpha-b\"}"))));
+
+        givenExitpointHook(
+                "module-beta",
+                "hook-a",
+                immediateHook(InvocationResultUtils.succeeded(payload -> ExitpointPayloadImpl.of(
+                        payload.responseHeaders().add("Header-beta-a", "beta-a"),
+                        payload.responseBody() + ",\"execution2\":\"beta-a\""))));
+
+        givenExitpointHook(
+                "module-beta",
+                "hook-b",
+                immediateHook(InvocationResultUtils.succeeded(payload -> ExitpointPayloadImpl.of(
+                        payload.responseHeaders().add("Header-beta-b", "beta-b"),
+                        payload.responseBody() + ",\"execution3\":\"beta-b\""))));
+
+        final HookStageExecutor executor = createExecutor(
+                executionPlan(singletonMap(
+                        Endpoint.openrtb2_auction,
+                        EndpointExecutionPlan.of(singletonMap(
+                                Stage.exitpoint,
+                                execPlanTwoGroupsTwoHooksEach())))));
+
+        final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
+
+        // when
+        final Future<HookStageExecutionResult<ExitpointPayload>> future = executor.executeExitpointStage(
+                MultiMap.caseInsensitiveMultiMap().add("Header-Name", "Header-Value"),
+                "{}",
+                AuctionContext.builder()
+                        .bidRequest(BidRequest.builder().build())
+                        .account(Account.empty("accountId"))
+                        .hookExecutionContext(hookExecutionContext)
+                        .debugContext(DebugContext.empty())
+                        .build());
+
+        // then
+        future.onComplete(context.succeeding(result -> {
+            assertThat(result).isNotNull();
+            assertThat(result.getPayload()).isNotNull().satisfies(payload -> {
+                assertThat(payload.responseBody())
+                        .isEqualTo("{\"execution1\":\"alpha-a\",\"execution2\":\"beta-a\","
+                                + "\"execution3\":\"beta-b\",\"execution4\":\"alpha-b\"}");
+                assertThat(payload.responseHeaders()).hasSize(5)
+                        .extracting(Map.Entry::getKey, Map.Entry::getValue)
+                        .containsOnly(
+                                tuple("Header-Name", "Header-Value"),
+                                tuple("Header-alpha-a", "alpha-a"),
+                                tuple("Header-alpha-b", "alpha-b"),
+                                tuple("Header-beta-a", "beta-a"),
+                                tuple("Header-beta-b", "beta-b"));
+            });
+
+            context.completeNow();
+        }));
+    }
+
+    @Test
+    public void shouldExecuteExitpointHooksAndPassAuctionInvocationContext(VertxTestContext context) {
+        // given
+        final ExitpointHookImpl hookImpl = spy(
+                ExitpointHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity()))));
+        given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.EXITPOINT)))
+                .willReturn(hookImpl);
+
+        final HookStageExecutor executor = createExecutor(
+                executionPlan(singletonMap(
+                        Endpoint.openrtb2_auction,
+                        EndpointExecutionPlan.of(singletonMap(
+                                Stage.exitpoint, execPlanOneGroupOneHook("module-alpha", "hook-a"))))));
+
+        final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
+
+        // when
+        final Future<HookStageExecutionResult<ExitpointPayload>> future = executor.executeExitpointStage(
+                MultiMap.caseInsensitiveMultiMap().add("Header-Name", "Header-Value"),
+                "{}",
+                AuctionContext.builder()
+                        .bidRequest(BidRequest.builder().build())
+                        .account(Account.builder()
+                                .hooks(AccountHooksConfiguration.of(
+                                        null, singletonMap("module-alpha", mapper.createObjectNode()), null))
+                                .build())
+                        .hookExecutionContext(hookExecutionContext)
+                        .debugContext(DebugContext.empty())
+                        .build());
+
+        // then
+        future.onComplete(context.succeeding(result -> {
+            final ArgumentCaptor<AuctionInvocationContext> invocationContextCaptor =
+                    ArgumentCaptor.forClass(AuctionInvocationContext.class);
+            verify(hookImpl).call(any(), invocationContextCaptor.capture());
+
+            assertThat(invocationContextCaptor.getValue()).satisfies(invocationContext -> {
+                assertThat(invocationContext.endpoint()).isNotNull();
+                assertThat(invocationContext.timeout()).isNotNull();
+                assertThat(invocationContext.accountConfig()).isNotNull();
+            });
+
+            context.completeNow();
+        }));
+    }
+
+    @Test
+    public void shouldExecuteExitpointHooksAndIgnoreRejection(VertxTestContext context) {
+        // given
+        givenExitpointHook(
+                "module-alpha",
+                "hook-a",
+                immediateHook(InvocationResultUtils.rejected("Will not apply")));
+
+        final HookStageExecutor executor = createExecutor(
+                executionPlan(singletonMap(
+                        Endpoint.openrtb2_auction,
+                        EndpointExecutionPlan.of(singletonMap(
+                                Stage.exitpoint, execPlanOneGroupOneHook("module-alpha", "hook-a"))))));
+
+        final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
+
+        // when
+        final Future<HookStageExecutionResult<ExitpointPayload>> future = executor.executeExitpointStage(
+                MultiMap.caseInsensitiveMultiMap().add("Header-Name", "Header-Value"),
+                "{}",
+                AuctionContext.builder()
+                        .account(Account.empty("accountId"))
+                        .hookExecutionContext(hookExecutionContext)
+                        .debugContext(DebugContext.empty())
+                        .build());
+
+        // then
+        future.onComplete(context.succeeding(result -> {
+            assertThat(result.isShouldReject()).isFalse();
+            assertThat(result.getPayload()).isNotNull().satisfies(payload -> {
+                assertThat(payload.responseBody()).isNotNull();
+                assertThat(payload.responseBody()).isNotEmpty();
+            });
+
+            assertThat(hookExecutionContext.getStageOutcomes())
+                    .hasEntrySatisfying(
+                            Stage.exitpoint,
+                            stageOutcomes -> assertThat(stageOutcomes)
+                                    .hasSize(1)
+                                    .allSatisfy(stageOutcome -> {
+                                        assertThat(stageOutcome.getEntity()).isEqualTo("http-response");
+
+                                        final List<GroupExecutionOutcome> groups = stageOutcome.getGroups();
+
+                                        final List<HookExecutionOutcome> group0Hooks = groups.getFirst().getHooks();
+                                        assertThat(group0Hooks.getFirst()).satisfies(hookOutcome -> {
+                                            assertThat(hookOutcome.getHookId())
+                                                    .isEqualTo(HookId.of("module-alpha", "hook-a"));
+                                            assertThat(hookOutcome.getStatus())
+                                                    .isEqualTo(ExecutionStatus.execution_failure);
+                                            assertThat(hookOutcome.getMessage())
+                                                    .isEqualTo("Rejection is not supported during this stage");
+                                        });
+                                    }));
+
+            context.completeNow();
+        }));
+    }
+
+    @Test
+    public void abTestsForEntrypointStageShouldReturnEnabledTests() {
+        // given
+        final HookStageExecutor executor = createExecutor(executionPlan(asList(
+                ABTest.builder().enabled(true).accounts(singleton("1")).build(),
+                ABTest.builder().enabled(false).accounts(singleton("1")).build(),
+                ABTest.builder().enabled(false).accounts(singleton("2")).build(),
+                ABTest.builder().enabled(true).build())));
+
+        // when
+        final List<ABTest> abTests = executor.abTestsForEntrypointStage();
+
+        // then
+        assertThat(abTests)
+                .hasSize(2)
+                .extracting(ABTest::isEnabled)
+                .containsOnly(true);
+    }
+
+    @Test
+    public void abTestsShouldReturnEnabledTestsFromAccount() {
+        // given
+        final HookStageExecutor executor = createExecutor(executionPlan(asList(
+                ABTest.builder().enabled(true).accounts(singleton("1")).build(),
+                ABTest.builder().enabled(false).accounts(singleton("1")).build(),
+                ABTest.builder().enabled(false).accounts(singleton("2")).build(),
+                ABTest.builder().enabled(true).build())));
+
+        final Account account = Account.builder()
+                .id("1")
+                .hooks(AccountHooksConfiguration.of(
+                        ExecutionPlan.of(
+                                asList(
+                                        ABTest.builder().enabled(true).accounts(singleton("3")).build(),
+                                        ABTest.builder().enabled(false).accounts(singleton("4")).build(),
+                                        ABTest.builder().enabled(true).build()),
+                                emptyMap()),
+                        emptyMap(),
+                        null))
+                .build();
+
+        // when
+        final List<ABTest> abTests = executor.abTests(account);
+
+        // then
+        assertThat(abTests).containsExactly(
+                ABTest.builder().enabled(true).accounts(singleton("3")).build(),
+                ABTest.builder().enabled(true).build());
+    }
+
+    @Test
+    public void abTestsShouldReturnEnabledTestsFromHost() {
+        // given
+        final HookStageExecutor executor = createExecutor(
+                executionPlan(asList(
+                        ABTest.builder().enabled(true).accounts(singleton("1")).build(),
+                        ABTest.builder().enabled(false).accounts(singleton("1")).build(),
+                        ABTest.builder().enabled(false).accounts(singleton("2")).build(),
+                        ABTest.builder().enabled(true).build())),
+                jacksonMapper.encodeToString(ExecutionPlan.empty()));
+
+        final Account account = Account.builder()
+                .id("1")
+                .build();
+
+        // when
+        final List<ABTest> abTests = executor.abTests(account);
+
+        // then
+        assertThat(abTests).containsExactly(
+                ABTest.builder().enabled(true).accounts(singleton("1")).build(),
+                ABTest.builder().enabled(true).build());
+    }
+
     private String executionPlan(Map<Endpoint, EndpointExecutionPlan> endpoints) {
-        return jacksonMapper.encodeToString(ExecutionPlan.of(endpoints));
+        return jacksonMapper.encodeToString(ExecutionPlan.of(null, endpoints));
+    }
+
+    private String executionPlan(List<ABTest> abTests) {
+        return jacksonMapper.encodeToString(ExecutionPlan.of(abTests, emptyMap()));
     }
 
     private static StageExecutionPlan execPlanTwoGroupsTwoHooksEach() {
@@ -2681,7 +3286,7 @@ private void givenEntrypointHook(
             String hookImplCode,
             BiFunction<EntrypointPayload, InvocationContext, Future<InvocationResult<EntrypointPayload>>> delegate) {
 
-        given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.ENTRYPOINT)))
+        given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.ENTRYPOINT)))
                 .willReturn(EntrypointHookImpl.of(delegate));
     }
 
@@ -2693,7 +3298,7 @@ private void givenRawAuctionRequestHook(
                     AuctionInvocationContext,
                     Future<InvocationResult<AuctionRequestPayload>>> delegate) {
 
-        given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.RAW_AUCTION_REQUEST)))
                 .willReturn(RawAuctionRequestHookImpl.of(delegate));
     }
 
@@ -2705,7 +3310,7 @@ private void givenProcessedAuctionRequestHook(
                     AuctionInvocationContext,
                     Future<InvocationResult<AuctionRequestPayload>>> delegate) {
 
-        given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
+        given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST)))
                 .willReturn(ProcessedAuctionRequestHookImpl.of(delegate));
     }
 
@@ -2717,7 +3322,7 @@ private void givenBidderRequestHook(
                     BidderInvocationContext,
                     Future<InvocationResult<BidderRequestPayload>>> delegate) {
 
-        given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.BIDDER_REQUEST)))
+        given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.BIDDER_REQUEST)))
                 .willReturn(BidderRequestHookImpl.of(delegate));
     }
 
@@ -2729,7 +3334,7 @@ private void givenRawBidderResponseHook(
                     BidderInvocationContext,
                     Future<InvocationResult<BidderResponsePayload>>> delegate) {
 
-        given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.RAW_BIDDER_RESPONSE)))
+        given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.RAW_BIDDER_RESPONSE)))
                 .willReturn(RawBidderResponseHookImpl.of(delegate));
     }
 
@@ -2741,7 +3346,7 @@ private void givenProcessedBidderResponseHook(
                     BidderInvocationContext,
                     Future<InvocationResult<BidderResponsePayload>>> delegate) {
 
-        given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE)))
+        given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE)))
                 .willReturn(ProcessedBidderResponseHookImpl.of(delegate));
     }
 
@@ -2753,7 +3358,7 @@ private void givenAllProcessedBidderResponsesHook(
                     AuctionInvocationContext,
                     Future<InvocationResult<AllProcessedBidResponsesPayload>>> delegate) {
 
-        given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES)))
+        given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES)))
                 .willReturn(AllProcessedBidResponsesHookImpl.of(delegate));
     }
 
@@ -2765,10 +3370,22 @@ private void givenAuctionResponseHook(
                     AuctionInvocationContext,
                     Future<InvocationResult<AuctionResponsePayload>>> delegate) {
 
-        given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.AUCTION_RESPONSE)))
+        given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.AUCTION_RESPONSE)))
                 .willReturn(AuctionResponseHookImpl.of(delegate));
     }
 
+    private void givenExitpointHook(
+            String moduleCode,
+            String hookImplCode,
+            BiFunction<
+                    ExitpointPayload,
+                    AuctionInvocationContext,
+                    Future<InvocationResult<ExitpointPayload>>> delegate) {
+
+        given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.EXITPOINT)))
+                .willReturn(ExitpointHookImpl.of(delegate));
+    }
+
     private <PAYLOAD, CONTEXT> BiFunction<PAYLOAD, CONTEXT, Future<InvocationResult<PAYLOAD>>> delayedHook(
             InvocationResult<PAYLOAD> result,
             int delay) {
@@ -2786,6 +3403,10 @@ private <PAYLOAD, CONTEXT> BiFunction<PAYLOAD, CONTEXT, Future<InvocationResult<
         return (payload, context) -> Future.succeededFuture(result);
     }
 
+    private static HookId eqHook(String moduleCode, String hookCode) {
+        return ArgumentMatchers.eq(HookId.of(moduleCode, hookCode));
+    }
+
     private HookStageExecutor createExecutor(String hostExecutionPlan) {
         return createExecutor(hostExecutionPlan, null);
     }
@@ -2794,11 +3415,13 @@ private HookStageExecutor createExecutor(String hostExecutionPlan, String defaul
         return HookStageExecutor.create(
                 hostExecutionPlan,
                 defaultAccountExecutionPlan,
+                Collections.emptyMap(),
                 hookCatalog,
                 timeoutFactory,
                 vertx,
                 clock,
-                jacksonMapper);
+                jacksonMapper,
+                false);
     }
 
     @Value(staticConstructor = "of")
@@ -2990,4 +3613,28 @@ public String code() {
             return code;
         }
     }
+
+    @Value(staticConstructor = "of")
+    @NonFinal
+    private static class ExitpointHookImpl implements ExitpointHook {
+
+        String code = "hook-code";
+
+        BiFunction<
+                ExitpointPayload,
+                AuctionInvocationContext,
+                Future<InvocationResult<ExitpointPayload>>> delegate;
+
+        @Override
+        public Future<InvocationResult<ExitpointPayload>> call(ExitpointPayload payload,
+                                                               AuctionInvocationContext invocationContext) {
+
+            return delegate.apply(payload, invocationContext);
+        }
+
+        @Override
+        public String code() {
+            return code;
+        }
+    }
 }
diff --git a/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProviderTest.java b/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProviderTest.java
new file mode 100644
index 00000000000..fa6f3291267
--- /dev/null
+++ b/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProviderTest.java
@@ -0,0 +1,132 @@
+package org.prebid.server.hooks.execution.provider.abtest;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.prebid.server.VertxTest;
+import org.prebid.server.hooks.execution.model.ABTest;
+import org.prebid.server.hooks.execution.model.ExecutionAction;
+import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookExecutionContext;
+import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
+import org.prebid.server.hooks.execution.provider.HookProvider;
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.InvocationContext;
+import org.prebid.server.model.Endpoint;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class ABTestHookProviderTest extends VertxTest {
+
+    @Mock
+    private HookProvider<Object, InvocationContext> innerHookProvider;
+
+    @Mock
+    private Hook<Object, InvocationContext> innerHook;
+
+    @BeforeEach
+    public void setUp() {
+        given(innerHookProvider.apply(any())).willReturn(innerHook);
+    }
+
+    @Test
+    public void applyShouldReturnOriginalHookIfNoABTestFound() {
+        // given
+        final HookProvider<Object, InvocationContext> target = new ABTestHookProvider<>(
+                innerHookProvider,
+                singletonList(ABTest.builder().moduleCode("otherModule").build()),
+                HookExecutionContext.of(Endpoint.openrtb2_auction),
+                mapper);
+
+        // when
+        final Hook<Object, InvocationContext> result = target.apply(hookId());
+
+        // then
+        verify(innerHookProvider).apply(any());
+        verifyNoInteractions(innerHook);
+        assertThat(result).isSameAs(innerHook);
+    }
+
+    @Test
+    public void applyShouldReturnWrappedHook() {
+        // given
+        final HookProvider<Object, InvocationContext> target = new ABTestHookProvider<>(
+                innerHookProvider,
+                singletonList(ABTest.builder().moduleCode("module").build()),
+                HookExecutionContext.of(Endpoint.openrtb2_auction),
+                mapper);
+
+        // when
+        final Hook<Object, InvocationContext> result = target.apply(hookId());
+
+        // then
+        verify(innerHookProvider).apply(any());
+        verifyNoInteractions(innerHook);
+        assertThat(result).isInstanceOf(ABTestHook.class);
+    }
+
+    @Test
+    public void shouldInvokeHookShouldReturnTrueIfThereIsAPreviousInvocation() {
+        // given
+        final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
+        hookExecutionContext.getStageOutcomes().put(Stage.entrypoint, singletonList(
+                StageExecutionOutcome.of("entity", singletonList(GroupExecutionOutcome.of(singletonList(
+                        HookExecutionOutcome.builder()
+                                .hookId(hookId())
+                                .action(ExecutionAction.update)
+                                .build()))))));
+
+        final ABTestHookProvider<Object, InvocationContext> target = new ABTestHookProvider<>(
+                innerHookProvider,
+                emptyList(),
+                hookExecutionContext,
+                mapper);
+
+        // when and then
+        verifyNoInteractions(innerHookProvider);
+        verifyNoInteractions(innerHook);
+        assertThat(target.shouldInvokeHook("module", null)).isTrue();
+    }
+
+    @Test
+    public void shouldInvokeHookShouldReturnFalseIfThereIsAPreviousExecutionWithoutInvocation() {
+        // given
+        final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction);
+        hookExecutionContext.getStageOutcomes().put(Stage.entrypoint, singletonList(
+                StageExecutionOutcome.of("entity", singletonList(GroupExecutionOutcome.of(singletonList(
+                        HookExecutionOutcome.builder()
+                                .hookId(hookId())
+                                .action(ExecutionAction.no_invocation)
+                                .build()))))));
+
+        final ABTestHookProvider<Object, InvocationContext> target = new ABTestHookProvider<>(
+                innerHookProvider,
+                emptyList(),
+                hookExecutionContext,
+                mapper);
+
+        // when and then
+        verifyNoInteractions(innerHookProvider);
+        verifyNoInteractions(innerHook);
+        assertThat(target.shouldInvokeHook("module", null)).isFalse();
+    }
+
+    private static HookId hookId() {
+        return HookId.of("module", "hook");
+    }
+}
diff --git a/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookTest.java b/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookTest.java
new file mode 100644
index 00000000000..f0d129cb5db
--- /dev/null
+++ b/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookTest.java
@@ -0,0 +1,183 @@
+package org.prebid.server.hooks.execution.provider.abtest;
+
+import io.vertx.core.Future;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.VertxTest;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.hooks.v1.Hook;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationContext;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
+import org.prebid.server.hooks.v1.InvocationStatus;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.prebid.server.hooks.v1.PayloadUpdate.identity;
+
+@ExtendWith(MockitoExtension.class)
+public class ABTestHookTest extends VertxTest {
+
+    @Mock
+    private Hook<Object, InvocationContext> innerHook;
+
+    @Mock
+    private Object payload;
+
+    @Mock
+    private InvocationContext invocationContext;
+
+    @Test
+    public void codeShouldReturnSameHookCode() {
+        // given
+        given(innerHook.code()).willReturn("code");
+
+        final Hook<Object, InvocationContext> target = new ABTestHook<>(
+                "module",
+                innerHook,
+                false,
+                false,
+                mapper);
+
+        // when and then
+        assertThat(target.code()).isEqualTo("code");
+    }
+
+    @Test
+    public void callShouldReturnSkippedResultWithoutTags() {
+        // given
+        final Hook<Object, InvocationContext> target = new ABTestHook<>(
+                "module",
+                innerHook,
+                false,
+                false,
+                mapper);
+
+        // when
+        final InvocationResult<Object> invocationResult = target.call(payload, invocationContext).result();
+
+        // then
+        assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success);
+        assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_invocation);
+        assertThat(invocationResult.analyticsTags()).isNull();
+    }
+
+    @Test
+    public void callShouldReturnSkippedResultWithTags() {
+        // given
+        final Hook<Object, InvocationContext> target = new ABTestHook<>(
+                "module",
+                innerHook,
+                false,
+                true,
+                mapper);
+
+        // when
+        final InvocationResult<Object> invocationResult = target.call(payload, invocationContext).result();
+
+        // then
+        assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success);
+        assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_invocation);
+        assertThat(invocationResult.analyticsTags().activities()).hasSize(1).allSatisfy(activity -> {
+            assertThat(activity.name()).isEqualTo("core-module-abtests");
+            assertThat(activity.status()).isEqualTo("success");
+            assertThat(activity.results()).hasSize(1).allSatisfy(result -> {
+                assertThat(result.status()).isEqualTo("skipped");
+                assertThat(result.values()).isEqualTo(mapper.createObjectNode().put("module", "module"));
+            });
+        });
+    }
+
+    @Test
+    public void callShouldReturnRunResultWithoutTags() {
+        // given
+        final Hook<Object, InvocationContext> target = new ABTestHook<>(
+                "module",
+                innerHook,
+                true,
+                false,
+                mapper);
+
+        final InvocationResult<Object> innerHookInvocationResult = spy(InvocationResultUtils.succeeded(identity()));
+        final Future<InvocationResult<Object>> innerHookResult = Future.succeededFuture(innerHookInvocationResult);
+        given(innerHook.call(any(), any())).willReturn(innerHookResult);
+
+        // when
+        final Future<InvocationResult<Object>> result = target.call(payload, invocationContext);
+
+        // then
+        verify(innerHook).call(same(payload), same(invocationContext));
+        verifyNoInteractions(innerHookInvocationResult);
+        assertThat(result).isSameAs(innerHookResult);
+    }
+
+    @Test
+    public void callShouldReturnRunResultWithTags() {
+        // given
+        final Hook<Object, InvocationContext> target = new ABTestHook<>(
+                "module",
+                innerHook,
+                true,
+                true,
+                mapper);
+
+        final InvocationResult<Object> innerHookInvocationResult = spy(InvocationResultImpl.builder()
+                .status(InvocationStatus.success)
+                .message("message")
+                .action(InvocationAction.update)
+                .payloadUpdate(identity())
+                .errors(singletonList("error"))
+                .warnings(singletonList("warning"))
+                .debugMessages(singletonList("debugMessages"))
+                .moduleContext(new Object())
+                .analyticsTags(TagsImpl.of(asList(
+                        ActivityImpl.of("activity0", null, null),
+                        ActivityImpl.of("activity1", null, null))))
+                .build());
+        given(innerHook.call(any(), any())).willReturn(Future.succeededFuture(innerHookInvocationResult));
+
+        // when
+        final Future<InvocationResult<Object>> result = target.call(payload, invocationContext);
+
+        // then
+        verify(innerHook).call(same(payload), same(invocationContext));
+        verifyNoInteractions(innerHookInvocationResult);
+
+        final InvocationResult<Object> invocationResult = result.result();
+        assertThat(invocationResult.status()).isSameAs(innerHookInvocationResult.status());
+        assertThat(invocationResult.message()).isSameAs(innerHookInvocationResult.message());
+        assertThat(invocationResult.action()).isSameAs(innerHookInvocationResult.action());
+        assertThat(invocationResult.payloadUpdate()).isSameAs(innerHookInvocationResult.payloadUpdate());
+        assertThat(invocationResult.errors()).isSameAs(innerHookInvocationResult.errors());
+        assertThat(invocationResult.warnings()).isSameAs(innerHookInvocationResult.warnings());
+        assertThat(invocationResult.debugMessages()).isSameAs(innerHookInvocationResult.debugMessages());
+        assertThat(invocationResult.moduleContext()).isSameAs(innerHookInvocationResult.moduleContext());
+        assertThat(invocationResult.analyticsTags().activities()).satisfies(activities -> {
+            for (int i = 0; i < activities.size() - 1; i++) {
+                assertThat(activities.get(i)).isSameAs(innerHookInvocationResult.analyticsTags().activities().get(i));
+            }
+
+            assertThat(activities.getLast()).satisfies(activity -> {
+                assertThat(activity.name()).isEqualTo("core-module-abtests");
+                assertThat(activity.status()).isEqualTo("success");
+                assertThat(activity.results()).hasSize(1).allSatisfy(activityResult -> {
+                    assertThat(activityResult.status()).isEqualTo("run");
+                    assertThat(activityResult.values())
+                            .isEqualTo(mapper.createObjectNode().put("module", "module"));
+                });
+            });
+        });
+    }
+}
diff --git a/src/test/java/org/prebid/server/hooks/v1/InvocationResultImpl.java b/src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java
similarity index 75%
rename from src/test/java/org/prebid/server/hooks/v1/InvocationResultImpl.java
rename to src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java
index 31426173e9c..71d21ce9596 100644
--- a/src/test/java/org/prebid/server/hooks/v1/InvocationResultImpl.java
+++ b/src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java
@@ -1,34 +1,12 @@
 package org.prebid.server.hooks.v1;
 
-import lombok.Builder;
-import lombok.Value;
-import lombok.experimental.Accessors;
-import org.prebid.server.hooks.v1.analytics.Tags;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
 
-import java.util.List;
+public class InvocationResultUtils {
 
-@Accessors(fluent = true)
-@Builder
-@Value
-public class InvocationResultImpl<PAYLOAD> implements InvocationResult<PAYLOAD> {
+    private InvocationResultUtils() {
 
-    InvocationStatus status;
-
-    String message;
-
-    InvocationAction action;
-
-    PayloadUpdate<PAYLOAD> payloadUpdate;
-
-    List<String> errors;
-
-    List<String> warnings;
-
-    List<String> debugMessages;
-
-    Object moduleContext;
-
-    Tags analyticsTags;
+    }
 
     public static <PAYLOAD> InvocationResult<PAYLOAD> succeeded(PayloadUpdate<PAYLOAD> payloadUpdate) {
         return InvocationResultImpl.<PAYLOAD>builder()
diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java
deleted file mode 100644
index 0965bef2b40..00000000000
--- a/src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.prebid.server.hooks.v1.analytics;
-
-import lombok.Value;
-import lombok.experimental.Accessors;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Value(staticConstructor = "of")
-public class ActivityImpl implements Activity {
-
-    String name;
-
-    String status;
-
-    List<Result> results;
-}
diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java
deleted file mode 100644
index 810313936a8..00000000000
--- a/src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.prebid.server.hooks.v1.analytics;
-
-import lombok.Builder;
-import lombok.Value;
-import lombok.experimental.Accessors;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Builder
-@Value
-public class AppliedToImpl implements AppliedTo {
-
-    List<String> impIds;
-
-    List<String> bidders;
-
-    boolean request;
-
-    boolean response;
-
-    List<String> bidIds;
-}
diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java
deleted file mode 100644
index 3558a22c3cd..00000000000
--- a/src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package org.prebid.server.hooks.v1.analytics;
-
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import lombok.Value;
-import lombok.experimental.Accessors;
-
-@Accessors(fluent = true)
-@Value(staticConstructor = "of")
-public class ResultImpl implements Result {
-
-    String status;
-
-    ObjectNode values;
-
-    AppliedTo appliedTo;
-}
diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java
deleted file mode 100644
index 92278f2469c..00000000000
--- a/src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.prebid.server.hooks.v1.analytics;
-
-import lombok.Value;
-import lombok.experimental.Accessors;
-
-import java.util.List;
-
-@Accessors(fluent = true)
-@Value(staticConstructor = "of")
-public class TagsImpl implements Tags {
-
-    List<Activity> activities;
-}
diff --git a/src/test/java/org/prebid/server/it/InsticatorTest.java b/src/test/java/org/prebid/server/it/InsticatorTest.java
new file mode 100644
index 00000000000..4a96954e5c5
--- /dev/null
+++ b/src/test/java/org/prebid/server/it/InsticatorTest.java
@@ -0,0 +1,36 @@
+package org.prebid.server.it;
+
+import io.restassured.response.Response;
+import org.json.JSONException;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.model.Endpoint;
+
+import java.io.IOException;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
+import static java.util.Collections.singletonList;
+
+public class InsticatorTest extends IntegrationTest {
+
+    @Test
+    public void openrtb2AuctionShouldRespondWithBidsFromInsticator() throws IOException, JSONException {
+        // given
+        WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/insticator-exchange"))
+                .withRequestBody(equalToJson(jsonFrom("openrtb2/insticator/test-insticator-bid-request.json")))
+                .willReturn(aResponse().withBody(jsonFrom("openrtb2/insticator/test-insticator-bid-response.json"))));
+
+        // when
+        final Response response = responseFor("openrtb2/insticator/test-auction-insticator-request.json",
+                Endpoint.openrtb2_auction);
+
+        // then
+        assertJsonEquals(
+                "openrtb2/insticator/test-auction-insticator-response.json",
+                response,
+                singletonList("insticator"));
+    }
+
+}
diff --git a/src/test/java/org/prebid/server/it/OwnAdxTest.java b/src/test/java/org/prebid/server/it/OwnAdxTest.java
index 1ab4ddb5d71..70c4d6e7686 100644
--- a/src/test/java/org/prebid/server/it/OwnAdxTest.java
+++ b/src/test/java/org/prebid/server/it/OwnAdxTest.java
@@ -3,9 +3,7 @@
 import io.restassured.response.Response;
 import org.json.JSONException;
 import org.junit.jupiter.api.Test;
-import org.junit.runner.RunWith;
 import org.prebid.server.model.Endpoint;
-import org.springframework.test.context.junit4.SpringRunner;
 
 import java.io.IOException;
 
@@ -16,7 +14,6 @@
 import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
 import static java.util.Collections.singletonList;
 
-@RunWith(SpringRunner.class)
 public class OwnAdxTest extends IntegrationTest {
 
     @Test
diff --git a/src/test/java/org/prebid/server/it/hooks/HooksTest.java b/src/test/java/org/prebid/server/it/hooks/HooksTest.java
index 3943338630c..902f12c6589 100644
--- a/src/test/java/org/prebid/server/it/hooks/HooksTest.java
+++ b/src/test/java/org/prebid/server/it/hooks/HooksTest.java
@@ -1,8 +1,21 @@
 package org.prebid.server.it.hooks;
 
+import io.restassured.http.Header;
 import io.restassured.response.Response;
 import org.json.JSONException;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.prebid.server.analytics.model.AuctionEvent;
+import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator;
+import org.prebid.server.hooks.execution.model.GroupExecutionOutcome;
+import org.prebid.server.hooks.execution.model.HookExecutionOutcome;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionOutcome;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
 import org.prebid.server.it.IntegrationTest;
 import org.prebid.server.version.PrebidVersionProvider;
 import org.skyscreamer.jsonassert.JSONAssert;
@@ -10,6 +23,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 
 import java.io.IOException;
+import java.util.List;
 
 import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
 import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson;
@@ -18,6 +32,8 @@
 import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
 import static io.restassured.RestAssured.given;
 import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
 import static org.hamcrest.Matchers.empty;
 
 public class HooksTest extends IntegrationTest {
@@ -27,8 +43,13 @@ public class HooksTest extends IntegrationTest {
     @Autowired
     private PrebidVersionProvider versionProvider;
 
+    @Autowired
+    private AnalyticsReporterDelegator analyticsReporterDelegator;
+
     @Test
     public void openrtb2AuctionShouldRunHooksAtEachStage() throws IOException, JSONException {
+        Mockito.reset(analyticsReporterDelegator);
+
         // given
         WIRE_MOCK_RULE.stubFor(post(urlPathEqualTo("/rubicon-exchange"))
                 .withRequestBody(equalToJson(
@@ -47,6 +68,39 @@ public void openrtb2AuctionShouldRunHooksAtEachStage() throws IOException, JSONE
                 "hooks/sample-module/test-auction-sample-module-response.json", response, singletonList(RUBICON));
 
         JSONAssert.assertEquals(expectedAuctionResponse, response.asString(), JSONCompareMode.LENIENT);
+
+        //todo: remove everything below after at least one exitpoint module is added and tested by functional tests
+        assertThat(response.getHeaders())
+                .extracting(Header::getName, Header::getValue)
+                .contains(tuple("Exitpoint-Hook-Header", "Exitpoint-Hook-Value"));
+
+        final ArgumentCaptor<AuctionEvent> eventCaptor = ArgumentCaptor.forClass(AuctionEvent.class);
+        Mockito.verify(analyticsReporterDelegator).processEvent(eventCaptor.capture(), Mockito.any());
+
+        final AuctionEvent actualEvent = eventCaptor.getValue();
+        final List<StageExecutionOutcome> exitpointHookOutcomes = actualEvent.getAuctionContext()
+                .getHookExecutionContext().getStageOutcomes().get(Stage.exitpoint);
+
+        final TagsImpl expectedTags = TagsImpl.of(singletonList(ActivityImpl.of(
+                "exitpoint-device-id",
+                "success",
+                singletonList(ResultImpl.of(
+                        "success",
+                        mapper.createObjectNode().put("exitpoint-some-field", "exitpoint-some-value"),
+                        AppliedToImpl.builder()
+                                .impIds(singletonList("impId1"))
+                                .request(true)
+                                .build())))));
+
+        assertThat(exitpointHookOutcomes).isNotEmpty().hasSize(1).first()
+                .extracting(StageExecutionOutcome::getGroups)
+                .extracting(List::getFirst)
+                .extracting(GroupExecutionOutcome::getHooks)
+                .extracting(List::getFirst)
+                .extracting(HookExecutionOutcome::getAnalyticsTags)
+                .isEqualTo(expectedTags);
+
+        Mockito.reset(analyticsReporterDelegator);
     }
 
     @Test
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java
index 360e61fae47..8073a5edc27 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItAuctionResponseHook.java
@@ -4,7 +4,7 @@
 import io.vertx.core.Future;
 import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
 import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
 import org.prebid.server.hooks.v1.auction.AuctionResponseHook;
 import org.prebid.server.hooks.v1.auction.AuctionResponsePayload;
@@ -19,7 +19,7 @@ public Future<InvocationResult<AuctionResponsePayload>> call(
 
         final BidResponse updatedBidResponse = updateBidResponse(originalBidResponse);
 
-        return Future.succeededFuture(InvocationResultImpl.succeeded(payload ->
+        return Future.succeededFuture(InvocationResultUtils.succeeded(payload ->
                 AuctionResponsePayloadImpl.of(payload.bidResponse().toBuilder()
                         .seatbid(updatedBidResponse.getSeatbid())
                         .build())));
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java
index 10af73e4d4f..4c95bcb5a7f 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItBidderRequestHook.java
@@ -5,7 +5,7 @@
 import io.vertx.core.Future;
 import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
 import org.prebid.server.hooks.v1.bidder.BidderInvocationContext;
 import org.prebid.server.hooks.v1.bidder.BidderRequestHook;
 import org.prebid.server.hooks.v1.bidder.BidderRequestPayload;
@@ -22,7 +22,7 @@ public Future<InvocationResult<BidderRequestPayload>> call(
 
         final BidRequest updatedBidRequest = updateBidRequest(originalBidRequest);
 
-        return Future.succeededFuture(InvocationResultImpl.succeeded(payload ->
+        return Future.succeededFuture(InvocationResultUtils.succeeded(payload ->
                 BidderRequestPayloadImpl.of(payload.bidRequest().toBuilder()
                         .imp(updatedBidRequest.getImp())
                         .build())));
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java
index 36827bd39df..a4011db9106 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItEntrypointHook.java
@@ -5,7 +5,7 @@
 import org.prebid.server.hooks.execution.v1.entrypoint.EntrypointPayloadImpl;
 import org.prebid.server.hooks.v1.InvocationContext;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
 import org.prebid.server.hooks.v1.entrypoint.EntrypointHook;
 import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload;
 import org.prebid.server.model.CaseInsensitiveMultiMap;
@@ -18,7 +18,7 @@ public Future<InvocationResult<EntrypointPayload>> call(
 
         final boolean rejectFlag = Boolean.parseBoolean(entrypointPayload.queryParams().get("sample-it-module-reject"));
         if (rejectFlag) {
-            return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by sample entrypoint hook"));
+            return Future.succeededFuture(InvocationResultUtils.rejected("Rejected by sample entrypoint hook"));
         }
 
         return maybeUpdate(entrypointPayload);
@@ -35,7 +35,7 @@ private Future<InvocationResult<EntrypointPayload>> maybeUpdate(EntrypointPayloa
                 ? updateBody(entrypointPayload.body())
                 : entrypointPayload.body();
 
-        return Future.succeededFuture(InvocationResultImpl.succeeded(payload -> EntrypointPayloadImpl.of(
+        return Future.succeededFuture(InvocationResultUtils.succeeded(payload -> EntrypointPayloadImpl.of(
                 payload.queryParams(),
                 updatedHeaders,
                 updatedBody)));
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItExitpointHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItExitpointHook.java
new file mode 100644
index 00000000000..82a494e7158
--- /dev/null
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItExitpointHook.java
@@ -0,0 +1,80 @@
+package org.prebid.server.it.hooks;
+
+import com.iab.openrtb.response.BidResponse;
+import com.iab.openrtb.response.SeatBid;
+import io.vertx.core.Future;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
+import org.prebid.server.hooks.execution.v1.exitpoint.ExitpointPayloadImpl;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.exitpoint.ExitpointHook;
+import org.prebid.server.hooks.v1.exitpoint.ExitpointPayload;
+import org.prebid.server.json.JacksonMapper;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class SampleItExitpointHook implements ExitpointHook {
+
+    private final JacksonMapper mapper;
+
+    public SampleItExitpointHook(JacksonMapper mapper) {
+        this.mapper = mapper;
+    }
+
+    @Override
+    public Future<InvocationResult<ExitpointPayload>> call(ExitpointPayload exitpointPayload,
+                                                           AuctionInvocationContext invocationContext) {
+
+        final BidResponse bidResponse = invocationContext.auctionContext().getBidResponse();
+        final List<SeatBid> seatBids = updateBids(bidResponse.getSeatbid());
+        final BidResponse updatedResponse = bidResponse.toBuilder().seatbid(seatBids).build();
+
+        return Future.succeededFuture(InvocationResultImpl.<ExitpointPayload>builder()
+                .status(InvocationStatus.success)
+                .action(InvocationAction.update)
+                .payloadUpdate(payload -> ExitpointPayloadImpl.of(
+                        exitpointPayload.responseHeaders().add("Exitpoint-Hook-Header", "Exitpoint-Hook-Value"),
+                        mapper.encodeToString(updatedResponse)))
+                .debugMessages(Arrays.asList(
+                        "exitpoint debug message 1",
+                        "exitpoint debug message 2"))
+                .analyticsTags(TagsImpl.of(Collections.singletonList(ActivityImpl.of(
+                        "exitpoint-device-id",
+                        "success",
+                        Collections.singletonList(ResultImpl.of(
+                                "success",
+                                mapper.mapper().createObjectNode().put("exitpoint-some-field", "exitpoint-some-value"),
+                                AppliedToImpl.builder()
+                                        .impIds(Collections.singletonList("impId1"))
+                                        .request(true)
+                                        .build()))))))
+                .build());
+    }
+
+    private List<SeatBid> updateBids(List<SeatBid> seatBids) {
+        return seatBids.stream()
+                .map(seatBid -> seatBid.toBuilder().bid(seatBid.getBid().stream()
+                                .map(bid -> bid.toBuilder()
+                                        .adm(bid.getAdm()
+                                                + "<Impression><![CDATA[Exitpoint hook have been here]]>"
+                                                + "</Impression>")
+                                        .build())
+                                .toList())
+                        .build())
+                .toList();
+    }
+
+    @Override
+    public String code() {
+        return "exitpoint";
+    }
+
+}
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItModule.java b/src/test/java/org/prebid/server/it/hooks/SampleItModule.java
index e2806f8c87f..441240e7a32 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItModule.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItModule.java
@@ -31,7 +31,8 @@ public SampleItModule(JacksonMapper mapper) {
                 new SampleItRejectingProcessedAuctionRequestHook(),
                 new SampleItRejectingBidderRequestHook(),
                 new SampleItRejectingRawBidderResponseHook(),
-                new SampleItRejectingProcessedBidderResponseHook());
+                new SampleItRejectingProcessedBidderResponseHook(),
+                new SampleItExitpointHook(mapper));
     }
 
     @Override
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java
index a285235f420..dca19dd6043 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedAuctionRequestHook.java
@@ -6,7 +6,7 @@
 import io.vertx.core.Future;
 import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
 import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
 import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
 import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook;
@@ -29,7 +29,7 @@ public Future<InvocationResult<AuctionRequestPayload>> call(
 
         final BidRequest updatedBidRequest = updateBidRequest(originalBidRequest);
 
-        return Future.succeededFuture(InvocationResultImpl.succeeded(payload ->
+        return Future.succeededFuture(InvocationResultUtils.succeeded(payload ->
                 AuctionRequestPayloadImpl.of(payload.bidRequest().toBuilder()
                         .ext(updatedBidRequest.getExt())
                         .build())));
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java
index 3f8e9ee7ae2..b626e03d5ac 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItProcessedBidderResponseHook.java
@@ -4,7 +4,7 @@
 import org.prebid.server.bidder.model.BidderBid;
 import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
 import org.prebid.server.hooks.v1.bidder.BidderInvocationContext;
 import org.prebid.server.hooks.v1.bidder.BidderResponsePayload;
 import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook;
@@ -21,7 +21,7 @@ public Future<InvocationResult<BidderResponsePayload>> call(
 
         final List<BidderBid> updatedBids = updateBids(originalBids);
 
-        return Future.succeededFuture(InvocationResultImpl.succeeded(payload ->
+        return Future.succeededFuture(InvocationResultUtils.succeeded(payload ->
                 BidderResponsePayloadImpl.of(updatedBids)));
     }
 
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java
index c1054166dc1..f843083b0fb 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java
@@ -4,15 +4,15 @@
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.iab.openrtb.request.BidRequest;
 import io.vertx.core.Future;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl;
+import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl;
+import org.prebid.server.hooks.execution.v1.analytics.ResultImpl;
+import org.prebid.server.hooks.execution.v1.analytics.TagsImpl;
 import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
 import org.prebid.server.hooks.v1.InvocationAction;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
 import org.prebid.server.hooks.v1.InvocationStatus;
-import org.prebid.server.hooks.v1.analytics.ActivityImpl;
-import org.prebid.server.hooks.v1.analytics.AppliedToImpl;
-import org.prebid.server.hooks.v1.analytics.ResultImpl;
-import org.prebid.server.hooks.v1.analytics.TagsImpl;
 import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
 import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
 import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook;
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java
index fb6d915717e..0f30527519b 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItRawBidderResponseHook.java
@@ -4,7 +4,7 @@
 import org.prebid.server.bidder.model.BidderBid;
 import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
 import org.prebid.server.hooks.v1.bidder.BidderInvocationContext;
 import org.prebid.server.hooks.v1.bidder.BidderResponsePayload;
 import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook;
@@ -21,7 +21,7 @@ public Future<InvocationResult<BidderResponsePayload>> call(
 
         final List<BidderBid> updatedBids = updateBids(originalBids);
 
-        return Future.succeededFuture(InvocationResultImpl.succeeded(payload ->
+        return Future.succeededFuture(InvocationResultUtils.succeeded(payload ->
                 BidderResponsePayloadImpl.of(updatedBids)));
     }
 
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java
index bd90a974936..d08303b93ce 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingBidderRequestHook.java
@@ -2,7 +2,7 @@
 
 import io.vertx.core.Future;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
 import org.prebid.server.hooks.v1.bidder.BidderInvocationContext;
 import org.prebid.server.hooks.v1.bidder.BidderRequestHook;
 import org.prebid.server.hooks.v1.bidder.BidderRequestPayload;
@@ -13,7 +13,7 @@ public class SampleItRejectingBidderRequestHook implements BidderRequestHook {
     public Future<InvocationResult<BidderRequestPayload>> call(
             BidderRequestPayload bidderRequestPayload, BidderInvocationContext invocationContext) {
 
-        return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by rejecting bidder request hook"));
+        return Future.succeededFuture(InvocationResultUtils.rejected("Rejected by rejecting bidder request hook"));
     }
 
     @Override
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java
index b5feb3aaef9..5dfad73d026 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedAuctionRequestHook.java
@@ -2,7 +2,7 @@
 
 import io.vertx.core.Future;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
 import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
 import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
 import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook;
@@ -13,7 +13,7 @@ public class SampleItRejectingProcessedAuctionRequestHook implements ProcessedAu
     public Future<InvocationResult<AuctionRequestPayload>> call(
             AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) {
 
-        return Future.succeededFuture(InvocationResultImpl.rejected(
+        return Future.succeededFuture(InvocationResultUtils.rejected(
                 "Rejected by rejecting processed auction request hook"));
     }
 
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java
index a6f1438402c..d2c568837ca 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingProcessedBidderResponseHook.java
@@ -2,7 +2,7 @@
 
 import io.vertx.core.Future;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
 import org.prebid.server.hooks.v1.bidder.BidderInvocationContext;
 import org.prebid.server.hooks.v1.bidder.BidderResponsePayload;
 import org.prebid.server.hooks.v1.bidder.ProcessedBidderResponseHook;
@@ -13,7 +13,7 @@ public class SampleItRejectingProcessedBidderResponseHook implements ProcessedBi
     public Future<InvocationResult<BidderResponsePayload>> call(
             BidderResponsePayload bidderResponsePayload, BidderInvocationContext invocationContext) {
 
-        return Future.succeededFuture(InvocationResultImpl.rejected(
+        return Future.succeededFuture(InvocationResultUtils.rejected(
                 "Rejected by rejecting processed bidder response hook"));
     }
 
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java
index 5532962afc2..d4eda0346ca 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawAuctionRequestHook.java
@@ -2,7 +2,7 @@
 
 import io.vertx.core.Future;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
 import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
 import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
 import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook;
@@ -13,7 +13,7 @@ public class SampleItRejectingRawAuctionRequestHook implements RawAuctionRequest
     public Future<InvocationResult<AuctionRequestPayload>> call(
             AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) {
 
-        return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by rejecting raw auction request hook"));
+        return Future.succeededFuture(InvocationResultUtils.rejected("Rejected by rejecting raw auction request hook"));
     }
 
     @Override
diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java
index 0eeeee4375c..f2964a9871a 100644
--- a/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java
+++ b/src/test/java/org/prebid/server/it/hooks/SampleItRejectingRawBidderResponseHook.java
@@ -2,7 +2,7 @@
 
 import io.vertx.core.Future;
 import org.prebid.server.hooks.v1.InvocationResult;
-import org.prebid.server.hooks.v1.InvocationResultImpl;
+import org.prebid.server.hooks.v1.InvocationResultUtils;
 import org.prebid.server.hooks.v1.bidder.BidderInvocationContext;
 import org.prebid.server.hooks.v1.bidder.BidderResponsePayload;
 import org.prebid.server.hooks.v1.bidder.RawBidderResponseHook;
@@ -13,7 +13,7 @@ public class SampleItRejectingRawBidderResponseHook implements RawBidderResponse
     public Future<InvocationResult<BidderResponsePayload>> call(
             BidderResponsePayload bidderResponsePayload, BidderInvocationContext invocationContext) {
 
-        return Future.succeededFuture(InvocationResultImpl.rejected("Rejected by rejecting raw bidder response hook"));
+        return Future.succeededFuture(InvocationResultUtils.rejected("Rejected by rejecting raw bidder response hook"));
     }
 
     @Override
diff --git a/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java b/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java
index a08845f9c19..5fdf0724015 100644
--- a/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java
+++ b/src/test/java/org/prebid/server/it/hooks/TestHooksConfiguration.java
@@ -1,9 +1,12 @@
 package org.prebid.server.it.hooks;
 
+import org.mockito.Mockito;
+import org.prebid.server.analytics.reporter.AnalyticsReporterDelegator;
 import org.prebid.server.hooks.v1.Module;
 import org.prebid.server.json.JacksonMapper;
 import org.springframework.boot.test.context.TestConfiguration;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
 
 @TestConfiguration
 public class TestHooksConfiguration {
@@ -12,4 +15,10 @@ public class TestHooksConfiguration {
     Module sampleItModule(JacksonMapper mapper) {
         return new SampleItModule(mapper);
     }
+
+    @Bean
+    @Primary
+    AnalyticsReporterDelegator spyAnalyticsReporterDelegator(AnalyticsReporterDelegator analyticsReporterDelegator) {
+        return Mockito.spy(analyticsReporterDelegator);
+    }
 }
diff --git a/src/test/java/org/prebid/server/metric/MetricsTest.java b/src/test/java/org/prebid/server/metric/MetricsTest.java
index 5594cad0c65..47b1da61b37 100644
--- a/src/test/java/org/prebid/server/metric/MetricsTest.java
+++ b/src/test/java/org/prebid/server/metric/MetricsTest.java
@@ -331,6 +331,16 @@ public void updateAppAndNoCookieAndImpsRequestedMetricsShouldIncrementMetrics()
         assertThat(metricRegistry.counter("imps_requested").getCount()).isEqualTo(4);
     }
 
+    @Test
+    public void updateDebugRequestsMetricsShouldIncrementMetrics() {
+        // when
+        metrics.updateDebugRequestMetrics(false);
+        metrics.updateDebugRequestMetrics(true);
+
+        // then
+        assertThat(metricRegistry.counter("debug_requests").getCount()).isOne();
+    }
+
     @Test
     public void updateImpTypesMetricsByCountPerMediaTypeShouldIncrementMetrics() {
         // given
@@ -427,6 +437,16 @@ public void updateAccountRequestMetricsShouldIncrementMetrics() {
         assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isOne();
     }
 
+    @Test
+    public void updateAccountDebugRequestMetricsShouldIncrementMetrics() {
+        // when
+        metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), false);
+        metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), true);
+
+        // then
+        assertThat(metricRegistry.counter("account.accountId.debug_requests").getCount()).isOne();
+    }
+
     @Test
     public void updateAdapterRequestTypeAndNoCookieMetricsShouldUpdateMetricsAsExpected() {
 
@@ -916,6 +936,8 @@ public void shouldNotUpdateAccountMetricsIfVerbosityIsNone() {
         given(accountMetricsVerbosityResolver.forAccount(any())).willReturn(AccountMetricsVerbosityLevel.none);
 
         // when
+        metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), false);
+        metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), true);
         metrics.updateAccountRequestMetrics(Account.empty(ACCOUNT_ID), MetricName.openrtb2web);
         metrics.updateAdapterResponseTime(RUBICON, Account.empty(ACCOUNT_ID), 500);
         metrics.updateAdapterRequestNobidMetrics(RUBICON, Account.empty(ACCOUNT_ID));
@@ -924,6 +946,7 @@ public void shouldNotUpdateAccountMetricsIfVerbosityIsNone() {
 
         // then
         assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isZero();
+        assertThat(metricRegistry.counter("account.accountId.debug_requests").getCount()).isZero();
         assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isZero();
         assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isZero();
         assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isZero();
@@ -939,6 +962,8 @@ public void shouldUpdateAccountRequestsMetricOnlyIfVerbosityIsBasic() {
 
         // when
         metrics.updateAccountRequestMetrics(Account.empty(ACCOUNT_ID), MetricName.openrtb2web);
+        metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), false);
+        metrics.updateAccountDebugRequestMetrics(Account.empty(ACCOUNT_ID), true);
         metrics.updateAdapterResponseTime(RUBICON, Account.empty(ACCOUNT_ID), 500);
         metrics.updateAdapterRequestNobidMetrics(RUBICON, Account.empty(ACCOUNT_ID));
         metrics.updateAdapterRequestGotbidsMetrics(RUBICON, Account.empty(ACCOUNT_ID));
@@ -946,6 +971,7 @@ public void shouldUpdateAccountRequestsMetricOnlyIfVerbosityIsBasic() {
 
         // then
         assertThat(metricRegistry.counter("account.accountId.requests").getCount()).isOne();
+        assertThat(metricRegistry.counter("account.accountId.debug_requests").getCount()).isZero();
         assertThat(metricRegistry.counter("account.accountId.requests.type.openrtb2-web").getCount()).isZero();
         assertThat(metricRegistry.timer("account.accountId.rubicon.request_time").getCount()).isZero();
         assertThat(metricRegistry.counter("account.accountId.rubicon.requests.nobid").getCount()).isZero();
@@ -1164,6 +1190,13 @@ public void updateHooksMetricsShouldIncrementMetrics() {
                 "module1", Stage.entrypoint, "hook1", ExecutionStatus.success, 5L, ExecutionAction.update);
         metrics.updateHooksMetrics(
                 "module1", Stage.raw_auction_request, "hook2", ExecutionStatus.success, 5L, ExecutionAction.no_action);
+        metrics.updateHooksMetrics(
+                "module1",
+                Stage.raw_auction_request,
+                "hook2",
+                ExecutionStatus.success,
+                5L,
+                ExecutionAction.no_invocation);
         metrics.updateHooksMetrics(
                 "module1",
                 Stage.processed_auction_request,
@@ -1176,10 +1209,13 @@ public void updateHooksMetricsShouldIncrementMetrics() {
         metrics.updateHooksMetrics(
                 "module2", Stage.raw_bidder_response, "hook2", ExecutionStatus.timeout, 7L, null);
         metrics.updateHooksMetrics(
-                "module2", Stage.processed_bidder_response, "hook3", ExecutionStatus.execution_failure, 5L, null);
+                "module2", Stage.all_processed_bid_responses, "hook3", ExecutionStatus.execution_failure, 5L, null);
         metrics.updateHooksMetrics(
                 "module2", Stage.auction_response, "hook4", ExecutionStatus.invocation_failure, 5L, null);
 
+        metrics.updateHooksMetrics(
+                "module1", Stage.exitpoint, "hook5", ExecutionStatus.success, 5L, ExecutionAction.update);
+
         // then
         assertThat(metricRegistry.counter("modules.module.module1.stage.entrypoint.hook.hook1.call")
                 .getCount())
@@ -1194,6 +1230,9 @@ public void updateHooksMetricsShouldIncrementMetrics() {
                 .isEqualTo(1);
         assertThat(metricRegistry.counter("modules.module.module1.stage.rawauction.hook.hook2.success.noop").getCount())
                 .isEqualTo(1);
+        assertThat(metricRegistry.counter("modules.module.module1.stage.rawauction.hook.hook2.success.no-invocation")
+                .getCount())
+                .isEqualTo(1);
         assertThat(metricRegistry.timer("modules.module.module1.stage.rawauction.hook.hook2.duration").getCount())
                 .isEqualTo(1);
 
@@ -1219,12 +1258,14 @@ public void updateHooksMetricsShouldIncrementMetrics() {
         assertThat(metricRegistry.timer("modules.module.module2.stage.rawbidresponse.hook.hook2.duration").getCount())
                 .isEqualTo(1);
 
-        assertThat(metricRegistry.counter("modules.module.module2.stage.procbidresponse.hook.hook3.call").getCount())
+        assertThat(metricRegistry.counter("modules.module.module2.stage.allprocbidresponses.hook.hook3.call")
+                .getCount())
                 .isEqualTo(1);
-        assertThat(metricRegistry.counter("modules.module.module2.stage.procbidresponse.hook.hook3.execution-error")
+        assertThat(metricRegistry.counter("modules.module.module2.stage.allprocbidresponses.hook.hook3.execution-error")
                 .getCount())
                 .isEqualTo(1);
-        assertThat(metricRegistry.timer("modules.module.module2.stage.procbidresponse.hook.hook3.duration").getCount())
+        assertThat(metricRegistry.timer("modules.module.module2.stage.allprocbidresponses.hook.hook3.duration")
+                .getCount())
                 .isEqualTo(1);
 
         assertThat(metricRegistry.counter("modules.module.module2.stage.auctionresponse.hook.hook4.call").getCount())
@@ -1234,6 +1275,15 @@ public void updateHooksMetricsShouldIncrementMetrics() {
                 .isEqualTo(1);
         assertThat(metricRegistry.timer("modules.module.module2.stage.auctionresponse.hook.hook4.duration").getCount())
                 .isEqualTo(1);
+
+        assertThat(metricRegistry.counter("modules.module.module1.stage.exitpoint.hook.hook5.call")
+                .getCount())
+                .isEqualTo(1);
+        assertThat(metricRegistry.counter("modules.module.module1.stage.exitpoint.hook.hook5.success.update")
+                .getCount())
+                .isEqualTo(1);
+        assertThat(metricRegistry.timer("modules.module.module1.stage.exitpoint.hook.hook5.duration").getCount())
+                .isEqualTo(1);
     }
 
     @Test
@@ -1248,6 +1298,8 @@ public void updateAccountHooksMetricsShouldIncrementMetricsIfVerbosityIsDetailed
                 Account.empty("accountId"), "module2", ExecutionStatus.failure, null);
         metrics.updateAccountHooksMetrics(
                 Account.empty("accountId"), "module3", ExecutionStatus.timeout, null);
+        metrics.updateAccountHooksMetrics(
+                Account.empty("accountId"), "module4", ExecutionStatus.success, ExecutionAction.no_invocation);
 
         // then
         assertThat(metricRegistry.counter("account.accountId.modules.module.module1.call").getCount())
@@ -1264,6 +1316,11 @@ public void updateAccountHooksMetricsShouldIncrementMetricsIfVerbosityIsDetailed
                 .isEqualTo(1);
         assertThat(metricRegistry.counter("account.accountId.modules.module.module3.failure").getCount())
                 .isEqualTo(1);
+
+        assertThat(metricRegistry.counter("account.accountId.modules.module.module4.call").getCount())
+                .isEqualTo(0);
+        assertThat(metricRegistry.counter("account.accountId.modules.module.module4.success.no-invocation").getCount())
+                .isEqualTo(1);
     }
 
     @Test
diff --git a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java
index d09df3327a8..1767491959a 100644
--- a/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java
+++ b/src/test/java/org/prebid/server/settings/CachingApplicationSettingsTest.java
@@ -8,8 +8,8 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.exception.InvalidRequestException;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.metric.MetricName;
 import org.prebid.server.metric.Metrics;
 import org.prebid.server.settings.model.Account;
diff --git a/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java
index bab03ea0bb8..86c72604150 100644
--- a/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java
+++ b/src/test/java/org/prebid/server/settings/DatabaseApplicationSettingsTest.java
@@ -8,8 +8,8 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.VertxTest;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.settings.helper.ParametrizedQueryHelper;
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.StoredDataResult;
diff --git a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java
index aaa72b45756..f3180d3651e 100644
--- a/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java
+++ b/src/test/java/org/prebid/server/settings/EnrichingApplicationSettingsTest.java
@@ -8,7 +8,7 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.VertxTest;
 import org.prebid.server.activity.ActivitiesConfigResolver;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.floors.PriceFloorsConfigResolver;
 import org.prebid.server.json.JsonMerger;
 import org.prebid.server.settings.model.Account;
diff --git a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java
index b2452e06cae..e3076ddbdfd 100644
--- a/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java
+++ b/src/test/java/org/prebid/server/settings/HttpApplicationSettingsTest.java
@@ -10,8 +10,8 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.VertxTest;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.AccountAuctionConfig;
 import org.prebid.server.settings.model.AccountPrivacyConfig;
diff --git a/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java b/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java
index 2f7c293f9f8..a702d71ab2e 100644
--- a/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java
+++ b/src/test/java/org/prebid/server/settings/S3ApplicationSettingsTest.java
@@ -13,7 +13,7 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.prebid.server.VertxTest;
 import org.prebid.server.exception.PreBidException;
-import org.prebid.server.execution.Timeout;
+import org.prebid.server.execution.timeout.Timeout;
 import org.prebid.server.settings.model.Account;
 import org.prebid.server.settings.model.StoredDataResult;
 import org.prebid.server.settings.model.StoredResponseDataResult;
diff --git a/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java b/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java
index d0010e7699c..1e1ffd37271 100644
--- a/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java
+++ b/src/test/java/org/prebid/server/settings/service/DatabasePeriodicRefreshServiceTest.java
@@ -10,7 +10,7 @@
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 import org.mockito.stubbing.Answer;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.metric.MetricName;
 import org.prebid.server.metric.Metrics;
 import org.prebid.server.settings.CacheNotificationListener;
diff --git a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java
index a14c6141355..674c4026a72 100644
--- a/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java
+++ b/src/test/java/org/prebid/server/validation/BidderParamValidatorTest.java
@@ -398,7 +398,8 @@ private static BidderInfo givenBidderInfo(String aliasOf) {
                 true,
                 false,
                 CompressionType.NONE,
-                Ortb.of(false));
+                Ortb.of(false),
+                0L);
     }
 
     private static BidderInfo givenBidderInfo() {
diff --git a/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java b/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java
index 2d6d1db8377..ffb6c9e6804 100644
--- a/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java
+++ b/src/test/java/org/prebid/server/validation/ResponseBidValidatorTest.java
@@ -141,8 +141,8 @@ public void validateShouldFailIfBidHasNoCrid() {
     @Test
     public void validateShouldFailIfBannerBidHasNoWidthAndHeight() {
         // when
-        final ValidationResult result = target.validate(
-                givenBid(builder -> builder.w(null).h(null)), BIDDER_NAME, givenAuctionContext(), bidderAliases);
+        final BidderBid givenBid = givenBid(builder -> builder.w(null).h(null));
+        final ValidationResult result = target.validate(givenBid, BIDDER_NAME, givenAuctionContext(), bidderAliases);
 
         // then
         assertThat(result.getErrors())
@@ -151,14 +151,14 @@ public void validateShouldFailIfBannerBidHasNoWidthAndHeight() {
                         creative size validation for bid bidId1, account=account, referrer=unknown, \
                         max imp size='100x200', bid response size='nullxnull'""");
         verify(bidRejectionTracker)
-                .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED);
+                .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED);
     }
 
     @Test
     public void validateShouldFailIfBannerBidWidthIsGreaterThanImposedByImp() {
         // when
-        final ValidationResult result = target.validate(
-                givenBid(builder -> builder.w(150).h(150)), BIDDER_NAME, givenAuctionContext(), bidderAliases);
+        final BidderBid givenBid = givenBid(builder -> builder.w(150).h(150));
+        final ValidationResult result = target.validate(givenBid, BIDDER_NAME, givenAuctionContext(), bidderAliases);
 
         // then
         assertThat(result.getErrors())
@@ -167,17 +167,14 @@ public void validateShouldFailIfBannerBidWidthIsGreaterThanImposedByImp() {
                         creative size validation for bid bidId1, account=account, referrer=unknown, \
                         max imp size='100x200', bid response size='150x150'""");
         verify(bidRejectionTracker)
-                .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED);
+                .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED);
     }
 
     @Test
     public void validateShouldFailIfBannerBidHeightIsGreaterThanImposedByImp() {
         // when
-        final ValidationResult result = target.validate(
-                givenBid(builder -> builder.w(50).h(250)),
-                BIDDER_NAME,
-                givenAuctionContext(),
-                bidderAliases);
+        final BidderBid givenBid = givenBid(builder -> builder.w(50).h(250));
+        final ValidationResult result = target.validate(givenBid, BIDDER_NAME, givenAuctionContext(), bidderAliases);
 
         // then
         assertThat(result.getErrors())
@@ -186,7 +183,7 @@ public void validateShouldFailIfBannerBidHeightIsGreaterThanImposedByImp() {
                         creative size validation for bid bidId1, account=account, referrer=unknown, \
                         max imp size='100x200', bid response size='50x250'""");
         verify(bidRejectionTracker)
-                .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED);
+                .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED);
     }
 
     @Test
@@ -256,8 +253,9 @@ public void validateShouldFailIfBidHasNoCorrespondingImp() {
     @Test
     public void validateShouldFailIfBidHasInsecureMarkerInCreativeInSecureContext() {
         // when
+        final BidderBid givenBid = givenBid(builder -> builder.adm("<tag>http://site.com/creative.jpg</tag>"));
         final ValidationResult result = target.validate(
-                givenBid(builder -> builder.adm("<tag>http://site.com/creative.jpg</tag>")),
+                givenBid,
                 BIDDER_NAME,
                 givenAuctionContext(givenBidRequest(builder -> builder.secure(1))),
                 bidderAliases);
@@ -269,14 +267,15 @@ public void validateShouldFailIfBidHasInsecureMarkerInCreativeInSecureContext()
                         secure creative validation for bid bidId1, account=account, referrer=unknown, \
                         adm=<tag>http://site.com/creative.jpg</tag>""");
         verify(bidRejectionTracker)
-                .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE);
+                .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE);
     }
 
     @Test
     public void validateShouldFailIfBidHasInsecureEncodedMarkerInCreativeInSecureContext() {
         // when
+        final BidderBid givenBid = givenBid(builder -> builder.adm("<tag>http%3A//site.com/creative.jpg</tag>"));
         final ValidationResult result = target.validate(
-                givenBid(builder -> builder.adm("<tag>http%3A//site.com/creative.jpg</tag>")),
+                givenBid,
                 BIDDER_NAME,
                 givenAuctionContext(givenBidRequest(builder -> builder.secure(1))),
                 bidderAliases);
@@ -288,14 +287,15 @@ public void validateShouldFailIfBidHasInsecureEncodedMarkerInCreativeInSecureCon
                         secure creative validation for bid bidId1, account=account, referrer=unknown, \
                         adm=<tag>http%3A//site.com/creative.jpg</tag>""");
         verify(bidRejectionTracker)
-                .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE);
+                .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE);
     }
 
     @Test
     public void validateShouldFailIfBidHasNoSecureMarkersInCreativeInSecureContext() {
         // when
+        final BidderBid givenBid = givenBid(builder -> builder.adm("<tag>//site.com/creative.jpg</tag>"));
         final ValidationResult result = target.validate(
-                givenBid(builder -> builder.adm("<tag>//site.com/creative.jpg</tag>")),
+                givenBid,
                 BIDDER_NAME,
                 givenAuctionContext(givenBidRequest(builder -> builder.secure(1))),
                 bidderAliases);
@@ -307,7 +307,7 @@ public void validateShouldFailIfBidHasNoSecureMarkersInCreativeInSecureContext()
                         secure creative validation for bid bidId1, account=account, referrer=unknown, \
                         adm=<tag>//site.com/creative.jpg</tag>""");
         verify(bidRejectionTracker)
-                .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE);
+                .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE);
     }
 
     @Test
@@ -405,8 +405,9 @@ public void validateShouldReturnSuccessWithWarningIfBannerSizeEnforcementIsWarn(
         target = new ResponseBidValidator(warn, enforce, metrics, 0.01);
 
         // when
+        final BidderBid givenBid = givenBid(builder -> builder.w(null).h(null));
         final ValidationResult result = target.validate(
-                givenBid(builder -> builder.w(null).h(null)),
+                givenBid,
                 BIDDER_NAME,
                 givenAuctionContext(),
                 bidderAliases);
@@ -418,8 +419,7 @@ public void validateShouldReturnSuccessWithWarningIfBannerSizeEnforcementIsWarn(
                         BidResponse validation `warn`: bidder `bidder` response triggers \
                         creative size validation for bid bidId1, account=account, referrer=unknown, \
                         max imp size='100x200', bid response size='nullxnull'""");
-        verify(bidRejectionTracker)
-                .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED);
+        verifyNoInteractions(bidRejectionTracker);
     }
 
     @Test
@@ -445,8 +445,9 @@ public void validateShouldReturnSuccessWithWarningIfSecureMarkupEnforcementIsWar
         target = new ResponseBidValidator(enforce, warn, metrics, 0.01);
 
         // when
+        final BidderBid givenBid = givenBid(builder -> builder.adm("<tag>http://site.com/creative.jpg</tag>"));
         final ValidationResult result = target.validate(
-                givenBid(builder -> builder.adm("<tag>http://site.com/creative.jpg</tag>")),
+                givenBid,
                 BIDDER_NAME,
                 givenAuctionContext(givenBidRequest(builder -> builder.secure(1))),
                 bidderAliases);
@@ -458,23 +459,19 @@ public void validateShouldReturnSuccessWithWarningIfSecureMarkupEnforcementIsWar
                         BidResponse validation `warn`: bidder `bidder` response triggers \
                         secure creative validation for bid bidId1, account=account, referrer=unknown, \
                         adm=<tag>http://site.com/creative.jpg</tag>""");
-        verify(bidRejectionTracker)
-                .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE);
+        verifyNoInteractions(bidRejectionTracker);
     }
 
     @Test
     public void validateShouldIncrementSizeValidationErrMetrics() {
         // when
-        target.validate(
-                givenBid(builder -> builder.w(150).h(200)),
-                BIDDER_NAME,
-                givenAuctionContext(),
-                bidderAliases);
+        final BidderBid givenBid = givenBid(builder -> builder.w(150).h(200));
+        target.validate(givenBid, BIDDER_NAME, givenAuctionContext(), bidderAliases);
 
         // then
         verify(metrics).updateSizeValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.err);
         verify(bidRejectionTracker)
-                .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED);
+                .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED);
     }
 
     @Test
@@ -483,23 +480,20 @@ public void validateShouldIncrementSizeValidationWarnMetrics() {
         target = new ResponseBidValidator(warn, warn, metrics, 0.01);
 
         // when
-        target.validate(
-                givenBid(builder -> builder.w(150).h(200)),
-                BIDDER_NAME,
-                givenAuctionContext(),
-                bidderAliases);
+        final BidderBid givenBid = givenBid(builder -> builder.w(150).h(200));
+        target.validate(givenBid, BIDDER_NAME, givenAuctionContext(), bidderAliases);
 
         // then
         verify(metrics).updateSizeValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.warn);
-        verify(bidRejectionTracker)
-                .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_SIZE_NOT_ALLOWED);
+        verifyNoInteractions(bidRejectionTracker);
     }
 
     @Test
     public void validateShouldIncrementSecureValidationErrMetrics() {
         // when
+        final BidderBid givenBid = givenBid(builder -> builder.adm("<tag>http://site.com/creative.jpg</tag>"));
         target.validate(
-                givenBid(builder -> builder.adm("<tag>http://site.com/creative.jpg</tag>")),
+                givenBid,
                 BIDDER_NAME,
                 givenAuctionContext(givenBidRequest(builder -> builder.secure(1))),
                 bidderAliases);
@@ -507,7 +501,7 @@ public void validateShouldIncrementSecureValidationErrMetrics() {
         // then
         verify(metrics).updateSecureValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.err);
         verify(bidRejectionTracker)
-                .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE);
+                .rejectBid(givenBid, BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE);
     }
 
     @Test
@@ -516,16 +510,16 @@ public void validateShouldIncrementSecureValidationWarnMetrics() {
         target = new ResponseBidValidator(warn, warn, metrics, 0.01);
 
         // when
+        final BidderBid givenBid = givenBid(builder -> builder.adm("<tag>http://site.com/creative.jpg</tag>"));
         target.validate(
-                givenBid(builder -> builder.adm("<tag>http://site.com/creative.jpg</tag>")),
+                givenBid,
                 BIDDER_NAME,
                 givenAuctionContext(givenBidRequest(builder -> builder.secure(1))),
                 bidderAliases);
 
         // then
         verify(metrics).updateSecureValidationMetrics(BIDDER_NAME, ACCOUNT_ID, MetricName.warn);
-        verify(bidRejectionTracker)
-                .reject("impId1", BidRejectionReason.RESPONSE_REJECTED_INVALID_CREATIVE_NOT_SECURE);
+        verifyNoInteractions(bidRejectionTracker);
     }
 
     private BidRequest givenRequest(UnaryOperator<Imp.ImpBuilder> impCustomizer) {
diff --git a/src/test/java/org/prebid/server/vertx/database/BasicDatabaseClientTest.java b/src/test/java/org/prebid/server/vertx/database/BasicDatabaseClientTest.java
index e182255df87..15c76f3b6c7 100644
--- a/src/test/java/org/prebid/server/vertx/database/BasicDatabaseClientTest.java
+++ b/src/test/java/org/prebid/server/vertx/database/BasicDatabaseClientTest.java
@@ -14,8 +14,8 @@
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.metric.Metrics;
 
 import java.time.Clock;
diff --git a/src/test/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClientTest.java b/src/test/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClientTest.java
index fe6bc6c337b..abc2d8af479 100644
--- a/src/test/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClientTest.java
+++ b/src/test/java/org/prebid/server/vertx/database/CircuitBreakerSecuredDatabaseClientTest.java
@@ -14,8 +14,8 @@
 import org.mockito.BDDMockito;
 import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
-import org.prebid.server.execution.Timeout;
-import org.prebid.server.execution.TimeoutFactory;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.execution.timeout.TimeoutFactory;
 import org.prebid.server.metric.Metrics;
 
 import java.time.Clock;
diff --git a/src/test/resources/org/prebid/server/it/amp/test-cache-request.json b/src/test/resources/org/prebid/server/it/amp/test-cache-request.json
index 4908b67e9c1..fe8eba5c934 100644
--- a/src/test/resources/org/prebid/server/it/amp/test-cache-request.json
+++ b/src/test/resources/org/prebid/server/it/amp/test-cache-request.json
@@ -27,7 +27,8 @@
           "origbidcpm": 12.09
         }
       },
-      "aid":"tid"
+      "aid":"tid",
+      "ttlseconds": 300
     },
     {
       "type": "json",
@@ -60,7 +61,8 @@
           "origbidcur": "USD"
         }
       },
-      "aid":"tid"
+      "aid":"tid",
+      "ttlseconds": 300
     }
   ]
 }
diff --git a/src/test/resources/org/prebid/server/it/amp/test-generic-bid-request.json b/src/test/resources/org/prebid/server/it/amp/test-generic-bid-request.json
index 5cc33c6206c..4d45a82bbc0 100644
--- a/src/test/resources/org/prebid/server/it/amp/test-generic-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/amp/test-generic-bid-request.json
@@ -51,6 +51,9 @@
     "ext": {
       "ConsentedProvidersSettings": {
         "consented_providers": "someConsent"
+      },
+      "consented_providers_settings": {
+        "consented_providers": "someConsent"
       }
     }
   },
diff --git a/src/test/resources/org/prebid/server/it/amp/test-genericAlias-bid-request.json b/src/test/resources/org/prebid/server/it/amp/test-genericAlias-bid-request.json
index 1d28937fef6..65febdb9a16 100644
--- a/src/test/resources/org/prebid/server/it/amp/test-genericAlias-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/amp/test-genericAlias-bid-request.json
@@ -49,6 +49,9 @@
     "ext": {
       "ConsentedProvidersSettings": {
         "consented_providers": "someConsent"
+      },
+      "consented_providers_settings": {
+        "consented_providers": "someConsent"
       }
     }
   },
diff --git a/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json b/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json
index 9d49c702f5e..e6127bea4f0 100644
--- a/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/cache/update/test-auction-response.json
@@ -11,6 +11,7 @@
           "crid": "crid2",
           "w": 120,
           "h": 600,
+          "exp": 300,
           "ext": {
             "prebid": {
               "type": "banner",
@@ -35,6 +36,7 @@
         {
           "id": "31124",
           "impid": "impId-video-cache-update",
+          "exp": 1500,
           "price": 3,
           "adm": "adm1",
           "crid": "crid1",
diff --git a/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json
index 0daecc354d4..3743bed491b 100644
--- a/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json
+++ b/src/test/resources/org/prebid/server/it/hooks/reject/test-rubicon-bid-request-1.json
@@ -37,7 +37,8 @@
             "mint_version": ""
           }
         },
-        "maxbids": 1
+        "maxbids": 1,
+        "tid": "${json-unit.any-string}"
       }
     }
   ],
diff --git a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json
index eacb079d5fe..cc893e56246 100644
--- a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json
+++ b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-auction-sample-module-response.json
@@ -7,7 +7,7 @@
           "id": "880290288",
           "impid": "impId1",
           "price": 8.43,
-          "adm": "<Impression><![CDATA[]]></Impression><Impression><![CDATA[Raw bidder response hook have been here]]></Impression><Impression><![CDATA[Processed bidder response hook have been here]]></Impression><Impression><![CDATA[Auction response hook have been here too]]></Impression>",
+          "adm": "<Impression><![CDATA[]]></Impression><Impression><![CDATA[Raw bidder response hook have been here]]></Impression><Impression><![CDATA[Processed bidder response hook have been here]]></Impression><Impression><![CDATA[Auction response hook have been here too]]></Impression><Impression><![CDATA[Exitpoint hook have been here]]></Impression>",
           "crid": "crid1",
           "w": 300,
           "h": 250,
diff --git a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json
index 14d752bc96d..e9a871c9f77 100644
--- a/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json
+++ b/src/test/resources/org/prebid/server/it/hooks/sample-module/test-rubicon-bid-request-1.json
@@ -38,7 +38,8 @@
             "mint_version": ""
           }
         },
-        "maxbids": 1
+        "maxbids": 1,
+        "tid": "${json-unit.any-string}"
       }
     }
   ],
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/33across/test-auction-33across-response.json b/src/test/resources/org/prebid/server/it/openrtb2/33across/test-auction-33across-response.json
index f086c053112..b7a0ac4311d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/33across/test-auction-33across-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/33across/test-auction-33across-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json
index c223e8f56d3..f6f8aa08087 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/aax/test-auction-aax-response.json
@@ -6,6 +6,7 @@
         {
           "id": "randomid",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.5,
           "adm": "some-test-ad",
           "adid": "12345678",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aceex/test-auction-aceex-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aceex/test-auction-aceex-response.json
index f80400fe5d1..b9b61a696c0 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/aceex/test-auction-aceex-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/aceex/test-auction-aceex-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json
index ba9ea7db56d..a0bab7d6cc2 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/acuityads/test-auction-acuityads-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adelement/test-auction-adelement-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adelement/test-auction-adelement-response.json
index ee0b96cb442..0c48f3a8431 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adelement/test-auction-adelement-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adelement/test-auction-adelement-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "some-test-ad",
           "adid": "12345678",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json
index 320108794b5..f4417095fa3 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adf/test-auction-adf-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price":  11.393,
           "adomain": [
           ],
@@ -22,6 +23,7 @@
         {
           "id": "bid_id_banner",
           "impid": "imp_id_banner",
+          "exp": 300,
           "price":  11.393,
           "adomain": [],
           "adm": "<html>",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-response.json
index 05c116d4aa2..25ebe533dcc 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adgeneration/test-auction-adgeneration-response.json
@@ -6,6 +6,7 @@
         {
           "id": "id",
           "impid": "id",
+          "exp": 300,
           "price": 46.6,
           "adm": "",
           "crid": "Dummy_supership.jp",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json
index 9895195c325..f5fd5214de2 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adhese/test-auction-adhese-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 2.184,
           "adm": "<script>console.log(\"Hello prebid server!\");</script><div style=\"position: absolute; left: 0px; top: 0px; visibility: hidden;\"><img src=\"https://ads-demo.adhese.com/track/2291//sl178/dtunknown/ogcontrol/II1927b7b0-0f50-48b9-a906-c1dd15e93568/tlall/A2127.68.78.84/?t=1687263510533\" border=\"0\" width=\"1\" height=\"1\" alt=\"\" style=\"display:none\"></div>",
           "crid": "demo-424",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json
index 92b00eb8c2b..a6a8e2bd0c8 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adkernel/test-auction-adkernel-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 2.25,
           "adm": "<!-- admarkup -->",
           "adid": "2002",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json
index 9f868cc7baa..53df688de45 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-adkerneladn-bid-response.json
@@ -24,4 +24,4 @@
     }
   ],
   "bidid": "bid_id"
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json
index f1d38a7780f..9563c1ff672 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adkerneladn/test-auction-adkerneladn-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "adm021",
           "adid": "19005",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json
index 8d448c61116..8884760961e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adman/test-auction-adman-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id1",
           "impid": "imp_id1",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admatic/test-auction-admatic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/admatic/test-auction-admatic-response.json
index 6ad0d2f637f..0277bb7f78d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/admatic/test-auction-admatic-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/admatic/test-auction-admatic-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json
index 5561b33da3b..aceadcc04ac 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-admixer-bid-response.json
@@ -17,4 +17,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json
index 4352d750af3..75f33a522f6 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/admixer/test-auction-admixer-response.json
@@ -16,6 +16,7 @@
           },
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01
         }
       ],
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json
index beffca0d359..61bac864a49 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adnuntius/test-auction-adnuntius-response.json
@@ -6,6 +6,7 @@
         {
           "id": "some_ad_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 42420.00,
           "adm": "some_html",
           "adid": "some_ad_id",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json
index 76c4005d49c..3f62c1fb7db 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adocean/test-auction-adocean-response.json
@@ -6,6 +6,7 @@
         {
           "id": "adoceanmyaozpniqismex",
           "impid": "imp_id",
+          "exp": 300,
           "price": 10,
           "adm": " <script> +function() {\nvar wu = \"https://win-url.com\";\nvar su = \"https://stats-url.com\".replace(/\\[TIMESTAMP\\]/, Date.now());\nif (wu && !(navigator.sendBeacon && navigator.sendBeacon(wu))) { (new Image(1,1)).src = wu }\nif (su && !(navigator.sendBeacon && navigator.sendBeacon(su))) { (new Image(1,1)).src = su } }();\n</script> <!-- code 1 --> ",
           "crid": "0af345b42983cc4bc0",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json
index ba80a545eed..4edc56ade74 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-adoppler-bid-response-1.json
@@ -26,4 +26,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json
index db0b1aeaa49..7822b00cdbb 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adoppler/test-auction-adoppler-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json
index dcff8f22c64..b2c52bb7d9e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adot/test-auction-adot-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.16346,
           "adm": "some-test-ad",
           "crid": "crid001",
@@ -36,4 +37,4 @@
       "auctiontimestamp": 1626182712962
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json
index 7f09ff7886b..1682aad2c48 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-adpone-bid-response.json
@@ -17,4 +17,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json
index e24dfce9f48..3c4fc34a311 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adpone/test-auction-adpone-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 6.66,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json
index 5b7e94562fd..073a812bcdb 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adprime/test-auction-adprime-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adquery/test-auction-adquery-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adquery/test-auction-adquery-response.json
index 0d05040052c..1d301f75624 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adquery/test-auction-adquery-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adquery/test-auction-adquery-response.json
@@ -6,6 +6,7 @@
         {
           "id": "22e26bd9a702bc1",
           "impid": "22e26bd9a702bc",
+          "exp": 300,
           "price": 1.090,
           "adm": "<script src=\"AdqLib_Example\"></script>Tag_Example",
           "adomain": [
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adrino/test-auction-adrino-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adrino/test-auction-adrino-response.json
index d480aae971a..e1341dbdcca 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adrino/test-auction-adrino-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adrino/test-auction-adrino-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adid": "adid001",
           "cid": "cid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adsyield/test-auction-adsyield-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adsyield/test-auction-adsyield-response.json
index b9d85d4e632..855d418643f 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adsyield/test-auction-adsyield-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adsyield/test-auction-adsyield-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json
index 03c5ee91218..d5b04833e91 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-adtarget-bid-response-1.json
@@ -17,4 +17,4 @@
       "group": 0
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json
index 23465125a4e..809b063e228 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adtarget/test-auction-adtarget-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 8.43,
           "adm": "adm14",
           "crid": "crid14",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json
index 15d06b7c923..1da8f18279d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-adtelligent-bid-response.json
@@ -17,4 +17,4 @@
       "group": 0
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json
index 458b300cb66..b73512ad65c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adtelligent/test-auction-adtelligent-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 8.43,
           "adm": "adm14",
           "crid": "crid14",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json
index e6795976a7f..c5bfdb6d592 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adtonos/test-auction-adtonos-response.json
@@ -7,6 +7,7 @@
           "id": "bid_id",
           "mtype": 1,
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adtrgtme/test-auction-adtrgtme-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adtrgtme/test-auction-adtrgtme-response.json
index 60c2ad42bab..d786080f717 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adtrgtme/test-auction-adtrgtme-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adtrgtme/test-auction-adtrgtme-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "h": 250,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json
index df8ec148aa1..92ba70c5c42 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/advangelists/test-auction-advangelists-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adview/test-auction-adview-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adview/test-auction-adview-response.json
index eccc7f38dec..a6c118e0913 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adview/test-auction-adview-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adview/test-auction-adview-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json
index 81b8aa40e9e..0961b1f67ec 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adxcg/test-auction-adxcg-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json
index e291739474c..a4c0edc3e09 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-adyoulike-bid-response.json
@@ -17,4 +17,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json
index ec08af30179..96aa18de5d8 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/adyoulike/test-auction-adyoulike-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aidem/test-auction-aidem-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aidem/test-auction-aidem-response.json
index 1dff571757c..f5ee4e08e9c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/aidem/test-auction-aidem-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/aidem/test-auction-aidem-response.json
@@ -7,6 +7,7 @@
           "id": "bid_id",
           "mtype": 1,
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json
index d2f3908c4b3..413a3ffe241 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-aja-bid-response.json
@@ -17,4 +17,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json
index 5b8ce2dfb32..7010f75f5e6 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/aja/test-auction-aja-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 10,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json
index e291739474c..a4c0edc3e09 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-algorix-bid-response.json
@@ -17,4 +17,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json
index f3b649ebbec..b61aebbccd6 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/algorix/test-auction-algorix-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/alkimi/test-auction-alkimi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/alkimi/test-auction-alkimi-response.json
index ca0b59e06b3..b8ccdb2d3b3 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/alkimi/test-auction-alkimi-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/alkimi/test-auction-alkimi-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json
index dc3186c5778..ec393ab7ca0 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/amx/test-auction-amx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/apacdex/test-auction-apacdex-response.json b/src/test/resources/org/prebid/server/it/openrtb2/apacdex/test-auction-apacdex-response.json
index e9c1f602280..2122bfc623a 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/apacdex/test-auction-apacdex-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/apacdex/test-auction-apacdex-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/appnexus/test-video-cache-request.json b/src/test/resources/org/prebid/server/it/openrtb2/appnexus/test-video-cache-request.json
index da99c54e188..15ae12d04c5 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/appnexus/test-video-cache-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/appnexus/test-video-cache-request.json
@@ -4,19 +4,22 @@
       "type": "xml",
       "value": "some-test-ad-3",
       "aid": "bid_id",
-      "key": "2.0_IAB10-1_0s_{{uuid}}"
+      "key": "2.0_IAB10-1_0s_{{uuid}}",
+      "ttlseconds": 1500
     },
     {
       "type": "xml",
       "value": "some-test-ad",
       "aid": "bid_id",
-      "key": "5.5_IAB20-3_0s_{{uuid}}"
+      "key": "5.5_IAB20-3_0s_{{uuid}}",
+      "ttlseconds": 1500
     },
     {
       "type": "xml",
       "value": "some-test-ad-2",
       "aid": "bid_id",
-      "key": "2.5_IAB18-5_0s_{{uuid}}"
+      "key": "2.5_IAB18-5_0s_{{uuid}}",
+      "ttlseconds": 1500
     }
   ]
 }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/appush/test-auction-appush-response.json b/src/test/resources/org/prebid/server/it/openrtb2/appush/test-auction-appush-response.json
index 4a71755b12d..f673f770ad2 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/appush/test-auction-appush-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/appush/test-auction-appush-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/aso/test-auction-aso-response.json b/src/test/resources/org/prebid/server/it/openrtb2/aso/test-auction-aso-response.json
index cef76b6cec9..6b9a19e7187 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/aso/test-auction-aso-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/aso/test-auction-aso-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 4.7,
           "adm": "adm6_4.7",
           "nurl": "nurl_4.7",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/audiencenetwork/test-auction-audiencenetwork-response.json b/src/test/resources/org/prebid/server/it/openrtb2/audiencenetwork/test-auction-audiencenetwork-response.json
index 376d88dbf44..e1d18a2ecb5 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/audiencenetwork/test-auction-audiencenetwork-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/audiencenetwork/test-auction-audiencenetwork-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 9.0,
           "adm": "{\"bid_id\":\"10\"}",
           "adid": "10",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/automatad/test-auction-automatad-response.json b/src/test/resources/org/prebid/server/it/openrtb2/automatad/test-auction-automatad-response.json
index 64383cd93de..c4c971e466e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/automatad/test-auction-automatad-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/automatad/test-auction-automatad-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json
index cc260a44b94..6fca036d997 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/avocet/test-auction-avocet-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 0.5,
           "adm": "some-test-ad",
           "adid": "29681110",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/axis/test-auction-axis-response.json b/src/test/resources/org/prebid/server/it/openrtb2/axis/test-auction-axis-response.json
index 37c3691752c..676eb7d802a 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/axis/test-auction-axis-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/axis/test-auction-axis-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id1",
           "impid": "imp_id1",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json
index 31a15adc1f9..e0e02fc7381 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/axonix/test-auction-axonix-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bcmint/test-auction-bcmint-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bcmint/test-auction-bcmint-response.json
index 1ca1cb7607c..c591ef97cfd 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bcmint/test-auction-bcmint-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bcmint/test-auction-bcmint-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 4.7,
           "adm": "adm6_4.7",
           "nurl": "nurl_4.7",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json
index 5338cc8c4d4..5e10a21b5de 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/beachfront/test-auction-beachfront-response.json
@@ -6,6 +6,7 @@
         {
           "id": "imp_idBanner",
           "impid": "imp_id",
+          "exp": 300,
           "price": 2.942807912826538,
           "adm": "<div id=\"44861168\"><script>!function(){console.log\"Hello, ad.\";}();</script></div>",
           "crid": "crid_3",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json
index 9419a85783d..bed5b0e3939 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/beintoo/test-auction-beintoo-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "bid_id",
+          "exp": 300,
           "price": 2.942808,
           "adid": "94395500",
           "crid": "94395500",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bematterfull/test-auction-bematterfull-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bematterfull/test-auction-bematterfull-response.json
index 4b0e7de3f44..1d2bac9f898 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bematterfull/test-auction-bematterfull-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bematterfull/test-auction-bematterfull-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 4.7,
           "adm": "adm6",
           "crid": "crid6",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/between/test-auction-between-response.json b/src/test/resources/org/prebid/server/it/openrtb2/between/test-auction-between-response.json
index ed208dd5654..cf2a6e0d031 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/between/test-auction-between-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/between/test-auction-between-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/beyondmedia/test-auction-beyondmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/beyondmedia/test-auction-beyondmedia-response.json
index 605deba4cb1..e63089babb2 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/beyondmedia/test-auction-beyondmedia-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/beyondmedia/test-auction-beyondmedia-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidagency/test-auction-bidagency-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidagency/test-auction-bidagency-response.json
index 80cfe99af9e..fdba12487c7 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bidagency/test-auction-bidagency-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bidagency/test-auction-bidagency-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 4.7,
           "adm": "adm6_4.7",
           "nurl": "nurl_4.7",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmachine/test-auction-bidmachine-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmachine/test-auction-bidmachine-response.json
index eb4e503494f..0ea56280b80 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bidmachine/test-auction-bidmachine-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmachine/test-auction-bidmachine-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json
index a45f9eeb3c9..ba0b73cfaf1 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmatic/test-auction-bidmatic-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 8.43,
           "adm": "adm14",
           "crid": "crid14",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidmyadz/test-auction-bidmyadz-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidmyadz/test-auction-bidmyadz-response.json
index ba657da0438..399a568d088 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bidmyadz/test-auction-bidmyadz-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bidmyadz/test-auction-bidmyadz-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidscube/test-auction-bidscube-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidscube/test-auction-bidscube-response.json
index 8cb8a61c009..8c8b0128e98 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bidscube/test-auction-bidscube-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bidscube/test-auction-bidscube-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bidstack/test-auction-bidstack-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bidstack/test-auction-bidstack-response.json
index 523bdbbcda4..9428b19fd0a 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bidstack/test-auction-bidstack-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bidstack/test-auction-bidstack-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 3.33,
           "adid": "adid001",
           "adm": "<VAST version=\"3.0\"></VAST>",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bigoad/test-auction-bigoad-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bigoad/test-auction-bigoad-response.json
index 287c05c61aa..013cd170712 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bigoad/test-auction-bigoad-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bigoad/test-auction-bigoad-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "mtype": 1,
           "price": 3.33,
           "adm": "adm001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json
index 9bf200e6d9d..22a67229971 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/blasto/test-auction-blasto-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bliink/test-auction-bliink-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bliink/test-auction-bliink-response.json
index de26e4363ea..f4ddf9222af 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bliink/test-auction-bliink-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bliink/test-auction-bliink-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bluesea/test-auction-bluesea-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bluesea/test-auction-bluesea-response.json
index 939897d89a1..71412aa8e6c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bluesea/test-auction-bluesea-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bluesea/test-auction-bluesea-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bmtm/test-auction-bmtm-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bmtm/test-auction-bmtm-response.json
index 1d36233fd2d..cbbae431606 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bmtm/test-auction-bmtm-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bmtm/test-auction-bmtm-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/boldwin/test-auction-boldwin-response.json b/src/test/resources/org/prebid/server/it/openrtb2/boldwin/test-auction-boldwin-response.json
index e9242e76699..4d5d8f5ce9e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/boldwin/test-auction-boldwin-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/boldwin/test-auction-boldwin-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/brave/test-auction-brave-response.json b/src/test/resources/org/prebid/server/it/openrtb2/brave/test-auction-brave-response.json
index 39819c16e18..43de8398d78 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/brave/test-auction-brave-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/brave/test-auction-brave-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "adid",
           "cid": "cid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/bwx/test-auction-bwx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/bwx/test-auction-bwx-response.json
index 6029a55596f..6107fe586a3 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/bwx/test-auction-bwx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/bwx/test-auction-bwx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "mtype": 1,
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/cadentaperturemx/test-auction-cadentaperturemx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/cadentaperturemx/test-auction-cadentaperturemx-response.json
index c5b97cc0914..f2f43ba2c1d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/cadentaperturemx/test-auction-cadentaperturemx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/cadentaperturemx/test-auction-cadentaperturemx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "imp_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 2.942808,
           "adm": "<div id=\"123456789_ad\"><script>!function(){console.log\"Hello, world.\";}();</script></div>",
           "adid": "94395500",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ccx/test-auction-ccx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ccx/test-auction-ccx-response.json
index 28eb0bc9e9d..7fcafb472c7 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/ccx/test-auction-ccx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/ccx/test-auction-ccx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/cointraffic/test-auction-cointraffic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/cointraffic/test-auction-cointraffic-response.json
index 4837ee79517..06fadbf0b09 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/cointraffic/test-auction-cointraffic-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/cointraffic/test-auction-cointraffic-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/coinzilla/test-auction-coinzilla-response.json b/src/test/resources/org/prebid/server/it/openrtb2/coinzilla/test-auction-coinzilla-response.json
index cbad770f0e5..9f59c942b28 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/coinzilla/test-auction-coinzilla-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/coinzilla/test-auction-coinzilla-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 6.66,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json
index 3491c77189e..f551e12bbe4 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/colossus/aliases/test-auction-colossusssp-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-response.json b/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-response.json
index c4d521102b5..d7914b5fb58 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/colossus/test-auction-colossus-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/compass/test-auction-compass-response.json b/src/test/resources/org/prebid/server/it/openrtb2/compass/test-auction-compass-response.json
index 0da511e197e..20f86d6a348 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/compass/test-auction-compass-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/compass/test-auction-compass-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/concert/test-auction-concert-response.json b/src/test/resources/org/prebid/server/it/openrtb2/concert/test-auction-concert-response.json
index aadc8da3482..666a177b9c1 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/concert/test-auction-concert-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/concert/test-auction-concert-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "mtype": 1,
           "price": 3.33,
           "adm": "adm001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-response.json b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-response.json
index 9aa7d077c16..d24913feeef 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/connectad/test-auction-connectad-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adm": "<b>hi</b>",
           "cid": "test_cid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-response.json b/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-response.json
index 1290f6775fa..a28d27cf14a 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/consumable/test-auction-consumable-response.json
@@ -7,6 +7,7 @@
         {
           "id": "8ee514f1-b2b8-4abb-89fd-084437d1e800",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.500000,
           "adm": "some-test-ad",
           "crid": "crid_10",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6/test-auction-copper6-response.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6/test-auction-copper6-response.json
index 464f7df6b6a..3ac3ccb1672 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/copper6/test-auction-copper6-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6/test-auction-copper6-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 8.43,
           "adm": "adm14",
           "crid": "crid14",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json
index fb24eb9368c..52b36682c8a 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/copper6ssp/test-auction-copper6ssp-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-response.json b/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-response.json
index 9444279b4a6..6a685a06ba4 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/cpmstar/test-auction-cpmstar-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-auction-criteo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-auction-criteo-response.json
index fb1c7803346..9d7e6b1ca5b 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-auction-criteo-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/criteo/test-auction-criteo-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json
index 7ce6a9dd1ae..a73407d0bb8 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/datablocks/test-auction-datablocks-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 7.77,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/decenterads/test-auction-decenterads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/decenterads/test-auction-decenterads-response.json
index dbbff620bf4..6b058b84507 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/decenterads/test-auction-decenterads-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/decenterads/test-auction-decenterads-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-response.json b/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-response.json
index 8f1dfac20f0..223095836df 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/deepintent/test-auction-deepintent-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/definemedia/test-auction-definemedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/definemedia/test-auction-definemedia-response.json
index d24a92228d1..34d7cd3fbf2 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/definemedia/test-auction-definemedia-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/definemedia/test-auction-definemedia-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price":  11.393,
           "adomain": [
           ],
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/dianomi/test-auction-dianomi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/dianomi/test-auction-dianomi-response.json
index 40de103c524..0725b710372 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/dianomi/test-auction-dianomi-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/dianomi/test-auction-dianomi-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id_banner",
           "impid": "imp_id_banner",
+          "exp": 300,
           "price":  11.393,
           "adomain": [],
           "adm": "<html>",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/displayio/test-auction-displayio-response.json b/src/test/resources/org/prebid/server/it/openrtb2/displayio/test-auction-displayio-response.json
index 8c5b4ea599e..ad84acca12c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/displayio/test-auction-displayio-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/displayio/test-auction-displayio-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-response.json
index c34b9f61946..e7b10ebdda4 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/dmx/test-auction-dmx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "cid": "test_cid",
           "crid": "test_banner_crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/driftpixel/test-auction-driftpixel-response.json b/src/test/resources/org/prebid/server/it/openrtb2/driftpixel/test-auction-driftpixel-response.json
index 81da7b92978..d28852c4ccd 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/driftpixel/test-auction-driftpixel-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/driftpixel/test-auction-driftpixel-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/dxkulture/test-auction-dxkulture-response.json b/src/test/resources/org/prebid/server/it/openrtb2/dxkulture/test-auction-dxkulture-response.json
index 0c6bd826b02..b2442775de6 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/dxkulture/test-auction-dxkulture-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/dxkulture/test-auction-dxkulture-response.json
@@ -7,6 +7,7 @@
           "id": "bid_id",
           "mtype": 1,
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/edge226/test-auction-edge226-response.json b/src/test/resources/org/prebid/server/it/openrtb2/edge226/test-auction-edge226-response.json
index ce119ca7efd..2b9d8f83ca4 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/edge226/test-auction-edge226-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/edge226/test-auction-edge226-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/embimedia/test-auction-embimedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/embimedia/test-auction-embimedia-response.json
index 5de88ba03df..930aa7c8b06 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/embimedia/test-auction-embimedia-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/embimedia/test-auction-embimedia-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emtv/test-auction-emtv-response.json b/src/test/resources/org/prebid/server/it/openrtb2/emtv/test-auction-emtv-response.json
index c71dd2560c2..3795c5a9c62 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/emtv/test-auction-emtv-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/emtv/test-auction-emtv-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json
index 6d2e3cf823c..552b0f3a934 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/emxdigital/test-auction-emxdigital-response.json
@@ -6,6 +6,7 @@
         {
           "id": "imp_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 2.942808,
           "adm": "<div id=\"123456789_ad\"><script>!function(){console.log\"Hello, world.\";}();</script></div>",
           "adid": "94395500",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-response.json b/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-response.json
index b3b8e50e406..ce65aba9a9b 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/eplanning/test-auction-eplanning-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "<div>test</div>",
           "adid": "imp_id",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/epom/test-auction-epom-response.json b/src/test/resources/org/prebid/server/it/openrtb2/epom/test-auction-epom-response.json
index 16a9acaefd1..f10ab8e286e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/epom/test-auction-epom-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/epom/test-auction-epom-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/epsilon/alias/test-auction-epsilon-response.json b/src/test/resources/org/prebid/server/it/openrtb2/epsilon/alias/test-auction-epsilon-response.json
index aadd13302aa..3dd393badc9 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/epsilon/alias/test-auction-epsilon-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/epsilon/alias/test-auction-epsilon-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 5.0,
           "adm": "adm4",
           "crid": "crid4",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/epsilon/test-auction-epsilon-response.json b/src/test/resources/org/prebid/server/it/openrtb2/epsilon/test-auction-epsilon-response.json
index 8cb45ddcbac..4aa8c6d0985 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/epsilon/test-auction-epsilon-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/epsilon/test-auction-epsilon-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 6.0,
           "adm": "adm4",
           "crid": "crid4",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json
index 0aa7a90e2d4..7f2babf609e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/escalax/test-auction-escalax-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/evolution/test-auction-evolution-response.json b/src/test/resources/org/prebid/server/it/openrtb2/evolution/test-auction-evolution-response.json
index 1d694702c90..d0043247edf 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/evolution/test-auction-evolution-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/evolution/test-auction-evolution-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json
index ae5c74865aa..0b63bd03f57 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/felixads/test-auction-felixads-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json
index 4a1aac1a8c5..e2c3508d1ab 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/filmzie/test-auction-filmzie-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/finative/test-auction-finative-response.json b/src/test/resources/org/prebid/server/it/openrtb2/finative/test-auction-finative-response.json
index 4246d1a5016..e055b56bb65 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/finative/test-auction-finative-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/finative/test-auction-finative-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 11.393,
           "adm": "some adm price 10",
           "adomain": [
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/flipp/test-auction-flipp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/flipp/test-auction-flipp-response.json
index a6d946f403d..a79558cbd9f 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/flipp/test-auction-flipp-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/flipp/test-auction-flipp-response.json
@@ -6,11 +6,12 @@
         {
           "id": "183599115",
           "impid": "imp_id",
+          "exp": 300,
           "price": 12.34,
           "adm": "creativeContent",
           "crid": "81325690",
           "w": 300,
-          "h": 0,
+          "h": 600,
           "ext": {
             "origbidcpm": 12.34,
             "origbidcur": "USD",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-auction-freewheelssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-auction-freewheelssp-response.json
index 834a4af5e9d..d583e32cc4f 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-auction-freewheelssp-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/freewheelssp/test-auction-freewheelssp-response.json
@@ -6,6 +6,7 @@
         {
           "id": "12345_freewheelssp-test_1",
           "impid": "imp-1",
+          "exp": 1500,
           "price": 1.0,
           "adid": "7857",
           "adm": "<?xml version='1.0' encoding='UTF-8'?><VAST version='2.0'></VAST>",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/frvradn/test-auction-frvradn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/frvradn/test-auction-frvradn-response.json
index fb69a968abd..683c42863d0 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/frvradn/test-auction-frvradn-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/frvradn/test-auction-frvradn-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-response.json b/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-response.json
index ca56cb19b7e..ce0e41775aa 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/gamma/test-auction-gamma-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "some-test-ad",
           "adid": "29681110",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-response.json
index c88d409c2c2..4d6ea3f9060 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/gamoshi/test-auction-gamoshi-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json
index 808b06e512e..8f2d2e4407c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/generic/test-auction-generic-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json
index a0d9014043a..7dc036ee1d4 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-request.json
@@ -8,7 +8,8 @@
           "mimes"
         ],
         "w": 300,
-        "h": 250
+        "h": 250,
+        "placement": 1
       },
       "ext": {
         "prebid": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json
index 552408995c8..4ee1ff6a6c8 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-auction-generic-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid001",
           "impid": "impId001",
+          "exp": 1500,
           "price": 2.997,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json
index 1b5c1802325..a6d65dfcae0 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-cache-generic-request.json
@@ -36,7 +36,8 @@
           "origbidcpm": 3.33
         }
       },
-      "aid": "tid"
+      "aid": "tid",
+      "ttlseconds" : 1500
     }
   ]
 }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json
index d1ae2f0fce0..754ed9ddfff 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/generic_core_functionality/test-generic-bid-request.json
@@ -3,6 +3,7 @@
   "imp" : [ {
     "id": "impId001",
     "video": {
+      "placement": 1,
       "mimes": [
         "mimes"
       ],
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/globalsun/test-auction-globalsun-response.json b/src/test/resources/org/prebid/server/it/openrtb2/globalsun/test-auction-globalsun-response.json
index 7ab5cf7f347..505a3ca4b5c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/globalsun/test-auction-globalsun-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/globalsun/test-auction-globalsun-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gothamads/test-auction-gothamads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/gothamads/test-auction-gothamads-response.json
index 4554400f4b1..728dccb2b13 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/gothamads/test-auction-gothamads-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/gothamads/test-auction-gothamads-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/greedygame/test-auction-greedygame-response.json b/src/test/resources/org/prebid/server/it/openrtb2/greedygame/test-auction-greedygame-response.json
index df8a43a904f..7d19f2a3f5c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/greedygame/test-auction-greedygame-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/greedygame/test-auction-greedygame-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-response.json
index a71711d597d..f6a034ca8a3 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/grid/test-auction-grid-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json
index 9c2842a59b6..95c6f56f2cc 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-request.json
@@ -19,8 +19,6 @@
     "buyeruid": "GUM-UID"
   },
   "regs": {
-    "ext": {
-      "gdpr": 0
-    }
+    "gdpr": 0
   }
 }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-response.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-response.json
index 682e43e4516..7c131a1f180 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-auction-gumgum-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request.json
index b02e2e91a38..4ac2be5d032 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/gumgum/test-gumgum-bid-request.json
@@ -43,9 +43,7 @@
     "USD"
   ],
   "regs": {
-    "ext": {
-      "gdpr": 0
-    }
+    "gdpr": 0
   },
   "ext": {
     "prebid": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_app_promotion_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_app_promotion_type/test-huaweiads-auction-response.json
index 95248ad02c0..2068eb254c5 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_app_promotion_type/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_app_promotion_type/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58025103",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "h": 300,
           "w": 250,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ch_endpoint/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ch_endpoint/test-huaweiads-auction-response.json
index c576adccb02..931449bc021 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ch_endpoint/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ch_endpoint/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58025103",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "h": 300,
           "w": 250,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_eu_endpoint/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_eu_endpoint/test-huaweiads-auction-response.json
index c576adccb02..931449bc021 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_eu_endpoint/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_eu_endpoint/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58025103",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "h": 300,
           "w": 250,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_imei/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_imei/test-huaweiads-auction-response.json
index c576adccb02..931449bc021 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_imei/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_imei/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58025103",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "h": 300,
           "w": 250,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_interstitial_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_interstitial_type/test-huaweiads-auction-response.json
index 1ad60895c93..3f3b9084e45 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_interstitial_type/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_interstitial_type/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58025103",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "h": 300,
           "w": 250,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_mccmnc/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_mccmnc/test-huaweiads-auction-response.json
index 033288ceb0e..d6d29d2c614 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_mccmnc/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_mccmnc/test-huaweiads-auction-response.json
@@ -29,6 +29,7 @@
           "h": 250,
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "w": 300,
           "nurl":""
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_non_integer_mccmnc/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_non_integer_mccmnc/test-huaweiads-auction-response.json
index 033288ceb0e..d6d29d2c614 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_non_integer_mccmnc/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_non_integer_mccmnc/test-huaweiads-auction-response.json
@@ -29,6 +29,7 @@
           "h": 250,
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "w": 300,
           "nurl":""
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_not_app_promotion_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_not_app_promotion_type/test-huaweiads-auction-response.json
index 3e1b8422400..05a00845f55 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_not_app_promotion_type/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_not_app_promotion_type/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58025103",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "h": 300,
           "w": 250,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ru_endpoint/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ru_endpoint/test-huaweiads-auction-response.json
index 2b08c0e9f75..dee5e9c6ebb 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ru_endpoint/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_ru_endpoint/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58025103",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "h": 250,
           "w": 300,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_with_user_geo/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_with_user_geo/test-huaweiads-auction-response.json
index 033288ceb0e..d6d29d2c614 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_with_user_geo/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_with_user_geo/test-huaweiads-auction-response.json
@@ -29,6 +29,7 @@
           "h": 250,
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "w": 300,
           "nurl":""
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_device_geo/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_device_geo/test-huaweiads-auction-response.json
index 033288ceb0e..d6d29d2c614 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_device_geo/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_device_geo/test-huaweiads-auction-response.json
@@ -29,6 +29,7 @@
           "h": 250,
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "w": 300,
           "nurl":""
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_userext/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_userext/test-huaweiads-auction-response.json
index 621a43422cf..2fcaffd7d5b 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_userext/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_without_userext/test-huaweiads-auction-response.json
@@ -29,6 +29,7 @@
           "h": 300,
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "w": 250,
           "nurl":""
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_wrong_mccmnc/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_wrong_mccmnc/test-huaweiads-auction-response.json
index 033288ceb0e..d6d29d2c614 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_wrong_mccmnc/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/banner_wrong_mccmnc/test-huaweiads-auction-response.json
@@ -29,6 +29,7 @@
           "h": 250,
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "w": 300,
           "nurl":""
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_include_video/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_include_video/test-huaweiads-auction-response.json
index aa5fa9e4250..78e93b06659 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_include_video/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_include_video/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58022259",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "h": 500,
           "w": 600,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_single_image/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_single_image/test-huaweiads-auction-response.json
index dde9fbcb4ea..3da566123a6 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_single_image/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_single_image/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58022259",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "h": 1280,
           "w": 720,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image/test-huaweiads-auction-response.json
index 22f804c5841..8253f6b3e62 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58022259",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "h": 350,
           "w": 400,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image_include_icon/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image_include_icon/test-huaweiads-auction-response.json
index 898142d244c..b67ae7292e1 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image_include_icon/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/native_three_image_include_icon/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58022259",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "h": 350,
           "w": 400,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/simple_video/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/simple_video/test-huaweiads-auction-response.json
index a32f2cdc011..928b8e37d12 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/simple_video/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/simple_video/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58001445",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 1500,
           "price": 0.404,
           "h": 1280,
           "w": 720,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/test-huaweiads-auction-response.json
index 048960f1312..c59dbe20672 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/test-huaweiads-auction-response.json
@@ -29,6 +29,7 @@
           "h": 300,
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.404,
           "w": 250,
           "nurl": ""
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_interstitial_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_interstitial_type/test-huaweiads-auction-response.json
index 84bfd612d2e..2d7403f3e7e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_interstitial_type/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_interstitial_type/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58001445",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 1500,
           "price": 0.404,
           "h": 500,
           "w": 600,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_no_icons_no_images/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_no_icons_no_images/test-huaweiads-auction-response.json
index 84bfd612d2e..2d7403f3e7e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_no_icons_no_images/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_no_icons_no_images/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58001445",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 1500,
           "price": 0.404,
           "h": 500,
           "w": 600,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_icon/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_icon/test-huaweiads-auction-response.json
index edf5ff6ca1e..72686361101 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_icon/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_icon/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58001445",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 1500,
           "price": 0.404,
           "h": 500,
           "w": 600,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_images/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_images/test-huaweiads-auction-response.json
index aa9c0cccf38..9425d23926c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_images/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_rewarded_type_with_images/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58001445",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 1500,
           "price": 0.404,
           "h": 500,
           "w": 600,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_roll_type/test-huaweiads-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_roll_type/test-huaweiads-auction-response.json
index dde2af86099..16f6247161f 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_roll_type/test-huaweiads-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/huaweiads/video_roll_type/test-huaweiads-auction-response.json
@@ -21,6 +21,7 @@
           "crid": "58001445",
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 1500,
           "price": 0.404,
           "h": 1280,
           "w": 720,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/iionads/test-auction-iionads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/iionads/test-auction-iionads-response.json
index e5e6af0ffa2..e37c977df53 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/iionads/test-auction-iionads-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/iionads/test-auction-iionads-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/imds/test-auction-imds-response.json b/src/test/resources/org/prebid/server/it/openrtb2/imds/test-auction-imds-response.json
index 42b87323685..8eae0a3d518 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/imds/test-auction-imds-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/imds/test-auction-imds-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 7.77,
           "adm": "adm001",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/impactify/test-auction-impactify-response.json b/src/test/resources/org/prebid/server/it/openrtb2/impactify/test-auction-impactify-response.json
index 4bfd0bcee03..9ea7cb8766f 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/impactify/test-auction-impactify-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/impactify/test-auction-impactify-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-response.json
index 2ed506e5a4c..a826a40c221 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-auction-improvedigital-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request.json
index b79274a221e..05c99b710fc 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/improvedigital/test-improvedigital-bid-request.json
@@ -41,13 +41,6 @@
     "ext": {
       "ConsentedProvidersSettings": {
         "consented_providers": "1~10.20.90"
-      },
-      "consented_providers_settings": {
-        "consented_providers": [
-          10,
-          20,
-          90
-        ]
       }
     }
   },
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/indicue/test-auction-indicue-response.json b/src/test/resources/org/prebid/server/it/openrtb2/indicue/test-auction-indicue-response.json
index c561c6a98e8..6361cafe796 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/indicue/test-auction-indicue-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/indicue/test-auction-indicue-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 8.43,
           "adm": "adm14",
           "crid": "crid14",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/infytv/test-auction-infytv-response.json b/src/test/resources/org/prebid/server/it/openrtb2/infytv/test-auction-infytv-response.json
index d5d69df907e..55bd555ed6b 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/infytv/test-auction-infytv-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/infytv/test-auction-infytv-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 8.43,
           "adm": "adm14",
           "crid": "crid14",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json
index ea2a8cebd9e..d2d1bd207fa 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/inmobi/test-auction-inmobi-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/insticator/test-auction-insticator-request.json b/src/test/resources/org/prebid/server/it/openrtb2/insticator/test-auction-insticator-request.json
new file mode 100644
index 00000000000..2ae15855dcd
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/insticator/test-auction-insticator-request.json
@@ -0,0 +1,24 @@
+{
+  "id": "request_id",
+  "imp": [
+    {
+      "id": "imp_id",
+      "banner": {
+        "w": 300,
+        "h": 250
+      },
+      "ext": {
+        "insticator": {
+          "adUnitId": "adUnitId",
+          "publisherId": "adUnitId"
+        }
+      }
+    }
+  ],
+  "tmax": 5000,
+  "regs": {
+    "ext": {
+      "gdpr": 0
+    }
+  }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/insticator/test-auction-insticator-response.json b/src/test/resources/org/prebid/server/it/openrtb2/insticator/test-auction-insticator-response.json
new file mode 100644
index 00000000000..fa2b0a4accd
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/insticator/test-auction-insticator-response.json
@@ -0,0 +1,40 @@
+{
+  "id": "request_id",
+  "seatbid": [
+    {
+      "bid": [
+        {
+          "id": "bid_id",
+          "impid": "imp_id",
+          "exp": 300,
+          "price": 3.33,
+          "adm": "adm001",
+          "adid": "adid001",
+          "cid": "cid001",
+          "crid": "crid001",
+          "mtype": 1,
+          "w": 300,
+          "h": 250,
+          "ext": {
+            "prebid": {
+              "type": "banner"
+            },
+            "origbidcpm": 3.33
+          }
+        }
+      ],
+      "seat": "insticator",
+      "group": 0
+    }
+  ],
+  "cur": "USD",
+  "ext": {
+    "responsetimemillis": {
+      "insticator": "{{ insticator.response_time_ms }}"
+    },
+    "prebid": {
+      "auctiontimestamp": 0
+    },
+    "tmaxrequest": 5000
+  }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/insticator/test-insticator-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/insticator/test-insticator-bid-request.json
new file mode 100644
index 00000000000..5baeef4526a
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/insticator/test-insticator-bid-request.json
@@ -0,0 +1,65 @@
+{
+  "id": "request_id",
+  "imp": [
+    {
+      "id": "imp_id",
+      "secure": 1,
+      "banner": {
+        "w": 300,
+        "h": 250
+      },
+      "ext": {
+        "insticator": {
+          "adUnitId": "adUnitId",
+          "publisherId": "adUnitId"
+        }
+      }
+    }
+  ],
+  "source": {
+    "tid": "${json-unit.any-string}"
+  },
+  "site": {
+    "domain": "www.example.com",
+    "page": "http://www.example.com",
+    "publisher": {
+      "domain": "example.com",
+      "id" : "adUnitId"
+    },
+    "ext": {
+      "amp": 0
+    }
+  },
+  "device": {
+    "ua": "userAgent",
+    "ip": "193.168.244.1"
+  },
+  "at": 1,
+  "tmax": "${json-unit.any-number}",
+  "cur": [
+    "USD"
+  ],
+  "regs": {
+    "ext": {
+      "gdpr": 0
+    }
+  },
+  "ext": {
+    "insticator": {
+      "caller": [
+        {
+          "name": "Prebid-Server",
+          "version": "n/a"
+        }
+      ]
+    },
+    "prebid": {
+      "server": {
+        "externalurl": "http://localhost:8080",
+        "gvlid": 1,
+        "datacenter": "local",
+        "endpoint": "/openrtb2/auction"
+      }
+    }
+  }
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/insticator/test-insticator-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/insticator/test-insticator-bid-response.json
new file mode 100644
index 00000000000..2769168e6ed
--- /dev/null
+++ b/src/test/resources/org/prebid/server/it/openrtb2/insticator/test-insticator-bid-response.json
@@ -0,0 +1,21 @@
+{
+  "id": "request_id",
+  "seatbid": [
+    {
+      "bid": [
+        {
+          "id": "bid_id",
+          "impid": "imp_id",
+          "price": 3.33,
+          "adid": "adid001",
+          "crid": "crid001",
+          "cid": "cid001",
+          "adm": "adm001",
+          "mtype": 1,
+          "h": 250,
+          "w": 300
+        }
+      ]
+    }
+  ]
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json b/src/test/resources/org/prebid/server/it/openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json
index 174c7a0894b..3100814d919 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/interactiveoffers/test-auction-interactiveoffers-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 10,
           "adomain": [
           ],
@@ -33,4 +34,4 @@
       "auctiontimestamp": 0
     }
   }
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/intertech/test-auction-intertech-response.json b/src/test/resources/org/prebid/server/it/openrtb2/intertech/test-auction-intertech-response.json
index 9255d5d9323..9a0fc145939 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/intertech/test-auction-intertech-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/intertech/test-auction-intertech-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json
index c07c5d21b49..4322850fa9d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/invibes/test-auction-invibes-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.3,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/iqx/test-auction-iqx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/iqx/test-auction-iqx-response.json
index 76ef74b808d..3fc5c87e169 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/iqx/test-auction-iqx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/iqx/test-auction-iqx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "mtype": 1,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json
index 4a6e48aac57..b73fb55b074 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/iqzone/test-auction-iqzone-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "mtype": 1,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json
index 0c46e680fbc..1fc85c3cd79 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-request.json
@@ -8,6 +8,7 @@
         "h": 250
       },
       "ext": {
+        "ae": 1,
         "ix": {
           "siteId": "10002"
         }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json
index 8685c8ede1a..1ecdc0136a1 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-auction-ix-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 4.7,
           "adm": "adm6",
           "crid": "crid6",
@@ -27,7 +28,47 @@
       "ix": "{{ ix.response_time_ms }}"
     },
     "prebid": {
-      "auctiontimestamp": 0
+      "auctiontimestamp": 0,
+      "fledge": {
+        "auctionconfigs": [
+          {
+            "impid": "imp_id",
+            "bidder": "ix",
+            "adapter": "ix",
+            "config": {
+              "seller": "https://test.casalemedia.com",
+              "decisionLogicUrl": "https://test.casalemedia.com/decision-logic.js",
+              "trustedScoringSignalsURL": "https://test.casalemedia.com/123",
+              "interestGroupBuyers": [
+                "https://test.com"
+              ],
+              "sellerSignals": {
+                "callbackURL": "https://test.casalemedia.com/callback/1",
+                "debugURL": "https://test.casalemedia.com/debug/1",
+                "width": 300,
+                "height": 250
+              },
+              "sellerTimeout": 150,
+              "perBuyerSignals": {
+                "https://test.com": [
+                  {
+                    "key": "value"
+                  }
+                ]
+              },
+              "perBuyerCurrencies": {
+                "*": "USD"
+              },
+              "sellerCurrency": "USD",
+              "requestedSize": {
+                "width": 300,
+                "height": 250
+              },
+              "maxTrustedBiddingSignalsURLLength": 1000
+            }
+          }
+        ]
+      }
     },
     "tmaxrequest": 5000
   }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request.json
index 0658f90c813..ef303b14b8e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-request.json
@@ -15,6 +15,7 @@
         "h": 250
       },
       "ext": {
+        "ae": 1,
         "tid": "${json-unit.any-string}",
         "bidder": {
           "siteId": "10002"
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-response.json
index 9d9d7035ed7..c62e7c626b9 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/ix/test-ix-bid-response.json
@@ -13,5 +13,43 @@
       ],
       "seat": "seatId6"
     }
-  ]
+  ],
+  "ext": {
+    "protectedAudienceAuctionConfigs": [
+      {
+        "bidId": "imp_id",
+        "config": {
+          "seller": "https://test.casalemedia.com",
+          "decisionLogicUrl": "https://test.casalemedia.com/decision-logic.js",
+          "trustedScoringSignalsURL": "https://test.casalemedia.com/123",
+          "interestGroupBuyers": [
+            "https://test.com"
+          ],
+          "sellerSignals": {
+            "callbackURL": "https://test.casalemedia.com/callback/1",
+            "debugURL": "https://test.casalemedia.com/debug/1",
+            "width": 300,
+            "height": 250
+          },
+          "sellerTimeout": 150,
+          "perBuyerSignals": {
+            "https://test.com": [
+              {
+                "key": "value"
+              }
+            ]
+          },
+          "perBuyerCurrencies": {
+            "*": "USD"
+          },
+          "sellerCurrency": "USD",
+          "requestedSize": {
+            "width": 300,
+            "height": 250
+          },
+          "maxTrustedBiddingSignalsURLLength": 1000
+        }
+      }
+    ]
+  }
 }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/jdpmedia/test-auction-jdpmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/jdpmedia/test-auction-jdpmedia-response.json
index 48fed77c3e7..31a01fdf368 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/jdpmedia/test-auction-jdpmedia-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/jdpmedia/test-auction-jdpmedia-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/jixie/test-auction-jixie-response.json b/src/test/resources/org/prebid/server/it/openrtb2/jixie/test-auction-jixie-response.json
index f9c2484affa..196ed6b59e7 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/jixie/test-auction-jixie-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/jixie/test-auction-jixie-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-auction-kargo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-auction-kargo-response.json
index 10481c0accb..2589f6a4a4d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-auction-kargo-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/kargo/test-auction-kargo-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kayzen/test-auction-kayzen-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kayzen/test-auction-kayzen-response.json
index 6b513b46072..aaccad9be2d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/kayzen/test-auction-kayzen-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/kayzen/test-auction-kayzen-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-response.json
index 668b51a85a8..5cec9fef037 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/kidoz/test-auction-kidoz-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/kiviads/test-auction-kiviads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/kiviads/test-auction-kiviads-response.json
index 046aeaa3005..07da2713c33 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/kiviads/test-auction-kiviads-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/kiviads/test-auction-kiviads-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/krushmedia/test-auction-krushmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/krushmedia/test-auction-krushmedia-response.json
index 25228273c75..56f25641c69 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/krushmedia/test-auction-krushmedia-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/krushmedia/test-auction-krushmedia-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "adid",
           "cid": "cid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lemmaDigital/test-auction-lemmaDigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/lemmaDigital/test-auction-lemmaDigital-response.json
index a21706f9abf..59dc0706b2d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/lemmaDigital/test-auction-lemmaDigital-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/lemmaDigital/test-auction-lemmaDigital-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/liftoff/test-auction-liftoff-response.json b/src/test/resources/org/prebid/server/it/openrtb2/liftoff/test-auction-liftoff-response.json
index 999b4184dbd..e90d9b5f6aa 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/liftoff/test-auction-liftoff-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/liftoff/test-auction-liftoff-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 3.33,
           "adid": "adid001",
           "adm": "<VAST version=\"3.0\"></VAST>",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/limelightDigital/test-auction-limelightDigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/limelightDigital/test-auction-limelightDigital-response.json
index 409ecdcd328..5bf9bad0853 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/limelightDigital/test-auction-limelightDigital-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/limelightDigital/test-auction-limelightDigital-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lmkiviads/test-auction-lmkiviads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/lmkiviads/test-auction-lmkiviads-response.json
index 1036e1e84d0..200e13238f8 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/lmkiviads/test-auction-lmkiviads-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/lmkiviads/test-auction-lmkiviads-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-response.json b/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-response.json
index 0433389b511..d1ae959bebe 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/lockerdome/test-auction-lockerdome-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 7.35,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/logan/test-auction-logan-response.json b/src/test/resources/org/prebid/server/it/openrtb2/logan/test-auction-logan-response.json
index 5079a616a00..f5503cf319d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/logan/test-auction-logan-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/logan/test-auction-logan-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-response.json b/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-response.json
index dd7aa14d18c..4319787685b 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/logicad/test-auction-logicad-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "adid",
           "cid": "cid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-response.json b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-response.json
index 2c86435fb5b..30397fe4de4 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/loopme/test-auction-loopme-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/loyal/test-auction-loyal-response.json b/src/test/resources/org/prebid/server/it/openrtb2/loyal/test-auction-loyal-response.json
index 80cbbdf2ebb..58b5b47ee41 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/loyal/test-auction-loyal-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/loyal/test-auction-loyal-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "mtype": 1,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-response.json
index a7c3e9ba1e2..194301eae99 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/lunamedia/test-auction-lunamedia-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "adid",
           "cid": "cid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mabidder/test-auction-mabidder-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mabidder/test-auction-mabidder-response.json
index dacba278abe..28e33d7e084 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/mabidder/test-auction-mabidder-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/mabidder/test-auction-mabidder-response.json
@@ -6,6 +6,7 @@
         {
           "id": "test-imp-id",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 2.734,
           "adm": "<script type='text/javascript' src='https://adsvr.ecdrsvc.com/js?6002677'></script>",
           "adomain": [
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/madvertise/test-auction-madvertise-response.json b/src/test/resources/org/prebid/server/it/openrtb2/madvertise/test-auction-madvertise-response.json
index c7c4540bd47..1c31dddaea0 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/madvertise/test-auction-madvertise-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/madvertise/test-auction-madvertise-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json
index fd8cbf0a699..111e147d710 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-auction-magnite-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json
index 08104541960..4b14180a370 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/magnite/test-magnite-bid-request.json
@@ -27,7 +27,8 @@
             "mint_version": ""
           }
         },
-        "maxbids": 1
+        "maxbids": 1,
+        "tid": "${json-unit.any-string}"
       }
     }
   ],
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/markapp/test-auction-markapp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/markapp/test-auction-markapp-response.json
index dad8c29bdf5..a6bccc7525a 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/markapp/test-auction-markapp-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/markapp/test-auction-markapp-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-response.json
index 8bd2fdc004e..b2536b23494 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/marsmedia/test-auction-marsmedia-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 7.35,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mediago/test-auction-mediago-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mediago/test-auction-mediago-response.json
index 5c5f50670a2..7eb299b660f 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/mediago/test-auction-mediago-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/mediago/test-auction-mediago-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/medianet/test-auction-medianet-response.json b/src/test/resources/org/prebid/server/it/openrtb2/medianet/test-auction-medianet-response.json
index 14ebb3b62f8..55b8dba2496 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/medianet/test-auction-medianet-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/medianet/test-auction-medianet-response.json
@@ -6,6 +6,7 @@
         {
           "id": "randomid",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.5,
           "adm": "some-test-ad",
           "adid": "12345678",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json
index 42c6696f9bb..a810ba066a1 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/melozen/test-auction-melozen-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json
index b37ebc3ebea..82cc50b31be 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/metax/test-auction-metax-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json
index 89dc1790afe..f2c474641aa 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/mgid/test-auction-mgid-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.5,
           "nurl": "nurl",
           "adm": "some-test-ad",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mgidx/test-auction-mgidx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mgidx/test-auction-mgidx-response.json
index 217cac91e56..d24c8274ed9 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/mgidx/test-auction-mgidx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/mgidx/test-auction-mgidx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/minutemedia/test-auction-minutemedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/minutemedia/test-auction-minutemedia-response.json
index 898532edc4b..10cdbd0a6f5 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/minutemedia/test-auction-minutemedia-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/minutemedia/test-auction-minutemedia-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "banner_imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "some-test-ad",
           "adid": "29681110",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json
index d76e9f071e5..28f9caabf7e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/missena/test-auction-missena-response.json
@@ -6,6 +6,7 @@
         {
           "id": "request_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 10.2,
           "adm": "adm",
           "crid": "id",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json
index 1c1fbe7791b..fb8a20ae2b0 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/mobfoxpb/test-auction-mobfoxpb-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"https://bes.mobfox.com/?c=o&m=adm&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
           "cid": "test_cid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-response.json b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-response.json
index d4ff118ad3c..3f22980426a 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/mobilefuse/test-auction-mobilefuse-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adm": "<b>hi</b>",
           "cid": "test_cid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/motorik/test-auction-motorik-response.json b/src/test/resources/org/prebid/server/it/openrtb2/motorik/test-auction-motorik-response.json
index 17ba2b420b7..fba432c5be4 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/motorik/test-auction-motorik-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/motorik/test-auction-motorik-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-auction-generic-genericAlias-response.json b/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-auction-generic-genericAlias-response.json
index bfd9600aa75..1e4f6f4a489 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-auction-generic-genericAlias-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-auction-generic-genericAlias-response.json
@@ -11,7 +11,7 @@
           "crid": "crid1",
           "w": 300,
           "h": 250,
-          "exp": 120,
+          "exp": 1500,
           "ext": {
             "prebid": {
               "type": "video",
@@ -68,7 +68,7 @@
           "crid": "crid1",
           "w": 300,
           "h": 250,
-          "exp": 120,
+          "exp": 1500,
           "ext": {
             "prebid": {
               "type": "video",
@@ -128,7 +128,7 @@
           "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
           "cid": "958",
           "crid": "29681110",
-          "exp": 120,
+          "exp": 1500,
           "ext": {
             "prebid": {
               "type": "video",
@@ -179,7 +179,7 @@
           "iurl": "http://nym1-ib.adnxs.com/cr?id=69595837",
           "cid": "958",
           "crid": "69595837",
-          "exp": 120,
+          "exp": 1500,
           "ext": {
             "prebid": {
               "type": "video",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-cache-generic-genericAlias-request.json b/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-cache-generic-genericAlias-request.json
index 2d951abdbab..770ee7c9d39 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-cache-generic-genericAlias-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/multi_bid/test-cache-generic-genericAlias-request.json
@@ -32,7 +32,8 @@
         },
         "wurl": "http://localhost:8080/event?t=win&b=21521324&a=5001&aid=tid&ts=1000&bidder=generic&f=i&int="
       },
-      "aid": "tid"
+      "aid": "tid",
+      "ttlseconds": 1500
     },
     {
       "type": "json",
@@ -68,7 +69,8 @@
         },
         "wurl": "http://localhost:8080/event?t=win&b=7706636740145184841&a=5001&aid=tid&ts=1000&bidder=genericAlias&f=i&int="
       },
-      "aid": "tid"
+      "aid": "tid",
+      "ttlseconds": 1500
     },
     {
       "type": "json",
@@ -102,7 +104,8 @@
         },
         "wurl": "http://localhost:8080/event?t=win&b=880290288&a=5001&aid=tid&ts=1000&bidder=generic&f=i&int="
       },
-      "aid": "tid"
+      "aid": "tid",
+      "ttlseconds": 1500
     },
     {
       "type": "json",
@@ -138,7 +141,8 @@
         },
         "wurl": "http://localhost:8080/event?t=win&b=222214214214&a=5001&aid=tid&ts=1000&bidder=genericAlias&f=i&int="
       },
-      "aid": "tid"
+      "aid": "tid",
+      "ttlseconds": 1500
     },
     {
       "type": "xml",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json
index 71068f2cb94..be5cf8b9277 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/nextmillennium/test-auction-nextmillennium-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "mtype": 1,
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json
index fc7edb8ce86..ebeea62aba0 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/nobid/test-auction-nobid-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "adid",
           "cid": "cid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json
index f6a94e868a8..315114b4f75 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/oms/test-auction-oms-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/onetag/test-auction-onetag-response.json b/src/test/resources/org/prebid/server/it/openrtb2/onetag/test-auction-onetag-response.json
index 80373e03cb7..4496bc2e4b6 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/onetag/test-auction-onetag-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/onetag/test-auction-onetag-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "adid",
           "cid": "cid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/openweb/test-auction-openweb-response.json b/src/test/resources/org/prebid/server/it/openrtb2/openweb/test-auction-openweb-response.json
index 53761fd9c76..a996e780237 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/openweb/test-auction-openweb-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/openweb/test-auction-openweb-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "mtype": 1,
           "price": 5.78,
           "adm": "adm00",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-response.json
index 8ffbc819833..0c22b5e89b9 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/openx/test-auction-openx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 5.78,
           "adm": "adm00",
           "crid": "crid00",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/operaads/test-auction-operaads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/operaads/test-auction-operaads-response.json
index 2c7b4e4f22c..91f9dc59e5c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/operaads/test-auction-operaads-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/operaads/test-auction-operaads-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json
index 6871f609875..4bbd63fe482 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/oraki/test-auction-oraki-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-response.json b/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-response.json
index 1a260bfa518..40a02aff7f5 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/orbidder/test-auction-orbidder-response.json
@@ -16,6 +16,7 @@
           },
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "mtype": 1
         }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/outbrain/test-auction-outbrain-response.json b/src/test/resources/org/prebid/server/it/openrtb2/outbrain/test-auction-outbrain-response.json
index 6c19a2a44b7..cb7fbc34fe8 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/outbrain/test-auction-outbrain-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/outbrain/test-auction-outbrain-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json
index 38e6c451540..d7a74ef6d6d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/ownadx/test-auction-ownadx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "mtype": 1,
           "price": 3.33,
           "adm": "adm001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pangle/test-auction-pangle-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pangle/test-auction-pangle-response.json
index 7a0cf377706..b0995267c08 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/pangle/test-auction-pangle-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/pangle/test-auction-pangle-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pgam/test-auction-pgam-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pgam/test-auction-pgam-response.json
index 79f17402a81..82e6a8182df 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/pgam/test-auction-pgam-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/pgam/test-auction-pgam-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 8.43,
           "adm": "adm14",
           "crid": "crid14",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pgamssp/test-auction-pgamssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pgamssp/test-auction-pgamssp-response.json
index f2c7121be84..303e8a7826c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/pgamssp/test-auction-pgamssp-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/pgamssp/test-auction-pgamssp-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/playdigo/test-auction-playdigo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/playdigo/test-auction-playdigo-response.json
index 1c26c09c8c7..8bfc8b3f515 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/playdigo/test-auction-playdigo-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/playdigo/test-auction-playdigo-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "mtype": 1,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/preciso/test-auction-preciso-response.json b/src/test/resources/org/prebid/server/it/openrtb2/preciso/test-auction-preciso-response.json
index 2b363db2f29..c26bf519990 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/preciso/test-auction-preciso-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/preciso/test-auction-preciso-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "some-test-ad",
           "adid": "12345678",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-request.json
index 4ce766dfbcc..3a20b08ce67 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-request.json
@@ -49,9 +49,7 @@
   ],
   "tmax": 5000,
   "regs": {
-    "ext": {
-      "gdpr": 0
-    }
+    "gdpr": 0
   },
   "ext": {
     "prebid": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json
index 53731b03bf0..4df8cf0c723 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-auction-pubmatic-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "test-imp-id",
+          "exp": 1500,
           "price": 4.75,
           "adm": "adm9",
           "crid": "crid9",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-request.json
index 7a4f1e0279d..a34e904ffe5 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/pubmatic/test-pubmatic-bid-request.json
@@ -12,7 +12,7 @@
         "h": 600
       },
       "tagid": "slot9",
-      "bidfloor" : 0.12,
+      "bidfloor": 0.12,
       "ext": {
         "ae": 1,
         "pmZoneId": "Zone1,Zone2",
@@ -45,27 +45,9 @@
     "USD"
   ],
   "regs": {
-    "ext": {
-      "gdpr": 0
-    }
+    "gdpr": 0
   },
   "ext": {
-    "prebid": {
-      "server": {
-        "externalurl": "http://localhost:8080",
-        "gvlid": 1,
-        "datacenter": "local",
-        "endpoint": "/openrtb2/auction"
-      },
-      "bidderparams": {
-        "pubmatic": {
-          "acat": [
-            "testValue1",
-            "testValue2"
-          ]
-        }
-      }
-    },
     "acat": [
       "testValue1",
       "testValue2"
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-response.json
index 7b7334c52fe..d263e2b1f43 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/pubnative/test-auction-pubnative-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json
index a49d1f99e75..752b5519b40 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/pubrise/test-auction-pubrise-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-response.json
index 332e1cae3ac..88c15aebf71 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/pulsepoint/test-auction-pulsepoint-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 4.75,
           "adm": "adm8",
           "crid": "crid8",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/pwbid/test-auction-pwbid-response.json b/src/test/resources/org/prebid/server/it/openrtb2/pwbid/test-auction-pwbid-response.json
index 0f38f603a90..b6c810ede52 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/pwbid/test-auction-pwbid-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/pwbid/test-auction-pwbid-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 8.43,
           "adm": "adm14",
           "crid": "crid14",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json
index 4ebd8ee119a..16db7e67c68 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/qt/test-auction-qt-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/readpeak/test-auction-readpeak-response.json b/src/test/resources/org/prebid/server/it/openrtb2/readpeak/test-auction-readpeak-response.json
index 101455149d7..06a8c3170aa 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/readpeak/test-auction-readpeak-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/readpeak/test-auction-readpeak-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "mtype": 1,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/relevantdigital/test-auction-relevantdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/relevantdigital/test-auction-relevantdigital-response.json
index 9e1e479b4ed..d3531b49cca 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/relevantdigital/test-auction-relevantdigital-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/relevantdigital/test-auction-relevantdigital-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 5.78,
           "adm": "adm",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-response.json b/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-response.json
index c3de29e49d4..f595ea39c9f 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/resetdigital/test-auction-resetdigital-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/revcontent/test-auction-revcontent-response.json b/src/test/resources/org/prebid/server/it/openrtb2/revcontent/test-auction-revcontent-response.json
index a2acc3d4df5..4e6047c3738 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/revcontent/test-auction-revcontent-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/revcontent/test-auction-revcontent-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "<div id='rtb-widget",
           "adid": "12345678",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/richaudience/test-auction-richaudience-response.json b/src/test/resources/org/prebid/server/it/openrtb2/richaudience/test-auction-richaudience-response.json
index ebbd8e5588f..eff55f8f91a 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/richaudience/test-auction-richaudience-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/richaudience/test-auction-richaudience-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rise/test-auction-rise-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rise/test-auction-rise-response.json
index b46b466c82b..bd6fcb25dff 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/rise/test-auction-rise-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/rise/test-auction-rise-response.json
@@ -7,6 +7,7 @@
           "id": "bid_id",
           "mtype": 1,
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/roulax/test-auction-roulax-response.json b/src/test/resources/org/prebid/server/it/openrtb2/roulax/test-auction-roulax-response.json
index 298f141429f..a5bbb8909ea 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/roulax/test-auction-roulax-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/roulax/test-auction-roulax-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "mtype": 1,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-response.json
index 841d82b403c..af521daf6b9 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/rtbhouse/test-auction-rtbhouse-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "some-test-ad_0.5",
           "adid": "12345678",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-auction-rubicon-response.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-auction-rubicon-response.json
index 75ac81780aa..c577324825d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-auction-rubicon-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-auction-rubicon-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-request.json
index 08104541960..4b14180a370 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/rubicon/test-rubicon-bid-request.json
@@ -27,7 +27,8 @@
             "mint_version": ""
           }
         },
-        "maxbids": 1
+        "maxbids": 1,
+        "tid": "${json-unit.any-string}"
       }
     }
   ],
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-auction-salunamedia-response.json b/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-auction-salunamedia-response.json
index d3bd2828fc5..1ac268bddad 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-auction-salunamedia-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/salunamedia/test-auction-salunamedia-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/screencore/test-auction-screencore-response.json b/src/test/resources/org/prebid/server/it/openrtb2/screencore/test-auction-screencore-response.json
index d6148cb49b5..b644dd7caf2 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/screencore/test-auction-screencore-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/screencore/test-auction-screencore-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/seedingAlliance/test-auction-seedingAlliance-response.json b/src/test/resources/org/prebid/server/it/openrtb2/seedingAlliance/test-auction-seedingAlliance-response.json
index eabd17aca5f..b444c1eb55d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/seedingAlliance/test-auction-seedingAlliance-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/seedingAlliance/test-auction-seedingAlliance-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 11.393,
           "adm": "some adm price 10",
           "adomain": [
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json
index 61109eff224..edb951916c4 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-auction-sharethrough-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "adid",
           "cid": "cid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-request.json
index 336cd9f774b..fa6695a0791 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/sharethrough/test-sharethrough-bid-request.json
@@ -33,9 +33,7 @@
   },
   "at": 1,
   "tmax": "${json-unit.any-number}",
-  "cur": [
-    "USD"
-  ],
+  "cur": ["USD"],
   "source": {
     "tid": "${json-unit.any-string}",
     "ext": {
@@ -44,9 +42,7 @@
     }
   },
   "regs": {
-    "ext": {
-      "gdpr": 0
-    }
+    "gdpr": 0
   },
   "ext": {
     "prebid": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-response.json b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-response.json
index a3dca51a91e..4a7bc8e5210 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/silvermob/test-auction-silvermob-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "adid",
           "cid": "cid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/silverpush/test-auction-silverpush-response.json b/src/test/resources/org/prebid/server/it/openrtb2/silverpush/test-auction-silverpush-response.json
index 8b9198675bf..35c5e13b170 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/silverpush/test-auction-silverpush-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/silverpush/test-auction-silverpush-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "mtype": 1,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-request.json
index a2fa2aac8db..9415b0be91e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-request.json
@@ -10,6 +10,31 @@
       },
       "instl": 1,
       "ext": {
+        "gpid": 1,
+        "skadn": {
+          "versions": [
+            "2.0",
+            "2.1",
+            "2.2",
+            "3.0",
+            "4.0"
+          ],
+          "sourceapp": "880047117",
+          "productpage": 1,
+          "skadnetlist": {
+            "max": 306,
+            "excl": [
+              2,
+              8,
+              10,
+              55
+            ],
+            "addl": [
+              "cdkw7geqsh.skadnetwork",
+              "qyJfv329m4.skadnetwork"
+            ]
+          }
+        },
         "smaato": {
           "publisherId": "11000",
           "adspaceId": "130563103"
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-response.json
index 01613d91865..463da2b0945 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-auction-smaato-response.json
@@ -9,6 +9,7 @@
           "crid": "crid",
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "exp": 300,
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-request.json
index 3be139028cb..5230998792c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/smaato/test-smaato-bid-request.json
@@ -9,7 +9,35 @@
         "w": 300,
         "h": 250
       },
-      "tagid": "130563103"
+      "tagid": "130563103",
+      "ext" : {
+        "gpid": 1,
+        "skadn": {
+          "versions": [
+            "2.0",
+            "2.1",
+            "2.2",
+            "3.0",
+            "4.0"
+          ],
+          "sourceapp": "880047117",
+          "productpage": 1,
+          "skadnetlist": {
+            "max": 306,
+            "excl": [
+              2,
+              8,
+              10,
+              55
+            ],
+            "addl": [
+              "cdkw7geqsh.skadnetwork",
+              "qyJfv329m4.skadnetwork"
+            ]
+          }
+        },
+        "tid": "${json-unit.any-string}"
+      }
     }
   ],
   "source": {
@@ -37,6 +65,6 @@
     }
   },
   "ext": {
-    "client": "prebid_server_1.1"
+    "client": "prebid_server_1.2"
   }
 }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json
index fde0fbaa115..7b143f69a40 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/smartadserver/test-auction-smartadserver-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "some-test-ad",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smarthub/test-auction-smarthub-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smarthub/test-auction-smarthub-response.json
index 7a7f8a42adc..5121930a9be 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/smarthub/test-auction-smarthub-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/smarthub/test-auction-smarthub-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-response.json
index fbe5d5a69a2..0797e39c9a7 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/smartrtb/test-auction-smartrtb-response.json
@@ -16,6 +16,7 @@
           },
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01
         }
       ],
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartx/test-auction-smartx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartx/test-auction-smartx-response.json
index 0e65f197c39..0e92d0f7a89 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/smartx/test-auction-smartx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/smartx/test-auction-smartx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 3.33,
           "crid": "creativeId",
           "adm": "adm001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-auction-smartyads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-auction-smartyads-response.json
index 5d5a737a256..0b6d4de8672 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-auction-smartyads-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/smartyads/test-auction-smartyads-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "adid",
           "cid": "cid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-auction-smilewanted-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-auction-smilewanted-response.json
index b882d1c8f28..c7e19cf69cf 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-auction-smilewanted-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/smilewanted/test-auction-smilewanted-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-auction-smrtconnect-response.json b/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-auction-smrtconnect-response.json
index 90d63b63f15..d3c9f4e8a2d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-auction-smrtconnect-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/smrtconnect/test-auction-smrtconnect-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "some-test-ad",
           "adid": "12345678",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-response.json
index 8753fcde862..59208dd177e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/sonobi/test-auction-sonobi-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-response.json
index 2f6f712de8b..58d1bef9d8d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/sovrn/test-auction-sovrn-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 5.78,
           "adm": "adm 13",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sovrnxsp/test-auction-sovrnxsp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sovrnxsp/test-auction-sovrnxsp-response.json
index 91440dcff88..3d0373bdb97 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/sovrnxsp/test-auction-sovrnxsp-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/sovrnxsp/test-auction-sovrnxsp-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 5.78,
           "adm": "adm",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/sspbc/test-auction-sspbc-response.json b/src/test/resources/org/prebid/server/it/openrtb2/sspbc/test-auction-sspbc-response.json
index 557203e3a84..8e87bb83efd 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/sspbc/test-auction-sspbc-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/sspbc/test-auction-sspbc-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "<!--preformatted-->",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-auction-response.json b/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-auction-response.json
index ab85c489792..f25cdd92a2f 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-auction-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-auction-response.json
@@ -6,6 +6,7 @@
         {
           "id": "466223845",
           "impid": "impStoredBidResponse",
+          "exp": 300,
           "price": 0.8,
           "adm": "adm1",
           "crid": "crid1",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-cache-request.json b/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-cache-request.json
index 65c5bb22030..ba1f6a2d32c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-cache-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/storedresponse/test-cache-request.json
@@ -17,7 +17,8 @@
           "origbidcpm": 0.8
         }
       },
-      "aid": "tid"
+      "aid": "tid",
+      "ttlseconds": 300
     }
   ]
 }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-auction-streamlyn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-auction-streamlyn-response.json
index bd20be683c2..919e3fcdcb6 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-auction-streamlyn-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/streamlyn/test-auction-streamlyn-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/stroeercore/test-auction-stroeercore-response.json b/src/test/resources/org/prebid/server/it/openrtb2/stroeercore/test-auction-stroeercore-response.json
index 6a990029f90..c27cc4c6474 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/stroeercore/test-auction-stroeercore-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/stroeercore/test-auction-stroeercore-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id_1",
           "impid": "imp_id_1",
+          "exp": 300,
           "price": 7.713,
           "adm": "<div>foo</div>",
           "crid":"banner_creative_id",
@@ -22,6 +23,7 @@
         {
           "id": "bid_id_2",
           "impid": "imp_id_2",
+          "exp": 1500,
           "price": 6.494,
           "adm": "<vast><div>video</div></vast>",
           "crid":"video_creative_id",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/suntContent/test-auction-suntContent-response.json b/src/test/resources/org/prebid/server/it/openrtb2/suntContent/test-auction-suntContent-response.json
index fb4b83209a7..7977110e93b 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/suntContent/test-auction-suntContent-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/suntContent/test-auction-suntContent-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 11.393,
           "adm": "some adm price 10",
           "adomain": [
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/taboola/test-auction-taboola-response.json b/src/test/resources/org/prebid/server/it/openrtb2/taboola/test-auction-taboola-response.json
index 0bf4f177bd0..ba87a68d153 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/taboola/test-auction-taboola-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/taboola/test-auction-taboola-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "1",
+          "exp": 300,
           "price": 8.43,
           "crid": "crid14",
           "adm": "some-test-ad",
@@ -21,6 +22,7 @@
         {
           "id": "bid_id",
           "impid": "2",
+          "exp": 300,
           "price": 8.43,
           "crid": "crid14",
           "adm": "{\"assets\":[{\"id\":0,\"title\":{\"text\":\"This is an example Prebid Native creative\"}}]}",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-response.json
index 98bd6f901a7..1fe9c183839 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/tappx/test-auction-tappx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "<!-- admarkup -->",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/teads/test-auction-teads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/teads/test-auction-teads-response.json
index 329f67dc261..37411e7dfb1 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/teads/test-auction-teads-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/teads/test-auction-teads-response.json
@@ -6,6 +6,7 @@
         {
           "id": "695ac187-fb3f-4d1f-8d5d-099c5e4c4d28",
           "impid": "imp_id",
+          "exp": 300,
           "price": 33,
           "nurl": "https://localhost:8080/prebid-server/win-notice?data=base64&clearingPrice=${AUCTION_PRICE}",
           "adm": "{\"settings\":{\"values\":{\"animations\":{\"expand\":0,\"collapse\":0.5},\"placementId\":2,\"adType\":\"video\",\"placementFormat\":\"inread\",\"allowedPlayer\":\"any\",\"pageId\":2},\"components\":{\"closeButton\":{\"display\":false,\"countdown\":0},\"credits\":{\"display\":false},\"soundButton\":{\"display\":true,\"countdown\":0,\"type\":\"equalizer\"},\"label\":{\"display\":false},\"slider\":{\"closeButtonDisplay\":false}},\"behaviors\":{\"smartPosition\":{\"top\":false,\"corner\":false,\"mustBypassWhitelist\":true},\"slider\":{\"enable\":false},\"friendly\":false,\"playerClick\":\"fullscreen\",\"soundStart\":{\"type\":\"mute\"},\"soundMute\":\"threshold\",\"soundOver\":\"over\",\"launch\":\"auto\",\"videoStart\":\"threshold\",\"videoPause\":\"threshold\",\"secure\":false}},\"ads\":[{\"settings\":{\"values\":{\"animations\":{\"expand\":0,\"collapse\":0.5},\"placementId\":2,\"adType\":\"video\",\"placementFormat\":\"inread\",\"allowedPlayer\":\"any\",\"pageId\":2},\"components\":{\"closeButton\":{\"display\":false,\"countdown\":0},\"credits\":{\"display\":false},\"soundButton\":{\"display\":true,\"countdown\":0,\"type\":\"equalizer\"},\"label\":{\"display\":false},\"slider\":{\"closeButtonDisplay\":false}},\"behaviors\":{\"smartPosition\":{\"top\":false,\"corner\":false,\"mustBypassWhitelist\":true},\"slider\":{\"enable\":false},\"friendly\":false,\"playerClick\":\"fullscreen\",\"soundStart\":{\"type\":\"mute\"},\"soundMute\":\"threshold\",\"soundOver\":\"over\",\"launch\":\"auto\",\"videoStart\":\"threshold\",\"videoPause\":\"threshold\",\"secure\":false}},\"type\":\"VastXml\",\"content\":\"<VAST version=\\\"3.0\\\"><Ad id=\\\"1\\\"><Wrapper><AdSystem>Teads Technology</AdSystem><VASTAdTagURI><![CDATA[http://vast.tv?auction_publisher_cost=LlBEcMjl2WSVYTIV0ZvZwFta&auction_publisher_cost_currency=USD&auction_ssb_provider_fee=&auction_ssb_provider_fee_currency=&auction_price=74Xz11zXoN9S_a18chCl1DB6&auction_currency=USD]]></VASTAdTagURI><Error><![CDATA[https://localhost:18281/track?action=error-vast&code=[ERRORCODE]&pid=2&vid=708ca808-ec55-4d97-ab81-9c4777e16058&pfid=971104812&mediaFileType=[MEDIAFILETYPE]&auctid=39312703-e970-4914-ae56-8e7d7d1fd16b__b6321d41-3840-4cb3-baad-b6fc5b0c8553__c0f2e6ba-63d0-4e20-ab41-fe0822eb65a6&sid=0&scid=971105412&pscid=971105411&psid=971105457&hb_provider=prebid-server&hb_ad_unit_code=742d38c4-7994-4c2b-ac82-18d3a64ba3c7&dsp_campaign_id=1&dsp_creative_id=1&env=thirdparty-inapp&p=GmbtDz8E6SttqPqekGLm3vHN7muObx7-w6kGLgR8KMiWgUo78VfzNzYlcfjjRwTen7Oad6lYvgPUaiHwDV0lZcpu7lXO4Y7at1NIyIPxfcgdBw&cts=1685971107728&1685971107728]]></Error><Creatives></Creatives></Wrapper></Ad></VAST>\",\"scenario_id\":971105412,\"dsp_campaign_id\":\"1\",\"dsp_creative_id\":\"1\",\"insertion_id\":1,\"placement_id\":2,\"portfolio_item_id\":971104812}],\"wigoEnabled\":false,\"placementMetadata\":{\"2\":{\"adCallTrackingUrl\":\"https://localhost:18281/track?action=adCall&pid=2&pageId=2&auctid=39312703-e970-4914-ae56-8e7d7d1fd16b__b6321d41-3840-4cb3-baad-b6fc5b0c8553__c0f2e6ba-63d0-4e20-ab41-fe0822eb65a6&vid=708ca808-ec55-4d97-ab81-9c4777e16058&hb_provider=prebid-server&hb_ad_unit_code=742d38c4-7994-4c2b-ac82-18d3a64ba3c7&env=thirdparty-inapp&gtc=1&gdpr_apply=false&gac=1&gap=1&ca=false&bsg=uncat&bsias=uncat&pfid=971104812&gid=1&brid=0&cid=1&rpm_reason=3&ut=1&p=5fwoPMJCquIB-txdmwQS0l79-hhHVnlTzyR9mmnBMtZRceP6-q31KzCfLpS8WTNaw_sXr-hkOFBxaxa-jyLblbVc&cts=1685971107773&cs=267268361555465193905\",\"auctionId\":\"39312703-e970-4914-ae56-8e7d7d1fd16b__b6321d41-3840-4cb3-baad-b6fc5b0c8553__c0f2e6ba-63d0-4e20-ab41-fe0822eb65a6\"}},\"viewerId\":\"708ca808-ec55-4d97-ab81-9c4777e16058\"}",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-response.json
index a312ae577d4..7630f73ad2a 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/tgm/test-auction-tgm-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/theadx/test-auction-theadx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/theadx/test-auction-theadx-response.json
index 6f93c6f15b8..023554088ff 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/theadx/test-auction-theadx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/theadx/test-auction-theadx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 4.7,
           "adm": "adm6",
           "crid": "crid6",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-response.json b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-response.json
index 9675846a5ce..1bc78519e0b 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/thetradedesk/test-auction-thetradedesk-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/thirtythreeacross/test-auction-thirtythreeacross-response.json b/src/test/resources/org/prebid/server/it/openrtb2/thirtythreeacross/test-auction-thirtythreeacross-response.json
index 89ac6871c53..e83af540d6d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/thirtythreeacross/test-auction-thirtythreeacross-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/thirtythreeacross/test-auction-thirtythreeacross-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tpmn/test-auction-tpmn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tpmn/test-auction-tpmn-response.json
index 8003f651a5a..96eba6c8ffc 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/tpmn/test-auction-tpmn-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/tpmn/test-auction-tpmn-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "mtype": 1,
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-auction-tradplus-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-auction-tradplus-response.json
index 9a101b0e81e..cf2087e1fab 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-auction-tradplus-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/tradplus/test-auction-tradplus-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "mtype": 1,
           "adm": "adm001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/trafficgate/test-auction-trafficgate-response.json b/src/test/resources/org/prebid/server/it/openrtb2/trafficgate/test-auction-trafficgate-response.json
index 442da0d776f..d6161b71c26 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/trafficgate/test-auction-trafficgate-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/trafficgate/test-auction-trafficgate-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tredio/test-auction-tredio-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tredio/test-auction-tredio-response.json
index f3a13fa3964..8bfb5d60490 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/tredio/test-auction-tredio-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/tredio/test-auction-tredio-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json
index e6bfa8cab8c..64ac2be961d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-request.json
@@ -16,8 +16,6 @@
   ],
   "tmax": 5000,
   "regs": {
-    "ext": {
-      "gdpr": 0
-    }
+    "gdpr": 0
   }
 }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-response.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-response.json
index e57fe1c9cbf..db4cc84b585 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-auction-triplelift-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "some-test-ad",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json
index 6a4c678e208..35eb63e37ce 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/triplelift/test-triplelift-bid-request.json
@@ -40,9 +40,7 @@
     "USD"
   ],
   "regs": {
-    "ext": {
-      "gdpr": 0
-    }
+    "gdpr": 0
   },
   "ext": {
     "prebid": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json
index 64a8c1dc3b5..f49127f75b1 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-request.json
@@ -24,8 +24,6 @@
   },
   "tmax": 5000,
   "regs": {
-    "ext": {
-      "gdpr": 0
-    }
+    "gdpr": 0
   }
 }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json
index 0c61803d3c3..e1b8d7476fa 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-auction-triplelift-native-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "{\"assets\":[{\"id\":0,\"title\":{\"text\":\"This is an example Prebid Native creative\"}}]}",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json
index a738eb40e83..d1199a5a30b 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/tripleliftnative/test-triplelift-native-bid-request.json
@@ -40,9 +40,7 @@
     "USD"
   ],
   "regs": {
-    "ext": {
-      "gdpr": 0
-    }
+    "gdpr": 0
   },
   "ext": {
     "prebid": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/trustedstack/test-auction-trustedstack-response.json b/src/test/resources/org/prebid/server/it/openrtb2/trustedstack/test-auction-trustedstack-response.json
index 8c8467b76bc..d9c328fcd58 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/trustedstack/test-auction-trustedstack-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/trustedstack/test-auction-trustedstack-response.json
@@ -6,6 +6,7 @@
         {
           "id": "randomid",
           "impid": "test-imp-id",
+          "exp": 300,
           "price": 0.5,
           "adm": "some-test-ad",
           "adid": "12345678",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-response.json
index 50c30722255..e8c5b12f12d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/ttx/test-auction-ttx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-response.json b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-response.json
index cb78783eafb..6fa5ba63fb0 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/ucfunnel/test-auction-ucfunnel-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/undertone/test-auction-undertone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/undertone/test-auction-undertone-response.json
index 9b5a2b41cfa..2d3ab23f164 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/undertone/test-auction-undertone-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/undertone/test-auction-undertone-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-auction-unicorn-response.json b/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-auction-unicorn-response.json
index abe21f3cada..1e5d00d5b89 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-auction-unicorn-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/unicorn/test-auction-unicorn-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json
index 984635a423f..c11663099d3 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-request.json
@@ -19,8 +19,6 @@
   ],
   "tmax": 5000,
   "regs": {
-    "ext": {
-      "gdpr": 0
-    }
+    "gdpr": 0
   }
 }
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-response.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-response.json
index 22ac518e357..94153ee8cdc 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-auction-unruly-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request.json b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request.json
index 21763c065aa..a71e9988f62 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/unruly/test-unruly-bid-request.json
@@ -41,9 +41,7 @@
     "USD"
   ],
   "regs": {
-    "ext": {
-      "gdpr": 0
-    }
+    "gdpr": 0
   },
   "ext": {
     "prebid": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vidazoo/test-auction-vidazoo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vidazoo/test-auction-vidazoo-response.json
index 351d4ebee68..57c24081c54 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/vidazoo/test-auction-vidazoo-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/vidazoo/test-auction-vidazoo-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.01,
           "adid": "2068416",
           "cid": "8048",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/videobyte/test-auction-videobyte-response.json b/src/test/resources/org/prebid/server/it/openrtb2/videobyte/test-auction-videobyte-response.json
index 20e3a8e09ef..705f5bf376b 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/videobyte/test-auction-videobyte-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/videobyte/test-auction-videobyte-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/videoheroes/test-auction-videoheroes-response.json b/src/test/resources/org/prebid/server/it/openrtb2/videoheroes/test-auction-videoheroes-response.json
index 0bb468d689c..4b345c3aef3 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/videoheroes/test-auction-videoheroes-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/videoheroes/test-auction-videoheroes-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vidoomy/test-auction-vidoomy-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vidoomy/test-auction-vidoomy-response.json
index 498535b49a0..45012edd4c0 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/vidoomy/test-auction-vidoomy-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/vidoomy/test-auction-vidoomy-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-auction-vimayx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-auction-vimayx-response.json
index 64843a7c2f7..0e17226a62c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-auction-vimayx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/vimayx/test-auction-vimayx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/visiblemeasures/test-auction-visiblemeasures-response.json b/src/test/resources/org/prebid/server/it/openrtb2/visiblemeasures/test-auction-visiblemeasures-response.json
index b04fa5e71df..141b8f5959a 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/visiblemeasures/test-auction-visiblemeasures-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/visiblemeasures/test-auction-visiblemeasures-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-response.json b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-response.json
index 8e8a1541709..6f9563edecd 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/visx/test-auction-visx-response.json
@@ -6,6 +6,7 @@
         {
           "id": "request_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 0.5,
           "adm": "some-test-ad",
           "adomain": [
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vox/test-auction-vox-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vox/test-auction-vox-response.json
index 97f9477d648..b7dfff7b29f 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/vox/test-auction-vox-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/vox/test-auction-vox-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.1415,
           "adm": "<html><h1>Hi, there</h1></html>",
           "crid": "24080",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-response.json
index b4116a339c7..554ace08a73 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/vrtcal/test-auction-vrtcal-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-auction-vungle-response.json b/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-auction-vungle-response.json
index fa3a8579909..2226f9e6b7f 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-auction-vungle-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/vungle/test-auction-vungle-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 1500,
           "price": 3.33,
           "adid": "adid001",
           "adm": "<VAST version=\"3.0\"></VAST>",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/xeworks/test-auction-xeworks-response.json b/src/test/resources/org/prebid/server/it/openrtb2/xeworks/test-auction-xeworks-response.json
index f4785cf5aa0..b3ece494e63 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/xeworks/test-auction-xeworks-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/xeworks/test-auction-xeworks-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 11.393,
           "adm": "some adm value",
           "adomain": [
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/xtrmqb/test-auction-xtrmqb-response.json b/src/test/resources/org/prebid/server/it/openrtb2/xtrmqb/test-auction-xtrmqb-response.json
index 44cecb751e8..961f3c98437 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/xtrmqb/test-auction-xtrmqb-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/xtrmqb/test-auction-xtrmqb-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "crid": "creativeId",
           "ext": {
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-auction-yahooads-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-auction-yahooads-response.json
index 2e1153776b4..2ef360e66b4 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-auction-yahooads-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/yahooads/test-auction-yahooads-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yandex/test-auction-yandex-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yandex/test-auction-yandex-response.json
index b5caee202db..50db4b9296f 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/yandex/test-auction-yandex-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/yandex/test-auction-yandex-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 1.25,
           "adm": "adm001",
           "crid": "crid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-response.json
index 4b393d47bff..4bc553b800e 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/yeahmobi/test-auction-yeahmobi-response.json
@@ -6,6 +6,7 @@
         {
           "id": "8400d766-58b3-47d4-80d7-6658b337d403",
           "impid": "test-imp-id",
+          "exp": 300,
           "adm": "{\"ver\":\"1.2\"}",
           "price": 1.2,
           "cid": "1",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yearxero/test-auction-yearxero-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yearxero/test-auction-yearxero-response.json
index f4d86020f74..f17d2e27d7d 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/yearxero/test-auction-yearxero-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/yearxero/test-auction-yearxero-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid001",
           "impid": "impId001",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json
index 4c7d6e6623e..52e84781542 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldlab/test-auction-yieldlab-response.json
@@ -6,6 +6,7 @@
         {
           "id": "12345",
           "impid": "imp_id",
+          "exp": 300,
           "price": 2.29,
           "crid": "",
           "dealid": "1234",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-response.json
index 15089c43484..7971beb1204 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldmo/test-auction-yieldmo-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-response.json b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-response.json
index 791bfa7a4de..e2bca209c2c 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/yieldone/test-auction-yieldone-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json
index 5fea726a00a..3fc40dda751 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/zeroclickfraud/test-auction-zeroclickfraud-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 7.77,
           "adm": "adm001",
           "adid": "adid",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-response.json
index 802e057bc7a..e1de127c348 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/zeta_global_ssp/test-auction-zeta_global_ssp-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid001",
           "impid": "impId001",
+          "exp": 300,
           "price": 3.33,
           "adm": "adm001",
           "adid": "adid001",
diff --git a/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-auction-zmaticoo-response.json b/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-auction-zmaticoo-response.json
index f2bb11ef448..ab36fa541e6 100644
--- a/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-auction-zmaticoo-response.json
+++ b/src/test/resources/org/prebid/server/it/openrtb2/zmaticoo/test-auction-zmaticoo-response.json
@@ -6,6 +6,7 @@
         {
           "id": "bid_id",
           "impid": "imp_id",
+          "exp": 300,
           "price": 3.33,
           "mtype": 1,
           "adm": "adm001",
diff --git a/src/test/resources/org/prebid/server/it/test-app-settings.yaml b/src/test/resources/org/prebid/server/it/test-app-settings.yaml
index 786b376ffed..c377f8ad3ce 100644
--- a/src/test/resources/org/prebid/server/it/test-app-settings.yaml
+++ b/src/test/resources/org/prebid/server/it/test-app-settings.yaml
@@ -60,6 +60,12 @@ accounts:
                     hook-sequence:
                       - module-code: sample-it-module
                         hook-impl-code: auction-response
+              exitpoint:
+                groups:
+                  - timeout: 5
+                    hook-sequence:
+                      - module-code: sample-it-module
+                        hook-impl-code: exitpoint
   - id: 7001
     hooks:
       execution-plan:
@@ -120,6 +126,18 @@ accounts:
                     hook-sequence:
                       - module-code: sample-it-module
                         hook-impl-code: rejecting-processed-bidder-response
+  - id: 13001
+    hooks:
+      execution-plan:
+        endpoints:
+          /openrtb2/auction:
+            stages:
+              exitpoint:
+                groups:
+                  - timeout: 5
+                    hook-sequence:
+                      - module-code: sample-it-module
+                        hook-impl-code: exitpoint
   - id: 12001
     auction:
       price-floors:
diff --git a/src/test/resources/org/prebid/server/it/test-application.properties b/src/test/resources/org/prebid/server/it/test-application.properties
index 485e32c5092..2e115d00348 100644
--- a/src/test/resources/org/prebid/server/it/test-application.properties
+++ b/src/test/resources/org/prebid/server/it/test-application.properties
@@ -514,6 +514,8 @@ adapters.vox.enabled=true
 adapters.vox.endpoint=http://localhost:8090/vox-exchange
 adapters.inmobi.enabled=true
 adapters.inmobi.endpoint=http://localhost:8090/inmobi-exchange
+adapters.insticator.enabled=true
+adapters.insticator.endpoint=http://localhost:8090/insticator-exchange
 adapters.interactiveoffers.enabled=true
 adapters.interactiveoffers.endpoint=http://localhost:8090/interactiveoffers-exchange
 adapters.invibes.enabled=true