Repository: prebid/prebid-server
Pull request #1928 (2021-07-15 12:07:46)

InMobi: adding native support
Accompanying docs PR: https://github.com/prebid/prebid.github.io/pull/3109


Patch:
diff --git a/adapters/admixer/admixer.go b/adapters/admixer/admixer.go
index ec49950a..5008b0ce 100644
--- a/adapters/admixer/admixer.go
+++ b/adapters/admixer/admixer.go
@@ -100,14 +100,17 @@ func preprocess(imp *openrtb2.Imp) error {
 	}
 
 	//don't use regexp due to possible performance reduce
-	if len(admixerExt.ZoneId) != 36 {
+	if len(admixerExt.ZoneId) < 32 || len(admixerExt.ZoneId) > 36 {
 		return &errortypes.BadInput{
 			Message: "ZoneId must be UUID/GUID",
 		}
 	}
 
 	imp.TagID = admixerExt.ZoneId
-	imp.BidFloor = admixerExt.CustomBidFloor
+
+	if imp.BidFloor == 0 && admixerExt.CustomBidFloor > 0 {
+		imp.BidFloor = admixerExt.CustomBidFloor
+	}
 
 	imp.Ext = nil
 
diff --git a/adapters/beachfront/beachfront.go b/adapters/beachfront/beachfront.go
index 6eba9923..7f4658d4 100644
--- a/adapters/beachfront/beachfront.go
+++ b/adapters/beachfront/beachfront.go
@@ -449,8 +449,9 @@ func getVideoRequests(request *openrtb2.BidRequest) ([]beachfrontVideoRequest, [
 				var chunks = strings.Split(strings.Trim(bfReqs[i].Request.App.Bundle, "_"), ".")
 
 				if len(chunks) > 1 {
-					bfReqs[i].Request.App.Domain =
-						fmt.Sprintf("%s.%s", chunks[len(chunks)-(len(chunks)-1)], chunks[0])
+					appCopy := *bfReqs[i].Request.App
+					appCopy.Domain = fmt.Sprintf("%s.%s", chunks[len(chunks)-(len(chunks)-1)], chunks[0])
+					bfReqs[i].Request.App = &appCopy
 				}
 			}
 		}
diff --git a/adapters/bidder.go b/adapters/bidder.go
index b7bde4bc..dbe56f8e 100644
--- a/adapters/bidder.go
+++ b/adapters/bidder.go
@@ -139,12 +139,12 @@ func (r *RequestData) SetBasicAuth(username string, password string) {
 type ExtraRequestInfo struct {
 	PbsEntryPoint              metrics.RequestType
 	GlobalPrivacyControlHeader string
-	currencyConversions        currency.Conversions
+	CurrencyConversions        currency.Conversions
 }
 
 func NewExtraRequestInfo(c currency.Conversions) ExtraRequestInfo {
 	return ExtraRequestInfo{
-		currencyConversions: c,
+		CurrencyConversions: c,
 	}
 }
 
@@ -153,7 +153,7 @@ func NewExtraRequestInfo(c currency.Conversions) ExtraRequestInfo {
 //  - ConversionNotFoundError if the conversion mapping is unknown to Prebid Server
 //    and not provided in the bid request.
 func (r ExtraRequestInfo) ConvertCurrency(value float64, from, to string) (float64, error) {
-	if rate, err := r.currencyConversions.GetRate(from, to); err == nil {
+	if rate, err := r.CurrencyConversions.GetRate(from, to); err == nil {
 		return value * rate, nil
 	} else {
 		return 0, err
diff --git a/adapters/inmobi/inmobi.go b/adapters/inmobi/inmobi.go
index a23472e8..63baa8a4 100644
--- a/adapters/inmobi/inmobi.go
+++ b/adapters/inmobi/inmobi.go
@@ -124,6 +124,9 @@ func getMediaTypeForImp(impId string, imps []openrtb2.Imp) openrtb_ext.BidType {
 			if imp.Video != nil {
 				mediaType = openrtb_ext.BidTypeVideo
 			}
+			if imp.Native != nil {
+				mediaType = openrtb_ext.BidTypeNative
+			}
 			break
 		}
 	}
diff --git a/adapters/operaads/operaads.go b/adapters/operaads/operaads.go
new file mode 100644
index 00000000..890a15dd
--- /dev/null
+++ b/adapters/operaads/operaads.go
@@ -0,0 +1,215 @@
+package operaads
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/prebid/prebid-server/macros"
+	"net/http"
+	"text/template"
+
+	"github.com/mxmCherry/openrtb/v15/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"
+)
+
+type adapter struct {
+	epTemplate *template.Template
+}
+
+// Builder builds a new instance of the operaads adapter for the given bidder with the given config.
+func Builder(bidderName openrtb_ext.BidderName, config config.Adapter) (adapters.Bidder, error) {
+	epTemplate, err := template.New("endpoint").Parse(config.Endpoint)
+	if err != nil {
+		return nil, err
+	}
+	bidder := &adapter{
+		epTemplate: epTemplate,
+	}
+	return bidder, nil
+}
+
+func (a *adapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *adapters.ExtraRequestInfo) ([]*adapters.RequestData, []error) {
+	impCount := len(request.Imp)
+	requestData := make([]*adapters.RequestData, 0, impCount)
+	errs := []error{}
+	headers := http.Header{}
+	headers.Add("Content-Type", "application/json;charset=utf-8")
+	headers.Add("Accept", "application/json")
+
+	err := checkRequest(request)
+	if err != nil {
+		errs = append(errs, err)
+		return nil, errs
+	}
+
+	for _, imp := range request.Imp {
+		requestCopy := *request
+		var bidderExt adapters.ExtImpBidder
+		if err := json.Unmarshal(imp.Ext, &bidderExt); err != nil {
+			errs = append(errs, &errortypes.BadInput{
+				Message: err.Error(),
+			})
+			continue
+		}
+
+		var operaadsExt openrtb_ext.ImpExtOperaads
+		if err := json.Unmarshal(bidderExt.Bidder, &operaadsExt); err != nil {
+			errs = append(errs, &errortypes.BadInput{
+				Message: err.Error(),
+			})
+			continue
+		}
+
+		err := convertImpression(&imp)
+		if err != nil {
+			errs = append(errs, &errortypes.BadInput{
+				Message: err.Error(),
+			})
+			continue
+		}
+
+		imp.TagID = operaadsExt.PlacementID
+
+		requestCopy.Imp = []openrtb2.Imp{imp}
+		reqJSON, err := json.Marshal(&requestCopy)
+		if err != nil {
+			errs = append(errs, err)
+			return nil, errs
+		}
+
+		macro := macros.EndpointTemplateParams{PublisherID: operaadsExt.PublisherID, AccountID: operaadsExt.EndpointID}
+		endpoint, err := macros.ResolveMacros(*a.epTemplate, &macro)
+		if err != nil {
+			errs = append(errs, err)
+			continue
+		}
+		reqData := &adapters.RequestData{
+			Method:  http.MethodPost,
+			Uri:     endpoint,
+			Body:    reqJSON,
+			Headers: headers,
+		}
+		requestData = append(requestData, reqData)
+	}
+	return requestData, errs
+}
+
+func checkRequest(request *openrtb2.BidRequest) error {
+	if request.Device == nil || len(request.Device.OS) == 0 {
+		return &errortypes.BadInput{
+			Message: "Impression is missing device OS information",
+		}
+	}
+
+	return nil
+}
+
+func convertImpression(imp *openrtb2.Imp) error {
+	if imp.Banner != nil {
+		bannerCopy, err := convertBanner(imp.Banner)
+		if err != nil {
+			return err
+		}
+		imp.Banner = bannerCopy
+	}
+	if imp.Native != nil && imp.Native.Request != "" {
+		v := make(map[string]interface{})
+		err := json.Unmarshal([]byte(imp.Native.Request), &v)
+		if err != nil {
+			return err
+		}
+		_, ok := v["native"]
+		if !ok {
+			body, err := json.Marshal(struct {
+				Native interface{} `json:"native"`
+			}{
+				Native: v,
+			})
+			if err != nil {
+				return err
+			}
+			native := *imp.Native
+			native.Request = string(body)
+			imp.Native = &native
+		}
+	}
+	return nil
+}
+
+// make sure that banner has openrtb 2.3-compatible size information
+func convertBanner(banner *openrtb2.Banner) (*openrtb2.Banner, error) {
+	if banner.W == nil || banner.H == nil || *banner.W == 0 || *banner.H == 0 {
+		if len(banner.Format) > 0 {
+			f := banner.Format[0]
+
+			bannerCopy := *banner
+
+			bannerCopy.W = openrtb2.Int64Ptr(f.W)
+			bannerCopy.H = openrtb2.Int64Ptr(f.H)
+
+			return &bannerCopy, nil
+		} else {
+			return nil, &errortypes.BadInput{
+				Message: "Size information missing for banner",
+			}
+		}
+	}
+	return banner, nil
+}
+
+func (a *adapter) MakeBids(internalRequest *openrtb2.BidRequest, externalRequest *adapters.RequestData, response *adapters.ResponseData) (*adapters.BidderResponse, []error) {
+	if response.StatusCode == http.StatusNoContent {
+		return nil, nil
+	}
+
+	if response.StatusCode == http.StatusBadRequest {
+		return nil, []error{&errortypes.BadInput{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
+		}}
+	}
+
+	if response.StatusCode != http.StatusOK {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: fmt.Sprintf("Unexpected status code: %d. Run with request.debug = 1 for more info", response.StatusCode),
+		}}
+	}
+
+	var parsedResponse openrtb2.BidResponse
+	if err := json.Unmarshal(response.Body, &parsedResponse); err != nil {
+		return nil, []error{&errortypes.BadServerResponse{
+			Message: err.Error(),
+		}}
+	}
+
+	bidResponse := adapters.NewBidderResponseWithBidsCapacity(1)
+
+	for _, sb := range parsedResponse.SeatBid {
+		for i := 0; i < len(sb.Bid); i++ {
+			bid := sb.Bid[i]
+			if bid.Price != 0 {
+				bidResponse.Bids = append(bidResponse.Bids, &adapters.TypedBid{
+					Bid:     &bid,
+					BidType: getMediaTypeForImp(bid.ImpID, internalRequest.Imp),
+				})
+			}
+		}
+	}
+	return bidResponse, nil
+}
+
+func getMediaTypeForImp(impId string, imps []openrtb2.Imp) openrtb_ext.BidType {
+	mediaType := openrtb_ext.BidTypeBanner
+	for _, imp := range imps {
+		if imp.ID == impId {
+			if imp.Video != nil {
+				mediaType = openrtb_ext.BidTypeVideo
+			} else if imp.Native != nil {
+				mediaType = openrtb_ext.BidTypeNative
+			}
+			return mediaType
+		}
+	}
+	return mediaType
+}
diff --git a/adapters/operaads/usersync.go b/adapters/operaads/usersync.go
new file mode 100644
index 00000000..aae6fb60
--- /dev/null
+++ b/adapters/operaads/usersync.go
@@ -0,0 +1,11 @@
+package operaads
+
+import (
+	"github.com/prebid/prebid-server/adapters"
+	"github.com/prebid/prebid-server/usersync"
+	"text/template"
+)
+
+func NewOperaadsSyncer(temp *template.Template) usersync.Usersyncer {
+	return adapters.NewSyncer("operaads", temp, adapters.SyncTypeRedirect)
+}
diff --git a/adapters/rubicon/rubicon.go b/adapters/rubicon/rubicon.go
index 84200431..e9627916 100644
--- a/adapters/rubicon/rubicon.go
+++ b/adapters/rubicon/rubicon.go
@@ -676,7 +676,6 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ada
 	numRequests := len(request.Imp)
 	errs := make([]error, 0, len(request.Imp))
 	var err error
-
 	requestData := make([]*adapters.RequestData, 0, numRequests)
 	headers := http.Header{}
 	headers.Add("Content-Type", "application/json;charset=utf-8")
@@ -750,9 +749,19 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ada
 			continue
 		}
 
-		resolvedBidFloor, resolvedBidFloorCur := resolveBidFloorAttributes(thisImp.BidFloor, thisImp.BidFloorCur)
-		thisImp.BidFloorCur = resolvedBidFloorCur
-		thisImp.BidFloor = resolvedBidFloor
+		resolvedBidFloor, err := resolveBidFloor(thisImp.BidFloor, thisImp.BidFloorCur, reqInfo)
+		if err != nil {
+			errs = append(errs, &errortypes.BadInput{
+				Message: fmt.Sprintf("Unable to convert provided bid floor currency from %s to USD",
+					thisImp.BidFloorCur),
+			})
+			continue
+		}
+
+		if resolvedBidFloor > 0 {
+			thisImp.BidFloorCur = "USD"
+			thisImp.BidFloor = resolvedBidFloor
+		}
 
 		if request.User != nil {
 			userCopy := *request.User
@@ -908,15 +917,12 @@ func (a *RubiconAdapter) MakeRequests(request *openrtb2.BidRequest, reqInfo *ada
 	return requestData, errs
 }
 
-// Will be replaced after https://github.com/prebid/prebid-server/issues/1482 resolution
-func resolveBidFloorAttributes(bidFloor float64, bidFloorCur string) (float64, string) {
-	if bidFloor > 0 {
-		if strings.ToUpper(bidFloorCur) == "EUR" {
-			return bidFloor * 1.2, "USD"
-		}
+func resolveBidFloor(bidFloor float64, bidFloorCur string, reqInfo *adapters.ExtraRequestInfo) (float64, error) {
+	if bidFloor > 0 && bidFloorCur != "" && strings.ToUpper(bidFloorCur) != "USD" {
+		return reqInfo.ConvertCurrency(bidFloor, bidFloorCur, "USD")
 	}
 
-	return bidFloor, bidFloorCur
+	return bidFloor, nil
 }
 
 func updateExtWithIabAttribute(target json.RawMessage, data []openrtb2.Data, segTaxes []int) (json.RawMessage, error) {
diff --git a/adapters/sharethrough/butler.go b/adapters/sharethrough/butler.go
index b34ae084..b7dd5003 100644
--- a/adapters/sharethrough/butler.go
+++ b/adapters/sharethrough/butler.go
@@ -21,6 +21,7 @@ const defaultTmax = 10000 // 10 sec
 type StrAdSeverParams struct {
 	Pkey               string
 	BidID              string
+	GPID               string
 	ConsentRequired    bool
 	ConsentString      string
 	USPrivacySignal    string
@@ -97,6 +98,11 @@ func (s StrOpenRTBTranslator) requestFromOpenRTB(imp openrtb2.Imp, request *open
 		return nil, err
 	}
 
+	var gpid string
+	if strImpParams.Data != nil && strImpParams.Data.PBAdSlot != "" {
+		gpid = strImpParams.Data.PBAdSlot
+	}
+
 	usPolicySignal := ""
 	if usPolicy, err := ccpa.ReadFromRequest(request); err == nil {
 		usPolicySignal = usPolicy.Consent
@@ -107,6 +113,7 @@ func (s StrOpenRTBTranslator) requestFromOpenRTB(imp openrtb2.Imp, request *open
 		Uri: s.UriHelper.buildUri(StrAdSeverParams{
 			Pkey:               pKey,
 			BidID:              imp.ID,
+			GPID:               gpid,
 			ConsentRequired:    s.Util.gdprApplies(request),
 			ConsentString:      userInfo.Consent,
 			USPrivacySignal:    usPolicySignal,
@@ -191,6 +198,9 @@ func (h StrUriHelper) buildUri(params StrAdSeverParams) string {
 	v := url.Values{}
 	v.Set("placement_key", params.Pkey)
 	v.Set("bidId", params.BidID)
+	if params.GPID != "" {
+		v.Set("gpid", params.GPID)
+	}
 	v.Set("consent_required", fmt.Sprintf("%t", params.ConsentRequired))
 	v.Set("consent_string", params.ConsentString)
 	if params.USPrivacySignal != "" {
diff --git a/config/config.go b/config/config.go
index 96e3c1f1..b8ff3bd6 100644
--- a/config/config.go
+++ b/config/config.go
@@ -257,7 +257,8 @@ type TCF2 struct {
 
 // Making a purpose struct so purpose specific details can be added later.
 type TCF2Purpose struct {
-	Enabled bool `mapstructure:"enabled"`
+	Enabled        bool `mapstructure:"enabled"`
+	EnforceVendors bool `mapstructure:"enforce_vendors"`
 	// Array of vendor exceptions that is used to create the hash table VendorExceptionMap so vendor names can be instantly accessed
 	VendorExceptions   []openrtb_ext.BidderName `mapstructure:"vendor_exceptions"`
 	VendorExceptionMap map[openrtb_ext.BidderName]struct{}
@@ -662,6 +663,7 @@ func (cfg *Configuration) setDerivedDefaults() {
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderNinthDecimal, "https://rtb.ninthdecimal.com/xp/user-sync?acctid={aid}&&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dninthdecimal%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderNoBid, "https://ads.servenobid.com/getsync?tek=pbs&ver=1&gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&redirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dnobid%26uid%3D%24UID")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderOpenx, "https://rtb.openx.net/sync/prebid?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dopenx%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D")
+	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderOperaads, "https://t.adx.opera.com/pbs/sync?gdpr={{.GDPR}}&us_privacy={{.USPrivacy}}&gdpr_consent={{.GDPRConsent}}&r="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Doperaads%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderOneTag, "https://onetag-sys.com/usync/?redir="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Donetag%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUSER_TOKEN%7D")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderOutbrain, "https://prebidtest.zemanta.com/usersync/prebidtest?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&cb="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Doutbrain%26uid%3D__ZUID__")
 	setDefaultUsersync(cfg.Adapters, openrtb_ext.BidderPubmatic, "https://ads.pubmatic.com/AdServer/js/user_sync.html?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&predirect="+url.QueryEscape(externalURL)+"%2Fsetuid%3Fbidder%3Dpubmatic%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D")
@@ -929,6 +931,7 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("adapters.nobid.endpoint", "https://ads.servenobid.com/ortb_adreq?tek=pbs&ver=1")
 	v.SetDefault("adapters.onetag.endpoint", "https://prebid-server.onetag-sys.com/prebid-server/{{.PublisherID}}")
 	v.SetDefault("adapters.openx.endpoint", "http://rtb.openx.net/prebid")
+	v.SetDefault("adapters.operaads.endpoint", "https://s.adx.opera.com/ortb/v2/{{.PublisherID}}?ep={{.AccountID}}")
 	v.SetDefault("adapters.orbidder.endpoint", "https://orbidder.otto.de/openrtb2")
 	v.SetDefault("adapters.outbrain.endpoint", "https://prebidtest.zemanta.com/api/bidder/prebidtest/bid/")
 	v.SetDefault("adapters.pangle.disabled", true)
@@ -999,6 +1002,16 @@ func SetupViper(v *viper.Viper, filename string) {
 	v.SetDefault("gdpr.tcf2.purpose8.enabled", true)
 	v.SetDefault("gdpr.tcf2.purpose9.enabled", true)
 	v.SetDefault("gdpr.tcf2.purpose10.enabled", true)
+	v.SetDefault("gdpr.tcf2.purpose1.enforce_vendors", true)
+	v.SetDefault("gdpr.tcf2.purpose2.enforce_vendors", true)
+	v.SetDefault("gdpr.tcf2.purpose3.enforce_vendors", true)
+	v.SetDefault("gdpr.tcf2.purpose4.enforce_vendors", true)
+	v.SetDefault("gdpr.tcf2.purpose5.enforce_vendors", true)
+	v.SetDefault("gdpr.tcf2.purpose6.enforce_vendors", true)
+	v.SetDefault("gdpr.tcf2.purpose7.enforce_vendors", true)
+	v.SetDefault("gdpr.tcf2.purpose8.enforce_vendors", true)
+	v.SetDefault("gdpr.tcf2.purpose9.enforce_vendors", true)
+	v.SetDefault("gdpr.tcf2.purpose10.enforce_vendors", true)
 	v.SetDefault("gdpr.tcf2.purpose1.vendor_exceptions", []openrtb_ext.BidderName{})
 	v.SetDefault("gdpr.tcf2.purpose2.vendor_exceptions", []openrtb_ext.BidderName{})
 	v.SetDefault("gdpr.tcf2.purpose3.vendor_exceptions", []openrtb_ext.BidderName{})
diff --git a/endpoints/openrtb2/amp_auction.go b/endpoints/openrtb2/amp_auction.go
index 862311da..6b4fa689 100644
--- a/endpoints/openrtb2/amp_auction.go
+++ b/endpoints/openrtb2/amp_auction.go
@@ -310,7 +310,7 @@ func (deps *endpointDeps) parseAmpRequest(httpRequest *http.Request) (req *openr
 
 	// At this point, we should have a valid request that definitely has Targeting and Cache turned on
 
-	e = deps.validateRequest(req)
+	e = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: req})
 	errs = append(errs, e...)
 	return
 }
diff --git a/endpoints/openrtb2/auction.go b/endpoints/openrtb2/auction.go
index c9f2bbdb..a6b3479a 100644
--- a/endpoints/openrtb2/auction.go
+++ b/endpoints/openrtb2/auction.go
@@ -174,10 +174,17 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 		return
 	}
 
+	// rebuild/resync the request in the request wrapper.
+	if err := req.RebuildRequest(); err != nil {
+		errL = append(errL, err)
+		writeError(errL, w, &labels)
+		return
+	}
+
 	secGPC := r.Header.Get("Sec-GPC")
 
 	auctionRequest := exchange.AuctionRequest{
-		BidRequest:                 req,
+		BidRequest:                 req.BidRequest,
 		Account:                    *account,
 		UserSyncs:                  usersyncs,
 		RequestType:                labels.RType,
@@ -188,7 +195,7 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 	}
 
 	response, err := deps.ex.HoldAuction(ctx, auctionRequest, nil)
-	ao.Request = req
+	ao.Request = req.BidRequest
 	ao.Response = response
 	ao.Account = account
 	if err != nil {
@@ -227,8 +234,9 @@ func (deps *endpointDeps) Auction(w http.ResponseWriter, r *http.Request, _ http
 // possible, it will return errors with messages that suggest improvements.
 //
 // If the errors list has at least one element, then no guarantees are made about the returned request.
-func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb2.BidRequest, errs []error) {
-	req = &openrtb2.BidRequest{}
+func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb_ext.RequestWrapper, errs []error) {
+	req = &openrtb_ext.RequestWrapper{}
+	req.BidRequest = &openrtb2.BidRequest{}
 	errs = nil
 
 	// Pull the request body into a buffer, so we have it for later usage.
@@ -258,20 +266,20 @@ func (deps *endpointDeps) parseRequest(httpRequest *http.Request) (req *openrtb2
 		return
 	}
 
-	if err := json.Unmarshal(requestJson, req); err != nil {
+	if err := json.Unmarshal(requestJson, req.BidRequest); err != nil {
 		errs = []error{err}
 		return
 	}
 
 	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
-	deps.setFieldsImplicitly(httpRequest, req)
+	deps.setFieldsImplicitly(httpRequest, req.BidRequest)
 
 	if err := processInterstitials(req); err != nil {
 		errs = []error{err}
 		return
 	}
 
-	lmt.ModifyForIOS(req)
+	lmt.ModifyForIOS(req.BidRequest)
 
 	errL := deps.validateRequest(req)
 	if len(errL) > 0 {
@@ -296,7 +304,7 @@ func parseTimeout(requestJson []byte, defaultTimeout time.Duration) time.Duratio
 	return defaultTimeout
 }
 
-func (deps *endpointDeps) validateRequest(req *openrtb2.BidRequest) []error {
+func (deps *endpointDeps) validateRequest(req *openrtb_ext.RequestWrapper) []error {
 	errL := []error{}
 	if req.ID == "" {
 		return []error{errors.New("request missing required field: \"id\"")}
@@ -318,34 +326,39 @@ func (deps *endpointDeps) validateRequest(req *openrtb2.BidRequest) []error {
 	// If automatically filling source TID is enabled then validate that
 	// source.TID exists and If it doesn't, fill it with a randomly generated UUID
 	if deps.cfg.AutoGenSourceTID {
-		if err := validateAndFillSourceTID(req); err != nil {
+		if err := validateAndFillSourceTID(req.BidRequest); err != nil {
 			return []error{err}
 		}
 	}
 
 	var aliases map[string]string
-	if bidExt, err := deps.parseBidExt(req.Ext); err != nil {
+	reqExt, err := req.GetRequestExt()
+	if err != nil {
+		return []error{fmt.Errorf("request.ext is invalid: %v", err)}
+	}
+	reqPrebid := reqExt.GetPrebid()
+	if err := deps.parseBidExt(req); err != nil {
 		return []error{err}
-	} else if bidExt != nil {
-		aliases = bidExt.Prebid.Aliases
+	} else if reqPrebid != nil {
+		aliases = reqPrebid.Aliases
 
 		if err := deps.validateAliases(aliases); err != nil {
 			return []error{err}
 		}
 
-		if err := deps.validateBidAdjustmentFactors(bidExt.Prebid.BidAdjustmentFactors, aliases); err != nil {
+		if err := deps.validateBidAdjustmentFactors(reqPrebid.BidAdjustmentFactors, aliases); err != nil {
 			return []error{err}
 		}
 
-		if err := validateSChains(bidExt); err != nil {
+		if err := validateSChains(reqPrebid.SChains); err != nil {
 			return []error{err}
 		}
 
-		if err := deps.validateEidPermissions(bidExt, aliases); err != nil {
+		if err := deps.validateEidPermissions(reqPrebid.Data, aliases); err != nil {
 			return []error{err}
 		}
 
-		if err := validateCustomRates(bidExt.Prebid.CurrencyConversions); err != nil {
+		if err := validateCustomRates(reqPrebid.CurrencyConversions); err != nil {
 			return []error{err}
 		}
 	}
@@ -354,19 +367,19 @@ func (deps *endpointDeps) validateRequest(req *openrtb2.BidRequest) []error {
 		return append(errL, errors.New("request.site or request.app must be defined, but not both."))
 	}
 
-	if err := deps.validateSite(req.Site); err != nil {
+	if err := deps.validateSite(req); err != nil {
 		return append(errL, err)
 	}
 
-	if err := deps.validateApp(req.App); err != nil {
+	if err := deps.validateApp(req); err != nil {
 		return append(errL, err)
 	}
 
-	if err := deps.validateUser(req.User, aliases); err != nil {
+	if err := deps.validateUser(req, aliases); err != nil {
 		return append(errL, err)
 	}
 
-	if err := validateRegs(req.Regs); err != nil {
+	if err := validateRegs(req); err != nil {
 		return append(errL, err)
 	}
 
@@ -374,17 +387,18 @@ func (deps *endpointDeps) validateRequest(req *openrtb2.BidRequest) []error {
 		return append(errL, err)
 	}
 
-	if ccpaPolicy, err := ccpa.ReadFromRequest(req); err != nil {
+	if ccpaPolicy, err := ccpa.ReadFromRequestWrapper(req); err != nil {
 		return append(errL, err)
 	} else if _, err := ccpaPolicy.Parse(exchange.GetValidBidders(aliases)); err != nil {
 		if _, invalidConsent := err.(*errortypes.Warning); invalidConsent {
 			errL = append(errL, &errortypes.Warning{
 				Message:     fmt.Sprintf("CCPA consent is invalid and will be ignored. (%v)", err),
 				WarningCode: errortypes.InvalidPrivacyConsentWarningCode})
-			consentWriter := ccpa.ConsentWriter{Consent: ""}
-			if err := consentWriter.Write(req); err != nil {
-				return append(errL, fmt.Errorf("Unable to remove invalid CCPA consent from the request. (%v)", err))
+			regsExt, err := req.GetRegExt()
+			if err != nil {
+				return append(errL, err)
 			}
+			regsExt.SetUSPrivacy("")
 		} else {
 			return append(errL, err)
 		}
@@ -437,8 +451,8 @@ func (deps *endpointDeps) validateBidAdjustmentFactors(adjustmentFactors map[str
 	return nil
 }
 
-func validateSChains(req *openrtb_ext.ExtRequest) error {
-	_, err := exchange.BidderToPrebidSChains(req)
+func validateSChains(sChains []*openrtb_ext.ExtRequestPrebidSChain) error {
+	_, err := exchange.BidderToPrebidSChains(sChains)
 	return err
 }
 
@@ -466,13 +480,13 @@ func validateCustomRates(bidReqCurrencyRates *openrtb_ext.ExtRequestCurrency) er
 	return nil
 }
 
-func (deps *endpointDeps) validateEidPermissions(req *openrtb_ext.ExtRequest, aliases map[string]string) error {
-	if req == nil || req.Prebid.Data == nil {
+func (deps *endpointDeps) validateEidPermissions(prebid *openrtb_ext.ExtRequestPrebidData, aliases map[string]string) error {
+	if prebid == nil {
 		return nil
 	}
 
-	uniqueSources := make(map[string]struct{}, len(req.Prebid.Data.EidPermissions))
-	for i, eid := range req.Prebid.Data.EidPermissions {
+	uniqueSources := make(map[string]struct{}, len(prebid.EidPermissions))
+	for i, eid := range prebid.EidPermissions {
 		if len(eid.Source) == 0 {
 			return fmt.Errorf(`request.ext.prebid.data.eidpermissions[%d] missing required field: "source"`, i)
 		}
@@ -1045,15 +1059,11 @@ func isBidderToValidate(bidder string) bool {
 	}
 }
 
-func (deps *endpointDeps) parseBidExt(ext json.RawMessage) (*openrtb_ext.ExtRequest, error) {
-	if len(ext) < 1 {
-		return nil, nil
+func (deps *endpointDeps) parseBidExt(req *openrtb_ext.RequestWrapper) error {
+	if _, err := req.GetRequestExt(); err != nil {
+		return fmt.Errorf("request.ext is invalid: %v", err)
 	}
-	var tmpExt openrtb_ext.ExtRequest
-	if err := json.Unmarshal(ext, &tmpExt); err != nil {
-		return nil, fmt.Errorf("request.ext is invalid: %v", err)
-	}
-	return &tmpExt, nil
+	return nil
 }
 
 func (deps *endpointDeps) validateAliases(aliases map[string]string) error {
@@ -1073,120 +1083,112 @@ func (deps *endpointDeps) validateAliases(aliases map[string]string) error {
 	return nil
 }
 
-func (deps *endpointDeps) validateSite(site *openrtb2.Site) error {
-	if site == nil {
+func (deps *endpointDeps) validateSite(req *openrtb_ext.RequestWrapper) error {
+	if req.Site == nil {
 		return nil
 	}
 
-	if site.ID == "" && site.Page == "" {
+	if req.Site.ID == "" && req.Site.Page == "" {
 		return errors.New("request.site should include at least one of request.site.id or request.site.page.")
 	}
-	if len(site.Ext) > 0 {
-		var s openrtb_ext.ExtSite
-		if err := json.Unmarshal(site.Ext, &s); err != nil {
-			return err
-		}
+	siteExt, err := req.GetSiteExt()
+	if err != nil {
+		return err
+	}
+	siteAmp := siteExt.GetAmp()
+	if siteAmp < 0 || siteAmp > 1 {
+		return errors.New(`request.site.ext.amp must be either 1, 0, or undefined`)
 	}
 
 	return nil
 }
 
-func (deps *endpointDeps) validateApp(app *openrtb2.App) error {
-	if app == nil {
+func (deps *endpointDeps) validateApp(req *openrtb_ext.RequestWrapper) error {
+	if req.App == nil {
 		return nil
 	}
 
-	if app.ID != "" {
-		if _, found := deps.cfg.BlacklistedAppMap[app.ID]; found {
-			return &errortypes.BlacklistedApp{Message: fmt.Sprintf("Prebid-server does not process requests from App ID: %s", app.ID)}
-		}
-	}
-
-	if len(app.Ext) > 0 {
-		var a openrtb_ext.ExtApp
-		if err := json.Unmarshal(app.Ext, &a); err != nil {
-			return err
+	if req.App.ID != "" {
+		if _, found := deps.cfg.BlacklistedAppMap[req.App.ID]; found {
+			return &errortypes.BlacklistedApp{Message: fmt.Sprintf("Prebid-server does not process requests from App ID: %s", req.App.ID)}
 		}
 	}
 
-	return nil
+	_, err := req.GetAppExt()
+	return err
 }
 
-func (deps *endpointDeps) validateUser(user *openrtb2.User, aliases map[string]string) error {
-	if user == nil {
-		return nil
-	}
-
+func (deps *endpointDeps) validateUser(req *openrtb_ext.RequestWrapper, aliases map[string]string) error {
 	// The following fields were previously uints in the OpenRTB library we use, but have
 	// since been changed to ints. We decided to maintain the non-negative check.
-	if user.Geo != nil && user.Geo.Accuracy < 0 {
-		return errors.New("request.user.geo.accuracy must be a positive number")
-	}
-
-	if user.Ext != nil {
-		// Creating ExtUser object
-		var userExt openrtb_ext.ExtUser
-		if err := json.Unmarshal(user.Ext, &userExt); err == nil {
-			// Check if the buyeruids are valid
-			if userExt.Prebid != nil {
-				if len(userExt.Prebid.BuyerUIDs) < 1 {
-					return errors.New(`request.user.ext.prebid requires a "buyeruids" property with at least one ID defined. If none exist, then request.user.ext.prebid should not be defined.`)
-				}
-				for bidderName := range userExt.Prebid.BuyerUIDs {
-					if _, ok := deps.bidderMap[bidderName]; !ok {
-						if _, ok := aliases[bidderName]; !ok {
-							return fmt.Errorf("request.user.ext.%s is neither a known bidder name nor an alias in request.ext.prebid.aliases.", bidderName)
-						}
-					}
+	if req != nil && req.BidRequest != nil && req.User != nil {
+		if req.User.Geo != nil && req.User.Geo.Accuracy < 0 {
+			return errors.New("request.user.geo.accuracy must be a positive number")
+		}
+	}
+
+	userExt, err := req.GetUserExt()
+	if err != nil {
+		return fmt.Errorf("request.user.ext object is not valid: %v", err)
+	}
+	// Check if the buyeruids are valid
+	prebid := userExt.GetPrebid()
+	if prebid != nil {
+		if len(prebid.BuyerUIDs) < 1 {
+			return errors.New(`request.user.ext.prebid requires a "buyeruids" property with at least one ID defined. If none exist, then request.user.ext.prebid should not be defined.`)
+		}
+		for bidderName := range prebid.BuyerUIDs {
+			if _, ok := deps.bidderMap[bidderName]; !ok {
+				if _, ok := aliases[bidderName]; !ok {
+					return fmt.Errorf("request.user.ext.%s is neither a known bidder name nor an alias in request.ext.prebid.aliases.", bidderName)
 				}
 			}
-			// Check Universal User ID
-			if userExt.Eids != nil {
-				if len(userExt.Eids) == 0 {
-					return fmt.Errorf("request.user.ext.eids must contain at least one element or be undefined")
-				}
-				uniqueSources := make(map[string]struct{}, len(userExt.Eids))
-				for eidIndex, eid := range userExt.Eids {
-					if eid.Source == "" {
-						return fmt.Errorf("request.user.ext.eids[%d] missing required field: \"source\"", eidIndex)
-					}
-					if _, ok := uniqueSources[eid.Source]; ok {
-						return fmt.Errorf("request.user.ext.eids must contain unique sources")
-					}
-					uniqueSources[eid.Source] = struct{}{}
+		}
+	}
+	// Check Universal User ID
+	eids := userExt.GetEid()
+	if eids != nil {
+		if len(*eids) == 0 {
+			return errors.New("request.user.ext.eids must contain at least one element or be undefined")
+		}
+		uniqueSources := make(map[string]struct{}, len(*eids))
+		for eidIndex, eid := range *eids {
+			if eid.Source == "" {
+				return fmt.Errorf("request.user.ext.eids[%d] missing required field: \"source\"", eidIndex)
+			}
+			if _, ok := uniqueSources[eid.Source]; ok {
+				return errors.New("request.user.ext.eids must contain unique sources")
+			}
+			uniqueSources[eid.Source] = struct{}{}
 
-					if eid.ID == "" && eid.Uids == nil {
-						return fmt.Errorf("request.user.ext.eids[%d] must contain either \"id\" or \"uids\" field", eidIndex)
-					}
-					if eid.ID == "" {
-						if len(eid.Uids) == 0 {
-							return fmt.Errorf("request.user.ext.eids[%d].uids must contain at least one element or be undefined", eidIndex)
-						}
-						for uidIndex, uid := range eid.Uids {
-							if uid.ID == "" {
-								return fmt.Errorf("request.user.ext.eids[%d].uids[%d] missing required field: \"id\"", eidIndex, uidIndex)
-							}
-						}
+			if eid.ID == "" && eid.Uids == nil {
+				return fmt.Errorf("request.user.ext.eids[%d] must contain either \"id\" or \"uids\" field", eidIndex)
+			}
+			if eid.ID == "" {
+				if len(eid.Uids) == 0 {
+					return fmt.Errorf("request.user.ext.eids[%d].uids must contain at least one element or be undefined", eidIndex)
+				}
+				for uidIndex, uid := range eid.Uids {
+					if uid.ID == "" {
+						return fmt.Errorf("request.user.ext.eids[%d].uids[%d] missing required field: \"id\"", eidIndex, uidIndex)
 					}
 				}
 			}
-		} else {
-			return fmt.Errorf("request.user.ext object is not valid: %v", err)
 		}
 	}
 
 	return nil
 }
 
-func validateRegs(regs *openrtb2.Regs) error {
-	if regs != nil && len(regs.Ext) > 0 {
-		var regsExt openrtb_ext.ExtRegs
-		if err := json.Unmarshal(regs.Ext, &regsExt); err != nil {
-			return fmt.Errorf("request.regs.ext is invalid: %v", err)
-		}
-		if regsExt.GDPR != nil && (*regsExt.GDPR < 0 || *regsExt.GDPR > 1) {
-			return errors.New("request.regs.ext.gdpr must be either 0 or 1.")
-		}
+func validateRegs(req *openrtb_ext.RequestWrapper) error {
+	regsExt, err := req.GetRegExt()
+	if err != nil {
+		return fmt.Errorf("request.regs.ext is invalid: %v", err)
+	}
+	regExt := regsExt.GetExt()
+	gdprJSON, hasGDPR := regExt["gdpr"]
+	if hasGDPR && (string(gdprJSON) != "0" && string(gdprJSON) != "1") {
+		return errors.New("request.regs.ext.gdpr must be either 0 or 1.")
 	}
 	return nil
 }
diff --git a/endpoints/openrtb2/interstitial.go b/endpoints/openrtb2/interstitial.go
index 1aa2a7fc..359bae11 100644
--- a/endpoints/openrtb2/interstitial.go
+++ b/endpoints/openrtb2/interstitial.go
@@ -1,7 +1,6 @@
 package openrtb2
 
 import (
-	"encoding/json"
 	"fmt"
 
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
@@ -10,26 +9,27 @@ import (
 	"github.com/prebid/prebid-server/openrtb_ext"
 )
 
-func processInterstitials(req *openrtb2.BidRequest) error {
-	var devExt openrtb_ext.ExtDevice
+func processInterstitials(req *openrtb_ext.RequestWrapper) error {
 	unmarshalled := true
 	for i := range req.Imp {
 		if req.Imp[i].Instl == 1 {
+			var prebid *openrtb_ext.ExtDevicePrebid
 			if unmarshalled {
 				if req.Device.Ext == nil {
 					// No special interstitial support requested, so bail as there is nothing to do
 					return nil
 				}
-				err := json.Unmarshal(req.Device.Ext, &devExt)
+				deviceExt, err := req.GetDeviceExt()
 				if err != nil {
 					return err
 				}
-				if devExt.Prebid.Interstitial == nil {
+				prebid = deviceExt.GetPrebid()
+				if prebid.Interstitial == nil {
 					// No special interstitial support requested, so bail as there is nothing to do
 					return nil
 				}
 			}
-			err := processInterstitialsForImp(&req.Imp[i], &devExt, req.Device)
+			err := processInterstitialsForImp(&req.Imp[i], prebid, req.Device)
 			if err != nil {
 				return err
 			}
@@ -38,7 +38,7 @@ func processInterstitials(req *openrtb2.BidRequest) error {
 	return nil
 }
 
-func processInterstitialsForImp(imp *openrtb2.Imp, devExt *openrtb_ext.ExtDevice, device *openrtb2.Device) error {
+func processInterstitialsForImp(imp *openrtb2.Imp, devExtPrebid *openrtb_ext.ExtDevicePrebid, device *openrtb2.Device) error {
 	var maxWidth, maxHeight, minWidth, minHeight int64
 	if imp.Banner == nil {
 		// custom interstitial support is only available for banner requests.
@@ -56,8 +56,8 @@ func processInterstitialsForImp(imp *openrtb2.Imp, devExt *openrtb_ext.ExtDevice
 		maxWidth = device.W
 		maxHeight = device.H
 	}
-	minWidth = (maxWidth * int64(devExt.Prebid.Interstitial.MinWidthPerc)) / 100
-	minHeight = (maxHeight * int64(devExt.Prebid.Interstitial.MinHeightPerc)) / 100
+	minWidth = (maxWidth * devExtPrebid.Interstitial.MinWidthPerc) / 100
+	minHeight = (maxHeight * devExtPrebid.Interstitial.MinHeightPerc) / 100
 	imp.Banner.Format = genInterstitialFormat(minWidth, maxWidth, minHeight, maxHeight)
 	if len(imp.Banner.Format) == 0 {
 		return &errortypes.BadInput{Message: fmt.Sprintf("Unable to set interstitial size list for Imp id=%s (No valid sizes between %dx%d and %dx%d)", imp.ID, minWidth, minHeight, maxWidth, maxHeight)}
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/invalid-source.json b/endpoints/openrtb2/sample-requests/invalid-whole/invalid-source.json
index 8385f924..5aa7fd4d 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/invalid-source.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/invalid-source.json
@@ -39,5 +39,5 @@
     ]
   },
   "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: json: cannot unmarshal object into Go struct field ExtAppPrebid.prebid.source of type string"
+  "expectedErrorMessage": "Invalid request: json: cannot unmarshal object into Go struct field ExtAppPrebid.source of type string"
 }  
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json
index afdabdab..4a315911 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-gdpr-string.json
@@ -44,5 +44,5 @@
     }
   },
   "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: request.regs.ext is invalid: json: cannot unmarshal string into Go struct field ExtRegs.gdpr of type int8\n"
+  "expectedErrorMessage": "Invalid request: request.regs.ext.gdpr must be either 0 or 1.\n"
 }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json
index a8e94008..ab44e3e2 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/regs-ext-malformed.json
@@ -42,5 +42,5 @@
     }
   },
   "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: request.regs.ext is invalid: json: cannot unmarshal string into Go value of type openrtb_ext.ExtRegs\n"
+  "expectedErrorMessage": "Invalid request: request.regs.ext is invalid: json: cannot unmarshal string into Go value of type map[string]json.RawMessage\n"
 }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json
index b61be105..a26db8a5 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-ext-consent-int.json
@@ -46,5 +46,5 @@
     }
   },
   "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: request.user.ext object is not valid: json: cannot unmarshal number into Go struct field ExtUser.consent of type string\n"
+  "expectedErrorMessage": "Invalid request: request.user.ext object is not valid: json: cannot unmarshal number into Go value of type string\n"
 }
diff --git a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json
index 08eed44b..c4646550 100644
--- a/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json
+++ b/endpoints/openrtb2/sample-requests/invalid-whole/user-gdpr-consent-invalid.json
@@ -41,5 +41,5 @@
     }
   },
   "expectedReturnCode": 400,
-  "expectedErrorMessage": "Invalid request: request.user.ext object is not valid: json: cannot unmarshal number into Go struct field ExtUser.consent of type string"
+  "expectedErrorMessage": "Invalid request: request.user.ext object is not valid: json: cannot unmarshal number into Go value of type string"
 }
diff --git a/endpoints/openrtb2/video_auction.go b/endpoints/openrtb2/video_auction.go
index 0af3ba51..227f6c4a 100644
--- a/endpoints/openrtb2/video_auction.go
+++ b/endpoints/openrtb2/video_auction.go
@@ -241,7 +241,7 @@ func (deps *endpointDeps) VideoAuctionEndpoint(w http.ResponseWriter, r *http.Re
 	// Populate any "missing" OpenRTB fields with info from other sources, (e.g. HTTP request headers).
 	deps.setFieldsImplicitly(r, bidReq) // move after merge
 
-	errL = deps.validateRequest(bidReq)
+	errL = deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: bidReq})
 	if errortypes.ContainsFatalError(errL) {
 		handleError(&labels, w, errL, &vo, &debugLog)
 		return
diff --git a/exchange/adapter_builders.go b/exchange/adapter_builders.go
index 9adc9ebc..1d91fadd 100755
--- a/exchange/adapter_builders.go
+++ b/exchange/adapter_builders.go
@@ -80,6 +80,7 @@ import (
 	"github.com/prebid/prebid-server/adapters/nobid"
 	"github.com/prebid/prebid-server/adapters/onetag"
 	"github.com/prebid/prebid-server/adapters/openx"
+	"github.com/prebid/prebid-server/adapters/operaads"
 	"github.com/prebid/prebid-server/adapters/orbidder"
 	"github.com/prebid/prebid-server/adapters/outbrain"
 	"github.com/prebid/prebid-server/adapters/pangle"
@@ -206,6 +207,7 @@ func newAdapterBuilders() map[openrtb_ext.BidderName]adapters.Builder {
 		openrtb_ext.BidderNoBid:             nobid.Builder,
 		openrtb_ext.BidderOneTag:            onetag.Builder,
 		openrtb_ext.BidderOpenx:             openx.Builder,
+		openrtb_ext.BidderOperaads:          operaads.Builder,
 		openrtb_ext.BidderOrbidder:          orbidder.Builder,
 		openrtb_ext.BidderOutbrain:          outbrain.Builder,
 		openrtb_ext.BidderPangle:            pangle.Builder,
diff --git a/exchange/utils.go b/exchange/utils.go
index 0befeacd..12015246 100644
--- a/exchange/utils.go
+++ b/exchange/utils.go
@@ -28,18 +28,16 @@ var integrationTypeMap = map[metrics.RequestType]config.IntegrationType{
 
 const unknownBidder string = ""
 
-func BidderToPrebidSChains(req *openrtb_ext.ExtRequest) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) {
+func BidderToPrebidSChains(sChains []*openrtb_ext.ExtRequestPrebidSChain) (map[string]*openrtb_ext.ExtRequestPrebidSChainSChain, error) {
 	bidderToSChains := make(map[string]*openrtb_ext.ExtRequestPrebidSChainSChain)
 
-	if req != nil {
-		for _, schainWrapper := range req.Prebid.SChains {
-			for _, bidder := range schainWrapper.Bidders {
-				if _, present := bidderToSChains[bidder]; present {
-					return nil, fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder %s; "+
-						"it must contain no more than one per bidder.", bidder)
-				} else {
-					bidderToSChains[bidder] = &schainWrapper.SChain
-				}
+	for _, schainWrapper := range sChains {
+		for _, bidder := range schainWrapper.Bidders {
+			if _, present := bidderToSChains[bidder]; present {
+				return nil, fmt.Errorf("request.ext.prebid.schains contains multiple schains for bidder %s; "+
+					"it must contain no more than one per bidder.", bidder)
+			} else {
+				bidderToSChains[bidder] = &schainWrapper.SChain
 			}
 		}
 	}
@@ -178,7 +176,8 @@ func ccpaEnabled(account *config.Account, privacyConfig config.Privacy, requestT
 }
 
 func extractCCPA(orig *openrtb2.BidRequest, privacyConfig config.Privacy, account *config.Account, aliases map[string]string, requestType config.IntegrationType) (privacy.PolicyEnforcer, error) {
-	ccpaPolicy, err := ccpa.ReadFromRequest(orig)
+	// Quick extra wrapper until RequestWrapper makes its way into CleanRequests
+	ccpaPolicy, err := ccpa.ReadFromRequestWrapper(&openrtb_ext.RequestWrapper{BidRequest: orig})
 	if err != nil {
 		return privacy.NilPolicyEnforcer{}, err
 	}
@@ -217,9 +216,12 @@ func getAuctionBidderRequests(req AuctionRequest,
 
 	var sChainsByBidder map[string]*openrtb_ext.ExtRequestPrebidSChainSChain
 
-	sChainsByBidder, err = BidderToPrebidSChains(requestExt)
-	if err != nil {
-		return nil, []error{err}
+	// Quick extra wrapper until RequestWrapper makes its way into CleanRequests
+	if requestExt != nil {
+		sChainsByBidder, err = BidderToPrebidSChains(requestExt.Prebid.SChains)
+		if err != nil {
+			return nil, []error{err}
+		}
 	}
 
 	reqExt, err := getExtJson(req.BidRequest, requestExt)
diff --git a/gdpr/impl.go b/gdpr/impl.go
index ac0bfefd..5f7e3e73 100644
--- a/gdpr/impl.go
+++ b/gdpr/impl.go
@@ -210,8 +210,8 @@ func (p *permissionsImpl) checkPurpose(consent tcf2.ConsentMetadata, vendor api.
 		return true
 	}
 
-	purposeAllowed := consent.PurposeAllowed(purpose) && (weakVendorEnforcement || (vendor.Purpose(purpose) && consent.VendorConsent(vendorID)))
-	legitInterest := consent.PurposeLITransparency(purpose) && (weakVendorEnforcement || (vendor.LegitimateInterest(purpose) && consent.VendorLegitInterest(vendorID)))
+	purposeAllowed := p.consentEstablished(consent, vendor, vendorID, purpose, weakVendorEnforcement)
+	legitInterest := p.legitInterestEstablished(consent, vendor, vendorID, purpose, weakVendorEnforcement)
 
 	if consent.CheckPubRestriction(uint8(purpose), pubRestrictRequireConsent, vendorID) {
 		return purposeAllowed
@@ -224,6 +224,38 @@ func (p *permissionsImpl) checkPurpose(consent tcf2.ConsentMetadata, vendor api.
 	return purposeAllowed || legitInterest
 }
 
+func (p *permissionsImpl) consentEstablished(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose consentconstants.Purpose, weakVendorEnforcement bool) bool {
+	if !consent.PurposeAllowed(purpose) {
+		return false
+	}
+	if weakVendorEnforcement {
+		return true
+	}
+	if !p.purposeConfigs[purpose].EnforceVendors {
+		return true
+	}
+	if vendor.Purpose(purpose) && consent.VendorConsent(vendorID) {
+		return true
+	}
+	return false
+}
+
+func (p *permissionsImpl) legitInterestEstablished(consent tcf2.ConsentMetadata, vendor api.Vendor, vendorID uint16, purpose consentconstants.Purpose, weakVendorEnforcement bool) bool {
+	if !consent.PurposeLITransparency(purpose) {
+		return false
+	}
+	if weakVendorEnforcement {
+		return true
+	}
+	if !p.purposeConfigs[purpose].EnforceVendors {
+		return true
+	}
+	if vendor.LegitimateInterest(purpose) && consent.VendorLegitInterest(vendorID) {
+		return true
+	}
+	return false
+}
+
 func (p *permissionsImpl) parseVendor(ctx context.Context, vendorID uint16, consent string) (parsedConsent api.VendorConsents, vendor api.Vendor, err error) {
 	parsedConsent, err = vendorconsent.ParseString(consent)
 	if err != nil {
diff --git a/go.sum b/go.sum
index 4cb5863d..df2bfb9b 100644
--- a/go.sum
+++ b/go.sum
@@ -80,9 +80,11 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI
 github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
 github.com/onsi/ginkgo v1.16.1 h1:foqVmeWDD6yYpK+Yz3fHyNIxFYNxswxqNFjSKe+vI54=
 github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
 github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
 github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug=
diff --git a/openrtb_ext/bidders.go b/openrtb_ext/bidders.go
index 780b75f6..8a589bd1 100644
--- a/openrtb_ext/bidders.go
+++ b/openrtb_ext/bidders.go
@@ -153,6 +153,7 @@ const (
 	BidderNoBid             BidderName = "nobid"
 	BidderOneTag            BidderName = "onetag"
 	BidderOpenx             BidderName = "openx"
+	BidderOperaads          BidderName = "operaads"
 	BidderOrbidder          BidderName = "orbidder"
 	BidderOutbrain          BidderName = "outbrain"
 	BidderPangle            BidderName = "pangle"
@@ -277,6 +278,7 @@ func CoreBidderNames() []BidderName {
 		BidderNoBid,
 		BidderOneTag,
 		BidderOpenx,
+		BidderOperaads,
 		BidderOrbidder,
 		BidderOutbrain,
 		BidderPangle,
diff --git a/openrtb_ext/device.go b/openrtb_ext/device.go
index cc06f380..1e860556 100644
--- a/openrtb_ext/device.go
+++ b/openrtb_ext/device.go
@@ -70,8 +70,8 @@ type ExtDevicePrebid struct {
 
 // ExtDeviceInt defines the contract for bidrequest.device.ext.prebid.interstitial
 type ExtDeviceInt struct {
-	MinWidthPerc  uint64 `json:"minwidtheperc"`
-	MinHeightPerc uint64 `json:"minheightperc"`
+	MinWidthPerc  int64 `json:"minwidtheperc"`
+	MinHeightPerc int64 `json:"minheightperc"`
 }
 
 func (edi *ExtDeviceInt) UnmarshalJSON(b []byte) error {
@@ -85,7 +85,7 @@ func (edi *ExtDeviceInt) UnmarshalJSON(b []byte) error {
 		if err != nil || perc < 0 || perc > 100 {
 			return &errortypes.BadInput{Message: "request.device.ext.prebid.interstitial.minwidthperc must be a number between 0 and 100"}
 		}
-		edi.MinWidthPerc = uint64(perc)
+		edi.MinWidthPerc = int64(perc)
 	}
 	if value, dataType, _, _ := jsonparser.Get(b, "minheightperc"); dataType != jsonparser.Number {
 		return &errortypes.BadInput{Message: "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"}
@@ -94,7 +94,7 @@ func (edi *ExtDeviceInt) UnmarshalJSON(b []byte) error {
 		if err != nil || perc < 0 || perc > 100 {
 			return &errortypes.BadInput{Message: "request.device.ext.prebid.interstitial.minheightperc must be a number between 0 and 100"}
 		}
-		edi.MinHeightPerc = uint64(perc)
+		edi.MinHeightPerc = int64(perc)
 	}
 	return nil
 }
diff --git a/openrtb_ext/imp_operaads.go b/openrtb_ext/imp_operaads.go
new file mode 100644
index 00000000..99ccd7c4
--- /dev/null
+++ b/openrtb_ext/imp_operaads.go
@@ -0,0 +1,7 @@
+package openrtb_ext
+
+type ImpExtOperaads struct {
+	PlacementID string `json:"placementId"`
+	EndpointID  string `json:"endpointId"`
+	PublisherID string `json:"publisherId"`
+}
diff --git a/openrtb_ext/imp_sharethrough.go b/openrtb_ext/imp_sharethrough.go
index 3f378033..7c3f1f67 100644
--- a/openrtb_ext/imp_sharethrough.go
+++ b/openrtb_ext/imp_sharethrough.go
@@ -1,13 +1,18 @@
 package openrtb_ext
 
-type ExtImpSharethrough struct {
-	Pkey       string  `json:"pkey"`
-	Iframe     bool    `json:"iframe"`
-	IframeSize []int   `json:"iframeSize"`
-	BidFloor   float64 `json:"bidfloor"`
+type ExtData struct {
+	PBAdSlot string `json:"pbadslot"`
 }
 
 // ExtImpSharethrough defines the contract for bidrequest.imp[i].ext.sharethrough
+type ExtImpSharethrough struct {
+	Pkey       string   `json:"pkey"`
+	Iframe     bool     `json:"iframe"`
+	IframeSize []int    `json:"iframeSize"`
+	BidFloor   float64  `json:"bidfloor"`
+	Data       *ExtData `json:"data,omitempty"`
+}
+
 type ExtImpSharethroughResponse struct {
 	AdServerRequestID string                       `json:"adserverRequestId"`
 	BidID             string                       `json:"bidId"`
diff --git a/openrtb_ext/request_wrapper.go b/openrtb_ext/request_wrapper.go
new file mode 100644
index 00000000..276f8f5e
--- /dev/null
+++ b/openrtb_ext/request_wrapper.go
@@ -0,0 +1,787 @@
+package openrtb_ext
+
+import (
+	"encoding/json"
+	"errors"
+
+	"github.com/mxmCherry/openrtb/v15/openrtb2"
+)
+
+// RequestWrapper wraps the OpenRTB request to provide a storage location for unmarshalled ext fields, so they
+// will not need to be unmarshalled multiple times.
+//
+// To start with, the wrapper can be created for a request 'req' via:
+// reqWrapper := openrtb_ext.RequestWrapper{BidRequest: req}
+//
+// In order to access an object's ext field, fetch it via:
+// userExt, err := reqWrapper.GetUserExt()
+// or other Get method as appropriate.
+//
+// To read or write values, use the Ext objects Get and Set methods. If you need to write to a field that has its own Set
+// method, use that to set the value rather than using SetExt() with that change done in the map; when rewritting the
+// ext JSON the code will overwrite the the values in the map with the values stored in the seperate fields.
+//
+// userPrebid := userExt.GetPrebid()
+// userExt.SetConsent(consentString)
+//
+// The GetExt() and SetExt() should only be used to access fields that have not already been resolved in the object.
+// Using SetExt() at all is a strong hint that the ext object should be extended to support the new fields being set
+// in the map.
+//
+// NOTE: The RequestWrapper methods (particularly the ones calling (un)Marshal are not thread safe)
+
+type RequestWrapper struct {
+	*openrtb2.BidRequest
+	userExt    *UserExt
+	deviceExt  *DeviceExt
+	requestExt *RequestExt
+	appExt     *AppExt
+	regExt     *RegExt
+	siteExt    *SiteExt
+}
+
+func (rw *RequestWrapper) GetUserExt() (*UserExt, error) {
+	if rw.userExt != nil {
+		return rw.userExt, nil
+	}
+	rw.userExt = &UserExt{}
+	if rw.BidRequest == nil || rw.User == nil || rw.User.Ext == nil {
+		return rw.userExt, rw.userExt.unmarshal(json.RawMessage{})
+	}
+
+	return rw.userExt, rw.userExt.unmarshal(rw.User.Ext)
+}
+
+func (rw *RequestWrapper) GetDeviceExt() (*DeviceExt, error) {
+	if rw.deviceExt != nil {
+		return rw.deviceExt, nil
+	}
+	rw.deviceExt = &DeviceExt{}
+	if rw.BidRequest == nil || rw.Device == nil || rw.Device.Ext == nil {
+		return rw.deviceExt, rw.deviceExt.unmarshal(json.RawMessage{})
+	}
+	return rw.deviceExt, rw.deviceExt.unmarshal(rw.Device.Ext)
+}
+
+func (rw *RequestWrapper) GetRequestExt() (*RequestExt, error) {
+	if rw.requestExt != nil {
+		return rw.requestExt, nil
+	}
+	rw.requestExt = &RequestExt{}
+	if rw.BidRequest == nil || rw.Ext == nil {
+		return rw.requestExt, rw.requestExt.unmarshal(json.RawMessage{})
+	}
+	return rw.requestExt, rw.requestExt.unmarshal(rw.Ext)
+}
+
+func (rw *RequestWrapper) GetAppExt() (*AppExt, error) {
+	if rw.appExt != nil {
+		return rw.appExt, nil
+	}
+	rw.appExt = &AppExt{}
+	if rw.BidRequest == nil || rw.App == nil || rw.App.Ext == nil {
+		return rw.appExt, rw.appExt.unmarshal(json.RawMessage{})
+	}
+	return rw.appExt, rw.appExt.unmarshal(rw.App.Ext)
+}
+
+func (rw *RequestWrapper) GetRegExt() (*RegExt, error) {
+	if rw.regExt != nil {
+		return rw.regExt, nil
+	}
+	rw.regExt = &RegExt{}
+	if rw.BidRequest == nil || rw.Regs == nil || rw.Regs.Ext == nil {
+		return rw.regExt, rw.regExt.unmarshal(json.RawMessage{})
+	}
+	return rw.regExt, rw.regExt.unmarshal(rw.Regs.Ext)
+}
+
+func (rw *RequestWrapper) GetSiteExt() (*SiteExt, error) {
+	if rw.siteExt != nil {
+		return rw.siteExt, nil
+	}
+	rw.siteExt = &SiteExt{}
+	if rw.BidRequest == nil || rw.Site == nil || rw.Site.Ext == nil {
+		return rw.siteExt, rw.siteExt.unmarshal(json.RawMessage{})
+	}
+	return rw.siteExt, rw.siteExt.unmarshal(rw.Site.Ext)
+}
+
+func (rw *RequestWrapper) RebuildRequest() error {
+	if rw.BidRequest == nil {
+		return errors.New("Requestwrapper Sync called on a nil BidRequest")
+	}
+
+	if err := rw.rebuildUserExt(); err != nil {
+		return err
+	}
+	if err := rw.rebuildDeviceExt(); err != nil {
+		return err
+	}
+	if err := rw.rebuildRequestExt(); err != nil {
+		return err
+	}
+	if err := rw.rebuildAppExt(); err != nil {
+		return err
+	}
+	if err := rw.rebuildRegExt(); err != nil {
+		return err
+	}
+	if err := rw.rebuildSiteExt(); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (rw *RequestWrapper) rebuildUserExt() error {
+	if rw.BidRequest.User == nil && rw.userExt != nil && rw.userExt.Dirty() {
+		rw.User = &openrtb2.User{}
+	}
+	if rw.userExt != nil && rw.userExt.Dirty() {
+		userJson, err := rw.userExt.marshal()
+		if err != nil {
+			return err
+		}
+		rw.User.Ext = userJson
+	}
+	return nil
+}
+
+func (rw *RequestWrapper) rebuildDeviceExt() error {
+	if rw.Device == nil && rw.deviceExt != nil && rw.deviceExt.Dirty() {
+		rw.Device = &openrtb2.Device{}
+	}
+	if rw.deviceExt != nil && rw.deviceExt.Dirty() {
+		deviceJson, err := rw.deviceExt.marshal()
+		if err != nil {
+			return err
+		}
+		rw.Device.Ext = deviceJson
+	}
+	return nil
+}
+
+func (rw *RequestWrapper) rebuildRequestExt() error {
+	if rw.requestExt != nil && rw.requestExt.Dirty() {
+		requestJson, err := rw.requestExt.marshal()
+		if err != nil {
+			return err
+		}
+		rw.Ext = requestJson
+	}
+	return nil
+}
+
+func (rw *RequestWrapper) rebuildAppExt() error {
+	if rw.App == nil && rw.appExt != nil && rw.appExt.Dirty() {
+		rw.App = &openrtb2.App{}
+	}
+	if rw.appExt != nil && rw.appExt.Dirty() {
+		appJson, err := rw.appExt.marshal()
+		if err != nil {
+			return err
+		}
+		rw.App.Ext = appJson
+	}
+	return nil
+}
+
+func (rw *RequestWrapper) rebuildRegExt() error {
+	if rw.Regs == nil && rw.regExt != nil && rw.regExt.Dirty() {
+		rw.Regs = &openrtb2.Regs{}
+	}
+	if rw.regExt != nil && rw.regExt.Dirty() {
+		regsJson, err := rw.regExt.marshal()
+		if err != nil {
+			return err
+		}
+		rw.Regs.Ext = regsJson
+	}
+	return nil
+}
+
+func (rw *RequestWrapper) rebuildSiteExt() error {
+	if rw.Site == nil && rw.siteExt != nil && rw.siteExt.Dirty() {
+		rw.Site = &openrtb2.Site{}
+	}
+	if rw.siteExt != nil && rw.siteExt.Dirty() {
+		siteJson, err := rw.siteExt.marshal()
+		if err != nil {
+			return err
+		}
+		rw.Regs.Ext = siteJson
+	}
+	return nil
+}
+
+// ---------------------------------------------------------------
+// UserExt provides an interface for request.user.ext
+// ---------------------------------------------------------------
+
+type UserExt struct {
+	ext          map[string]json.RawMessage
+	extDirty     bool
+	consent      *string
+	consentDirty bool
+	prebid       *ExtUserPrebid
+	prebidDirty  bool
+	eids         *[]ExtUserEid
+	eidsDirty    bool
+}
+
+func (ue *UserExt) unmarshal(extJson json.RawMessage) error {
+	if len(ue.ext) != 0 || ue.Dirty() {
+		return nil
+	}
+	ue.ext = make(map[string]json.RawMessage)
+	if len(extJson) == 0 {
+		return nil
+	}
+
+	if err := json.Unmarshal(extJson, &ue.ext); err != nil {
+		return err
+	}
+
+	consentJson, hasConsent := ue.ext["consent"]
+	if hasConsent {
+		if err := json.Unmarshal(consentJson, &ue.consent); err != nil {
+			return err
+		}
+	}
+
+	prebidJson, hasPrebid := ue.ext["prebid"]
+	if hasPrebid {
+		ue.prebid = &ExtUserPrebid{}
+		if err := json.Unmarshal(prebidJson, ue.prebid); err != nil {
+			return err
+		}
+	}
+
+	eidsJson, hasEids := ue.ext["eids"]
+	if hasEids {
+		ue.eids = &[]ExtUserEid{}
+		if err := json.Unmarshal(eidsJson, ue.eids); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (ue *UserExt) marshal() (json.RawMessage, error) {
+	if ue.consentDirty {
+		consentJson, err := json.Marshal(ue.consent)
+		if err != nil {
+			return nil, err
+		}
+		if len(consentJson) > 2 {
+			ue.ext["consent"] = json.RawMessage(consentJson)
+		} else {
+			delete(ue.ext, "consent")
+		}
+		ue.consentDirty = false
+	}
+
+	if ue.prebidDirty {
+		prebidJson, err := json.Marshal(ue.prebid)
+		if err != nil {
+			return nil, err
+		}
+		if len(prebidJson) > 2 {
+			ue.ext["prebid"] = json.RawMessage(prebidJson)
+		} else {
+			delete(ue.ext, "prebid")
+		}
+		ue.prebidDirty = false
+	}
+
+	if ue.eidsDirty {
+		if len(*ue.eids) > 0 {
+			eidsJson, err := json.Marshal(ue.eids)
+			if err != nil {
+				return nil, err
+			}
+			ue.ext["eids"] = json.RawMessage(eidsJson)
+		} else {
+			delete(ue.ext, "eids")
+		}
+		ue.eidsDirty = false
+	}
+
+	ue.extDirty = false
+	if len(ue.ext) == 0 {
+		return nil, nil
+	}
+	return json.Marshal(ue.ext)
+
+}
+
+func (ue *UserExt) Dirty() bool {
+	return ue.extDirty || ue.eidsDirty || ue.prebidDirty || ue.consentDirty
+}
+
+func (ue *UserExt) GetExt() map[string]json.RawMessage {
+	ext := make(map[string]json.RawMessage)
+	for k, v := range ue.ext {
+		ext[k] = v
+	}
+	return ext
+}
+
+func (ue *UserExt) SetExt(ext map[string]json.RawMessage) {
+	ue.ext = ext
+	ue.extDirty = true
+}
+
+func (ue *UserExt) GetConsent() *string {
+	if ue.consent == nil {
+		return nil
+	}
+	consent := *ue.consent
+	return &consent
+}
+
+func (ue *UserExt) SetConsent(consent *string) {
+	ue.consent = consent
+	ue.consentDirty = true
+}
+
+func (ue *UserExt) GetPrebid() *ExtUserPrebid {
+	if ue.prebid == nil {
+		return nil
+	}
+	prebid := *ue.prebid
+	return &prebid
+}
+
+func (ue *UserExt) SetPrebid(prebid *ExtUserPrebid) {
+	ue.prebid = prebid
+	ue.prebidDirty = true
+}
+
+func (ue *UserExt) GetEid() *[]ExtUserEid {
+	if ue.eids == nil {
+		return nil
+	}
+	eids := *ue.eids
+	return &eids
+}
+
+func (ue *UserExt) SetEid(eid *[]ExtUserEid) {
+	ue.eids = eid
+	ue.eidsDirty = true
+}
+
+// ---------------------------------------------------------------
+// RequestExt provides an interface for request.ext
+// ---------------------------------------------------------------
+
+type RequestExt struct {
+	ext         map[string]json.RawMessage
+	extDirty    bool
+	prebid      *ExtRequestPrebid
+	prebidDirty bool
+}
+
+func (re *RequestExt) unmarshal(extJson json.RawMessage) error {
+	if len(re.ext) != 0 || re.Dirty() {
+		return nil
+	}
+	re.ext = make(map[string]json.RawMessage)
+	if len(extJson) == 0 {
+		return nil
+	}
+	err := json.Unmarshal(extJson, &re.ext)
+	if err != nil {
+		return err
+	}
+	prebidJson, hasPrebid := re.ext["prebid"]
+	if hasPrebid {
+		re.prebid = &ExtRequestPrebid{}
+		err = json.Unmarshal(prebidJson, re.prebid)
+	}
+
+	return err
+}
+
+func (re *RequestExt) marshal() (json.RawMessage, error) {
+	if re.prebidDirty {
+		prebidJson, err := json.Marshal(re.prebid)
+		if err != nil {
+			return nil, err
+		}
+		if len(prebidJson) > 2 {
+			re.ext["prebid"] = json.RawMessage(prebidJson)
+		} else {
+			delete(re.ext, "prebid")
+		}
+		re.prebidDirty = false
+	}
+
+	re.extDirty = false
+	if len(re.ext) == 0 {
+		return nil, nil
+	}
+	return json.Marshal(re.ext)
+}
+
+func (re *RequestExt) Dirty() bool {
+	return re.extDirty || re.prebidDirty
+}
+
+func (re *RequestExt) GetExt() map[string]json.RawMessage {
+	ext := make(map[string]json.RawMessage)
+	for k, v := range re.ext {
+		ext[k] = v
+	}
+	return ext
+}
+
+func (re *RequestExt) SetExt(ext map[string]json.RawMessage) {
+	re.ext = ext
+	re.extDirty = true
+}
+
+func (re *RequestExt) GetPrebid() *ExtRequestPrebid {
+	if re.prebid == nil {
+		return nil
+	}
+	prebid := *re.prebid
+	return &prebid
+}
+
+func (re *RequestExt) SetPrebid(prebid *ExtRequestPrebid) {
+	re.prebid = prebid
+	re.prebidDirty = true
+}
+
+// ---------------------------------------------------------------
+// DeviceExt provides an interface for request.device.ext
+// ---------------------------------------------------------------
+// NOTE: openrtb_ext/device.go:ParseDeviceExtATTS() uses ext.atts, as read only, via jsonparser, only for IOS.
+// Doesn't seem like we will see any performance savings by parsing atts at this point, and as it is read only,
+// we don't need to worry about write conflicts. Note here in case additional uses of atts evolve as things progress.
+// ---------------------------------------------------------------
+
+type DeviceExt struct {
+	ext         map[string]json.RawMessage
+	extDirty    bool
+	prebid      *ExtDevicePrebid
+	prebidDirty bool
+}
+
+func (de *DeviceExt) unmarshal(extJson json.RawMessage) error {
+	if len(de.ext) != 0 || de.Dirty() {
+		return nil
+	}
+	de.ext = make(map[string]json.RawMessage)
+	if len(extJson) == 0 {
+		return nil
+	}
+	err := json.Unmarshal(extJson, &de.ext)
+	if err != nil {
+		return err
+	}
+	prebidJson, hasPrebid := de.ext["prebid"]
+	if hasPrebid {
+		de.prebid = &ExtDevicePrebid{}
+		err = json.Unmarshal(prebidJson, de.prebid)
+	}
+
+	return err
+}
+
+func (de *DeviceExt) marshal() (json.RawMessage, error) {
+	if de.prebidDirty {
+		prebidJson, err := json.Marshal(de.prebid)
+		if err != nil {
+			return nil, err
+		}
+		if len(prebidJson) > 2 {
+			de.ext["prebid"] = json.RawMessage(prebidJson)
+		} else {
+			delete(de.ext, "prebid")
+		}
+		de.prebidDirty = false
+	}
+
+	de.extDirty = false
+	if len(de.ext) == 0 {
+		return nil, nil
+	}
+	return json.Marshal(de.ext)
+}
+
+func (de *DeviceExt) Dirty() bool {
+	return de.extDirty || de.prebidDirty
+}
+
+func (de *DeviceExt) GetExt() map[string]json.RawMessage {
+	ext := make(map[string]json.RawMessage)
+	for k, v := range de.ext {
+		ext[k] = v
+	}
+	return ext
+}
+
+func (de *DeviceExt) SetExt(ext map[string]json.RawMessage) {
+	de.ext = ext
+	de.extDirty = true
+}
+
+func (de *DeviceExt) GetPrebid() *ExtDevicePrebid {
+	if de.prebid == nil {
+		return nil
+	}
+	prebid := *de.prebid
+	return &prebid
+}
+
+func (de *DeviceExt) SetPrebid(prebid *ExtDevicePrebid) {
+	de.prebid = prebid
+	de.prebidDirty = true
+}
+
+// ---------------------------------------------------------------
+// AppExt provides an interface for request.app.ext
+// ---------------------------------------------------------------
+
+type AppExt struct {
+	ext         map[string]json.RawMessage
+	extDirty    bool
+	prebid      *ExtAppPrebid
+	prebidDirty bool
+}
+
+func (ae *AppExt) unmarshal(extJson json.RawMessage) error {
+	if len(ae.ext) != 0 || ae.Dirty() {
+		return nil
+	}
+	ae.ext = make(map[string]json.RawMessage)
+	if len(extJson) == 0 {
+		return nil
+	}
+	err := json.Unmarshal(extJson, &ae.ext)
+	if err != nil {
+		return err
+	}
+	prebidJson, hasPrebid := ae.ext["prebid"]
+	if hasPrebid {
+		ae.prebid = &ExtAppPrebid{}
+		err = json.Unmarshal(prebidJson, ae.prebid)
+	}
+
+	return err
+}
+
+func (ae *AppExt) marshal() (json.RawMessage, error) {
+	if ae.prebidDirty {
+		prebidJson, err := json.Marshal(ae.prebid)
+		if err != nil {
+			return nil, err
+		}
+		if len(prebidJson) > 2 {
+			ae.ext["prebid"] = json.RawMessage(prebidJson)
+		} else {
+			delete(ae.ext, "prebid")
+		}
+		ae.prebidDirty = false
+	}
+
+	ae.extDirty = false
+	if len(ae.ext) == 0 {
+		return nil, nil
+	}
+	return json.Marshal(ae.ext)
+}
+
+func (ae *AppExt) Dirty() bool {
+	return ae.extDirty || ae.prebidDirty
+}
+
+func (ae *AppExt) GetExt() map[string]json.RawMessage {
+	ext := make(map[string]json.RawMessage)
+	for k, v := range ae.ext {
+		ext[k] = v
+	}
+	return ext
+}
+
+func (ae *AppExt) SetExt(ext map[string]json.RawMessage) {
+	ae.ext = ext
+	ae.extDirty = true
+}
+
+func (ae *AppExt) GetPrebid() *ExtAppPrebid {
+	if ae.prebid == nil {
+		return nil
+	}
+	prebid := *ae.prebid
+	return &prebid
+}
+
+func (ae *AppExt) SetPrebid(prebid *ExtAppPrebid) {
+	ae.prebid = prebid
+	ae.prebidDirty = true
+}
+
+// ---------------------------------------------------------------
+// RegExt provides an interface for request.regs.ext
+// ---------------------------------------------------------------
+
+type RegExt struct {
+	ext            map[string]json.RawMessage
+	extDirty       bool
+	usPrivacy      string
+	usPrivacyDirty bool
+}
+
+func (re *RegExt) unmarshal(extJson json.RawMessage) error {
+	if len(re.ext) != 0 || re.Dirty() {
+		return nil
+	}
+	re.ext = make(map[string]json.RawMessage)
+	if len(extJson) == 0 {
+		return nil
+	}
+	err := json.Unmarshal(extJson, &re.ext)
+	if err != nil {
+		return err
+	}
+	uspJson, hasUsp := re.ext["us_privacy"]
+	if hasUsp {
+		err = json.Unmarshal(uspJson, &re.usPrivacy)
+	}
+
+	return err
+}
+
+func (re *RegExt) marshal() (json.RawMessage, error) {
+	if re.usPrivacyDirty {
+		if len(re.usPrivacy) > 0 {
+			rawjson, err := json.Marshal(re.usPrivacy)
+			if err != nil {
+				return nil, err
+			}
+			re.ext["us_privacy"] = rawjson
+		} else {
+			delete(re.ext, "us_privacy")
+		}
+		re.usPrivacyDirty = false
+	}
+
+	re.extDirty = false
+	if len(re.ext) == 0 {
+		return nil, nil
+	}
+	return json.Marshal(re.ext)
+}
+
+func (re *RegExt) Dirty() bool {
+	return re.extDirty || re.usPrivacyDirty
+}
+
+func (re *RegExt) GetExt() map[string]json.RawMessage {
+	ext := make(map[string]json.RawMessage)
+	for k, v := range re.ext {
+		ext[k] = v
+	}
+	return ext
+}
+
+func (re *RegExt) SetExt(ext map[string]json.RawMessage) {
+	re.ext = ext
+	re.extDirty = true
+}
+
+func (re *RegExt) GetUSPrivacy() string {
+	uSPrivacy := re.usPrivacy
+	return uSPrivacy
+}
+
+func (re *RegExt) SetUSPrivacy(uSPrivacy string) {
+	re.usPrivacy = uSPrivacy
+	re.usPrivacyDirty = true
+}
+
+// ---------------------------------------------------------------
+// SiteExt provides an interface for request.site.ext
+// ---------------------------------------------------------------
+
+type SiteExt struct {
+	ext      map[string]json.RawMessage
+	extDirty bool
+	amp      int8
+	ampDirty bool
+}
+
+func (se *SiteExt) unmarshal(extJson json.RawMessage) error {
+	if len(se.ext) != 0 || se.Dirty() {
+		return nil
+	}
+	se.ext = make(map[string]json.RawMessage)
+	if len(extJson) == 0 {
+		return nil
+	}
+	err := json.Unmarshal(extJson, &se.ext)
+	if err != nil {
+		return err
+	}
+	AmpJson, hasAmp := se.ext["amp"]
+	if hasAmp {
+		err = json.Unmarshal(AmpJson, &se.amp)
+		if err != nil {
+			err = errors.New(`request.site.ext.amp must be either 1, 0, or undefined`)
+		}
+	}
+
+	return err
+}
+
+func (se *SiteExt) marshal() (json.RawMessage, error) {
+	if se.ampDirty {
+		ampJson, err := json.Marshal(se.amp)
+		if err != nil {
+			return nil, err
+		}
+		if len(ampJson) > 2 {
+			se.ext["amp"] = json.RawMessage(ampJson)
+		} else {
+			delete(se.ext, "amp")
+		}
+		se.ampDirty = false
+	}
+
+	se.extDirty = false
+	if len(se.ext) == 0 {
+		return nil, nil
+	}
+	return json.Marshal(se.ext)
+}
+
+func (se *SiteExt) Dirty() bool {
+	return se.extDirty || se.ampDirty
+}
+
+func (se *SiteExt) GetExt() map[string]json.RawMessage {
+	ext := make(map[string]json.RawMessage)
+	for k, v := range se.ext {
+		ext[k] = v
+	}
+	return ext
+}
+
+func (se *SiteExt) SetExt(ext map[string]json.RawMessage) {
+	se.ext = ext
+	se.extDirty = true
+}
+
+func (se *SiteExt) GetAmp() int8 {
+	return se.amp
+}
+
+func (se *SiteExt) SetUSPrivacy(amp int8) {
+	se.amp = amp
+	se.ampDirty = true
+}
diff --git a/privacy/ccpa/consentwriter.go b/privacy/ccpa/consentwriter.go
index 41f1c394..451f2b40 100644
--- a/privacy/ccpa/consentwriter.go
+++ b/privacy/ccpa/consentwriter.go
@@ -1,8 +1,12 @@
 package ccpa
 
-import "github.com/mxmCherry/openrtb/v15/openrtb2"
+import (
+	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
 
-// ConsentWriter implements the PolicyWriter interface for CCPA.
+// ConsentWriter implements the old PolicyWriter interface for CCPA.
+// This is used where we have not converted to RequestWrapper yet
 type ConsentWriter struct {
 	Consent string
 }
@@ -12,12 +16,11 @@ func (c ConsentWriter) Write(req *openrtb2.BidRequest) error {
 	if req == nil {
 		return nil
 	}
-
-	regs, err := buildRegs(c.Consent, req.Regs)
-	if err != nil {
+	reqWrap := &openrtb_ext.RequestWrapper{BidRequest: req}
+	if regsExt, err := reqWrap.GetRegExt(); err == nil {
+		regsExt.SetUSPrivacy(c.Consent)
+	} else {
 		return err
 	}
-	req.Regs = regs
-
-	return nil
+	return reqWrap.RebuildRequest()
 }
diff --git a/privacy/ccpa/policy.go b/privacy/ccpa/policy.go
index d57ba8de..39322317 100644
--- a/privacy/ccpa/policy.go
+++ b/privacy/ccpa/policy.go
@@ -1,8 +1,6 @@
 package ccpa
 
 import (
-	"encoding/json"
-	"errors"
 	"fmt"
 
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
@@ -15,8 +13,8 @@ type Policy struct {
 	NoSaleBidders []string
 }
 
-// ReadFromRequest extracts the CCPA regulatory information from an OpenRTB bid request.
-func ReadFromRequest(req *openrtb2.BidRequest) (Policy, error) {
+// ReadFromRequestWrapper extracts the CCPA regulatory information from an OpenRTB bid request.
+func ReadFromRequestWrapper(req *openrtb_ext.RequestWrapper) (Policy, error) {
 	var consent string
 	var noSaleBidders []string
 
@@ -25,174 +23,80 @@ func ReadFromRequest(req *openrtb2.BidRequest) (Policy, error) {
 	}
 
 	// Read consent from request.regs.ext
-	if req.Regs != nil && len(req.Regs.Ext) > 0 {
-		var ext openrtb_ext.ExtRegs
-		if err := json.Unmarshal(req.Regs.Ext, &ext); err != nil {
-			return Policy{}, fmt.Errorf("error reading request.regs.ext: %s", err)
-		}
-		consent = ext.USPrivacy
+	regsExt, err := req.GetRegExt()
+	if err != nil {
+		return Policy{}, fmt.Errorf("error reading request.regs.ext: %s", err)
+	}
+	if regsExt != nil {
+		consent = regsExt.GetUSPrivacy()
 	}
-
 	// Read no sale bidders from request.ext.prebid
-	if len(req.Ext) > 0 {
-		var ext openrtb_ext.ExtRequest
-		if err := json.Unmarshal(req.Ext, &ext); err != nil {
-			return Policy{}, fmt.Errorf("error reading request.ext.prebid: %s", err)
-		}
-		noSaleBidders = ext.Prebid.NoSale
+	reqExt, err := req.GetRequestExt()
+	if err != nil {
+		return Policy{}, fmt.Errorf("error reading request.ext: %s", err)
+	}
+	reqPrebid := reqExt.GetPrebid()
+	if reqPrebid != nil {
+		noSaleBidders = reqPrebid.NoSale
 	}
 
 	return Policy{consent, noSaleBidders}, nil
 }
 
+func ReadFromRequest(req *openrtb2.BidRequest) (Policy, error) {
+	return ReadFromRequestWrapper(&openrtb_ext.RequestWrapper{BidRequest: req})
+}
+
 // Write mutates an OpenRTB bid request with the CCPA regulatory information.
-func (p Policy) Write(req *openrtb2.BidRequest) error {
+func (p Policy) Write(req *openrtb_ext.RequestWrapper) error {
 	if req == nil {
 		return nil
 	}
 
-	regs, err := buildRegs(p.Consent, req.Regs)
+	regsExt, err := req.GetRegExt()
 	if err != nil {
 		return err
 	}
-	ext, err := buildExt(p.NoSaleBidders, req.Ext)
+
+	reqExt, err := req.GetRequestExt()
 	if err != nil {
 		return err
 	}
 
-	req.Regs = regs
-	req.Ext = ext
+	regsExt.SetUSPrivacy(p.Consent)
+	setPrebidNoSale(p.NoSaleBidders, reqExt)
 	return nil
 }
 
-func buildRegs(consent string, regs *openrtb2.Regs) (*openrtb2.Regs, error) {
-	if consent == "" {
-		return buildRegsClear(regs)
-	}
-	return buildRegsWrite(consent, regs)
-}
-
-func buildRegsClear(regs *openrtb2.Regs) (*openrtb2.Regs, error) {
-	if regs == nil || len(regs.Ext) == 0 {
-		return regs, nil
-	}
-
-	var extMap map[string]interface{}
-	if err := json.Unmarshal(regs.Ext, &extMap); err != nil {
-		return nil, err
-	}
-
-	delete(extMap, "us_privacy")
-
-	// Remove entire ext if it's now empty
-	if len(extMap) == 0 {
-		regsResult := *regs
-		regsResult.Ext = nil
-		return &regsResult, nil
-	}
-
-	// Marshal ext if there are still other fields
-	var regsResult openrtb2.Regs
-	ext, err := json.Marshal(extMap)
-	if err == nil {
-		regsResult = *regs
-		regsResult.Ext = ext
-	}
-	return &regsResult, err
-}
-
-func buildRegsWrite(consent string, regs *openrtb2.Regs) (*openrtb2.Regs, error) {
-	if regs == nil {
-		return marshalRegsExt(openrtb2.Regs{}, openrtb_ext.ExtRegs{USPrivacy: consent})
-	}
-
-	if regs.Ext == nil {
-		return marshalRegsExt(*regs, openrtb_ext.ExtRegs{USPrivacy: consent})
-	}
-
-	var extMap map[string]interface{}
-	if err := json.Unmarshal(regs.Ext, &extMap); err != nil {
-		return nil, err
-	}
-
-	extMap["us_privacy"] = consent
-	return marshalRegsExt(*regs, extMap)
-}
-
-func marshalRegsExt(regs openrtb2.Regs, ext interface{}) (*openrtb2.Regs, error) {
-	extJSON, err := json.Marshal(ext)
-	if err == nil {
-		regs.Ext = extJSON
-	}
-	return &regs, err
-}
-
-func buildExt(noSaleBidders []string, ext json.RawMessage) (json.RawMessage, error) {
+func setPrebidNoSale(noSaleBidders []string, ext *openrtb_ext.RequestExt) {
 	if len(noSaleBidders) == 0 {
-		return buildExtClear(ext)
+		setPrebidNoSaleClear(ext)
+	} else {
+		setPrebidNoSaleWrite(noSaleBidders, ext)
 	}
-	return buildExtWrite(noSaleBidders, ext)
 }
 
-func buildExtClear(ext json.RawMessage) (json.RawMessage, error) {
-	if len(ext) == 0 {
-		return ext, nil
-	}
-
-	var extMap map[string]interface{}
-	if err := json.Unmarshal(ext, &extMap); err != nil {
-		return nil, err
-	}
-
-	prebidExt, exists := extMap["prebid"]
-	if !exists {
-		return ext, nil
-	}
-
-	// Verify prebid is an object
-	prebidExtMap, ok := prebidExt.(map[string]interface{})
-	if !ok {
-		return nil, errors.New("request.ext.prebid is not a json object")
+func setPrebidNoSaleClear(ext *openrtb_ext.RequestExt) {
+	prebid := ext.GetPrebid()
+	if prebid == nil {
+		return
 	}
 
 	// Remove no sale member
-	delete(prebidExtMap, "nosale")
-	if len(prebidExtMap) == 0 {
-		delete(extMap, "prebid")
-	}
-
-	// Remove entire ext if it's empty
-	if len(extMap) == 0 {
-		return nil, nil
-	}
-
-	return json.Marshal(extMap)
+	prebid.NoSale = []string{}
+	ext.SetPrebid(prebid)
 }
 
-func buildExtWrite(noSaleBidders []string, ext json.RawMessage) (json.RawMessage, error) {
-	if len(ext) == 0 {
-		return json.Marshal(openrtb_ext.ExtRequest{Prebid: openrtb_ext.ExtRequestPrebid{NoSale: noSaleBidders}})
-	}
-
-	var extMap map[string]interface{}
-	if err := json.Unmarshal(ext, &extMap); err != nil {
-		return nil, err
+func setPrebidNoSaleWrite(noSaleBidders []string, ext *openrtb_ext.RequestExt) {
+	if ext == nil {
+		// This should hopefully not be possible. The only caller insures that this has been initialized
+		return
 	}
 
-	var prebidExt map[string]interface{}
-	if prebidExtInterface, exists := extMap["prebid"]; exists {
-		// Reference Existing Prebid Ext Map
-		if prebidExtMap, ok := prebidExtInterface.(map[string]interface{}); ok {
-			prebidExt = prebidExtMap
-		} else {
-			return nil, errors.New("request.ext.prebid is not a json object")
-		}
-	} else {
-		// Create New Empty Prebid Ext Map
-		prebidExt = make(map[string]interface{})
-		extMap["prebid"] = prebidExt
+	prebid := ext.GetPrebid()
+	if prebid == nil {
+		prebid = &openrtb_ext.ExtRequestPrebid{}
 	}
-
-	prebidExt["nosale"] = noSaleBidders
-	return json.Marshal(extMap)
+	prebid.NoSale = noSaleBidders
+	ext.SetPrebid(prebid)
 }
diff --git a/static/bidder-info/inmobi.yaml b/static/bidder-info/inmobi.yaml
index 634c0348..d62a2c92 100644
--- a/static/bidder-info/inmobi.yaml
+++ b/static/bidder-info/inmobi.yaml
@@ -6,6 +6,7 @@ capabilities:
     mediaTypes:
       - banner
       - video
+      - native
   site:
     mediaTypes:
       - banner
diff --git a/static/bidder-info/operaads.yaml b/static/bidder-info/operaads.yaml
new file mode 100644
index 00000000..b95d8115
--- /dev/null
+++ b/static/bidder-info/operaads.yaml
@@ -0,0 +1,14 @@
+maintainer:
+  email: adtech-prebid-group@opera.com
+modifyingVastXmlAllowed: true
+capabilities:
+  app:
+    mediaTypes:
+      - banner
+      - video
+      - native
+  site:
+    mediaTypes:
+      - banner
+      - video
+      - native
\ No newline at end of file
diff --git a/static/bidder-params/admixer.json b/static/bidder-params/admixer.json
index 886e33ff..78671931 100644
--- a/static/bidder-params/admixer.json
+++ b/static/bidder-params/admixer.json
@@ -8,7 +8,7 @@
     "zone": {
       "type": "string",
       "description": "Zone ID.",
-      "pattern": "^([a-fA-F\\d\\-]{36})$"
+      "pattern": "^([a-fA-F\\d\\-]{32,36})$"
     },
     "customFloor": {
       "type": "number",
@@ -22,4 +22,4 @@
   },
 
   "required": ["zone"]
-}
+}
\ No newline at end of file
diff --git a/static/bidder-params/operaads.json b/static/bidder-params/operaads.json
new file mode 100644
index 00000000..5095c5b2
--- /dev/null
+++ b/static/bidder-params/operaads.json
@@ -0,0 +1,28 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "description": "A schema which validates params accepted by the OperaAds adapter",
+  "properties": {
+    "placementId": {
+      "description": "Placement ID",
+      "type": "string",
+      "minLength": 1
+    },
+    "endpointId": {
+      "description": "Endpoint ID",
+      "type": "string",
+      "minLength": 1
+    },
+    "publisherId": {
+      "description": "Publisher ID",
+      "type": "string",
+      "minLength": 1
+    }
+  },
+  "required": [
+    "placementId",
+    "endpointId",
+    "publisherId"
+  ],
+  "title": "OperaAds Adapter Params",
+  "type": "object"
+}
\ No newline at end of file
diff --git a/static/bidder-params/sharethrough.json b/static/bidder-params/sharethrough.json
index ba6580e2..1fe2949b 100644
--- a/static/bidder-params/sharethrough.json
+++ b/static/bidder-params/sharethrough.json
@@ -26,6 +26,16 @@
     "bidfloor": {
       "type": "number",
       "description": "The floor price, or minimum amount, a publisher will accept for an impression, given in CPM in USD"
+    },
+    "data": {
+      "type": "object",
+      "description": "Ad-specific first party data",
+      "properties": {
+        "pbadslot": {
+          "type": "string",
+          "description": "Prebid Ad Slot, see: https://docs.prebid.org/features/pbAdSlot.html"
+        }
+      }
     }
   },
   "required": ["pkey"]
diff --git a/stored_requests/config/config.go b/stored_requests/config/config.go
index 7f92f252..f682ff93 100644
--- a/stored_requests/config/config.go
+++ b/stored_requests/config/config.go
@@ -166,6 +166,9 @@ func newFetcher(cfg *config.StoredRequests, client *http.Client, db *sql.DB) (fe
 	if cfg.Postgres.FetcherQueries.QueryTemplate != "" {
 		glog.Infof("Loading Stored %s data via Postgres.\nQuery: %s", cfg.DataType(), cfg.Postgres.FetcherQueries.QueryTemplate)
 		idList = append(idList, db_fetcher.NewFetcher(db, cfg.Postgres.FetcherQueries.MakeQuery))
+	} else if cfg.Postgres.CacheInitialization.Query != "" && cfg.Postgres.PollUpdates.Query != "" {
+		//in this case data will be loaded to cache via poll for updates event
+		idList = append(idList, empty_fetcher.EmptyFetcher{})
 	}
 	if cfg.HTTP.Endpoint != "" {
 		glog.Infof("Loading Stored %s data via HTTP. endpoint=%s", cfg.DataType(), cfg.HTTP.Endpoint)
diff --git a/usersync/usersyncers/syncer.go b/usersync/usersyncers/syncer.go
index 53472018..674fc136 100644
--- a/usersync/usersyncers/syncer.go
+++ b/usersync/usersyncers/syncer.go
@@ -1,6 +1,7 @@
 package usersyncers
 
 import (
+	"github.com/prebid/prebid-server/adapters/operaads"
 	"strings"
 	"text/template"
 
@@ -169,6 +170,7 @@ func NewSyncerMap(cfg *config.Configuration) map[openrtb_ext.BidderName]usersync
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderOneTag, onetag.NewSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderOutbrain, outbrain.NewOutbrainSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderOpenx, openx.NewOpenxSyncer)
+	insertIntoMap(cfg, syncers, openrtb_ext.BidderOperaads, operaads.NewOperaadsSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderPubmatic, pubmatic.NewPubmaticSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderPulsepoint, pulsepoint.NewPulsepointSyncer)
 	insertIntoMap(cfg, syncers, openrtb_ext.BidderRhythmone, rhythmone.NewRhythmoneSyncer)


Test patch:
diff --git a/adapters/admixer/admixertest/exemplary/optional-params.json b/adapters/admixer/admixertest/exemplary/optional-params.json
index 8ef112bb..b93aa9c8 100644
--- a/adapters/admixer/admixertest/exemplary/optional-params.json
+++ b/adapters/admixer/admixertest/exemplary/optional-params.json
@@ -44,6 +44,76 @@
             }
           }
         }
+      },
+      {
+        "id": "test-imp-id",
+        "bidfloor": 0.5,
+        "banner": {
+          "format": [
+            {
+              "w": 728,
+              "h": 90
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "zone": "2eb6bd58-865c-47ce-af7f-a918108c3fd2",
+            "customParams": {
+              "foo": [
+                "bar",
+                "baz"
+              ]
+            }
+          }
+        }
+      },
+      {
+        "id": "test-imp-id",
+        "bidfloor": 0.5,
+        "banner": {
+          "format": [
+            {
+              "w": 728,
+              "h": 90
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "zone": "2eb6bd58-865c-47ce-af7f-a918108c3fd2",
+            "customParams": {
+              "foo": [
+                "bar",
+                "baz"
+              ]
+            }
+          },
+          "customFloor": 0.9
+        }
+      },
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 728,
+              "h": 90
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {
+            "customFloor": 0.9,
+            "zone": "2eb6bd58-865c-47ce-af7f-a918108c3fd2",
+            "customParams": {
+              "foo": [
+                "bar",
+                "baz"
+              ]
+            }
+          }
+        }
       }
     ]
   },
@@ -92,6 +162,69 @@
                   ]
                 }
               }
+            },
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 728,
+                    "h": 90
+                  }
+                ]
+              },
+              "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2",
+              "bidfloor": 0.5,
+              "ext": {
+                "customParams": {
+                  "foo": [
+                    "bar",
+                    "baz"
+                  ]
+                }
+              }
+            },
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 728,
+                    "h": 90
+                  }
+                ]
+              },
+              "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2",
+              "bidfloor": 0.5,
+              "ext": {
+                "customParams": {
+                  "foo": [
+                    "bar",
+                    "baz"
+                  ]
+                }
+              }
+            },
+            {
+              "id": "test-imp-id",
+              "banner": {
+                "format": [
+                  {
+                    "w": 728,
+                    "h": 90
+                  }
+                ]
+              },
+              "tagid": "2eb6bd58-865c-47ce-af7f-a918108c3fd2",
+              "bidfloor": 0.9,
+              "ext": {
+                "customParams": {
+                  "foo": [
+                    "bar",
+                    "baz"
+                  ]
+                }
+              }
             }
           ]
         }
diff --git a/adapters/admixer/params_test.go b/adapters/admixer/params_test.go
index 71cccb6a..11f3feb0 100644
--- a/adapters/admixer/params_test.go
+++ b/adapters/admixer/params_test.go
@@ -44,6 +44,8 @@ var validParams = []string{
 	`{"zone": "9FF668A2-4122-462E-AAF8-36EA3A54BA21", "customFloor": 0.1}`,
 	`{"zone": "9FF668A2-4122-462E-AAF8-36EA3A54BA21", "customParams": {"foo": "bar"}}`,
 	`{"zone": "9ff668a2-4122-462e-aaf8-36ea3a54ba21", "customFloor": 0.1, "customParams": {"foo": ["bar", "baz"]}}`,
+	`{"zone": "9FF668A24122462EAAF836EA3A54BA21"}`,
+	`{"zone": "9FF668A24122462EAAF836EA3A54BA212"}`,
 }
 
 var invalidParams = []string{
@@ -54,4 +56,6 @@ var invalidParams = []string{
 	`{"zone": "123", "customFloor": "0.1"}`,
 	`{"zone": "9FF668A2-4122-462E-AAF8-36EA3A54BA21",  "customFloor": -0.1}`,
 	`{"zone": "9FF668A2-4122-462E-AAF8-36EA3A54BA21",  "customParams": "foo: bar"}`,
+	`{"zone": "9FF668A24122462EAAF836EA3A54BA2"}`,
+	`{"zone": "9FF668A24122462EAAF836EA3A54BA2112336"}`,
 }
diff --git a/adapters/beachfront/beachfronttest/exemplary/adm-video-app.json b/adapters/beachfront/beachfronttest/exemplary/adm-video-app.json
new file mode 100644
index 00000000..0734a212
--- /dev/null
+++ b/adapters/beachfront/beachfronttest/exemplary/adm-video-app.json
@@ -0,0 +1,124 @@
+{
+  "mockBidRequest": {
+    "id": "adm-video",
+    "imp": [
+      {
+        "id": "video1",
+        "ext": {
+          "bidder": {
+            "bidfloor": 3.01,
+            "appId": "videoAppId1"
+          }
+        },
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "context": "instream",
+          "w": 300,
+          "h": 250
+        }
+      }
+    ],
+    "app": {
+      "bundle": "com.domain.some"
+    },
+    "device":{
+      "ip":"192.168.168.168"
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://qa.beachrtb.com/bid.json?exchange_id=videoAppId1",
+        "body": {
+          "id": "adm-video",
+          "imp": [
+            {
+              "video": {
+                "w": 300,
+                "h": 250,
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "bidfloor": 3.01,
+              "id": "video1",
+              "secure": 0
+            }
+          ],
+          "app": {
+            "bundle": "com.domain.some",
+            "domain": "domain.com"
+          },
+          "cur": [
+            "USD"
+          ],
+          "device":{
+            "devicetype": 1,
+            "ip":"192.168.168.168"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "adm-video",
+          "seatBid": [
+            {
+              "bid": [
+                {
+                  "id": "5fd7c8a6ff2f1f0d42ee6427",
+                  "impid": "video1",
+                  "price": 20,
+                  "adm": "<VAST version=\"2.0\"><Ad><Wrapper>http://example.com/vast.xml</Wrapper></Ad></VAST>",
+                  "adid": "1088",
+                  "adomain": [
+                    "beachfront.io"
+                  ],
+                  "cid": "277",
+                  "crid": "532",
+                  "w": 300,
+                  "h": 250,
+                  "ext": {
+                    "duration": 30
+                  }
+                }
+              ],
+              "seat": "bfio-s-1"
+            }
+          ]
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "video1AdmVideo",
+            "impid": "video1",
+            "price": 20,
+            "adm": "<VAST version=\"2.0\"><Ad><Wrapper>http://example.com/vast.xml</Wrapper></Ad></VAST>",
+            "adid": "1088",
+            "adomain": [
+              "beachfront.io"
+            ],
+            "cid": "277",
+            "crid": "532",
+            "w": 300,
+            "h": 250,
+            "ext": {
+              "duration": 30
+            }
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/beachfront/beachfronttest/supplemental/adm-video-app-alphanum-bundle.json b/adapters/beachfront/beachfronttest/supplemental/adm-video-app-alphanum-bundle.json
new file mode 100644
index 00000000..0e3a32d5
--- /dev/null
+++ b/adapters/beachfront/beachfronttest/supplemental/adm-video-app-alphanum-bundle.json
@@ -0,0 +1,123 @@
+{
+  "mockBidRequest": {
+    "id": "adm-video",
+    "imp": [
+      {
+        "id": "video1",
+        "ext": {
+          "bidder": {
+            "bidfloor": 3.01,
+            "appId": "videoAppId1"
+          }
+        },
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "context": "instream",
+          "w": 300,
+          "h": 250
+        }
+      }
+    ],
+    "app": {
+      "bundle": "id1234567890"
+    },
+    "device":{
+      "ip":"192.168.168.168"
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://qa.beachrtb.com/bid.json?exchange_id=videoAppId1",
+        "body": {
+          "id": "adm-video",
+          "imp": [
+            {
+              "video": {
+                "w": 300,
+                "h": 250,
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "bidfloor": 3.01,
+              "id": "video1",
+              "secure": 0
+            }
+          ],
+          "app": {
+            "bundle": "id1234567890"
+          },
+          "cur": [
+            "USD"
+          ],
+          "device":{
+            "devicetype": 1,
+            "ip":"192.168.168.168"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "adm-video",
+          "seatBid": [
+            {
+              "bid": [
+                {
+                  "id": "5fd7c8a6ff2f1f0d42ee6427",
+                  "impid": "video1",
+                  "price": 20,
+                  "adm": "<VAST version=\"2.0\"><Ad><Wrapper>http://example.com/vast.xml</Wrapper></Ad></VAST>",
+                  "adid": "1088",
+                  "adomain": [
+                    "beachfront.io"
+                  ],
+                  "cid": "277",
+                  "crid": "532",
+                  "w": 300,
+                  "h": 250,
+                  "ext": {
+                    "duration": 30
+                  }
+                }
+              ],
+              "seat": "bfio-s-1"
+            }
+          ]
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "video1AdmVideo",
+            "impid": "video1",
+            "price": 20,
+            "adm": "<VAST version=\"2.0\"><Ad><Wrapper>http://example.com/vast.xml</Wrapper></Ad></VAST>",
+            "adid": "1088",
+            "adomain": [
+              "beachfront.io"
+            ],
+            "cid": "277",
+            "crid": "532",
+            "w": 300,
+            "h": 250,
+            "ext": {
+              "duration": 30
+            }
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/beachfront/beachfronttest/supplemental/adm-video-app-malformed-bundle.json b/adapters/beachfront/beachfronttest/supplemental/adm-video-app-malformed-bundle.json
new file mode 100644
index 00000000..8327426c
--- /dev/null
+++ b/adapters/beachfront/beachfronttest/supplemental/adm-video-app-malformed-bundle.json
@@ -0,0 +1,124 @@
+{
+  "mockBidRequest": {
+    "id": "adm-video",
+    "imp": [
+      {
+        "id": "video1",
+        "ext": {
+          "bidder": {
+            "bidfloor": 3.01,
+            "appId": "videoAppId1"
+          }
+        },
+        "video": {
+          "mimes": [
+            "video/mp4"
+          ],
+          "context": "instream",
+          "w": 300,
+          "h": 250
+        }
+      }
+    ],
+    "app": {
+      "bundle": "some.domain.us/some/page.html"
+    },
+    "device":{
+      "ip":"192.168.168.168"
+    }
+  },
+
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "https://qa.beachrtb.com/bid.json?exchange_id=videoAppId1",
+        "body": {
+          "id": "adm-video",
+          "imp": [
+            {
+              "video": {
+                "w": 300,
+                "h": 250,
+                "mimes": [
+                  "video/mp4"
+                ]
+              },
+              "bidfloor": 3.01,
+              "id": "video1",
+              "secure": 0
+            }
+          ],
+          "app": {
+            "bundle": "some.domain.us/some/page.html",
+            "domain": "domain.some"
+          },
+          "cur": [
+            "USD"
+          ],
+          "device":{
+            "devicetype": 1,
+            "ip":"192.168.168.168"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "adm-video",
+          "seatBid": [
+            {
+              "bid": [
+                {
+                  "id": "5fd7c8a6ff2f1f0d42ee6427",
+                  "impid": "video1",
+                  "price": 20,
+                  "adm": "<VAST version=\"2.0\"><Ad><Wrapper>http://example.com/vast.xml</Wrapper></Ad></VAST>",
+                  "adid": "1088",
+                  "adomain": [
+                    "beachfront.io"
+                  ],
+                  "cid": "277",
+                  "crid": "532",
+                  "w": 300,
+                  "h": 250,
+                  "ext": {
+                    "duration": 30
+                  }
+                }
+              ],
+              "seat": "bfio-s-1"
+            }
+          ]
+        }
+      }
+    }
+  ],
+
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "video1AdmVideo",
+            "impid": "video1",
+            "price": 20,
+            "adm": "<VAST version=\"2.0\"><Ad><Wrapper>http://example.com/vast.xml</Wrapper></Ad></VAST>",
+            "adid": "1088",
+            "adomain": [
+              "beachfront.io"
+            ],
+            "cid": "277",
+            "crid": "532",
+            "w": 300,
+            "h": 250,
+            "ext": {
+              "duration": 30
+            }
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/inmobi/inmobitest/exemplary/simple-app-native.json b/adapters/inmobi/inmobitest/exemplary/simple-app-native.json
new file mode 100644
index 00000000..3a5bfd38
--- /dev/null
+++ b/adapters/inmobi/inmobitest/exemplary/simple-app-native.json
@@ -0,0 +1,105 @@
+{
+  "mockBidRequest": {
+    "app": {
+      "bundle": "com.example.app"
+    },
+    "id": "req-id",
+    "device": {
+      "ifa": "9d8fe0a9-c0dd-4482-b16b-5709b00c608d",
+      "ip": "1.1.1.1",
+      "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36"
+    },
+    "imp": [
+      {
+        "ext": {
+          "bidder": {
+            "plc": "1618933962959"
+          }
+        },
+        "native": {
+          "request": "{\"ver\":\"1.2\",\"context\":2,\"contextsubtype\":20,\"plcmttype\":11,\"plcmtcnt\":1,\"aurlsupport\":1,\"durlsupport\":1,\"assets\":[{\"id\":123,\"required\":1,\"title\":{\"len\":140}},{\"id\":128,\"required\":0,\"img\":{\"wmin\":836,\"hmin\":627,\"type\":3}}]}"
+        },
+        "id": "imp-id"
+      }
+    ]
+  },
+  "httpCalls": [{
+    "expectedRequest": {
+      "uri": "https://api.w.inmobi.com/showad/openrtb/bidder/prebid",
+      "body": {
+        "app": {
+          "bundle": "com.example.app"
+        },
+        "id": "req-id",
+        "device": {
+          "ifa": "9d8fe0a9-c0dd-4482-b16b-5709b00c608d",
+          "ip": "1.1.1.1",
+          "ua": "Mozilla/5.0 (Linux; Android 8.0.0; SM-G960F Build/R16NW) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.84 Mobile Safari/537.36"
+        },
+        "imp": [
+          {
+            "ext": {
+              "bidder": {
+                "plc": "1618933962959"
+              }
+            },
+            "native": {
+              "request": "{\"ver\":\"1.2\",\"context\":2,\"contextsubtype\":20,\"plcmttype\":11,\"plcmtcnt\":1,\"aurlsupport\":1,\"durlsupport\":1,\"assets\":[{\"id\":123,\"required\":1,\"title\":{\"len\":140}},{\"id\":128,\"required\":0,\"img\":{\"wmin\":836,\"hmin\":627,\"type\":3}}]}"
+            },
+            "id": "imp-id"
+          }
+        ]
+      }
+    },
+    "mockResponse": {
+      "status": 200,
+      "body": {
+        "id": "req-id",
+        "seatbid": [
+          {
+            "bid": [
+              {
+                "ext": {
+                  "prebid": {
+                    "meta": {
+                      "networkName": "inmobi"
+                    }
+                  }
+                },
+                "nurl": "https://some.event.url/params",
+                "crid": "123456789",
+                "adomain": [],
+                "price": 2.0,
+                "id": "1234",
+                "adm": "native-json",
+                "impid": "imp-id"
+              }
+            ]
+          }
+        ]
+      }
+    }
+  }],
+
+  "expectedBidResponses": [{
+    "currency": "USD",
+    "bids": [{
+      "bid": {
+        "id": "1234",
+        "impid": "imp-id",
+        "price": 2.0,
+        "adm": "native-json",
+        "crid": "123456789",
+        "nurl": "https://some.event.url/params",
+        "ext": {
+          "prebid": {
+            "meta": {
+              "networkName": "inmobi"
+            }
+          }
+        }
+      },
+      "type": "native"
+    }]
+  }]
+}
diff --git a/adapters/operaads/operaads_test.go b/adapters/operaads/operaads_test.go
new file mode 100644
index 00000000..eb4280b6
--- /dev/null
+++ b/adapters/operaads/operaads_test.go
@@ -0,0 +1,20 @@
+package operaads
+
+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.BidderOperaads, config.Adapter{
+		Endpoint: "http://example.com/operaads/ortb/v2/{{.PublisherID}}?ep={{.AccountID}}"})
+
+	if buildErr != nil {
+		t.Fatalf("Builder returned unexpected error %v", buildErr)
+	}
+
+	adapterstest.RunJSONBidderTest(t, "operaadstest", bidder)
+}
diff --git a/adapters/operaads/operaadstest/exemplary/native.json b/adapters/operaads/operaadstest/exemplary/native.json
new file mode 100644
index 00000000..4491bd15
--- /dev/null
+++ b/adapters/operaads/operaadstest/exemplary/native.json
@@ -0,0 +1,137 @@
+{
+  "mockBidRequest":{
+    "id":"some-req-id",
+    "imp":[
+      {
+        "id":"some-imp-id",
+        "native":{
+          "request":"{\"native\":{\"ver\":\"1.1\",\"layout\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":344,\"hmin\":194}},{\"id\":3,\"required\":1,\"img\":{\"type\":1,\"w\":128,\"wmin\":80,\"h\":128,\"hmin\":80}},{\"id\":4,\"required\":1,\"data\":{\"type\":2,\"len\":90}},{\"id\":6,\"data\":{\"type\":12,\"len\":15}}]}}",
+          "ver":"1.1"
+        },
+        "ext":{
+          "bidder":{
+            "placementId":"s123456",
+            "endpointId":"ep19978",
+            "publisherId":"pub123"
+          }
+        }
+      }
+    ],
+    "site":{
+      "domain":"example.com",
+      "page":"example.com"
+    },
+    "device":{
+      "ip":"152.193.6.74",
+      "os":"android"
+    },
+    "user":{
+      "id":"db089de9-a62e-4861-a881-0ff15e052516",
+      "buyeruid":"8299345306627569435"
+    },
+    "tmax":500
+  },
+  "httpcalls":[
+    {
+      "expectedRequest":{
+        "uri":"http://example.com/operaads/ortb/v2/pub123?ep=ep19978",
+        "body":{
+          "id":"some-req-id",
+          "imp":[
+            {
+              "id":"some-imp-id",
+              "tagid":"s123456",
+              "ext":{
+                "bidder":{
+                  "placementId":"s123456",
+                  "endpointId":"ep19978",
+                  "publisherId":"pub123"
+                }
+              },
+              "native":{
+                "request":"{\"native\":{\"ver\":\"1.1\",\"layout\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":344,\"hmin\":194}},{\"id\":3,\"required\":1,\"img\":{\"type\":1,\"w\":128,\"wmin\":80,\"h\":128,\"hmin\":80}},{\"id\":4,\"required\":1,\"data\":{\"type\":2,\"len\":90}},{\"id\":6,\"data\":{\"type\":12,\"len\":15}}]}}",
+                "ver":"1.1"
+              }
+            }
+          ],
+          "site":{
+            "domain":"example.com",
+            "page":"example.com"
+          },
+          "device":{
+            "ip":"152.193.6.74",
+            "os":"android"
+          },
+          "user":{
+            "id":"db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid":"8299345306627569435"
+          },
+          "tmax":500
+        }
+      },
+      "mockResponse":{
+        "status":200,
+        "body":{
+          "id":"some-req-id",
+          "seatbid":[
+            {
+              "bid":[
+                {
+                  "id":"928185755156387460",
+                  "impid":"some-imp-id",
+                  "price":1,
+                  "adid":"69595837",
+                  "adm":"{\"assets\":[{\"id\": 2,\"img\":{\"url\":\"http://example.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\": 344,\"h\": 194}},{\"id\": 3,\"img\":{\"url\":\"http://example.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\": 100,\"h\": 100}} ,{\"id\": 1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\": 4,\"data\":{\"value\":\"This is a Prebid Native Creative. There are many like it, but this one is mine.\"}},{\"id\": 6,\"data\":{\"value\":\"Please.\"}} ],\"link\":{\"url\":\"http://example.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://example.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"]}",
+                  "adomain":[
+                    "example.com"
+                  ],
+                  "iurl":"http://example.com/cr?id=69595837",
+                  "cid":"958",
+                  "crid":"69595837",
+                  "cat":[
+                    "IAB3-1"
+                  ],
+                  "ext":{
+
+                  }
+                }
+              ],
+              "seat":"958"
+            }
+          ],
+          "bidid":"8141327771600527856",
+          "cur":"USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses":[
+    {
+      "currency":"USD",
+      "bids":[
+        {
+          "bid":{
+            "id":"928185755156387460",
+            "impid":"some-imp-id",
+            "price":1,
+            "adm":"{\"assets\":[{\"id\": 2,\"img\":{\"url\":\"http://example.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\": 344,\"h\": 194}},{\"id\": 3,\"img\":{\"url\":\"http://example.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\": 100,\"h\": 100}} ,{\"id\": 1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\": 4,\"data\":{\"value\":\"This is a Prebid Native Creative. There are many like it, but this one is mine.\"}},{\"id\": 6,\"data\":{\"value\":\"Please.\"}} ],\"link\":{\"url\":\"http://example.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://example.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"]}",
+            "adid":"69595837",
+            "adomain":[
+              "example.com"
+            ],
+            "iurl":"http://example.com/cr?id=69595837",
+            "cid":"958",
+            "crid":"69595837",
+            "cat":[
+              "IAB3-1"
+            ],
+            "ext":{
+
+            }
+          },
+          "type":"native"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/operaads/operaadstest/exemplary/simple-banner.json b/adapters/operaads/operaadstest/exemplary/simple-banner.json
new file mode 100644
index 00000000..53b19c82
--- /dev/null
+++ b/adapters/operaads/operaadstest/exemplary/simple-banner.json
@@ -0,0 +1,156 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "site": {
+      "domain": "good.site",
+      "page": "https://good.site/url",
+      "publisher": {
+        "id": "test-publisher-id"
+      },
+      "ext": {
+        "amp": 0
+      }
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "secure": 1,
+        "ext": {
+          "bidder": {
+            "placementId": "s17890",
+            "endpointId": "ep19979",
+            "publisherId": "pub456"
+          }
+        }
+      }
+    ],
+    "device": {
+      "os": "android",
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "h": 700,
+      "w": 375
+    },
+    "at": 1,
+    "tmax": 200,
+    "test": 1,
+    "source": {
+      "tid": "283746293874293"
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://example.com/operaads/ortb/v2/pub456?ep=ep19979",
+        "body": {
+          "id": "test-request-id",
+          "site": {
+            "domain": "good.site",
+            "page": "https://good.site/url",
+            "publisher": {
+              "id": "test-publisher-id"
+            },
+            "ext": {
+              "amp": 0
+            }
+          },
+          "imp": [
+            {
+              "id": "test-imp-id",
+              "tagid": "s17890",
+              "banner": {
+                "h": 250,
+                "w": 300,
+                "format": [
+                  {
+                    "w": 300,
+                    "h": 250
+                  }
+                ]
+              },
+              "secure": 1,
+              "ext": {
+                "bidder": {
+                  "placementId": "s17890",
+                  "endpointId": "ep19979",
+                  "publisherId": "pub456"
+                }
+              }
+            }
+          ],
+          "device": {
+            "os": "android",
+            "ua": "test-user-agent",
+            "ip": "123.123.123.123",
+            "h": 700,
+            "w": 375
+          },
+          "at": 1,
+          "tmax": 200,
+          "test": 1,
+          "source": {
+            "tid": "283746293874293"
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "1519967420713_259406708_583019428",
+          "seatbid": [
+            {
+              "bid": [
+                {
+                  "price": 0.5,
+                  "adm": "some-test-ad",
+                  "impid": "1",
+                  "auid": 46,
+                  "id": "1",
+                  "h": 250,
+                  "adomain": [
+                    "goodadvertiser.com"
+                  ],
+                  "crid": "11_222222",
+                  "w": 300
+                }
+              ],
+              "seat": "51"
+            }
+          ],
+          "bidid":"8141327771600527856",
+          "cur":"USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency":"USD",
+      "bids": [
+        {
+          "bid": {
+            "price": 0.5,
+            "adm": "some-test-ad",
+            "impid": "1",
+            "id": "1",
+            "h": 250,
+            "adomain": [
+              "goodadvertiser.com"
+            ],
+            "crid": "11_222222",
+            "w": 300
+          },
+          "type": "banner"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/operaads/operaadstest/exemplary/video.json b/adapters/operaads/operaadstest/exemplary/video.json
new file mode 100644
index 00000000..a76bbe5c
--- /dev/null
+++ b/adapters/operaads/operaadstest/exemplary/video.json
@@ -0,0 +1,173 @@
+{
+  "mockBidRequest": {
+    "id": "test-video-request",
+    "imp": [
+      {
+        "id": "test-video-imp",
+        "video": {
+          "w": 640,
+          "h": 480,
+          "mimes": [
+            "video/mp4",
+            "video/x-flv"
+          ],
+          "minduration": 5,
+          "maxduration": 30,
+          "startdelay": 5,
+          "playbackmethod": [
+            1,
+            3
+          ],
+          "api": [
+            1,
+            2
+          ],
+          "protocols": [
+            2,
+            3
+          ],
+          "battr": [
+            13,
+            14
+          ],
+          "linearity": 1,
+          "placement": 2,
+          "minbitrate": 10,
+          "maxbitrate": 10
+        },
+        "ext": {
+          "bidder": {
+            "placementId":"s00000",
+            "endpointId":"ep00000",
+            "publisherId":"pub00000"
+          }
+        }
+      }
+    ],
+    "device": {
+      "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
+      "os": "android"
+    },
+    "site": {
+      "id": "siteID",
+      "publisher": {
+        "id": "1234"
+      }
+    }
+  },
+  "httpCalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://example.com/operaads/ortb/v2/pub00000?ep=ep00000",
+        "body": {
+          "id": "test-video-request",
+          "imp": [
+            {
+              "id": "test-video-imp",
+              "tagid": "s00000",
+              "video": {
+                "w": 640,
+                "h": 480,
+                "mimes": [
+                  "video/mp4",
+                  "video/x-flv"
+                ],
+                "minduration": 5,
+                "maxduration": 30,
+                "startdelay": 5,
+                "playbackmethod": [
+                  1,
+                  3
+                ],
+                "api": [
+                  1,
+                  2
+                ],
+                "protocols": [
+                  2,
+                  3
+                ],
+                "battr": [
+                  13,
+                  14
+                ],
+                "linearity": 1,
+                "placement": 2,
+                "minbitrate": 10,
+                "maxbitrate": 10
+              },
+              "ext": {
+                "bidder": {
+                  "placementId":"s00000",
+                  "endpointId":"ep00000",
+                  "publisherId":"pub00000"
+                }
+              }
+            }
+          ],
+          "device": {
+            "ua": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36",
+            "os": "android"
+          },
+          "site": {
+            "id": "siteID",
+            "publisher": {
+              "id": "1234"
+            }
+          }
+        }
+      },
+      "mockResponse": {
+        "status": 200,
+        "body": {
+          "id": "test-video-request",
+          "seatbid": [
+            {
+              "seat": "958",
+              "bid": [
+                {
+                  "id": "7706636740145184841",
+                  "impid": "test-video-imp",
+                  "price": 0.5,
+                  "adid": "29681110",
+                  "adm": "some-test-ad",
+                  "adomain": [
+                    "example.com"
+                  ],
+                  "crid": "29681110",
+                  "h": 250,
+                  "w": 300
+                }
+              ]
+            }
+          ],
+          "bidid": "5778926625248726496",
+          "cur": "USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses": [
+    {
+      "currency": "USD",
+      "bids": [
+        {
+          "bid": {
+            "id": "7706636740145184841",
+            "impid": "test-video-imp",
+            "price": 0.5,
+            "adid": "29681110",
+            "adm": "some-test-ad",
+            "adomain": [
+              "example.com"
+            ],
+            "crid": "29681110",
+            "w": 300,
+            "h": 250
+          },
+          "type": "video"
+        }
+      ]
+    }
+  ]
+}
diff --git a/adapters/operaads/operaadstest/supplemental/badrequest.json b/adapters/operaads/operaadstest/supplemental/badrequest.json
new file mode 100644
index 00000000..ff3fe071
--- /dev/null
+++ b/adapters/operaads/operaadstest/supplemental/badrequest.json
@@ -0,0 +1,84 @@
+{
+  "mockBidRequest":{
+    "id":"some-req-id",
+    "imp":[
+      {
+        "id":"some-imp-id",
+        "native":{
+          "request":"{\"native\":{\"ver\":\"1.1\",\"layout\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":344,\"hmin\":194}},{\"id\":3,\"required\":1,\"img\":{\"type\":1,\"w\":128,\"wmin\":80,\"h\":128,\"hmin\":80}},{\"id\":4,\"required\":1,\"data\":{\"type\":2,\"len\":90}},{\"id\":6,\"data\":{\"type\":12,\"len\":15}}]}}",
+          "ver":"1.1"
+        },
+        "ext":{
+          "bidder":{
+            "placementId":"s123456",
+            "endpointId":"ep19978",
+            "publisherId":"pub123"
+          }
+        }
+      }
+    ],
+    "site":{
+      "domain":"example.com",
+      "page":"example.com"
+    },
+    "device":{
+      "ip":"152.193.6.74",
+      "os":"android"
+    },
+    "user":{
+      "id":"db089de9-a62e-4861-a881-0ff15e052516",
+      "buyeruid":"8299345306627569435"
+    },
+    "tmax":500
+  },
+  "httpcalls":[
+    {
+      "expectedRequest":{
+        "uri":"http://example.com/operaads/ortb/v2/pub123?ep=ep19978",
+        "body":{
+          "id":"some-req-id",
+          "imp":[
+            {
+              "id":"some-imp-id",
+              "tagid":"s123456",
+              "ext":{
+                "bidder":{
+                  "placementId":"s123456",
+                  "endpointId":"ep19978",
+                  "publisherId":"pub123"
+                }
+              },
+              "native":{
+                "request":"{\"native\":{\"ver\":\"1.1\",\"layout\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":344,\"hmin\":194}},{\"id\":3,\"required\":1,\"img\":{\"type\":1,\"w\":128,\"wmin\":80,\"h\":128,\"hmin\":80}},{\"id\":4,\"required\":1,\"data\":{\"type\":2,\"len\":90}},{\"id\":6,\"data\":{\"type\":12,\"len\":15}}]}}",
+                "ver":"1.1"
+              }
+            }
+          ],
+          "site":{
+            "domain":"example.com",
+            "page":"example.com"
+          },
+          "device":{
+            "ip":"152.193.6.74",
+            "os":"android"
+          },
+          "user":{
+            "id":"db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid":"8299345306627569435"
+          },
+          "tmax":500
+        }
+      },
+      "mockResponse":{
+        "status":400,
+        "body":{}
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 400. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/operaads/operaadstest/supplemental/banner-size-miss.json b/adapters/operaads/operaadstest/supplemental/banner-size-miss.json
new file mode 100644
index 00000000..70ac350c
--- /dev/null
+++ b/adapters/operaads/operaadstest/supplemental/banner-size-miss.json
@@ -0,0 +1,50 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "site": {
+      "domain": "good.site",
+      "page": "https://good.site/url",
+      "publisher": {
+        "id": "test-publisher-id"
+      },
+      "ext": {
+        "amp": 0
+      }
+    },
+    "imp": [
+      {
+        "id": "test-imp-id",
+        "banner": {
+          "format": []
+        },
+        "secure": 1,
+        "ext": {
+          "bidder": {
+            "placementId": "s17890",
+            "endpointId": "ep19979",
+            "publisherId": "pub456"
+          }
+        }
+      }
+    ],
+    "device": {
+      "os": "android",
+      "ua": "test-user-agent",
+      "ip": "123.123.123.123",
+      "h": 700,
+      "w": 375
+    },
+    "at": 1,
+    "tmax": 200,
+    "test": 1,
+    "source": {
+      "tid": "283746293874293"
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Size information missing for banner",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/operaads/operaadstest/supplemental/miss-native.json b/adapters/operaads/operaadstest/supplemental/miss-native.json
new file mode 100644
index 00000000..918bbc4d
--- /dev/null
+++ b/adapters/operaads/operaadstest/supplemental/miss-native.json
@@ -0,0 +1,137 @@
+{
+  "mockBidRequest":{
+    "id":"some-req-id",
+    "imp":[
+      {
+        "id":"some-imp-id",
+        "native":{
+          "request":"{\"ver\":\"1.1\",\"layout\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":344,\"hmin\":194}},{\"id\":3,\"required\":1,\"img\":{\"type\":1,\"w\":128,\"wmin\":80,\"h\":128,\"hmin\":80}},{\"id\":4,\"required\":1,\"data\":{\"type\":2,\"len\":90}},{\"id\":6,\"data\":{\"type\":12,\"len\":15}}]}",
+          "ver":"1.1"
+        },
+        "ext":{
+          "bidder":{
+            "placementId":"s123456",
+            "endpointId":"ep19978",
+            "publisherId":"pub123"
+          }
+        }
+      }
+    ],
+    "site":{
+      "domain":"example.com",
+      "page":"example.com"
+    },
+    "device":{
+      "ip":"152.193.6.74",
+      "os":"android"
+    },
+    "user":{
+      "id":"db089de9-a62e-4861-a881-0ff15e052516",
+      "buyeruid":"8299345306627569435"
+    },
+    "tmax":500
+  },
+  "httpcalls":[
+    {
+      "expectedRequest":{
+        "uri":"http://example.com/operaads/ortb/v2/pub123?ep=ep19978",
+        "body":{
+          "id":"some-req-id",
+          "imp":[
+            {
+              "id":"some-imp-id",
+              "tagid":"s123456",
+              "ext":{
+                "bidder":{
+                  "placementId":"s123456",
+                  "endpointId":"ep19978",
+                  "publisherId":"pub123"
+                }
+              },
+              "native":{
+                "request":"{\"native\":{\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"img\":{\"hmin\":194,\"type\":3,\"wmin\":344},\"required\":1},{\"id\":3,\"img\":{\"h\":128,\"hmin\":80,\"type\":1,\"w\":128,\"wmin\":80},\"required\":1},{\"data\":{\"len\":90,\"type\":2},\"id\":4,\"required\":1},{\"data\":{\"len\":15,\"type\":12},\"id\":6}],\"layout\":3,\"ver\":\"1.1\"}}",
+                "ver":"1.1"
+              }
+            }
+          ],
+          "site":{
+            "domain":"example.com",
+            "page":"example.com"
+          },
+          "device":{
+            "ip":"152.193.6.74",
+            "os":"android"
+          },
+          "user":{
+            "id":"db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid":"8299345306627569435"
+          },
+          "tmax":500
+        }
+      },
+      "mockResponse":{
+        "status":200,
+        "body":{
+          "id":"some-req-id",
+          "seatbid":[
+            {
+              "bid":[
+                {
+                  "id":"928185755156387460",
+                  "impid":"some-imp-id",
+                  "price":1,
+                  "adid":"69595837",
+                  "adm":"{\"assets\":[{\"id\": 2,\"img\":{\"url\":\"http://example.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\": 344,\"h\": 194}},{\"id\": 3,\"img\":{\"url\":\"http://example.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\": 100,\"h\": 100}} ,{\"id\": 1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\": 4,\"data\":{\"value\":\"This is a Prebid Native Creative. There are many like it, but this one is mine.\"}},{\"id\": 6,\"data\":{\"value\":\"Please.\"}} ],\"link\":{\"url\":\"http://example.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://example.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"]}",
+                  "adomain":[
+                    "example.com"
+                  ],
+                  "iurl":"http://example.com/cr?id=69595837",
+                  "cid":"958",
+                  "crid":"69595837",
+                  "cat":[
+                    "IAB3-1"
+                  ],
+                  "ext":{
+
+                  }
+                }
+              ],
+              "seat":"958"
+            }
+          ],
+          "bidid":"8141327771600527856",
+          "cur":"USD"
+        }
+      }
+    }
+  ],
+  "expectedBidResponses":[
+    {
+      "currency":"USD",
+      "bids":[
+        {
+          "bid":{
+            "id":"928185755156387460",
+            "impid":"some-imp-id",
+            "price":1,
+            "adm":"{\"assets\":[{\"id\": 2,\"img\":{\"url\":\"http://example.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\": 344,\"h\": 194}},{\"id\": 3,\"img\":{\"url\":\"http://example.com/p/creative-image/5e/b6/de/c3/5eb6dec3-4854-4dcd-980a-347f36ab502e.jpg\",\"w\": 100,\"h\": 100}} ,{\"id\": 1,\"title\":{\"text\":\"This is an example Prebid Native creative\"}},{\"id\": 4,\"data\":{\"value\":\"This is a Prebid Native Creative. There are many like it, but this one is mine.\"}},{\"id\": 6,\"data\":{\"value\":\"Please.\"}} ],\"link\":{\"url\":\"http://example.com/click?AAAAAAAA8D8AAAAAAADwPwAAAAAAAAAAAAAAAAAA8D8AAAAAAADwPwhdYz3ZyNFNG3fXpZUyLXNZ0o5aAAAAACrElgC-AwAAvgMAAAIAAAC98iUEeP4QAAAAAABVU0QAVVNEAAEAAQARIAAAAAABAgQCAAAAAAEAhBaSXgAAAAA./pp=${AUCTION_PRICE}/cnd=%21OwwGAQiGmooHEL3llyEY-PxDIAQoADoRZGVmYXVsdCNOWU0yOjQwMjM./bn=75922/test=1/referrer=prebid.org/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html\"},\"imptrackers\":[\"http://example.com/openrtb_win?e=wqT_3QLFBqBFAwAAAwDWAAUBCNmku9QFEIi6jeuTm_LoTRib7t2u2tLMlnMqNgkAAAECCPA_EQEHEAAA8D8ZCQkIAAAhCQkI8D8pEQkAMQkJqAAAMKqI2wQ4vgdAvgdIAlC95ZchWPj8Q2AAaJFAeJLRBIABAYoBA1VTRJIFBvBQmAEBoAEBqAEBsAEAuAECwAEEyAEC0AEJ2AEA4AEB8AEAigI7dWYoJ2EnLCAxMzc2ODYwLCAxNTE5MzA5NDAxKTt1ZigncicsIDY5NTk1ODM3Nh4A8IqSAvUBIXRETkdfUWlHbW9vSEVMM2xseUVZQUNENF9FTXdBRGdBUUFSSXZnZFFxb2piQkZnQVlMTURhQUJ3QUhnQWdBRUFpQUVBa0FFQm1BRUJvQUVCcUFFRHNBRUF1UUVwaTRpREFBRHdQOEVCS1l1SWd3QUE4RF9KQVhfelYzek1zXzBfMlFFQUFBAQMkRHdQLUFCQVBVQgEOLEFKZ0NBS0FDQUxVQwUQBEwwCQjwTE1BQ0FNZ0NBT0FDQU9nQ0FQZ0NBSUFEQVpBREFKZ0RBYWdEaHBxS0I3b0RFV1JsWm1GMWJIUWpUbGxOTWpvME1ESXqaAjkhT3d3R0FRNvgA8E4tUHhESUFRb0FEb1JaR1ZtWVhWc2RDTk9XVTB5T2pRd01qTS7YAugH4ALH0wHqAgpwcmViaWQub3Jn8gIRCgZBRFZfSUQSBzEzNzY4NjDyARQMQ1BHXwEUNDM1MDMwOTjyAhEKBUNQARPwmQgxNDg0NzIzOIADAYgDAZADAJgDFKADAaoDAMADkBzIAwDYAwDgAwDoAwD4AwOABACSBAkvb3BlbnJ0YjKYBACiBAwxNTIuMTkzLjYuNzSoBJrMI7IEDAgAEAAYACAAMAA4ALgEAMAEAMgEANIEEWRlZmF1bHQjTllNMjo0MDIz2gQCCADgBADwBL3llyGIBQGYBQCgBf____8FA1ABqgULc29tZS1yZXEtaWTABQDJBQAFARTwP9IFCQkFC2QAAADYBQHgBQHwBd4C-gUECAAQAJAGAZgGAA..&s=08b1535744639c904684afe46e3c6c0e4786089f&test=1&referrer=prebid.org&pp=${AUCTION_PRICE}\"]}",
+            "adid":"69595837",
+            "adomain":[
+              "example.com"
+            ],
+            "iurl":"http://example.com/cr?id=69595837",
+            "cid":"958",
+            "crid":"69595837",
+            "cat":[
+              "IAB3-1"
+            ],
+            "ext":{
+
+            }
+          },
+          "type":"native"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/operaads/operaadstest/supplemental/missing-device.json b/adapters/operaads/operaadstest/supplemental/missing-device.json
new file mode 100644
index 00000000..3ba01d81
--- /dev/null
+++ b/adapters/operaads/operaadstest/supplemental/missing-device.json
@@ -0,0 +1,33 @@
+{
+  "mockBidRequest": {
+    "id": "test-request-id",
+    "imp": [
+      {
+        "id": "test-imp-id-1",
+        "banner": {
+          "format": [
+            {
+              "w": 300,
+              "h": 250
+            }
+          ]
+        },
+        "ext": {
+          "bidder": {}
+        }
+      }
+    ],
+    "site": {
+      "id": "siteID",
+      "publisher": {
+        "id": "1234"
+      }
+    }
+  },
+  "expectedMakeRequestsErrors": [
+    {
+      "value": "Impression is missing device OS information",
+      "comparison": "literal"
+    }
+  ]
+}
diff --git a/adapters/operaads/operaadstest/supplemental/nocontent.json b/adapters/operaads/operaadstest/supplemental/nocontent.json
new file mode 100644
index 00000000..d6baf35d
--- /dev/null
+++ b/adapters/operaads/operaadstest/supplemental/nocontent.json
@@ -0,0 +1,82 @@
+{
+  "mockBidRequest": {
+    "id": "some-req-id",
+    "imp": [
+      {
+        "id": "some-imp-id",
+        "native": {
+          "request": "{\"native\":{\"ver\":\"1.1\",\"layout\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":344,\"hmin\":194}},{\"id\":3,\"required\":1,\"img\":{\"type\":1,\"w\":128,\"wmin\":80,\"h\":128,\"hmin\":80}},{\"id\":4,\"required\":1,\"data\":{\"type\":2,\"len\":90}},{\"id\":6,\"data\":{\"type\":12,\"len\":15}}]}}",
+          "ver": "1.1"
+        },
+        "ext": {
+          "bidder": {
+            "placementId": "s123456",
+            "endpointId": "ep19978",
+            "publisherId": "pub123"
+          }
+        }
+      }
+    ],
+    "site": {
+      "domain": "example.com",
+      "page": "example.com"
+    },
+    "device": {
+      "ip": "152.193.6.74",
+      "os": "android"
+    },
+    "user": {
+      "id": "db089de9-a62e-4861-a881-0ff15e052516",
+      "buyeruid": "8299345306627569435"
+    },
+    "tmax": 500
+  },
+  "httpcalls": [
+    {
+      "expectedRequest": {
+        "uri": "http://example.com/operaads/ortb/v2/pub123?ep=ep19978",
+        "body": {
+          "id": "some-req-id",
+          "imp": [
+            {
+              "id": "some-imp-id",
+              "tagid": "s123456",
+              "ext": {
+                "bidder": {
+                  "placementId": "s123456",
+                  "endpointId": "ep19978",
+                  "publisherId": "pub123"
+                }
+              },
+              "native": {
+                "request": "{\"native\":{\"ver\":\"1.1\",\"layout\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":344,\"hmin\":194}},{\"id\":3,\"required\":1,\"img\":{\"type\":1,\"w\":128,\"wmin\":80,\"h\":128,\"hmin\":80}},{\"id\":4,\"required\":1,\"data\":{\"type\":2,\"len\":90}},{\"id\":6,\"data\":{\"type\":12,\"len\":15}}]}}",
+                "ver": "1.1"
+              }
+            }
+          ],
+          "site": {
+            "domain": "example.com",
+            "page": "example.com"
+          },
+          "device": {
+            "ip": "152.193.6.74",
+            "os": "android"
+          },
+          "user": {
+            "id": "db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid": "8299345306627569435"
+          },
+          "tmax": 500
+        }
+      },
+      "mockResponse": {
+        "status": 204,
+        "body": {}
+      }
+    }
+  ],
+  "expectedBidResponses": [
+  ],
+  "expectedMakeBidsErrors": [
+  ]
+}
\ No newline at end of file
diff --git a/adapters/operaads/operaadstest/supplemental/unexcept-statuscode.json b/adapters/operaads/operaadstest/supplemental/unexcept-statuscode.json
new file mode 100644
index 00000000..a6c8c505
--- /dev/null
+++ b/adapters/operaads/operaadstest/supplemental/unexcept-statuscode.json
@@ -0,0 +1,84 @@
+{
+  "mockBidRequest":{
+    "id":"some-req-id",
+    "imp":[
+      {
+        "id":"some-imp-id",
+        "native":{
+          "request":"{\"native\":{\"ver\":\"1.1\",\"layout\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":344,\"hmin\":194}},{\"id\":3,\"required\":1,\"img\":{\"type\":1,\"w\":128,\"wmin\":80,\"h\":128,\"hmin\":80}},{\"id\":4,\"required\":1,\"data\":{\"type\":2,\"len\":90}},{\"id\":6,\"data\":{\"type\":12,\"len\":15}}]}}",
+          "ver":"1.1"
+        },
+        "ext":{
+          "bidder":{
+            "placementId":"s123456",
+            "endpointId":"ep19978",
+            "publisherId":"pub123"
+          }
+        }
+      }
+    ],
+    "site":{
+      "domain":"example.com",
+      "page":"example.com"
+    },
+    "device":{
+      "ip":"152.193.6.74",
+      "os":"android"
+    },
+    "user":{
+      "id":"db089de9-a62e-4861-a881-0ff15e052516",
+      "buyeruid":"8299345306627569435"
+    },
+    "tmax":500
+  },
+  "httpcalls":[
+    {
+      "expectedRequest":{
+        "uri":"http://example.com/operaads/ortb/v2/pub123?ep=ep19978",
+        "body":{
+          "id":"some-req-id",
+          "imp":[
+            {
+              "id":"some-imp-id",
+              "tagid":"s123456",
+              "ext":{
+                "bidder":{
+                  "placementId":"s123456",
+                  "endpointId":"ep19978",
+                  "publisherId":"pub123"
+                }
+              },
+              "native":{
+                "request":"{\"native\":{\"ver\":\"1.1\",\"layout\":3,\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":90}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":344,\"hmin\":194}},{\"id\":3,\"required\":1,\"img\":{\"type\":1,\"w\":128,\"wmin\":80,\"h\":128,\"hmin\":80}},{\"id\":4,\"required\":1,\"data\":{\"type\":2,\"len\":90}},{\"id\":6,\"data\":{\"type\":12,\"len\":15}}]}}",
+                "ver":"1.1"
+              }
+            }
+          ],
+          "site":{
+            "domain":"example.com",
+            "page":"example.com"
+          },
+          "device":{
+            "ip":"152.193.6.74",
+            "os":"android"
+          },
+          "user":{
+            "id":"db089de9-a62e-4861-a881-0ff15e052516",
+            "buyeruid":"8299345306627569435"
+          },
+          "tmax":500
+        }
+      },
+      "mockResponse":{
+        "status":205,
+        "body":{}
+      }
+    }
+  ],
+  "expectedMakeBidsErrors": [
+    {
+      "value": "Unexpected status code: 205. Run with request.debug = 1 for more info",
+      "comparison": "literal"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/adapters/operaads/params_test.go b/adapters/operaads/params_test.go
new file mode 100644
index 00000000..e998127b
--- /dev/null
+++ b/adapters/operaads/params_test.go
@@ -0,0 +1,54 @@
+package operaads
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/prebid/prebid-server/openrtb_ext"
+)
+
+// This file actually intends to test static/bidder-params/operaads.json
+//
+// These also validate the format of the external API: request.imp[i].ext.operaads
+
+// TestValidParams makes sure that the operaads schema accepts all imp.ext fields which we intend to support.
+func TestValidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, validParam := range validParams {
+		if err := validator.Validate(openrtb_ext.BidderOperaads, json.RawMessage(validParam)); err != nil {
+			t.Errorf("Schema rejected operaads params: %s", validParam)
+		}
+	}
+}
+
+// TestInvalidParams makes sure that the operaads schema rejects all the imp.ext fields we don't support.
+func TestInvalidParams(t *testing.T) {
+	validator, err := openrtb_ext.NewBidderParamsValidator("../../static/bidder-params")
+	if err != nil {
+		t.Fatalf("Failed to fetch the json-schemas. %v", err)
+	}
+
+	for _, invalidParam := range invalidParams {
+		if err := validator.Validate(openrtb_ext.BidderOpenx, json.RawMessage(invalidParam)); err == nil {
+			t.Errorf("Schema allowed unexpected params: %s", invalidParam)
+		}
+	}
+}
+
+var validParams = []string{
+	`{"placementId": "s123", "endpointId": "ep12345", "publisherId": "pub12345"}`,
+}
+
+var invalidParams = []string{
+	`{"placementId": "s123"}`,
+	`{"endpointId": "ep12345"}`,
+	`{"publisherId": "pub12345"}`,
+	`{"placementId": "s123", "endpointId": "ep12345"}`,
+	`{"placementId": "s123", "publisherId": "pub12345"}`,
+	`{"endpointId": "ep12345", "publisherId": "pub12345"}`,
+	`{"placementId": "", "endpointId": "", "publisherId": ""}`,
+}
diff --git a/adapters/operaads/usersync_test.go b/adapters/operaads/usersync_test.go
new file mode 100644
index 00000000..e9b402ac
--- /dev/null
+++ b/adapters/operaads/usersync_test.go
@@ -0,0 +1,33 @@
+package operaads
+
+import (
+	"github.com/prebid/prebid-server/privacy/ccpa"
+	"github.com/prebid/prebid-server/privacy/gdpr"
+	"testing"
+	"text/template"
+
+	"github.com/prebid/prebid-server/privacy"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestOperaadsSyncer(t *testing.T) {
+	syncURL := "https://t-odx.op-mobile.opera.com/pbs/sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&r=localhost%2Fsetuid%3Fbidder%3Doperaads%26gdpr%3D{{.GDPR}}%26gdpr_consent%3D{{.GDPRConsent}}%26uid%3D%24%7BUID%7D"
+	syncURLTemplate := template.Must(
+		template.New("sync-template").Parse(syncURL),
+	)
+
+	syncer := NewOperaadsSyncer(syncURLTemplate)
+	syncInfo, err := syncer.GetUsersyncInfo(privacy.Policies{
+		GDPR: gdpr.Policy{
+			Signal:  "A",
+			Consent: "B",
+		},
+		CCPA: ccpa.Policy{
+			Consent: "C",
+		}})
+
+	assert.NoError(t, err)
+	assert.Equal(t, "https://t-odx.op-mobile.opera.com/pbs/sync?gdpr=A&gdpr_consent=B&r=localhost%2Fsetuid%3Fbidder%3Doperaads%26gdpr%3DA%26gdpr_consent%3DB%26uid%3D%24%7BUID%7D", syncInfo.URL)
+	assert.Equal(t, "redirect", syncInfo.Type)
+	assert.Equal(t, false, syncInfo.SupportCORS)
+}
diff --git a/adapters/rubicon/rubicon_test.go b/adapters/rubicon/rubicon_test.go
index 76904a42..28ddebbf 100644
--- a/adapters/rubicon/rubicon_test.go
+++ b/adapters/rubicon/rubicon_test.go
@@ -4,6 +4,8 @@ import (
 	"bytes"
 	"context"
 	"encoding/json"
+	"errors"
+	"github.com/stretchr/testify/mock"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
@@ -572,52 +574,125 @@ func TestResolveVideoSizeId(t *testing.T) {
 	}
 }
 
-func TestResolveBidFloorAttributes(t *testing.T) {
+func TestOpenRTBRequestWithDifferentBidFloorAttributes(t *testing.T) {
 	testScenarios := []struct {
-		bidFloor            float64
-		bidFloorCur         string
-		expectedBidFloor    float64
-		expectedBidFloorCur string
+		bidFloor         float64
+		bidFloorCur      string
+		setMock          func(m *mock.Mock)
+		expectedBidFloor float64
+		expectedBidCur   string
+		expectedErrors   []error
 	}{
 		{
-			bidFloor:            1,
-			bidFloorCur:         "EUR",
-			expectedBidFloor:    1.2,
-			expectedBidFloorCur: "USD",
+			bidFloor:         1,
+			bidFloorCur:      "WRONG",
+			setMock:          func(m *mock.Mock) { m.On("GetRate", "WRONG", "USD").Return(2.5, errors.New("some error")) },
+			expectedBidFloor: 0,
+			expectedBidCur:   "",
+			expectedErrors: []error{
+				&errortypes.BadInput{Message: "Unable to convert provided bid floor currency from WRONG to USD"},
+			},
 		},
 		{
-			bidFloor:            1,
-			bidFloorCur:         "Eur",
-			expectedBidFloor:    1.2,
-			expectedBidFloorCur: "USD",
+			bidFloor:         1,
+			bidFloorCur:      "USD",
+			setMock:          func(m *mock.Mock) {},
+			expectedBidFloor: 1,
+			expectedBidCur:   "USD",
+			expectedErrors:   nil,
 		},
 		{
-			bidFloor:            0,
-			bidFloorCur:         "EUR",
-			expectedBidFloor:    0,
-			expectedBidFloorCur: "EUR",
+			bidFloor:         1,
+			bidFloorCur:      "EUR",
+			setMock:          func(m *mock.Mock) { m.On("GetRate", "EUR", "USD").Return(1.2, nil) },
+			expectedBidFloor: 1.2,
+			expectedBidCur:   "USD",
+			expectedErrors:   nil,
 		},
 		{
-			bidFloor:            -1,
-			bidFloorCur:         "EUR",
-			expectedBidFloor:    -1,
-			expectedBidFloorCur: "EUR",
+			bidFloor:         0,
+			bidFloorCur:      "",
+			setMock:          func(m *mock.Mock) {},
+			expectedBidFloor: 0,
+			expectedBidCur:   "",
+			expectedErrors:   nil,
 		},
 		{
-			bidFloor:            1,
-			bidFloorCur:         "USD",
-			expectedBidFloor:    1,
-			expectedBidFloorCur: "USD",
+			bidFloor:         -1,
+			bidFloorCur:      "CZK",
+			setMock:          func(m *mock.Mock) {},
+			expectedBidFloor: -1,
+			expectedBidCur:   "CZK",
+			expectedErrors:   nil,
 		},
 	}
 
 	for _, scenario := range testScenarios {
-		bidFloor, bidFloorCur := resolveBidFloorAttributes(scenario.bidFloor, scenario.bidFloorCur)
-		assert.Equal(t, scenario.expectedBidFloor, bidFloor)
-		assert.Equal(t, scenario.expectedBidFloorCur, bidFloorCur)
+		mockConversions := &mockCurrencyConversion{}
+		scenario.setMock(&mockConversions.Mock)
+
+		extraRequestInfo := adapters.ExtraRequestInfo{
+			CurrencyConversions: mockConversions,
+		}
+
+		SIZE_ID := getTestSizes()
+		bidder := new(RubiconAdapter)
+
+		request := &openrtb2.BidRequest{
+			ID: "test-request-id",
+			Imp: []openrtb2.Imp{{
+				ID:          "test-imp-id",
+				BidFloorCur: scenario.bidFloorCur,
+				BidFloor:    scenario.bidFloor,
+				Banner: &openrtb2.Banner{
+					Format: []openrtb2.Format{
+						SIZE_ID[15],
+						SIZE_ID[10],
+					},
+				},
+				Ext: json.RawMessage(`{"bidder": {
+										"zoneId": 8394,
+										"siteId": 283282,
+										"accountId": 7891
+                                      }}`),
+			}},
+			App: &openrtb2.App{
+				ID:   "com.test",
+				Name: "testApp",
+			},
+		}
+
+		reqs, errs := bidder.MakeRequests(request, &extraRequestInfo)
+
+		mockConversions.AssertExpectations(t)
+
+		if scenario.expectedErrors == nil {
+			rubiconReq := &openrtb2.BidRequest{}
+			if err := json.Unmarshal(reqs[0].Body, rubiconReq); err != nil {
+				t.Fatalf("Unexpected error while decoding request: %s", err)
+			}
+			assert.Equal(t, scenario.expectedBidFloor, rubiconReq.Imp[0].BidFloor)
+			assert.Equal(t, scenario.expectedBidCur, rubiconReq.Imp[0].BidFloorCur)
+		} else {
+			assert.Equal(t, scenario.expectedErrors, errs)
+		}
 	}
 }
 
+type mockCurrencyConversion struct {
+	mock.Mock
+}
+
+func (m mockCurrencyConversion) GetRate(from string, to string) (float64, error) {
+	args := m.Called(from, to)
+	return args.Get(0).(float64), args.Error(1)
+}
+
+func (m mockCurrencyConversion) GetRates() *map[string]map[string]float64 {
+	args := m.Called()
+	return args.Get(0).(*map[string]map[string]float64)
+}
+
 func TestNoContentResponse(t *testing.T) {
 	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusNoContent)
diff --git a/adapters/rubicon/rubicontest/exemplary/simple-video.json b/adapters/rubicon/rubicontest/exemplary/simple-video.json
index 11afdd50..1daffe9b 100644
--- a/adapters/rubicon/rubicontest/exemplary/simple-video.json
+++ b/adapters/rubicon/rubicontest/exemplary/simple-video.json
@@ -120,8 +120,6 @@
           "w": 1024,
           "h": 576
         },
-        "bidfloor": 1,
-        "bidfloorcur": "EuR",
         "ext": {
           "bidder": {
             "video": {
@@ -298,8 +296,6 @@
                 "w": 1024,
                 "h": 576
               },
-              "bidfloor": 1.2,
-              "bidfloorcur": "USD",
               "ext": {
                 "rp": {
                   "track": {
diff --git a/adapters/rubicon/rubicontest/supplemental/no-site-content-data.json b/adapters/rubicon/rubicontest/supplemental/no-site-content-data.json
index f67788a3..0be214da 100644
--- a/adapters/rubicon/rubicontest/supplemental/no-site-content-data.json
+++ b/adapters/rubicon/rubicontest/supplemental/no-site-content-data.json
@@ -85,8 +85,6 @@
           "w": 1024,
           "h": 576
         },
-        "bidfloor": 1,
-        "bidfloorcur": "EuR",
         "ext": {
           "bidder": {
             "video": {
@@ -221,8 +219,6 @@
                 "w": 1024,
                 "h": 576
               },
-              "bidfloor": 1.2,
-              "bidfloorcur": "USD",
               "ext": {
                 "rp": {
                   "track": {
diff --git a/adapters/rubicon/rubicontest/supplemental/no-site-content.json b/adapters/rubicon/rubicontest/supplemental/no-site-content.json
index d3b8f8b7..2e830a2d 100644
--- a/adapters/rubicon/rubicontest/supplemental/no-site-content.json
+++ b/adapters/rubicon/rubicontest/supplemental/no-site-content.json
@@ -83,8 +83,6 @@
           "w": 1024,
           "h": 576
         },
-        "bidfloor": 1,
-        "bidfloorcur": "EuR",
         "ext": {
           "bidder": {
             "video": {
@@ -217,8 +215,6 @@
                 "w": 1024,
                 "h": 576
               },
-              "bidfloor": 1.2,
-              "bidfloorcur": "USD",
               "ext": {
                 "rp": {
                   "track": {
diff --git a/adapters/sharethrough/butler_test.go b/adapters/sharethrough/butler_test.go
index fbef417e..3d19eea9 100644
--- a/adapters/sharethrough/butler_test.go
+++ b/adapters/sharethrough/butler_test.go
@@ -83,7 +83,7 @@ func TestSuccessRequestFromOpenRTB(t *testing.T) {
 		"Generates the correct AdServer request from Imp (no user provided)": {
 			inputImp: openrtb2.Imp{
 				ID:  "abc",
-				Ext: []byte(`{ "bidder": {"pkey": "pkey", "iframe": true, "iframeSize": [10, 20], "bidfloor": 1.0} }`),
+				Ext: []byte(`{ "bidder": {"pkey": "pkey", "iframe": true, "iframeSize": [10, 20], "bidfloor": 1.0, "data": { "pbadslot": "adslot" } } }`),
 				Banner: &openrtb2.Banner{
 					Format: []openrtb2.Format{{H: 30, W: 40}},
 				},
@@ -435,6 +435,7 @@ func TestBuildUri(t *testing.T) {
 			inputParams: StrAdSeverParams{
 				Pkey:               "pkey",
 				BidID:              "bid",
+				GPID:               "gpid",
 				ConsentRequired:    true,
 				ConsentString:      "consent",
 				USPrivacySignal:    "ccpa",
@@ -449,6 +450,7 @@ func TestBuildUri(t *testing.T) {
 				"http://abc.com?",
 				"placement_key=pkey",
 				"bidId=bid",
+				"gpid=gpid",
 				"consent_required=true",
 				"consent_string=consent",
 				"us_privacy=ccpa",
diff --git a/config/config_test.go b/config/config_test.go
index c33a345c..a87d65af 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -138,8 +138,81 @@ func TestDefaults(t *testing.T) {
 	cmpStrings(t, "stored_requests.filesystem.directorypath", "./stored_requests/data/by_id", cfg.StoredRequests.Files.Path)
 	cmpBools(t, "auto_gen_source_tid", cfg.AutoGenSourceTID, true)
 	cmpBools(t, "generate_bid_id", cfg.GenerateBidID, false)
-	cmpBools(t, "gdpr.tcf2.purpose_one_treatment.enabled", true, cfg.GDPR.TCF2.PurposeOneTreatment.Enabled)
-	cmpBools(t, "gdpr.tcf2.purpose_one_treatment.access_allowed", true, cfg.GDPR.TCF2.PurposeOneTreatment.AccessAllowed)
+
+	//Assert purpose VendorExceptionMap hash tables were built correctly
+	expectedTCF2 := TCF2{
+		Enabled: true,
+		Purpose1: TCF2Purpose{
+			Enabled:            true,
+			EnforceVendors:     true,
+			VendorExceptions:   []openrtb_ext.BidderName{},
+			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+		},
+		Purpose2: TCF2Purpose{
+			Enabled:            true,
+			EnforceVendors:     true,
+			VendorExceptions:   []openrtb_ext.BidderName{},
+			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+		},
+		Purpose3: TCF2Purpose{
+			Enabled:            true,
+			EnforceVendors:     true,
+			VendorExceptions:   []openrtb_ext.BidderName{},
+			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+		},
+		Purpose4: TCF2Purpose{
+			Enabled:            true,
+			EnforceVendors:     true,
+			VendorExceptions:   []openrtb_ext.BidderName{},
+			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+		},
+		Purpose5: TCF2Purpose{
+			Enabled:            true,
+			EnforceVendors:     true,
+			VendorExceptions:   []openrtb_ext.BidderName{},
+			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+		},
+		Purpose6: TCF2Purpose{
+			Enabled:            true,
+			EnforceVendors:     true,
+			VendorExceptions:   []openrtb_ext.BidderName{},
+			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+		},
+		Purpose7: TCF2Purpose{
+			Enabled:            true,
+			EnforceVendors:     true,
+			VendorExceptions:   []openrtb_ext.BidderName{},
+			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+		},
+		Purpose8: TCF2Purpose{
+			Enabled:            true,
+			EnforceVendors:     true,
+			VendorExceptions:   []openrtb_ext.BidderName{},
+			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+		},
+		Purpose9: TCF2Purpose{
+			Enabled:            true,
+			EnforceVendors:     true,
+			VendorExceptions:   []openrtb_ext.BidderName{},
+			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+		},
+		Purpose10: TCF2Purpose{
+			Enabled:            true,
+			EnforceVendors:     true,
+			VendorExceptions:   []openrtb_ext.BidderName{},
+			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+		},
+		SpecialPurpose1: TCF2Purpose{
+			Enabled:            true,
+			VendorExceptions:   []openrtb_ext.BidderName{},
+			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{},
+		},
+		PurposeOneTreatment: TCF2PurposeOneTreatment{
+			Enabled:       true,
+			AccessAllowed: true,
+		},
+	}
+	assert.Equal(t, expectedTCF2, cfg.GDPR.TCF2, "gdpr.tcf2")
 }
 
 var fullConfig = []byte(`
@@ -149,25 +222,35 @@ gdpr:
   non_standard_publishers: ["siteID","fake-site-id","appID","agltb3B1Yi1pbmNyDAsSA0FwcBiJkfIUDA"]
   tcf2:
     purpose1:
+      enforce_vendors: false
       vendor_exceptions: ["foo1a", "foo1b"]
     purpose2:
       enabled: false
+      enforce_vendors: false
       vendor_exceptions: ["foo2"]
     purpose3:
+      enforce_vendors: false
       vendor_exceptions: ["foo3"]
     purpose4:
+      enforce_vendors: false
       vendor_exceptions: ["foo4"]
     purpose5:
+      enforce_vendors: false
       vendor_exceptions: ["foo5"]
     purpose6:
+      enforce_vendors: false
       vendor_exceptions: ["foo6"]
     purpose7:
+      enforce_vendors: false
       vendor_exceptions: ["foo7"]
     purpose8:
+      enforce_vendors: false
       vendor_exceptions: ["foo8"]
     purpose9:
+      enforce_vendors: false
       vendor_exceptions: ["foo9"]
     purpose10:
+      enforce_vendors: false
       vendor_exceptions: ["foo10"]
     special_purpose1:
       vendor_exceptions: ["fooSP1"]
@@ -407,51 +490,61 @@ func TestFullConfig(t *testing.T) {
 		Enabled: true,
 		Purpose1: TCF2Purpose{
 			Enabled:            true, // true by default
+			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo1a"), openrtb_ext.BidderName("foo1b")},
 			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo1a"): {}, openrtb_ext.BidderName("foo1b"): {}},
 		},
 		Purpose2: TCF2Purpose{
 			Enabled:            false,
+			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo2")},
 			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo2"): {}},
 		},
 		Purpose3: TCF2Purpose{
 			Enabled:            true, // true by default
+			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo3")},
 			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo3"): {}},
 		},
 		Purpose4: TCF2Purpose{
 			Enabled:            true, // true by default
+			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo4")},
 			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo4"): {}},
 		},
 		Purpose5: TCF2Purpose{
 			Enabled:            true, // true by default
+			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo5")},
 			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo5"): {}},
 		},
 		Purpose6: TCF2Purpose{
 			Enabled:            true, // true by default
+			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo6")},
 			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo6"): {}},
 		},
 		Purpose7: TCF2Purpose{
 			Enabled:            true, // true by default
+			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo7")},
 			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo7"): {}},
 		},
 		Purpose8: TCF2Purpose{
 			Enabled:            true, // true by default
+			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo8")},
 			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo8"): {}},
 		},
 		Purpose9: TCF2Purpose{
 			Enabled:            true, // true by default
+			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo9")},
 			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo9"): {}},
 		},
 		Purpose10: TCF2Purpose{
 			Enabled:            true, // true by default
+			EnforceVendors:     false,
 			VendorExceptions:   []openrtb_ext.BidderName{openrtb_ext.BidderName("foo10")},
 			VendorExceptionMap: map[openrtb_ext.BidderName]struct{}{openrtb_ext.BidderName("foo10"): {}},
 		},
diff --git a/endpoints/openrtb2/amp_auction_test.go b/endpoints/openrtb2/amp_auction_test.go
index 61164bd7..2ce1180d 100644
--- a/endpoints/openrtb2/amp_auction_test.go
+++ b/endpoints/openrtb2/amp_auction_test.go
@@ -66,6 +66,7 @@ func TestGoodAmpRequests(t *testing.T) {
 
 		var response AmpResponse
 		if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
+			t.Errorf("AMP response was: %s", recorder.Body.Bytes())
 			t.Fatalf("Error unmarshalling response: %s", err.Error())
 		}
 
diff --git a/endpoints/openrtb2/auction_test.go b/endpoints/openrtb2/auction_test.go
index bcdac13d..4835dd92 100644
--- a/endpoints/openrtb2/auction_test.go
+++ b/endpoints/openrtb2/auction_test.go
@@ -1587,7 +1587,7 @@ func TestCurrencyTrunc(t *testing.T) {
 		Cur: []string{"USD", "EUR"},
 	}
 
-	errL := deps.validateRequest(&req)
+	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req})
 
 	expectedError := errortypes.Warning{Message: "A prebid request can only process one currency. Taking the first currency in the list, USD, as the active currency"}
 	assert.ElementsMatch(t, errL, []error{&expectedError})
@@ -1633,14 +1633,12 @@ func TestCCPAInvalid(t *testing.T) {
 		},
 	}
 
-	errL := deps.validateRequest(&req)
+	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req})
 
 	expectedWarning := errortypes.Warning{
 		Message:     "CCPA consent is invalid and will be ignored. (request.regs.ext.us_privacy must contain 4 characters)",
 		WarningCode: errortypes.InvalidPrivacyConsentWarningCode}
 	assert.ElementsMatch(t, errL, []error{&expectedWarning})
-
-	assert.Empty(t, req.Regs.Ext, "Invalid Consent Removed From Request")
 }
 
 func TestNoSaleInvalid(t *testing.T) {
@@ -1684,7 +1682,7 @@ func TestNoSaleInvalid(t *testing.T) {
 		Ext: json.RawMessage(`{"prebid": {"nosale": ["*", "appnexus"]} }`),
 	}
 
-	errL := deps.validateRequest(&req)
+	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req})
 
 	expectedError := errors.New("request.ext.prebid.nosale is invalid: can only specify all bidders if no other bidders are provided")
 	assert.ElementsMatch(t, errL, []error{expectedError})
@@ -1731,7 +1729,7 @@ func TestValidateSourceTID(t *testing.T) {
 		},
 	}
 
-	deps.validateRequest(&req)
+	deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req})
 	assert.NotEmpty(t, req.Source.TID, "Expected req.Source.TID to be filled with a randomly generated UID")
 }
 
@@ -1773,7 +1771,7 @@ func TestSChainInvalid(t *testing.T) {
 		Ext: json.RawMessage(`{"prebid":{"schains":[{"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller1.com","sid":"00001","rid":"BidRequest1","hp":1}],"ver":"1.0"}}, {"bidders":["appnexus"],"schain":{"complete":1,"nodes":[{"asi":"directseller2.com","sid":"00002","rid":"BidRequest2","hp":1}],"ver":"1.0"}}]}}`),
 	}
 
-	errL := deps.validateRequest(&req)
+	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req})
 
 	expectedError := errors.New("request.ext.prebid.schains contains multiple schains for bidder appnexus; it must contain no more than one per bidder.")
 	assert.ElementsMatch(t, errL, []error{expectedError})
@@ -1992,7 +1990,7 @@ func TestEidPermissionsInvalid(t *testing.T) {
 		Ext: json.RawMessage(`{"prebid": {"data": {"eidpermissions": [{"source":"a", "bidders":[]}]} } }`),
 	}
 
-	errL := deps.validateRequest(&req)
+	errL := deps.validateRequest(&openrtb_ext.RequestWrapper{BidRequest: &req})
 
 	expectedError := errors.New(`request.ext.prebid.data.eidpermissions[0] missing or empty required field: "bidders"`)
 	assert.ElementsMatch(t, errL, []error{expectedError})
@@ -2007,11 +2005,6 @@ func TestValidateEidPermissions(t *testing.T) {
 		request       *openrtb_ext.ExtRequest
 		expectedError error
 	}{
-		{
-			description:   "Valid - Nil ext",
-			request:       nil,
-			expectedError: nil,
-		},
 		{
 			description:   "Valid - Empty ext",
 			request:       &openrtb_ext.ExtRequest{},
@@ -2096,7 +2089,7 @@ func TestValidateEidPermissions(t *testing.T) {
 
 	endpoint := &endpointDeps{bidderMap: knownBidders}
 	for _, test := range testCases {
-		result := endpoint.validateEidPermissions(test.request, knownAliases)
+		result := endpoint.validateEidPermissions(test.request.Prebid.Data, knownAliases)
 		assert.Equal(t, test.expectedError, result, test.description)
 	}
 }
diff --git a/endpoints/openrtb2/interstitial_test.go b/endpoints/openrtb2/interstitial_test.go
index 1d7ad9e3..fe0ed966 100644
--- a/endpoints/openrtb2/interstitial_test.go
+++ b/endpoints/openrtb2/interstitial_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -34,7 +35,7 @@ var request = &openrtb2.BidRequest{
 
 func TestInterstitial(t *testing.T) {
 	myRequest := request
-	if err := processInterstitials(myRequest); err != nil {
+	if err := processInterstitials(&openrtb_ext.RequestWrapper{BidRequest: myRequest}); err != nil {
 		t.Fatalf("Error processing interstitials: %v", err)
 	}
 	targetFormat := []openrtb2.Format{
diff --git a/endpoints/openrtb2/video_auction_test.go b/endpoints/openrtb2/video_auction_test.go
index 9f0859a3..5452d6c2 100644
--- a/endpoints/openrtb2/video_auction_test.go
+++ b/endpoints/openrtb2/video_auction_test.go
@@ -1087,11 +1087,13 @@ func TestCCPA(t *testing.T) {
 		description         string
 		testFilePath        string
 		expectConsentString bool
+		expectEmptyConsent  bool
 	}{
 		{
 			description:         "Missing Consent",
 			testFilePath:        "sample-requests/video/video_valid_sample.json",
 			expectConsentString: false,
+			expectEmptyConsent:  true,
 		},
 		{
 			description:         "Valid Consent",
@@ -1132,7 +1134,7 @@ func TestCCPA(t *testing.T) {
 		}
 		if test.expectConsentString {
 			assert.Len(t, extRegs.USPrivacy, 4, test.description+":consent")
-		} else {
+		} else if test.expectEmptyConsent {
 			assert.Empty(t, extRegs.USPrivacy, test.description+":consent")
 		}
 
diff --git a/exchange/utils_test.go b/exchange/utils_test.go
index 5a9fa187..1d13928b 100644
--- a/exchange/utils_test.go
+++ b/exchange/utils_test.go
@@ -1908,7 +1908,7 @@ func TestBidderToPrebidChains(t *testing.T) {
 		},
 	}
 
-	output, err := BidderToPrebidSChains(&input)
+	output, err := BidderToPrebidSChains(input.Prebid.SChains)
 
 	assert.Nil(t, err)
 	assert.Equal(t, len(output), 4)
@@ -1934,7 +1934,7 @@ func TestBidderToPrebidChainsDiscardMultipleChainsForBidder(t *testing.T) {
 		},
 	}
 
-	output, err := BidderToPrebidSChains(&input)
+	output, err := BidderToPrebidSChains(input.Prebid.SChains)
 
 	assert.NotNil(t, err)
 	assert.Nil(t, output)
@@ -1947,7 +1947,7 @@ func TestBidderToPrebidChainsNilSChains(t *testing.T) {
 		},
 	}
 
-	output, err := BidderToPrebidSChains(&input)
+	output, err := BidderToPrebidSChains(input.Prebid.SChains)
 
 	assert.Nil(t, err)
 	assert.Equal(t, len(output), 0)
@@ -1960,7 +1960,7 @@ func TestBidderToPrebidChainsZeroLengthSChains(t *testing.T) {
 		},
 	}
 
-	output, err := BidderToPrebidSChains(&input)
+	output, err := BidderToPrebidSChains(input.Prebid.SChains)
 
 	assert.Nil(t, err)
 	assert.Equal(t, len(output), 0)
diff --git a/gdpr/impl_test.go b/gdpr/impl_test.go
index 93d23c1a..f7d90f36 100644
--- a/gdpr/impl_test.go
+++ b/gdpr/impl_test.go
@@ -66,7 +66,8 @@ func TestAllowedSyncs(t *testing.T) {
 			HostVendorID: 2,
 			TCF2: config.TCF2{
 				Purpose1: config.TCF2Purpose{
-					Enabled: true,
+					Enabled:        true,
+					EnforceVendors: true,
 				},
 			},
 		},
@@ -79,6 +80,9 @@ func TestAllowedSyncs(t *testing.T) {
 			}),
 		},
 	}
+	perms.purposeConfigs = map[consentconstants.Purpose]config.TCF2Purpose{
+		consentconstants.Purpose(1): perms.cfg.TCF2.Purpose1,
+	}
 
 	allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, vendor2AndPurpose1Consent)
 	assertNilErr(t, err)
@@ -145,7 +149,8 @@ func TestProhibitedVendors(t *testing.T) {
 			HostVendorID: 2,
 			TCF2: config.TCF2{
 				Purpose1: config.TCF2Purpose{
-					Enabled: true,
+					Enabled:        true,
+					EnforceVendors: true,
 				},
 			},
 		},
@@ -158,6 +163,9 @@ func TestProhibitedVendors(t *testing.T) {
 			}),
 		},
 	}
+	perms.purposeConfigs = map[consentconstants.Purpose]config.TCF2Purpose{
+		consentconstants.Purpose(1): perms.cfg.TCF2.Purpose1,
+	}
 
 	allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, purpose1NoVendorConsent)
 	assertNilErr(t, err)
@@ -289,7 +297,8 @@ func TestAllowActivities(t *testing.T) {
 			TCF2: config.TCF2{
 				Enabled: true,
 				Purpose2: config.TCF2Purpose{
-					Enabled: true,
+					Enabled:        true,
+					EnforceVendors: true,
 				},
 			},
 		},
@@ -302,6 +311,9 @@ func TestAllowActivities(t *testing.T) {
 			}),
 		},
 	}
+	perms.purposeConfigs = map[consentconstants.Purpose]config.TCF2Purpose{
+		consentconstants.Purpose(2): perms.cfg.TCF2.Purpose2,
+	}
 
 	for _, tt := range tests {
 		perms.cfg.DefaultValue = tt.gdprDefaultValue
@@ -357,15 +369,39 @@ func buildVendorList34() vendorList {
 	}
 }
 
-var gdprConfig = config.GDPR{
-	HostVendorID: 2,
-	TCF2: config.TCF2{
-		Enabled:         true,
-		Purpose1:        config.TCF2Purpose{Enabled: true},
-		Purpose2:        config.TCF2Purpose{Enabled: true},
-		Purpose7:        config.TCF2Purpose{Enabled: true},
-		SpecialPurpose1: config.TCF2Purpose{Enabled: true},
-	},
+func allPurposesEnabledPermissions() (perms permissionsImpl) {
+	perms = permissionsImpl{
+		cfg: config.GDPR{
+			HostVendorID: 2,
+			TCF2: config.TCF2{
+				Enabled:         true,
+				Purpose1:        config.TCF2Purpose{Enabled: true, EnforceVendors: true},
+				Purpose2:        config.TCF2Purpose{Enabled: true, EnforceVendors: true},
+				Purpose3:        config.TCF2Purpose{Enabled: true, EnforceVendors: true},
+				Purpose4:        config.TCF2Purpose{Enabled: true, EnforceVendors: true},
+				Purpose5:        config.TCF2Purpose{Enabled: true, EnforceVendors: true},
+				Purpose6:        config.TCF2Purpose{Enabled: true, EnforceVendors: true},
+				Purpose7:        config.TCF2Purpose{Enabled: true, EnforceVendors: true},
+				Purpose8:        config.TCF2Purpose{Enabled: true, EnforceVendors: true},
+				Purpose9:        config.TCF2Purpose{Enabled: true, EnforceVendors: true},
+				Purpose10:       config.TCF2Purpose{Enabled: true, EnforceVendors: true},
+				SpecialPurpose1: config.TCF2Purpose{Enabled: true, EnforceVendors: true},
+			},
+		},
+	}
+	perms.purposeConfigs = map[consentconstants.Purpose]config.TCF2Purpose{
+		consentconstants.Purpose(1):  perms.cfg.TCF2.Purpose1,
+		consentconstants.Purpose(2):  perms.cfg.TCF2.Purpose2,
+		consentconstants.Purpose(3):  perms.cfg.TCF2.Purpose3,
+		consentconstants.Purpose(4):  perms.cfg.TCF2.Purpose4,
+		consentconstants.Purpose(5):  perms.cfg.TCF2.Purpose5,
+		consentconstants.Purpose(6):  perms.cfg.TCF2.Purpose6,
+		consentconstants.Purpose(7):  perms.cfg.TCF2.Purpose7,
+		consentconstants.Purpose(8):  perms.cfg.TCF2.Purpose8,
+		consentconstants.Purpose(9):  perms.cfg.TCF2.Purpose9,
+		consentconstants.Purpose(10): perms.cfg.TCF2.Purpose10,
+	}
+	return
 }
 
 type testDef struct {
@@ -380,20 +416,20 @@ type testDef struct {
 
 func TestAllowActivitiesGeoAndID(t *testing.T) {
 	vendorListData := MarshalVendorList(buildVendorList34())
-	perms := permissionsImpl{
-		cfg: gdprConfig,
-		vendorIDs: map[openrtb_ext.BidderName]uint16{
-			openrtb_ext.BidderAppnexus: 2,
-			openrtb_ext.BidderPubmatic: 6,
-			openrtb_ext.BidderRubicon:  8,
-			openrtb_ext.BidderOpenx:    20,
-		},
-		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
-				34: parseVendorListDataV2(t, vendorListData),
-				74: parseVendorListDataV2(t, vendorListData),
-			}),
-		},
+
+	perms := allPurposesEnabledPermissions()
+	perms.vendorIDs = map[openrtb_ext.BidderName]uint16{
+		openrtb_ext.BidderAppnexus:        2,
+		openrtb_ext.BidderPubmatic:        6,
+		openrtb_ext.BidderRubicon:         8,
+		openrtb_ext.BidderOpenx:           20,
+		openrtb_ext.BidderAudienceNetwork: 55,
+	}
+	perms.fetchVendorList = map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
+		tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
+			34: parseVendorListDataV2(t, vendorListData),
+			74: parseVendorListDataV2(t, vendorListData),
+		}),
 	}
 
 	// COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : full consents to purposes and vendors 2, 6, 8
@@ -464,19 +500,19 @@ func TestAllowActivitiesGeoAndID(t *testing.T) {
 
 func TestAllowActivitiesWhitelist(t *testing.T) {
 	vendorListData := MarshalVendorList(buildVendorList34())
-	perms := permissionsImpl{
-		cfg: gdprConfig,
-		vendorIDs: map[openrtb_ext.BidderName]uint16{
-			openrtb_ext.BidderAppnexus: 2,
-			openrtb_ext.BidderPubmatic: 6,
-			openrtb_ext.BidderRubicon:  8,
-		},
-		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
-				34: parseVendorListDataV2(t, vendorListData),
-			}),
-		},
+
+	perms := allPurposesEnabledPermissions()
+	perms.vendorIDs = map[openrtb_ext.BidderName]uint16{
+		openrtb_ext.BidderAppnexus: 2,
+		openrtb_ext.BidderPubmatic: 6,
+		openrtb_ext.BidderRubicon:  8,
 	}
+	perms.fetchVendorList = map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
+		tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
+			34: parseVendorListDataV2(t, vendorListData),
+		}),
+	}
+
 	// Assert that an item that otherwise would not be allowed PI access, gets approved because it is found in the GDPR.NonStandardPublishers array
 	perms.cfg.NonStandardPublisherMap = map[string]struct{}{"appNexusAppID": {}}
 	_, passGeo, passID, err := perms.AuctionActivitiesAllowed(context.Background(), openrtb_ext.BidderAppnexus, "appNexusAppID", SignalYes, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA", false)
@@ -487,18 +523,17 @@ func TestAllowActivitiesWhitelist(t *testing.T) {
 
 func TestAllowActivitiesPubRestrict(t *testing.T) {
 	vendorListData := MarshalVendorList(buildVendorList34())
-	perms := permissionsImpl{
-		cfg: gdprConfig,
-		vendorIDs: map[openrtb_ext.BidderName]uint16{
-			openrtb_ext.BidderAppnexus: 2,
-			openrtb_ext.BidderPubmatic: 32,
-			openrtb_ext.BidderRubicon:  8,
-		},
-		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
-				15: parseVendorListDataV2(t, vendorListData),
-			}),
-		},
+
+	perms := allPurposesEnabledPermissions()
+	perms.vendorIDs = map[openrtb_ext.BidderName]uint16{
+		openrtb_ext.BidderAppnexus: 2,
+		openrtb_ext.BidderPubmatic: 32,
+		openrtb_ext.BidderRubicon:  8,
+	}
+	perms.fetchVendorList = map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
+		tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
+			15: parseVendorListDataV2(t, vendorListData),
+		}),
 	}
 
 	// COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA - vendors 1-10 legit interest only,
@@ -537,18 +572,17 @@ func TestAllowActivitiesPubRestrict(t *testing.T) {
 
 func TestAllowSync(t *testing.T) {
 	vendorListData := MarshalVendorList(buildVendorList34())
-	perms := permissionsImpl{
-		cfg: gdprConfig,
-		vendorIDs: map[openrtb_ext.BidderName]uint16{
-			openrtb_ext.BidderAppnexus: 2,
-			openrtb_ext.BidderPubmatic: 6,
-			openrtb_ext.BidderRubicon:  8,
-		},
-		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
-				34: parseVendorListDataV2(t, vendorListData),
-			}),
-		},
+
+	perms := allPurposesEnabledPermissions()
+	perms.vendorIDs = map[openrtb_ext.BidderName]uint16{
+		openrtb_ext.BidderAppnexus: 2,
+		openrtb_ext.BidderPubmatic: 6,
+		openrtb_ext.BidderRubicon:  8,
+	}
+	perms.fetchVendorList = map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
+		tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
+			34: parseVendorListDataV2(t, vendorListData),
+		}),
 	}
 
 	// COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : full consensts to purposes and vendors 2, 6, 8
@@ -565,20 +599,19 @@ func TestProhibitedPurposeSync(t *testing.T) {
 	vendorList34 := buildVendorList34()
 	vendorList34.Vendors["8"].Purposes = []int{7}
 	vendorListData := MarshalVendorList(vendorList34)
-	perms := permissionsImpl{
-		cfg: gdprConfig,
-		vendorIDs: map[openrtb_ext.BidderName]uint16{
-			openrtb_ext.BidderAppnexus: 2,
-			openrtb_ext.BidderPubmatic: 6,
-			openrtb_ext.BidderRubicon:  8,
-		},
-		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
-				34: parseVendorListDataV2(t, vendorListData),
-			}),
-		},
-	}
+
+	perms := allPurposesEnabledPermissions()
 	perms.cfg.HostVendorID = 8
+	perms.vendorIDs = map[openrtb_ext.BidderName]uint16{
+		openrtb_ext.BidderAppnexus: 2,
+		openrtb_ext.BidderPubmatic: 6,
+		openrtb_ext.BidderRubicon:  8,
+	}
+	perms.fetchVendorList = map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
+		tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
+			34: parseVendorListDataV2(t, vendorListData),
+		}),
+	}
 
 	// COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : full consents to purposes for vendors 2, 6, 8
 	allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA")
@@ -592,21 +625,20 @@ func TestProhibitedPurposeSync(t *testing.T) {
 
 func TestProhibitedVendorSync(t *testing.T) {
 	vendorListData := MarshalVendorList(buildVendorList34())
-	perms := permissionsImpl{
-		cfg: gdprConfig,
-		vendorIDs: map[openrtb_ext.BidderName]uint16{
-			openrtb_ext.BidderAppnexus: 2,
-			openrtb_ext.BidderPubmatic: 6,
-			openrtb_ext.BidderRubicon:  8,
-			openrtb_ext.BidderOpenx:    10,
-		},
-		fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
-				34: parseVendorListDataV2(t, vendorListData),
-			}),
-		},
-	}
+
+	perms := allPurposesEnabledPermissions()
 	perms.cfg.HostVendorID = 10
+	perms.vendorIDs = map[openrtb_ext.BidderName]uint16{
+		openrtb_ext.BidderAppnexus: 2,
+		openrtb_ext.BidderPubmatic: 6,
+		openrtb_ext.BidderRubicon:  8,
+		openrtb_ext.BidderOpenx:    10,
+	}
+	perms.fetchVendorList = map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
+		tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
+			34: parseVendorListDataV2(t, vendorListData),
+		}),
+	}
 
 	// COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA : full consents to purposes for vendors 2, 6, 8
 	allowSync, err := perms.HostCookiesAllowed(context.Background(), SignalYes, "COzTVhaOzTVhaGvAAAENAiCIAP_AAH_AAAAAAEEUACCKAAA")
@@ -751,67 +783,110 @@ func TestAllowActivitiesBidRequests(t *testing.T) {
 	purpose2AndVendorConsent := "CPF_61ePF_61eFxAAAENAiCAAEAAAAAAAAAAADAQAAAAAA"
 	purpose2ConsentWithoutVendorConsent := "CPF_61ePF_61eFxAAAENAiCAAEAAAAAAAAAAABIAAAAA"
 
+	purpose2AndVendorLI := "CPF_61ePF_61eFxAAAENAiCAAAAAAEAAAAAAAAAAIAIAAA"
+	purpose2LIWithoutVendorLI := "CPF_61ePF_61eFxAAAENAiCAAAAAAEAAAAAAABIAAAAA"
+
 	testDefs := []struct {
-		description           string
-		purpose2Enabled       bool
-		bidder                openrtb_ext.BidderName
-		consent               string
-		allowBid              bool
-		passGeo               bool
-		passID                bool
-		weakVendorEnforcement bool
+		description            string
+		purpose2Enabled        bool
+		purpose2EnforceVendors bool
+		bidder                 openrtb_ext.BidderName
+		consent                string
+		allowBid               bool
+		passGeo                bool
+		passID                 bool
+		weakVendorEnforcement  bool
 	}{
 		{
-			description:     "Bid blocked - p2 enabled, user consents to p2 but not vendor, vendor consents to p2",
-			purpose2Enabled: true,
-			bidder:          openrtb_ext.BidderPubmatic,
-			consent:         purpose2ConsentWithoutVendorConsent,
-			allowBid:        false,
-			passGeo:         false,
-			passID:          false,
+			description:            "Bid blocked - p2 enabled, user consents to p2 but not vendor, vendor consents to p2",
+			purpose2Enabled:        true,
+			purpose2EnforceVendors: true,
+			bidder:                 openrtb_ext.BidderPubmatic,
+			consent:                purpose2ConsentWithoutVendorConsent,
+			allowBid:               false,
+			passGeo:                false,
+			passID:                 false,
+		},
+		{
+			description:            "Bid allowed - p2 enabled not enforcing vendors, user consents to p2 but not vendor, vendor consents to p2",
+			purpose2Enabled:        true,
+			purpose2EnforceVendors: false,
+			bidder:                 openrtb_ext.BidderPubmatic,
+			consent:                purpose2ConsentWithoutVendorConsent,
+			allowBid:               true,
+			passGeo:                false,
+			passID:                 true,
+		},
+		{
+			description:            "Bid allowed - p2 disabled, user consents to p2 but not vendor, vendor consents to p2",
+			purpose2Enabled:        false,
+			purpose2EnforceVendors: true,
+			bidder:                 openrtb_ext.BidderPubmatic,
+			consent:                purpose2ConsentWithoutVendorConsent,
+			allowBid:               true,
+			passGeo:                false,
+			passID:                 false,
+		},
+		{
+			description:            "Bid allowed - p2 enabled, user consents to p2 and vendor, vendor consents to p2",
+			purpose2Enabled:        true,
+			purpose2EnforceVendors: true,
+			bidder:                 openrtb_ext.BidderPubmatic,
+			consent:                purpose2AndVendorConsent,
+			allowBid:               true,
+			passGeo:                false,
+			passID:                 true,
+		},
+		{
+			description:            "Bid blocked - p2 enabled, user consents to p2 LI but not vendor, vendor consents to p2",
+			purpose2Enabled:        true,
+			purpose2EnforceVendors: true,
+			bidder:                 openrtb_ext.BidderRubicon,
+			consent:                purpose2LIWithoutVendorLI,
+			allowBid:               false,
+			passGeo:                false,
+			passID:                 false,
 		},
 		{
-			description:     "Bid allowed - p2 disabled, user consents to p2 but not vendor, vendor consents to p2",
-			purpose2Enabled: false,
-			bidder:          openrtb_ext.BidderPubmatic,
-			consent:         purpose2ConsentWithoutVendorConsent,
-			allowBid:        true,
-			passGeo:         false,
-			passID:          false,
+			description:            "Bid allowed - p2 enabled, user consents to p2 LI and vendor, vendor consents to p2",
+			purpose2Enabled:        true,
+			purpose2EnforceVendors: true,
+			bidder:                 openrtb_ext.BidderRubicon,
+			consent:                purpose2AndVendorLI,
+			allowBid:               true,
+			passGeo:                false,
+			passID:                 true,
 		},
 		{
-			description:     "Bid allowed - p2 enabled, user consents to p2 and vendor, vendor consents to p2",
-			purpose2Enabled: true,
-			bidder:          openrtb_ext.BidderPubmatic,
-			consent:         purpose2AndVendorConsent,
-			allowBid:        true,
-			passGeo:         false,
-			passID:          true,
+			description:            "Bid allowed - p2 enabled not enforcing vendors, user consents to p2 LI but not vendor, vendor consents to p2",
+			purpose2Enabled:        true,
+			purpose2EnforceVendors: false,
+			bidder:                 openrtb_ext.BidderPubmatic,
+			consent:                purpose2AndVendorLI,
+			allowBid:               true,
+			passGeo:                false,
+			passID:                 true,
 		},
 	}
 
 	for _, td := range testDefs {
 		vendorListData := MarshalVendorList(buildVendorList34())
-		perms := permissionsImpl{
-			cfg: config.GDPR{
-				HostVendorID: 2,
-				TCF2: config.TCF2{
-					Enabled:         true,
-					Purpose1:        config.TCF2Purpose{Enabled: true},
-					Purpose2:        config.TCF2Purpose{Enabled: td.purpose2Enabled},
-					Purpose7:        config.TCF2Purpose{Enabled: true},
-					SpecialPurpose1: config.TCF2Purpose{Enabled: true},
-				},
-			},
-			vendorIDs: map[openrtb_ext.BidderName]uint16{
-				openrtb_ext.BidderPubmatic: 6,
-			},
-			fetchVendorList: map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
-				tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
-					34: parseVendorListDataV2(t, vendorListData),
-				}),
-			},
+
+		perms := allPurposesEnabledPermissions()
+		perms.vendorIDs = map[openrtb_ext.BidderName]uint16{
+			openrtb_ext.BidderPubmatic: 6,
+			openrtb_ext.BidderRubicon:  8,
+		}
+		perms.fetchVendorList = map[uint8]func(ctx context.Context, id uint16) (vendorlist.VendorList, error){
+			tcf2SpecVersion: listFetcher(map[uint16]vendorlist.VendorList{
+				34: parseVendorListDataV2(t, vendorListData),
+			}),
 		}
+		perms.cfg.TCF2.Purpose2.Enabled = td.purpose2Enabled
+		p2Config := perms.purposeConfigs[consentconstants.Purpose(2)]
+		p2Config.Enabled = td.purpose2Enabled
+		p2Config.EnforceVendors = td.purpose2EnforceVendors
+		perms.purposeConfigs[consentconstants.Purpose(2)] = p2Config
 
 		allowBid, passGeo, passID, err := perms.AuctionActivitiesAllowed(context.Background(), td.bidder, "", SignalYes, td.consent, td.weakVendorEnforcement)
 		assert.NoErrorf(t, err, "Error processing AuctionActivitiesAllowed for %s", td.description)
diff --git a/openrtb_ext/request_wrapper_test.go b/openrtb_ext/request_wrapper_test.go
new file mode 100644
index 00000000..06cad49a
--- /dev/null
+++ b/openrtb_ext/request_wrapper_test.go
@@ -0,0 +1,22 @@
+package openrtb_ext
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// Some minimal tests to get code coverage above 30%. The real tests are when other modules use these structures.
+
+func TestUserExt(t *testing.T) {
+	userExt := &UserExt{}
+
+	userExt.unmarshal(nil)
+	assert.Equal(t, false, userExt.Dirty(), "New UserExt should not be dirty.")
+	assert.Nil(t, userExt.GetConsent(), "Empty UserExt should have nil consent")
+
+	newConsent := "NewConsent"
+	userExt.SetConsent(&newConsent)
+	assert.Equal(t, "NewConsent", *userExt.GetConsent())
+
+}
diff --git a/privacy/ccpa/consentwriter_test.go b/privacy/ccpa/consentwriter_test.go
index d5942862..28dfd417 100644
--- a/privacy/ccpa/consentwriter_test.go
+++ b/privacy/ccpa/consentwriter_test.go
@@ -5,10 +5,60 @@ import (
 	"testing"
 
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/stretchr/testify/assert"
 )
 
+// RegExt.SetUSPrivacy() is the new ConsentWriter
 func TestConsentWriter(t *testing.T) {
+	consent := "anyConsent"
+	testCases := []struct {
+		description   string
+		request       *openrtb2.BidRequest
+		expected      *openrtb2.BidRequest
+		expectedError bool
+	}{
+		{
+			description: "Nil Request",
+			request:     nil,
+			expected:    nil,
+		},
+		{
+			description: "Success",
+			request:     &openrtb2.BidRequest{},
+			expected: &openrtb2.BidRequest{
+				Regs: &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)},
+			},
+		},
+		{
+			description: "Error With Regs.Ext - Does Not Mutate",
+			request: &openrtb2.BidRequest{
+				Regs: &openrtb2.Regs{Ext: json.RawMessage(`malformed}`)},
+			},
+			expectedError: false,
+			expected: &openrtb2.BidRequest{
+				Regs: &openrtb2.Regs{Ext: json.RawMessage(`malformed}`)},
+			},
+		},
+	}
+
+	for _, test := range testCases {
+
+		reqWrapper := &openrtb_ext.RequestWrapper{BidRequest: test.request}
+		var err error
+		regsExt, err1 := reqWrapper.GetRegExt()
+		if err1 == nil {
+			regsExt.SetUSPrivacy(consent)
+			if reqWrapper.BidRequest != nil {
+				err = reqWrapper.RebuildRequest()
+			}
+		}
+		assertError(t, test.expectedError, err, test.description)
+		assert.Equal(t, test.expected, reqWrapper.BidRequest, test.description)
+	}
+}
+
+func TestConsentWriterLegacy(t *testing.T) {
 	consent := "anyConsent"
 	testCases := []struct {
 		description   string
diff --git a/privacy/ccpa/policy_test.go b/privacy/ccpa/policy_test.go
index 416ebffa..ca6d0f8a 100644
--- a/privacy/ccpa/policy_test.go
+++ b/privacy/ccpa/policy_test.go
@@ -5,6 +5,7 @@ import (
 	"testing"
 
 	"github.com/mxmCherry/openrtb/v15/openrtb2"
+	"github.com/prebid/prebid-server/openrtb_ext"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -155,7 +156,8 @@ func TestReadFromRequest(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		result, err := ReadFromRequest(test.request)
+		reqWrapper := &openrtb_ext.RequestWrapper{BidRequest: test.request}
+		result, err := ReadFromRequestWrapper(reqWrapper)
 		assertError(t, test.expectedError, err, test.description)
 		assert.Equal(t, test.expectedPolicy, result, test.description)
 	}
@@ -209,9 +211,20 @@ func TestWrite(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		err := test.policy.Write(test.request)
+		reqWrapper := &openrtb_ext.RequestWrapper{BidRequest: test.request}
+		var err error
+		_, err = reqWrapper.GetRegExt()
+		if err == nil {
+			_, err = reqWrapper.GetRequestExt()
+			if err == nil {
+				err = test.policy.Write(reqWrapper)
+				if err == nil && reqWrapper.BidRequest != nil {
+					err = reqWrapper.RebuildRequest()
+				}
+			}
+		}
 		assertError(t, test.expectedError, err, test.description)
-		assert.Equal(t, test.expected, test.request, test.description)
+		assert.Equal(t, test.expected, reqWrapper.BidRequest, test.description)
 	}
 }
 
@@ -237,6 +250,9 @@ func TestBuildRegs(t *testing.T) {
 			regs: &openrtb2.Regs{
 				Ext: json.RawMessage(`malformed`),
 			},
+			expected: &openrtb2.Regs{
+				Ext: json.RawMessage(`malformed`),
+			},
 			expectedError: true,
 		},
 		{
@@ -253,14 +269,22 @@ func TestBuildRegs(t *testing.T) {
 			regs: &openrtb2.Regs{
 				Ext: json.RawMessage(`malformed`),
 			},
+			expected: &openrtb2.Regs{
+				Ext: json.RawMessage(`malformed`),
+			},
 			expectedError: true,
 		},
 	}
 
 	for _, test := range testCases {
-		result, err := buildRegs(test.consent, test.regs)
+		request := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Regs: test.regs}}
+		regsExt, err := request.GetRegExt()
+		if err == nil {
+			regsExt.SetUSPrivacy(test.consent)
+			request.RebuildRequest()
+		}
 		assertError(t, test.expectedError, err, test.description)
-		assert.Equal(t, test.expected, result, test.description)
+		assert.Equal(t, test.expected, request.Regs, test.description)
 	}
 }
 
@@ -274,7 +298,7 @@ func TestBuildRegsClear(t *testing.T) {
 		{
 			description: "Nil Regs",
 			regs:        nil,
-			expected:    nil,
+			expected:    &openrtb2.Regs{Ext: nil},
 		},
 		{
 			description: "Nil Regs.Ext",
@@ -297,21 +321,28 @@ func TestBuildRegsClear(t *testing.T) {
 			expected:    &openrtb2.Regs{Ext: json.RawMessage(`{"other":"any"}`)},
 		},
 		{
-			description: "Invalid Regs.Ext Type - Still Cleared",
-			regs:        &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)},
-			expected:    &openrtb2.Regs{},
+			description:   "Invalid Regs.Ext Type - Returns Error, doesn't clear",
+			regs:          &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)},
+			expected:      &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)},
+			expectedError: true,
 		},
 		{
 			description:   "Malformed Regs.Ext",
 			regs:          &openrtb2.Regs{Ext: json.RawMessage(`malformed`)},
+			expected:      &openrtb2.Regs{Ext: json.RawMessage(`malformed`)},
 			expectedError: true,
 		},
 	}
 
 	for _, test := range testCases {
-		result, err := buildRegsClear(test.regs)
+		request := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Regs: test.regs}}
+		regsExt, err := request.GetRegExt()
+		if err == nil {
+			regsExt.SetUSPrivacy("")
+			request.RebuildRequest()
+		}
 		assertError(t, test.expectedError, err, test.description)
-		assert.Equal(t, test.expected, result, test.description)
+		assert.Equal(t, test.expected, request.Regs, test.description)
 	}
 }
 
@@ -354,23 +385,30 @@ func TestBuildRegsWrite(t *testing.T) {
 			expected:    &openrtb2.Regs{Ext: json.RawMessage(`{"other":"any","us_privacy":"anyConsent"}`)},
 		},
 		{
-			description: "Invalid Regs.Ext Type - Still Overwrites",
-			consent:     "anyConsent",
-			regs:        &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)},
-			expected:    &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":"anyConsent"}`)},
+			description:   "Invalid Regs.Ext Type - Doesn't Overwrite",
+			consent:       "anyConsent",
+			regs:          &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)},
+			expected:      &openrtb2.Regs{Ext: json.RawMessage(`{"us_privacy":123}`)},
+			expectedError: true,
 		},
 		{
 			description:   "Malformed Regs.Ext",
 			consent:       "anyConsent",
 			regs:          &openrtb2.Regs{Ext: json.RawMessage(`malformed`)},
+			expected:      &openrtb2.Regs{Ext: json.RawMessage(`malformed`)},
 			expectedError: true,
 		},
 	}
 
 	for _, test := range testCases {
-		result, err := buildRegsWrite(test.consent, test.regs)
+		request := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Regs: test.regs}}
+		regsExt, err := request.GetRegExt()
+		if err == nil {
+			regsExt.SetUSPrivacy(test.consent)
+			request.RebuildRequest()
+		}
 		assertError(t, test.expectedError, err, test.description)
-		assert.Equal(t, test.expected, result, test.description)
+		assert.Equal(t, test.expected, request.Regs, test.description)
 	}
 }
 
@@ -415,7 +453,14 @@ func TestBuildExt(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		result, err := buildExt(test.noSaleBidders, test.ext)
+		request := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: test.ext}}
+		reqExt, err := request.GetRequestExt()
+		var result json.RawMessage
+		if err == nil {
+			setPrebidNoSale(test.noSaleBidders, reqExt)
+			err = request.RebuildRequest()
+			result = request.Ext
+		}
 		assertError(t, test.expectedError, err, test.description)
 		assert.Equal(t, test.expected, result, test.description)
 	}
@@ -460,13 +505,13 @@ func TestBuildExtClear(t *testing.T) {
 		},
 		{
 			description: "Leaves Other Ext.Prebid Values",
-			ext:         json.RawMessage(`{"prebid":{"nosale":["a","b"],"other":"any"}}`),
-			expected:    json.RawMessage(`{"prebid":{"other":"any"}}`),
+			ext:         json.RawMessage(`{"prebid":{"nosale":["a","b"],"aliases":{"a":"b"}}}`),
+			expected:    json.RawMessage(`{"prebid":{"aliases":{"a":"b"}}}`),
 		},
 		{
 			description: "Leaves All Other Values",
-			ext:         json.RawMessage(`{"other":"ABC","prebid":{"nosale":["a","b"],"other":"123"}}`),
-			expected:    json.RawMessage(`{"other":"ABC","prebid":{"other":"123"}}`),
+			ext:         json.RawMessage(`{"other":"ABC","prebid":{"nosale":["a","b"],"supportdeals":true}}`),
+			expected:    json.RawMessage(`{"other":"ABC","prebid":{"supportdeals":true}}`),
 		},
 		{
 			description:   "Malformed Ext",
@@ -486,7 +531,14 @@ func TestBuildExtClear(t *testing.T) {
 	}
 
 	for _, test := range testCases {
-		result, err := buildExtClear(test.ext)
+		request := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: test.ext}}
+		reqExt, err := request.GetRequestExt()
+		var result json.RawMessage
+		if err == nil {
+			setPrebidNoSaleClear(reqExt)
+			err = request.RebuildRequest()
+			result = request.Ext
+		}
 		assertError(t, test.expectedError, err, test.description)
 		assert.Equal(t, test.expected, result, test.description)
 	}
@@ -539,43 +591,56 @@ func TestBuildExtWrite(t *testing.T) {
 		{
 			description:   "Leaves Other Ext.Prebid Values",
 			noSaleBidders: []string{"a", "b"},
-			ext:           json.RawMessage(`{"prebid":{"other":"any"}}`),
-			expected:      json.RawMessage(`{"prebid":{"nosale":["a","b"],"other":"any"}}`),
+			ext:           json.RawMessage(`{"prebid":{"supportdeals":true}}`),
+			expected:      json.RawMessage(`{"prebid":{"supportdeals":true,"nosale":["a","b"]}}`),
 		},
 		{
 			description:   "Leaves All Other Values",
 			noSaleBidders: []string{"a", "b"},
-			ext:           json.RawMessage(`{"other":"ABC","prebid":{"other":"123"}}`),
-			expected:      json.RawMessage(`{"other":"ABC","prebid":{"nosale":["a","b"],"other":"123"}}`),
+			ext:           json.RawMessage(`{"other":"ABC","prebid":{"aliases":{"a":"b"}}}`),
+			expected:      json.RawMessage(`{"other":"ABC","prebid":{"aliases":{"a":"b"},"nosale":["a","b"]}}`),
 		},
 		{
 			description:   "Invalid Ext.Prebid No Sale Type - Still Overrides",
 			noSaleBidders: []string{"a", "b"},
 			ext:           json.RawMessage(`{"prebid":{"nosale":123}}`),
-			expected:      json.RawMessage(`{"prebid":{"nosale":["a","b"]}}`),
+			expected:      json.RawMessage(`{"prebid":{"nosale":123}}`),
+			expectedError: true,
 		},
 		{
 			description:   "Invalid Ext.Prebid Type ",
 			noSaleBidders: []string{"a", "b"},
 			ext:           json.RawMessage(`{"prebid":"wrongtype"}`),
+			expected:      json.RawMessage(`{"prebid":"wrongtype"}`),
 			expectedError: true,
 		},
 		{
 			description:   "Malformed Ext",
 			noSaleBidders: []string{"a", "b"},
 			ext:           json.RawMessage(`{malformed`),
+			expected:      json.RawMessage(`{malformed`),
 			expectedError: true,
 		},
 		{
 			description:   "Malformed Ext.Prebid",
 			noSaleBidders: []string{"a", "b"},
 			ext:           json.RawMessage(`{"prebid":malformed}`),
+			expected:      json.RawMessage(`{"prebid":malformed}`),
 			expectedError: true,
 		},
 	}
 
 	for _, test := range testCases {
-		result, err := buildExtWrite(test.noSaleBidders, test.ext)
+		request := &openrtb_ext.RequestWrapper{BidRequest: &openrtb2.BidRequest{Ext: test.ext}}
+		reqExt, err := request.GetRequestExt()
+		var result json.RawMessage
+		if err == nil {
+			setPrebidNoSaleWrite(test.noSaleBidders, reqExt)
+			err = request.RebuildRequest()
+			result = request.Ext
+		} else {
+			result = test.ext
+		}
 		assertError(t, test.expectedError, err, test.description)
 		assert.Equal(t, test.expected, result, test.description)
 	}
diff --git a/stored_requests/config/config_test.go b/stored_requests/config/config_test.go
index 6c8cd612..4a8d10a9 100644
--- a/stored_requests/config/config_test.go
+++ b/stored_requests/config/config_test.go
@@ -2,6 +2,7 @@ package config
 
 import (
 	"context"
+	"database/sql"
 	"encoding/json"
 	"errors"
 	"net/http"
@@ -41,12 +42,79 @@ func isMemoryCacheType(cache stored_requests.CacheJSON) bool {
 }
 
 func TestNewEmptyFetcher(t *testing.T) {
-	fetcher := newFetcher(&config.StoredRequests{}, nil, nil)
-	if fetcher == nil {
-		t.Errorf("The fetcher should be non-nil, even with an empty config.")
+
+	type testCase struct {
+		config       *config.StoredRequests
+		emptyFetcher bool
+		description  string
 	}
-	if _, ok := fetcher.(empty_fetcher.EmptyFetcher); !ok {
-		t.Errorf("If the config is empty, and EmptyFetcher should be returned")
+	testCases := []testCase{
+		{
+			config:       &config.StoredRequests{},
+			emptyFetcher: true,
+			description:  "If the config is empty, an EmptyFetcher should be returned",
+		},
+		{
+			config: &config.StoredRequests{
+				Postgres: config.PostgresConfig{
+					CacheInitialization: config.PostgresCacheInitializer{
+						Query: "test query",
+					},
+					PollUpdates: config.PostgresUpdatePolling{
+						Query: "test poll query",
+					},
+					FetcherQueries: config.PostgresFetcherQueries{
+						QueryTemplate: "",
+					},
+				},
+			},
+			emptyFetcher: true,
+			description:  "If Postgres fetcher query is not defined, but Postgres Cache init query and Postgres update polling query are defined EmptyFetcher should be returned",
+		},
+		{
+			config: &config.StoredRequests{
+				Postgres: config.PostgresConfig{
+					CacheInitialization: config.PostgresCacheInitializer{
+						Query: "",
+					},
+					PollUpdates: config.PostgresUpdatePolling{
+						Query: "",
+					},
+					FetcherQueries: config.PostgresFetcherQueries{
+						QueryTemplate: "test fetcher query",
+					},
+				},
+			},
+			emptyFetcher: false,
+			description:  "If Postgres fetcher query is  defined, but Postgres Cache init query and Postgres update polling query are not defined not EmptyFetcher (DBFetcher) should be returned",
+		},
+		{
+			config: &config.StoredRequests{
+				Postgres: config.PostgresConfig{
+					CacheInitialization: config.PostgresCacheInitializer{
+						Query: "test cache query",
+					},
+					PollUpdates: config.PostgresUpdatePolling{
+						Query: "test poll query",
+					},
+					FetcherQueries: config.PostgresFetcherQueries{
+						QueryTemplate: "test fetcher query",
+					},
+				},
+			},
+			emptyFetcher: false,
+			description:  "If Postgres fetcher query is  defined and Postgres Cache init query and Postgres update polling query are  defined not EmptyFetcher (DBFetcher) should be returned",
+		},
+	}
+
+	for _, test := range testCases {
+		fetcher := newFetcher(test.config, nil, &sql.DB{})
+		assert.NotNil(t, fetcher, "The fetcher should be non-nil.")
+		if test.emptyFetcher {
+			assert.Equal(t, empty_fetcher.EmptyFetcher{}, fetcher, "Empty fetcher should be returned")
+		} else {
+			assert.NotEqual(t, empty_fetcher.EmptyFetcher{}, fetcher)
+		}
 	}
 }
 
diff --git a/usersync/usersyncers/syncer_test.go b/usersync/usersyncers/syncer_test.go
index 59ed67cc..8a2a3e5d 100644
--- a/usersync/usersyncers/syncer_test.go
+++ b/usersync/usersyncers/syncer_test.go
@@ -75,6 +75,7 @@ func TestNewSyncerMap(t *testing.T) {
 			string(openrtb_ext.BidderNoBid):            syncConfig,
 			string(openrtb_ext.BidderOneTag):           syncConfig,
 			string(openrtb_ext.BidderOpenx):            syncConfig,
+			string(openrtb_ext.BidderOperaads):         syncConfig,
 			string(openrtb_ext.BidderOutbrain):         syncConfig,
 			string(openrtb_ext.BidderPubmatic):         syncConfig,
 			string(openrtb_ext.BidderPulsepoint):       syncConfig,