Repository: prebid/prebid-server
Pull request #2762 (2023-05-10 08:32:16)

Set lmt to 1 if device.ext.atts is 0 for ios versions 14.2 or greater
Addressing update in requirements for #1699 


Patch:
diff --git a/adapters/appnexus/appnexus.go b/adapters/appnexus/appnexus.go
index 36822e94..76104193 100644
--- a/adapters/appnexus/appnexus.go
+++ b/adapters/appnexus/appnexus.go
@@ -346,7 +346,7 @@ func buildRequestImp(imp *openrtb2.Imp, appnexusExt *openrtb_ext.ExtImpAppnexus,
 	impExt := appnexusImpExt{Appnexus: appnexusImpExtAppnexus{
 		PlacementID:       int(appnexusExt.PlacementId),
 		TrafficSourceCode: appnexusExt.TrafficSourceCode,
-		Keywords:          makeKeywordStr(appnexusExt.Keywords),
+		Keywords:          appnexusExt.Keywords.String(),
 		UsePmtRule:        appnexusExt.UsePaymentRule,
 		PrivateSizes:      appnexusExt.PrivateSizes,
 	}}
@@ -359,21 +359,6 @@ func buildRequestImp(imp *openrtb2.Imp, appnexusExt *openrtb_ext.ExtImpAppnexus,
 	return nil
 }
 
-func makeKeywordStr(keywords []*openrtb_ext.ExtImpAppnexusKeyVal) string {
-	kvs := make([]string, 0, len(keywords)*2)
-	for _, kv := range keywords {
-		if len(kv.Values) == 0 {
-			kvs = append(kvs, kv.Key)
-		} else {
-			for _, val := range kv.Values {
-				kvs = append(kvs, fmt.Sprintf("%s=%s", kv.Key, val))
-			}
-		}
-	}
-
-	return strings.Join(kvs, ",")
-}
-
 // getMediaTypeForBid determines which type of bid.
 func getMediaTypeForBid(bid *appnexusBidExt) (openrtb_ext.BidType, error) {
 	switch bid.Appnexus.BidType {
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/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..11de45aa 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -72,6 +72,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/dianomi"
 	"github.com/prebid/prebid-server/adapters/dmx"
 	evolution "github.com/prebid/prebid-server/adapters/e_volution"
+	"github.com/prebid/prebid-server/adapters/emtv"
 	"github.com/prebid/prebid-server/adapters/emx_digital"
 	"github.com/prebid/prebid-server/adapters/engagebdr"
 	"github.com/prebid/prebid-server/adapters/eplanning"
@@ -248,6 +249,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderDefinemedia:       definemedia.Builder,
 		openrtb_ext.BidderDianomi:           dianomi.Builder,
 		openrtb_ext.BidderDmx:               dmx.Builder,
+		openrtb_ext.BidderEmtv:              emtv.Builder,
 		openrtb_ext.BidderEmxDigital:        emx_digital.Builder,
 		openrtb_ext.BidderEngageBDR:         engagebdr.Builder,
 		openrtb_ext.BidderEPlanning:         eplanning.Builder,
diff --git a/exchange/bidder.go b/exchange/bidder.go
index 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..f976fc77 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -157,6 +157,7 @@ const (
 	BidderDefinemedia       BidderName = "definemedia"
 	BidderDianomi           BidderName = "dianomi"
 	BidderDmx               BidderName = "dmx"
+	BidderEmtv              BidderName = "emtv"
 	BidderEmxDigital        BidderName = "emx_digital"
 	BidderEngageBDR         BidderName = "engagebdr"
 	BidderEPlanning         BidderName = "eplanning"
@@ -345,6 +346,7 @@ func CoreBidderNames() []BidderName {
 		BidderDefinemedia,
 		BidderDianomi,
 		BidderDmx,
+		BidderEmtv,
 		BidderEmxDigital,
 		BidderEngageBDR,
 		BidderEPlanning,
diff --git a/openrtb_ext/imp_appnexus.go b/openrtb_ext/imp_appnexus.go
index 56dc0dbb..c1f1a856 100644
--- a/openrtb_ext/imp_appnexus.go
+++ b/openrtb_ext/imp_appnexus.go
@@ -2,31 +2,92 @@ package openrtb_ext
 
 import (
 	"encoding/json"
+	"fmt"
+	"strings"
 
 	"github.com/prebid/prebid-server/util/jsonutil"
 )
 
 // ExtImpAppnexus defines the contract for bidrequest.imp[i].ext.prebid.bidder.appnexus
 type ExtImpAppnexus struct {
-	DeprecatedPlacementId    jsonutil.StringInt      `json:"placementId"`
-	LegacyInvCode            string                  `json:"invCode"`
-	LegacyTrafficSourceCode  string                  `json:"trafficSourceCode"`
-	PlacementId              jsonutil.StringInt      `json:"placement_id"`
-	InvCode                  string                  `json:"inv_code"`
-	Member                   string                  `json:"member"`
-	Keywords                 []*ExtImpAppnexusKeyVal `json:"keywords"`
-	TrafficSourceCode        string                  `json:"traffic_source_code"`
-	Reserve                  float64                 `json:"reserve"`
-	Position                 string                  `json:"position"`
-	UsePaymentRule           *bool                   `json:"use_pmt_rule"`
-	DeprecatedUsePaymentRule *bool                   `json:"use_payment_rule"`
+	DeprecatedPlacementId    jsonutil.StringInt     `json:"placementId"`
+	LegacyInvCode            string                 `json:"invCode"`
+	LegacyTrafficSourceCode  string                 `json:"trafficSourceCode"`
+	PlacementId              jsonutil.StringInt     `json:"placement_id"`
+	InvCode                  string                 `json:"inv_code"`
+	Member                   string                 `json:"member"`
+	Keywords                 ExtImpAppnexusKeywords `json:"keywords"`
+	TrafficSourceCode        string                 `json:"traffic_source_code"`
+	Reserve                  float64                `json:"reserve"`
+	Position                 string                 `json:"position"`
+	UsePaymentRule           *bool                  `json:"use_pmt_rule"`
+	DeprecatedUsePaymentRule *bool                  `json:"use_payment_rule"`
 	// At this time we do no processing on the private sizes, so just leaving it as a JSON blob.
 	PrivateSizes json.RawMessage `json:"private_sizes"`
 	AdPodId      bool            `json:"generate_ad_pod_id"`
 }
 
-// ExtImpAppnexusKeyVal defines the contract for bidrequest.imp[i].ext.prebid.bidder.appnexus.keywords[i]
-type ExtImpAppnexusKeyVal struct {
+type ExtImpAppnexusKeywords string
+
+// extImpAppnexusKeyVal defines the contract for bidrequest.imp[i].ext.prebid.bidder.appnexus.keywords[i]
+type extImpAppnexusKeyVal struct {
 	Key    string   `json:"key,omitempty"`
 	Values []string `json:"value,omitempty"`
 }
+
+func (ks *ExtImpAppnexusKeywords) UnmarshalJSON(b []byte) error {
+	if len(b) == 0 {
+		return nil
+	}
+
+	switch b[0] {
+	case '{':
+		var results map[string][]string
+		if err := json.Unmarshal(b, &results); err != nil {
+			return err
+		}
+
+		var keywords strings.Builder
+		for key, values := range results {
+			if len(values) == 0 {
+				keywords.WriteString(fmt.Sprintf("%s,", key))
+			} else {
+				for _, val := range values {
+					keywords.WriteString(fmt.Sprintf("%s=%s,", key, val))
+				}
+			}
+		}
+		if len(keywords.String()) > 0 {
+			*ks = ExtImpAppnexusKeywords(keywords.String()[:keywords.Len()-1])
+		}
+	case '[':
+		var results []extImpAppnexusKeyVal
+		if err := json.Unmarshal(b, &results); err != nil {
+			return err
+		}
+		var kvs strings.Builder
+		for _, kv := range results {
+			if len(kv.Values) == 0 {
+				kvs.WriteString(fmt.Sprintf("%s,", kv.Key))
+			} else {
+				for _, val := range kv.Values {
+					kvs.WriteString(fmt.Sprintf("%s=%s,", kv.Key, val))
+				}
+			}
+		}
+		if len(kvs.String()) > 0 {
+			*ks = ExtImpAppnexusKeywords(kvs.String()[:kvs.Len()-1])
+		}
+	case '"':
+		var keywords string
+		if err := json.Unmarshal(b, &keywords); err != nil {
+			return err
+		}
+		*ks = ExtImpAppnexusKeywords(keywords)
+	}
+	return nil
+}
+
+func (ks *ExtImpAppnexusKeywords) String() string {
+	return *(*string)(ks)
+}
diff --git a/openrtb_ext/imp_emtv.go b/openrtb_ext/imp_emtv.go
new file mode 100644
index 00000000..a7701074
--- /dev/null
+++ b/openrtb_ext/imp_emtv.go
@@ -0,0 +1,6 @@
+package openrtb_ext
+
+type ImpExtEmtv struct {
+	PlacementID string `json:"placementId"`
+	EndpointID  string `json:"endpointId"`
+}
diff --git a/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/emtv.yaml b/static/bidder-info/emtv.yaml
new file mode 100644
index 00000000..6c9b3a11
--- /dev/null
+++ b/static/bidder-info/emtv.yaml
@@ -0,0 +1,19 @@
+endpoint: "https://us-east-ep.engagemedia.tv/pserver"
+maintainer:
+  email: "support@engagemedia.tv"
+capabilities:
+  site:
+    mediaTypes:
+    - banner
+    - video
+    - native
+    
+  app:
+    mediaTypes:
+    - banner
+    - video
+    - native
+userSync:
+  redirect:
+    url: "https://cs.engagemedia.tv/pbserver?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&ccpa={{.USPrivacy}}&redir={{.RedirectURL}}"
+    userMacro: "[UID]"
diff --git a/static/bidder-params/appnexus.json b/static/bidder-params/appnexus.json
index c3e47129..27b9bfe9 100644
--- a/static/bidder-params/appnexus.json
+++ b/static/bidder-params/appnexus.json
@@ -26,25 +26,42 @@
       "description": "An ID which identifies the member selling the impression."
     },
     "keywords": {
-      "type": "array",
-      "minItems": 1,
-      "items": {
-        "type": "object",
-        "description": "A key with one or more values associated with it. These are used in buy-side segment targeting.",
-        "properties": {
-          "key": {
-            "type": "string"
-          },
-          "value": {
+      "anyOf": [
+        {
+          "type": "string",
+          "minLength": 1
+        },
+        {
+          "type": "array",
+          "minItems": 1,
+          "items": {
+            "type": "object",
+            "description": "A key with one or more values associated with it. These are used in buy-side segment targeting.",
+            "properties": {
+              "key": {
+                "type": "string"
+              },
+              "value": {
+                "type": "array",
+                "minItems": 1,
+                "items": {
+                  "type": "string"
+                }
+              }
+            },
+            "required": ["key"]
+          }
+        },
+        {
+          "type": "object",
+          "additionalProperties": {
             "type": "array",
-            "minItems": 1,
             "items": {
               "type": "string"
             }
           }
-        },
-        "required": ["key"]
-      }
+        }
+      ]
     },
     "traffic_source_code": {
       "type": "string",
diff --git a/static/bidder-params/emtv.json b/static/bidder-params/emtv.json
new file mode 100644
index 00000000..a491b825
--- /dev/null
+++ b/static/bidder-params/emtv.json
@@ -0,0 +1,23 @@
+{
+    "$schema": "http://json-schema.org/draft-04/schema#",
+    "title": "EMTV Adapter Params",
+    "description": "A schema which validates params accepted by the EMTV adapter",
+    "type": "object",
+  
+    "properties": {
+      "placementId": {
+        "type": "string",
+        "minLength": 1,
+        "description": "Placement ID"
+      },
+      "endpointId": {
+        "type": "string",
+        "minLength": 1,
+        "description": "Endpoint ID"
+      }
+    },
+    "oneOf": [
+      { "required": ["placementId"] },
+      { "required": ["endpointId"] }
+    ]
+  }
\ No newline at end of file


Test patch:
diff --git a/adapters/appnexus/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/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/endpoints/openrtb2/auction_benchmark_test.go b/endpoints/openrtb2/auction_benchmark_test.go
index 15d70efa..b91f5ef8 100644
--- a/endpoints/openrtb2/auction_benchmark_test.go
+++ b/endpoints/openrtb2/auction_benchmark_test.go
@@ -15,7 +15,6 @@ import (
 	"github.com/prebid/prebid-server/currency"
 	"github.com/prebid/prebid-server/exchange"
 	"github.com/prebid/prebid-server/experiment/adscert"
-	"github.com/prebid/prebid-server/gdpr"
 	"github.com/prebid/prebid-server/hooks"
 	metricsConfig "github.com/prebid/prebid-server/metrics/config"
 	"github.com/prebid/prebid-server/openrtb_ext"
@@ -82,9 +81,6 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
 	gdprPermsBuilder := fakePermissionsBuilder{
 		permissions: &fakePermissions{},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
 	exchange := exchange.NewExchange(
 		adapters,
@@ -94,7 +90,6 @@ func BenchmarkOpenrtbEndpoint(b *testing.B) {
 		nilMetrics,
 		infos,
 		gdprPermsBuilder,
-		tcf2ConfigBuilder,
 		currency.NewRateConverter(&http.Client{}, "", time.Duration(0)),
 		empty_fetcher.EmptyFetcher{},
 		&adscert.NilSigner{},
diff --git a/endpoints/openrtb2/test_utils.go b/endpoints/openrtb2/test_utils.go
index 6811249b..f4ae437b 100644
--- a/endpoints/openrtb2/test_utils.go
+++ b/endpoints/openrtb2/test_utils.go
@@ -1217,9 +1217,6 @@ func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.Bid
 	gdprPermsBuilder := fakePermissionsBuilder{
 		permissions: &fakePermissions{},
 	}.Builder
-	tcf2ConfigBuilder := fakeTCF2ConfigBuilder{
-		cfg: gdpr.NewTCF2Config(config.TCF2{}, config.AccountGDPR{}),
-	}.Builder
 
 	testExchange := exchange.NewExchange(adapterMap,
 		&wellBehavedCache{},
@@ -1228,7 +1225,6 @@ func buildTestExchange(testCfg *testConfigValues, adapterMap map[openrtb_ext.Bid
 		met,
 		bidderInfos,
 		gdprPermsBuilder,
-		tcf2ConfigBuilder,
 		mockCurrencyConverter,
 		mockFetcher,
 		&adscert.NilSigner{},
@@ -1415,14 +1411,6 @@ func (fpb fakePermissionsBuilder) Builder(gdpr.TCF2ConfigReader, gdpr.RequestInf
 	return fpb.permissions
 }
 
-type fakeTCF2ConfigBuilder struct {
-	cfg gdpr.TCF2ConfigReader
-}
-
-func (fcr fakeTCF2ConfigBuilder) Builder(hostConfig config.TCF2, accountConfig config.AccountGDPR) gdpr.TCF2ConfigReader {
-	return fcr.cfg
-}
-
 type fakePermissions struct {
 }
 
diff --git a/exchange/bidder_test.go b/exchange/bidder_test.go
index 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",