Repository: prebid/prebid-server
Pull request #2754 (2023-05-08 14:31:09)

HuaweiAds: accept Alpha2 country code
modified the function convertCountryCode to accept Alpha2 country code


Patch:
diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go
index e56d8f2f..f70914a1 100644
--- a/adapters/appnexus/appnexus.go
+++ b/adapters/appnexus/appnexus.go
@@ -5,6 +5,7 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
+	"net/url"
 	"strconv"
 	"strings"
 
@@ -21,25 +22,26 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-const defaultPlatformID int = 5
+const (
+	defaultPlatformID int = 5
+	maxImpsPerReq         = 10
+)
 
 type adapter struct {
-	URI             string
-	iabCategoryMap  map[string]string
+	uri             url.URL
 	hbSource        int
 	randomGenerator randomutil.RandomGenerator
 }
 
-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) {
+	uri, err := url.Parse(config.Endpoint)
+	if err != nil {
+		return nil, err
+	}
+
 	bidder := &adapter{
-		URI: config.Endpoint,
-		iabCategoryMap: map[string]string{
-			"1": "IAB20-3",
-			"9": "IAB5-3",
-		},
+		uri:             *uri,
 		hbSource:        resolvePlatformID(config.PlatformID),
 		randomGenerator: randomutil.RandomNumberGenerator{},
 	}
@@ -57,15 +59,14 @@ func resolvePlatformID(platformID string) int {
 }
 
 func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
-
 	// appnexus adapter expects imp.displaymanagerver to be populated in openrtb2 endpoint
 	// but some SDKs will put it in imp.ext.prebid instead
 	displayManagerVer := buildDisplayManageVer(request)
 
 	var (
 		shouldGenerateAdPodId *bool
-		uniqueMemberIds       []string
-		memberIds             = make(map[string]struct{})
+		uniqueMemberIds       = make([]string, 0, len(request.Imp))
+		memberIds             = make(map[string]struct{}, len(request.Imp))
 		errs                  = make([]error, 0, len(request.Imp))
 	)
 
@@ -102,7 +103,12 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E
 	}
 	request.Imp = validImps
 
-	requestURI := a.URI
+	// If all the requests were malformed, don't bother making a server call with no impressions.
+	if len(request.Imp) == 0 {
+		return nil, errs
+	}
+
+	requestURI := a.uri
 	// The Appnexus API requires a Member ID in the URL. This means the request may fail if
 	// different impressions have different member IDs.
 	// Check for this condition, and log an error if it's a problem.
@@ -110,14 +116,10 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E
 		requestURI = appendMemberId(requestURI, uniqueMemberIds[0])
 		if len(uniqueMemberIds) > 1 {
 			errs = append(errs, fmt.Errorf("All request.imp[i].ext.prebid.bidder.appnexus.member params must match. Request contained: %v", uniqueMemberIds))
+			return nil, errs
 		}
 	}
 
-	// If all the requests were malformed, don't bother making a server call with no impressions.
-	if len(request.Imp) == 0 {
-		return nil, errs
-	}
-
 	// Add Appnexus request level extension
 	var isAMP, isVIDEO int
 	if reqInfo.PbsEntryPoint == metrics.ReqTypeAMP {
@@ -144,8 +146,6 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E
 	reqExt.Appnexus.IsAMP = isAMP
 	reqExt.Appnexus.HeaderBiddingSource = a.hbSource + isVIDEO
 
-	imps := request.Imp
-
 	// For long form requests if adpodId feature enabled, adpod_id must be sent downstream.
 	// Adpod id is a unique identifier for pod
 	// All impressions in the same pod must have the same pod id in request extension
@@ -153,11 +153,11 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E
 	// If impressions number per pod is more than maxImpsPerReq - divide those imps to several requests but keep pod id the same
 	// If  adpodId feature disabled and impressions number per pod is more than maxImpsPerReq  - divide those imps to several requests but do not include ad pod id
 	if isVIDEO == 1 && *shouldGenerateAdPodId {
-		requests, errors := a.buildAdPodRequests(imps, request, reqExt, requestURI)
+		requests, errors := a.buildAdPodRequests(request.Imp, request, reqExt, requestURI.String())
 		return requests, append(errs, errors...)
 	}
 
-	requests, errors := splitRequests(imps, request, reqExt, requestURI)
+	requests, errors := splitRequests(request.Imp, request, reqExt, requestURI.String())
 	return requests, append(errs, errors...)
 }
 
@@ -198,7 +198,7 @@ func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest
 				bid.Cat = []string{iabCategory}
 			} else if len(bid.Cat) > 1 {
 				//create empty categories array to force bid to be rejected
-				bid.Cat = make([]string, 0)
+				bid.Cat = []string{}
 			}
 
 			bidderResponse.Bids = append(bidderResponse.Bids, &adapters.TypedBid{
@@ -263,7 +263,7 @@ func groupByPods(imps []openrtb2.Imp) map[string]([]openrtb2.Imp) {
 }
 
 func splitRequests(imps []openrtb2.Imp, request *openrtb2.BidRequest, requestExtension appnexusReqExt, uri string) ([]*adapters.RequestData, []error) {
-	errs := []error{}
+	var errs []error
 	// Initial capacity for future array of requests, memory optimization.
 	// Let's say there are 35 impressions and limit impressions per request equals to 10.
 	// In this case we need to create 4 requests with 10, 10, 10 and 5 impressions.
@@ -334,7 +334,6 @@ func buildRequestImp(imp *openrtb2.Imp, appnexusExt *openrtb_ext.ExtImpAppnexus,
 			bannerCopy.Pos = adcom1.PositionBelowFold.Ptr()
 		}
 
-		// Fixes #307
 		if bannerCopy.W == nil && bannerCopy.H == nil && len(bannerCopy.Format) > 0 {
 			firstFormat := bannerCopy.Format[0]
 			bannerCopy.W = &(firstFormat.W)
@@ -351,32 +350,15 @@ 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,
 	}}
 
 	var err error
-	if imp.Ext, err = json.Marshal(&impExt); err != nil {
-		return err
-	}
-
-	return nil
-}
+	imp.Ext, err = json.Marshal(&impExt)
 
-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, ",")
+	return err
 }
 
 // getMediaTypeForBid determines which type of bid.
@@ -386,8 +368,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,16 +378,15 @@ 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
 }
 
-func appendMemberId(uri string, memberId string) string {
-	if strings.Contains(uri, "?") {
-		return uri + "&member_id=" + memberId
-	}
-
-	return uri + "?member_id=" + memberId
+func appendMemberId(uri url.URL, memberId string) url.URL {
+	q := uri.Query()
+	q.Set("member_id", memberId)
+	uri.RawQuery = q.Encode()
+	return uri
 }
 
 func buildDisplayManageVer(req *openrtb2.BidRequest) string {
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/adapters/flipp/flipp.go b/adapters/flipp/flipp.go
new file mode 100644
index 00000000..2eca3e41
--- /dev/null
+++ b/adapters/flipp/flipp.go
@@ -0,0 +1,228 @@
+package flipp
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/buger/jsonparser"
+	"github.com/gofrs/uuid"
+	"github.com/prebid/openrtb/v19/openrtb2"
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/config"
+	"github.com/prebid/prebid-server/openrtb_ext"
+	"github.com/prebid/prebid-server/util/httputil"
+)
+
+const (
+	bannerType      = "banner"
+	inlineDivName   = "inline"
+	flippBidder     = "flipp"
+	defaultCurrency = "USD"
+)
+
+var (
+	count    int64 = 1
+	adTypes        = []int64{4309, 641}
+	dtxTypes       = []int64{5061}
+)
+
+type adapter struct {
+	endpoint string
+}
+
+// Builder builds a new instance of the Flipp 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{
+		endpoint: config.Endpoint,
+	}
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	adapterRequests := make([]*adapters.RequestData, 0, len(request.Imp))
+	var errors []error
+
+	for _, imp := range request.Imp {
+		adapterReq, err := a.processImp(request, imp)
+		if err != nil {
+			errors = append(errors, err)
+			continue
+		}
+		adapterRequests = append(adapterRequests, adapterReq)
+	}
+	if len(adapterRequests) == 0 {
+		return nil, append(errors, fmt.Errorf("adapterRequest is empty"))
+	}
+	return adapterRequests, errors
+}
+
+func (a *adapter) makeRequest(request *openrtb2.BidRequest, campaignRequestBody CampaignRequestBody) (*adapters.RequestData, error) {
+	campaignRequestBodyJSON, err := json.Marshal(campaignRequestBody)
+	if err != nil {
+		return nil, err
+	}
+
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json")
+	if request.Device != nil && request.Device.UA != "" {
+		headers.Add("User-Agent", request.Device.UA)
+	}
+	return &adapters.RequestData{
+		Method:  http.MethodPost,
+		Uri:     a.endpoint,
+		Body:    campaignRequestBodyJSON,
+		Headers: headers,
+	}, err
+}
+
+func (a *adapter) processImp(request *openrtb2.BidRequest, imp openrtb2.Imp) (*adapters.RequestData, error) {
+	var flippExtParams openrtb_ext.ImpExtFlipp
+	params, _, _, err := jsonparser.Get(imp.Ext, "bidder")
+	if err != nil {
+		return nil, fmt.Errorf("flipp params not found. %v", err)
+	}
+	err = json.Unmarshal(params, &flippExtParams)
+	if err != nil {
+		return nil, fmt.Errorf("unable to extract flipp params. %v", err)
+	}
+
+	publisherUrl, err := url.Parse(request.Site.Page)
+	if err != nil {
+		return nil, fmt.Errorf("unable to parse site url. %v", err)
+	}
+
+	var contentCode string
+	if flippExtParams.Options.ContentCode != "" {
+		contentCode = flippExtParams.Options.ContentCode
+	} else if publisherUrl != nil {
+		contentCode = publisherUrl.Query().Get("flipp-content-code")
+	}
+
+	placement := Placement{
+		DivName: inlineDivName,
+		SiteID:  &flippExtParams.SiteID,
+		AdTypes: getAdTypes(flippExtParams.CreativeType),
+		ZoneIds: flippExtParams.ZoneIds,
+		Count:   &count,
+		Prebid:  buildPrebidRequest(flippExtParams, request, imp),
+		Properties: &Properties{
+			ContentCode: &contentCode,
+		},
+		Options: flippExtParams.Options,
+	}
+
+	var userIP string
+	if flippExtParams.IP != "" {
+		userIP = flippExtParams.IP
+	} else if request.Device != nil && request.Device.IP != "" {
+		userIP = request.Device.IP
+	} else {
+		return nil, fmt.Errorf("no IP set in flipp bidder params or request device")
+	}
+
+	var userKey string
+	if request.User != nil && request.User.ID != "" {
+		userKey = request.User.ID
+	} else if flippExtParams.UserKey != "" {
+		userKey = flippExtParams.UserKey
+	} else {
+		uid, err := uuid.NewV4()
+		if err != nil {
+			return nil, fmt.Errorf("unable to generate user uuid. %v", err)
+		}
+		userKey = uid.String()
+	}
+
+	keywordsArray := strings.Split(request.Site.Keywords, ",")
+
+	campaignRequestBody := CampaignRequestBody{
+		Placements: []*Placement{&placement},
+		URL:        request.Site.Page,
+		Keywords:   keywordsArray,
+		IP:         userIP,
+		User: &CampaignRequestBodyUser{
+			Key: &userKey,
+		},
+	}
+
+	adapterReq, err := a.makeRequest(request, campaignRequestBody)
+	if err != nil {
+		return nil, fmt.Errorf("make request failed with err %v", err)
+	}
+
+	return adapterReq, nil
+}
+
+func buildPrebidRequest(flippExtParams openrtb_ext.ImpExtFlipp, request *openrtb2.BidRequest, imp openrtb2.Imp) *PrebidRequest {
+	var height int64
+	var width int64
+	if imp.Banner != nil && len(imp.Banner.Format) > 0 {
+		height = imp.Banner.Format[0].H
+		width = imp.Banner.Format[0].W
+	}
+	prebidRequest := PrebidRequest{
+		CreativeType:            &flippExtParams.CreativeType,
+		PublisherNameIdentifier: &flippExtParams.PublisherNameIdentifier,
+		RequestID:               &imp.ID,
+		Height:                  &height,
+		Width:                   &width,
+	}
+	return &prebidRequest
+}
+
+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 campaignResponseBody CampaignResponseBody
+	if err := json.Unmarshal(responseData.Body, &campaignResponseBody); err != nil {
+		return nil, []error{err}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(len(request.Imp))
+	bidResponse.Currency = defaultCurrency
+	for _, imp := range request.Imp {
+		for _, decision := range campaignResponseBody.Decisions.Inline {
+			if *decision.Prebid.RequestID == imp.ID {
+				b := &adapters.TypedBid{
+					Bid:     buildBid(decision, imp.ID),
+					BidType: openrtb_ext.BidType(bannerType),
+				}
+				bidResponse.Bids = append(bidResponse.Bids, b)
+			}
+		}
+	}
+	return bidResponse, nil
+}
+
+func getAdTypes(creativeType string) []int64 {
+	if creativeType == "DTX" {
+		return dtxTypes
+	}
+	return adTypes
+}
+
+func buildBid(decision *InlineModel, impId string) *openrtb2.Bid {
+	bid := &openrtb2.Bid{
+		CrID:  fmt.Sprint(decision.CreativeID),
+		Price: *decision.Prebid.Cpm,
+		AdM:   *decision.Prebid.Creative,
+		ID:    fmt.Sprint(decision.AdID),
+		ImpID: impId,
+	}
+	if len(decision.Contents) > 0 || decision.Contents[0] != nil || decision.Contents[0].Data != nil {
+		if decision.Contents[0].Data.Width != 0 {
+			bid.W = decision.Contents[0].Data.Width
+		}
+		bid.H = 0
+	}
+	return bid
+}
diff --git a/adapters/flipp/flipp_params.go b/adapters/flipp/flipp_params.go
new file mode 100644
index 00000000..0eef61b4
--- /dev/null
+++ b/adapters/flipp/flipp_params.go
@@ -0,0 +1,88 @@
+package flipp
+
+import "github.com/prebid/prebid-server/openrtb_ext"
+
+type CampaignRequestBodyUser struct {
+	Key *string `json:"key"`
+}
+
+type Properties struct {
+	ContentCode *string `json:"contentCode,omitempty"`
+}
+
+type PrebidRequest struct {
+	CreativeType            *string `json:"creativeType"`
+	Height                  *int64  `json:"height"`
+	PublisherNameIdentifier *string `json:"publisherNameIdentifier"`
+	RequestID               *string `json:"requestId"`
+	Width                   *int64  `json:"width"`
+}
+
+type Placement struct {
+	AdTypes    []int64                        `json:"adTypes"`
+	Count      *int64                         `json:"count"`
+	DivName    string                         `json:"divName,omitempty"`
+	NetworkID  int64                          `json:"networkId,omitempty"`
+	Prebid     *PrebidRequest                 `json:"prebid,omitempty"`
+	Properties *Properties                    `json:"properties,omitempty"`
+	SiteID     *int64                         `json:"siteId"`
+	ZoneIds    []int64                        `json:"zoneIds"`
+	Options    openrtb_ext.ImpExtFlippOptions `json:"options,omitempty"`
+}
+
+type CampaignRequestBody struct {
+	IP                string                   `json:"ip,omitempty"`
+	Keywords          []string                 `json:"keywords"`
+	Placements        []*Placement             `json:"placements"`
+	PreferredLanguage *string                  `json:"preferred_language,omitempty"`
+	URL               string                   `json:"url,omitempty"`
+	User              *CampaignRequestBodyUser `json:"user"`
+}
+
+type CampaignResponseBody struct {
+	CandidateRetrieval interface{} `json:"candidateRetrieval,omitempty"`
+	Decisions          *Decisions  `json:"decisions"`
+}
+
+type Decisions struct {
+	Inline Inline `json:"inline,omitempty"`
+}
+
+type Inline []*InlineModel
+
+type Contents []*Content
+
+type Content struct {
+	Body           string `json:"body,omitempty"`
+	CustomTemplate string `json:"customTemplate,omitempty"`
+	Data           *Data2 `json:"data,omitempty"`
+	Type           string `json:"type,omitempty"`
+}
+
+type Data2 struct {
+	CustomData interface{} `json:"customData,omitempty"`
+	Height     int64       `json:"height,omitempty"`
+	Width      int64       `json:"width,omitempty"`
+}
+
+type InlineModel struct {
+	AdID          int64           `json:"adId,omitempty"`
+	AdvertiserID  int64           `json:"advertiserId,omitempty"`
+	CampaignID    int64           `json:"campaignId,omitempty"`
+	ClickURL      string          `json:"clickUrl,omitempty"`
+	Contents      Contents        `json:"contents,omitempty"`
+	CreativeID    int64           `json:"creativeId,omitempty"`
+	FlightID      int64           `json:"flightId,omitempty"`
+	Height        int64           `json:"height,omitempty"`
+	ImpressionURL string          `json:"impressionUrl,omitempty"`
+	Prebid        *PrebidResponse `json:"prebid,omitempty"`
+	PriorityID    int64           `json:"priorityId,omitempty"`
+	Width         int64           `json:"width,omitempty"`
+}
+
+type PrebidResponse struct {
+	Cpm          *float64 `json:"cpm"`
+	Creative     *string  `json:"creative"`
+	CreativeType *string  `json:"creativeType"`
+	RequestID    *string  `json:"requestId"`
+}
diff --git a/adapters/huaweiads/huaweiads.go b/adapters/huaweiads/huaweiads.go
index 28ae0d9a..4944fd5e 100644
--- a/adapters/huaweiads/huaweiads.go
+++ b/adapters/huaweiads/huaweiads.go
@@ -771,7 +771,7 @@ func convertCountryCode(country string) (out string) {
 		return mappedCountry
 	}
 
-	if len(country) >= 3 {
+	if len(country) >= 2 {
 		return country[0:2]
 	}
 
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/config/accounts.go b/config/account.go
similarity index 98%
rename from config/accounts.go
rename to config/account.go
index f96eb53a..393194bc 100644
--- a/config/accounts.go
+++ b/config/account.go
@@ -40,6 +40,7 @@ type Account struct {
 	Validations             Validations                                 `mapstructure:"validations" json:"validations"`
 	DefaultBidLimit         int                                         `mapstructure:"default_bid_limit" json:"default_bid_limit"`
 	BidAdjustments          *openrtb_ext.ExtRequestPrebidBidAdjustments `mapstructure:"bidadjustments" json:"bidadjustments"`
+	Privacy                 AccountPrivacy                              `mapstructure:"privacy" json:"privacy"`
 }
 
 // CookieSync represents the account-level defaults for the cookie sync endpoint.
@@ -291,3 +292,7 @@ func (m AccountModules) ModuleConfig(id string) (json.RawMessage, error) {
 func (a *AccountChannel) IsSet() bool {
 	return a.AMP != nil || a.App != nil || a.Video != nil || a.Web != nil
 }
+
+type AccountPrivacy struct {
+	AllowActivities AllowActivities `mapstructure:"allowactivities" json:"allowactivities"`
+}
diff --git a/config/accounts_test.go b/config/account_test.go
similarity index 100%
rename from config/accounts_test.go
rename to config/account_test.go
diff --git a/config/activity.go b/config/activity.go
new file mode 100644
index 00000000..987cbe84
--- /dev/null
+++ b/config/activity.go
@@ -0,0 +1,26 @@
+package config
+
+type AllowActivities struct {
+	SyncUser                 Activity `mapstructure:"syncUser" json:"syncUser"`
+	FetchBids                Activity `mapstructure:"fetchBids" json:"fetchBids"`
+	EnrichUserFPD            Activity `mapstructure:"enrichUfpd" json:"enrichUfpd"`
+	ReportAnalytics          Activity `mapstructure:"reportAnalytics" json:"reportAnalytics"`
+	TransmitUserFPD          Activity `mapstructure:"transmitUfpd" json:"transmitUfpd"`
+	TransmitPreciseGeo       Activity `mapstructure:"transmitPreciseGeo" json:"transmitPreciseGeo"`
+	TransmitUniqueRequestIds Activity `mapstructure:"transmitUniqueRequestIds" json:"transmitUniqueRequestIds"`
+}
+
+type Activity struct {
+	Default *bool          `mapstructure:"default" json:"default"`
+	Rules   []ActivityRule `mapstructure:"rules" json:"rules"`
+	Allow   bool           `mapstructure:"allow" json:"allow"`
+}
+
+type ActivityRule struct {
+	Condition ActivityCondition `mapstructure:"condition" json:"condition"`
+}
+
+type ActivityCondition struct {
+	ComponentName []string `mapstructure:"componentName" json:"componentName"`
+	ComponentType []string `mapstructure:"componentType" json:"componentType"`
+}
diff --git a/config/config.go b/config/config.go
index d70afc21..b537d128 100644
--- a/config/config.go
+++ b/config/config.go
@@ -39,6 +39,7 @@ type Configuration struct {
 	// If empty, it will return a 204 with no content.
 	StatusResponse    string          `mapstructure:"status_response"`
 	AuctionTimeouts   AuctionTimeouts `mapstructure:"auction_timeouts_ms"`
+	TmaxAdjustments   TmaxAdjustments `mapstructure:"tmax_adjustments"`
 	CacheURL          Cache           `mapstructure:"cache"`
 	ExtCacheURL       ExternalCache   `mapstructure:"external_cache"`
 	RecaptchaSecret   string          `mapstructure:"recaptcha_secret"`
@@ -148,6 +149,11 @@ func (cfg *Configuration) validate(v *viper.Viper) []error {
 		errs = append(errs, errors.New("account_defaults.Events.VASTEvents has no effect as the feature is under development."))
 	}
 
+	if cfg.TmaxAdjustments.Enabled {
+		glog.Warning(`cfg.TmaxAdjustments.Enabled will currently not do anything as tmax adjustment feature is still under development.`)
+		cfg.TmaxAdjustments.Enabled = false
+	}
+
 	errs = cfg.Experiment.validate(errs)
 	errs = cfg.BidderInfos.validate(errs)
 	return errs
@@ -1029,6 +1035,11 @@ func SetupViper(v *viper.Viper, filename string, bidderInfos BidderInfos) {
 	v.SetDefault("debug.timeout_notification.fail_only", false)
 	v.SetDefault("debug.override_token", "")
 
+	v.SetDefault("tmax_adjustments.enabled", false)
+	v.SetDefault("tmax_adjustments.bidder_response_duration_min_ms", 0)
+	v.SetDefault("tmax_adjustments.bidder_network_latency_buffer_ms", 0)
+	v.SetDefault("tmax_adjustments.pbs_response_preparation_duration_ms", 0)
+
 	/* IPv4
 	/*  Site Local: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
 	/*  Link Local: 169.254.0.0/16
@@ -1506,3 +1517,22 @@ func isValidCookieSize(maxCookieSize int) error {
 	}
 	return nil
 }
+
+// Tmax Adjustments enables PBS to estimate the tmax value for bidders, indicating the allotted time for them to respond to a request.
+// It's important to note that the calculated tmax is just an estimate and will not be entirely precise.
+// PBS will calculate the bidder tmax as follows:
+// bidderTmax = request.tmax - reqProcessingTime - BidderNetworkLatencyBuffer - PBSResponsePreparationDuration
+// Note that reqProcessingTime is time taken by PBS to process a given request before it is sent to bid adapters and is computed at run time.
+type TmaxAdjustments struct {
+	// Enabled indicates whether bidder tmax should be calculated and passed on to bid adapters
+	Enabled bool `mapstructure:"enabled"`
+	// BidderNetworkLatencyBuffer accounts for network delays between PBS and bidder servers.
+	// A value of 0 indicates no network latency buffer should be accounted for when calculating the bidder tmax.
+	BidderNetworkLatencyBuffer uint `mapstructure:"bidder_network_latency_buffer_ms"`
+	// PBSResponsePreparationDuration accounts for amount of time required for PBS to process all bidder responses and generate final response for a request.
+	// A value of 0 indicates PBS response preparation time shouldn't be accounted for when calculating bidder tmax.
+	PBSResponsePreparationDuration uint `mapstructure:"pbs_response_preparation_duration_ms"`
+	// BidderResponseDurationMin is the minimum amount of time expected to get a response from a bidder request.
+	// PBS won't send a request to the bidder if the bidder tmax calculated is less than the BidderResponseDurationMin value
+	BidderResponseDurationMin uint `mapstructure:"bidder_response_duration_min_ms"`
+}
diff --git a/endpoints/cookie_sync.go b/endpoints/cookie_sync.go
index 4b043bdd..8b930962 100644
--- a/endpoints/cookie_sync.go
+++ b/endpoints/cookie_sync.go
@@ -129,7 +129,7 @@ func (c *cookieSyncEndpoint) parseRequest(r *http.Request) (usersync.Request, pr
 	if request.GDPR != nil {
 		gdprString = strconv.Itoa(*request.GDPR)
 	}
-	gdprSignal, err := gdpr.SignalParse(gdprString)
+	gdprSignal, err := gdpr.StrSignalParse(gdprString)
 	if err != nil {
 		return usersync.Request{}, privacy.Policies{}, err
 	}
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 7f361720..62353711 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -37,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"
@@ -186,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)
@@ -230,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
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index af9ac883..e2b3af82 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -72,10 +72,12 @@ 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"
 	"github.com/prebid/prebid-server/adapters/epom"
+	"github.com/prebid/prebid-server/adapters/flipp"
 	"github.com/prebid/prebid-server/adapters/freewheelssp"
 	"github.com/prebid/prebid-server/adapters/frvradn"
 	"github.com/prebid/prebid-server/adapters/gamma"
@@ -239,6 +241,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderConnectAd:         connectad.Builder,
 		openrtb_ext.BidderConsumable:        consumable.Builder,
 		openrtb_ext.BidderConversant:        conversant.Builder,
+		openrtb_ext.BidderCopper6:           adtelligent.Builder,
 		openrtb_ext.BidderCpmstar:           cpmstar.Builder,
 		openrtb_ext.BidderCriteo:            criteo.Builder,
 		openrtb_ext.BidderCWire:             cwire.Builder,
@@ -248,12 +251,14 @@ 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,
 		openrtb_ext.BidderEpom:              epom.Builder,
 		openrtb_ext.BidderEVolution:         evolution.Builder,
 		openrtb_ext.BidderEvtech:            limelightDigital.Builder,
+		openrtb_ext.BidderFlipp:             flipp.Builder,
 		openrtb_ext.BidderFreewheelSSP:      freewheelssp.Builder,
 		openrtb_ext.BidderFreewheelSSPOld:   freewheelssp.Builder,
 		openrtb_ext.BidderFRVRAdNetwork:     frvradn.Builder,
diff --git a/exchange/bidder.go b/exchange/bidder.go
index e355c18c..f5efcff3 100644
--- a/exchange/bidder.go
+++ b/exchange/bidder.go
@@ -699,7 +699,7 @@ func getBidTypeForAdjustments(bidType openrtb_ext.BidType, impID string, imp []o
 	if bidType == openrtb_ext.BidTypeVideo {
 		for _, imp := range imp {
 			if imp.ID == impID {
-				if imp.Video.Plcmt == adcom1.VideoPlcmtAccompanyingContent {
+				if imp.Video != nil && imp.Video.Plcmt == adcom1.VideoPlcmtAccompanyingContent {
 					return "video-outstream"
 				}
 				break
diff --git a/exchange/exchange.go b/exchange/exchange.go
index 7ce070eb..c1b46d33 100644
--- a/exchange/exchange.go
+++ b/exchange/exchange.go
@@ -65,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
@@ -126,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()
@@ -147,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,
 	}
@@ -162,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,
@@ -194,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.
diff --git a/exchange/gdpr.go b/exchange/gdpr.go
index c8b205ad..d503eb5d 100644
--- a/exchange/gdpr.go
+++ b/exchange/gdpr.go
@@ -1,59 +1,37 @@
 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"
+	gppPolicy "github.com/prebid/prebid-server/privacy/gpp"
 )
 
-// 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 {
-			if id == int8(gppConstants.SectionTCFEU2) {
-				return gdpr.SignalYes, nil
-			}
+// 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 {
+		if gppPolicy.IsSIDInList(req.Regs.GPPSID, 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) {
-	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)
+// getConsent will pull the consent string from an openrtb request
+func getConsent(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) (consent string, err error) {
+	if i := gppPolicy.IndexOfSID(gpp, gppConstants.SectionTCFEU2); i >= 0 {
+		consent = gpp.Sections[i].GetValue()
+		return
 	}
-	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/gdpr/signal.go b/gdpr/signal.go
index 8a5317a4..ed7fe1dd 100644
--- a/gdpr/signal.go
+++ b/gdpr/signal.go
@@ -16,15 +16,24 @@ const (
 
 var gdprSignalError = &errortypes.BadInput{Message: "GDPR signal should be integer 0 or 1"}
 
-// SignalParse returns a parsed GDPR signal or a parse error.
-func SignalParse(rawSignal string) (Signal, error) {
-	if rawSignal == "" {
+// StrSignalParse returns a parsed GDPR signal or a parse error.
+func StrSignalParse(signal string) (Signal, error) {
+	if signal == "" {
 		return SignalAmbiguous, nil
 	}
 
-	i, err := strconv.Atoi(rawSignal)
+	i, err := strconv.Atoi(signal)
 
-	if err != nil || (i != 0 && i != 1) {
+	if err != nil {
+		return SignalAmbiguous, gdprSignalError
+	}
+
+	return IntSignalParse(i)
+}
+
+// IntSignalParse checks parameter i is not out of bounds and returns a GDPR signal error
+func IntSignalParse(i int) (Signal, error) {
+	if i != 0 && i != 1 {
 		return SignalAmbiguous, gdprSignalError
 	}
 
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 1b959c5d..5a342a98 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -148,6 +148,7 @@ const (
 	BidderConnectAd         BidderName = "connectad"
 	BidderConsumable        BidderName = "consumable"
 	BidderConversant        BidderName = "conversant"
+	BidderCopper6           BidderName = "copper6"
 	BidderCpmstar           BidderName = "cpmstar"
 	BidderCriteo            BidderName = "criteo"
 	BidderCWire             BidderName = "cwire"
@@ -157,12 +158,14 @@ const (
 	BidderDefinemedia       BidderName = "definemedia"
 	BidderDianomi           BidderName = "dianomi"
 	BidderDmx               BidderName = "dmx"
+	BidderEmtv              BidderName = "emtv"
 	BidderEmxDigital        BidderName = "emx_digital"
 	BidderEngageBDR         BidderName = "engagebdr"
 	BidderEPlanning         BidderName = "eplanning"
 	BidderEpom              BidderName = "epom"
 	BidderEVolution         BidderName = "e_volution"
 	BidderEvtech            BidderName = "evtech"
+	BidderFlipp             BidderName = "flipp"
 	BidderFreewheelSSP      BidderName = "freewheelssp"
 	BidderFreewheelSSPOld   BidderName = "freewheel-ssp"
 	BidderFRVRAdNetwork     BidderName = "frvradn"
@@ -336,6 +339,7 @@ func CoreBidderNames() []BidderName {
 		BidderConnectAd,
 		BidderConsumable,
 		BidderConversant,
+		BidderCopper6,
 		BidderCpmstar,
 		BidderCriteo,
 		BidderCWire,
@@ -345,12 +349,14 @@ func CoreBidderNames() []BidderName {
 		BidderDefinemedia,
 		BidderDianomi,
 		BidderDmx,
+		BidderEmtv,
 		BidderEmxDigital,
 		BidderEngageBDR,
 		BidderEPlanning,
 		BidderEpom,
 		BidderEVolution,
 		BidderEvtech,
+		BidderFlipp,
 		BidderFreewheelSSP,
 		BidderFreewheelSSPOld,
 		BidderFRVRAdNetwork,
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/imp_flipp.go b/openrtb_ext/imp_flipp.go
new file mode 100644
index 00000000..029e2882
--- /dev/null
+++ b/openrtb_ext/imp_flipp.go
@@ -0,0 +1,17 @@
+package openrtb_ext
+
+type ImpExtFlipp struct {
+	PublisherNameIdentifier string             `json:"publisherNameIdentifier"`
+	CreativeType            string             `json:"creativeType"`
+	SiteID                  int64              `json:"siteId"`
+	ZoneIds                 []int64            `json:"zoneIds,omitempty"`
+	UserKey                 string             `json:"userKey,omitempty"`
+	IP                      string             `json:"ip,omitempty"`
+	Options                 ImpExtFlippOptions `json:"options,omitempty"`
+}
+
+type ImpExtFlippOptions struct {
+	StartCompact bool   `json:"startCompact,omitempty"`
+	DwellExpand  bool   `json:"dwellExpand,omitempty"`
+	ContentCode  string `json:"contentCode,omitempty"`
+}
diff --git a/privacy/activity.go b/privacy/activity.go
new file mode 100644
index 00000000..7cdef175
--- /dev/null
+++ b/privacy/activity.go
@@ -0,0 +1,35 @@
+package privacy
+
+// Activity defines privileges which can be controlled directly by the publisher or via privacy policies.
+type Activity int
+
+const (
+	ActivitySyncUser Activity = iota + 1
+	ActivityFetchBids
+	ActivityEnrichUserFPD
+	ActivityReportAnalytics
+	ActivityTransmitUserFPD
+	ActivityTransmitPreciseGeo
+	ActivityTransmitUniqueRequestIds
+)
+
+func (a Activity) String() string {
+	switch a {
+	case ActivitySyncUser:
+		return "syncUser"
+	case ActivityFetchBids:
+		return "fetchBids"
+	case ActivityEnrichUserFPD:
+		return "enrichUfpd"
+	case ActivityReportAnalytics:
+		return "reportAnalytics"
+	case ActivityTransmitUserFPD:
+		return "transmitUfpd"
+	case ActivityTransmitPreciseGeo:
+		return "transmitPreciseGeo"
+	case ActivityTransmitUniqueRequestIds:
+		return "transmitUniqueRequestIds"
+	}
+
+	return ""
+}
diff --git a/privacy/ccpa/policy.go b/privacy/ccpa/policy.go
index 6333e2ad..fbafd8a8 100644
--- a/privacy/ccpa/policy.go
+++ b/privacy/ccpa/policy.go
@@ -1,6 +1,7 @@
 package ccpa
 
 import (
+	"errors"
 	"fmt"
 
 	gpplib "github.com/prebid/go-gpp"
@@ -8,6 +9,7 @@ import (
 	"github.com/prebid/openrtb/v19/openrtb2"
 	"github.com/prebid/prebid-server/errortypes"
 	"github.com/prebid/prebid-server/openrtb_ext"
+	gppPolicy "github.com/prebid/prebid-server/privacy/gpp"
 )
 
 // Policy represents the CCPA regulatory information from an OpenRTB bid request.
@@ -18,33 +20,25 @@ type Policy struct {
 
 // ReadFromRequestWrapper extracts the CCPA regulatory information from an OpenRTB bid request.
 func ReadFromRequestWrapper(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) (Policy, error) {
-	var consent string
 	var noSaleBidders []string
-	var warn error = nil
+	var gppSIDs []int8
+	var requestUSPrivacy string
+	var warn error
 
 	if req == nil || req.BidRequest == nil {
 		return Policy{}, nil
 	}
 
 	if req.BidRequest.Regs != nil {
-		for _, s := range req.BidRequest.Regs.GPPSID {
-			if s == int8(gppConstants.SectionUSPV1) {
-				for i, id := range gpp.SectionTypes {
-					if id == gppConstants.SectionUSPV1 {
-						consent = gpp.Sections[i].GetValue()
-					}
-				}
-			}
-		}
-		if req.BidRequest.Regs.USPrivacy != "" {
-			if consent == "" {
-				consent = req.BidRequest.Regs.USPrivacy
-			} else if consent != req.BidRequest.Regs.USPrivacy {
-				warn = &errortypes.Warning{
-					Message:     "regs.us_privacy consent does not match uspv1 in GPP, using regs.gpp",
-					WarningCode: errortypes.InvalidPrivacyConsentWarningCode}
-			}
-		}
+		requestUSPrivacy = req.BidRequest.Regs.USPrivacy
+		gppSIDs = req.BidRequest.Regs.GPPSID
+	}
+
+	consent, err := SelectCCPAConsent(requestUSPrivacy, gpp, gppSIDs)
+	if err != nil {
+		warn = &errortypes.Warning{
+			Message:     "regs.us_privacy consent does not match uspv1 in GPP, using regs.gpp",
+			WarningCode: errortypes.InvalidPrivacyConsentWarningCode}
 	}
 
 	if consent == "" {
@@ -100,6 +94,29 @@ func (p Policy) Write(req *openrtb_ext.RequestWrapper) error {
 	return nil
 }
 
+func SelectCCPAConsent(requestUSPrivacy string, gpp gpplib.GppContainer, gppSIDs []int8) (string, error) {
+	var consent string
+	var err error
+
+	if len(gpp.SectionTypes) > 0 {
+		if gppPolicy.IsSIDInList(gppSIDs, gppConstants.SectionUSPV1) {
+			if i := gppPolicy.IndexOfSID(gpp, gppConstants.SectionUSPV1); i >= 0 {
+				consent = gpp.Sections[i].GetValue()
+			}
+		}
+	}
+
+	if requestUSPrivacy != "" {
+		if consent == "" {
+			consent = requestUSPrivacy
+		} else if consent != requestUSPrivacy {
+			err = errors.New("request.us_privacy consent does not match uspv1")
+		}
+	}
+
+	return consent, err
+}
+
 func setPrebidNoSale(noSaleBidders []string, ext *openrtb_ext.RequestExt) {
 	if len(noSaleBidders) == 0 {
 		setPrebidNoSaleClear(ext)
diff --git a/privacy/gpp/gpp.go b/privacy/gpp/gpp.go
index fe3507b6..2ebf4b97 100644
--- a/privacy/gpp/gpp.go
+++ b/privacy/gpp/gpp.go
@@ -1,8 +1,36 @@
 package gpp
 
+import (
+	gpplib "github.com/prebid/go-gpp"
+	gppConstants "github.com/prebid/go-gpp/constants"
+)
+
 // Policy represents the GPP privacy string container.
 // Currently just a placeholder until more expansive support is made.
 type Policy struct {
 	Consent string
 	RawSID  string // This is the CSV format ("2,6") that the IAB recommends for passing the SID(s) on a query string.
 }
+
+// IsSIDInList returns true if the 'sid' value is found in the gppSIDs array. Its logic is used in more than
+// one place in our codebase, therefore it was decided to make it its own function.
+func IsSIDInList(gppSIDs []int8, sid gppConstants.SectionID) bool {
+	for _, id := range gppSIDs {
+		if id == int8(sid) {
+			return true
+		}
+	}
+	return false
+}
+
+// IndexOfSID returns a zero or non-negative integer that represents the position of the 'sid' value in the
+// 'gpp.SectionTypes' array. If the 'sid' value is not found, returns -1. This logic is used in
+// more than one place in our codebase, therefore it was decided to make it its own function.
+func IndexOfSID(gpp gpplib.GppContainer, sid gppConstants.SectionID) int {
+	for i, id := range gpp.SectionTypes {
+		if id == sid {
+			return i
+		}
+	}
+	return -1
+}
diff --git a/privacy/lmt/ios.go b/privacy/lmt/ios.go
index 9e1847b3..0b308a9c 100644
--- a/privacy/lmt/ios.go
+++ b/privacy/lmt/ios.go
@@ -56,7 +56,7 @@ func modifyForIOS142OrGreater(req *openrtb2.BidRequest) {
 
 	switch *atts {
 	case openrtb_ext.IOSAppTrackingStatusNotDetermined:
-		req.Device.Lmt = &int8Zero
+		req.Device.Lmt = &int8One
 	case openrtb_ext.IOSAppTrackingStatusRestricted:
 		req.Device.Lmt = &int8One
 	case openrtb_ext.IOSAppTrackingStatusDenied:
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/copper6.yaml b/static/bidder-info/copper6.yaml
new file mode 100644
index 00000000..213cbe36
--- /dev/null
+++ b/static/bidder-info/copper6.yaml
@@ -0,0 +1,17 @@
+endpoint: "http://ghb.app.copper6.com/pbs/ortb"
+maintainer:
+  email: "info@copper6.com"
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+  site:
+    mediaTypes:
+      - banner
+      - video
+userSync:
+  # Copper6 ssp supports user syncing, but requires configuration by the host. contact this
+  # bidder directly at the email address in this file to ask about enabling user sync.
+  supports:
+    - iframe
diff --git a/static/bidder-info/criteo.yaml b/static/bidder-info/criteo.yaml
index 6d6b5b61..7d58c68d 100644
--- a/static/bidder-info/criteo.yaml
+++ b/static/bidder-info/criteo.yaml
@@ -14,9 +14,9 @@ capabilities:
 userSync:
   # criteo supports user syncing, but requires configuration by the host. contact this
   # bidder directly at the email address in this file to ask about enabling user sync.
-  supports:
-    - redirect
-
   redirect:
     url: "https://ssp-sync.criteo.com/user-sync/redirect?gdprapplies={{.GDPR}}&gdpr={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}&profile=230"
+    userMacro: "${CRITEO_USER_ID}"
+  iframe:
+    url: "https://ssp-sync.criteo.com/user-sync/iframe?gdprapplies={{.GDPR}}&gdpr={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}&profile=230"
     userMacro: "${CRITEO_USER_ID}"
\ No newline at end of file
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-info/flipp.yaml b/static/bidder-info/flipp.yaml
new file mode 100644
index 00000000..431c1345
--- /dev/null
+++ b/static/bidder-info/flipp.yaml
@@ -0,0 +1,7 @@
+endpoint: "https://cdn-gateflipp.flippback.com/flyer-locator-service/prebid_campaigns"
+maintainer:
+  email: prebid@flipp.com
+capabilities:
+  site:
+    mediaTypes:
+      - banner
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/copper6.json b/static/bidder-params/copper6.json
new file mode 100644
index 00000000..fa4050f6
--- /dev/null
+++ b/static/bidder-params/copper6.json
@@ -0,0 +1,26 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Copper6 Adapter Params",
+  "description": "A schema which validates params accepted by the Copper6 ssp adapter",
+
+  "type": "object",
+  "properties": {
+    "placementId": {
+      "type": "integer",
+      "description": "An ID which identifies this placement of the impression"
+    },
+    "siteId": {
+      "type": "integer",
+      "description": "An ID which identifies the site selling the impression"
+    },
+    "aid": {
+      "type": "integer",
+      "description": "An ID which identifies the channel"
+    },
+    "bidFloor": {
+      "type": "number",
+      "description": "BidFloor, US Dollars"
+    }
+  },
+  "required": ["aid"]
+}
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
diff --git a/static/bidder-params/flipp.json b/static/bidder-params/flipp.json
new file mode 100644
index 00000000..b2952eda
--- /dev/null
+++ b/static/bidder-params/flipp.json
@@ -0,0 +1,52 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Flipp Adapter Params",
+  "description": "A schema which validates params accepted by the Flipp adapter",
+  "type": "object",
+  "properties": {
+    "publisherNameIdentifier": {
+      "type": "string",
+      "minLength": 1,
+      "description": "Publisher Name Identifier"
+    },
+    "creativeType": {
+      "type": "string",
+      "enum": ["NativeX", "DTX"]
+    },
+    "siteId": {
+      "type": "integer"
+    },
+    "zoneIds": {
+      "type": "array",
+      "items": {
+        "type": "integer"
+      }
+    },
+    "userKey": {
+      "type": "string",
+      "format": "uuid"
+    },
+    "ip": {
+      "type": "string"
+    },
+    "options": {
+      "type": "object",
+      "properties": {
+        "startCompact": {
+          "type": "boolean"
+        },
+        "dwellExpand": {
+          "type": "boolean"
+        },
+        "contentCode": {
+          "type": "string"
+        }
+      }
+    }
+  },
+  "required": [
+    "publisherNameIdentifier",
+    "siteId",
+    "creativeType"
+  ]
+}
\ No newline at end of file
diff --git a/usersync/cookie.go b/usersync/cookie.go
index 5732e6c4..51474afd 100644
--- a/usersync/cookie.go
+++ b/usersync/cookie.go
@@ -226,10 +226,9 @@ func (cookie *Cookie) TrySync(key string, uid string) error {
 // This exists so that Cookie (which is public) can have private fields, and the rest of
 // the code doesn't have to worry about the cookie data storage format.
 type cookieJson struct {
-	LegacyUIDs map[string]string        `json:"uids,omitempty"`
-	UIDs       map[string]uidWithExpiry `json:"tempUIDs,omitempty"`
-	OptOut     bool                     `json:"optout,omitempty"`
-	Birthday   *time.Time               `json:"bday,omitempty"`
+	UIDs     map[string]uidWithExpiry `json:"tempUIDs,omitempty"`
+	OptOut   bool                     `json:"optout,omitempty"`
+	Birthday *time.Time               `json:"bday,omitempty"`
 }
 
 func (cookie *Cookie) MarshalJSON() ([]byte, error) {
@@ -240,53 +239,31 @@ func (cookie *Cookie) MarshalJSON() ([]byte, error) {
 	})
 }
 
-// UnmarshalJSON holds some transition code.
-//
-// "Legacy" cookies had UIDs *without* expiration dates, and recognized "0" as a legitimate UID for audienceNetwork.
-// "Current" cookies always include UIDs with expiration dates, and never allow "0" for audienceNetwork.
-//
-// This Unmarshal method interprets both data formats, and does some conversions on legacy data to make it current.
-// If you're seeing this message after March 2018, it's safe to assume that all the legacy cookies have been
-// updated and remove the legacy logic.
 func (cookie *Cookie) UnmarshalJSON(b []byte) error {
 	var cookieContract cookieJson
-	err := json.Unmarshal(b, &cookieContract)
-	if err == nil {
-		cookie.optOut = cookieContract.OptOut
-		cookie.birthday = cookieContract.Birthday
-
-		if cookie.optOut {
-			cookie.uids = make(map[string]uidWithExpiry)
-		} else {
-			cookie.uids = cookieContract.UIDs
-
-			if cookie.uids == nil {
-				cookie.uids = make(map[string]uidWithExpiry, len(cookieContract.LegacyUIDs))
-			}
+	if err := json.Unmarshal(b, &cookieContract); err != nil {
+		return err
+	}
 
-			// Interpret "legacy" UIDs as having been expired already.
-			// This should cause us to re-sync, since it would be time for a new one.
-			for bidder, uid := range cookieContract.LegacyUIDs {
-				if _, ok := cookie.uids[bidder]; !ok {
-					cookie.uids[bidder] = uidWithExpiry{
-						UID:     uid,
-						Expires: time.Now().Add(-5 * time.Minute),
-					}
-				}
-			}
+	cookie.optOut = cookieContract.OptOut
+	cookie.birthday = cookieContract.Birthday
 
-			// Any "0" values from audienceNetwork really meant "no ID available." This happens if they've never
-			// logged into Facebook. However... once we know a user's ID, we stop trying to re-sync them until the
-			// expiration date has passed.
-			//
-			// Since users may log into facebook later, this is a bad strategy.
-			// Since "0" is a fake ID for this bidder, we'll just treat it like it doesn't exist.
-			if id, ok := cookie.uids[string(openrtb_ext.BidderAudienceNetwork)]; ok && id.UID == "0" {
-				delete(cookie.uids, string(openrtb_ext.BidderAudienceNetwork))
-			}
-		}
+	if cookie.optOut {
+		cookie.uids = nil
+	} else {
+		cookie.uids = cookieContract.UIDs
 	}
-	return err
+
+	if cookie.uids == nil {
+		cookie.uids = make(map[string]uidWithExpiry)
+	}
+
+	// Audience Network / Facebook Handling
+	if id, ok := cookie.uids[string(openrtb_ext.BidderAudienceNetwork)]; ok && id.UID == "0" {
+		delete(cookie.uids, string(openrtb_ext.BidderAudienceNetwork))
+	}
+
+	return nil
 }
 
 func timestamp() *time.Time {
diff --git a/util/stringutil/stringutil.go b/util/stringutil/stringutil.go
new file mode 100644
index 00000000..0443a442
--- /dev/null
+++ b/util/stringutil/stringutil.go
@@ -0,0 +1,25 @@
+package stringutil
+
+import (
+	"strconv"
+	"strings"
+)
+
+// StrToInt8Slice breaks a string into a series of tokens using a comma as a delimiter but only
+// appends the tokens into the return array if tokens can be interpreted as an 'int8'
+func StrToInt8Slice(str string) ([]int8, error) {
+	var r []int8
+
+	if len(str) > 0 {
+		strSlice := strings.Split(str, ",")
+		for _, s := range strSlice {
+			v, err := strconv.ParseInt(s, 10, 8)
+			if err != nil {
+				return nil, err
+			}
+			r = append(r, int8(v))
+		}
+	}
+
+	return r, nil
+}


Test patch:
diff --git a/adapters/adapterstest/test_json.go b/adapters/adapterstest/test_json.go
index 6748cb8a..bdd25990 100644
--- a/adapters/adapterstest/test_json.go
+++ b/adapters/adapterstest/test_json.go
@@ -3,8 +3,10 @@ package adapterstest
 import (
 	"encoding/json"
 	"fmt"
+	"io/fs"
 	"net/http"
 	"os"
+	"path/filepath"
 	"regexp"
 	"testing"
 
@@ -18,6 +20,16 @@ import (
 	"github.com/yudai/gojsondiff/formatter"
 )
 
+const jsonFileExtension string = ".json"
+
+var supportedDirs = map[string]struct{}{
+	"exemplary":         {},
+	"supplemental":      {},
+	"amp":               {},
+	"video":             {},
+	"videosupplemental": {},
+}
+
 // RunJSONBidderTest is a helper method intended to unit test Bidders' adapters.
 // It requires that:
 //
@@ -53,30 +65,32 @@ import (
 //	  adapterstest.RunJSONBidderTest(t, "{bidder}test", instanceOfYourBidder)
 //	}
 func RunJSONBidderTest(t *testing.T, rootDir string, bidder adapters.Bidder) {
-	runTests(t, fmt.Sprintf("%s/exemplary", rootDir), bidder, false, false, false)
-	runTests(t, fmt.Sprintf("%s/supplemental", rootDir), bidder, true, false, false)
-	runTests(t, fmt.Sprintf("%s/amp", rootDir), bidder, true, true, false)
-	runTests(t, fmt.Sprintf("%s/video", rootDir), bidder, false, false, true)
-	runTests(t, fmt.Sprintf("%s/videosupplemental", rootDir), bidder, true, false, true)
-}
+	err := filepath.WalkDir(rootDir, func(path string, info fs.DirEntry, err error) error {
+		if err != nil {
+			return err
+		}
+
+		base := filepath.Base(filepath.Dir(path))
+		if _, ok := supportedDirs[base]; !ok {
+			return nil
+		}
 
-// runTests runs all the *.json files in a directory. If allowErrors is false, and one of the test files
-// expects errors from the bidder, then the test will fail.
-func runTests(t *testing.T, directory string, bidder adapters.Bidder, allowErrors, isAmpTest, isVideoTest bool) {
-	if specFiles, err := os.ReadDir(directory); err == nil {
-		for _, specFile := range specFiles {
-			fileName := fmt.Sprintf("%s/%s", directory, specFile.Name())
-			specData, err := loadFile(fileName)
+		allowErrors := base != "exemplary" && base != "video"
+		if !info.IsDir() && filepath.Ext(info.Name()) == jsonFileExtension {
+			specData, err := loadFile(path)
 			if err != nil {
-				t.Fatalf("Failed to load contents of file %s: %v", fileName, err)
+				t.Fatalf("Failed to load contents of file %s: %v", path, err)
 			}
 
 			if !allowErrors && specData.expectsErrors() {
-				t.Fatalf("Exemplary spec %s must not expect errors.", fileName)
+				t.Fatalf("Exemplary spec %s must not expect errors.", path)
 			}
-			runSpec(t, fileName, specData, bidder, isAmpTest, isVideoTest)
+
+			runSpec(t, path, specData, bidder, base == "amp", base == "videosupplemental" || base == "video")
 		}
-	}
+		return nil
+	})
+	assert.NoError(t, err, "Error reading files from directory %s \n", rootDir)
 }
 
 // LoadFile reads and parses a file as a test case. If something goes wrong, it returns an error.
diff --git a/adapters/appnexus/appnexus_test.go b/adapters/appnexus/appnexus_test.go
index 6797f4bb..e6ba0109 100644
--- a/adapters/appnexus/appnexus_test.go
+++ b/adapters/appnexus/appnexus_test.go
@@ -1,11 +1,13 @@
 package appnexus
 
 import (
+	"net/url"
 	"testing"
 
 	"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) {
@@ -22,12 +24,21 @@ func TestJsonSamples(t *testing.T) {
 	adapterstest.RunJSONBidderTest(t, "appnexustest", bidder)
 }
 
-func TestMemberQueryParam(t *testing.T) {
-	uriWithMember := appendMemberId("http://ib.adnxs.com/openrtb2?query_param=true", "102")
-	expected := "http://ib.adnxs.com/openrtb2?query_param=true&member_id=102"
-	if uriWithMember != expected {
-		t.Errorf("appendMemberId() failed on URI with query string. Expected %s, got %s", expected, uriWithMember)
-	}
+func TestAppendMemberID(t *testing.T) {
+	uri, err := url.Parse("http://ib.adnxs.com/openrtb2?query_param=true")
+	assert.NoError(t, err, "Failed to parse URI with query string")
+	uriWithMember := appendMemberId(*uri, "102")
+	expected := "http://ib.adnxs.com/openrtb2?member_id=102&query_param=true"
+	assert.Equal(t, expected, uriWithMember.String(), "Failed to append member id to URI with query string")
+}
+
+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
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/appnexustest/supplemental/multiple-member-ids.json b/adapters/appnexus/appnexustest/supplemental/multiple-member-ids.json
index ce56b01e..03a72fd0 100644
--- a/adapters/appnexus/appnexustest/supplemental/multiple-member-ids.json
+++ b/adapters/appnexus/appnexustest/supplemental/multiple-member-ids.json
@@ -58,69 +58,6 @@
       }
     ]
   },
-  "httpCalls": [
-    {
-      "expectedRequest": {
-        "uri": "http://ib.adnxs.com/openrtb2?member_id=103",
-        "body": {
-          "id": "test-request-id",
-          "ext": {
-            "appnexus": {
-              "hb_source": 5,
-              "brand_category_uniqueness": true,
-              "include_brand_category": true
-            },
-            "prebid": {
-              "targeting": {"pricegranularity":{"precision":2,"ranges":[{"min":0,"max":20,"increment":0.1}]},"includebidderkeys":true,"includewinners":true,"includebrandcategory":{"primaryadserver":1,"publisher":"","withcategory":true}}
-            }
-          },
-          "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": {
-                  "use_pmt_rule": true
-                }
-              }
-            },
-            {
-              "id":"test-imp-id-2",
-              "banner": {
-                "format": [
-                  {"w":300,"h":250},
-                  {"w":300,"h":600}
-                ],
-                "w": 300,
-                "h": 250,
-                "pos": 3
-              },
-              "tagid": "abc",
-              "bidfloor": 20,
-              "ext": {
-                "appnexus": {
-                  "use_pmt_rule": true
-                }
-              }
-            }
-          ]
-        }
-      },
-      "mockResponse": {
-        "status": 204
-      }
-    }
-  ],
   "expectedBidResponses": [],
   "expectedMakeRequestsErrors": [
     {
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/avocet/avocet_test.go b/adapters/avocet/avocet_test.go
index b8a38ac1..1dcf81c7 100644
--- a/adapters/avocet/avocet_test.go
+++ b/adapters/avocet/avocet_test.go
@@ -23,7 +23,7 @@ func TestJsonSamples(t *testing.T) {
 		t.Fatalf("Builder returned unexpected error %v", buildErr)
 	}
 
-	adapterstest.RunJSONBidderTest(t, "avocet", bidder)
+	adapterstest.RunJSONBidderTest(t, "avocettest", bidder)
 }
 
 func TestAvocetAdapter_MakeRequests(t *testing.T) {
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/adapters/flipp/flipp_test.go b/adapters/flipp/flipp_test.go
new file mode 100644
index 00000000..79b79e20
--- /dev/null
+++ b/adapters/flipp/flipp_test.go
@@ -0,0 +1,21 @@
+package flipp
+
+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.BidderFlipp, 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, "flipptest", bidder)
+}
diff --git a/adapters/flipp/flipptest/exemplary/simple-banner-dtx.json b/adapters/flipp/flipptest/exemplary/simple-banner-dtx.json
new file mode 100644
index 00000000..6bdad61d
--- /dev/null
+++ b/adapters/flipp/flipptest/exemplary/simple-banner-dtx.json
@@ -0,0 +1,173 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "test": 1,
+        "device": {
+            "ip": "123.123.123.123"
+        },
+        "site": {
+            "id": "1243066",
+            "page": "http://www.example.com/test?flipp-content-code=publisher-test"
+        },
+        "user": {
+            "id": "1234"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 1800
+                        }
+                    ]
+                },
+                "ext": {
+                        "bidder": {
+                                "publisherNameIdentifier": "wishabi-test-publisher",
+                                "creativeType": "DTX",
+                                "siteId": 1243066,
+                                "zoneIds": [285431],
+                                "options": {
+                                   "startCompact": true
+                                }
+                        }
+                }
+            }
+                
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "ip":"123.123.123.123",
+                    "keywords":[
+                       ""
+                    ],
+                    "placements":[
+                       {
+                          "adTypes":[
+                             5061
+                          ],
+                          "count":1,
+                          "divName":"inline",
+                          "prebid":{
+                             "creativeType":"DTX",
+                             "height":1800,
+                             "publisherNameIdentifier":"wishabi-test-publisher",
+                             "requestId":"test-imp-id",
+                             "width":300
+                          },
+                          "properties":{
+                             "contentCode":"publisher-test"
+                          },
+                          "siteId":1243066,
+                          "zoneIds":[
+                             285431
+                          ],
+                          "options": {
+                             "startCompact": true
+                          }
+                       }
+                    ],
+                    "url":"http://www.example.com/test?flipp-content-code=publisher-test",
+                    "user":{
+                       "key":"1234"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "decisions": {
+                        "inline": [
+                            {
+                                "adId": 183599115,
+                                "advertiserId": 1988027,
+                                "campaignId": 63285392,
+                                "clickUrl": "https://e-11090.adzerk.net/r?e=eyJ2IjoiMS4xMSIsImF2IjoxOTg4MDI3LCJhdCI6NDMwOSwiYnQiOjAsImNtIjo2MzI4NTM5MiwiY2giOjU4MDgxLCJjayI6e30sImNyIjo4MTMyNTY5MCwiZGkiOiJiOTg3MGNkYTA5MTU0NDlmOTkwZGNkZTNmNjYyNGNhMyIsImRqIjowLCJpaSI6IjJmNjYwMjMyODBmYjQ4NTRiYTY0YzFlYzA1ZDU5MTNiIiwiZG0iOjMsImZjIjoxODM1OTkxMTUsImZsIjoxNzU0MjE3OTUsImlwIjoiMTQyLjE4MS41OC41MiIsIm53IjoxMDkyMiwicGMiOjAsIm9wIjowLCJlYyI6MCwiZ20iOjAsImVwIjpudWxsLCJwciI6MjMyNjk5LCJydCI6MywicnMiOjUwMCwic2EiOiIzNCIsInNiIjoiaS0wNDZjMWNlNWRjYmExMTVjNSIsInNwIjozNzIzMDU1LCJzdCI6MTI0MzA2NiwidWsiOiJkOTU1N2Q2NS1kNWI5LTQyOTItYjg2My0xNGEyOTcyNTk3ZjQiLCJ6biI6Mjg1NDMxLCJ0cyI6MTY4MDU1NTc4MzkyMiwicG4iOiJpbmxpbmUiLCJnYyI6dHJ1ZSwiZ0MiOnRydWUsImdzIjoibm9uZSIsInR6IjoiQW1lcmljYS9OZXdfWW9yayIsInVyIjoiaHR0cDovL3d3dy5mbGlwcC5jb20ifQ&s=Mnss8P1kc37Eftik5RJvLJb4S9Y",
+                                "contents": [
+                                    {
+                                        "data": {
+                                            "customData": {
+                                                "campaignConfigUrl": "https://campaign-config.flipp.com/dist-campaign-admin/215",
+                                                "campaignNameIdentifier": "US Grocery Demo (Kroger)"
+                                            },
+                                            "height": 1800,
+                                            "width": 300
+                                        },
+                                        "type": "raw"
+                                    }
+                                ],
+                                "creativeId": 81325690,
+                                "flightId": 175421795,
+                                "height": 1800,
+                                "impressionUrl": "https://e-11090.adzerk.net/i.gif?e=eyJ2IjoiMS4xMSIsImF2IjoxOTg4MDI3LCJhdCI6NDMwOSwiYnQiOjAsImNtIjo2MzI4NTM5MiwiY2giOjU4MDgxLCJjayI6e30sImNyIjo4MTMyNTY5MCwiZGkiOiJiOTg3MGNkYTA5MTU0NDlmOTkwZGNkZTNmNjYyNGNhMyIsImRqIjowLCJpaSI6IjJmNjYwMjMyODBmYjQ4NTRiYTY0YzFlYzA1ZDU5MTNiIiwiZG0iOjMsImZjIjoxODM1OTkxMTUsImZsIjoxNzU0MjE3OTUsImlwIjoiMTQyLjE4MS41OC41MiIsIm53IjoxMDkyMiwicGMiOjAsIm9wIjowLCJlYyI6MCwiZ20iOjAsImVwIjpudWxsLCJwciI6MjMyNjk5LCJydCI6MywicnMiOjUwMCwic2EiOiIzNCIsInNiIjoiaS0wNDZjMWNlNWRjYmExMTVjNSIsInNwIjozNzIzMDU1LCJzdCI6MTI0MzA2NiwidWsiOiJkOTU1N2Q2NS1kNWI5LTQyOTItYjg2My0xNGEyOTcyNTk3ZjQiLCJ6biI6Mjg1NDMxLCJ0cyI6MTY4MDU1NTc4MzkyMywicG4iOiJpbmxpbmUiLCJnYyI6dHJ1ZSwiZ0MiOnRydWUsImdzIjoibm9uZSIsInR6IjoiQW1lcmljYS9OZXdfWW9yayIsImJhIjoxLCJmcSI6MH0&s=Qce4_IohtESeNA_sB71Qjb4TouY",
+                                "prebid": {
+                                    "cpm": 12.34,
+                                    "creative":"creativeContent",
+                                    "creativeType": "DTX",
+                                    "requestId": "test-imp-id"
+                                },
+                                "priorityId": 232699,
+                                "storefront": {
+                                    "campaignConfig": {
+                                        "fallbackImageUrl": "https://f.wishabi.net/arbitrary_files/115856/1666018811/115856_Featured Local Savings - Flipp Fallback - US (1).png",
+                                        "fallbackLinkUrl": "https://flipp.com/flyers/",
+                                        "tags": {
+                                            "advertiser": {
+                                                "engagement": "https://f.wishabi.net/creative/happyfruits/_itemlevelv2/apple.png?cb=[[random]]"
+                                            }
+                                        }
+                                    },
+                                    "flyer_id": 5554080,
+                                    "flyer_run_id": 873256,
+                                    "flyer_type_id": 2826,
+                                    "is_fallback": true,
+                                    "merchant": "Kroger",
+                                    "merchant_id": 2778,
+                                    "name": "Weekly Ad",
+                                    "storefront_logo_url": "https://images.wishabi.net/merchants/qX1/BGIzc9sFcA==/RackMultipart20210421-1-e3k5rx.jpeg",
+                                    "storefront_payload_url": "https://cdn-gateflipp.flippback.com/storefront-payload/v2/873256/5554080/ff14f675705934507c269b5750e124a323bc9bf60e8a6f210f422f4528b233ff?merchant_id=2778",
+                                    "valid_from": "2023-03-29 04:00:00 +0000 UTC",
+                                    "valid_to": "2023-04-05 03:59:59 +0000 UTC"
+                                },
+                                "width": 300
+                            }
+                        ]
+                    },
+                    "location": {
+                        "accuracy_radius": 5,
+                        "city": "Toronto",
+                        "country": "CA",
+                        "ip": "142.181.58.52",
+                        "postal_code": "M4S",
+                        "region": "ON",
+                        "region_name": "Ontario"
+                    }
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "183599115",
+                        "impid": "test-imp-id",
+                        "price": 12.34,
+                        "adm": "creativeContent",
+                        "crid": "81325690",
+                        "w": 300
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/flipp/flipptest/exemplary/simple-banner-native-param-user.json b/adapters/flipp/flipptest/exemplary/simple-banner-native-param-user.json
new file mode 100644
index 00000000..272bcd74
--- /dev/null
+++ b/adapters/flipp/flipptest/exemplary/simple-banner-native-param-user.json
@@ -0,0 +1,174 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "test": 1,
+        "device": {
+            "ip": "123.123.123.123"
+        },
+        "site": {
+            "id": "1243066",
+            "page": "http://www.example.com/test?flipp-content-code=publisher-test"
+        },
+        "user": {
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 1800
+                        }
+                    ]
+                },
+                "ext": {
+                        "bidder": {
+                                "publisherNameIdentifier": "wishabi-test-publisher",
+                                "creativeType": "NativeX",
+                                "siteId": 1243066,
+                                "zoneIds": [285431],
+                                "options": {
+                                   "startCompact": true
+                                },
+                                "userKey": "abc123"
+                        }
+                }
+            }
+                
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "ip":"123.123.123.123",
+                    "keywords":[
+                       ""
+                    ],
+                    "placements":[
+                       {
+                          "adTypes":[
+                             4309,
+                             641
+                          ],
+                          "count":1,
+                          "divName":"inline",
+                          "prebid":{
+                             "creativeType":"NativeX",
+                             "height":1800,
+                             "publisherNameIdentifier":"wishabi-test-publisher",
+                             "requestId":"test-imp-id",
+                             "width":300
+                          },
+                          "properties":{
+                             "contentCode":"publisher-test"
+                          },
+                          "siteId":1243066,
+                          "zoneIds":[
+                             285431
+                          ],
+                          "options": {
+                             "startCompact": true
+                          }
+                       }
+                    ],
+                    "url":"http://www.example.com/test?flipp-content-code=publisher-test",
+                    "user":{
+                       "key":"abc123"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "decisions": {
+                        "inline": [
+                            {
+                                "adId": 183599115,
+                                "advertiserId": 1988027,
+                                "campaignId": 63285392,
+                                "clickUrl": "https://e-11090.adzerk.net/r?e=eyJ2IjoiMS4xMSIsImF2IjoxOTg4MDI3LCJhdCI6NDMwOSwiYnQiOjAsImNtIjo2MzI4NTM5MiwiY2giOjU4MDgxLCJjayI6e30sImNyIjo4MTMyNTY5MCwiZGkiOiJiOTg3MGNkYTA5MTU0NDlmOTkwZGNkZTNmNjYyNGNhMyIsImRqIjowLCJpaSI6IjJmNjYwMjMyODBmYjQ4NTRiYTY0YzFlYzA1ZDU5MTNiIiwiZG0iOjMsImZjIjoxODM1OTkxMTUsImZsIjoxNzU0MjE3OTUsImlwIjoiMTQyLjE4MS41OC41MiIsIm53IjoxMDkyMiwicGMiOjAsIm9wIjowLCJlYyI6MCwiZ20iOjAsImVwIjpudWxsLCJwciI6MjMyNjk5LCJydCI6MywicnMiOjUwMCwic2EiOiIzNCIsInNiIjoiaS0wNDZjMWNlNWRjYmExMTVjNSIsInNwIjozNzIzMDU1LCJzdCI6MTI0MzA2NiwidWsiOiJkOTU1N2Q2NS1kNWI5LTQyOTItYjg2My0xNGEyOTcyNTk3ZjQiLCJ6biI6Mjg1NDMxLCJ0cyI6MTY4MDU1NTc4MzkyMiwicG4iOiJpbmxpbmUiLCJnYyI6dHJ1ZSwiZ0MiOnRydWUsImdzIjoibm9uZSIsInR6IjoiQW1lcmljYS9OZXdfWW9yayIsInVyIjoiaHR0cDovL3d3dy5mbGlwcC5jb20ifQ&s=Mnss8P1kc37Eftik5RJvLJb4S9Y",
+                                "contents": [
+                                    {
+                                        "data": {
+                                            "customData": {
+                                                "campaignConfigUrl": "https://campaign-config.flipp.com/dist-campaign-admin/215",
+                                                "campaignNameIdentifier": "US Grocery Demo (Kroger)"
+                                            },
+                                            "height": 1800,
+                                            "width": 300
+                                        },
+                                        "type": "raw"
+                                    }
+                                ],
+                                "creativeId": 81325690,
+                                "flightId": 175421795,
+                                "height": 1800,
+                                "impressionUrl": "https://e-11090.adzerk.net/i.gif?e=eyJ2IjoiMS4xMSIsImF2IjoxOTg4MDI3LCJhdCI6NDMwOSwiYnQiOjAsImNtIjo2MzI4NTM5MiwiY2giOjU4MDgxLCJjayI6e30sImNyIjo4MTMyNTY5MCwiZGkiOiJiOTg3MGNkYTA5MTU0NDlmOTkwZGNkZTNmNjYyNGNhMyIsImRqIjowLCJpaSI6IjJmNjYwMjMyODBmYjQ4NTRiYTY0YzFlYzA1ZDU5MTNiIiwiZG0iOjMsImZjIjoxODM1OTkxMTUsImZsIjoxNzU0MjE3OTUsImlwIjoiMTQyLjE4MS41OC41MiIsIm53IjoxMDkyMiwicGMiOjAsIm9wIjowLCJlYyI6MCwiZ20iOjAsImVwIjpudWxsLCJwciI6MjMyNjk5LCJydCI6MywicnMiOjUwMCwic2EiOiIzNCIsInNiIjoiaS0wNDZjMWNlNWRjYmExMTVjNSIsInNwIjozNzIzMDU1LCJzdCI6MTI0MzA2NiwidWsiOiJkOTU1N2Q2NS1kNWI5LTQyOTItYjg2My0xNGEyOTcyNTk3ZjQiLCJ6biI6Mjg1NDMxLCJ0cyI6MTY4MDU1NTc4MzkyMywicG4iOiJpbmxpbmUiLCJnYyI6dHJ1ZSwiZ0MiOnRydWUsImdzIjoibm9uZSIsInR6IjoiQW1lcmljYS9OZXdfWW9yayIsImJhIjoxLCJmcSI6MH0&s=Qce4_IohtESeNA_sB71Qjb4TouY",
+                                "prebid": {
+                                    "cpm": 12.34,
+                                    "creative":"creativeContent",
+                                    "creativeType": "NativeX",
+                                    "requestId": "test-imp-id"
+                                },
+                                "priorityId": 232699,
+                                "storefront": {
+                                    "campaignConfig": {
+                                        "fallbackImageUrl": "https://f.wishabi.net/arbitrary_files/115856/1666018811/115856_Featured Local Savings - Flipp Fallback - US (1).png",
+                                        "fallbackLinkUrl": "https://flipp.com/flyers/",
+                                        "tags": {
+                                            "advertiser": {
+                                                "engagement": "https://f.wishabi.net/creative/happyfruits/_itemlevelv2/apple.png?cb=[[random]]"
+                                            }
+                                        }
+                                    },
+                                    "flyer_id": 5554080,
+                                    "flyer_run_id": 873256,
+                                    "flyer_type_id": 2826,
+                                    "is_fallback": true,
+                                    "merchant": "Kroger",
+                                    "merchant_id": 2778,
+                                    "name": "Weekly Ad",
+                                    "storefront_logo_url": "https://images.wishabi.net/merchants/qX1/BGIzc9sFcA==/RackMultipart20210421-1-e3k5rx.jpeg",
+                                    "storefront_payload_url": "https://cdn-gateflipp.flippback.com/storefront-payload/v2/873256/5554080/ff14f675705934507c269b5750e124a323bc9bf60e8a6f210f422f4528b233ff?merchant_id=2778",
+                                    "valid_from": "2023-03-29 04:00:00 +0000 UTC",
+                                    "valid_to": "2023-04-05 03:59:59 +0000 UTC"
+                                },
+                                "width": 300
+                            }
+                        ]
+                    },
+                    "location": {
+                        "accuracy_radius": 5,
+                        "city": "Toronto",
+                        "country": "CA",
+                        "ip": "123.123.123.123",
+                        "postal_code": "M4S",
+                        "region": "ON",
+                        "region_name": "Ontario"
+                    }
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "183599115",
+                        "impid": "test-imp-id",
+                        "price": 12.34,
+                        "adm": "creativeContent",
+                        "crid": "81325690",
+                        "w": 300
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/flipp/flipptest/exemplary/simple-banner-native.json b/adapters/flipp/flipptest/exemplary/simple-banner-native.json
new file mode 100644
index 00000000..c242c323
--- /dev/null
+++ b/adapters/flipp/flipptest/exemplary/simple-banner-native.json
@@ -0,0 +1,177 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "test": 1,
+        "device": {
+            "ip": "123.123.123.123"
+        },
+        "site": {
+            "id": "1243066",
+            "page": "http://www.example.com/test?flipp-content-code=publisher-test"
+        },
+        "user": {
+            "id": "1234"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 1800
+                        }
+                    ]
+                },
+                "ext": {
+                        "bidder": {
+                                "publisherNameIdentifier": "wishabi-test-publisher",
+                                "creativeType": "NativeX",
+                                "siteId": 1243066,
+                                "zoneIds": [285431],
+                                "ip": "123.123.123.124",
+                                "options": {
+                                   "startCompact": true,
+                                   "contentCode": "publisher-test-2"
+                                }
+                        }
+                }
+            }
+                
+        ]
+    },
+    "httpCalls": [
+        {
+            "expectedRequest": {
+                "uri": "http://example.com/pserver",
+                "body": {
+                    "ip":"123.123.123.124",
+                    "keywords":[
+                       ""
+                    ],
+                    "placements":[
+                       {
+                          "adTypes":[
+                             4309,
+                             641
+                          ],
+                          "count":1,
+                          "divName":"inline",
+                          "prebid":{
+                             "creativeType":"NativeX",
+                             "height":1800,
+                             "publisherNameIdentifier":"wishabi-test-publisher",
+                             "requestId":"test-imp-id",
+                             "width":300
+                          },
+                          "properties":{
+                             "contentCode":"publisher-test-2"
+                          },
+                          "siteId":1243066,
+                          "zoneIds":[
+                             285431
+                          ],
+                          "options": {
+                             "startCompact": true,
+                             "contentCode": "publisher-test-2"
+                          }
+                       }
+                    ],
+                    "url":"http://www.example.com/test?flipp-content-code=publisher-test",
+                    "user":{
+                       "key":"1234"
+                    }
+                }
+            },
+            "mockResponse": {
+                "status": 200,
+                "body": {
+                    "decisions": {
+                        "inline": [
+                            {
+                                "adId": 183599115,
+                                "advertiserId": 1988027,
+                                "campaignId": 63285392,
+                                "clickUrl": "https://e-11090.adzerk.net/r?e=eyJ2IjoiMS4xMSIsImF2IjoxOTg4MDI3LCJhdCI6NDMwOSwiYnQiOjAsImNtIjo2MzI4NTM5MiwiY2giOjU4MDgxLCJjayI6e30sImNyIjo4MTMyNTY5MCwiZGkiOiJiOTg3MGNkYTA5MTU0NDlmOTkwZGNkZTNmNjYyNGNhMyIsImRqIjowLCJpaSI6IjJmNjYwMjMyODBmYjQ4NTRiYTY0YzFlYzA1ZDU5MTNiIiwiZG0iOjMsImZjIjoxODM1OTkxMTUsImZsIjoxNzU0MjE3OTUsImlwIjoiMTQyLjE4MS41OC41MiIsIm53IjoxMDkyMiwicGMiOjAsIm9wIjowLCJlYyI6MCwiZ20iOjAsImVwIjpudWxsLCJwciI6MjMyNjk5LCJydCI6MywicnMiOjUwMCwic2EiOiIzNCIsInNiIjoiaS0wNDZjMWNlNWRjYmExMTVjNSIsInNwIjozNzIzMDU1LCJzdCI6MTI0MzA2NiwidWsiOiJkOTU1N2Q2NS1kNWI5LTQyOTItYjg2My0xNGEyOTcyNTk3ZjQiLCJ6biI6Mjg1NDMxLCJ0cyI6MTY4MDU1NTc4MzkyMiwicG4iOiJpbmxpbmUiLCJnYyI6dHJ1ZSwiZ0MiOnRydWUsImdzIjoibm9uZSIsInR6IjoiQW1lcmljYS9OZXdfWW9yayIsInVyIjoiaHR0cDovL3d3dy5mbGlwcC5jb20ifQ&s=Mnss8P1kc37Eftik5RJvLJb4S9Y",
+                                "contents": [
+                                    {
+                                        "data": {
+                                            "customData": {
+                                                "campaignConfigUrl": "https://campaign-config.flipp.com/dist-campaign-admin/215",
+                                                "campaignNameIdentifier": "US Grocery Demo (Kroger)"
+                                            },
+                                            "height": 1800,
+                                            "width": 300
+                                        },
+                                        "type": "raw"
+                                    }
+                                ],
+                                "creativeId": 81325690,
+                                "flightId": 175421795,
+                                "height": 1800,
+                                "impressionUrl": "https://e-11090.adzerk.net/i.gif?e=eyJ2IjoiMS4xMSIsImF2IjoxOTg4MDI3LCJhdCI6NDMwOSwiYnQiOjAsImNtIjo2MzI4NTM5MiwiY2giOjU4MDgxLCJjayI6e30sImNyIjo4MTMyNTY5MCwiZGkiOiJiOTg3MGNkYTA5MTU0NDlmOTkwZGNkZTNmNjYyNGNhMyIsImRqIjowLCJpaSI6IjJmNjYwMjMyODBmYjQ4NTRiYTY0YzFlYzA1ZDU5MTNiIiwiZG0iOjMsImZjIjoxODM1OTkxMTUsImZsIjoxNzU0MjE3OTUsImlwIjoiMTQyLjE4MS41OC41MiIsIm53IjoxMDkyMiwicGMiOjAsIm9wIjowLCJlYyI6MCwiZ20iOjAsImVwIjpudWxsLCJwciI6MjMyNjk5LCJydCI6MywicnMiOjUwMCwic2EiOiIzNCIsInNiIjoiaS0wNDZjMWNlNWRjYmExMTVjNSIsInNwIjozNzIzMDU1LCJzdCI6MTI0MzA2NiwidWsiOiJkOTU1N2Q2NS1kNWI5LTQyOTItYjg2My0xNGEyOTcyNTk3ZjQiLCJ6biI6Mjg1NDMxLCJ0cyI6MTY4MDU1NTc4MzkyMywicG4iOiJpbmxpbmUiLCJnYyI6dHJ1ZSwiZ0MiOnRydWUsImdzIjoibm9uZSIsInR6IjoiQW1lcmljYS9OZXdfWW9yayIsImJhIjoxLCJmcSI6MH0&s=Qce4_IohtESeNA_sB71Qjb4TouY",
+                                "prebid": {
+                                    "cpm": 12.34,
+                                    "creative":"creativeContent",
+                                    "creativeType": "NativeX",
+                                    "requestId": "test-imp-id"
+                                },
+                                "priorityId": 232699,
+                                "storefront": {
+                                    "campaignConfig": {
+                                        "fallbackImageUrl": "https://f.wishabi.net/arbitrary_files/115856/1666018811/115856_Featured Local Savings - Flipp Fallback - US (1).png",
+                                        "fallbackLinkUrl": "https://flipp.com/flyers/",
+                                        "tags": {
+                                            "advertiser": {
+                                                "engagement": "https://f.wishabi.net/creative/happyfruits/_itemlevelv2/apple.png?cb=[[random]]"
+                                            }
+                                        }
+                                    },
+                                    "flyer_id": 5554080,
+                                    "flyer_run_id": 873256,
+                                    "flyer_type_id": 2826,
+                                    "is_fallback": true,
+                                    "merchant": "Kroger",
+                                    "merchant_id": 2778,
+                                    "name": "Weekly Ad",
+                                    "storefront_logo_url": "https://images.wishabi.net/merchants/qX1/BGIzc9sFcA==/RackMultipart20210421-1-e3k5rx.jpeg",
+                                    "storefront_payload_url": "https://cdn-gateflipp.flippback.com/storefront-payload/v2/873256/5554080/ff14f675705934507c269b5750e124a323bc9bf60e8a6f210f422f4528b233ff?merchant_id=2778",
+                                    "valid_from": "2023-03-29 04:00:00 +0000 UTC",
+                                    "valid_to": "2023-04-05 03:59:59 +0000 UTC"
+                                },
+                                "width": 300
+                            }
+                        ]
+                    },
+                    "location": {
+                        "accuracy_radius": 5,
+                        "city": "Toronto",
+                        "country": "CA",
+                        "ip": "123.123.123.124",
+                        "postal_code": "M4S",
+                        "region": "ON",
+                        "region_name": "Ontario"
+                    }
+                }
+            }
+        }
+    ],
+    "expectedBidResponses": [
+        {
+            "bids": [
+                {
+                    "bid": {
+                        "id": "183599115",
+                        "impid": "test-imp-id",
+                        "price": 12.34,
+                        "adm": "creativeContent",
+                        "crid": "81325690",
+                        "w": 300
+                    },
+                    "type": "banner"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/flipp/flipptest/supplemental/bad-response.json b/adapters/flipp/flipptest/supplemental/bad-response.json
new file mode 100644
index 00000000..764932ec
--- /dev/null
+++ b/adapters/flipp/flipptest/supplemental/bad-response.json
@@ -0,0 +1,88 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "test": 1,
+        "device": {
+            "ip": "123.123.123.123"
+        },
+        "site": {
+            "id": "1243066",
+            "page": "http://www.example.com/test?flipp-content-code=publisher-test"
+        },
+        "user": {
+            "id": "1234"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 1800
+                        }
+                    ]
+                },
+                "ext": {
+                        "bidder": {
+                                "publisherNameIdentifier": "wishabi-test-publisher",
+                                "creativeType": "NativeX",
+                                "siteId": 1243066,
+                                "zoneIds": [285431]
+                        }
+                    }
+                }
+        ]
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+            "uri": "http://example.com/pserver",
+            "body": {
+                "ip":"123.123.123.123",
+                "keywords":[
+                   ""
+                ],
+                "placements":[
+                   {
+                      "adTypes":[
+                         4309,
+                         641
+                      ],
+                      "count":1,
+                      "divName":"inline",
+                      "prebid":{
+                         "creativeType":"NativeX",
+                         "height":1800,
+                         "publisherNameIdentifier":"wishabi-test-publisher",
+                         "requestId":"test-imp-id",
+                         "width":300
+                      },
+                      "properties":{
+                         "contentCode":"publisher-test"
+                      },
+                      "options":{},
+                      "siteId":1243066,
+                      "zoneIds":[
+                         285431
+                      ]
+                   }
+                ],
+                "url":"http://www.example.com/test?flipp-content-code=publisher-test",
+                "user":{
+                   "key":"1234"
+                }
+            }
+        },
+		"mockResponse": {
+			"status": 200,
+			"body": ""
+		}
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "json: cannot unmarshal string into Go value of type flipp.CampaignResponseBody",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/flipp/flipptest/supplemental/flipp-params-not-found.json b/adapters/flipp/flipptest/supplemental/flipp-params-not-found.json
new file mode 100644
index 00000000..a559e8d5
--- /dev/null
+++ b/adapters/flipp/flipptest/supplemental/flipp-params-not-found.json
@@ -0,0 +1,44 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "test": 1,
+        "device": {
+            "ip": "123.123.123.123"
+        },
+        "site": {
+            "id": "1243066",
+            "page": "http://www.example.com/test?flipp-content-code=publisher-test"
+        },
+        "user": {
+            "id": "1234"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 1800
+                        }
+                    ]
+                },
+                "ext": {
+
+                }
+            }
+                
+        ]
+    },
+    "expectedMakeRequestsErrors": [
+        {
+            "value": "flipp params not found. Key path not found",
+            "comparison": "literal"
+        },
+        {
+            "value": "adapterRequest is empty",
+            "comparison": "literal"
+        }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/flipp/flipptest/supplemental/no-ip-error.json b/adapters/flipp/flipptest/supplemental/no-ip-error.json
new file mode 100644
index 00000000..37120161
--- /dev/null
+++ b/adapters/flipp/flipptest/supplemental/no-ip-error.json
@@ -0,0 +1,50 @@
+{
+    "mockBidRequest": {
+        "id": "test-request-id",
+        "test": 1,
+
+        "site": {
+            "id": "1243066",
+            "page": "http://www.example.com/test?flipp-content-code=publisher-test"
+        },
+        "user": {
+            "id": "1234"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 1800
+                        }
+                    ]
+                },
+                "ext": {
+                        "bidder": {
+                                "publisherNameIdentifier": "wishabi-test-publisher",
+                                "creativeType": "NativeX",
+                                "siteId": 1243066,
+                                "zoneIds": [285431],
+                                "options": {
+                                   "startCompact": true
+                                }
+                        }
+                }
+            }
+                
+        ]
+    },
+    "expectedMakeRequestsErrors": [
+        {
+          "value": "no IP set in flipp bidder params or request device",
+          "comparison": "literal"
+        },
+        {
+            "value": "adapterRequest is empty",
+            "comparison": "literal"
+          }
+    ]
+}
\ No newline at end of file
diff --git a/adapters/flipp/flipptest/supplemental/status-not-200.json b/adapters/flipp/flipptest/supplemental/status-not-200.json
new file mode 100644
index 00000000..5039a905
--- /dev/null
+++ b/adapters/flipp/flipptest/supplemental/status-not-200.json
@@ -0,0 +1,88 @@
+{
+	"mockBidRequest": {
+        "id": "test-request-id",
+        "test": 1,
+        "device": {
+            "ip": "123.123.123.123"
+        },
+        "site": {
+            "id": "1243066",
+            "page": "http://www.example.com/test?flipp-content-code=publisher-test"
+        },
+        "user": {
+            "id": "1234"
+        },
+        "imp": [
+            {
+                "id": "test-imp-id",
+                "tagid": "test",
+                "banner": {
+                    "format": [
+                        {
+                            "w": 300,
+                            "h": 1800
+                        }
+                    ]
+                },
+                "ext": {
+                        "bidder": {
+                                "publisherNameIdentifier": "wishabi-test-publisher",
+                                "creativeType": "NativeX",
+                                "siteId": 1243066,
+                                "zoneIds": [285431]
+                        }
+                    }
+                }
+        ]
+    },
+	"httpCalls": [{
+		"expectedRequest": {
+            "uri": "http://example.com/pserver",
+            "body": {
+                "ip":"123.123.123.123",
+                "keywords":[
+                   ""
+                ],
+                "placements":[
+                   {
+                      "adTypes":[
+                         4309,
+                         641
+                      ],
+                      "count":1,
+                      "divName":"inline",
+                      "prebid":{
+                         "creativeType":"NativeX",
+                         "height":1800,
+                         "publisherNameIdentifier":"wishabi-test-publisher",
+                         "requestId":"test-imp-id",
+                         "width":300
+                      },
+                      "properties":{
+                         "contentCode":"publisher-test"
+                      },
+                      "siteId":1243066,
+                      "zoneIds":[
+                         285431
+                      ],
+                      "options": {}
+                   }
+                ],
+                "url":"http://www.example.com/test?flipp-content-code=publisher-test",
+                "user":{
+                   "key":"1234"
+                }
+            }
+        },
+		"mockResponse": {
+			"status": 404,
+			"body": {}
+		}
+    }],
+    "expectedMakeBidsErrors": [
+        {
+          "value": "Unexpected status code: 404. Run with request.debug = 1 for more info",
+          "comparison": "literal"
+        }
+    ]
+}
diff --git a/adapters/flipp/params_test.go b/adapters/flipp/params_test.go
new file mode 100644
index 00000000..56f2fadb
--- /dev/null
+++ b/adapters/flipp/params_test.go
@@ -0,0 +1,127 @@
+package flipp
+
+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.BidderFlipp, 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.BidderFlipp, json.RawMessage(p)); err == nil {
+			t.Errorf("Schema allowed invalid params: %s", p)
+		}
+	}
+}
+
+var validParams = []string{
+	`{
+		"publisherNameIdentifier": "wishabi-test-publisher",
+		"creativeType": "NativeX",
+		"siteId": 1243066,
+		"zoneIds": [285431]
+	}`,
+	`{
+		"publisherNameIdentifier": "wishabi-test-publisher",
+		"creativeType": "NativeX",
+		"siteId": 1243066,
+		"options": {}
+	}`,
+	`{
+		"publisherNameIdentifier": "wishabi-test-publisher",
+		"creativeType": "NativeX",
+		"siteId": 1243066,
+		"zoneIds": [285431],
+		"options": {
+			"startCompact": true
+		}
+	}`,
+	`{
+		"publisherNameIdentifier": "wishabi-test-publisher",
+		"creativeType": "NativeX",
+		"siteId": 1243066,
+		"zoneIds": [285431],
+		"options": {
+			"startCompact": false,
+			"dwellExpand": true,
+			"contentCode": "test-code"
+		}
+	}`,
+	`{
+		"publisherNameIdentifier": "wishabi-test-publisher",
+		"creativeType": "NativeX",
+		"siteId": 1243066,
+		"zoneIds": [285431],
+		"ip": "123.123.123.123",
+		"options": {
+			"startCompact": false
+		}
+	}`,
+}
+
+var invalidParams = []string{
+	`{
+		"publisherNameIdentifier": "wishabi-test-publisher",
+		"creativeType": "NativeX",
+		"zoneIds": [285431]
+	}`,
+	`{
+		"publisherNameIdentifier": "wishabi-test-publisher",
+		"siteId": 1243066,
+		"zoneIds": [285431]
+	}`,
+	`{
+		"publisherNameIdentifier": "wishabi-test-publisher",
+		"creativeType": "abc",
+		"siteId": 1243066,
+		"zoneIds": [285431]
+	}`,
+	`{
+		"creativeType": "NativeX",
+		"siteId": 1243066,
+		"zoneIds": [285431]
+	}`,
+	`{
+		"publisherNameIdentifier": "wishabi-test-publisher",
+		"creativeType": "NativeX",
+		"siteId": "123abc",
+		"zoneIds": [285431]
+	}`,
+	`{
+		"publisherNameIdentifier": "wishabi-test-publisher",
+		"creativeType": "NativeX",
+		"siteId": 1243066,
+		"zoneIds": ["abc123"]
+	}`,
+	`{
+		"publisherNameIdentifier": "wishabi-test-publisher",
+		"siteId": 1243066,
+		"startCompact": "true"
+	}`,
+	`{
+		"publisherNameIdentifier": "wishabi-test-publisher",
+		"siteId": 1243066,
+		"options": {
+			"startCompact": "true"
+		}
+	}`,
+}
diff --git a/adapters/huaweiads/huaweiadstest/exemplary/banner2.json b/adapters/huaweiads/huaweiadstest/exemplary/banner2.json
index 4f1cc6b4..92afb558 100644
--- a/adapters/huaweiads/huaweiadstest/exemplary/banner2.json
+++ b/adapters/huaweiads/huaweiadstest/exemplary/banner2.json
@@ -51,7 +51,7 @@
       "carrier": "carrier",
       "connectiontype": 1,
       "geo": {
-        "country": "ZAF"
+        "country": "ZA"
       }
     },
     "app": {
@@ -269,4 +269,4 @@
       ]
     }
   ]
-}
\ No newline at end of file
+}
diff --git a/adapters/richaudience/richaudience_test.go b/adapters/richaudience/richaudience_test.go
index c9611fbc..30d04775 100644
--- a/adapters/richaudience/richaudience_test.go
+++ b/adapters/richaudience/richaudience_test.go
@@ -64,7 +64,7 @@ func TestGetBuilder(t *testing.T) {
 		t.Errorf("error %s", buildErr)
 	}
 
-	adapterstest.RunJSONBidderTest(t, "richaudience", bidder)
+	adapterstest.RunJSONBidderTest(t, "richaudiencetest", bidder)
 }
 
 func TestGetSite(t *testing.T) {
diff --git a/config/config_test.go b/config/config_test.go
index fbd96d3f..64f4f8a0 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -196,6 +196,11 @@ func TestDefaults(t *testing.T) {
 	cmpInts(t, "validations.max_creative_height", 0, int(cfg.Validations.MaxCreativeHeight))
 	cmpBools(t, "account_modules_metrics", false, cfg.Metrics.Disabled.AccountModulesMetrics)
 
+	cmpBools(t, "tmax_adjustments.enabled", false, cfg.TmaxAdjustments.Enabled)
+	cmpUnsignedInts(t, "tmax_adjustments.bidder_response_duration_min_ms", 0, cfg.TmaxAdjustments.BidderResponseDurationMin)
+	cmpUnsignedInts(t, "tmax_adjustments.bidder_network_latency_buffer_ms", 0, cfg.TmaxAdjustments.BidderNetworkLatencyBuffer)
+	cmpUnsignedInts(t, "tmax_adjustments.pbs_response_preparation_duration_ms", 0, cfg.TmaxAdjustments.PBSResponsePreparationDuration)
+
 	//Assert purpose VendorExceptionMap hash tables were built correctly
 	expectedTCF2 := TCF2{
 		Enabled: true,
@@ -461,6 +466,11 @@ account_defaults:
         use_dynamic_data: true
         max_rules: 120
         max_schema_dims: 5
+tmax_adjustments:
+  enabled: true
+  bidder_response_duration_min_ms: 700
+  bidder_network_latency_buffer_ms: 100
+  pbs_response_preparation_duration_ms: 100
 `)
 
 var oldStoredRequestsConfig = []byte(`
@@ -479,6 +489,11 @@ func cmpInts(t *testing.T, key string, expected, actual int) {
 	assert.Equal(t, expected, actual, "%s: %d != %d", key, expected, actual)
 }
 
+func cmpUnsignedInts(t *testing.T, key string, expected, actual uint) {
+	t.Helper()
+	assert.Equal(t, expected, actual, "%s: %d != %d", key, expected, actual)
+}
+
 func cmpInt8s(t *testing.T, key string, expected, actual *int8) {
 	t.Helper()
 	assert.Equal(t, expected, actual, "%s: %d != %d", key, expected, actual)
@@ -540,6 +555,10 @@ func TestFullConfig(t *testing.T) {
 	cmpStrings(t, "validations.secure_markup", "skip", cfg.Validations.SecureMarkup)
 	cmpInts(t, "validations.max_creative_width", 0, int(cfg.Validations.MaxCreativeWidth))
 	cmpInts(t, "validations.max_creative_height", 0, int(cfg.Validations.MaxCreativeHeight))
+	cmpBools(t, "tmax_adjustments.enabled", false, cfg.TmaxAdjustments.Enabled) // Tmax adjustment feature is still under development. Therefore enabled flag is set to false
+	cmpUnsignedInts(t, "tmax_adjustments.bidder_response_duration_min_ms", 700, cfg.TmaxAdjustments.BidderResponseDurationMin)
+	cmpUnsignedInts(t, "tmax_adjustments.bidder_network_latency_buffer_ms", 100, cfg.TmaxAdjustments.BidderNetworkLatencyBuffer)
+	cmpUnsignedInts(t, "tmax_adjustments.pbs_response_preparation_duration_ms", 100, cfg.TmaxAdjustments.PBSResponsePreparationDuration)
 
 	//Assert the price floor values
 	cmpBools(t, "price_floors.enabled", true, cfg.PriceFloors.Enabled)
diff --git a/endpoints/openrtb2/auction_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go
index 15d70efa..2866a31e 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{},
@@ -142,7 +137,7 @@ func BenchmarkValidWholeExemplary(b *testing.B) {
 			if err != nil {
 				b.Fatalf("unable to read file %s", testFile)
 			}
-			test, err := parseTestFile(fileData, testFile)
+			test, err := parseTestData(fileData, testFile)
 			if err != nil {
 				b.Fatal(err.Error())
 			}
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index 053bcc10..544a46d0 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -6,6 +6,7 @@ import (
 	"encoding/json"
 	"errors"
 	"io"
+	"io/fs"
 	"net"
 	"net/http"
 	"net/http/httptest"
@@ -41,6 +42,8 @@ import (
 	"github.com/prebid/prebid-server/util/ptrutil"
 )
 
+const jsonFileExtension string = ".json"
+
 func TestJsonSampleRequests(t *testing.T) {
 	testSuites := []struct {
 		description          string
@@ -121,46 +124,64 @@ func TestJsonSampleRequests(t *testing.T) {
 	}
 
 	for _, tc := range testSuites {
-		testCaseFiles, err := getTestFiles(filepath.Join("sample-requests", tc.sampleRequestsSubDir))
-		if assert.NoError(t, err, "Test case %s. Error reading files from directory %s \n", tc.description, tc.sampleRequestsSubDir) {
-			for _, testFile := range testCaseFiles {
-				fileData, err := os.ReadFile(testFile)
-				if assert.NoError(t, err, "Test case %s. Error reading file %s \n", tc.description, testFile) {
-					// Retrieve test case input and expected output from JSON file
-					test, err := parseTestFile(fileData, testFile)
-					if !assert.NoError(t, err) {
-						continue
-					}
+		err := filepath.WalkDir(filepath.Join("sample-requests", tc.sampleRequestsSubDir), func(path string, info fs.DirEntry, err error) error {
+			// According to documentation, needed to avoid panics
+			if err != nil {
+				return err
+			}
 
-					// Build endpoint for testing. If no error, run test case
-					cfg := &config.Configuration{MaxRequestSize: maxSize}
-					if test.Config != nil {
-						cfg.BlacklistedApps = test.Config.BlacklistedApps
-						cfg.BlacklistedAppMap = test.Config.getBlacklistedAppMap()
-						cfg.BlacklistedAccts = test.Config.BlacklistedAccounts
-						cfg.BlacklistedAcctMap = test.Config.getBlackListedAccountMap()
-						cfg.AccountRequired = test.Config.AccountRequired
-					}
-					cfg.MarshalAccountDefaults()
-					test.endpointType = OPENRTB_ENDPOINT
+			// Test suite will traverse the directory tree recursively and will only consider files with `json` extension
+			if !info.IsDir() && filepath.Ext(info.Name()) == jsonFileExtension {
+				t.Run(tc.description, func(t *testing.T) {
+					runJsonBasedTest(t, path, tc.description)
+				})
+			}
 
-					auctionEndpointHandler, _, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, cfg)
-					if assert.NoError(t, err) {
-						assert.NotPanics(t, func() { runTestCase(t, auctionEndpointHandler, test, fileData, testFile) }, testFile)
-					}
+			return nil
+		})
+		assert.NoError(t, err, "Test case %s. Error reading files from directory %s \n", tc.description, tc.sampleRequestsSubDir)
+	}
+}
 
-					// Close servers regardless if the test case was run or not
-					for _, mockBidServer := range mockBidServers {
-						mockBidServer.Close()
-					}
-					mockCurrencyRatesServer.Close()
-				}
-			}
-		}
+func runJsonBasedTest(t *testing.T, filename, desc string) {
+	t.Helper()
+
+	fileData, err := os.ReadFile(filename)
+	if !assert.NoError(t, err, "Test case %s. Error reading file %s \n", desc, filename) {
+		return
+	}
+
+	// Retrieve test case input and expected output from JSON file
+	test, err := parseTestData(fileData, filename)
+	if !assert.NoError(t, err) {
+		return
+	}
+
+	// Build endpoint for testing. If no error, run test case
+	cfg := &config.Configuration{MaxRequestSize: maxSize}
+	if test.Config != nil {
+		cfg.BlacklistedApps = test.Config.BlacklistedApps
+		cfg.BlacklistedAppMap = test.Config.getBlacklistedAppMap()
+		cfg.BlacklistedAccts = test.Config.BlacklistedAccounts
+		cfg.BlacklistedAcctMap = test.Config.getBlackListedAccountMap()
+		cfg.AccountRequired = test.Config.AccountRequired
+	}
+	cfg.MarshalAccountDefaults()
+	test.endpointType = OPENRTB_ENDPOINT
+
+	auctionEndpointHandler, _, mockBidServers, mockCurrencyRatesServer, err := buildTestEndpoint(test, cfg)
+	if assert.NoError(t, err) {
+		assert.NotPanics(t, func() { runEndToEndTest(t, auctionEndpointHandler, test, fileData, filename) }, filename)
+	}
+
+	// Close servers regardless if the test case was run or not
+	for _, mockBidServer := range mockBidServers {
+		mockBidServer.Close()
 	}
+	mockCurrencyRatesServer.Close()
 }
 
-func runTestCase(t *testing.T, auctionEndpointHandler httprouter.Handle, test testCase, fileData []byte, testFile string) {
+func runEndToEndTest(t *testing.T, auctionEndpointHandler httprouter.Handle, test testCase, fileData []byte, testFile string) {
 	t.Helper()
 
 	// Hit the auction endpoint with the test case configuration and mockBidRequest
@@ -5098,7 +5119,7 @@ func TestValidResponseAfterExecutingStages(t *testing.T) {
 			fileData, err := os.ReadFile(tc.file)
 			assert.NoError(t, err, "Failed to read test file.")
 
-			test, err := parseTestFile(fileData, tc.file)
+			test, err := parseTestData(fileData, tc.file)
 			assert.NoError(t, err, "Failed to parse test file.")
 			test.planBuilder = tc.planBuilder
 			test.endpointType = OPENRTB_ENDPOINT
diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go
index 6811249b..814f2611 100644
--- a/endpoints/openrtb2/test_utils.go
+++ b/endpoints/openrtb2/test_utils.go
@@ -10,7 +10,6 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"os"
-	"path/filepath"
 	"strconv"
 	"testing"
 	"time"
@@ -1094,23 +1093,7 @@ func newBidderInfo(isDisabled bool) config.BidderInfo {
 	}
 }
 
-func getTestFiles(dir string) ([]string, error) {
-	var filesToAssert []string
-
-	fileList, err := os.ReadDir(dir)
-	if err != nil {
-		return nil, err
-	}
-
-	// Append the path of every file found in `dir` to the `filesToAssert` array
-	for _, fileInfo := range fileList {
-		filesToAssert = append(filesToAssert, filepath.Join(dir, fileInfo.Name()))
-	}
-
-	return filesToAssert, nil
-}
-
-func parseTestFile(fileData []byte, testFile string) (testCase, error) {
+func parseTestData(fileData []byte, testFile string) (testCase, error) {
 
 	parsedTestData := testCase{}
 	var err, errEm error
@@ -1143,9 +1126,9 @@ func parseTestFile(fileData []byte, testFile string) (testCase, error) {
 	parsedTestData.ExpectedErrorMessage, errEm = jsonparser.GetString(fileData, "expectedErrorMessage")
 
 	if err == nil && errEm == nil {
-		return parsedTestData, errors.New("Test case file can't have both a valid expectedBidResponse and a valid expectedErrorMessage, fields are mutually exclusive")
+		return parsedTestData, fmt.Errorf("Test case %s can't have both a valid expectedBidResponse and a valid expectedErrorMessage, fields are mutually exclusive", testFile)
 	} else if err != nil && errEm != nil {
-		return parsedTestData, errors.New("Test case file should come with either a valid expectedBidResponse or a valid expectedErrorMessage, not both.")
+		return parsedTestData, fmt.Errorf("Test case %s should come with either a valid expectedBidResponse or a valid expectedErrorMessage, not both.", testFile)
 	}
 
 	parsedTestData.ExpectedReturnCode = int(parsedReturnCode)
@@ -1217,9 +1200,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{},
@@ -1228,7 +1208,6 @@ func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.Bid
 		met,
 		bidderInfos,
 		gdprPermsBuilder,
-		tcf2ConfigBuilder,
 		mockCurrencyConverter,
 		mockFetcher,
 		&adscert.NilSigner{},
@@ -1415,14 +1394,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 3bd023dd..4e94a18f 100644
--- a/exchange/bidder_test.go
+++ b/exchange/bidder_test.go
@@ -3036,6 +3036,17 @@ func TestGetBidType(t *testing.T) {
 			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 {
diff --git a/exchange/exchange_test.go b/exchange/exchange_test.go
index 661233ec..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
@@ -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/gdpr/signal_test.go b/gdpr/signal_test.go
index 4c7b0aab..3e8d3c4f 100644
--- a/gdpr/signal_test.go
+++ b/gdpr/signal_test.go
@@ -43,18 +43,71 @@ func TestSignalParse(t *testing.T) {
 			wantSignal:  SignalAmbiguous,
 			wantError:   true,
 		},
+		{
+			description: "Out of bounds signal - raw signal is 5",
+			rawSignal:   "5",
+			wantSignal:  SignalAmbiguous,
+			wantError:   true,
+		},
 	}
 
 	for _, test := range tests {
-		signal, err := SignalParse(test.rawSignal)
+		t.Run(test.description, func(t *testing.T) {
+			signal, err := StrSignalParse(test.rawSignal)
+
+			assert.Equal(t, test.wantSignal, signal, test.description)
 
-		assert.Equal(t, test.wantSignal, signal, test.description)
+			if test.wantError {
+				assert.NotNil(t, err, test.description)
+			} else {
+				assert.Nil(t, err, test.description)
+			}
+		})
+	}
+}
+
+func TestIntSignalParse(t *testing.T) {
+	type testOutput struct {
+		signal Signal
+		err    error
+	}
+	testCases := []struct {
+		desc     string
+		input    int
+		expected testOutput
+	}{
+		{
+			desc:  "input out of bounds, return SgnalAmbituous and gdprSignalError",
+			input: -1,
+			expected: testOutput{
+				signal: SignalAmbiguous,
+				err:    gdprSignalError,
+			},
+		},
+		{
+			desc:  "input in bounds equals signalNo, return signalNo and nil error",
+			input: 0,
+			expected: testOutput{
+				signal: SignalNo,
+				err:    nil,
+			},
+		},
+		{
+			desc:  "input in bounds equals signalYes, return signalYes and nil error",
+			input: 1,
+			expected: testOutput{
+				signal: SignalYes,
+				err:    nil,
+			},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) {
+			outSignal, outErr := IntSignalParse(tc.input)
 
-		if test.wantError {
-			assert.NotNil(t, err, test.description)
-		} else {
-			assert.Nil(t, err, test.description)
-		}
+			assert.Equal(t, tc.expected.signal, outSignal, tc.desc)
+			assert.Equal(t, tc.expected.err, outErr, tc.desc)
+		})
 	}
 }
 
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)
+	}
+}
diff --git a/privacy/ccpa/policy_test.go b/privacy/ccpa/policy_test.go
index cd494ccf..3a143333 100644
--- a/privacy/ccpa/policy_test.go
+++ b/privacy/ccpa/policy_test.go
@@ -2,6 +2,7 @@ package ccpa
 
 import (
 	"encoding/json"
+	"errors"
 	"testing"
 
 	gpplib "github.com/prebid/go-gpp"
@@ -165,7 +166,7 @@ func TestReadFromRequestWrapper(t *testing.T) {
 			},
 			giveGPP: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{6}, Sections: []gpplib.Section{&upsv1Section}},
 			expectedPolicy: Policy{
-				Consent:       "present",
+				Consent:       "gppContainerConsent",
 				NoSaleBidders: []string{"a", "b"},
 			},
 		},
@@ -179,7 +180,7 @@ func TestReadFromRequestWrapper(t *testing.T) {
 			},
 			giveGPP: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{6}, Sections: []gpplib.Section{&upsv1Section}},
 			expectedPolicy: Policy{
-				Consent:       "present",
+				Consent:       "gppContainerConsent",
 				NoSaleBidders: []string{"a", "b"},
 			},
 		},
@@ -193,7 +194,7 @@ func TestReadFromRequestWrapper(t *testing.T) {
 			},
 			giveGPP: gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{6}, Sections: []gpplib.Section{&upsv1Section}},
 			expectedPolicy: Policy{
-				Consent:       "present",
+				Consent:       "gppContainerConsent",
 				NoSaleBidders: []string{"a", "b"},
 			},
 			expectedError: true,
@@ -816,6 +817,87 @@ func TestBuildExtWrite(t *testing.T) {
 	}
 }
 
+func TestSelectCCPAConsent(t *testing.T) {
+	type testInput struct {
+		requestUSPrivacy string
+		gpp              gpplib.GppContainer
+		gppSIDs          []int8
+	}
+	testCases := []struct {
+		desc         string
+		in           testInput
+		expectedCCPA string
+		expectedErr  error
+	}{
+		{
+			desc: "SectionUSPV1 in both GPP_SID and GPP container. Consent equal to request US_Privacy. Expect valid string and nil error",
+			in: testInput{
+				requestUSPrivacy: "gppContainerConsent",
+				gpp:              gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{gppConstants.SectionUSPV1}, Sections: []gpplib.Section{upsv1Section}},
+				gppSIDs:          []int8{int8(6)},
+			},
+			expectedCCPA: "gppContainerConsent",
+			expectedErr:  nil,
+		},
+		{
+			desc: "No SectionUSPV1 in GPP_SID array expect request US_Privacy",
+			in: testInput{
+				requestUSPrivacy: "requestConsent",
+				gpp:              gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{gppConstants.SectionUSPV1}, Sections: []gpplib.Section{upsv1Section}},
+				gppSIDs:          []int8{int8(2), int8(4)},
+			},
+			expectedCCPA: "requestConsent",
+			expectedErr:  nil,
+		},
+		{
+			desc: "No SectionUSPV1 in gpp.SectionTypes array expect request US_Privacy",
+			in: testInput{
+				requestUSPrivacy: "requestConsent",
+				gpp:              gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{}, Sections: []gpplib.Section{upsv1Section}},
+				gppSIDs:          []int8{int8(6)},
+			},
+			expectedCCPA: "requestConsent",
+			expectedErr:  nil,
+		},
+		{
+			desc: "No SectionUSPV1 in GPP_SID array, blank request US_Privacy, expect blank consent",
+			in: testInput{
+				requestUSPrivacy: "",
+				gpp:              gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{gppConstants.SectionUSPV1}, Sections: []gpplib.Section{upsv1Section}},
+				gppSIDs:          []int8{int8(2), int8(4)},
+			},
+			expectedCCPA: "",
+			expectedErr:  nil,
+		},
+		{
+			desc: "No SectionUSPV1 in gpp.SectionTypes array, blank request US_Privacy, expect blank consent",
+			in: testInput{
+				requestUSPrivacy: "",
+				gpp:              gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{}, Sections: []gpplib.Section{upsv1Section}},
+				gppSIDs:          []int8{int8(6)},
+			},
+			expectedCCPA: "",
+			expectedErr:  nil,
+		},
+		{
+			desc: "SectionUSPV1 in both GPP_SID and GPP container. Consent equal to request US_Privacy. Expect valid string and nil error",
+			in: testInput{
+				requestUSPrivacy: "requestConsent",
+				gpp:              gpplib.GppContainer{Version: 1, SectionTypes: []gppConstants.SectionID{gppConstants.SectionUSPV1}, Sections: []gpplib.Section{upsv1Section}},
+				gppSIDs:          []int8{int8(6)},
+			},
+			expectedCCPA: "gppContainerConsent",
+			expectedErr:  errors.New("request.us_privacy consent does not match uspv1"),
+		},
+	}
+	for _, tc := range testCases {
+		out, outErr := SelectCCPAConsent(tc.in.requestUSPrivacy, tc.in.gpp, tc.in.gppSIDs)
+
+		assert.Equal(t, tc.expectedCCPA, out, tc.desc)
+		assert.Equal(t, tc.expectedErr, outErr, tc.desc)
+	}
+}
+
 func assertError(t *testing.T, expectError bool, err error, description string) {
 	t.Helper()
 	if expectError {
@@ -825,7 +907,7 @@ func assertError(t *testing.T, expectError bool, err error, description string)
 	}
 }
 
-var upsv1Section mockGPPSection = mockGPPSection{sectionID: 6, value: "present"}
+var upsv1Section mockGPPSection = mockGPPSection{sectionID: 6, value: "gppContainerConsent"}
 var tcf1Section mockGPPSection = mockGPPSection{sectionID: 2, value: "BOS2bx5OS2bx5ABABBAAABoAAAAAFA"}
 
 type mockGPPSection struct {
diff --git a/privacy/gpp/gpp_test.go b/privacy/gpp/gpp_test.go
new file mode 100644
index 00000000..c76c6598
--- /dev/null
+++ b/privacy/gpp/gpp_test.go
@@ -0,0 +1,107 @@
+package gpp
+
+import (
+	"testing"
+
+	gpplib "github.com/prebid/go-gpp"
+	gppConstants "github.com/prebid/go-gpp/constants"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestIsSIDInList(t *testing.T) {
+	type testInput struct {
+		gppSIDs []int8
+		sid     gppConstants.SectionID
+	}
+	testCases := []struct {
+		desc     string
+		in       testInput
+		expected bool
+	}{
+		{
+			desc: "nil_gppSID_array,_expect_false",
+			in: testInput{
+				gppSIDs: nil,
+				sid:     gppConstants.SectionTCFEU2,
+			},
+			expected: false,
+		},
+		{
+			desc: "empty_gppSID_array, expect_false",
+			in: testInput{
+				gppSIDs: []int8{},
+				sid:     gppConstants.SectionTCFEU2,
+			},
+			expected: false,
+		},
+		{
+			desc: "SID_not_found_in_gppSID_array,_expect_false",
+			in: testInput{
+				gppSIDs: []int8{int8(8), int8(9)},
+				sid:     gppConstants.SectionTCFEU2,
+			},
+			expected: false,
+		},
+		{
+			desc: "SID_found_in_gppSID_array,_expect_true",
+			in: testInput{
+				gppSIDs: []int8{int8(2)},
+				sid:     gppConstants.SectionTCFEU2,
+			},
+			expected: true,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) { assert.Equal(t, tc.expected, IsSIDInList(tc.in.gppSIDs, tc.in.sid)) })
+	}
+}
+
+func TestIndexOfSID(t *testing.T) {
+	type testInput struct {
+		gpp gpplib.GppContainer
+		sid gppConstants.SectionID
+	}
+	testCases := []struct {
+		desc     string
+		in       testInput
+		expected int
+	}{
+		{
+			desc: "Empty_SectionTypes_array,_expect_-1_out",
+			in: testInput{
+				gpp: gpplib.GppContainer{},
+				sid: gppConstants.SectionTCFEU2,
+			},
+			expected: -1,
+		},
+		{
+			desc: "SID_not_found_in_SectionTypes_array,_expect_-1_out",
+			in: testInput{
+				gpp: gpplib.GppContainer{
+					Version:      1,
+					SectionTypes: []gppConstants.SectionID{gppConstants.SectionUSPV1},
+				},
+				sid: gppConstants.SectionTCFEU2,
+			},
+			expected: -1,
+		},
+		{
+			desc: "SID_matches_an_element_in_SectionTypes_array,_expect_index_1_out",
+			in: testInput{
+				gpp: gpplib.GppContainer{
+					Version: 1,
+					SectionTypes: []gppConstants.SectionID{
+						gppConstants.SectionUSPV1,
+						gppConstants.SectionTCFEU2,
+						gppConstants.SectionUSPCA,
+					},
+				},
+				sid: gppConstants.SectionTCFEU2,
+			},
+			expected: 1,
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.desc, func(t *testing.T) { assert.Equal(t, tc.expected, IndexOfSID(tc.in.gpp, tc.in.sid)) })
+	}
+}
diff --git a/privacy/lmt/ios_test.go b/privacy/lmt/ios_test.go
index 3387ff8e..2a679bfb 100644
--- a/privacy/lmt/ios_test.go
+++ b/privacy/lmt/ios_test.go
@@ -54,7 +54,7 @@ func TestModifyForIOS(t *testing.T) {
 				App:    &openrtb2.App{},
 				Device: &openrtb2.Device{Ext: json.RawMessage(`{"atts":0}`), OS: "iOS", OSV: "14.2", IFA: "", Lmt: nil},
 			},
-			expectedLMT: openrtb2.Int8Ptr(0),
+			expectedLMT: openrtb2.Int8Ptr(1),
 		},
 		{
 			description: "14.2",
@@ -272,7 +272,7 @@ func TestModifyForIOS142OrGreater(t *testing.T) {
 		{
 			description: "Not Determined",
 			givenDevice: openrtb2.Device{Ext: json.RawMessage(`{"atts":0}`), Lmt: nil},
-			expectedLMT: openrtb2.Int8Ptr(0),
+			expectedLMT: openrtb2.Int8Ptr(1),
 		},
 		{
 			description: "Restricted",
diff --git a/usersync/cookie_test.go b/usersync/cookie_test.go
index 4e87db5d..8efa3b74 100644
--- a/usersync/cookie_test.go
+++ b/usersync/cookie_test.go
@@ -2,7 +2,6 @@ package usersync
 
 import (
 	"encoding/base64"
-	"encoding/json"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -15,7 +14,7 @@ import (
 
 func TestOptOutCookie(t *testing.T) {
 	cookie := &Cookie{
-		uids:     make(map[string]uidWithExpiry),
+		uids:     map[string]uidWithExpiry{"appnexus": {UID: "test"}},
 		optOut:   true,
 		birthday: timestamp(),
 	}
@@ -260,32 +259,6 @@ func TestCookieReadWrite(t *testing.T) {
 	assert.Len(t, received.uids, 2, "Sync Count")
 }
 
-func TestPopulatedLegacyCookieRead(t *testing.T) {
-	legacyJson := `{"uids":{"adnxs":"123","audienceNetwork":"456"},"bday":"2017-08-03T21:04:52.629198911Z"}`
-	var cookie Cookie
-	json.Unmarshal([]byte(legacyJson), &cookie)
-
-	if cookie.HasAnyLiveSyncs() {
-		t.Error("Expected 0 user syncs. Found at least 1.")
-	}
-	if cookie.HasLiveSync("adnxs") {
-		t.Errorf("Received cookie should act like it has no ID for adnxs.")
-	}
-	if cookie.HasLiveSync("audienceNetwork") {
-		t.Errorf("Received cookie should act like it has no ID for audienceNetwork.")
-	}
-}
-
-func TestEmptyLegacyCookieRead(t *testing.T) {
-	legacyJson := `{"bday":"2017-08-29T18:54:18.393925772Z"}`
-	var cookie Cookie
-	json.Unmarshal([]byte(legacyJson), &cookie)
-
-	if cookie.HasAnyLiveSyncs() {
-		t.Error("Expected 0 user syncs. Found at least 1.")
-	}
-}
-
 func TestNilCookie(t *testing.T) {
 	var nilCookie *Cookie
 
@@ -427,7 +400,11 @@ func ensureConsistency(t *testing.T, cookie *Cookie) {
 		t.Error("The PBSCookie interface shouldn't let modifications happen if the user has opted out")
 	}
 
-	assert.Equal(t, len(cookie.uids), len(copiedCookie.uids), "Incorrect sync count on reparsed cookie.")
+	if cookie.optOut {
+		assert.Equal(t, 0, len(copiedCookie.uids), "Incorrect sync count on reparsed cookie.")
+	} else {
+		assert.Equal(t, len(cookie.uids), len(copiedCookie.uids), "Incorrect sync count on reparsed cookie.")
+	}
 
 	for family, uid := range copiedCookie.uids {
 		if !cookie.HasLiveSync(family) {
diff --git a/util/stringutil/stringutil_test.go b/util/stringutil/stringutil_test.go
new file mode 100644
index 00000000..94988ee4
--- /dev/null
+++ b/util/stringutil/stringutil_test.go
@@ -0,0 +1,77 @@
+package stringutil
+
+import (
+	"strconv"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestStrToInt8Slice(t *testing.T) {
+	type testOutput struct {
+		arr []int8
+		err error
+	}
+	tests := []struct {
+		desc     string
+		in       string
+		expected testOutput
+	}{
+		{
+			desc: "empty string, expect nil output array",
+			in:   "",
+			expected: testOutput{
+				arr: nil,
+				err: nil,
+			},
+		},
+		{
+			desc: "string doesn't contain digits, expect nil output array",
+			in:   "malformed",
+			expected: testOutput{
+				arr: nil,
+				err: &strconv.NumError{"ParseInt", "malformed", strconv.ErrSyntax},
+			},
+		},
+		{
+			desc: "string contains int8 digits and non-digits, expect array with a single int8 element",
+			in:   "malformed,2,malformed",
+			expected: testOutput{
+				arr: nil,
+				err: &strconv.NumError{"ParseInt", "malformed", strconv.ErrSyntax},
+			},
+		},
+		{
+			desc: "string comes with single digit too big to fit into a signed int8, expect nil output array",
+			in:   "128",
+			expected: testOutput{
+				arr: nil,
+				err: &strconv.NumError{"ParseInt", "128", strconv.ErrRange},
+			},
+		},
+		{
+			desc: "string comes with single digit that fits into 8 bits, expect array with a single int8 element",
+			in:   "127",
+			expected: testOutput{
+				arr: []int8{int8(127)},
+				err: nil,
+			},
+		},
+		{
+			desc: "string comes with multiple, comma-separated numbers that fit into 8 bits, expect array with int8 elements",
+			in:   "127,2,-127",
+			expected: testOutput{
+				arr: []int8{int8(127), int8(2), int8(-127)},
+				err: nil,
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.desc, func(t *testing.T) {
+			outArr, outErr := StrToInt8Slice(tt.in)
+			assert.Equal(t, tt.expected.arr, outArr, tt.desc)
+			assert.Equal(t, tt.expected.err, outErr, tt.desc)
+		})
+	}
+}