Repository: prebid/prebid-server
Pull request #2759 (2023-05-09 15:50:27)

New adapter: Flipp
New Flipp Adapter
Link to doc: https://github.com/prebid/prebid.github.io/pull/4551


Patch:
diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go
index 36822e94..d566cc2b 100644
--- a/adapters/appnexus/appnexus.go
+++ b/adapters/appnexus/appnexus.go
@@ -21,20 +21,21 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-const defaultPlatformID int = 5
+const (
+	defaultPlatformID int = 5
+	maxImpsPerReq         = 10
+)
 
 type adapter struct {
-	URI             string
+	uri             string
 	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) {
 	bidder := &adapter{
-		URI:             config.Endpoint,
+		uri:             config.Endpoint,
 		hbSource:        resolvePlatformID(config.PlatformID),
 		randomGenerator: randomutil.RandomNumberGenerator{},
 	}
@@ -52,7 +53,6 @@ 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)
@@ -97,7 +97,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.
@@ -108,11 +113,6 @@ func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.E
 		}
 	}
 
-	// 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 {
@@ -139,8 +139,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
@@ -148,11 +146,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)
 		return requests, append(errs, errors...)
 	}
 
-	requests, errors := splitRequests(imps, request, reqExt, requestURI)
+	requests, errors := splitRequests(request.Imp, request, reqExt, requestURI)
 	return requests, append(errs, errors...)
 }
 
@@ -193,7 +191,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{
@@ -258,7 +256,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.
@@ -329,7 +327,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)
@@ -346,32 +343,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
-}
-
-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))
-			}
-		}
-	}
+	imp.Ext, err = json.Marshal(&impExt)
 
-	return strings.Join(kvs, ",")
+	return err
 }
 
 // getMediaTypeForBid determines which type of bid.
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/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..ef45f71e 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"
@@ -248,12 +250,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..e6a8d779 100644
--- a/exchange/gdpr.go
+++ b/exchange/gdpr.go
@@ -1,59 +1,40 @@
 package exchange
 
 import (
-	"encoding/json"
-
 	gpplib "github.com/prebid/go-gpp"
 	gppConstants "github.com/prebid/go-gpp/constants"
-	"github.com/prebid/openrtb/v19/openrtb2"
 	"github.com/prebid/prebid-server/gdpr"
+	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-// ExtractGDPR will pull the gdpr flag from an openrtb request
-func extractGDPR(bidRequest *openrtb2.BidRequest) (gdpr.Signal, error) {
-	var re regsExt
-	var err error
-
-	if bidRequest.Regs != nil && len(bidRequest.Regs.GPPSID) > 0 {
-		for _, id := range bidRequest.Regs.GPPSID {
+// getGDPR will pull the gdpr flag from an openrtb request
+func getGDPR(req *openrtb_ext.RequestWrapper) (gdpr.Signal, error) {
+	if req.Regs != nil && len(req.Regs.GPPSID) > 0 {
+		for _, id := range req.Regs.GPPSID {
 			if id == int8(gppConstants.SectionTCFEU2) {
 				return gdpr.SignalYes, nil
 			}
 		}
 		return gdpr.SignalNo, nil
 	}
-	if bidRequest.Regs != nil && bidRequest.Regs.Ext != nil {
-		err = json.Unmarshal(bidRequest.Regs.Ext, &re)
-	}
-	if re.GDPR == nil || err != nil {
+	re, err := req.GetRegExt()
+	if re == nil || re.GetGDPR() == nil || err != nil {
 		return gdpr.SignalAmbiguous, err
 	}
-	return gdpr.Signal(*re.GDPR), nil
+	return gdpr.Signal(*re.GetGDPR()), nil
 }
 
-// ExtractConsent will pull the consent string from an openrtb request
-func extractConsent(bidRequest *openrtb2.BidRequest, gpp gpplib.GppContainer) (consent string, err error) {
+// getConsent will pull the consent string from an openrtb request
+func getConsent(req *openrtb_ext.RequestWrapper, gpp gpplib.GppContainer) (consent string, err error) {
 	for i, id := range gpp.SectionTypes {
 		if id == gppConstants.SectionTCFEU2 {
 			consent = gpp.Sections[i].GetValue()
 			return
 		}
 	}
-	var ue userExt
-	if bidRequest.User != nil && bidRequest.User.Ext != nil {
-		err = json.Unmarshal(bidRequest.User.Ext, &ue)
-	}
-	if err != nil {
+	ue, err := req.GetUserExt()
+	if ue == nil || ue.GetConsent() == nil || err != nil {
 		return
 	}
-	consent = ue.Consent
-	return
-}
-
-type userExt struct {
-	Consent string `json:"consent,omitempty"`
-}
-
-type regsExt struct {
-	GDPR *int `json:"gdpr,omitempty"`
+	return *ue.GetConsent(), nil
 }
diff --git a/exchange/utils.go b/exchange/utils.go
index d6b30a8c..de74c10d 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -41,7 +41,6 @@ type requestSplitter struct {
 	me                metrics.MetricsEngine
 	privacyConfig     config.Privacy
 	gdprPermsBuilder  gdpr.PermissionsBuilder
-	tcf2ConfigBuilder gdpr.TCF2ConfigBuilder
 	hostSChainNode    *openrtb2.SupplyChainNode
 	bidderInfo        config.BidderInfos
 }
@@ -93,12 +92,12 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 		}
 	}
 
-	gdprSignal, err := extractGDPR(req.BidRequest)
+	gdprSignal, err := getGDPR(req)
 	if err != nil {
 		errs = append(errs, err)
 	}
 
-	consent, err := extractConsent(req.BidRequest, gpp)
+	consent, err := getConsent(req, gpp)
 	if err != nil {
 		errs = append(errs, err)
 	}
@@ -122,13 +121,11 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 	privacyLabels.COPPAEnforced = privacyEnforcement.COPPA
 	privacyLabels.LMTEnforced = lmtEnforcer.ShouldEnforce(unknownBidder)
 
-	tcf2Cfg := rs.tcf2ConfigBuilder(rs.privacyConfig.GDPR.TCF2, auctionReq.Account.GDPR)
-
 	var gdprEnforced bool
 	var gdprPerms gdpr.Permissions = &gdpr.AlwaysAllow{}
 
 	if gdprApplies {
-		gdprEnforced = tcf2Cfg.ChannelEnabled(channelTypeMap[auctionReq.LegacyLabels.RType])
+		gdprEnforced = auctionReq.TCF2Config.ChannelEnabled(channelTypeMap[auctionReq.LegacyLabels.RType])
 	}
 
 	if gdprEnforced {
@@ -145,7 +142,7 @@ func (rs *requestSplitter) cleanOpenRTBRequests(ctx context.Context,
 			GDPRSignal:  gdprSignal,
 			PublisherID: auctionReq.LegacyLabels.PubID,
 		}
-		gdprPerms = rs.gdprPermsBuilder(tcf2Cfg, gdprRequestInfo)
+		gdprPerms = rs.gdprPermsBuilder(auctionReq.TCF2Config, gdprRequestInfo)
 	}
 
 	// bidder level privacy policies
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 1b959c5d..145e66f3 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -157,12 +157,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"
@@ -345,12 +347,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/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/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/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


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/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/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/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/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/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/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/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",