Repository: prebid/prebid-server
Pull request #2746 (2023-05-04 13:32:25)

GDPR: instantiate TCF2 config in handler instead of exchange


Patch:
diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go
index e56d8f2f..76104193 100644
--- a/adapters/appnexus/appnexus.go
+++ b/adapters/appnexus/appnexus.go
@@ -25,7 +25,6 @@ const defaultPlatformID int = 5
 
 type adapter struct {
 	URI             string
-	iabCategoryMap  map[string]string
 	hbSource        int
 	randomGenerator randomutil.RandomGenerator
 }
@@ -35,11 +34,7 @@ var maxImpsPerReq = 10
 // Builder builds a new instance of the AppNexus adapter for the given bidder with the given config.
 func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
 	bidder := &adapter{
-		URI: config.Endpoint,
-		iabCategoryMap: map[string]string{
-			"1": "IAB20-3",
-			"9": "IAB5-3",
-		},
+		URI:             config.Endpoint,
 		hbSource:        resolvePlatformID(config.PlatformID),
 		randomGenerator: randomutil.RandomNumberGenerator{},
 	}
@@ -351,7 +346,7 @@ func buildRequestImp(imp *openrtb2.Imp, appnexusExt *openrtb_ext.ExtImpAppnexus,
 	impExt := appnexusImpExt{Appnexus: appnexusImpExtAppnexus{
 		PlacementID:       int(appnexusExt.PlacementId),
 		TrafficSourceCode: appnexusExt.TrafficSourceCode,
-		Keywords:          makeKeywordStr(appnexusExt.Keywords),
+		Keywords:          appnexusExt.Keywords.String(),
 		UsePmtRule:        appnexusExt.UsePaymentRule,
 		PrivateSizes:      appnexusExt.PrivateSizes,
 	}}
@@ -364,21 +359,6 @@ func buildRequestImp(imp *openrtb2.Imp, appnexusExt *openrtb_ext.ExtImpAppnexus,
 	return nil
 }
 
-func makeKeywordStr(keywords []*openrtb_ext.ExtImpAppnexusKeyVal) string {
-	kvs := make([]string, 0, len(keywords)*2)
-	for _, kv := range keywords {
-		if len(kv.Values) == 0 {
-			kvs = append(kvs, kv.Key)
-		} else {
-			for _, val := range kv.Values {
-				kvs = append(kvs, fmt.Sprintf("%s=%s", kv.Key, val))
-			}
-		}
-	}
-
-	return strings.Join(kvs, ",")
-}
-
 // getMediaTypeForBid determines which type of bid.
 func getMediaTypeForBid(bid *appnexusBidExt) (openrtb_ext.BidType, error) {
 	switch bid.Appnexus.BidType {
@@ -386,8 +366,6 @@ func getMediaTypeForBid(bid *appnexusBidExt) (openrtb_ext.BidType, error) {
 		return openrtb_ext.BidTypeBanner, nil
 	case 1:
 		return openrtb_ext.BidTypeVideo, nil
-	case 2:
-		return openrtb_ext.BidTypeAudio, nil
 	case 3:
 		return openrtb_ext.BidTypeNative, nil
 	default:
@@ -398,7 +376,7 @@ func getMediaTypeForBid(bid *appnexusBidExt) (openrtb_ext.BidType, error) {
 // getIabCategoryForBid maps an appnexus brand id to an IAB category.
 func (a *adapter) findIabCategoryForBid(bid *appnexusBidExt) (string, bool) {
 	brandIDString := strconv.Itoa(bid.Appnexus.BrandCategory)
-	iabCategory, ok := a.iabCategoryMap[brandIDString]
+	iabCategory, ok := iabCategoryMap[brandIDString]
 	return iabCategory, ok
 }
 
diff --git a/adapters/appnexus/iab_categories.go b/adapters/appnexus/iab_categories.go
new file mode 100644
index 00000000..c9c1c65c
--- /dev/null
+++ b/adapters/appnexus/iab_categories.go
@@ -0,0 +1,99 @@
+package appnexus
+
+var iabCategoryMap = map[string]string{
+	"1":   "IAB20-3",
+	"2":   "IAB18-5",
+	"3":   "IAB10-1",
+	"4":   "IAB2-3",
+	"5":   "IAB19-8",
+	"6":   "IAB22-1",
+	"7":   "IAB18-1",
+	"8":   "IAB12-3",
+	"9":   "IAB5-1",
+	"10":  "IAB4-5",
+	"11":  "IAB13-4",
+	"12":  "IAB8-7",
+	"13":  "IAB9-7",
+	"14":  "IAB7-1",
+	"15":  "IAB20-18",
+	"16":  "IAB10-7",
+	"17":  "IAB19-18",
+	"18":  "IAB13-6",
+	"19":  "IAB18-4",
+	"20":  "IAB1-5",
+	"21":  "IAB1-6",
+	"22":  "IAB3-4",
+	"23":  "IAB19-13",
+	"24":  "IAB22-2",
+	"25":  "IAB3-9",
+	"26":  "IAB17-18",
+	"27":  "IAB19-6",
+	"28":  "IAB1-7",
+	"29":  "IAB9-30",
+	"30":  "IAB20-7",
+	"31":  "IAB20-17",
+	"32":  "IAB7-32",
+	"33":  "IAB16-5",
+	"34":  "IAB19-34",
+	"35":  "IAB11-5",
+	"36":  "IAB12-3",
+	"37":  "IAB11-4",
+	"38":  "IAB12-3",
+	"39":  "IAB9-30",
+	"41":  "IAB7-44",
+	"42":  "IAB7-1",
+	"43":  "IAB7-30",
+	"50":  "IAB19-30",
+	"51":  "IAB17-12",
+	"52":  "IAB19-30",
+	"53":  "IAB3-1",
+	"55":  "IAB13-2",
+	"56":  "IAB19-30",
+	"57":  "IAB19-30",
+	"58":  "IAB7-39",
+	"59":  "IAB22-1",
+	"60":  "IAB7-39",
+	"61":  "IAB21-3",
+	"62":  "IAB5-1",
+	"63":  "IAB12-3",
+	"64":  "IAB20-18",
+	"65":  "IAB11-2",
+	"66":  "IAB17-18",
+	"67":  "IAB9-9",
+	"68":  "IAB9-5",
+	"69":  "IAB7-44",
+	"71":  "IAB22-3",
+	"73":  "IAB19-30",
+	"74":  "IAB8-5",
+	"78":  "IAB22-1",
+	"85":  "IAB12-2",
+	"86":  "IAB22-3",
+	"87":  "IAB11-3",
+	"112": "IAB7-32",
+	"113": "IAB7-32",
+	"114": "IAB7-32",
+	"115": "IAB7-32",
+	"118": "IAB9-5",
+	"119": "IAB9-5",
+	"120": "IAB9-5",
+	"121": "IAB9-5",
+	"122": "IAB9-5",
+	"123": "IAB9-5",
+	"124": "IAB9-5",
+	"125": "IAB9-5",
+	"126": "IAB9-5",
+	"127": "IAB22-1",
+	"132": "IAB1-2",
+	"133": "IAB19-30",
+	"137": "IAB3-9",
+	"138": "IAB19-3",
+	"140": "IAB2-3",
+	"141": "IAB2-1",
+	"142": "IAB2-3",
+	"143": "IAB17-13",
+	"166": "IAB11-4",
+	"175": "IAB3-1",
+	"176": "IAB13-4",
+	"182": "IAB8-9",
+	"183": "IAB3-5",
+}
diff --git a/adapters/emtv/emtv.go b/adapters/emtv/emtv.go
new file mode 100644
index 00000000..64101941
--- /dev/null
+++ b/adapters/emtv/emtv.go
@@ -0,0 +1,164 @@
+package emtv
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/errortypes"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/httputil"
+)
+
+type adapter struct {
+	endpoint string
+}
+
+type reqBodyExt struct {
+	EmtvBidderExt reqBodyExtBidder `json:"bidder"`
+}
+
+type reqBodyExtBidder struct {
+	Type        string `json:"type"`
+	PlacementID string `json:"placementId,omitempty"`
+	EndpointID  string `json:"endpointId,omitempty"`
+}
+
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter, server config.Server) (adapters.Bidder, error) {
+	bidder := &adapter{
+		endpoint: config.Endpoint,
+	}
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	var err error
+	var adapterRequests []*adapters.RequestData
+
+	reqCopy := *request
+	for _, imp := range request.Imp {
+		reqCopy.Imp = []openrtb2.Imp{imp}
+
+		var bidderExt adapters.ExtImpBidder
+		var emtvExt openrtb_ext.ImpExtEmtv
+
+		if err = json.Unmarshal(reqCopy.Imp[0].Ext, &bidderExt); err != nil {
+			return nil, []error{err}
+		}
+		if err = json.Unmarshal(bidderExt.Bidder, &emtvExt); err != nil {
+			return nil, []error{err}
+		}
+
+		impExt := reqBodyExt{EmtvBidderExt: reqBodyExtBidder{}}
+
+		if emtvExt.PlacementID != "" {
+			impExt.EmtvBidderExt.PlacementID = emtvExt.PlacementID
+			impExt.EmtvBidderExt.Type = "publisher"
+		} else if emtvExt.EndpointID != "" {
+			impExt.EmtvBidderExt.EndpointID = emtvExt.EndpointID
+			impExt.EmtvBidderExt.Type = "network"
+		} else {
+			continue
+		}
+
+		finalyImpExt, err := json.Marshal(impExt)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		reqCopy.Imp[0].Ext = finalyImpExt
+
+		adapterReq, err := a.makeRequest(&reqCopy)
+		if err != nil {
+			return nil, []error{err}
+		}
+
+		if adapterReq != nil {
+			adapterRequests = append(adapterRequests, adapterReq)
+		}
+	}
+
+	if len(adapterRequests) == 0 {
+		return nil, []error{errors.New("found no valid impressions")}
+	}
+
+	return adapterRequests, nil
+}
+
+func (a *adapter) makeRequest(request *openrtb2.BidRequest) (*adapters.RequestData, error) {
+	reqJSON, err := json.Marshal(request)
+	if err != nil {
+		return nil, err
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+	return &adapters.RequestData{
+		Method:  "POST",
+		Uri:     a.endpoint,
+		Body:    reqJSON,
+		Headers: headers,
+	}, nil
+}
+
+func (a *adapter) MakeBids(request *openrtb2.BidRequest, requestData *adapters.RequestData, responseData *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if httputil.IsResponseStatusCodeNoContent(responseData) {
+		return nil, nil
+	}
+
+	if err := httputil.CheckResponseStatusCodeForErrors(responseData); err != nil {
+		return nil, []error{err}
+	}
+
+	var response openrtb2.BidResponse
+	if err := json.Unmarshal(responseData.Body, &response); err != nil {
+		return nil, []error{err}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+	bidResponse.Currency = response.Cur
+
+	impsMappedByID := make(map[string]openrtb2.Imp, len(request.Imp))
+	for i, imp := range request.Imp {
+		impsMappedByID[request.Imp[i].ID] = imp
+	}
+
+	for _, seatBid := range response.SeatBid {
+		for i := range seatBid.Bid {
+			bidType, err := getMediaTypeForImp(seatBid.Bid[i].ImpID, request.Imp, impsMappedByID)
+			if err != nil {
+				return nil, []error{err}
+			}
+
+			b := &adapters.TypedBid{
+				Bid:     &seatBid.Bid[i],
+				BidType: bidType,
+			}
+			bidResponse.Bids = append(bidResponse.Bids, b)
+		}
+	}
+	return bidResponse, nil
+}
+
+func getMediaTypeForImp(impID string, imps []openrtb2.Imp, impMap map[string]openrtb2.Imp) (openrtb_ext.BidType, error) {
+	if index, found := impMap[impID]; found {
+		if index.Banner != nil {
+			return openrtb_ext.BidTypeBanner, nil
+		}
+		if index.Video != nil {
+			return openrtb_ext.BidTypeVideo, nil
+		}
+		if index.Native != nil {
+			return openrtb_ext.BidTypeNative, nil
+		}
+	}
+
+	return "", &errortypes.BadInput{
+		Message: fmt.Sprintf("Failed to find impression \"%s\"", impID),
+	}
+}
diff --git a/analytics/core.go b/analytics/core.go
index d5d5e914..0d17cdeb 100644
--- a/analytics/core.go
+++ b/analytics/core.go
@@ -9,14 +9,9 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-/*
-  	PBSAnalyticsModule must be implemented by any analytics module that does transactional logging.
-
-	New modules can use the /analytics/endpoint_data_objects, extract the
-	information required and are responsible for handling all their logging activities inside LogAuctionObject, LogAmpObject
-	LogCookieSyncObject and LogSetUIDObject method implementations.
-*/
-
+// PBSAnalyticsModule must be implemented by analytics modules to extract the required information and logging
+// activities. Do not use marshal the parameter objects directly as they can change over time. Use a separate
+// model for each analytics module and transform as appropriate.
 type PBSAnalyticsModule interface {
 	LogAuctionObject(*AuctionObject)
 	LogVideoObject(*VideoObject)
@@ -88,7 +83,7 @@ type UsersyncInfo struct {
 	SupportCORS bool   `json:"supportCORS,omitempty"`
 }
 
-// NotificationEvent is a loggable object
+// NotificationEvent object of a transaction at /event
 type NotificationEvent struct {
 	Request *EventRequest   `json:"request"`
 	Account *config.Account `json:"account"`
diff --git a/analytics/filesystem/file_module.go b/analytics/filesystem/file_module.go
index d6a03ea2..7c690f66 100644
--- a/analytics/filesystem/file_module.go
+++ b/analytics/filesystem/file_module.go
@@ -27,7 +27,6 @@ type FileLogger struct {
 
 // Writes AuctionObject to file
 func (f *FileLogger) LogAuctionObject(ao *analytics.AuctionObject) {
-	//Code to parse the object and log in a way required
 	var b bytes.Buffer
 	b.WriteString(jsonifyAuctionObject(ao))
 	f.Logger.Debug(b.String())
@@ -103,13 +102,25 @@ func NewFileLogger(filename string) (analytics.PBSAnalyticsModule, error) {
 }
 
 func jsonifyAuctionObject(ao *analytics.AuctionObject) string {
-	type alias analytics.AuctionObject
+	var logEntry *logAuction
+	if ao != nil {
+		logEntry = &logAuction{
+			Status:               ao.Status,
+			Errors:               ao.Errors,
+			Request:              ao.Request,
+			Response:             ao.Response,
+			Account:              ao.Account,
+			StartTime:            ao.StartTime,
+			HookExecutionOutcome: ao.HookExecutionOutcome,
+		}
+	}
+
 	b, err := json.Marshal(&struct {
 		Type RequestType `json:"type"`
-		*alias
+		*logAuction
 	}{
-		Type:  AUCTION,
-		alias: (*alias)(ao),
+		Type:       AUCTION,
+		logAuction: logEntry,
 	})
 
 	if err == nil {
@@ -120,13 +131,25 @@ func jsonifyAuctionObject(ao *analytics.AuctionObject) string {
 }
 
 func jsonifyVideoObject(vo *analytics.VideoObject) string {
-	type alias analytics.VideoObject
+	var logEntry *logVideo
+	if vo != nil {
+		logEntry = &logVideo{
+			Status:        vo.Status,
+			Errors:        vo.Errors,
+			Request:       vo.Request,
+			Response:      vo.Response,
+			VideoRequest:  vo.VideoRequest,
+			VideoResponse: vo.VideoResponse,
+			StartTime:     vo.StartTime,
+		}
+	}
+
 	b, err := json.Marshal(&struct {
 		Type RequestType `json:"type"`
-		*alias
+		*logVideo
 	}{
-		Type:  VIDEO,
-		alias: (*alias)(vo),
+		Type:     VIDEO,
+		logVideo: logEntry,
 	})
 
 	if err == nil {
@@ -137,14 +160,21 @@ func jsonifyVideoObject(vo *analytics.VideoObject) string {
 }
 
 func jsonifyCookieSync(cso *analytics.CookieSyncObject) string {
-	type alias analytics.CookieSyncObject
+	var logEntry *logUserSync
+	if cso != nil {
+		logEntry = &logUserSync{
+			Status:       cso.Status,
+			Errors:       cso.Errors,
+			BidderStatus: cso.BidderStatus,
+		}
+	}
 
 	b, err := json.Marshal(&struct {
 		Type RequestType `json:"type"`
-		*alias
+		*logUserSync
 	}{
-		Type:  COOKIE_SYNC,
-		alias: (*alias)(cso),
+		Type:        COOKIE_SYNC,
+		logUserSync: logEntry,
 	})
 
 	if err == nil {
@@ -155,13 +185,23 @@ func jsonifyCookieSync(cso *analytics.CookieSyncObject) string {
 }
 
 func jsonifySetUIDObject(so *analytics.SetUIDObject) string {
-	type alias analytics.SetUIDObject
+	var logEntry *logSetUID
+	if so != nil {
+		logEntry = &logSetUID{
+			Status:  so.Status,
+			Bidder:  so.Bidder,
+			UID:     so.UID,
+			Errors:  so.Errors,
+			Success: so.Success,
+		}
+	}
+
 	b, err := json.Marshal(&struct {
 		Type RequestType `json:"type"`
-		*alias
+		*logSetUID
 	}{
-		Type:  SETUID,
-		alias: (*alias)(so),
+		Type:      SETUID,
+		logSetUID: logEntry,
 	})
 
 	if err == nil {
@@ -172,13 +212,26 @@ func jsonifySetUIDObject(so *analytics.SetUIDObject) string {
 }
 
 func jsonifyAmpObject(ao *analytics.AmpObject) string {
-	type alias analytics.AmpObject
+	var logEntry *logAMP
+	if ao != nil {
+		logEntry = &logAMP{
+			Status:               ao.Status,
+			Errors:               ao.Errors,
+			Request:              ao.Request,
+			AuctionResponse:      ao.AuctionResponse,
+			AmpTargetingValues:   ao.AmpTargetingValues,
+			Origin:               ao.Origin,
+			StartTime:            ao.StartTime,
+			HookExecutionOutcome: ao.HookExecutionOutcome,
+		}
+	}
+
 	b, err := json.Marshal(&struct {
 		Type RequestType `json:"type"`
-		*alias
+		*logAMP
 	}{
-		Type:  AMP,
-		alias: (*alias)(ao),
+		Type:   AMP,
+		logAMP: logEntry,
 	})
 
 	if err == nil {
@@ -189,13 +242,20 @@ func jsonifyAmpObject(ao *analytics.AmpObject) string {
 }
 
 func jsonifyNotificationEventObject(ne *analytics.NotificationEvent) string {
-	type alias analytics.NotificationEvent
+	var logEntry *logNotificationEvent
+	if ne != nil {
+		logEntry = &logNotificationEvent{
+			Request: ne.Request,
+			Account: ne.Account,
+		}
+	}
+
 	b, err := json.Marshal(&struct {
 		Type RequestType `json:"type"`
-		*alias
+		*logNotificationEvent
 	}{
-		Type:  NOTIFICATION_EVENT,
-		alias: (*alias)(ne),
+		Type:                 NOTIFICATION_EVENT,
+		logNotificationEvent: logEntry,
 	})
 
 	if err == nil {
diff --git a/analytics/filesystem/model.go b/analytics/filesystem/model.go
new file mode 100644
index 00000000..9fc7a6e1
--- /dev/null
+++ b/analytics/filesystem/model.go
@@ -0,0 +1,61 @@
+package filesystem
+
+import (
+	"time"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/analytics"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/hooks/hookexecution"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type logAuction struct {
+	Status               int
+	Errors               []error
+	Request              *openrtb2.BidRequest
+	Response             *openrtb2.BidResponse
+	Account              *config.Account
+	StartTime            time.Time
+	HookExecutionOutcome []hookexecution.StageOutcome
+}
+
+type logVideo struct {
+	Status        int
+	Errors        []error
+	Request       *openrtb2.BidRequest
+	Response      *openrtb2.BidResponse
+	VideoRequest  *openrtb_ext.BidRequestVideo
+	VideoResponse *openrtb_ext.BidResponseVideo
+	StartTime     time.Time
+}
+
+type logSetUID struct {
+	Status  int
+	Bidder  string
+	UID     string
+	Errors  []error
+	Success bool
+}
+
+type logUserSync struct {
+	Status       int
+	Errors       []error
+	BidderStatus []*analytics.CookieSyncBidder
+}
+
+type logAMP struct {
+	Status               int
+	Errors               []error
+	Request              *openrtb2.BidRequest
+	AuctionResponse      *openrtb2.BidResponse
+	AmpTargetingValues   map[string]string
+	Origin               string
+	StartTime            time.Time
+	HookExecutionOutcome []hookexecution.StageOutcome
+}
+
+type logNotificationEvent struct {
+	Request *analytics.EventRequest `json:"request"`
+	Account *config.Account         `json:"account"`
+}
diff --git a/analytics/pubstack/helpers/json.go b/analytics/pubstack/helpers/json.go
index f02f1120..cde6aef2 100644
--- a/analytics/pubstack/helpers/json.go
+++ b/analytics/pubstack/helpers/json.go
@@ -8,12 +8,25 @@ import (
 )
 
 func JsonifyAuctionObject(ao *analytics.AuctionObject, scope string) ([]byte, error) {
+	var logEntry *logAuction
+	if ao != nil {
+		logEntry = &logAuction{
+			Status:               ao.Status,
+			Errors:               ao.Errors,
+			Request:              ao.Request,
+			Response:             ao.Response,
+			Account:              ao.Account,
+			StartTime:            ao.StartTime,
+			HookExecutionOutcome: ao.HookExecutionOutcome,
+		}
+	}
+
 	b, err := json.Marshal(&struct {
 		Scope string `json:"scope"`
-		*analytics.AuctionObject
+		*logAuction
 	}{
-		Scope:         scope,
-		AuctionObject: ao,
+		Scope:      scope,
+		logAuction: logEntry,
 	})
 
 	if err == nil {
@@ -24,12 +37,25 @@ func JsonifyAuctionObject(ao *analytics.AuctionObject, scope string) ([]byte, er
 }
 
 func JsonifyVideoObject(vo *analytics.VideoObject, scope string) ([]byte, error) {
+	var logEntry *logVideo
+	if vo != nil {
+		logEntry = &logVideo{
+			Status:        vo.Status,
+			Errors:        vo.Errors,
+			Request:       vo.Request,
+			Response:      vo.Response,
+			VideoRequest:  vo.VideoRequest,
+			VideoResponse: vo.VideoResponse,
+			StartTime:     vo.StartTime,
+		}
+	}
+
 	b, err := json.Marshal(&struct {
 		Scope string `json:"scope"`
-		*analytics.VideoObject
+		*logVideo
 	}{
-		Scope:       scope,
-		VideoObject: vo,
+		Scope:    scope,
+		logVideo: logEntry,
 	})
 
 	if err == nil {
@@ -40,12 +66,21 @@ func JsonifyVideoObject(vo *analytics.VideoObject, scope string) ([]byte, error)
 }
 
 func JsonifyCookieSync(cso *analytics.CookieSyncObject, scope string) ([]byte, error) {
+	var logEntry *logUserSync
+	if cso != nil {
+		logEntry = &logUserSync{
+			Status:       cso.Status,
+			Errors:       cso.Errors,
+			BidderStatus: cso.BidderStatus,
+		}
+	}
+
 	b, err := json.Marshal(&struct {
 		Scope string `json:"scope"`
-		*analytics.CookieSyncObject
+		*logUserSync
 	}{
-		Scope:            scope,
-		CookieSyncObject: cso,
+		Scope:       scope,
+		logUserSync: logEntry,
 	})
 
 	if err == nil {
@@ -56,12 +91,23 @@ func JsonifyCookieSync(cso *analytics.CookieSyncObject, scope string) ([]byte, e
 }
 
 func JsonifySetUIDObject(so *analytics.SetUIDObject, scope string) ([]byte, error) {
+	var logEntry *logSetUID
+	if so != nil {
+		logEntry = &logSetUID{
+			Status:  so.Status,
+			Bidder:  so.Bidder,
+			UID:     so.UID,
+			Errors:  so.Errors,
+			Success: so.Success,
+		}
+	}
+
 	b, err := json.Marshal(&struct {
 		Scope string `json:"scope"`
-		*analytics.SetUIDObject
+		*logSetUID
 	}{
-		Scope:        scope,
-		SetUIDObject: so,
+		Scope:     scope,
+		logSetUID: logEntry,
 	})
 
 	if err == nil {
@@ -72,12 +118,26 @@ func JsonifySetUIDObject(so *analytics.SetUIDObject, scope string) ([]byte, erro
 }
 
 func JsonifyAmpObject(ao *analytics.AmpObject, scope string) ([]byte, error) {
+	var logEntry *logAMP
+	if ao != nil {
+		logEntry = &logAMP{
+			Status:               ao.Status,
+			Errors:               ao.Errors,
+			Request:              ao.Request,
+			AuctionResponse:      ao.AuctionResponse,
+			AmpTargetingValues:   ao.AmpTargetingValues,
+			Origin:               ao.Origin,
+			StartTime:            ao.StartTime,
+			HookExecutionOutcome: ao.HookExecutionOutcome,
+		}
+	}
+
 	b, err := json.Marshal(&struct {
 		Scope string `json:"scope"`
-		*analytics.AmpObject
+		*logAMP
 	}{
-		Scope:     scope,
-		AmpObject: ao,
+		Scope:  scope,
+		logAMP: logEntry,
 	})
 
 	if err == nil {
diff --git a/analytics/pubstack/helpers/model.go b/analytics/pubstack/helpers/model.go
new file mode 100644
index 00000000..91b86d7f
--- /dev/null
+++ b/analytics/pubstack/helpers/model.go
@@ -0,0 +1,56 @@
+package helpers
+
+import (
+	"time"
+
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/analytics"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/hooks/hookexecution"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+type logAuction struct {
+	Status               int
+	Errors               []error
+	Request              *openrtb2.BidRequest
+	Response             *openrtb2.BidResponse
+	Account              *config.Account
+	StartTime            time.Time
+	HookExecutionOutcome []hookexecution.StageOutcome
+}
+
+type logVideo struct {
+	Status        int
+	Errors        []error
+	Request       *openrtb2.BidRequest
+	Response      *openrtb2.BidResponse
+	VideoRequest  *openrtb_ext.BidRequestVideo
+	VideoResponse *openrtb_ext.BidResponseVideo
+	StartTime     time.Time
+}
+
+type logSetUID struct {
+	Status  int
+	Bidder  string
+	UID     string
+	Errors  []error
+	Success bool
+}
+
+type logUserSync struct {
+	Status       int
+	Errors       []error
+	BidderStatus []*analytics.CookieSyncBidder
+}
+
+type logAMP struct {
+	Status               int
+	Errors               []error
+	Request              *openrtb2.BidRequest
+	AuctionResponse      *openrtb2.BidResponse
+	AmpTargetingValues   map[string]string
+	Origin               string
+	StartTime            time.Time
+	HookExecutionOutcome []hookexecution.StageOutcome
+}
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/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index fb9af8ca..36248a88 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -27,6 +27,7 @@ import (
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/exchange"
+	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/hooks"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -219,6 +220,8 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 		return
 	}
 
+	tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR)
+
 	secGPC := r.Header.Get("Sec-GPC")
 
 	auctionRequest := &exchange.AuctionRequest{
@@ -235,6 +238,7 @@ func (deps *endpointDeps) AmpAuction(w http.ResponseWriter, r *http.Request, _ h
 		PubID:                      labels.PubID,
 		HookExecutor:               hookExecutor,
 		QueryParams:                r.URL.Query(),
+		TCF2Config:                 tcf2Config,
 	}
 
 	response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index 2811a9e7..62353711 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"
@@ -36,6 +37,7 @@ import (
 	"github.com/prebid/prebid-server/currency"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/exchange"
+	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/hooks/hookexecution"
 	"github.com/prebid/prebid-server/metrics"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -185,6 +187,8 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		return
 	}
 
+	tcf2Config := gdpr.NewTCF2Config(deps.cfg.GDPR.TCF2, account.GDPR)
+
 	ctx := context.Background()
 
 	timeout := deps.cfg.AuctionTimeouts.LimitAuctionTimeout(time.Duration(req.TMax) * time.Millisecond)
@@ -229,6 +233,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		BidderImpReplaceImpID:      bidderImpReplaceImp,
 		PubID:                      labels.PubID,
 		HookExecutor:               hookExecutor,
+		TCF2Config:                 tcf2Config,
 	}
 	response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
 	ao.Request = req.BidRequest
@@ -723,7 +728,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 +1562,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/adapter_builders.go b/exchange/adapter_builders.go
index 7a2ffd54..11de45aa 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -72,6 +72,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/dianomi"
 	"github.com/prebid/prebid-server/adapters/dmx"
 	evolution "github.com/prebid/prebid-server/adapters/e_volution"
+	"github.com/prebid/prebid-server/adapters/emtv"
 	"github.com/prebid/prebid-server/adapters/emx_digital"
 	"github.com/prebid/prebid-server/adapters/engagebdr"
 	"github.com/prebid/prebid-server/adapters/eplanning"
@@ -197,6 +198,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderAdprime:           adprime.Builder,
 		openrtb_ext.BidderAdrino:            adrino.Builder,
 		openrtb_ext.BidderAdsinteractive:    adsinteractive.Builder,
+		openrtb_ext.BidderAdsyield:          limelightDigital.Builder,
 		openrtb_ext.BidderAdtarget:          adtarget.Builder,
 		openrtb_ext.BidderAdtrgtme:          adtrgtme.Builder,
 		openrtb_ext.BidderAdtelligent:       adtelligent.Builder,
@@ -247,6 +249,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderDefinemedia:       definemedia.Builder,
 		openrtb_ext.BidderDianomi:           dianomi.Builder,
 		openrtb_ext.BidderDmx:               dmx.Builder,
+		openrtb_ext.BidderEmtv:              emtv.Builder,
 		openrtb_ext.BidderEmxDigital:        emx_digital.Builder,
 		openrtb_ext.BidderEngageBDR:         engagebdr.Builder,
 		openrtb_ext.BidderEPlanning:         eplanning.Builder,
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 8e6f133f..f5efcff3 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 != nil && 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..c1b46d33 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"
@@ -64,7 +65,6 @@ type exchange struct {
 	cache                    prebid_cache_client.Client
 	cacheTime                time.Duration
 	gdprPermsBuilder         gdpr.PermissionsBuilder
-	tcf2ConfigBuilder        gdpr.TCF2ConfigBuilder
 	currencyConverter        *currency.RateConverter
 	externalURL              string
 	gdprDefaultValue         gdpr.Signal
@@ -125,7 +125,7 @@ func (randomDeduplicateBidBooleanGenerator) Generate() bool {
 	return rand.Intn(100) < 50
 }
 
-func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gdprPermsBuilder gdpr.PermissionsBuilder, tcf2CfgBuilder gdpr.TCF2ConfigBuilder, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher, adsCertSigner adscert.Signer) Exchange {
+func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid_cache_client.Client, cfg *config.Configuration, syncersByBidder map[string]usersync.Syncer, metricsEngine metrics.MetricsEngine, infos config.BidderInfos, gdprPermsBuilder gdpr.PermissionsBuilder, currencyConverter *currency.RateConverter, categoriesFetcher stored_requests.CategoryFetcher, adsCertSigner adscert.Signer) Exchange {
 	bidderToSyncerKey := map[string]string{}
 	for bidder, syncer := range syncersByBidder {
 		bidderToSyncerKey[bidder] = syncer.Key()
@@ -146,7 +146,6 @@ func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid
 		me:                metricsEngine,
 		privacyConfig:     privacyConfig,
 		gdprPermsBuilder:  gdprPermsBuilder,
-		tcf2ConfigBuilder: tcf2CfgBuilder,
 		hostSChainNode:    cfg.HostSChainNode,
 		bidderInfo:        infos,
 	}
@@ -161,7 +160,6 @@ func NewExchange(adapters map[openrtb_ext.BidderName]AdaptedBidder, cache prebid
 		currencyConverter:        currencyConverter,
 		externalURL:              cfg.ExternalURL,
 		gdprPermsBuilder:         gdprPermsBuilder,
-		tcf2ConfigBuilder:        tcf2CfgBuilder,
 		me:                       metricsEngine,
 		gdprDefaultValue:         gdprDefaultValue,
 		privacyConfig:            privacyConfig,
@@ -193,6 +191,7 @@ type AuctionRequest struct {
 	Warnings                   []error
 	GlobalPrivacyControlHeader string
 	ImpExtInfoMap              map[string]ImpExtInfo
+	TCF2Config                 gdpr.TCF2ConfigReader
 
 	// LegacyLabels is included here for temporary compatibility with cleanOpenRTBRequests
 	// in HoldAuction until we get to factoring it away. Do not use for anything new.
@@ -317,6 +316,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 +363,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 +636,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 +678,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/exchange/gdpr.go b/exchange/gdpr.go
index c8b205ad..e6a8d779 100644
--- a/exchange/gdpr.go
+++ b/exchange/gdpr.go
@@ -1,59 +1,40 @@
 package exchange
 
 import (
-	"encoding/json"
-
 	gpplib "github.com/prebid/go-gpp"
 	gppConstants "github.com/prebid/go-gpp/constants"
-	"github.com/prebid/openrtb/v19/openrtb2"
 	"github.com/prebid/prebid-server/gdpr"
+	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-// ExtractGDPR will pull the gdpr flag from an openrtb request
-func extractGDPR(bidRequest *openrtb2.BidRequest) (gdpr.Signal, error) {
-	var re regsExt
-	var err error
-
-	if bidRequest.Regs != nil && len(bidRequest.Regs.GPPSID) > 0 {
-		for _, id := range bidRequest.Regs.GPPSID {
+// getGDPR will pull the gdpr flag from an openrtb request
+func getGDPR(req *openrtb_ext.RequestWrapper) (gdpr.Signal, error) {
+	if req.Regs != nil && len(req.Regs.GPPSID) > 0 {
+		for _, id := range req.Regs.GPPSID {
 			if id == int8(gppConstants.SectionTCFEU2) {
 				return gdpr.SignalYes, nil
 			}
 		}
 		return gdpr.SignalNo, nil
 	}
-	if bidRequest.Regs != nil && bidRequest.Regs.Ext != nil {
-		err = json.Unmarshal(bidRequest.Regs.Ext, &re)
-	}
-	if re.GDPR == nil || err != nil {
+	re, err := req.GetRegExt()
+	if re == nil || re.GetGDPR() == nil || err != nil {
 		return gdpr.SignalAmbiguous, err
 	}
-	return gdpr.Signal(*re.GDPR), nil
+	return gdpr.Signal(*re.GetGDPR()), nil
 }
 
-// ExtractConsent will pull the consent string from an openrtb request
-func extractConsent(bidRequest *openrtb2.BidRequest, gpp gpplib.GppContainer) (consent string, err error) {
+// getConsent will pull the consent string from an openrtb request
+func getConsent(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) (consent string, err error) {
 	for i, id := range gpp.SectionTypes {
 		if id == gppConstants.SectionTCFEU2 {
 			consent = gpp.Sections[i].GetValue()
 			return
 		}
 	}
-	var ue userExt
-	if bidRequest.User != nil && bidRequest.User.Ext != nil {
-		err = json.Unmarshal(bidRequest.User.Ext, &ue)
-	}
-	if err != nil {
+	ue, err := req.GetUserExt()
+	if ue == nil || ue.GetConsent() == nil || err != nil {
 		return
 	}
-	consent = ue.Consent
-	return
-}
-
-type userExt struct {
-	Consent string `json:"consent,omitempty"`
-}
-
-type regsExt struct {
-	GDPR *int `json:"gdpr,omitempty"`
+	return *ue.GetConsent(), nil
 }
diff --git a/exchange/utils.go b/exchange/utils.go
index d6b30a8c..de74c10d 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -41,7 +41,6 @@ type requestSplitter struct {
 	me                metrics.MetricsEngine
 	privacyConfig     config.Privacy
 	gdprPermsBuilder  gdpr.PermissionsBuilder
-	tcf2ConfigBuilder gdpr.TCF2ConfigBuilder
 	hostSChainNode    *openrtb2.SupplyChainNode
 	bidderInfo        config.BidderInfos
 }
@@ -93,12 +92,12 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 		}
 	}
 
-	gdprSignal, err := extractGDPR(req.BidRequest)
+	gdprSignal, err := getGDPR(req)
 	if err != nil {
 		errs = append(errs, err)
 	}
 
-	consent, err := extractConsent(req.BidRequest, gpp)
+	consent, err := getConsent(req, gpp)
 	if err != nil {
 		errs = append(errs, err)
 	}
@@ -122,13 +121,11 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 	privacyLabels.COPPAEnforced = privacyEnforcement.COPPA
 	privacyLabels.LMTEnforced = lmtEnforcer.ShouldEnforce(unknownBidder)
 
-	tcf2Cfg := rs.tcf2ConfigBuilder(rs.privacyConfig.GDPR.TCF2, auctionReq.Account.GDPR)
-
 	var gdprEnforced bool
 	var gdprPerms gdpr.Permissions = &gdpr.AlwaysAllow{}
 
 	if gdprApplies {
-		gdprEnforced = tcf2Cfg.ChannelEnabled(channelTypeMap[auctionReq.LegacyLabels.RType])
+		gdprEnforced = auctionReq.TCF2Config.ChannelEnabled(channelTypeMap[auctionReq.LegacyLabels.RType])
 	}
 
 	if gdprEnforced {
@@ -145,7 +142,7 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 			GDPRSignal:  gdprSignal,
 			PublisherID: auctionReq.LegacyLabels.PubID,
 		}
-		gdprPerms = rs.gdprPermsBuilder(tcf2Cfg, gdprRequestInfo)
+		gdprPerms = rs.gdprPermsBuilder(auctionReq.TCF2Config, gdprRequestInfo)
 	}
 
 	// bidder level privacy policies
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index a5a1ecff..f976fc77 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -106,6 +106,7 @@ const (
 	BidderAdprime           BidderName = "adprime"
 	BidderAdrino            BidderName = "adrino"
 	BidderAdsinteractive    BidderName = "adsinteractive"
+	BidderAdsyield          BidderName = "adsyield"
 	BidderAdtarget          BidderName = "adtarget"
 	BidderAdtrgtme          BidderName = "adtrgtme"
 	BidderAdtelligent       BidderName = "adtelligent"
@@ -156,6 +157,7 @@ const (
 	BidderDefinemedia       BidderName = "definemedia"
 	BidderDianomi           BidderName = "dianomi"
 	BidderDmx               BidderName = "dmx"
+	BidderEmtv              BidderName = "emtv"
 	BidderEmxDigital        BidderName = "emx_digital"
 	BidderEngageBDR         BidderName = "engagebdr"
 	BidderEPlanning         BidderName = "eplanning"
@@ -293,6 +295,7 @@ func CoreBidderNames() []BidderName {
 		BidderAdprime,
 		BidderAdrino,
 		BidderAdsinteractive,
+		BidderAdsyield,
 		BidderAdtarget,
 		BidderAdtrgtme,
 		BidderAdtelligent,
@@ -343,6 +346,7 @@ func CoreBidderNames() []BidderName {
 		BidderDefinemedia,
 		BidderDianomi,
 		BidderDmx,
+		BidderEmtv,
 		BidderEmxDigital,
 		BidderEngageBDR,
 		BidderEPlanning,
diff --git a/openrtb_ext/imp_appnexus.go b/openrtb_ext/imp_appnexus.go
index 56dc0dbb..c1f1a856 100644
--- a/openrtb_ext/imp_appnexus.go
+++ b/openrtb_ext/imp_appnexus.go
@@ -2,31 +2,92 @@ package openrtb_ext
 
 import (
 	"encoding/json"
+	"fmt"
+	"strings"
 
 	"github.com/prebid/prebid-server/util/jsonutil"
 )
 
 // ExtImpAppnexus defines the contract for bidrequest.imp[i].ext.prebid.bidder.appnexus
 type ExtImpAppnexus struct {
-	DeprecatedPlacementId    jsonutil.StringInt      `json:"placementId"`
-	LegacyInvCode            string                  `json:"invCode"`
-	LegacyTrafficSourceCode  string                  `json:"trafficSourceCode"`
-	PlacementId              jsonutil.StringInt      `json:"placement_id"`
-	InvCode                  string                  `json:"inv_code"`
-	Member                   string                  `json:"member"`
-	Keywords                 []*ExtImpAppnexusKeyVal `json:"keywords"`
-	TrafficSourceCode        string                  `json:"traffic_source_code"`
-	Reserve                  float64                 `json:"reserve"`
-	Position                 string                  `json:"position"`
-	UsePaymentRule           *bool                   `json:"use_pmt_rule"`
-	DeprecatedUsePaymentRule *bool                   `json:"use_payment_rule"`
+	DeprecatedPlacementId    jsonutil.StringInt     `json:"placementId"`
+	LegacyInvCode            string                 `json:"invCode"`
+	LegacyTrafficSourceCode  string                 `json:"trafficSourceCode"`
+	PlacementId              jsonutil.StringInt     `json:"placement_id"`
+	InvCode                  string                 `json:"inv_code"`
+	Member                   string                 `json:"member"`
+	Keywords                 ExtImpAppnexusKeywords `json:"keywords"`
+	TrafficSourceCode        string                 `json:"traffic_source_code"`
+	Reserve                  float64                `json:"reserve"`
+	Position                 string                 `json:"position"`
+	UsePaymentRule           *bool                  `json:"use_pmt_rule"`
+	DeprecatedUsePaymentRule *bool                  `json:"use_payment_rule"`
 	// At this time we do no processing on the private sizes, so just leaving it as a JSON blob.
 	PrivateSizes json.RawMessage `json:"private_sizes"`
 	AdPodId      bool            `json:"generate_ad_pod_id"`
 }
 
-// ExtImpAppnexusKeyVal defines the contract for bidrequest.imp[i].ext.prebid.bidder.appnexus.keywords[i]
-type ExtImpAppnexusKeyVal struct {
+type ExtImpAppnexusKeywords string
+
+// extImpAppnexusKeyVal defines the contract for bidrequest.imp[i].ext.prebid.bidder.appnexus.keywords[i]
+type extImpAppnexusKeyVal struct {
 	Key    string   `json:"key,omitempty"`
 	Values []string `json:"value,omitempty"`
 }
+
+func (ks *ExtImpAppnexusKeywords) UnmarshalJSON(b []byte) error {
+	if len(b) == 0 {
+		return nil
+	}
+
+	switch b[0] {
+	case '{':
+		var results map[string][]string
+		if err := json.Unmarshal(b, &results); err != nil {
+			return err
+		}
+
+		var keywords strings.Builder
+		for key, values := range results {
+			if len(values) == 0 {
+				keywords.WriteString(fmt.Sprintf("%s,", key))
+			} else {
+				for _, val := range values {
+					keywords.WriteString(fmt.Sprintf("%s=%s,", key, val))
+				}
+			}
+		}
+		if len(keywords.String()) > 0 {
+			*ks = ExtImpAppnexusKeywords(keywords.String()[:keywords.Len()-1])
+		}
+	case '[':
+		var results []extImpAppnexusKeyVal
+		if err := json.Unmarshal(b, &results); err != nil {
+			return err
+		}
+		var kvs strings.Builder
+		for _, kv := range results {
+			if len(kv.Values) == 0 {
+				kvs.WriteString(fmt.Sprintf("%s,", kv.Key))
+			} else {
+				for _, val := range kv.Values {
+					kvs.WriteString(fmt.Sprintf("%s=%s,", kv.Key, val))
+				}
+			}
+		}
+		if len(kvs.String()) > 0 {
+			*ks = ExtImpAppnexusKeywords(kvs.String()[:kvs.Len()-1])
+		}
+	case '"':
+		var keywords string
+		if err := json.Unmarshal(b, &keywords); err != nil {
+			return err
+		}
+		*ks = ExtImpAppnexusKeywords(keywords)
+	}
+	return nil
+}
+
+func (ks *ExtImpAppnexusKeywords) String() string {
+	return *(*string)(ks)
+}
diff --git a/openrtb_ext/imp_emtv.go b/openrtb_ext/imp_emtv.go
new file mode 100644
index 00000000..a7701074
--- /dev/null
+++ b/openrtb_ext/imp_emtv.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+type ImpExtEmtv struct {
+	PlacementID string `json:"placementId"`
+	EndpointID  string `json:"endpointId"`
+}
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"`
diff --git a/router/router.go b/router/router.go
index 23cecff5..de120cfe 100644
--- a/router/router.go
+++ b/router/router.go
@@ -214,7 +214,7 @@ func New(cfg *config.Configuration, rateConvertor *currency.RateConverter) (r *R
 	}
 
 	planBuilder := hooks.NewExecutionPlanBuilder(cfg.Hooks, repo)
-	theExchange := exchange.NewExchange(adapters, cacheClient, cfg, syncersByBidder, r.MetricsEngine, cfg.BidderInfos, gdprPermsBuilder, tcf2CfgBuilder, rateConvertor, categoriesFetcher, adsCertSigner)
+	theExchange := exchange.NewExchange(adapters, cacheClient, cfg, syncersByBidder, r.MetricsEngine, cfg.BidderInfos, gdprPermsBuilder, rateConvertor, categoriesFetcher, adsCertSigner)
 	var uuidGenerator uuidutil.UUIDRandomGenerator
 	openrtbEndpoint, err := openrtb2.NewEndpoint(uuidGenerator, theExchange, paramsValidator, fetcher, accounts, cfg, r.MetricsEngine, pbsAnalytics, disabledBidders, defReqJSON, activeBidders, storedRespFetcher, planBuilder)
 	if err != nil {
diff --git a/static/adapter/appnexus/opts.json b/static/adapter/appnexus/opts.json
deleted file mode 100644
index 41ee3c8f..00000000
--- a/static/adapter/appnexus/opts.json
+++ /dev/null
@@ -1,99 +0,0 @@
-{
-  "iab_categories": {
-    "1": "IAB20-3",
-    "2": "IAB18-5",
-    "3": "IAB10-1",
-    "4": "IAB2-3",
-    "5": "IAB19-8",
-    "6": "IAB22-1",
-    "7": "IAB18-1",
-    "8": "IAB12-3",
-    "9": "IAB5-1",
-    "10": "IAB4-5",
-    "11": "IAB13-4",
-    "12": "IAB8-7",
-    "13": "IAB9-7",
-    "14": "IAB7-1",
-    "15": "IAB20-18",
-    "16": "IAB10-7",
-    "17": "IAB19-18",
-    "18": "IAB13-6",
-    "19": "IAB18-4",
-    "20": "IAB1-5",
-    "21": "IAB1-6",
-    "22": "IAB3-4",
-    "23": "IAB19-13",
-    "24": "IAB22-2",
-    "25": "IAB3-9",
-    "26": "IAB17-18",
-    "27": "IAB19-6",
-    "28": "IAB1-7",
-    "29": "IAB9-30",
-    "30": "IAB20-7",
-    "31": "IAB20-17",
-    "32": "IAB7-32",
-    "33": "IAB16-5",
-    "34": "IAB19-34",
-    "35": "IAB11-5",
-    "36": "IAB12-3",
-    "37": "IAB11-4",
-    "38": "IAB12-3",
-    "39": "IAB9-30",
-    "41": "IAB7-44",
-    "42": "IAB7-1",
-    "43": "IAB7-30",
-    "50": "IAB19-30",
-    "51": "IAB17-12",
-    "52": "IAB19-30",
-    "53": "IAB3-1",
-    "55": "IAB13-2",
-    "56": "IAB19-30",
-    "57": "IAB19-30",
-    "58": "IAB7-39",
-    "59": "IAB22-1",
-    "60": "IAB7-39",
-    "61": "IAB21-3",
-    "62": "IAB5-1",
-    "63": "IAB12-3",
-    "64": "IAB20-18",
-    "65": "IAB11-2",
-    "66": "IAB17-18",
-    "67": "IAB9-9",
-    "68": "IAB9-5",
-    "69": "IAB7-44",
-    "71": "IAB22-3",
-    "73": "IAB19-30",
-    "74": "IAB8-5",
-    "78": "IAB22-1",
-    "85": "IAB12-2",
-    "86": "IAB22-3",
-    "87": "IAB11-3",
-    "112": "IAB7-32",
-    "113": "IAB7-32",
-    "114": "IAB7-32",
-    "115": "IAB7-32",
-    "118": "IAB9-5",
-    "119": "IAB9-5",
-    "120": "IAB9-5",
-    "121": "IAB9-5",
-    "122": "IAB9-5",
-    "123": "IAB9-5",
-    "124": "IAB9-5",
-    "125": "IAB9-5",
-    "126": "IAB9-5",
-    "127": "IAB22-1",
-    "132": "IAB1-2",
-    "133": "IAB19-30",
-    "137": "IAB3-9",
-    "138": "IAB19-3",
-    "140": "IAB2-3",
-    "141": "IAB2-1",
-    "142": "IAB2-3",
-    "143": "IAB17-13",
-    "166": "IAB11-4",
-    "175": "IAB3-1",
-    "176": "IAB13-4",
-    "182": "IAB8-9",
-    "183": "IAB3-5"
-    }
-}
diff --git a/static/bidder-info/adsyield.yaml b/static/bidder-info/adsyield.yaml
new file mode 100644
index 00000000..dadd9cc3
--- /dev/null
+++ b/static/bidder-info/adsyield.yaml
@@ -0,0 +1,16 @@
+endpoint: "http://ads-pbs.open-adsyield.com/openrtb/{{.PublisherID}}?host={{.Host}}"
+maintainer:
+  email: "engineering@project-limelight.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+      - audio
+      - native
+  site:
+    mediaTypes:
+      - banner
+      - video
+      - audio
+      - native
diff --git a/static/bidder-info/emtv.yaml b/static/bidder-info/emtv.yaml
new file mode 100644
index 00000000..6c9b3a11
--- /dev/null
+++ b/static/bidder-info/emtv.yaml
@@ -0,0 +1,19 @@
+endpoint: "https://us-east-ep.engagemedia.tv/pserver"
+maintainer:
+  email: "support@engagemedia.tv"
+capabilities:
+  site:
+    mediaTypes:
+    - banner
+    - video
+    - native
+    
+  app:
+    mediaTypes:
+    - banner
+    - video
+    - native
+userSync:
+  redirect:
+    url: "https://cs.engagemedia.tv/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}"
+    userMacro: "[UID]"
diff --git a/static/bidder-params/adsyield.json b/static/bidder-params/adsyield.json
new file mode 100644
index 00000000..c7c53088
--- /dev/null
+++ b/static/bidder-params/adsyield.json
@@ -0,0 +1,23 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Adsyield Adapter Params",
+  "description": "A schema which validates params accepted by the Adsyield adapter",
+  "type": "object",
+
+  "properties": {
+    "host": {
+      "type": "string",
+      "description": "Ad network's RTB host",
+      "format": "hostname",
+      "pattern": "^.+\\..+$"
+    },
+    "publisherId": {
+      "type": ["integer", "string"],
+      "description": "Publisher ID",
+      "minimum": 1,
+      "pattern": "^[1-9][0-9]*$"
+    }
+  },
+
+  "required": ["host", "publisherId"]
+}
diff --git a/static/bidder-params/appnexus.json b/static/bidder-params/appnexus.json
index c3e47129..27b9bfe9 100644
--- a/static/bidder-params/appnexus.json
+++ b/static/bidder-params/appnexus.json
@@ -26,25 +26,42 @@
       "description": "An ID which identifies the member selling the impression."
     },
     "keywords": {
-      "type": "array",
-      "minItems": 1,
-      "items": {
-        "type": "object",
-        "description": "A key with one or more values associated with it. These are used in buy-side segment targeting.",
-        "properties": {
-          "key": {
-            "type": "string"
-          },
-          "value": {
+      "anyOf": [
+        {
+          "type": "string",
+          "minLength": 1
+        },
+        {
+          "type": "array",
+          "minItems": 1,
+          "items": {
+            "type": "object",
+            "description": "A key with one or more values associated with it. These are used in buy-side segment targeting.",
+            "properties": {
+              "key": {
+                "type": "string"
+              },
+              "value": {
+                "type": "array",
+                "minItems": 1,
+                "items": {
+                  "type": "string"
+                }
+              }
+            },
+            "required": ["key"]
+          }
+        },
+        {
+          "type": "object",
+          "additionalProperties": {
             "type": "array",
-            "minItems": 1,
             "items": {
               "type": "string"
             }
           }
-        },
-        "required": ["key"]
-      }
+        }
+      ]
     },
     "traffic_source_code": {
       "type": "string",
diff --git a/static/bidder-params/emtv.json b/static/bidder-params/emtv.json
new file mode 100644
index 00000000..a491b825
--- /dev/null
+++ b/static/bidder-params/emtv.json
@@ -0,0 +1,23 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "EMTV Adapter Params",
+    "description": "A schema which validates params accepted by the EMTV adapter",
+    "type": "object",
+  
+    "properties": {
+      "placementId": {
+        "type": "string",
+        "minLength": 1,
+        "description": "Placement ID"
+      },
+      "endpointId": {
+        "type": "string",
+        "minLength": 1,
+        "description": "Endpoint ID"
+      }
+    },
+    "oneOf": [
+      { "required": ["placementId"] },
+      { "required": ["endpointId"] }
+    ]
+  }
\ No newline at end of file


Test patch:
diff --git a/adapters/appnexus/appnexus_test.go b/adapters/appnexus/appnexus_test.go
index 6797f4bb..0e0cfc5b 100644
--- a/adapters/appnexus/appnexus_test.go
+++ b/adapters/appnexus/appnexus_test.go
@@ -6,6 +6,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/adapterstest"
 	"github.com/prebid/prebid-server/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/stretchr/testify/assert"
 )
 
 func TestJsonSamples(t *testing.T) {
@@ -30,6 +31,15 @@ func TestMemberQueryParam(t *testing.T) {
 	}
 }
 
+func TestBuilderWithPlatformID(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderAppnexus, config.Adapter{
+		Endpoint: "http://ib.adnxs.com/openrtb2", PlatformID: "3"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+	assert.NoError(t, buildErr)
+	assert.NotNil(t, bidder)
+	assert.Equal(t, 3, (*bidder.(*adapter)).hbSource)
+}
+
 // fakerandomNumberGenerator
 type FakeRandomNumberGenerator struct {
 	Number int64
diff --git a/adapters/appnexus/appnexustest/amp/simple-video.json b/adapters/appnexus/appnexustest/amp/simple-video.json
index 061d5c94..c9744831 100644
--- a/adapters/appnexus/appnexustest/amp/simple-video.json
+++ b/adapters/appnexus/appnexustest/amp/simple-video.json
@@ -113,7 +113,7 @@
 			  "crid": "29681110",
 			  "w": 300,
 			  "h": 250,
-			  "cat": ["IAB5-3"],
+			  "cat": ["IAB5-1"],
 			  "ext": {
 				"appnexus": {
 				  "brand_id": 9,
diff --git a/adapters/appnexus/appnexustest/exemplary/dynamic-keywords-params.json b/adapters/appnexus/appnexustest/exemplary/dynamic-keywords-params.json
new file mode 100644
index 00000000..869c32d8
--- /dev/null
+++ b/adapters/appnexus/appnexustest/exemplary/dynamic-keywords-params.json
@@ -0,0 +1,147 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "member": "103",
+            "inv_code": "abc",
+            "reserve": 20,
+            "position": "below",
+            "traffic_source_code": "trafficSource",
+            "keywords": {"abc": ["bas"]},
+            "use_pmt_rule": true,
+            "private_sizes": [{"w": 300, "h": 250}]
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ib.adnxs.com/openrtb2?member_id=103",
+        "body": {
+          "id": "test-request-id",
+          "ext": {
+            "appnexus": {
+              "hb_source": 5
+            },
+            "prebid": {}
+          },
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "banner": {
+                "format": [
+                  {"w":300,"h":250},
+                  {"w":300,"h":600}
+                ],
+                "w": 300,
+                "h": 250,
+                "pos": 3
+              },
+              "tagid": "abc",
+              "bidfloor": 20,
+              "ext": {
+                "appnexus": {
+                  "keywords": "abc=bas",
+                  "traffic_source_code": "trafficSource",
+                  "use_pmt_rule": true,
+                  "private_sizes": [{"w": 300, "h": 250}]
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [{
+                "id": "7706636740145184841",
+                "impid": "test-imp-id",
+                "price": 0.500000,
+                "adid": "29681110",
+                "adm": "some-test-ad",
+                "adomain": ["appnexus.com"],
+                "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+                "cid": "958",
+                "crid": "29681110",
+                "h": 250,
+                "w": 300,
+                "ext": {
+                  "appnexus": {
+                    "brand_id": 1,
+                    "brand_category_id": 1,
+                    "auction_id": 8189378542222915032,
+                    "bid_ad_type": 0,
+                    "bidder_id": 2,
+                    "ranking_price": 0.000000,
+                    "deal_priority": 5
+                  }
+                }
+              }]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "7706636740145184841",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adm": "some-test-ad",
+            "adid": "29681110",
+            "adomain": ["appnexus.com"],
+            "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+            "cid": "958",
+            "crid": "29681110",
+            "w": 300,
+            "h": 250,
+            "cat": ["IAB20-3"],
+            "ext": {
+              "appnexus": {
+                "brand_id": 1,
+                "brand_category_id": 1,
+                "auction_id": 8189378542222915032,
+                "bid_ad_type": 0,
+                "bidder_id": 2,
+                "ranking_price": 0.000000,
+                "deal_priority": 5
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/appnexus/appnexustest/exemplary/simple-banner.json b/adapters/appnexus/appnexustest/exemplary/simple-banner.json
index 15a7666e..ec769abd 100644
--- a/adapters/appnexus/appnexustest/exemplary/simple-banner.json
+++ b/adapters/appnexus/appnexustest/exemplary/simple-banner.json
@@ -1,6 +1,14 @@
 {
   "mockBidRequest": {
     "id": "test-request-id",
+    "app": {
+      "ext": {
+        "prebid": {
+          "source": "source",
+          "version": "version"
+        }
+      }
+    },
     "imp": [
       {
         "id": "test-imp-id",
@@ -32,6 +40,14 @@
         "uri": "http://ib.adnxs.com/openrtb2",
         "body": {
           "id": "test-request-id",
+          "app": {
+            "ext": {
+              "prebid": {
+                "source": "source",
+                "version": "version"
+              }
+            }
+          },
           "ext": {
             "appnexus": {
               "hb_source": 5
@@ -56,6 +72,7 @@
                 "pos": 1,
                 "h": 250
               },
+              "displaymanagerver": "source-version",
               "ext": {
                 "appnexus": {
                   "placement_id": 1
diff --git a/adapters/appnexus/appnexustest/exemplary/simple-video.json b/adapters/appnexus/appnexustest/exemplary/simple-video.json
index ced90c39..a127e2a0 100644
--- a/adapters/appnexus/appnexustest/exemplary/simple-video.json
+++ b/adapters/appnexus/appnexustest/exemplary/simple-video.json
@@ -111,7 +111,7 @@
 			  "crid": "29681110",
 			  "w": 300,
 			  "h": 250,
-			  "cat": ["IAB5-3"],
+			  "cat": ["IAB5-1"],
 			  "ext": {
 				"appnexus": {
 				  "brand_id": 9,
diff --git a/adapters/appnexus/appnexustest/exemplary/string-keywords-params.json b/adapters/appnexus/appnexustest/exemplary/string-keywords-params.json
new file mode 100644
index 00000000..f43c47fe
--- /dev/null
+++ b/adapters/appnexus/appnexustest/exemplary/string-keywords-params.json
@@ -0,0 +1,147 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "member": "103",
+            "inv_code": "abc",
+            "reserve": 20,
+            "position": "below",
+            "traffic_source_code": "trafficSource",
+            "keywords": "foo=bar,foo=baz,valueless",
+            "use_pmt_rule": true,
+            "private_sizes": [{"w": 300, "h": 250}]
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ib.adnxs.com/openrtb2?member_id=103",
+        "body": {
+          "id": "test-request-id",
+          "ext": {
+            "appnexus": {
+              "hb_source": 5
+            },
+            "prebid": {}
+          },
+          "imp": [
+            {
+              "id":"test-imp-id",
+              "banner": {
+                "format": [
+                  {"w":300,"h":250},
+                  {"w":300,"h":600}
+                ],
+                "w": 300,
+                "h": 250,
+                "pos": 3
+              },
+              "tagid": "abc",
+              "bidfloor": 20,
+              "ext": {
+                "appnexus": {
+                  "keywords": "foo=bar,foo=baz,valueless",
+                  "traffic_source_code": "trafficSource",
+                  "use_pmt_rule": true,
+                  "private_sizes": [{"w": 300, "h": 250}]
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [{
+                "id": "7706636740145184841",
+                "impid": "test-imp-id",
+                "price": 0.500000,
+                "adid": "29681110",
+                "adm": "some-test-ad",
+                "adomain": ["appnexus.com"],
+                "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+                "cid": "958",
+                "crid": "29681110",
+                "h": 250,
+                "w": 300,
+                "ext": {
+                  "appnexus": {
+                    "brand_id": 1,
+                    "brand_category_id": 1,
+                    "auction_id": 8189378542222915032,
+                    "bid_ad_type": 0,
+                    "bidder_id": 2,
+                    "ranking_price": 0.000000,
+                    "deal_priority": 5
+                  }
+                }
+              }]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "7706636740145184841",
+            "impid": "test-imp-id",
+            "price": 0.5,
+            "adm": "some-test-ad",
+            "adid": "29681110",
+            "adomain": ["appnexus.com"],
+            "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+            "cid": "958",
+            "crid": "29681110",
+            "w": 300,
+            "h": 250,
+            "cat": ["IAB20-3"],
+            "ext": {
+              "appnexus": {
+                "brand_id": 1,
+                "brand_category_id": 1,
+                "auction_id": 8189378542222915032,
+                "bid_ad_type": 0,
+                "bidder_id": 2,
+                "ranking_price": 0.000000,
+                "deal_priority": 5
+              }
+            }
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/appnexus/appnexustest/supplemental/invalid-bid-type.json b/adapters/appnexus/appnexustest/supplemental/invalid-bid-type.json
new file mode 100644
index 00000000..9f5a07b4
--- /dev/null
+++ b/adapters/appnexus/appnexustest/supplemental/invalid-bid-type.json
@@ -0,0 +1,131 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "app": {
+      "ext": {
+        "prebid": {
+          "source": "source",
+          "version": "version"
+        }
+      }
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            },
+            {
+              "w": 300,
+              "h": 600
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "placement_id": 1,
+            "position": "above"
+          }
+        }
+      }
+    ]
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://ib.adnxs.com/openrtb2",
+        "body": {
+          "id": "test-request-id",
+          "app": {
+            "ext": {
+              "prebid": {
+                "source": "source",
+                "version": "version"
+              }
+            }
+          },
+          "ext": {
+            "appnexus": {
+              "hb_source": 5
+            },
+            "prebid": {}
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  },
+                  {
+                    "w": 300,
+                    "h": 600
+                  }
+                ],
+                "w": 300,
+                "pos": 1,
+                "h": 250
+              },
+              "displaymanagerver": "source-version",
+              "ext": {
+                "appnexus": {
+                  "placement_id": 1
+                }
+              }
+            }
+          ]
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-request-id",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [{
+                "id": "7706636740145184841",
+                "impid": "test-imp-id",
+                "price": 0.500000,
+                "adid": "29681110",
+                "adm": "some-test-ad",
+                "adomain": ["appnexus.com"],
+                "iurl": "http://nym1-ib.adnxs.com/cr?id=29681110",
+                "cid": "958",
+                "crid": "29681110",
+                "h": 250,
+                "w": 300,
+                "ext": {
+                  "appnexus": {
+                    "brand_id": 1,
+                    "brand_category_id": 1,
+                    "auction_id": 8189378542222915032,
+                    "bid_ad_type": 2,
+                    "bidder_id": 2,
+                    "ranking_price": 0.000000,
+                    "deal_priority": 5
+                  }
+                }
+              }]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unrecognized bid_ad_type in response from appnexus: 2",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/appnexus/params_test.go b/adapters/appnexus/params_test.go
index d12fba12..db94e3f2 100644
--- a/adapters/appnexus/params_test.go
+++ b/adapters/appnexus/params_test.go
@@ -50,6 +50,8 @@ var validParams = []string{
 	`{"placementId":123, "keywords":[{"key":"foo","value":["bar"]}]}`,
 	`{"placement_id":123, "keywords":[{"key":"foo","value":["bar", "baz"]}]}`,
 	`{"placement_id":123, "keywords":[{"key":"foo"}]}`,
+	`{"placement_id":123, "keywords":"foo=bar,foo=baz"}`,
+	`{"placement_id":123, "keywords":{"genre": ["rock", "pop"], "pets": ["dog"]}}`,
 	`{"placement_id":123, "use_pmt_rule": true, "private_sizes": [{"w": 300, "h":250}]}`,
 }
 
@@ -72,6 +74,7 @@ var invalidParams = []string{
 	`{"placement_id":123, "keywords":["foo"]}`,
 	`{"placementId":123, "keywords":[{"key":"foo","value":[]}]}`,
 	`{"placementId":123, "keywords":[{"value":["bar"]}]}`,
+	`{"placement_id":123, "keywords":{"genre": [12]}}`,
 	`{"placement_id":123, "use_pmt_rule": "true"}`,
 	`{"placement_id":123, "private_sizes": [[300,250]]}`,
 	`{"placement_id":123, "private_sizes": [{"w": "300", "h": "250"}]}`,
diff --git a/adapters/emtv/emtv_test.go b/adapters/emtv/emtv_test.go
new file mode 100644
index 00000000..b1287357
--- /dev/null
+++ b/adapters/emtv/emtv_test.go
@@ -0,0 +1,20 @@
+package emtv
+
+import (
+	"testing"
+
+	"github.com/prebid/prebid-server/adapters/adapterstest"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestJsonSamples(t *testing.T) {
+	bidder, buildErr := Builder(openrtb_ext.BidderEmtv, config.Adapter{
+		Endpoint: "http://example.com/pserver"}, config.Server{ExternalUrl: "http://hosturl.com", GvlID: 1, DataCenter: "2"})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "emtvtest", bidder)
+}
diff --git a/adapters/emtv/emtvtest/exemplary/endpointId.json b/adapters/emtv/emtvtest/exemplary/endpointId.json
new file mode 100644
index 00000000..ef73cb84
--- /dev/null
+++ b/adapters/emtv/emtvtest/exemplary/endpointId.json
@@ -0,0 +1,133 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "endpointId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "endpointId": "test",
+                                    "type": "network"
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "banner"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "emtv"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "prebid": {
+                                "type": "banner"
+                            }
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/emtv/emtvtest/exemplary/simple-banner.json b/adapters/emtv/emtvtest/exemplary/simple-banner.json
new file mode 100644
index 00000000..4399da28
--- /dev/null
+++ b/adapters/emtv/emtvtest/exemplary/simple-banner.json
@@ -0,0 +1,133 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "test",
+                                    "type": "publisher"
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "banner"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "emtv"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "prebid": {
+                                "type": "banner"
+                            }
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/emtv/emtvtest/exemplary/simple-native.json b/adapters/emtv/emtvtest/exemplary/simple-native.json
new file mode 100644
index 00000000..63bb7c73
--- /dev/null
+++ b/adapters/emtv/emtvtest/exemplary/simple-native.json
@@ -0,0 +1,117 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "native": {
+                    "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}",
+                    "ver": "1.1"
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "native": {
+                                "request": "{\"ver\":\"1.1\",\"layout\":1,\"adunit\":2,\"plcmtcnt\":6,\"plcmttype\":4,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":75}},{\"id\":2,\"required\":1,\"img\":{\"wmin\":492,\"hmin\":328,\"type\":3,\"mimes\":[\"image/jpeg\",\"image/jpg\",\"image/png\"]}},{\"id\":4,\"required\":0,\"data\":{\"type\":6}},{\"id\":5,\"required\":0,\"data\":{\"type\":7}},{\"id\":6,\"required\":0,\"data\":{\"type\":1,\"len\":20}}]}",
+                                "ver": "1.1"
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "test",
+                                    "type": "publisher"
+                                }
+                            }
+                        }
+                    ],
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 300,
+                                    "h": 250,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "native"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "emtv"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 300,
+                        "h": 250,
+                        "ext": {
+                            "prebid": {
+                                "type": "native"
+                            }
+                        }
+                    },
+                    "type": "native"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/emtv/emtvtest/exemplary/simple-video.json b/adapters/emtv/emtvtest/exemplary/simple-video.json
new file mode 100644
index 00000000..9bb3c20e
--- /dev/null
+++ b/adapters/emtv/emtvtest/exemplary/simple-video.json
@@ -0,0 +1,128 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "iPad"
+        },
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "video": {
+                    "mimes": [
+                        "video/mp4"
+                    ],
+                    "protocols": [
+                        2,
+                        5
+                    ],
+                    "w": 1024,
+                    "h": 576
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "iPad"
+                    },
+                    "app": {
+                        "id": "1",
+                        "bundle": "com.wls.testwlsapplication"
+                    },
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "video": {
+                                "mimes": [
+                                    "video/mp4"
+                                ],
+                                "protocols": [
+                                    2,
+                                    5
+                                ],
+                                "w": 1024,
+                                "h": 576
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "test",
+                                    "type": "publisher"
+                                }
+                            }
+                        }
+                    ]
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<VAST version=\"3.0\"><Ad id=\"20001\" sequence=\"1\"><InLine><AdSystem version=\"4.0\"><![CDATA[iabtechlab]]></AdSystem><AdTitle><![CDATA[Inline Simple Ad]]></AdTitle><Impression><![CDATA[]]></Impression><Creatives><Creative id=\"5480\" sequence=\"1\"><Linear><Duration>00:01:00</Duration><MediaFiles><MediaFile id=\"5246\" delivery=\"progressive\" type=\"video/mp4\" bitrate=\"600\" width=\"640\" height=\"360\" minBitrate=\"500\" maxBitrate=\"700\" scalable=\"1\" maintainAspectRatio=\"1\" codec=\"0\"><![CDATA[https://s0.2mdn.net/4253510/google_ddm_animation_480P.mp4]]></MediaFile></MediaFiles><VideoClicks><ClickThrough id=\"blog\"><![CDATA[https://example.com]]></ClickThrough></VideoClicks></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "video"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "emtv"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "currency": "USD",
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<VAST version=\"3.0\"><Ad id=\"20001\" sequence=\"1\"><InLine><AdSystem version=\"4.0\"><![CDATA[iabtechlab]]></AdSystem><AdTitle><![CDATA[Inline Simple Ad]]></AdTitle><Impression><![CDATA[]]></Impression><Creatives><Creative id=\"5480\" sequence=\"1\"><Linear><Duration>00:01:00</Duration><MediaFiles><MediaFile id=\"5246\" delivery=\"progressive\" type=\"video/mp4\" bitrate=\"600\" width=\"640\" height=\"360\" minBitrate=\"500\" maxBitrate=\"700\" scalable=\"1\" maintainAspectRatio=\"1\" codec=\"0\"><![CDATA[https://s0.2mdn.net/4253510/google_ddm_animation_480P.mp4]]></MediaFile></MediaFiles><VideoClicks><ClickThrough id=\"blog\"><![CDATA[https://example.com]]></ClickThrough></VideoClicks></Linear></Creative></Creatives></InLine></Ad></VAST>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "ext": {
+                            "prebid": {
+                                "type": "video"
+                            }
+                        }
+                    },
+                    "type": "video"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/emtv/emtvtest/exemplary/simple-web-banner.json b/adapters/emtv/emtvtest/exemplary/simple-web-banner.json
new file mode 100644
index 00000000..458ee258
--- /dev/null
+++ b/adapters/emtv/emtvtest/exemplary/simple-web-banner.json
@@ -0,0 +1,133 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "site": {
+            "id": "1",
+            "domain": "test.com"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ua": "Ubuntu"
+        }
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "id": "test-request-id",
+                    "imp": [
+                        {
+                            "id": "test-imp-id",
+                            "tagid": "test",
+                            "banner": {
+                                "format": [
+                                    {
+                                        "w": 300,
+                                        "h": 250
+                                    },
+                                    {
+                                        "w": 300,
+                                        "h": 600
+                                    }
+                                ]
+                            },
+                            "ext": {
+                                "bidder": {
+                                    "placementId": "test",
+                                    "type": "publisher"
+                                }
+                            }
+                        }
+                    ],
+                    "site": {
+                        "id": "1",
+                        "domain": "test.com"
+                    },
+                    "device": {
+                        "ip": "123.123.123.123",
+                        "ua": "Ubuntu"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "id": "test-request-id",
+                    "seatbid": [
+                        {
+                            "bid": [
+                                {
+                                    "id": "test_bid_id",
+                                    "impid": "test-imp-id",
+                                    "price": 0.27543,
+                                    "adm": "<iframe id=\"adm-banner-1\" width=\"468\" height=\"60\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=bc2d316f39931a07d9a8dd249bf85fc0\"></iframe>",
+                                    "cid": "test_cid",
+                                    "crid": "test_crid",
+                                    "dealid": "test_dealid",
+                                    "w": 468,
+                                    "h": 60,
+                                    "ext": {
+                                        "prebid": {
+                                            "type": "banner"
+                                        }
+                                    }
+                                }
+                            ],
+                            "seat": "emtv"
+                        }
+                    ],
+                    "cur": "USD"
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "test_bid_id",
+                        "impid": "test-imp-id",
+                        "price": 0.27543,
+                        "adm": "<iframe id=\"adm-banner-1\" width=\"468\" height=\"60\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=bc2d316f39931a07d9a8dd249bf85fc0\"></iframe>",
+                        "cid": "test_cid",
+                        "crid": "test_crid",
+                        "dealid": "test_dealid",
+                        "w": 468,
+                        "h": 60,
+                        "ext": {
+                            "prebid": {
+                                "type": "banner"
+                            }
+                        }
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/emtv/emtvtest/supplemental/bad_media_type.json b/adapters/emtv/emtvtest/supplemental/bad_media_type.json
new file mode 100644
index 00000000..5a565246
--- /dev/null
+++ b/adapters/emtv/emtvtest/supplemental/bad_media_type.json
@@ -0,0 +1,86 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/pserver",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "ext": {
+                            "bidder": {
+                                "placementId": "test",
+                                "type": "publisher"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": {
+                "id": "test-request-id",
+                "seatbid": [
+                    {
+                        "bid": [
+                            {
+                                "id": "test_bid_id",
+                                "impid": "test-imp-id",
+                                "price": 0.27543,
+                                "adm": "<iframe id=\"adm-banner-16\" width=\"300\" height=\"250\" frameborder=\"0\" marginheight=\"0\" marginwidth=\"0\" style=\"{overflow:hidden}\" src=\"http://example.com/pserver&k=882b2510ed6d6c94fa69c99aa522a708\"></iframe>",
+                                "cid": "test_cid",
+                                "crid": "test_crid",
+                                "dealid": "test_dealid",
+                                "w": 300,
+                                "h": 250,
+                                "ext": {
+                                    "prebid": {
+                                        "type": "banner"
+                                    }
+                                }
+                            }
+                        ],
+                        "seat": "emtv"
+                    }
+                ],
+                "cur": "USD"
+            }
+        }
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "Failed to find impression \"test-imp-id\"",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/emtv/emtvtest/supplemental/bad_response.json b/adapters/emtv/emtvtest/supplemental/bad_response.json
new file mode 100644
index 00000000..8c1cc375
--- /dev/null
+++ b/adapters/emtv/emtvtest/supplemental/bad_response.json
@@ -0,0 +1,84 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/pserver",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": "test",
+                                "type": "publisher"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 200,
+			"body": ""
+        }
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "json: cannot unmarshal string into Go value of type openrtb2.BidResponse",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/emtv/emtvtest/supplemental/no-valid-impressions.json b/adapters/emtv/emtvtest/supplemental/no-valid-impressions.json
new file mode 100644
index 00000000..cc1edd68
--- /dev/null
+++ b/adapters/emtv/emtvtest/supplemental/no-valid-impressions.json
@@ -0,0 +1,20 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [],
+    "app": {
+      "id": "1",
+      "bundle": "com.wls.testwlsapplication"
+    },
+    "device": {
+      "ip": "123.123.123.123",
+      "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "found no valid impressions",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/emtv/emtvtest/supplemental/status-204.json b/adapters/emtv/emtvtest/supplemental/status-204.json
new file mode 100644
index 00000000..3e536c25
--- /dev/null
+++ b/adapters/emtv/emtvtest/supplemental/status-204.json
@@ -0,0 +1,79 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/pserver",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": "test",
+                                "type": "publisher"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 204,
+			"body": {}
+		}
+    }],
+    "expectedBidResponses": []
+}
diff --git a/adapters/emtv/emtvtest/supplemental/status-not-200.json b/adapters/emtv/emtvtest/supplemental/status-not-200.json
new file mode 100644
index 00000000..4bb3c8d5
--- /dev/null
+++ b/adapters/emtv/emtvtest/supplemental/status-not-200.json
@@ -0,0 +1,84 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 250
+                        },
+                        {
+                            "w": 300,
+                            "h": 600
+                        }
+                    ]
+                },
+                "ext": {
+                    "bidder": {
+                        "placementId": "test"
+                    }
+                }
+            }
+        ],
+        "app": {
+            "id": "1",
+            "bundle": "com.wls.testwlsapplication"
+        },
+        "device": {
+            "ip": "123.123.123.123",
+            "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+        }
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+			"uri": "http://example.com/pserver",
+			"body": {
+                "id": "test-request-id",
+                "imp": [
+                    {
+                        "id": "test-imp-id",
+                        "banner": {
+                            "format": [
+                                {
+                                    "w": 300,
+                                    "h": 250
+                                },
+                                {
+                                    "w": 300,
+                                    "h": 600
+                                }
+                            ]
+                        },
+                        "ext": {
+                            "bidder": {
+                                "placementId": "test",
+                                "type": "publisher"
+                            }
+                        }
+                    }
+                ],
+                "app": {
+                    "id": "1",
+                    "bundle": "com.wls.testwlsapplication"
+                },
+                "device": {
+                    "ip": "123.123.123.123",
+                    "ifa": "sdjfksdf-dfsds-dsdg-dsgg"
+                }
+            }
+		},
+		"mockResponse": {
+			"status": 404,
+			"body": {}
+		}
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "Unexpected status code: 404. Run with request.debug = 1 for more info",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/emtv/params_test.go b/adapters/emtv/params_test.go
new file mode 100644
index 00000000..966dd7dd
--- /dev/null
+++ b/adapters/emtv/params_test.go
@@ -0,0 +1,47 @@
+package emtv
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderEmtv, json.RawMessage(p)); err != nil {
+			t.Errorf("Schema rejected valid params: %s", p)
+		}
+	}
+}
+
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json schema. %v", err)
+	}
+
+	for _, p := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderEmtv, json.RawMessage(p)); err == nil {
+			t.Errorf("Schema allowed invalid params: %s", p)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"placementId": "test"}`,
+	`{"placementId": "1"}`,
+	`{"endpointId": "test"}`,
+	`{"endpointId": "1"}`,
+}
+
+var invalidParams = []string{
+	`{"placementId": 42}`,
+	`{"endpointId": 42}`,
+	`{"placementId": "1", "endpointId": "1"}`,
+}
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_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go
index 15d70efa..b91f5ef8 100644
--- a/endpoints/openrtb2/auction_benchmark_test.go
+++ b/endpoints/openrtb2/auction_benchmark_test.go
@@ -15,7 +15,6 @@ import (
 	"github.com/prebid/prebid-server/currency"
 	"github.com/prebid/prebid-server/exchange"
 	"github.com/prebid/prebid-server/experiment/adscert"
-	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/hooks"
 	metricsConfig "github.com/prebid/prebid-server/metrics/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -82,9 +81,6 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
 	gdprPermsBuilder := fakePermissionsBuilder{
 		permissions: &fakePermissions{},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
 	exchange := exchange.NewExchange(
 		adapters,
@@ -94,7 +90,6 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
 		nilMetrics,
 		infos,
 		gdprPermsBuilder,
-		tcf2ConfigBuilder,
 		currency.NewRateConverter(&http.Client{}, "", time.Duration(0)),
 		empty_fetcher.EmptyFetcher{},
 		&adscert.NilSigner{},
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..f4ae437b 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,
@@ -1215,9 +1217,6 @@ func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.Bid
 	gdprPermsBuilder := fakePermissionsBuilder{
 		permissions: &fakePermissions{},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
 	testExchange := exchange.NewExchange(adapterMap,
 		&wellBehavedCache{},
@@ -1226,7 +1225,6 @@ func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.Bid
 		met,
 		bidderInfos,
 		gdprPermsBuilder,
-		tcf2ConfigBuilder,
 		mockCurrencyConverter,
 		mockFetcher,
 		&adscert.NilSigner{},
@@ -1413,14 +1411,6 @@ func (fpb fakePermissionsBuilder) Builder(gdpr.TCF2ConfigReader, gdpr.RequestInf
 	return fpb.permissions
 }
 
-type fakeTCF2ConfigBuilder struct {
-	cfg gdpr.TCF2ConfigReader
-}
-
-func (fcr fakeTCF2ConfigBuilder) Builder(hostConfig config.TCF2, accountConfig config.AccountGDPR) gdpr.TCF2ConfigReader {
-	return fcr.cfg
-}
-
 type fakePermissions struct {
 }
 
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index 4e367ebe..4e94a18f 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,67 @@ 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),
+		},
+		{
+			name: "VideoBidTypeImpVideoIsNil",
+			givenImp: []openrtb2.Imp{
+				{
+					ID: "imp-id",
+				},
+			},
+			givenBidType: openrtb_ext.BidTypeVideo,
+			givenImpId:   "imp-id",
+			expected:     "video-instream",
+		},
+	}
+
+	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..158bef50 100644
--- a/exchange/exchange_test.go
+++ b/exchange/exchange_test.go
@@ -78,11 +78,8 @@ func TestNewExchange(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
+	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
 	for _, bidderName := range knownAdapters {
 		if _, ok := e.adapterMap[bidderName]; !ok {
 			if biddersInfo[string(bidderName)].IsEnabled() {
@@ -131,11 +128,8 @@ func TestCharacterEscape(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
+	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
 
 	// 	3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs
 	//liveAdapters []openrtb_ext.BidderName,
@@ -335,15 +329,11 @@ func TestDebugBehaviour(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 	e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
 	e.categoriesFetcher = categoriesFetcher
 	e.requestSplitter = requestSplitter{
-		me:                &metricsConf.NilMetricsEngine{},
-		gdprPermsBuilder:  e.gdprPermsBuilder,
-		tcf2ConfigBuilder: e.tcf2ConfigBuilder,
+		me:               &metricsConf.NilMetricsEngine{},
+		gdprPermsBuilder: e.gdprPermsBuilder,
 	}
 	ctx := context.Background()
 
@@ -368,6 +358,7 @@ func TestDebugBehaviour(t *testing.T) {
 			UserSyncs:         &emptyUsersync{},
 			StartTime:         time.Now(),
 			HookExecutor:      &hookexecution.EmptyHookExecutor{},
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 		if test.generateWarnings {
 			var errL []error
@@ -504,15 +495,11 @@ func TestTwoBiddersDebugDisabledAndEnabled(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 	e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
 	e.categoriesFetcher = categoriesFetcher
 	e.requestSplitter = requestSplitter{
-		me:                e.me,
-		gdprPermsBuilder:  e.gdprPermsBuilder,
-		tcf2ConfigBuilder: e.tcf2ConfigBuilder,
+		me:               e.me,
+		gdprPermsBuilder: e.gdprPermsBuilder,
 	}
 
 	debugLog := DebugLog{Enabled: true}
@@ -539,6 +526,7 @@ func TestTwoBiddersDebugDisabledAndEnabled(t *testing.T) {
 			UserSyncs:         &emptyUsersync{},
 			StartTime:         time.Now(),
 			HookExecutor:      &hookexecution.EmptyHookExecutor{},
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 
 		e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{
@@ -671,16 +659,12 @@ func TestOverrideWithCustomCurrency(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 	e.currencyConverter = mockCurrencyConverter
 	e.categoriesFetcher = categoriesFetcher
 	e.bidIDGenerator = &mockBidIDGenerator{false, false}
 	e.requestSplitter = requestSplitter{
-		me:                e.me,
-		gdprPermsBuilder:  e.gdprPermsBuilder,
-		tcf2ConfigBuilder: e.tcf2ConfigBuilder,
+		me:               e.me,
+		gdprPermsBuilder: e.gdprPermsBuilder,
 	}
 
 	// Define mock incoming bid requeset
@@ -721,6 +705,7 @@ func TestOverrideWithCustomCurrency(t *testing.T) {
 			Account:           config.Account{},
 			UserSyncs:         &emptyUsersync{},
 			HookExecutor:      &hookexecution.EmptyHookExecutor{},
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 
 		// Run test
@@ -778,9 +763,6 @@ func TestAdapterCurrency(t *testing.T) {
 				allowAllBidders: true,
 			},
 		}.Builder,
-		tcf2ConfigBuilder: fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-		}.Builder,
 		currencyConverter: currencyConverter,
 		categoriesFetcher: nilCategoryFetcher{},
 		bidIDGenerator:    &mockBidIDGenerator{false, false},
@@ -789,9 +771,8 @@ func TestAdapterCurrency(t *testing.T) {
 		},
 	}
 	e.requestSplitter = requestSplitter{
-		me:                e.me,
-		gdprPermsBuilder:  e.gdprPermsBuilder,
-		tcf2ConfigBuilder: e.tcf2ConfigBuilder,
+		me:               e.me,
+		gdprPermsBuilder: e.gdprPermsBuilder,
 	}
 
 	// Define Bid Request
@@ -816,6 +797,7 @@ func TestAdapterCurrency(t *testing.T) {
 		Account:           config.Account{},
 		UserSyncs:         &emptyUsersync{},
 		HookExecutor:      &hookexecution.EmptyHookExecutor{},
+		TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 	}
 	response, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{})
 	assert.NoError(t, err)
@@ -852,18 +834,14 @@ func TestFloorsSignalling(t *testing.T) {
 				allowAllBidders: true,
 			},
 		}.Builder,
-		tcf2ConfigBuilder: fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-		}.Builder,
 		currencyConverter: currencyConverter,
 		categoriesFetcher: nilCategoryFetcher{},
 		bidIDGenerator:    &mockBidIDGenerator{false, false},
 		floor:             config.PriceFloors{Enabled: true},
 	}
 	e.requestSplitter = requestSplitter{
-		me:                e.me,
-		gdprPermsBuilder:  e.gdprPermsBuilder,
-		tcf2ConfigBuilder: e.tcf2ConfigBuilder,
+		me:               e.me,
+		gdprPermsBuilder: e.gdprPermsBuilder,
 	}
 
 	type testResults struct {
@@ -1340,16 +1318,12 @@ func TestReturnCreativeEndToEnd(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 	e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
 	e.categoriesFetcher = categoriesFetcher
 	e.bidIDGenerator = &mockBidIDGenerator{false, false}
 	e.requestSplitter = requestSplitter{
-		me:                e.me,
-		gdprPermsBuilder:  e.gdprPermsBuilder,
-		tcf2ConfigBuilder: e.tcf2ConfigBuilder,
+		me:               e.me,
+		gdprPermsBuilder: e.gdprPermsBuilder,
 	}
 
 	// Define mock incoming bid requeset
@@ -1373,6 +1347,7 @@ func TestReturnCreativeEndToEnd(t *testing.T) {
 				Account:           config.Account{},
 				UserSyncs:         &emptyUsersync{},
 				HookExecutor:      &hookexecution.EmptyHookExecutor{},
+				TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 			}
 
 			// Run test
@@ -1448,11 +1423,8 @@ func TestGetBidCacheInfoEndToEnd(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
-	e := NewExchange(adapters, pbc, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
+	e := NewExchange(adapters, pbc, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
 	// 	3) Build all the parameters e.buildBidResponse(ctx.Background(), liveA... ) needs
 	liveAdapters := []openrtb_ext.BidderName{bidderName}
 
@@ -1811,11 +1783,8 @@ func TestBidResponseCurrency(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
+	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
 
 	liveAdapters := make([]openrtb_ext.BidderName, 1)
 	liveAdapters[0] = "appnexus"
@@ -1950,9 +1919,6 @@ func TestBidResponseImpExtInfo(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
 	noBidHandler := func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(204) }
 	server := httptest.NewServer(http.HandlerFunc(noBidHandler))
@@ -1965,7 +1931,7 @@ func TestBidResponseImpExtInfo(t *testing.T) {
 		t.Fatalf("Error intializing adapters: %v", adaptersErr)
 	}
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, tcf2ConfigBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
+	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, nil, gdprPermsBuilder, nil, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
 
 	liveAdapters := make([]openrtb_ext.BidderName, 1)
 	liveAdapters[0] = "appnexus"
@@ -2047,6 +2013,7 @@ func TestRaceIntegration(t *testing.T) {
 		Account:           config.Account{},
 		UserSyncs:         &emptyUsersync{},
 		HookExecutor:      &hookexecution.EmptyHookExecutor{},
+		TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 	}
 
 	debugLog := DebugLog{}
@@ -2056,11 +2023,8 @@ func TestRaceIntegration(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	tcf2CfgBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
-	ex := NewExchange(adapters, &wellBehavedCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2CfgBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
+	ex := NewExchange(adapters, &wellBehavedCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, &nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
 	_, err = ex.HoldAuction(context.Background(), auctionRequest, &debugLog)
 	if err != nil {
 		t.Errorf("HoldAuction returned unexpected error: %v", err)
@@ -2157,11 +2121,8 @@ func TestPanicRecovery(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
+	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &adscert.NilSigner{}).(*exchange)
 
 	chBids := make(chan *bidResponseWrapper, 1)
 	panicker := func(bidderRequest BidderRequest, conversions currency.Conversions) {
@@ -2231,10 +2192,7 @@ func TestPanicRecoveryHighLevel(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
-	e := NewExchange(adapters, &mockCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}).(*exchange)
+	e := NewExchange(adapters, &mockCache{}, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, categoriesFetcher, &adscert.NilSigner{}).(*exchange)
 
 	e.adapterMap[openrtb_ext.BidderBeachfront] = panicingAdapter{}
 	e.adapterMap[openrtb_ext.BidderAppnexus] = panicingAdapter{}
@@ -2272,6 +2230,7 @@ func TestPanicRecoveryHighLevel(t *testing.T) {
 		Account:           config.Account{},
 		UserSyncs:         &emptyUsersync{},
 		HookExecutor:      &hookexecution.EmptyHookExecutor{},
+		TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 	}
 	debugLog := DebugLog{}
 	_, err = e.HoldAuction(context.Background(), auctionRequest, &debugLog)
@@ -2408,6 +2367,7 @@ func runSpec(t *testing.T, filename string, spec *exchangeSpec) {
 		UserSyncs:     mockIdFetcher(spec.IncomingRequest.Usersyncs),
 		ImpExtInfoMap: impExtInfoMap,
 		HookExecutor:  &hookexecution.EmptyHookExecutor{},
+		TCF2Config:    gdpr.NewTCF2Config(privacyConfig.GDPR.TCF2, config.AccountGDPR{}),
 	}
 
 	if spec.MultiBid != nil {
@@ -2616,9 +2576,6 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 			allowAllBidders: true,
 		},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(privacyConfig.GDPR.TCF2, config.AccountGDPR{}),
-	}.Builder
 
 	bidderToSyncerKey := map[string]string{}
 	for _, bidderName := range openrtb_ext.CoreBidderNames() {
@@ -2643,7 +2600,6 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 		me:                metricsEngine,
 		privacyConfig:     privacyConfig,
 		gdprPermsBuilder:  gdprPermsBuilder,
-		tcf2ConfigBuilder: tcf2ConfigBuilder,
 		hostSChainNode:    hostSChainNode,
 		bidderInfo:        bidderInfos,
 	}
@@ -2656,7 +2612,6 @@ func newExchangeForTests(t *testing.T, filename string, expectations map[string]
 		currencyConverter:        currency.NewRateConverter(&http.Client{}, "", time.Duration(0)),
 		gdprDefaultValue:         gdprDefaultValue,
 		gdprPermsBuilder:         gdprPermsBuilder,
-		tcf2ConfigBuilder:        tcf2ConfigBuilder,
 		privacyConfig:            privacyConfig,
 		categoriesFetcher:        categoriesFetcher,
 		bidderInfo:               bidderInfos,
@@ -4264,9 +4219,6 @@ func TestStoredAuctionResponses(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
 	// Define mock incoming bid requeset
 	mockBidRequest := &openrtb2.BidRequest{
@@ -4318,6 +4270,7 @@ func TestStoredAuctionResponses(t *testing.T) {
 			UserSyncs:              &emptyUsersync{},
 			StoredAuctionResponses: test.storedAuctionResp,
 			HookExecutor:           &hookexecution.EmptyHookExecutor{},
+			TCF2Config:             gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 		// Run test
 		outBidResponse, err := e.HoldAuction(context.Background(), auctionRequest, &DebugLog{})
@@ -4632,13 +4585,9 @@ func TestAuctionDebugEnabled(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 	e.requestSplitter = requestSplitter{
-		me:                e.me,
-		gdprPermsBuilder:  e.gdprPermsBuilder,
-		tcf2ConfigBuilder: e.tcf2ConfigBuilder,
+		me:               e.me,
+		gdprPermsBuilder: e.gdprPermsBuilder,
 	}
 
 	ctx := context.Background()
@@ -4655,6 +4604,7 @@ func TestAuctionDebugEnabled(t *testing.T) {
 		StartTime:         time.Now(),
 		RequestType:       metrics.ReqTypeORTB2Web,
 		HookExecutor:      &hookexecution.EmptyHookExecutor{},
+		TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 	}
 
 	debugLog := &DebugLog{DebugOverride: true, DebugEnabledOrOverridden: true}
@@ -4704,11 +4654,8 @@ func TestPassExperimentConfigsToHoldAuction(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
-	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, tcf2ConfigBuilder, currencyConverter, nilCategoryFetcher{}, &signer).(*exchange)
+	e := NewExchange(adapters, nil, cfg, map[string]usersync.Syncer{}, &metricsConf.NilMetricsEngine{}, biddersInfo, gdprPermsBuilder, currencyConverter, nilCategoryFetcher{}, &signer).(*exchange)
 
 	// Define mock incoming bid requeset
 	mockBidRequest := &openrtb2.BidRequest{
@@ -4727,6 +4674,7 @@ func TestPassExperimentConfigsToHoldAuction(t *testing.T) {
 		Account:           config.Account{},
 		UserSyncs:         &emptyUsersync{},
 		HookExecutor:      &hookexecution.EmptyHookExecutor{},
+		TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 	}
 
 	debugLog := DebugLog{}
@@ -5146,16 +5094,12 @@ func TestOverrideConfigAlternateBidderCodesWithRequestValues(t *testing.T) {
 			allowAllBidders: true,
 		},
 	}.Builder
-	e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 	e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
 	e.categoriesFetcher = categoriesFetcher
 	e.bidIDGenerator = &mockBidIDGenerator{false, false}
 	e.requestSplitter = requestSplitter{
-		me:                e.me,
-		gdprPermsBuilder:  e.gdprPermsBuilder,
-		tcf2ConfigBuilder: e.tcf2ConfigBuilder,
+		me:               e.me,
+		gdprPermsBuilder: e.gdprPermsBuilder,
 	}
 
 	// Define mock incoming bid requeset
@@ -5182,6 +5126,7 @@ func TestOverrideConfigAlternateBidderCodesWithRequestValues(t *testing.T) {
 			Account:           test.in.config.AccountDefaults,
 			UserSyncs:         &emptyUsersync{},
 			HookExecutor:      &hookexecution.EmptyHookExecutor{},
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 
 		// Run test
@@ -5315,7 +5260,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 +5318,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 +5425,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!")
 }
 
@@ -5570,14 +5515,10 @@ func TestModulesCanBeExecutedForMultipleBiddersSimultaneously(t *testing.T) {
 
 	e := new(exchange)
 	e.me = &metricsConf.NilMetricsEngine{}
-	e.tcf2ConfigBuilder = fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 	e.currencyConverter = currency.NewRateConverter(&http.Client{}, "", time.Duration(0))
 	e.requestSplitter = requestSplitter{
-		me:                e.me,
-		gdprPermsBuilder:  e.gdprPermsBuilder,
-		tcf2ConfigBuilder: e.tcf2ConfigBuilder,
+		me:               e.me,
+		gdprPermsBuilder: e.gdprPermsBuilder,
 	}
 
 	bidRequest := &openrtb2.BidRequest{
@@ -5603,6 +5544,7 @@ func TestModulesCanBeExecutedForMultipleBiddersSimultaneously(t *testing.T) {
 		UserSyncs:         &emptyUsersync{},
 		StartTime:         time.Now(),
 		HookExecutor:      exec,
+		TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 	}
 
 	e.adapterMap = map[openrtb_ext.BidderName]AdaptedBidder{
diff --git a/exchange/gdpr_test.go b/exchange/gdpr_test.go
index 42553874..44573b59 100644
--- a/exchange/gdpr_test.go
+++ b/exchange/gdpr_test.go
@@ -8,10 +8,11 @@ import (
 	gppConstants "github.com/prebid/go-gpp/constants"
 	"github.com/prebid/openrtb/v19/openrtb2"
 	"github.com/prebid/prebid-server/gdpr"
+	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/stretchr/testify/assert"
 )
 
-func TestExtractGDPR(t *testing.T) {
+func TestGetGDPR(t *testing.T) {
 	tests := []struct {
 		description string
 		giveRegs    *openrtb2.Regs
@@ -78,11 +79,12 @@ func TestExtractGDPR(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.description, func(t *testing.T) {
-			bidReq := openrtb2.BidRequest{
-				Regs: tt.giveRegs,
+			req := openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					Regs: tt.giveRegs,
+				},
 			}
-
-			result, err := extractGDPR(&bidReq)
+			result, err := getGDPR(&req)
 			assert.Equal(t, tt.wantGDPR, result)
 
 			if tt.wantError {
@@ -94,7 +96,7 @@ func TestExtractGDPR(t *testing.T) {
 	}
 }
 
-func TestExtractConsent(t *testing.T) {
+func TestGetConsent(t *testing.T) {
 	tests := []struct {
 		description string
 		giveUser    *openrtb2.User
@@ -150,11 +152,13 @@ func TestExtractConsent(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.description, func(t *testing.T) {
-			bidReq := openrtb2.BidRequest{
-				User: tt.giveUser,
+			req := openrtb_ext.RequestWrapper{
+				BidRequest: &openrtb2.BidRequest{
+					User: tt.giveUser,
+				},
 			}
 
-			result, err := extractConsent(&bidReq, tt.giveGPP)
+			result, err := getConsent(&req, tt.giveGPP)
 			assert.Equal(t, tt.wantConsent, result, tt.description)
 
 			if tt.wantError {
diff --git a/exchange/targeting_test.go b/exchange/targeting_test.go
index 49d8b5fa..f1f5df42 100644
--- a/exchange/targeting_test.go
+++ b/exchange/targeting_test.go
@@ -91,9 +91,6 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op
 			allowAllBidders: true,
 		},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
 	ex := &exchange{
 		adapterMap:        buildAdapterMap(mockBids, server.URL, server.Client()),
@@ -101,16 +98,14 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op
 		cache:             &wellBehavedCache{},
 		cacheTime:         time.Duration(0),
 		gdprPermsBuilder:  gdprPermsBuilder,
-		tcf2ConfigBuilder: tcf2ConfigBuilder,
 		currencyConverter: currency.NewRateConverter(&http.Client{}, "", time.Duration(0)),
 		gdprDefaultValue:  gdpr.SignalYes,
 		categoriesFetcher: categoriesFetcher,
 		bidIDGenerator:    &mockBidIDGenerator{false, false},
 	}
 	ex.requestSplitter = requestSplitter{
-		me:                ex.me,
-		gdprPermsBuilder:  ex.gdprPermsBuilder,
-		tcf2ConfigBuilder: ex.tcf2ConfigBuilder,
+		me:               ex.me,
+		gdprPermsBuilder: ex.gdprPermsBuilder,
 	}
 
 	imps := buildImps(t, mockBids)
@@ -130,6 +125,7 @@ func runTargetingAuction(t *testing.T, mockBids map[openrtb_ext.BidderName][]*op
 		Account:           config.Account{},
 		UserSyncs:         &emptyUsersync{},
 		HookExecutor:      &hookexecution.EmptyHookExecutor{},
+		TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 	}
 
 	debugLog := DebugLog{}
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 59b9c858..e054f33c 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -67,14 +67,6 @@ func (fpb fakePermissionsBuilder) Builder(gdpr.TCF2ConfigReader, gdpr.RequestInf
 	return fpb.permissions
 }
 
-type fakeTCF2ConfigBuilder struct {
-	cfg gdpr.TCF2ConfigReader
-}
-
-func (fcr fakeTCF2ConfigBuilder) Builder(hostConfig config.TCF2, accountConfig config.AccountGDPR) gdpr.TCF2ConfigReader {
-	return fcr.cfg
-}
-
 func assertReq(t *testing.T, bidderRequests []BidderRequest,
 	applyCOPPA bool, consentedVendors map[string]bool) {
 	// assert individual bidder requests
@@ -444,6 +436,8 @@ func TestCreateSanitizedImpExt(t *testing.T) {
 }
 
 func TestCleanOpenRTBRequests(t *testing.T) {
+	emptyTCF2Config := gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{})
+
 	testCases := []struct {
 		req              AuctionRequest
 		bidReqAssertions func(t *testing.T, bidderRequests []BidderRequest,
@@ -453,14 +447,14 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 		consentedVendors map[string]bool
 	}{
 		{
-			req:              AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)}, UserSyncs: &emptyUsersync{}},
+			req:              AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)}, UserSyncs: &emptyUsersync{}, TCF2Config: emptyTCF2Config},
 			bidReqAssertions: assertReq,
 			hasError:         false,
 			applyCOPPA:       true,
 			consentedVendors: map[string]bool{"appnexus": true},
 		},
 		{
-			req:              AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}},
+			req:              AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, TCF2Config: emptyTCF2Config},
 			bidReqAssertions: assertReq,
 			hasError:         false,
 			applyCOPPA:       false,
@@ -484,16 +478,12 @@ func TestCleanOpenRTBRequests(t *testing.T) {
 				allowAllBidders: true,
 			},
 		}.Builder
-		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-		}.Builder
 
 		reqSplitter := &requestSplitter{
 			bidderToSyncerKey: map[string]string{},
 			me:                &metrics.MetricsEngineMock{},
 			privacyConfig:     privacyConfig,
 			gdprPermsBuilder:  gdprPermsBuilder,
-			tcf2ConfigBuilder: tcf2ConfigBuilder,
 			hostSChainNode:    nil,
 			bidderInfo:        config.BidderInfos{},
 		}
@@ -524,6 +514,8 @@ func TestCleanOpenRTBRequestsWithFPD(t *testing.T) {
 	}
 	fpd[openrtb_ext.BidderName("brightroll")] = &brightrollFpd
 
+	emptyTCF2Config := gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{})
+
 	testCases := []struct {
 		description string
 		req         AuctionRequest
@@ -531,22 +523,22 @@ func TestCleanOpenRTBRequestsWithFPD(t *testing.T) {
 	}{
 		{
 			description: "Pass valid FPD data for bidder not found in the request",
-			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: fpd},
+			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: getTestBuildRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: fpd, TCF2Config: emptyTCF2Config},
 			fpdExpected: false,
 		},
 		{
 			description: "Pass valid FPD data for bidders specified in request",
-			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: fpd},
+			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: fpd, TCF2Config: emptyTCF2Config},
 			fpdExpected: true,
 		},
 		{
 			description: "Bidders specified in request but there is no fpd data for this bidder",
-			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: make(map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData)},
+			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: make(map[openrtb_ext.BidderName]*firstpartydata.ResolvedFirstPartyData), TCF2Config: emptyTCF2Config},
 			fpdExpected: false,
 		},
 		{
 			description: "No FPD data passed",
-			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: nil},
+			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: newAdapterAliasBidRequest(t)}, UserSyncs: &emptyUsersync{}, FirstPartyData: nil, TCF2Config: emptyTCF2Config},
 			fpdExpected: false,
 		},
 	}
@@ -558,16 +550,12 @@ func TestCleanOpenRTBRequestsWithFPD(t *testing.T) {
 				allowAllBidders: true,
 			},
 		}.Builder
-		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-		}.Builder
 
 		reqSplitter := &requestSplitter{
 			bidderToSyncerKey: map[string]string{},
 			me:                &metrics.MetricsEngineMock{},
 			privacyConfig:     config.Privacy{},
 			gdprPermsBuilder:  gdprPermissionsBuilder,
-			tcf2ConfigBuilder: tcf2ConfigBuilder,
 			hostSChainNode:    nil,
 			bidderInfo:        config.BidderInfos{},
 		}
@@ -829,14 +817,12 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) {
 				allowAllBidders: true,
 			},
 		}.Builder
-		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-		}.Builder
 
 		auctionReq := AuctionRequest{
 			BidRequestWrapper:  &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Imp: test.imps}},
 			UserSyncs:          &emptyUsersync{},
 			StoredBidResponses: test.storedBidResponses,
+			TCF2Config:         gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 
 		reqSplitter := &requestSplitter{
@@ -844,7 +830,6 @@ func TestCleanOpenRTBRequestsWithBidResponses(t *testing.T) {
 			me:                &metrics.MetricsEngineMock{},
 			privacyConfig:     config.Privacy{},
 			gdprPermsBuilder:  gdprPermissionsBuilder,
-			tcf2ConfigBuilder: tcf2ConfigBuilder,
 			hostSChainNode:    nil,
 			bidderInfo:        config.BidderInfos{},
 		}
@@ -999,6 +984,7 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
 			UserSyncs:         &emptyUsersync{},
 			Account:           accountConfig,
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, accountConfig.GDPR),
 		}
 
 		gdprPermissionsBuilder := fakePermissionsBuilder{
@@ -1006,9 +992,6 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 				allowAllBidders: true,
 			},
 		}.Builder
-		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(config.TCF2{}, accountConfig.GDPR),
-		}.Builder
 
 		bidderToSyncerKey := map[string]string{}
 		reqSplitter := &requestSplitter{
@@ -1016,7 +999,6 @@ func TestCleanOpenRTBRequestsCCPA(t *testing.T) {
 			me:                &metrics.MetricsEngineMock{},
 			privacyConfig:     privacyConfig,
 			gdprPermsBuilder:  gdprPermissionsBuilder,
-			tcf2ConfigBuilder: tcf2ConfigBuilder,
 			hostSChainNode:    nil,
 			bidderInfo:        config.BidderInfos{},
 		}
@@ -1072,6 +1054,7 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) {
 		auctionReq := AuctionRequest{
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
 			UserSyncs:         &emptyUsersync{},
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 
 		gdprPermissionsBuilder := fakePermissionsBuilder{
@@ -1079,9 +1062,6 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) {
 				allowAllBidders: true,
 			},
 		}.Builder
-		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-		}.Builder
 
 		privacyConfig := config.Privacy{
 			CCPA: config.CCPA{
@@ -1096,7 +1076,6 @@ func TestCleanOpenRTBRequestsCCPAErrors(t *testing.T) {
 			me:                &metrics,
 			privacyConfig:     privacyConfig,
 			gdprPermsBuilder:  gdprPermissionsBuilder,
-			tcf2ConfigBuilder: tcf2ConfigBuilder,
 			hostSChainNode:    nil,
 			bidderInfo:        config.BidderInfos{},
 		}
@@ -1139,6 +1118,7 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) {
 		auctionReq := AuctionRequest{
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
 			UserSyncs:         &emptyUsersync{},
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 
 		gdprPermissionsBuilder := fakePermissionsBuilder{
@@ -1146,9 +1126,6 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) {
 				allowAllBidders: true,
 			},
 		}.Builder
-		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-		}.Builder
 
 		bidderToSyncerKey := map[string]string{}
 		metrics := metrics.MetricsEngineMock{}
@@ -1158,7 +1135,6 @@ func TestCleanOpenRTBRequestsCOPPA(t *testing.T) {
 			me:                &metrics,
 			privacyConfig:     config.Privacy{},
 			gdprPermsBuilder:  gdprPermissionsBuilder,
-			tcf2ConfigBuilder: tcf2ConfigBuilder,
 			hostSChainNode:    nil,
 			bidderInfo:        config.BidderInfos{},
 		}
@@ -1238,6 +1214,7 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) {
 		auctionReq := AuctionRequest{
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
 			UserSyncs:         &emptyUsersync{},
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 
 		gdprPermissionsBuilder := fakePermissionsBuilder{
@@ -1245,16 +1222,12 @@ func TestCleanOpenRTBRequestsSChain(t *testing.T) {
 				allowAllBidders: true,
 			},
 		}.Builder
-		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-		}.Builder
 
 		reqSplitter := &requestSplitter{
 			bidderToSyncerKey: map[string]string{},
 			me:                &metrics.MetricsEngineMock{},
 			privacyConfig:     config.Privacy{},
 			gdprPermsBuilder:  gdprPermissionsBuilder,
-			tcf2ConfigBuilder: tcf2ConfigBuilder,
 			hostSChainNode:    nil,
 			bidderInfo:        config.BidderInfos{},
 		}
@@ -1312,6 +1285,7 @@ func TestCleanOpenRTBRequestsBidderParams(t *testing.T) {
 		auctionReq := AuctionRequest{
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
 			UserSyncs:         &emptyUsersync{},
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 
 		gdprPermissionsBuilder := fakePermissionsBuilder{
@@ -1319,16 +1293,12 @@ func TestCleanOpenRTBRequestsBidderParams(t *testing.T) {
 				allowAllBidders: true,
 			},
 		}.Builder
-		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-		}.Builder
 
 		reqSplitter := &requestSplitter{
 			bidderToSyncerKey: map[string]string{},
 			me:                &metrics.MetricsEngineMock{},
 			privacyConfig:     config.Privacy{},
 			gdprPermsBuilder:  gdprPermissionsBuilder,
-			tcf2ConfigBuilder: tcf2ConfigBuilder,
 			hostSChainNode:    nil,
 			bidderInfo:        config.BidderInfos{},
 		}
@@ -1896,6 +1866,7 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 		auctionReq := AuctionRequest{
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
 			UserSyncs:         &emptyUsersync{},
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 
 		gdprPermissionsBuilder := fakePermissionsBuilder{
@@ -1903,9 +1874,6 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 				allowAllBidders: true,
 			},
 		}.Builder
-		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-		}.Builder
 
 		privacyConfig := config.Privacy{
 			LMT: config.LMT{
@@ -1918,7 +1886,6 @@ func TestCleanOpenRTBRequestsLMT(t *testing.T) {
 			me:                &metrics.MetricsEngineMock{},
 			privacyConfig:     privacyConfig,
 			gdprPermsBuilder:  gdprPermissionsBuilder,
-			tcf2ConfigBuilder: tcf2ConfigBuilder,
 			hostSChainNode:    nil,
 			bidderInfo:        config.BidderInfos{},
 		}
@@ -2127,6 +2094,10 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
 			UserSyncs:         &emptyUsersync{},
 			Account:           accountConfig,
+			TCF2Config: gdpr.NewTCF2Config(
+				privacyConfig.GDPR.TCF2,
+				accountConfig.GDPR,
+			),
 		}
 
 		gdprPermissionsBuilder := fakePermissionsBuilder{
@@ -2137,12 +2108,6 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 				activitiesError: test.permissionsError,
 			},
 		}.Builder
-		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(
-				privacyConfig.GDPR.TCF2,
-				accountConfig.GDPR,
-			),
-		}.Builder
 
 		gdprDefaultValue := gdpr.SignalYes
 		if test.gdprDefaultValue == "0" {
@@ -2154,7 +2119,6 @@ func TestCleanOpenRTBRequestsGDPR(t *testing.T) {
 			me:                &metrics.MetricsEngineMock{},
 			privacyConfig:     privacyConfig,
 			gdprPermsBuilder:  gdprPermissionsBuilder,
-			tcf2ConfigBuilder: tcf2ConfigBuilder,
 			hostSChainNode:    nil,
 			bidderInfo:        config.BidderInfos{},
 		}
@@ -2236,6 +2200,7 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) {
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
 			UserSyncs:         &emptyUsersync{},
 			Account:           accountConfig,
+			TCF2Config:        gdpr.NewTCF2Config(privacyConfig.GDPR.TCF2, accountConfig.GDPR),
 		}
 
 		gdprPermissionsBuilder := fakePermissionsBuilder{
@@ -2246,9 +2211,6 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) {
 				activitiesError: nil,
 			},
 		}.Builder
-		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(privacyConfig.GDPR.TCF2, accountConfig.GDPR),
-		}.Builder
 
 		metricsMock := metrics.MetricsEngineMock{}
 		metricsMock.Mock.On("RecordAdapterGDPRRequestBlocked", mock.Anything).Return()
@@ -2258,7 +2220,6 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) {
 			me:                &metricsMock,
 			privacyConfig:     privacyConfig,
 			gdprPermsBuilder:  gdprPermissionsBuilder,
-			tcf2ConfigBuilder: tcf2ConfigBuilder,
 			hostSChainNode:    nil,
 			bidderInfo:        config.BidderInfos{},
 		}
@@ -2284,6 +2245,8 @@ func TestCleanOpenRTBRequestsGDPRBlockBidRequest(t *testing.T) {
 }
 
 func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) {
+	emptyTCF2Config := gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{})
+
 	bidReq := newBidRequest(t)
 	bidReq.Regs = &openrtb2.Regs{}
 	bidReq.Regs.GPP = "DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1NYN"
@@ -2306,14 +2269,14 @@ func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) {
 	}{
 		{
 			name:        "NotSupported",
-			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidReq}, UserSyncs: &emptyUsersync{}},
+			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidReq}, UserSyncs: &emptyUsersync{}, TCF2Config: emptyTCF2Config},
 			expectRegs:  &downgradedRegs,
 			expectUser:  &downgradedUser,
 			bidderInfos: config.BidderInfos{"appnexus": config.BidderInfo{OpenRTB: &config.OpenRTBInfo{GPPSupported: false}}},
 		},
 		{
 			name:        "Supported",
-			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidReq}, UserSyncs: &emptyUsersync{}},
+			req:         AuctionRequest{BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: bidReq}, UserSyncs: &emptyUsersync{}, TCF2Config: emptyTCF2Config},
 			expectRegs:  bidReq.Regs,
 			expectUser:  bidReq.User,
 			bidderInfos: config.BidderInfos{"appnexus": config.BidderInfo{OpenRTB: &config.OpenRTBInfo{GPPSupported: true}}},
@@ -2337,16 +2300,12 @@ func TestCleanOpenRTBRequestsWithOpenRTBDowngrade(t *testing.T) {
 					allowAllBidders: true,
 				},
 			}.Builder
-			tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-				cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-			}.Builder
 
 			reqSplitter := &requestSplitter{
 				bidderToSyncerKey: map[string]string{},
 				me:                &metrics.MetricsEngineMock{},
 				privacyConfig:     privacyConfig,
 				gdprPermsBuilder:  gdprPermsBuilder,
-				tcf2ConfigBuilder: tcf2ConfigBuilder,
 				hostSChainNode:    nil,
 				bidderInfo:        test.bidderInfos,
 			}
@@ -3085,6 +3044,7 @@ func TestCleanOpenRTBRequestsSChainMultipleBidders(t *testing.T) {
 	auctionReq := AuctionRequest{
 		BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
 		UserSyncs:         &emptyUsersync{},
+		TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 	}
 
 	gdprPermissionsBuilder := fakePermissionsBuilder{
@@ -3095,16 +3055,12 @@ func TestCleanOpenRTBRequestsSChainMultipleBidders(t *testing.T) {
 			activitiesError: nil,
 		},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
 	reqSplitter := &requestSplitter{
 		bidderToSyncerKey: map[string]string{},
 		me:                &metrics.MetricsEngineMock{},
 		privacyConfig:     config.Privacy{},
 		gdprPermsBuilder:  gdprPermissionsBuilder,
-		tcf2ConfigBuilder: tcf2ConfigBuilder,
 		hostSChainNode:    nil,
 		bidderInfo:        config.BidderInfos{},
 	}
@@ -3441,22 +3397,19 @@ func TestCleanOpenRTBRequestsFilterBidderRequestExt(t *testing.T) {
 			BidRequestWrapper: &openrtb_ext.RequestWrapper{BidRequest: req},
 			UserSyncs:         &emptyUsersync{},
 			Account:           config.Account{AlternateBidderCodes: test.inCfgABC},
+			TCF2Config:        gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
 		}
 		gdprPermissionsBuilder := fakePermissionsBuilder{
 			permissions: &permissionsMock{
 				allowAllBidders: true,
 			},
 		}.Builder
-		tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-			cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-		}.Builder
 
 		reqSplitter := &requestSplitter{
 			bidderToSyncerKey: map[string]string{},
 			me:                &metrics.MetricsEngineMock{},
 			privacyConfig:     config.Privacy{},
 			gdprPermsBuilder:  gdprPermissionsBuilder,
-			tcf2ConfigBuilder: tcf2ConfigBuilder,
 			hostSChainNode:    nil,
 			bidderInfo:        config.BidderInfos{},
 		}
diff --git a/openrtb_ext/imp_appnexus_test.go b/openrtb_ext/imp_appnexus_test.go
new file mode 100644
index 00000000..cbd6779d
--- /dev/null
+++ b/openrtb_ext/imp_appnexus_test.go
@@ -0,0 +1,47 @@
+package openrtb_ext
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestKeywordsUnmarshalJSON(t *testing.T) {
+	type keywords struct {
+		Keywords ExtImpAppnexusKeywords `json:"keywords"`
+	}
+
+	type testCase struct {
+		input    []byte
+		expected string
+		desc     string
+	}
+
+	validTestCases := []testCase{
+		{input: []byte(`{"keywords" : { "pets": ["dog"] }}`), expected: "pets=dog", desc: "dynamic json object"},
+		{input: []byte(`{"keywords" : { "foo":[] }}`), expected: "foo", desc: "dynamic json object with empty value array"},
+		{input: []byte(`{"keywords" : [{"key": "foo", "value": ["bar","baz"]},{"key": "valueless"}]}`), expected: "foo=bar,foo=baz,valueless", desc: "array of objects"},
+		{input: []byte(`{"keywords" : "foo=bar,foo=baz,valueless"}`), expected: "foo=bar,foo=baz,valueless", desc: "string keywords"},
+		{input: []byte(`{"keywords" : ""}`), expected: "", desc: "empty string"},
+		{input: []byte(`{"keywords" : {}}`), expected: "", desc: "empty keywords object"},
+		{input: []byte(`{"keywords" : [{}]}`), expected: "", desc: "empty keywords object array"},
+		{input: []byte(`{"keywords": []}`), expected: "", desc: "empty keywords array"},
+	}
+
+	for _, test := range validTestCases {
+		var keywords keywords
+		assert.NoError(t, json.Unmarshal(test.input, &keywords), test.desc)
+		assert.Equal(t, test.expected, keywords.Keywords.String())
+	}
+
+	invalidTestCases := []testCase{
+		{input: []byte(`{"keywords": [{]}`), desc: "invalid keywords array"},
+		{input: []byte(`{"keywords" : {"}}`), desc: "invalid keywords object"},
+	}
+
+	for _, test := range invalidTestCases {
+		var keywords keywords
+		assert.Error(t, json.Unmarshal(test.input, &keywords), test.desc)
+	}
+}