Repository: prebid/prebid-server
Pull request #2678 (2023-04-03 16:44:20)

Upgrade Bid Adjustments
Addresses this feature: https://github.com/prebid/prebid-server/issues/2085

The goal of this feature is to add support for bid adjustments that works differently and is more complex than the existing `bidadjustmentfactors`. 

This feature allows `bidadjustments` to be defined on the request at the path `ext.prebid.bidadjustments`. Within this definition, `bidadjustments` will specify the mediatype, the bidder name, the dealID, and finally an adjustment array.

This adjustment array will contain information that'll be used to the adjust the bid price of a particular bid that matches the given mediatype, bidder name, and dealID. 

An entry in the adjustment array will include the following information: an `adjtype`, a `value`, and sometimes a `currency`

`Adjtype` refers to the type of adjustment that'll be occurring. A `mulitplier` adjtype will result in the bid price being multiplied by the given value. A `static` adjtype will result in the bid price being overwritten by the given value (after currency conversions). A `cpm` adjtype, will result in the given value being subtracted from the bid price.

VeronikaSolovei9: About `bidPrice = bidPrice - convertedVal`
Round the final number to 4 decimal digits first and then check if it's `if bidPrice <= 0 {`. 
In my example result after subtraction is `0.000009999999999621423` which rounds to `0.0000` and it leads to bid rejection and this message in debug logs: 

```json
"errors": {
            "appnexus": [
                {
                    "code": 999,
                    "message": "Bid \"9109836616608581859\" does not contain positive 'price' which is required since there is no deal set for this bid"
                }
            ]
        },
```

Also `roundTo := math.Pow(10, amountOfDecimalPlaces)` looks like a good candidate to be a constant: 
`const roundTo = 10000`

Here is my final end of this function:

```go
...
rounded := math.Round(bidPrice*roundTo) / roundTo // Returns Bid Price rounded to 4 decimal places

if rounded <= 0 {
	return originalBidPrice
}
return rounded
```


VeronikaSolovei9: Do you think it makes sense to move all new functions related to bid adjustments from bidder.go and exchange.go to a separate file in `exchange` directory? 
AlexBVolcy: @VeronikaSolovei9 just pushed a commit that moves adjustment functions that were present in bidder.go and exchange.go into it's own file in exchange directory. Also moved tests tied to those functions into their own file as well. I also updated the rounding logic!
AlexBVolcy: Getting this failure on the new test file I added:

exchange/bidadjustments_test.go:6:2: no required module provides package github.com/prebid/openrtb/v17/openrtb2; to add it: go get github.com/prebid/openrtb/v17/openrtb2

Just want to highlight this for tomorrow's team time
VeronikaSolovei9: It feels like you need to merge your branch with latest master. Even if there are no conflicts. 
There are should not be any usages of `github.com/prebid/openrtb/v17/openrtb2`
VeronikaSolovei9: Use `"github.com/prebid/openrtb/v19/openrtb2"` in bidadjustments_test.go
VeronikaSolovei9: Thank you for addressing comments! Changes look good :)

Patch:
diff --git a/adapters/pubmatic/pubmatic.go b/adapters/pubmatic/pubmatic.go
index 4b4fbff7..8084b2bc 100644
--- a/adapters/pubmatic/pubmatic.go
+++ b/adapters/pubmatic/pubmatic.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
+	"math"
 	"net/http"
 	"strconv"
 	"strings"
@@ -283,8 +284,8 @@ func parseImpressionObject(imp *openrtb2.Imp, extractWrapperExtFromImp, extractP
 	if pubmaticExt.Kadfloor != "" {
 		bidfloor, err := strconv.ParseFloat(strings.TrimSpace(pubmaticExt.Kadfloor), 64)
 		if err == nil {
-			//do not overwrite existing value if kadfloor is invalid
-			imp.BidFloor = bidfloor
+			// In case of valid kadfloor, select maximum of original imp.bidfloor and kadfloor
+			imp.BidFloor = math.Max(bidfloor, imp.BidFloor)
 		}
 	}
 
diff --git a/bidadjustment/apply.go b/bidadjustment/apply.go
new file mode 100644
index 00000000..4fa3b737
--- /dev/null
+++ b/bidadjustment/apply.go
@@ -0,0 +1,94 @@
+package bidadjustment
+
+import (
+	"math"
+
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+const (
+	AdjustmentTypeCPM        = "cpm"
+	AdjustmentTypeMultiplier = "multiplier"
+	AdjustmentTypeStatic     = "static"
+	WildCard                 = "*"
+	Delimiter                = "|"
+)
+
+const maxNumOfCombos = 8
+const pricePrecision float64 = 10000 // Rounds to 4 Decimal Places
+const minBid = 0.1
+
+// Apply gets the highest priority adjustment slice given a map of rules, and applies those adjustments to a bid's price
+func Apply(rules map[string][]openrtb_ext.Adjustment, bidInfo *adapters.TypedBid, bidderName openrtb_ext.BidderName, currency string, reqInfo *adapters.ExtraRequestInfo, bidType string) (float64, string) {
+	adjustments := []openrtb_ext.Adjustment{}
+	if len(rules) > 0 {
+		adjustments = get(rules, bidType, string(bidderName), bidInfo.Bid.DealID)
+	} else {
+		return bidInfo.Bid.Price, currency
+	}
+	adjustedPrice, adjustedCurrency := apply(adjustments, bidInfo.Bid.Price, currency, reqInfo)
+
+	if bidInfo.Bid.DealID != "" && adjustedPrice < 0 {
+		return 0, currency
+	}
+	if bidInfo.Bid.DealID == "" && adjustedPrice <= 0 {
+		return minBid, currency
+	}
+	return adjustedPrice, adjustedCurrency
+}
+
+func apply(adjustments []openrtb_ext.Adjustment, bidPrice float64, currency string, reqInfo *adapters.ExtraRequestInfo) (float64, string) {
+	if len(adjustments) == 0 {
+		return bidPrice, currency
+	}
+	originalBidPrice := bidPrice
+
+	for _, adjustment := range adjustments {
+		switch adjustment.Type {
+		case AdjustmentTypeMultiplier:
+			bidPrice = bidPrice * adjustment.Value
+		case AdjustmentTypeCPM:
+			convertedVal, err := reqInfo.ConvertCurrency(adjustment.Value, adjustment.Currency, currency)
+			if err != nil {
+				return originalBidPrice, currency
+			}
+			bidPrice = bidPrice - convertedVal
+		case AdjustmentTypeStatic:
+			bidPrice = adjustment.Value
+			currency = adjustment.Currency
+		}
+	}
+	roundedBidPrice := math.Round(bidPrice*pricePrecision) / pricePrecision
+
+	return roundedBidPrice, currency
+}
+
+// get() should return the highest priority slice of adjustments from the map that we can match with the given bid info
+// given the bid info, we create the same format of combinations that's present in the key of the ruleToAdjustments map
+// the slice is ordered by priority from highest to lowest, as soon as we find a match, we return that slice
+func get(rules map[string][]openrtb_ext.Adjustment, bidType, bidderName, dealID string) []openrtb_ext.Adjustment {
+	priorityRules := [maxNumOfCombos]string{}
+	if dealID != "" {
+		priorityRules[0] = bidType + Delimiter + bidderName + Delimiter + dealID
+		priorityRules[1] = bidType + Delimiter + bidderName + Delimiter + WildCard
+		priorityRules[2] = bidType + Delimiter + WildCard + Delimiter + dealID
+		priorityRules[3] = WildCard + Delimiter + bidderName + Delimiter + dealID
+		priorityRules[4] = bidType + Delimiter + WildCard + Delimiter + WildCard
+		priorityRules[5] = WildCard + Delimiter + bidderName + Delimiter + WildCard
+		priorityRules[6] = WildCard + Delimiter + WildCard + Delimiter + dealID
+		priorityRules[7] = WildCard + Delimiter + WildCard + Delimiter + WildCard
+	} else {
+		priorityRules[0] = bidType + Delimiter + bidderName + Delimiter + WildCard
+		priorityRules[1] = bidType + Delimiter + WildCard + Delimiter + WildCard
+		priorityRules[2] = WildCard + Delimiter + bidderName + Delimiter + WildCard
+		priorityRules[3] = WildCard + Delimiter + WildCard + Delimiter + WildCard
+	}
+
+	for _, rule := range priorityRules {
+		if _, ok := rules[rule]; ok {
+			return rules[rule]
+		}
+	}
+	return nil
+}
diff --git a/bidadjustment/build_rules.go b/bidadjustment/build_rules.go
new file mode 100644
index 00000000..bccb3bc8
--- /dev/null
+++ b/bidadjustment/build_rules.go
@@ -0,0 +1,103 @@
+package bidadjustment
+
+import (
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+const (
+	VideoInstream  = "video-instream"
+	VideoOutstream = "video-outstream"
+)
+
+// BuildRules() will populate the rules map with a rule that's a combination of the mediaType, bidderName, and dealId for a particular adjustment
+// The result will be a map that'll map a given rule with its adjustment
+func BuildRules(bidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments) map[string][]openrtb_ext.Adjustment {
+	if bidAdjustments == nil {
+		return nil
+	}
+	rules := make(map[string][]openrtb_ext.Adjustment)
+
+	buildRulesForMediaType(string(openrtb_ext.BidTypeBanner), bidAdjustments.MediaType.Banner, rules)
+	buildRulesForMediaType(string(openrtb_ext.BidTypeAudio), bidAdjustments.MediaType.Audio, rules)
+	buildRulesForMediaType(string(openrtb_ext.BidTypeNative), bidAdjustments.MediaType.Native, rules)
+	buildRulesForMediaType(VideoInstream, bidAdjustments.MediaType.VideoInstream, rules)
+	buildRulesForMediaType(VideoOutstream, bidAdjustments.MediaType.VideoOutstream, rules)
+	buildRulesForMediaType(WildCard, bidAdjustments.MediaType.WildCard, rules)
+
+	return rules
+}
+
+func buildRulesForMediaType(mediaType string, rulesByBidder map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID, rules map[string][]openrtb_ext.Adjustment) {
+	for bidderName := range rulesByBidder {
+		for dealID, adjustments := range rulesByBidder[bidderName] {
+			rule := mediaType + Delimiter + string(bidderName) + Delimiter + dealID
+			rules[rule] = adjustments
+		}
+	}
+}
+
+// Merge takes bid adjustments defined on the request and on the account, and combines/validates them, with the adjustments on the request taking precedence.
+func Merge(req *openrtb_ext.RequestWrapper, acctBidAdjs *openrtb_ext.ExtRequestPrebidBidAdjustments) (*openrtb_ext.ExtRequestPrebidBidAdjustments, error) {
+	mergedBidAdj, err := merge(req, acctBidAdjs)
+	if err != nil {
+		return nil, err
+	}
+	if !Validate(mergedBidAdj) {
+		mergedBidAdj = nil
+		err = &errortypes.Warning{
+			WarningCode: errortypes.BidAdjustmentWarningCode,
+			Message:     "bid adjustment on account was invalid",
+		}
+	}
+	return mergedBidAdj, err
+}
+
+func merge(req *openrtb_ext.RequestWrapper, acct *openrtb_ext.ExtRequestPrebidBidAdjustments) (*openrtb_ext.ExtRequestPrebidBidAdjustments, error) {
+	reqExt, err := req.GetRequestExt()
+	if err != nil {
+		return nil, err
+	}
+	extPrebid := reqExt.GetPrebid()
+
+	if extPrebid == nil && acct == nil {
+		return nil, nil
+	}
+	if extPrebid == nil && acct != nil {
+		return acct, nil
+	}
+	if extPrebid != nil && acct == nil {
+		return extPrebid.BidAdjustments, nil
+	}
+
+	extPrebid.BidAdjustments.MediaType.Banner = mergeForMediaType(extPrebid.BidAdjustments.MediaType.Banner, acct.MediaType.Banner)
+	extPrebid.BidAdjustments.MediaType.Native = mergeForMediaType(extPrebid.BidAdjustments.MediaType.Native, acct.MediaType.Native)
+	extPrebid.BidAdjustments.MediaType.Audio = mergeForMediaType(extPrebid.BidAdjustments.MediaType.Audio, acct.MediaType.Audio)
+	extPrebid.BidAdjustments.MediaType.VideoInstream = mergeForMediaType(extPrebid.BidAdjustments.MediaType.VideoInstream, acct.MediaType.VideoInstream)
+	extPrebid.BidAdjustments.MediaType.VideoOutstream = mergeForMediaType(extPrebid.BidAdjustments.MediaType.VideoOutstream, acct.MediaType.VideoOutstream)
+	extPrebid.BidAdjustments.MediaType.WildCard = mergeForMediaType(extPrebid.BidAdjustments.MediaType.WildCard, acct.MediaType.WildCard)
+
+	return extPrebid.BidAdjustments, nil
+}
+
+func mergeForMediaType(reqAdj, acctAdj map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID) map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID {
+	if reqAdj != nil && acctAdj == nil {
+		return reqAdj
+	}
+	if reqAdj == nil && acctAdj != nil {
+		return acctAdj
+	}
+
+	for bidderName, dealIDToAdjustments := range acctAdj {
+		if _, ok := reqAdj[bidderName]; ok {
+			for dealID, acctAdjustments := range acctAdj[bidderName] {
+				if _, ok := reqAdj[bidderName][dealID]; !ok {
+					reqAdj[bidderName][dealID] = acctAdjustments
+				}
+			}
+		} else {
+			reqAdj[bidderName] = dealIDToAdjustments
+		}
+	}
+	return reqAdj
+}
diff --git a/bidadjustment/validate.go b/bidadjustment/validate.go
new file mode 100644
index 00000000..c0ae3d4a
--- /dev/null
+++ b/bidadjustment/validate.go
@@ -0,0 +1,70 @@
+package bidadjustment
+
+import (
+	"math"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// Validate checks whether all provided bid adjustments are valid or not against the requirements defined in the issue
+func Validate(bidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments) bool {
+	if bidAdjustments == nil {
+		return true
+	}
+	if !validateForMediaType(bidAdjustments.MediaType.Banner) {
+		return false
+	}
+	if !validateForMediaType(bidAdjustments.MediaType.Audio) {
+		return false
+	}
+	if !validateForMediaType(bidAdjustments.MediaType.VideoInstream) {
+		return false
+	}
+	if !validateForMediaType(bidAdjustments.MediaType.VideoOutstream) {
+		return false
+	}
+	if !validateForMediaType(bidAdjustments.MediaType.Native) {
+		return false
+	}
+	if !validateForMediaType(bidAdjustments.MediaType.WildCard) {
+		return false
+	}
+	return true
+}
+
+func validateForMediaType(bidAdj map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID) bool {
+	for bidderName := range bidAdj {
+		if bidAdj[bidderName] == nil {
+			return false
+		}
+		for dealID := range bidAdj[bidderName] {
+			if bidAdj[bidderName][dealID] == nil {
+				return false
+			}
+			for _, adjustment := range bidAdj[bidderName][dealID] {
+				if !validateAdjustment(adjustment) {
+					return false
+				}
+			}
+		}
+	}
+	return true
+}
+
+func validateAdjustment(adjustment openrtb_ext.Adjustment) bool {
+	switch adjustment.Type {
+	case AdjustmentTypeCPM:
+		if adjustment.Currency != "" && adjustment.Value >= 0 && adjustment.Value < math.MaxFloat64 {
+			return true
+		}
+	case AdjustmentTypeMultiplier:
+		if adjustment.Value >= 0 && adjustment.Value < 100 {
+			return true
+		}
+	case AdjustmentTypeStatic:
+		if adjustment.Currency != "" && adjustment.Value >= 0 && adjustment.Value < math.MaxFloat64 {
+			return true
+		}
+	}
+	return false
+}
diff --git a/config/accounts.go b/config/accounts.go
index ff69762a..f96eb53a 100644
--- a/config/accounts.go
+++ b/config/accounts.go
@@ -23,22 +23,23 @@ const (
 
 // Account represents a publisher account configuration
 type Account struct {
-	ID                      string                               `mapstructure:"id" json:"id"`
-	Disabled                bool                                 `mapstructure:"disabled" json:"disabled"`
-	CacheTTL                DefaultTTLs                          `mapstructure:"cache_ttl" json:"cache_ttl"`
-	EventsEnabled           *bool                                `mapstructure:"events_enabled" json:"events_enabled"` // Deprecated: Use events.enabled instead.
-	CCPA                    AccountCCPA                          `mapstructure:"ccpa" json:"ccpa"`
-	GDPR                    AccountGDPR                          `mapstructure:"gdpr" json:"gdpr"`
-	DebugAllow              bool                                 `mapstructure:"debug_allow" json:"debug_allow"`
-	DefaultIntegration      string                               `mapstructure:"default_integration" json:"default_integration"`
-	CookieSync              CookieSync                           `mapstructure:"cookie_sync" json:"cookie_sync"`
-	Events                  Events                               `mapstructure:"events" json:"events"` // Don't enable this feature. It is still under developmment - https://github.com/prebid/prebid-server/issues/1725
-	TruncateTargetAttribute *int                                 `mapstructure:"truncate_target_attr" json:"truncate_target_attr"`
-	AlternateBidderCodes    *openrtb_ext.ExtAlternateBidderCodes `mapstructure:"alternatebiddercodes" json:"alternatebiddercodes"`
-	Hooks                   AccountHooks                         `mapstructure:"hooks" json:"hooks"`
-	PriceFloors             AccountPriceFloors                   `mapstructure:"price_floors" json:"price_floors"`
-	Validations             Validations                          `mapstructure:"validations" json:"validations"`
-	DefaultBidLimit         int                                  `mapstructure:"default_bid_limit" json:"default_bid_limit"`
+	ID                      string                                      `mapstructure:"id" json:"id"`
+	Disabled                bool                                        `mapstructure:"disabled" json:"disabled"`
+	CacheTTL                DefaultTTLs                                 `mapstructure:"cache_ttl" json:"cache_ttl"`
+	EventsEnabled           *bool                                       `mapstructure:"events_enabled" json:"events_enabled"` // Deprecated: Use events.enabled instead.
+	CCPA                    AccountCCPA                                 `mapstructure:"ccpa" json:"ccpa"`
+	GDPR                    AccountGDPR                                 `mapstructure:"gdpr" json:"gdpr"`
+	DebugAllow              bool                                        `mapstructure:"debug_allow" json:"debug_allow"`
+	DefaultIntegration      string                                      `mapstructure:"default_integration" json:"default_integration"`
+	CookieSync              CookieSync                                  `mapstructure:"cookie_sync" json:"cookie_sync"`
+	Events                  Events                                      `mapstructure:"events" json:"events"` // Don't enable this feature. It is still under developmment - https://github.com/prebid/prebid-server/issues/1725
+	TruncateTargetAttribute *int                                        `mapstructure:"truncate_target_attr" json:"truncate_target_attr"`
+	AlternateBidderCodes    *openrtb_ext.ExtAlternateBidderCodes        `mapstructure:"alternatebiddercodes" json:"alternatebiddercodes"`
+	Hooks                   AccountHooks                                `mapstructure:"hooks" json:"hooks"`
+	PriceFloors             AccountPriceFloors                          `mapstructure:"price_floors" json:"price_floors"`
+	Validations             Validations                                 `mapstructure:"validations" json:"validations"`
+	DefaultBidLimit         int                                         `mapstructure:"default_bid_limit" json:"default_bid_limit"`
+	BidAdjustments          *openrtb_ext.ExtRequestPrebidBidAdjustments `mapstructure:"bidadjustments" json:"bidadjustments"`
 }
 
 // CookieSync represents the account-level defaults for the cookie sync endpoint.
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 2811a9e7..7f361720 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -25,6 +25,7 @@ import (
 	"github.com/prebid/openrtb/v19/openrtb2"
 	"github.com/prebid/openrtb/v19/openrtb3"
 	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/bidadjustment"
 	"github.com/prebid/prebid-server/hooks"
 	"github.com/prebid/prebid-server/ortb"
 	"golang.org/x/net/publicsuffix"
@@ -723,7 +724,10 @@ func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper, isAmp
 	}
 
 	if errs := validateRequestExt(req); len(errs) != 0 {
-		return append(errL, errs...)
+		if errortypes.ContainsFatalError(errs) {
+			return append(errL, errs...)
+		}
+		errL = append(errL, errs...)
 	}
 
 	if err := deps.validateSite(req); err != nil {
@@ -1554,6 +1558,15 @@ func validateRequestExt(req *openrtb_ext.RequestWrapper) []error {
 		reqExt.SetPrebid(prebid)
 	}
 
+	if !bidadjustment.Validate(prebid.BidAdjustments) {
+		prebid.BidAdjustments = nil
+		reqExt.SetPrebid(prebid)
+		errs = append(errs, &errortypes.Warning{
+			WarningCode: errortypes.BidAdjustmentWarningCode,
+			Message:     "bid adjustment from request was invalid",
+		})
+	}
+
 	return errs
 }
 
diff --git a/endpoints/openrtb2/sample-requests/bidadjustments/cpm.json b/endpoints/openrtb2/sample-requests/bidadjustments/cpm.json
new file mode 100644
index 00000000..db0a06e6
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/bidadjustments/cpm.json
@@ -0,0 +1,105 @@
+{
+  "description": "Bid Adjustment Test with CPM, and WildCard Preference Testing",
+  "config": {
+    "mockBidders": [
+      {
+        "bidderName": "appnexus",
+        "currency": "USD",
+        "price": 20.00
+      }
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "tmax": 500,
+    "ext": {
+      "prebid": {
+        "currency": {
+          "rates": {
+            "EUR": {
+              "USD": 2.0
+            }
+          },
+          "usepbsrates": false
+        },
+        "bidadjustments": {
+          "mediatype": {
+            "banner": {
+              "appnexus": {
+                "*": [
+                  {
+                    "adjtype": "cpm",
+                    "value": 5.0,
+                    "currency": "EUR"
+                  }
+                ]
+              },
+              "*": {
+                "*": [
+                  {
+                    "adjtype": "multiplier",
+                    "value": 3.0
+                  }
+                ]
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id": "some-request-id",
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "some-impression-id",
+            "price": 10.0,
+            "ext": {
+              "origbidcpm": 20,
+              "origbidcur": "USD",
+              "prebid": {
+                "meta": {
+                  "adaptercode": "appnexus"
+                },
+                "type": "banner"
+              }
+            }
+          }
+        ],
+        "seat": "appnexus"
+      }
+    ],
+    "bidid": "test bid id",
+    "cur": "USD",
+    "nbr": 0
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/bidadjustments/invalid.json b/endpoints/openrtb2/sample-requests/bidadjustments/invalid.json
new file mode 100644
index 00000000..cbd5f758
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/bidadjustments/invalid.json
@@ -0,0 +1,90 @@
+{
+  "description": "Bid Adjustment Test With Invalid Adjustment",
+  "config": {
+    "mockBidders": [
+      {
+        "bidderName": "appnexus",
+        "currency": "USD",
+        "price": 2.00,
+        "dealid": "some-deal-id"
+      }
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "tmax": 500,
+    "ext": {
+      "prebid": {
+        "bidadjustments": {
+          "mediatype": {
+            "banner": {
+              "appnexus": {
+                "some-deal-id": [
+                  {
+                    "adjtype": "multiplier",
+                    "value": -2.0
+                  }
+                ]
+              }
+            }
+          }
+        },
+        "debug": true
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id": "some-request-id",
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "some-impression-id",
+            "price": 2.0,
+            "ext": {
+              "origbidcpm": 2,
+              "origbidcur": "USD",
+              "prebid": {
+                "meta": {
+                  "adaptercode": "appnexus"
+                },
+                "type": "banner"
+              }
+            }
+          }
+        ],
+        "seat": "appnexus"
+      }
+    ],
+    "bidid": "test bid id",
+    "cur": "USD",
+    "nbr": 0
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/bidadjustments/multiplier.json b/endpoints/openrtb2/sample-requests/bidadjustments/multiplier.json
new file mode 100644
index 00000000..72a8dca9
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/bidadjustments/multiplier.json
@@ -0,0 +1,89 @@
+{
+  "description": "Bid Adjustment Test With One Adjustment",
+  "config": {
+    "mockBidders": [
+      {
+        "bidderName": "appnexus",
+        "currency": "USD",
+        "price": 2.00,
+        "dealid": "some-deal-id"
+      }
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "tmax": 500,
+    "ext": {
+      "prebid": {
+        "bidadjustments": {
+          "mediatype": {
+            "banner": {
+              "appnexus": {
+                "some-deal-id": [
+                  {
+                    "adjtype": "multiplier",
+                    "value": 2.0
+                  }
+                ]
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id": "some-request-id",
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "some-impression-id",
+            "price": 4.0,
+            "ext": {
+              "origbidcpm": 2,
+              "origbidcur": "USD",
+              "prebid": {
+                "meta": {
+                  "adaptercode": "appnexus"
+                },
+                "type": "banner"
+              }
+            }
+          }
+        ],
+        "seat": "appnexus"
+      }
+    ],
+    "bidid": "test bid id",
+    "cur": "USD",
+    "nbr": 0
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/endpoints/openrtb2/sample-requests/bidadjustments/static.json b/endpoints/openrtb2/sample-requests/bidadjustments/static.json
new file mode 100644
index 00000000..9969c47e
--- /dev/null
+++ b/endpoints/openrtb2/sample-requests/bidadjustments/static.json
@@ -0,0 +1,106 @@
+{
+  "description": "Bid Adjustment with Static and WildCard testing",
+  "config": {
+    "mockBidders": [
+      {
+        "bidderName": "appnexus",
+        "currency": "USD",
+        "price": 2.00,
+        "dealid": "some-deal-id"
+      }
+    ]
+  },
+  "mockBidRequest": {
+    "id": "some-request-id",
+    "site": {
+      "page": "prebid.org"
+    },
+    "imp": [
+      {
+        "id": "some-impression-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "appnexus": {
+            "placementId": 12883451
+          }
+        }
+      }
+    ],
+    "tmax": 500,
+    "ext": {
+      "prebid": {
+        "currency": {
+          "rates": {
+            "EUR": {
+              "USD": 2.0
+            }
+          },
+          "usepbsrates": false
+        },
+        "bidadjustments": {
+          "mediatype": {
+            "banner": {
+              "*": {
+                "some-deal-id": [
+                  {
+                    "adjtype": "static",
+                    "value": 10.0,
+                    "currency": "EUR"
+                  }
+                ]
+              },
+              "some-bidder-name": {
+                "*": [
+                  {
+                    "adjtype": "multiplier",
+                    "value": 2.0
+                  }
+                ]
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "expectedBidResponse": {
+    "id": "some-request-id",
+    "seatbid": [
+      {
+        "bid": [
+          {
+            "id": "appnexus-bid",
+            "impid": "some-impression-id",
+            "price": 10.0,
+            "ext": {
+              "origbidcpm": 2,
+              "origbidcur": "USD",
+              "prebid": {
+                "meta": {
+                  "adaptercode": "appnexus"
+                },
+                "type": "banner"
+              }
+            }
+          }
+        ],
+        "seat": "appnexus"
+      }
+    ],
+    "bidid": "test bid id",
+    "cur": "EUR",
+    "nbr": 0
+  },
+  "expectedReturnCode": 200
+}
\ No newline at end of file
diff --git a/errortypes/code.go b/errortypes/code.go
index f81ee464..99afb02a 100644
--- a/errortypes/code.go
+++ b/errortypes/code.go
@@ -26,6 +26,7 @@ const (
 	AlternateBidderCodeWarningCode
 	MultiBidWarningCode
 	AdServerTargetingWarningCode
+	BidAdjustmentWarningCode
 )
 
 // Coder provides an error or warning code with severity.
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 8e6f133f..e355c18c 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -16,6 +16,7 @@ import (
 	"time"
 
 	"github.com/golang/glog"
+	"github.com/prebid/prebid-server/bidadjustment"
 	"github.com/prebid/prebid-server/config/util"
 	"github.com/prebid/prebid-server/currency"
 	"github.com/prebid/prebid-server/exchange/entities"
@@ -23,6 +24,7 @@ import (
 	"github.com/prebid/prebid-server/hooks/hookexecution"
 	"github.com/prebid/prebid-server/version"
 
+	"github.com/prebid/openrtb/v19/adcom1"
 	nativeRequests "github.com/prebid/openrtb/v19/native1/request"
 	nativeResponse "github.com/prebid/openrtb/v19/native1/response"
 	"github.com/prebid/openrtb/v19/openrtb2"
@@ -56,7 +58,7 @@ type AdaptedBidder interface {
 	//
 	// Any errors will be user-facing in the API.
 	// Error messages should help publishers understand what might account for "bad" bids.
-	requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor) ([]*entities.PbsOrtbSeatBid, []error)
+	requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, []error)
 }
 
 // bidRequestOptions holds additional options for bid request execution to maintain clean code and reasonable number of parameters
@@ -115,7 +117,7 @@ type bidderAdapterConfig struct {
 	EndpointCompression string
 }
 
-func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor) ([]*entities.PbsOrtbSeatBid, []error) {
+func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, []error) {
 	reject := hookExecutor.ExecuteBidderRequestStage(bidderRequest.BidRequest, string(bidderRequest.BidderName))
 	if reject != nil {
 		return nil, []error{reject}
@@ -335,9 +337,13 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde
 						}
 
 						originalBidCpm := 0.0
+						currencyAfterAdjustments := ""
 						if bidResponse.Bids[i].Bid != nil {
 							originalBidCpm = bidResponse.Bids[i].Bid.Price
 							bidResponse.Bids[i].Bid.Price = bidResponse.Bids[i].Bid.Price * adjustmentFactor * conversionRate
+
+							bidType := getBidTypeForAdjustments(bidResponse.Bids[i].BidType, bidResponse.Bids[i].Bid.ImpID, bidderRequest.BidRequest.Imp)
+							bidResponse.Bids[i].Bid.Price, currencyAfterAdjustments = bidadjustment.Apply(ruleToAdjustments, bidResponse.Bids[i], bidderRequest.BidderName, seatBidMap[bidderRequest.BidderName].Currency, reqInfo, bidType)
 						}
 
 						if _, ok := seatBidMap[bidderName]; !ok {
@@ -360,6 +366,7 @@ func (bidder *bidderAdapter) requestBid(ctx context.Context, bidderRequest Bidde
 							OriginalBidCPM: originalBidCpm,
 							OriginalBidCur: bidResponse.Currency,
 						})
+						seatBidMap[bidderName].Currency = currencyAfterAdjustments
 					}
 				} else {
 					// If no conversions found, do not handle the bid
@@ -687,3 +694,18 @@ func compressToGZIP(requestBody []byte) []byte {
 	w.Close()
 	return b.Bytes()
 }
+
+func getBidTypeForAdjustments(bidType openrtb_ext.BidType, impID string, imp []openrtb2.Imp) string {
+	if bidType == openrtb_ext.BidTypeVideo {
+		for _, imp := range imp {
+			if imp.ID == impID {
+				if imp.Video.Plcmt == adcom1.VideoPlcmtAccompanyingContent {
+					return "video-outstream"
+				}
+				break
+			}
+		}
+		return "video-instream"
+	}
+	return string(bidType)
+}
diff --git a/exchange/bidder_validate_bids.go b/exchange/bidder_validate_bids.go
index 44d44a35..8283a674 100644
--- a/exchange/bidder_validate_bids.go
+++ b/exchange/bidder_validate_bids.go
@@ -31,8 +31,8 @@ type validatedBidder struct {
 	bidder AdaptedBidder
 }
 
-func (v *validatedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor) ([]*entities.PbsOrtbSeatBid, []error) {
-	seatBids, errs := v.bidder.requestBid(ctx, bidderRequest, conversions, reqInfo, adsCertSigner, bidRequestOptions, alternateBidderCodes, hookExecutor)
+func (v *validatedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, hookExecutor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, []error) {
+	seatBids, errs := v.bidder.requestBid(ctx, bidderRequest, conversions, reqInfo, adsCertSigner, bidRequestOptions, alternateBidderCodes, hookExecutor, ruleToAdjustments)
 	for _, seatBid := range seatBids {
 		if validationErrors := removeInvalidBids(bidderRequest.BidRequest, seatBid); len(validationErrors) > 0 {
 			errs = append(errs, validationErrors...)
diff --git a/exchange/exchange.go b/exchange/exchange.go
index c3728180..7ce070eb 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -16,6 +16,7 @@ import (
 
 	"github.com/prebid/prebid-server/adapters"
 	"github.com/prebid/prebid-server/adservertargeting"
+	"github.com/prebid/prebid-server/bidadjustment"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/currency"
 	"github.com/prebid/prebid-server/errortypes"
@@ -317,6 +318,15 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 	bidderRequests, privacyLabels, errs := e.requestSplitter.cleanOpenRTBRequests(ctx, *r, requestExtLegacy, gdprDefaultValue)
 	errs = append(errs, floorErrs...)
 
+	mergedBidAdj, err := bidadjustment.Merge(r.BidRequestWrapper, r.Account.BidAdjustments)
+	if err != nil {
+		if errortypes.ContainsFatalError([]error{err}) {
+			return nil, err
+		}
+		errs = append(errs, err)
+	}
+	bidAdjustmentRules := bidadjustment.BuildRules(mergedBidAdj)
+
 	e.me.RecordRequestPrivacy(privacyLabels)
 
 	if len(r.StoredAuctionResponses) > 0 || len(r.StoredBidResponses) > 0 {
@@ -355,8 +365,7 @@ func (e *exchange) HoldAuction(ctx context.Context, r *AuctionRequest, debugLog
 		} else if r.Account.AlternateBidderCodes != nil {
 			alternateBidderCodes = *r.Account.AlternateBidderCodes
 		}
-
-		adapterBids, adapterExtra, fledge, anyBidsReturned = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride, alternateBidderCodes, requestExtLegacy.Prebid.Experiment, r.HookExecutor, r.StartTime)
+		adapterBids, adapterExtra, fledge, anyBidsReturned = e.getAllBids(auctionCtx, bidderRequests, bidAdjustmentFactors, conversions, accountDebugAllow, r.GlobalPrivacyControlHeader, debugLog.DebugOverride, alternateBidderCodes, requestExtLegacy.Prebid.Experiment, r.HookExecutor, r.StartTime, bidAdjustmentRules)
 		r.MakeBidsTimeInfo = buildMakeBidsTimeInfoMap(adapterExtra)
 	}
 
@@ -629,7 +638,8 @@ func (e *exchange) getAllBids(
 	alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes,
 	experiment *openrtb_ext.Experiment,
 	hookExecutor hookexecution.StageExecutor,
-	pbsRequestStartTime time.Time) (
+	pbsRequestStartTime time.Time,
+	bidAdjustmentRules map[string][]openrtb_ext.Adjustment) (
 	map[openrtb_ext.BidderName]*entities.PbsOrtbSeatBid,
 	map[openrtb_ext.BidderName]*seatResponseExtra,
 	*openrtb_ext.Fledge,
@@ -670,7 +680,7 @@ func (e *exchange) getAllBids(
 				addCallSignHeader:   isAdsCertEnabled(experiment, e.bidderInfo[string(bidderRequest.BidderName)]),
 				bidAdjustments:      bidAdjustments,
 			}
-			seatBids, err := e.adapterMap[bidderRequest.BidderCoreName].requestBid(ctx, bidderRequest, conversions, &reqInfo, e.adsCertSigner, bidReqOptions, alternateBidderCodes, hookExecutor)
+			seatBids, err := e.adapterMap[bidderRequest.BidderCoreName].requestBid(ctx, bidderRequest, conversions, &reqInfo, e.adsCertSigner, bidReqOptions, alternateBidderCodes, hookExecutor, bidAdjustmentRules)
 
 			// Add in time reporting
 			elapsed := time.Since(start)
diff --git a/openrtb_ext/request.go b/openrtb_ext/request.go
index 5e222ab7..a6c7d5dc 100644
--- a/openrtb_ext/request.go
+++ b/openrtb_ext/request.go
@@ -79,7 +79,8 @@ type ExtRequestPrebid struct {
 	// any other value or an empty string disables trace output at all.
 	Trace string `json:"trace,omitempty"`
 
-	AdServerTargeting []AdServerTarget `json:"adservertargeting,omitempty"`
+	BidAdjustments    *ExtRequestPrebidBidAdjustments `json:"bidadjustments,omitempty"`
+	AdServerTargeting []AdServerTarget                `json:"adservertargeting,omitempty"`
 }
 
 type AdServerTarget struct {
@@ -152,6 +153,32 @@ type ExtRequestPrebidCacheVAST struct {
 	ReturnCreative *bool `json:"returnCreative,omitempty"`
 }
 
+// ExtRequestPrebidBidAdjustments defines the contract for bidrequest.ext.prebid.bidadjustments
+type ExtRequestPrebidBidAdjustments struct {
+	MediaType MediaType `json:"mediatype,omitempty"`
+}
+
+// AdjustmentsByDealID maps a dealID to a slice of bid adjustments
+type AdjustmentsByDealID map[string][]Adjustment
+
+// MediaType defines contract for bidrequest.ext.prebid.bidadjustments.mediatype
+// BidderName will map to a DealID that will map to a slice of bid adjustments
+type MediaType struct {
+	Banner         map[BidderName]AdjustmentsByDealID `json:"banner,omitempty"`
+	VideoInstream  map[BidderName]AdjustmentsByDealID `json:"video-instream,omitempty"`
+	VideoOutstream map[BidderName]AdjustmentsByDealID `json:"video-outstream,omitempty"`
+	Audio          map[BidderName]AdjustmentsByDealID `json:"audio,omitempty"`
+	Native         map[BidderName]AdjustmentsByDealID `json:"native,omitempty"`
+	WildCard       map[BidderName]AdjustmentsByDealID `json:"*,omitempty"`
+}
+
+// Adjustment defines the object that will be present in the slice of bid adjustments found from MediaType map
+type Adjustment struct {
+	Type     string  `json:"adjtype,omitempty"`
+	Value    float64 `json:"value,omitempty"`
+	Currency string  `json:"currency,omitempty"`
+}
+
 // ExtRequestTargeting defines the contract for bidrequest.ext.prebid.targeting
 type ExtRequestTargeting struct {
 	PriceGranularity     *PriceGranularity        `json:"pricegranularity,omitempty"`


Test patch:
diff --git a/adapters/pubmatic/pubmatic_test.go b/adapters/pubmatic/pubmatic_test.go
index 57914228..7553519f 100644
--- a/adapters/pubmatic/pubmatic_test.go
+++ b/adapters/pubmatic/pubmatic_test.go
@@ -122,7 +122,7 @@ func TestParseImpressionObject(t *testing.T) {
 			expectedBidfloor: 0.12,
 		},
 		{
-			name: "imp.bidfloor set and kadfloor set, preference to kadfloor",
+			name: "imp.bidfloor set and kadfloor set, higher imp.bidfloor",
 			args: args{
 				imp: &openrtb2.Imp{
 					BidFloor: 0.12,
@@ -130,7 +130,18 @@ func TestParseImpressionObject(t *testing.T) {
 					Ext:      json.RawMessage(`{"bidder":{"kadfloor":"0.11"}}`),
 				},
 			},
-			expectedBidfloor: 0.11,
+			expectedBidfloor: 0.12,
+		},
+		{
+			name: "imp.bidfloor set and kadfloor set, higher kadfloor",
+			args: args{
+				imp: &openrtb2.Imp{
+					BidFloor: 0.12,
+					Video:    &openrtb2.Video{},
+					Ext:      json.RawMessage(`{"bidder":{"kadfloor":"0.13"}}`),
+				},
+			},
+			expectedBidfloor: 0.13,
 		},
 		{
 			name: "kadfloor string set with whitespace",
diff --git a/bidadjustment/apply_test.go b/bidadjustment/apply_test.go
new file mode 100644
index 00000000..27daa9c7
--- /dev/null
+++ b/bidadjustment/apply_test.go
@@ -0,0 +1,532 @@
+package bidadjustment
+
+import (
+	"testing"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+)
+
+func TestGetAndApply(t *testing.T) {
+	var (
+		adjCur string = "EUR"
+		bidCur string = "USA"
+	)
+
+	testCases := []struct {
+		name                   string
+		givenRuleToAdjustments map[string][]openrtb_ext.Adjustment
+		givenBidderName        openrtb_ext.BidderName
+		givenBidInfo           *adapters.TypedBid
+		givenBidType           string
+		setMock                func(m *mock.Mock)
+		expectedBidPrice       float64
+		expectedCurrency       string
+	}{
+		{
+			name: "CpmAdjustment",
+			givenBidInfo: &adapters.TypedBid{
+				Bid: &openrtb2.Bid{
+					Price:  10.0,
+					DealID: "dealId",
+				},
+			},
+			givenBidType: string(openrtb_ext.BidTypeBanner),
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"banner|bidderA|dealId": {
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    1.0,
+						Currency: adjCur,
+					},
+				},
+				"banner|bidderA|*": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 2.0,
+					},
+				},
+			},
+			givenBidderName:  "bidderA",
+			setMock:          func(m *mock.Mock) { m.On("GetRate", adjCur, bidCur).Return(2.5, nil) },
+			expectedBidPrice: 7.5,
+			expectedCurrency: bidCur,
+		},
+		{
+			name: "StaticAdjustment",
+			givenBidInfo: &adapters.TypedBid{
+				Bid: &openrtb2.Bid{
+					Price:  10.0,
+					DealID: "dealId",
+				},
+			},
+			givenBidType: string(openrtb_ext.BidTypeBanner),
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"*|bidderA|dealId": {
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    1.0,
+						Currency: adjCur,
+					},
+				},
+				"banner|bidderA|*": {
+					{
+						Type:     AdjustmentTypeStatic,
+						Value:    2.0,
+						Currency: adjCur,
+					},
+				},
+			},
+			givenBidderName:  "bidderA",
+			setMock:          nil,
+			expectedBidPrice: 2.0,
+			expectedCurrency: adjCur,
+		},
+		{
+			name: "MultiplierAdjustment",
+			givenBidInfo: &adapters.TypedBid{
+				Bid: &openrtb2.Bid{
+					Price:  10.0,
+					DealID: "dealId",
+				},
+			},
+			givenBidType: VideoInstream,
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"*|*|dealId": {
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    1.0,
+						Currency: adjCur,
+					},
+				},
+				"video-instream|*|*": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 2.0,
+					},
+				},
+			},
+			givenBidderName:  "bidderA",
+			setMock:          nil,
+			expectedBidPrice: 20.0,
+			expectedCurrency: bidCur,
+		},
+		{
+			name: "CpmAndMultiplierAdjustments",
+			givenBidInfo: &adapters.TypedBid{
+				Bid: &openrtb2.Bid{
+					Price:  10.0,
+					DealID: "dealId",
+				},
+			},
+			givenBidType: VideoInstream,
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"video-instream|*|*": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 2.0,
+					},
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    1.0,
+						Currency: adjCur,
+					},
+				},
+			},
+			givenBidderName:  "bidderA",
+			setMock:          func(m *mock.Mock) { m.On("GetRate", adjCur, bidCur).Return(2.5, nil) },
+			expectedBidPrice: 17.5,
+			expectedCurrency: bidCur,
+		},
+		{
+			name: "DealIdPresentAndNegativeAdjustedPrice",
+			givenBidInfo: &adapters.TypedBid{
+				Bid: &openrtb2.Bid{
+					Price:  1.0,
+					DealID: "dealId",
+				},
+			},
+			givenBidType: VideoInstream,
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"*|*|dealId": {
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    1.0,
+						Currency: adjCur,
+					},
+				},
+			},
+			givenBidderName:  "bidderA",
+			setMock:          func(m *mock.Mock) { m.On("GetRate", adjCur, bidCur).Return(2.5, nil) },
+			expectedBidPrice: 0.0,
+			expectedCurrency: bidCur,
+		},
+		{
+			name: "NoDealIdNegativeAdjustedPrice",
+			givenBidInfo: &adapters.TypedBid{
+				Bid: &openrtb2.Bid{
+					Price:  1.0,
+					DealID: "",
+				},
+			},
+			givenBidType: string(openrtb_ext.BidTypeAudio),
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"*|*|*": {
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    1.0,
+						Currency: adjCur,
+					},
+				},
+			},
+			givenBidderName:  "bidderA",
+			setMock:          func(m *mock.Mock) { m.On("GetRate", adjCur, bidCur).Return(2.5, nil) },
+			expectedBidPrice: 0.1,
+			expectedCurrency: bidCur,
+		},
+		{
+			name: "NilMap",
+			givenBidInfo: &adapters.TypedBid{
+				Bid: &openrtb2.Bid{
+					Price:  10.0,
+					DealID: "dealId",
+				},
+			},
+			givenBidType:           string(openrtb_ext.BidTypeBanner),
+			givenRuleToAdjustments: nil,
+			givenBidderName:        "bidderA",
+			setMock:                nil,
+			expectedBidPrice:       10.0,
+			expectedCurrency:       bidCur,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			reqInfo := adapters.ExtraRequestInfo{}
+			if test.setMock != nil {
+				mockConversions := &mockConversions{}
+				test.setMock(&mockConversions.Mock)
+				reqInfo = adapters.NewExtraRequestInfo(mockConversions)
+			}
+			bidPrice, currencyAfterAdjustment := Apply(test.givenRuleToAdjustments, test.givenBidInfo, test.givenBidderName, bidCur, &reqInfo, test.givenBidType)
+			assert.Equal(t, test.expectedBidPrice, bidPrice)
+			assert.Equal(t, test.expectedCurrency, currencyAfterAdjustment)
+		})
+	}
+}
+
+type mockConversions struct {
+	mock.Mock
+}
+
+func (m mockConversions) GetRate(from string, to string) (float64, error) {
+	args := m.Called(from, to)
+	return args.Get(0).(float64), args.Error(1)
+}
+
+func (m mockConversions) GetRates() *map[string]map[string]float64 {
+	args := m.Called()
+	return args.Get(0).(*map[string]map[string]float64)
+}
+
+func TestApply(t *testing.T) {
+	var (
+		adjCur string = "EUR"
+		bidCur string = "USA"
+	)
+
+	testCases := []struct {
+		name             string
+		givenAdjustments []openrtb_ext.Adjustment
+		setMock          func(m *mock.Mock)
+		givenBidPrice    float64
+		expectedBidPrice float64
+		expectedCurrency string
+	}{
+		{
+			name:             "CpmAdjustment",
+			givenAdjustments: []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 1.0, Currency: adjCur}},
+			givenBidPrice:    10.58687,
+			setMock:          func(m *mock.Mock) { m.On("GetRate", adjCur, bidCur).Return(2.5, nil) },
+			expectedBidPrice: 8.0869,
+			expectedCurrency: bidCur,
+		},
+		{
+			name:             "StaticAdjustment",
+			givenAdjustments: []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 4.0, Currency: adjCur}},
+			givenBidPrice:    10.0,
+			setMock:          nil,
+			expectedBidPrice: 4.0,
+			expectedCurrency: adjCur,
+		},
+		{
+			name:             "MultiplierAdjustment",
+			givenAdjustments: []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 3.0}},
+			givenBidPrice:    10.0,
+			setMock:          nil,
+			expectedBidPrice: 30.0,
+			expectedCurrency: bidCur,
+		},
+		{
+			name:             "NilAdjustment",
+			givenAdjustments: nil,
+			givenBidPrice:    10.0,
+			setMock:          nil,
+			expectedBidPrice: 10.0,
+			expectedCurrency: bidCur,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			reqInfo := adapters.ExtraRequestInfo{}
+			if test.setMock != nil {
+				mockConversions := &mockConversions{}
+				test.setMock(&mockConversions.Mock)
+				reqInfo = adapters.NewExtraRequestInfo(mockConversions)
+			}
+
+			bidPrice, currencyAfterAdjustment := apply(test.givenAdjustments, test.givenBidPrice, bidCur, &reqInfo)
+			assert.Equal(t, test.expectedBidPrice, bidPrice)
+			assert.Equal(t, test.expectedCurrency, currencyAfterAdjustment)
+		})
+	}
+}
+
+func TestGet(t *testing.T) {
+	testCases := []struct {
+		name                   string
+		givenRuleToAdjustments map[string][]openrtb_ext.Adjustment
+		givenBidType           openrtb_ext.BidType
+		givenBidderName        openrtb_ext.BidderName
+		givenDealId            string
+		expected               []openrtb_ext.Adjustment
+	}{
+		{
+			name: "Priority1",
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"banner|bidderA|dealId": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+				"banner|bidderA|*": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 2.0,
+					},
+				},
+			},
+			givenBidType:    openrtb_ext.BidTypeBanner,
+			givenBidderName: "bidderA",
+			givenDealId:     "dealId",
+			expected:        []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+		},
+		{
+			name: "Priority2",
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"banner|bidderA|*": {
+					{
+						Type:  AdjustmentTypeStatic,
+						Value: 5.0,
+					},
+				},
+				"banner|*|dealId": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 2.0,
+					},
+				},
+			},
+			givenBidType:    openrtb_ext.BidTypeBanner,
+			givenBidderName: "bidderA",
+			givenDealId:     "dealId",
+			expected:        []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 5.0}},
+		},
+		{
+			name: "Priority3",
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"banner|*|dealId": {
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    3.0,
+						Currency: "USD",
+					},
+				},
+				"*|bidderA|dealId": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+			},
+			givenBidType:    openrtb_ext.BidTypeBanner,
+			givenBidderName: "bidderA",
+			givenDealId:     "dealId",
+			expected:        []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3.0, Currency: "USD"}},
+		},
+		{
+			name: "Priority4",
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"*|bidderA|dealId": {
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    3.0,
+						Currency: "USD",
+					},
+				},
+				"banner|*|*": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+			},
+			givenBidType:    openrtb_ext.BidTypeBanner,
+			givenBidderName: "bidderA",
+			givenDealId:     "dealId",
+			expected:        []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3.0, Currency: "USD"}},
+		},
+		{
+			name: "Priority5",
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"banner|*|*": {
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    3.0,
+						Currency: "USD",
+					},
+				},
+				"*|bidderA|*": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+			},
+			givenBidType:    openrtb_ext.BidTypeBanner,
+			givenBidderName: "bidderA",
+			givenDealId:     "dealId",
+			expected:        []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3.0, Currency: "USD"}},
+		},
+		{
+			name: "Priority6",
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"*|bidderA|*": {
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    3.0,
+						Currency: "USD",
+					},
+				},
+				"*|*|dealId": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+			},
+			givenBidType:    openrtb_ext.BidTypeBanner,
+			givenBidderName: "bidderA",
+			givenDealId:     "dealId",
+			expected:        []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3.0, Currency: "USD"}},
+		},
+		{
+			name: "Priority7",
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"*|*|dealId": {
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    3.0,
+						Currency: "USD",
+					},
+				},
+				"*|*|*": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+			},
+			givenBidType:    openrtb_ext.BidTypeBanner,
+			givenBidderName: "bidderA",
+			givenDealId:     "dealId",
+			expected:        []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3.0, Currency: "USD"}},
+		},
+		{
+			name: "Priority8",
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"*|*|*": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+				"banner|bidderA|dealId": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+			},
+			givenBidType:    openrtb_ext.BidTypeBanner,
+			givenBidderName: "bidderB",
+			givenDealId:     "dealId",
+			expected:        []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+		},
+		{
+			name: "NoDealID",
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"banner|bidderA|*": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+				"banner|*|*": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+			},
+			givenBidType:    openrtb_ext.BidTypeBanner,
+			givenBidderName: "bidderA",
+			givenDealId:     "",
+			expected:        []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+		},
+		{
+			name: "NoPriorityRulesMatch",
+			givenRuleToAdjustments: map[string][]openrtb_ext.Adjustment{
+				"banner|bidderA|dealId": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+				"banner|bidderA|*": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 2.0,
+					},
+				},
+			},
+			givenBidType:    openrtb_ext.BidTypeVideo,
+			givenBidderName: "bidderB",
+			givenDealId:     "diffDealId",
+			expected:        nil,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			adjArray := get(test.givenRuleToAdjustments, string(test.givenBidType), string(test.givenBidderName), test.givenDealId)
+			assert.Equal(t, test.expected, adjArray)
+		})
+	}
+}
diff --git a/bidadjustment/build_rules_test.go b/bidadjustment/build_rules_test.go
new file mode 100644
index 00000000..263a7821
--- /dev/null
+++ b/bidadjustment/build_rules_test.go
@@ -0,0 +1,458 @@
+package bidadjustment
+
+import (
+	"testing"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBuildRules(t *testing.T) {
+	testCases := []struct {
+		name                string
+		givenBidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments
+		expectedRules       map[string][]openrtb_ext.Adjustment
+	}{
+		{
+			name: "OneAdjustment",
+			givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+						},
+					},
+				},
+			},
+			expectedRules: map[string][]openrtb_ext.Adjustment{
+				"banner|bidderA|dealId": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+			},
+		},
+		{
+			name: "MultipleAdjustments",
+			givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+						},
+						"*": {
+							"diffDealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 1.1, Currency: "USD"}},
+							"*":          []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 5.0, Currency: "USD"}},
+						},
+					},
+					VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"*": {
+							"*": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}, {Type: AdjustmentTypeCPM, Value: 0.18, Currency: "USD"}},
+						},
+					},
+					VideoOutstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderB": {
+							"*": []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 0.25, Currency: "USD"}},
+						},
+					},
+				},
+			},
+			expectedRules: map[string][]openrtb_ext.Adjustment{
+				"banner|bidderA|dealId": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+				},
+				"banner|*|diffDealId": {
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    1.1,
+						Currency: "USD",
+					},
+				},
+				"banner|*|*": {
+					{
+						Type:     AdjustmentTypeStatic,
+						Value:    5.0,
+						Currency: "USD",
+					},
+				},
+				"video-instream|*|*": {
+					{
+						Type:  AdjustmentTypeMultiplier,
+						Value: 1.1,
+					},
+					{
+						Type:     AdjustmentTypeCPM,
+						Value:    0.18,
+						Currency: "USD",
+					},
+				},
+				"video-outstream|bidderB|*": {
+					{
+						Type:     AdjustmentTypeStatic,
+						Value:    0.25,
+						Currency: "USD",
+					},
+				},
+			},
+		},
+		{
+			name:                "NilAdjustments",
+			givenBidAdjustments: nil,
+			expectedRules:       nil,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			rules := BuildRules(test.givenBidAdjustments)
+			assert.Equal(t, test.expectedRules, rules)
+		})
+	}
+}
+
+func TestMergeAndValidate(t *testing.T) {
+	testCases := []struct {
+		name                   string
+		givenRequestWrapper    *openrtb_ext.RequestWrapper
+		givenAccount           *config.Account
+		expectError            bool
+		expectedBidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments
+	}{
+		{
+			name: "ValidReqAndAcctAdjustments",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)},
+			},
+			givenAccount: &config.Account{
+				BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+					MediaType: openrtb_ext.MediaType{
+						Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+							"bidderB": {
+								"dealId": []openrtb_ext.Adjustment{{Type: "multiplier", Value: 1.5}},
+							},
+						},
+					},
+				},
+			},
+			expectError: false,
+			expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: "multiplier", Value: 1.1}},
+						},
+						"bidderB": {
+							"dealId": []openrtb_ext.Adjustment{{Type: "multiplier", Value: 1.5}},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "InvalidReqAdjustment",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 200}]}}}}}}`)},
+			},
+			givenAccount: &config.Account{
+				BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+					MediaType: openrtb_ext.MediaType{
+						Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+							"bidderB": {
+								"dealId": []openrtb_ext.Adjustment{{Type: "multiplier", Value: 1.5}},
+							},
+						},
+					},
+				},
+			},
+			expectError:            true,
+			expectedBidAdjustments: nil,
+		},
+		{
+			name: "InvalidAcctAdjustment",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)},
+			},
+			givenAccount: &config.Account{
+				BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+					MediaType: openrtb_ext.MediaType{
+						Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+							"bidderB": {
+								"dealId": []openrtb_ext.Adjustment{{Type: "multiplier", Value: -1.5}},
+							},
+						},
+					},
+				},
+			},
+			expectError:            true,
+			expectedBidAdjustments: nil,
+		},
+		{
+			name: "InvalidJSON",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{}}`)},
+			},
+			givenAccount:           &config.Account{},
+			expectError:            true,
+			expectedBidAdjustments: nil,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			mergedBidAdj, err := Merge(test.givenRequestWrapper, test.givenAccount.BidAdjustments)
+			if !test.expectError {
+				assert.NoError(t, err)
+			} else {
+				assert.Error(t, err)
+			}
+			assert.Equal(t, test.expectedBidAdjustments, mergedBidAdj)
+		})
+	}
+}
+
+func TestMerge(t *testing.T) {
+	testCases := []struct {
+		name                   string
+		givenRequestWrapper    *openrtb_ext.RequestWrapper
+		givenAccount           *config.Account
+		expectedBidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments
+	}{
+		{
+			name: "DiffBidderNames",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)},
+			},
+			givenAccount: &config.Account{
+				BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+					MediaType: openrtb_ext.MediaType{
+						Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+							"bidderB": {
+								"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+							},
+						},
+					},
+				},
+			},
+			expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+						},
+						"bidderB": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "RequestTakesPrecedence",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"audio":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)},
+			},
+			givenAccount: &config.Account{
+				BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+					MediaType: openrtb_ext.MediaType{
+						Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+							"bidderA": {
+								"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+							},
+						},
+					},
+				},
+			},
+			expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "DiffDealIds",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"video-instream":{"bidderA":{"dealId":[{ "adjtype": "static", "value": 3.00, "currency": "USD"}]}}}}}}`)},
+			},
+			givenAccount: &config.Account{
+				BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+					MediaType: openrtb_ext.MediaType{
+						VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+							"bidderA": {
+								"diffDealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+							},
+						},
+					},
+				},
+			},
+			expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId":     []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 3.00, Currency: "USD"}},
+							"diffDealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "DiffBidderNamesCpm",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"native":{"bidderA":{"dealId":[{"adjtype": "cpm", "value": 0.18, "currency": "USD"}]}}}}}}`)},
+			},
+			givenAccount: &config.Account{
+				BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+					MediaType: openrtb_ext.MediaType{
+						Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+							"bidderB": {
+								"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+							},
+						},
+					},
+				},
+			},
+			expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 0.18, Currency: "USD"}},
+						},
+						"bidderB": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "ReqAdjVideoAcctAdjBanner",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"video-outstream":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)},
+			},
+			givenAccount: &config.Account{
+				BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+					MediaType: openrtb_ext.MediaType{
+						Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+							"bidderB": {
+								"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+							},
+						},
+					},
+				},
+			},
+			expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderB": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+						},
+					},
+					VideoOutstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "RequestNilPrebid",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"ext":{"bidder": {}}}`)},
+			},
+			givenAccount: &config.Account{
+				BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+					MediaType: openrtb_ext.MediaType{
+						Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+							"bidderB": {
+								"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+							},
+						},
+					},
+				},
+			},
+			expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderB": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "AcctWildCardRequestVideo",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"video-instream":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)},
+			},
+			givenAccount: &config.Account{
+				BidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+					MediaType: openrtb_ext.MediaType{
+						WildCard: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+							"bidderB": {
+								"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+							},
+						},
+					},
+				},
+			},
+			expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					WildCard: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderB": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.5}},
+						},
+					},
+					VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "NilReqExtPrebidAndAcctBidAdj",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"ext":{"bidder": {}}}`)},
+			},
+			givenAccount:           &config.Account{},
+			expectedBidAdjustments: nil,
+		},
+		{
+			name: "NilAcctBidAdj",
+			givenRequestWrapper: &openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{Ext: []byte(`{"prebid":{"bidadjustments":{"mediatype":{"banner":{"bidderA":{"dealId":[{ "adjtype": "multiplier", "value": 1.1}]}}}}}}`)},
+			},
+			givenAccount: &config.Account{},
+			expectedBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			mergedBidAdj, err := merge(test.givenRequestWrapper, test.givenAccount.BidAdjustments)
+			assert.NoError(t, err)
+			assert.Equal(t, test.expectedBidAdjustments, mergedBidAdj)
+		})
+	}
+}
diff --git a/bidadjustment/validate_test.go b/bidadjustment/validate_test.go
new file mode 100644
index 00000000..a0b4eb43
--- /dev/null
+++ b/bidadjustment/validate_test.go
@@ -0,0 +1,338 @@
+package bidadjustment
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestValidate(t *testing.T) {
+	testCases := []struct {
+		name                string
+		givenBidAdjustments *openrtb_ext.ExtRequestPrebidBidAdjustments
+		expected            bool
+	}{
+		{
+			name: "OneAdjustmentValid",
+			givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+						},
+					},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "MultipleAdjustmentsValid",
+			givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+						},
+					},
+					VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 3.0, Currency: "USD"}},
+						},
+					},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "MixOfValidandInvalid",
+			givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+						},
+					},
+					VideoOutstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: 3.0, Currency: ""}},
+						},
+					},
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "WildCardInvalid",
+			givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					WildCard: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: -1.1, Currency: "USD"}},
+						},
+					},
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "AudioInvalid",
+			givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Audio: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeStatic, Value: 5.0, Currency: ""}},
+						},
+					},
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "NativeInvalid",
+			givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Native: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeCPM, Value: -1.1, Currency: "USD"}},
+						},
+					},
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "BannerInvalid",
+			givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					Banner: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 150}},
+						},
+					},
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "InstreamInvalid",
+			givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{
+				MediaType: openrtb_ext.MediaType{
+					VideoInstream: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+						"bidderA": {
+							"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 150}},
+						},
+					},
+				},
+			},
+			expected: false,
+		},
+		{
+			name:                "EmptyBidAdjustments",
+			givenBidAdjustments: &openrtb_ext.ExtRequestPrebidBidAdjustments{},
+			expected:            true,
+		},
+		{
+			name:                "NilBidAdjustments",
+			givenBidAdjustments: nil,
+			expected:            true,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actual := Validate(test.givenBidAdjustments)
+			assert.Equal(t, test.expected, actual)
+		})
+	}
+}
+
+func TestValidateForMediaType(t *testing.T) {
+	testCases := []struct {
+		name        string
+		givenBidAdj map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID
+		expected    bool
+	}{
+		{
+			name: "OneAdjustmentValid",
+			givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+				"bidderA": {
+					"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: 1.1}},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "OneAdjustmentInvalid",
+			givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+				"bidderA": {
+					"dealId": []openrtb_ext.Adjustment{{Type: AdjustmentTypeMultiplier, Value: -1.1}},
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "MultipleAdjustmentsValid",
+			givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+				"bidderA": {
+					"dealId": []openrtb_ext.Adjustment{
+						{Type: AdjustmentTypeMultiplier, Value: 1.1},
+						{Type: AdjustmentTypeStatic, Value: 3.0, Currency: "USD"},
+					},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "MultipleAdjustmentsInvalid",
+			givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+				"bidderA": {
+					"dealId": []openrtb_ext.Adjustment{
+						{Type: AdjustmentTypeMultiplier, Value: -1.1},
+						{Type: AdjustmentTypeCPM, Value: -3.0, Currency: "USD"},
+					},
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "MultipleDealIdsValid",
+			givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+				"bidderA": {
+					"dealId": []openrtb_ext.Adjustment{
+						{Type: AdjustmentTypeStatic, Value: 3.0, Currency: "USD"},
+					},
+					"diffDealId": []openrtb_ext.Adjustment{
+						{Type: AdjustmentTypeMultiplier, Value: 1.1},
+					},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "MultipleBiddersValid",
+			givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+				"bidderA": {
+					"dealId": []openrtb_ext.Adjustment{
+						{Type: AdjustmentTypeStatic, Value: 5.0, Currency: "USD"},
+					},
+				},
+				"bidderB": {
+					"dealId": []openrtb_ext.Adjustment{
+						{Type: AdjustmentTypeCPM, Value: 3.0, Currency: "USD"},
+					},
+				},
+			},
+			expected: true,
+		},
+		{
+			name:        "NilBidAdj",
+			givenBidAdj: nil,
+			expected:    true,
+		},
+		{
+			name: "NilBidderToAdjustmentsByDealID",
+			givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+				"bidderA": nil,
+			},
+			expected: false,
+		},
+		{
+			name: "NilDealIdToAdjustments",
+			givenBidAdj: map[openrtb_ext.BidderName]openrtb_ext.AdjustmentsByDealID{
+				"bidderA": {
+					"dealId": nil,
+				},
+			},
+			expected: false,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actual := validateForMediaType(test.givenBidAdj)
+			assert.Equal(t, test.expected, actual)
+		})
+	}
+}
+
+func TestValidateAdjustment(t *testing.T) {
+	testCases := []struct {
+		name            string
+		givenAdjustment openrtb_ext.Adjustment
+		expected        bool
+	}{
+		{
+			name: "ValidCpm",
+			givenAdjustment: openrtb_ext.Adjustment{
+				Type:     AdjustmentTypeCPM,
+				Value:    5.0,
+				Currency: "USD",
+			},
+			expected: true,
+		},
+		{
+			name: "ValidMultiplier",
+			givenAdjustment: openrtb_ext.Adjustment{
+				Type:  AdjustmentTypeMultiplier,
+				Value: 2.0,
+			},
+			expected: true,
+		},
+		{
+			name: "ValidStatic",
+			givenAdjustment: openrtb_ext.Adjustment{
+				Type:     AdjustmentTypeStatic,
+				Value:    3.0,
+				Currency: "USD",
+			},
+			expected: true,
+		},
+		{
+			name: "InvalidCpm",
+			givenAdjustment: openrtb_ext.Adjustment{
+				Type:     AdjustmentTypeCPM,
+				Value:    5.0,
+				Currency: "",
+			},
+			expected: false,
+		},
+		{
+			name: "InvalidMultiplier",
+			givenAdjustment: openrtb_ext.Adjustment{
+				Type:  AdjustmentTypeMultiplier,
+				Value: 200,
+			},
+			expected: false,
+		},
+		{
+			name: "InvalidStatic",
+			givenAdjustment: openrtb_ext.Adjustment{
+				Type:     AdjustmentTypeStatic,
+				Value:    -3.0,
+				Currency: "USD",
+			},
+			expected: false,
+		},
+		{
+			name: "InvalidAdjType",
+			givenAdjustment: openrtb_ext.Adjustment{
+				Type:  "Invalid",
+				Value: 1.0,
+			},
+			expected: false,
+		},
+		{
+			name:            "EmptyAdjustment",
+			givenAdjustment: openrtb_ext.Adjustment{},
+			expected:        false,
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actual := validateAdjustment(test.givenAdjustment)
+			assert.Equal(t, test.expected, actual)
+		})
+	}
+}
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index f291d2e8..053bcc10 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -114,6 +114,10 @@ func TestJsonSampleRequests(t *testing.T) {
 			"Assert request with ad server targeting is processing correctly",
 			"adservertargeting",
 		},
+		{
+			"Assert request with bid adjustments defined is processing correctly",
+			"bidadjustments",
+		},
 	}
 
 	for _, tc := range testSuites {
diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go
index 0ad47982..6811249b 100644
--- a/endpoints/openrtb2/test_utils.go
+++ b/endpoints/openrtb2/test_utils.go
@@ -936,6 +936,7 @@ type mockBidderHandler struct {
 	BidderName string  `json:"bidderName"`
 	Currency   string  `json:"currency"`
 	Price      float64 `json:"price"`
+	DealID     string  `json:"dealid"`
 }
 
 func (b mockBidderHandler) bid(w http.ResponseWriter, req *http.Request) {
@@ -970,9 +971,10 @@ func (b mockBidderHandler) bid(w http.ResponseWriter, req *http.Request) {
 			{
 				Bid: []openrtb2.Bid{
 					{
-						ID:    b.BidderName + "-bid",
-						ImpID: openrtb2Request.Imp[0].ID,
-						Price: b.Price,
+						ID:     b.BidderName + "-bid",
+						ImpID:  openrtb2Request.Imp[0].ID,
+						Price:  b.Price,
+						DealID: b.DealID,
 					},
 				},
 				Seat: b.BidderName,
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index 4e367ebe..3bd023dd 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -17,6 +17,7 @@ import (
 	"time"
 
 	"github.com/golang/glog"
+	"github.com/prebid/openrtb/v19/adcom1"
 	nativeRequests "github.com/prebid/openrtb/v19/native1/request"
 	nativeResponse "github.com/prebid/openrtb/v19/native1/response"
 	"github.com/prebid/openrtb/v19/openrtb2"
@@ -109,7 +110,7 @@ func TestSingleBidder(t *testing.T) {
 			bidAdjustments:      bidAdjustments,
 		}
 		extraInfo := &adapters.ExtraRequestInfo{}
-		seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+		seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 		assert.NotEmpty(t, extraInfo.MakeBidsTimeInfo.Durations)
 		assert.False(t, extraInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime.IsZero())
 
@@ -235,7 +236,7 @@ func TestSingleBidderGzip(t *testing.T) {
 			bidAdjustments:      bidAdjustments,
 		}
 		extraInfo := &adapters.ExtraRequestInfo{}
-		seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+		seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 		assert.NotEmpty(t, extraInfo.MakeBidsTimeInfo.Durations)
 		assert.False(t, extraInfo.MakeBidsTimeInfo.AfterMakeBidsStartTime.IsZero())
 		assert.Len(t, seatBids, 1)
@@ -338,7 +339,7 @@ func TestRequestBidRemovesSensitiveHeaders(t *testing.T) {
 		bidAdjustments:      bidAdjustments,
 	}
 	extraInfo := &adapters.ExtraRequestInfo{}
-	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 	expectedHttpCalls := []*openrtb_ext.ExtHttpCall{
 		{
 			Uri:            server.URL,
@@ -392,7 +393,7 @@ func TestSetGPCHeader(t *testing.T) {
 		bidAdjustments:      bidAdjustments,
 	}
 	extraInfo := &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}
-	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 
 	expectedHttpCall := []*openrtb_ext.ExtHttpCall{
 		{
@@ -445,7 +446,7 @@ func TestSetGPCHeaderNil(t *testing.T) {
 		bidAdjustments:      bidAdjustments,
 	}
 	extraInfo := &adapters.ExtraRequestInfo{GlobalPrivacyControlHeader: "1"}
-	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	seatBids, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), extraInfo, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 
 	expectedHttpCall := []*openrtb_ext.ExtHttpCall{
 		{
@@ -517,7 +518,7 @@ func TestMultiBidder(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 
 	if len(seatBids) != 1 {
 		t.Fatalf("SeatBid should exist, because bids exist.")
@@ -898,6 +899,7 @@ func TestMultiCurrencies(t *testing.T) {
 			},
 			openrtb_ext.ExtAlternateBidderCodes{},
 			&hookexecution.EmptyHookExecutor{},
+			nil,
 		)
 		assert.Len(t, seatBids, 1)
 		seatBid := seatBids[0]
@@ -1057,6 +1059,7 @@ func TestMultiCurrencies_RateConverterNotSet(t *testing.T) {
 			},
 			openrtb_ext.ExtAlternateBidderCodes{},
 			&hookexecution.EmptyHookExecutor{},
+			nil,
 		)
 		assert.Len(t, seatBids, 1)
 		seatBid := seatBids[0]
@@ -1235,6 +1238,7 @@ func TestMultiCurrencies_RequestCurrencyPick(t *testing.T) {
 			},
 			openrtb_ext.ExtAlternateBidderCodes{},
 			&hookexecution.EmptyHookExecutor{},
+			nil,
 		)
 		assert.Len(t, seatBids, 1)
 		seatBid := seatBids[0]
@@ -1552,6 +1556,7 @@ func TestMobileNativeTypes(t *testing.T) {
 			},
 			openrtb_ext.ExtAlternateBidderCodes{},
 			&hookexecution.EmptyHookExecutor{},
+			nil,
 		)
 		assert.Len(t, seatBids, 1)
 
@@ -1671,6 +1676,7 @@ func TestRequestBidsStoredBidResponses(t *testing.T) {
 			},
 			openrtb_ext.ExtAlternateBidderCodes{},
 			&hookexecution.EmptyHookExecutor{},
+			nil,
 		)
 		assert.Len(t, seatBids, 1)
 
@@ -1785,6 +1791,7 @@ func TestFledge(t *testing.T) {
 			},
 			openrtb_ext.ExtAlternateBidderCodes{},
 			&hookexecution.EmptyHookExecutor{},
+			nil,
 		)
 		assert.Len(t, seatBids, 1)
 		assert.NotNil(t, seatBids[0].FledgeAuctionConfigs)
@@ -1808,7 +1815,7 @@ func TestErrorReporting(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	bids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	bids, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 	if bids != nil {
 		t.Errorf("There should be no seatbid if no http requests are returned.")
 	}
@@ -2042,7 +2049,7 @@ func TestCallRecordAdapterConnections(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	_, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{PbsEntryPoint: metrics.ReqTypeORTB2Web}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	_, errs := bidder.requestBid(context.Background(), bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{PbsEntryPoint: metrics.ReqTypeORTB2Web}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 
 	// Assert no errors
 	assert.Equal(t, 0, len(errs), "bidder.requestBid returned errors %v \n", errs)
@@ -2289,7 +2296,7 @@ func TestRequestBidsWithAdsCertsSigner(t *testing.T) {
 		addCallSignHeader:   true,
 		bidAdjustments:      bidAdjustments,
 	}
-	_, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	_, errs := bidder.requestBid(ctx, bidderReq, currencyConverter.Rates(), &adapters.ExtraRequestInfo{}, &MockSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 
 	assert.Empty(t, errs, "no errors should be returned")
 }
@@ -2513,7 +2520,8 @@ func TestExtraBid(t *testing.T) {
 				},
 			},
 		},
-		&hookexecution.EmptyHookExecutor{})
+		&hookexecution.EmptyHookExecutor{},
+		nil)
 	assert.Nil(t, errs)
 	assert.Len(t, seatBids, 2)
 	sort.Slice(seatBids, func(i, j int) bool {
@@ -2626,7 +2634,8 @@ func TestExtraBidWithAlternateBidderCodeDisabled(t *testing.T) {
 				},
 			},
 		},
-		&hookexecution.EmptyHookExecutor{})
+		&hookexecution.EmptyHookExecutor{},
+		nil)
 	assert.Equal(t, wantErrs, errs)
 	assert.Len(t, seatBids, 2)
 	assert.ElementsMatch(t, wantSeatBids, seatBids)
@@ -2736,7 +2745,8 @@ func TestExtraBidWithBidAdjustments(t *testing.T) {
 				},
 			},
 		},
-		&hookexecution.EmptyHookExecutor{})
+		&hookexecution.EmptyHookExecutor{},
+		nil)
 	assert.Nil(t, errs)
 	assert.Len(t, seatBids, 2)
 	sort.Slice(seatBids, func(i, j int) bool {
@@ -2848,7 +2858,8 @@ func TestExtraBidWithBidAdjustmentsUsingAdapterCode(t *testing.T) {
 				},
 			},
 		},
-		&hookexecution.EmptyHookExecutor{})
+		&hookexecution.EmptyHookExecutor{},
+		nil)
 	assert.Nil(t, errs)
 	assert.Len(t, seatBids, 2)
 	sort.Slice(seatBids, func(i, j int) bool {
@@ -2972,7 +2983,8 @@ func TestExtraBidWithMultiCurrencies(t *testing.T) {
 				},
 			},
 		},
-		&hookexecution.EmptyHookExecutor{})
+		&hookexecution.EmptyHookExecutor{},
+		nil)
 	assert.Nil(t, errs)
 	assert.Len(t, seatBids, 2)
 	sort.Slice(seatBids, func(i, j int) bool {
@@ -2980,3 +2992,56 @@ func TestExtraBidWithMultiCurrencies(t *testing.T) {
 	})
 	assert.Equal(t, wantSeatBids, seatBids)
 }
+
+func TestGetBidType(t *testing.T) {
+	testCases := []struct {
+		name         string
+		givenBidType openrtb_ext.BidType
+		givenImpId   string
+		givenImp     []openrtb2.Imp
+		expected     string
+	}{
+		{
+			name: "VideoInstream",
+			givenImp: []openrtb2.Imp{
+				{
+					ID: "imp-id",
+					Video: &openrtb2.Video{
+						Plcmt: adcom1.VideoPlcmtInstream,
+					},
+				},
+			},
+			givenBidType: openrtb_ext.BidTypeVideo,
+			givenImpId:   "imp-id",
+			expected:     "video-instream",
+		},
+		{
+			name: "VideoOutstream",
+			givenImp: []openrtb2.Imp{
+				{
+					ID: "imp-id",
+					Video: &openrtb2.Video{
+						Plcmt: adcom1.VideoPlcmtAccompanyingContent,
+					},
+				},
+			},
+			givenBidType: openrtb_ext.BidTypeVideo,
+			givenImpId:   "imp-id",
+			expected:     "video-outstream",
+		},
+		{
+			name:         "NonVideoBidType",
+			givenImp:     []openrtb2.Imp{},
+			givenBidType: openrtb_ext.BidTypeBanner,
+			givenImpId:   "imp-id",
+			expected:     string(openrtb_ext.BidTypeBanner),
+		},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			actual := getBidTypeForAdjustments(test.givenBidType, test.givenImpId, test.givenImp)
+			assert.Equal(t, test.expected, actual, "Bid type doesn't match")
+		})
+	}
+}
diff --git a/exchange/bidder_validate_bids_test.go b/exchange/bidder_validate_bids_test.go
index f863e932..e7340e7d 100644
--- a/exchange/bidder_validate_bids_test.go
+++ b/exchange/bidder_validate_bids_test.go
@@ -65,7 +65,7 @@ func TestAllValidBids(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 	assert.Len(t, seatBids, 1)
 	assert.Len(t, seatBids[0].Bids, 4)
 	assert.Len(t, errs, 0)
@@ -135,7 +135,7 @@ func TestAllBadBids(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 	assert.Len(t, seatBids, 1)
 	assert.Len(t, seatBids[0].Bids, 0)
 	assert.Len(t, errs, 7)
@@ -216,7 +216,7 @@ func TestMixedBids(t *testing.T) {
 		addCallSignHeader:   false,
 		bidAdjustments:      bidAdjustments,
 	}
-	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+	seatBids, errs := bidder.requestBid(context.Background(), bidderReq, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 	assert.Len(t, seatBids, 1)
 	assert.Len(t, seatBids[0].Bids, 3)
 	assert.Len(t, errs, 5)
@@ -345,7 +345,7 @@ func TestCurrencyBids(t *testing.T) {
 			addCallSignHeader:   false,
 			bidAdjustments:      bidAdjustments,
 		}
-		seatBids, errs := bidder.requestBid(context.Background(), bidderRequest, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{})
+		seatBids, errs := bidder.requestBid(context.Background(), bidderRequest, currency.NewConstantRates(), &adapters.ExtraRequestInfo{}, &adscert.NilSigner{}, bidReqOptions, openrtb_ext.ExtAlternateBidderCodes{}, &hookexecution.EmptyHookExecutor{}, nil)
 		assert.Len(t, seatBids, 1)
 		assert.Len(t, seatBids[0].Bids, expectedValidBids)
 		assert.Len(t, errs, expectedErrs)
@@ -357,6 +357,6 @@ type mockAdaptedBidder struct {
 	errorResponse []error
 }
 
-func (b *mockAdaptedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor) ([]*entities.PbsOrtbSeatBid, []error) {
+func (b *mockAdaptedBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) ([]*entities.PbsOrtbSeatBid, []error) {
 	return b.bidResponse, b.errorResponse
 }
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 323b299e..661233ec 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -5315,7 +5315,7 @@ type validatingBidder struct {
 	mockResponses map[string]bidderResponse
 }
 
-func (b *validatingBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor) (seatBids []*entities.PbsOrtbSeatBid, errs []error) {
+func (b *validatingBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (seatBids []*entities.PbsOrtbSeatBid, errs []error) {
 	if expectedRequest, ok := b.expectations[string(bidderRequest.BidderName)]; ok {
 		if expectedRequest != nil {
 			if !reflect.DeepEqual(expectedRequest.BidAdjustments, bidRequestOptions.bidAdjustments) {
@@ -5373,7 +5373,7 @@ type capturingRequestBidder struct {
 	req *openrtb2.BidRequest
 }
 
-func (b *capturingRequestBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor) (seatBid []*entities.PbsOrtbSeatBid, errs []error) {
+func (b *capturingRequestBidder) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestOptions bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (seatBid []*entities.PbsOrtbSeatBid, errs []error) {
 	b.req = bidderRequest.BidRequest
 	return []*entities.PbsOrtbSeatBid{{}}, nil
 }
@@ -5480,7 +5480,7 @@ func (e *emptyUsersync) HasAnyLiveSyncs() bool {
 
 type panicingAdapter struct{}
 
-func (panicingAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor) (posb []*entities.PbsOrtbSeatBid, errs []error) {
+func (panicingAdapter) requestBid(ctx context.Context, bidderRequest BidderRequest, conversions currency.Conversions, reqInfo *adapters.ExtraRequestInfo, adsCertSigner adscert.Signer, bidRequestMetadata bidRequestOptions, alternateBidderCodes openrtb_ext.ExtAlternateBidderCodes, executor hookexecution.StageExecutor, ruleToAdjustments openrtb_ext.AdjustmentsByDealID) (posb []*entities.PbsOrtbSeatBid, errs []error) {
 	panic("Panic! Panic! The world is ending!")
 }